diff --git a/data/Makefile.am b/data/Makefile.am
index 6ad4dca92..b2e98174f 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -41,6 +41,8 @@ dist_theme_DATA = \
theme/message-tray-background.png \
theme/more-results.svg \
theme/noise-texture.png \
+ theme/page-indicator-active.svg \
+ theme/page-indicator-inactive.svg \
theme/panel-button-border.svg \
theme/panel-button-highlight-narrow.svg \
theme/panel-button-highlight-wide.svg \
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 24f1c75ec..9ed2867af 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -941,6 +941,22 @@ StScrollBar StButton#vhandle:active {
padding: 0px 88px 10px 88px;
}
+.page-indicator {
+ width: 18px;
+ height: 18px;
+ background-image: url(page-indicator-inactive.svg);
+}
+
+.page-indicator:hover,
+.page-indicator:checked{
+ background-image: url(page-indicator-active.svg);
+}
+
+.page-indicators {
+ spacing: 30px;
+ padding: 0px 30px;
+}
+
.app-folder-icon {
padding: 5px;
}
diff --git a/data/theme/page-indicator-active.svg b/data/theme/page-indicator-active.svg
new file mode 100644
index 000000000..38b720f86
--- /dev/null
+++ b/data/theme/page-indicator-active.svg
@@ -0,0 +1,67 @@
+
+
+
+
diff --git a/data/theme/page-indicator-inactive.svg b/data/theme/page-indicator-inactive.svg
new file mode 100644
index 000000000..3048f5628
--- /dev/null
+++ b/data/theme/page-indicator-inactive.svg
@@ -0,0 +1,67 @@
+
+
+
+
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 0dd388502..c9d2ff85a 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -37,6 +37,12 @@ const INACTIVE_GRID_OPACITY = 77;
const INACTIVE_GRID_OPACITY_ANIMATION_TIME = 0.15;
const FOLDER_SUBICON_FRACTION = .4;
+const INDICATORS_ANIMATION_TIME = 0.5;
+// 100% means indicators wait for be animated until the previous one
+// is animated completely. 0% means all animators are animated
+// at once without delay
+const INDICATORS_ANIMATION_DELAY_PERCENTAGE = 50;
+
const PAGE_SWITCH_TIME = 0.3;
// Recursively load a GMenuTreeDirectory; we could put this in ShellAppSystem too
@@ -169,6 +175,90 @@ const PagesBin = new Lang.Class({
}
});
+const PageIndicators = new Lang.Class({
+ Name:'PageIndicators',
+
+ _init: function() {
+ this.actor = new St.BoxLayout({ style_class: 'page-indicators',
+ vertical: true,
+ x_expand: true, y_expand: true,
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.CENTER });
+ this._nPages = 0;
+ this._currentPage = undefined;
+
+ this.actor.connect('notify::mapped',
+ Lang.bind(this, this._animateIndicators));
+ },
+
+ setNPages: function(nPages) {
+ if (this._nPages == nPages)
+ return;
+
+ let diff = nPages - this._nPages;
+ if (diff > 0) {
+ for (let i = 0; i < diff; i++) {
+ let pageIndex = this._nPages + i;
+ let indicator = new St.Button({ style_class: 'page-indicator',
+ button_mask: St.ButtonMask.ONE |
+ St.ButtonMask.TWO |
+ St.ButtonMask.THREE,
+ toggle_mode: true,
+ checked: pageIndex == this._currentPage });
+ indicator.connect('clicked', Lang.bind(this,
+ function() {
+ this.emit('page-activated', pageIndex);
+ }));
+ this.actor.add_actor(indicator);
+ }
+ } else {
+ let children = this.actor.get_children().splice(diff);
+ for (let i = 0; i < children.length; i++)
+ children[i].destroy();
+ }
+ this._nPages = nPages;
+ },
+
+ setCurrentPage: function(currentPage) {
+ this._currentPage = currentPage;
+
+ let children = this.actor.get_children();
+ for (let i = 0; i < children.length; i++)
+ children[i].set_checked(i == this._currentPage);
+ },
+
+ _animateIndicators: function() {
+ if (!this.actor.mapped)
+ return;
+
+ let children = this.actor.get_children();
+ if (children.length == 0)
+ return;
+
+ let timePerChild = INDICATORS_ANIMATION_TIME / this._nPages;
+ let delay = INDICATORS_ANIMATION_DELAY_PERCENTAGE / 100 * timePerChild;
+
+ let [stageX, ] = children[0].get_transformed_position();
+ let offset;
+ let monitor = Main.layoutManager.primaryMonitor;
+ if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)
+ offset = monitor.x - stageX - children[0].width;
+ else
+ offset = monitor.x + monitor.width - stageX;
+
+ for (let i = 0; i < this._nPages; i++) {
+ children[i].translation_x = offset;
+ Tweener.addTween(children[i],
+ { translation_x: 0,
+ time: timePerChild,
+ delay: delay * i,
+ transition: 'easeOutQuad'
+ });
+ }
+ }
+});
+Signals.addSignalMethods(PageIndicators.prototype);
+
const AllView = new Lang.Class({
Name: 'AllView',
Extends: BaseAppView,
@@ -186,6 +276,13 @@ const AllView = new Lang.Class({
x_expand:true, y_expand:true });
this.actor.add_actor(this._pagesBin);
+ this._pageIndicators = new PageIndicators();
+ this._pageIndicators.connect('page-activated', Lang.bind(this,
+ function(indicators, pageIndex) {
+ this.goToPage(pageIndex);
+ }));
+ this.actor.add_actor(this._pageIndicators.actor);
+
this._stack = new St.Widget({ layout_manager: new Clutter.BinLayout() });
let box = new St.BoxLayout({ vertical: true });
this._verticalAdjustment = new St.Adjustment();
@@ -223,6 +320,7 @@ const AllView = new Lang.Class({
{ value: this._grid.getPageY(this._currentPage),
time: PAGE_SWITCH_TIME,
transition: 'easeOutQuad' });
+ this._pageIndicators.setCurrentPage(pageNumber);
},
_onScroll: function(actor, event) {
@@ -331,8 +429,14 @@ const AllView = new Lang.Class({
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())
+ if (this._availWidth != availWidth || this._availHeight != availHeight || oldNPages != this._grid.nPages()) {
this._verticalAdjustment.value = 0;
+ Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this,
+ function() {
+ this._pageIndicators.setNPages(this._grid.nPages());
+ this._pageIndicators.setCurrentPage(0);
+ }));
+ }
this._availWidth = availWidth;
this._availHeight = availHeight;