From 754ca7c8f2db3268e29a84ab985e263888f55d11 Mon Sep 17 00:00:00 2001 From: Carlos Soriano Date: Tue, 27 Aug 2013 21:22:16 +0200 Subject: [PATCH] appDisplay: Paginate AllView Organize applications in AllView by pages using the new PaginatedIconGrid added previously. Pagination is generally a better pattern for collections than scrolling, as it better suits spacial memory. Hook into AppDisplay's allocation function to communicate the available size to the different views before child allocations - this is only required by the paginated view (as pages must be computed before calling get_preferred_height/get_preferred_width), but doing it for all views will guarantee that their dynamic spacing calculation is based on the same values. https://bugzilla.gnome.org/show_bug.cgi?id=706081 --- data/theme/gnome-shell.css | 3 +- js/ui/appDisplay.js | 218 +++++++++++++++++++++++++------------ js/ui/iconGrid.js | 15 ++- 3 files changed, 162 insertions(+), 74 deletions(-) diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index afb1f48e8..8992fc709 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -934,9 +934,8 @@ StScrollBar StButton#vhandle:active { padding: 3px 31px; } - .search-display > StBoxLayout, -.all-apps > StBoxLayout, +.all-apps, .frequent-apps > StBoxLayout { /* horizontal padding to make sure scrollbars or dash don't overlap content */ padding: 0px 88px; diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index b68a330f7..30d033cea 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -35,6 +35,7 @@ const INACTIVE_GRID_OPACITY = 77; const INACTIVE_GRID_OPACITY_ANIMATION_TIME = 0.15; const FOLDER_SUBICON_FRACTION = .4; +const PAGE_SWITCH_TIME = 0.3; // Recursively load a GMenuTreeDirectory; we could put this in ShellAppSystem too function _loadCategory(dir, view) { @@ -59,9 +60,16 @@ const BaseAppView = new Lang.Class({ Name: 'BaseAppView', Abstract: true, - _init: function() { - this._grid = new IconGrid.IconGrid({ xAlign: St.Align.MIDDLE, - columnLimit: MAX_COLUMNS }); + _init: function(params, gridParams) { + gridParams = Params.parse(gridParams, { xAlign: St.Align.MIDDLE, + columnLimit: MAX_COLUMNS, + fillParent: false }); + params = Params.parse(params, { usePagination: false }); + + if(params.usePagination) + this._grid = new IconGrid.PaginatedIconGrid(gridParams); + else + this._grid = new IconGrid.IconGrid(gridParams); // Standard hack for ClutterBinLayout this._grid.actor.x_expand = true; @@ -142,27 +150,18 @@ const BaseAppView = new Lang.Class({ }); Signals.addSignalMethods(BaseAppView.prototype); -const AllViewLayout = new Lang.Class({ - Name: 'AllViewLayout', - Extends: Clutter.BinLayout, - vfunc_get_preferred_height: function(container, forWidth) { - let minBottom = 0; - let naturalBottom = 0; +// Ignore child size requests to use the available size from the parent +const PagesBin = new Lang.Class({ + Name: 'PagesBin', + Extends: St.Bin, - for (let child = container.get_first_child(); - child; - child = child.get_next_sibling()) { - let childY = child.y; - let [childMin, childNatural] = child.get_preferred_height(forWidth); + vfunc_get_preferred_height: function (forWidth) { + return [0, 0]; + }, - if (childMin + childY > minBottom) - minBottom = childMin + childY; - - if (childNatural + childY > naturalBottom) - naturalBottom = childNatural + childY; - } - return [minBottom, naturalBottom]; + vfunc_get_preferred_width: function(forHeight) { + return [0, 0]; } }); @@ -171,30 +170,32 @@ const AllView = new Lang.Class({ Extends: BaseAppView, _init: function() { - this.parent(); - - this._grid.actor.y_align = Clutter.ActorAlign.START; - this._grid.actor.y_expand = true; + this.parent({ usePagination: true }, null); + this._pagesBin = new PagesBin({ style_class: 'all-apps', + x_expand: true, + y_expand: true, + x_fill: true, + y_fill: false, + reactive: true, + y_align: St.Align.START }); + this.actor = new St.Widget({ layout_manager: new Clutter.BinLayout(), + x_expand:true, y_expand:true }); + this.actor.add_actor(this._pagesBin); + this._stack = new St.Widget({ layout_manager: new Clutter.BinLayout() }); let box = new St.BoxLayout({ vertical: true }); - this._stack = new St.Widget({ layout_manager: new AllViewLayout() }); + this._verticalAdjustment = new St.Adjustment(); + box.set_adjustments(new St.Adjustment() /* unused */, this._verticalAdjustment); + + this._currentPage = 0; this._stack.add_actor(this._grid.actor); this._eventBlocker = new St.Widget({ x_expand: true, y_expand: true }); this._stack.add_actor(this._eventBlocker); - box.add(this._stack, { y_align: St.Align.START, expand: true }); - this.actor = new St.ScrollView({ x_fill: true, - y_fill: false, - y_align: St.Align.START, - x_expand: true, - y_expand: true, - overlay_scrollbars: true, - style_class: 'all-apps vfade' }); - this.actor.add_actor(box); - this.actor.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); - let action = new Clutter.PanAction({ interpolate: true }); - action.connect('pan', Lang.bind(this, this._onPan)); - this.actor.add_action(action); + box.add_actor(this._stack); + this._pagesBin.add_actor(box); + + this._pagesBin.connect('scroll-event', Lang.bind(this, this._onScroll)); this._clickAction = new Clutter.ClickAction(); this._clickAction.connect('clicked', Lang.bind(this, function() { @@ -207,15 +208,30 @@ const AllView = new Lang.Class({ this._currentPopup.popdown(); })); this._eventBlocker.add_action(this._clickAction); + + this._availWidth = 0; + this._availHeight = 0; }, - _onPan: function(action) { - this._clickAction.release(); + goToPage: function(pageNumber) { + this._currentPage = pageNumber; + Tweener.addTween(this._verticalAdjustment, + { value: this._grid.getPageY(this._currentPage), + time: PAGE_SWITCH_TIME, + transition: 'easeOutQuad' }); + }, - let [dist, dx, dy] = action.get_motion_delta(0); - let adjustment = this.actor.vscroll.adjustment; - adjustment.value -= (dy / this.actor.height) * adjustment.page_size; - return false; + _onScroll: function(actor, event) { + let direction = event.get_scroll_direction(); + if (direction == Clutter.ScrollDirection.UP) { + if (this._currentPage > 0) + this.goToPage(this._currentPage - 1); + } else { + if (direction == Clutter.ScrollDirection.DOWN) { + if (this._currentPage < (this._grid.nPages() - 1)) + this.goToPage(this._currentPage + 1); + } + } }, _getItemId: function(item) { @@ -265,17 +281,17 @@ const AllView = new Lang.Class({ this._eventBlocker.reactive = isOpen; this._currentPopup = isOpen ? popup : null; this._updateIconOpacities(isOpen); - if (isOpen) { - this._ensureIconVisible(popup.actor); - this._grid.actor.y = popup.parentOffset; - } else { - this._grid.actor.y = 0; - } })); }, _ensureIconVisible: function(icon) { - Util.ensureActorVisibleInScrollView(this.actor, icon); + let itemPage = this._grid.getItemPage(icon); + this.goToPage(itemPage); + }, + + _updateAdjustment: function(availHeight) { + this._verticalAdjustment.page_size = availHeight; + this._verticalAdjustment.upper = this._stack.height; }, _updateIconOpacities: function(folderOpen) { @@ -290,16 +306,41 @@ const AllView = new Lang.Class({ transition: 'easeOutQuad' }; Tweener.addTween(this._items[id].actor, params); } + }, + + // Called before allocation to calculate dynamic spacing + adaptToSize: function(width, height) { + let box = new Clutter.ActorBox(); + box.x1 = 0; + box.x2 = width; + box.y1 = 0; + box.y2 = height; + box = this.actor.get_theme_node().get_content_box(box); + box = this._pagesBin.get_theme_node().get_content_box(box); + box = this._grid.actor.get_theme_node().get_content_box(box); + let availWidth = box.x2 - box.x1; + let availHeight = box.y2 - box.y1; + let oldNPages = this._grid.nPages(); + + this._updateAdjustment(availHeight); + this._grid.updateSpacingForSize(availWidth, availHeight); + this._grid.computePages(availWidth, availHeight); + // Make sure the view doesn't have a bad adjustment value after screen size changes + // and therefore the pages computation. + if (this._availWidth != availWidth || this._availHeight != availHeight || oldNPages != this._grid.nPages()) + this._verticalAdjustment.value = 0; + + this._availWidth = availWidth; + this._availHeight = availHeight; } }); const FrequentView = new Lang.Class({ Name: 'FrequentView', + Extends: BaseAppView, _init: function() { - this._grid = new IconGrid.IconGrid({ xAlign: St.Align.MIDDLE, - fillParent: true, - columnLimit: MAX_COLUMNS }); + this.parent(null, { fillParent: true }); this.actor = new St.Widget({ style_class: 'frequent-apps', x_expand: true, y_expand: true }); this.actor.add_actor(this._grid.actor); @@ -307,10 +348,6 @@ const FrequentView = new Lang.Class({ this._usage = Shell.AppUsage.get_default(); }, - removeAll: function() { - this._grid.removeAll(); - }, - loadApps: function() { let mostUsed = this._usage.get_most_used (""); for (let i = 0; i < mostUsed.length; i++) { @@ -319,6 +356,19 @@ const FrequentView = new Lang.Class({ let appIcon = new AppIcon(mostUsed[i]); this._grid.addItem(appIcon.actor, -1); } + }, + + // Called before allocation to calculate dynamic spacing + adaptToSize: function(width, height) { + let box = new Clutter.ActorBox(); + box.x1 = box.y1 = 0; + box.x2 = width; + box.y2 = height; + box = this.actor.get_theme_node().get_content_box(box); + box = this._grid.actor.get_theme_node().get_content_box(box); + let availWidth = box.x2 - box.x1; + let availHeight = box.y2 - box.y1; + this._grid.updateSpacingForSize(availWidth, availHeight); } }); @@ -352,6 +402,21 @@ const ControlsBoxLayout = Lang.Class({ } }); +const ViewStackLayout = new Lang.Class({ + Name: 'ViewStackLayout', + Extends: Clutter.BinLayout, + + vfunc_allocate: function (actor, box, flags) { + let availWidth = box.x2 - box.x1; + let availHeight = box.y2 - box.y1; + // Prepare children of all views for the upcoming allocation, calculate all + // the needed values to adapt available size + this.emit('allocated-size-changed', availWidth, availHeight); + this.parent(actor, box, flags); + } +}); +Signals.addSignalMethods(ViewStackLayout.prototype); + const AppDisplay = new Lang.Class({ Name: 'AppDisplay', @@ -387,20 +452,19 @@ const AppDisplay = new Lang.Class({ x_expand: true }); this._views[Views.ALL] = { 'view': view, 'control': button }; - this.actor = new St.BoxLayout({ style_class: 'app-display', - vertical: true, - x_expand: true, y_expand: true }); - - this._viewStack = new St.Widget({ layout_manager: new Clutter.BinLayout(), - x_expand: true, y_expand: true }); - this.actor.add(this._viewStack, { expand: true }); - + this.actor = new St.BoxLayout ({ style_class: 'app-display', + x_expand: true, y_expand: true, + vertical: true }); + this._viewStackLayout = new ViewStackLayout(); + this._viewStack = new St.Widget({ x_expand: true, y_expand: true, + layout_manager: this._viewStackLayout }); + this._viewStackLayout.connect('allocated-size-changed', Lang.bind(this, this._onAllocatedSizeChanged)); + this.actor.add_actor(this._viewStack, { expand: true }); let layout = new ControlsBoxLayout({ homogeneous: true }); this._controls = new St.Widget({ style_class: 'app-view-controls', layout_manager: layout }); layout.hookup_style(this._controls); - this.actor.add(new St.Bin({ child: this._controls })); - + this.actor.add_actor(new St.Bin({ child: this._controls })); for (let i = 0; i < this._views.length; i++) { this._viewStack.add_actor(this._views[i].view.actor); @@ -506,7 +570,19 @@ const AppDisplay = new Lang.Class({ this._showView(Views.ALL); this._views[Views.ALL].view.selectApp(id); }, -}); + + _onAllocatedSizeChanged: function(actor, width, height) { + let box = new Clutter.ActorBox(); + box.x1 = box.y1 =0; + box.x2 = width; + box.y2 = height; + box = this._viewStack.get_theme_node().get_content_box(box); + let availWidth = box.x2 - box.x1; + let availHeight = box.y2 - box.y1; + for (let i = 0; i < this._views.length; i++) + this._views[i].view.adaptToSize(availWidth, availHeight); + } +}) const AppSearchProvider = new Lang.Class({ Name: 'AppSearchProvider', @@ -569,7 +645,7 @@ const FolderView = new Lang.Class({ Extends: BaseAppView, _init: function() { - this.parent(); + this.parent(null, null); this.actor = this._grid.actor; }, diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index d408b1be7..59c6360d0 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -261,7 +261,6 @@ const IconGrid = new Lang.Class({ let children = this._getVisibleChildren(); let availWidth = box.x2 - box.x1; let availHeight = box.y2 - box.y1; - this.updateSpacingForSize(availWidth); let spacing = this._getSpacing(); let [nColumns, usedWidth] = this._computeLayout(availWidth); @@ -390,6 +389,10 @@ const IconGrid = new Lang.Class({ return this._fixedSpacing ? this._fixedSpacing : this._spacing; }, + /** + * This function must to be called before iconGrid allocation, + * to know how much spacing can the grid has + */ updateSpacingForSize: function(availWidth) { let spacing = this._spacing; @@ -504,5 +507,15 @@ const PaginatedIconGrid = new Lang.Class({ let firstPageItem = pageNumber * this._childrenPerPage let childBox = this._getVisibleChildren()[firstPageItem].get_allocation_box(); return childBox.y1; + }, + + getItemPage: function(item) { + let children = this._getVisibleChildren(); + let index = children.indexOf(item); + if (index == -1) { + throw new Error('Item not found.'); + return 0; + } + return Math.floor(index / this._childrenPerPage); } });