[gnome-shell] New workspaces view



commit 20abc4cb99485b586972ad3445f8323036cad266
Author: Maxim Ermilov <zaspire rambler ru>
Date:   Fri Jan 22 05:33:48 2010 +0300

    New workspaces view
    
    Matching the 20091114 mockup, the default workspace view
    is now a scrollable horizontal list, with a control to
    switch between this and the previous grid view.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=593844

 data/Makefile.am                      |   18 +-
 data/theme/add-workspace.svg          |   98 ++++
 data/theme/gnome-shell.css            |   79 +++
 data/theme/mosaic-view-active.svg     |  113 ++++
 data/theme/mosaic-view.svg            |  113 ++++
 data/theme/remove-workspace.svg       |   92 ++++
 data/theme/single-view-active.svg     |   81 +++
 data/theme/single-view.svg            |   81 +++
 data/theme/switch-scroll-hhandle.svg  |  123 +++++
 js/ui/Makefile.am                     |    3 +-
 js/ui/appDisplay.js                   |    6 +-
 js/ui/overview.js                     |  124 ++---
 js/ui/{workspaces.js => workspace.js} |  573 +++------------------
 js/ui/workspacesView.js               |  956 +++++++++++++++++++++++++++++++++
 14 files changed, 1870 insertions(+), 590 deletions(-)
---
diff --git a/data/Makefile.am b/data/Makefile.am
index f0ca34e..6fa151e 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -21,16 +21,24 @@ dist_images_DATA =				\
 
 themedir = $(pkgdatadir)/theme
 dist_theme_DATA =				\
-	theme/gnome-shell.css			\
-	theme/close.svg                 \
+	theme/add-workspace.svg			\
 	theme/close-window.svg			\
-	theme/scroll-button-down.png		\
+	theme/close.svg                 \
+	theme/gnome-shell.css			\
+	theme/mosaic-view-active.svg          \
+	theme/mosaic-view.svg          \
+	theme/remove-workspace.svg          \
 	theme/scroll-button-down-hover.png	\
-	theme/scroll-button-up.png		\
+	theme/scroll-button-down.png		\
 	theme/scroll-button-up-hover.png	\
+	theme/scroll-button-up.png		\
 	theme/scroll-vhandle.png        \
 	theme/section-back.svg          \
-	theme/section-more.svg
+	theme/section-more.svg          \
+	theme/single-view-active.svg          \
+	theme/single-view.svg          \
+	theme/switch-scroll-hhandle.svg
+
 
 schemadir  = @GCONF_SCHEMA_FILE_DIR@
 schema_DATA = gnome-shell.schemas
diff --git a/data/theme/add-workspace.svg b/data/theme/add-workspace.svg
new file mode 100644
index 0000000..6df7cbd
--- /dev/null
+++ b/data/theme/add-workspace.svg
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="23"
+   height="15"
+   id="svg6375"
+   version="1.1"
+   inkscape:version="0.47pre4 r22446"
+   sodipodi:docname="New document 13">
+  <defs
+     id="defs6377">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 16 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="32 : 16 : 1"
+       inkscape:persp3d-origin="16 : 10.666667 : 1"
+       id="perspective6383" />
+    <inkscape:perspective
+       id="perspective6366"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.197802"
+     inkscape:cx="16"
+     inkscape:cy="16"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="1680"
+     inkscape:window-height="997"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata6380">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     transform="translate(0,-17)">
+    <g
+       style="display:inline"
+       id="g6243"
+       transform="translate(-986.28859,-658.2796)">
+      <rect
+         style="fill:#000000;fill-opacity:0.98770495;stroke:#666666;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
+         id="rect5318"
+         width="22"
+         height="14"
+         x="986.89801"
+         y="675.86743"
+         rx="0.49999979"
+         ry="0.5" />
+      <g
+         id="g5320"
+         transform="translate(402.77304,-12.882544)">
+        <path
+           id="path5322"
+           d="m 595.125,692.53048 0,6.43903"
+           style="fill:none;stroke:#666666;stroke-width:1.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+        <path
+           id="path5324"
+           d="m 598.34451,695.75 -6.43902,0"
+           style="fill:none;stroke:#666666;stroke-width:1.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 67d917b..5828beb 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -137,6 +137,24 @@ StTooltip {
     color: white;
 }
 
+.workspaces-bar {
+    height: 60px;
+}
+
+.workspace-indicator {
+    width: 24px;
+    height: 15px;
+    background: rgba(155,155,155,0.8);
+    border-spacing: 15px;
+}
+
+.workspace-indicator-active {
+    width: 24px;
+    height: 15px;
+    background: rgba(255,255,255,0.8);
+    border-spacing: 15px;
+}
+
 .window-caption {
     background: rgba(0,0,0,0.8);
     border: 1px solid rgba(128,128,128,0.40);
@@ -154,6 +172,67 @@ StTooltip {
     -shell-close-overlap: 16px;
 }
 
+.single-view-add {
+    background-image: url("add-workspace.svg");
+    width: 24px;
+    height: 15px;
+}
+
+.single-view-remove {
+    background-image: url("remove-workspace.svg");
+    width: 24px;
+    height: 15px;
+}
+
+.switch-view-single {
+    background-image: url("single-view.svg");
+    width: 24px;
+    height: 15px;
+}
+
+.switch-view-mosaic {
+    background-image: url("mosaic-view.svg");
+    width: 24px;
+    height: 15px;
+}
+
+.switch-view-single:checked {
+    background-image: url("single-view-active.svg");
+    width: 24px;
+    height: 15px;
+}
+
+.switch-view-mosaic:checked {
+    background-image: url("mosaic-view-active.svg");
+    width: 24px;
+    height: 15px;
+}
+
+.scroll-separator {
+    width: 9px;
+    height: 15px;
+}
+
+#SwitchScroll {
+    height: 15px;
+}
+
+#SwitchScroll StBin{
+    border: 1px solid rgba(128,128,128,0.40);
+    border-radius: 5px;
+}
+
+#SwitchScroll StButton#hhandle {
+  border-image: url("switch-scroll-hhandle.svg") 5;
+}
+
+#SwitchScroll StButton#backward-stepper,
+#SwitchScroll StButton#forward-stepper
+{
+    width: 0px;
+    border: 0px;
+}
+
 /* Dash */
 
 #dash {
diff --git a/data/theme/mosaic-view-active.svg b/data/theme/mosaic-view-active.svg
new file mode 100644
index 0000000..296e776
--- /dev/null
+++ b/data/theme/mosaic-view-active.svg
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="24"
+   height="16"
+   id="svg6503"
+   version="1.1"
+   inkscape:version="0.47pre4 r22446"
+   sodipodi:docname="mosaic-view-active.svg">
+  <defs
+     id="defs6505">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 16 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="32 : 16 : 1"
+       inkscape:persp3d-origin="16 : 10.666667 : 1"
+       id="perspective6511" />
+    <inkscape:perspective
+       id="perspective6494"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.197802"
+     inkscape:cx="-15.97056"
+     inkscape:cy="16"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="1680"
+     inkscape:window-height="997"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata6508">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     transform="translate(0,-16)">
+    <g
+       style="display:inline;fill:#cbcbcb;fill-opacity:1"
+       transform="translate(-449.85476,-685.85869)"
+       id="g5306">
+      <rect
+         style="fill:#cbcbcb;fill-opacity:1;stroke:#000000;stroke-width:0.99999970000000005;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.44262299999999999;stroke-dasharray:none"
+         id="rect5308"
+         width="11"
+         height="7"
+         x="450.5"
+         y="710.5"
+         rx="0.99999958"
+         ry="1" />
+      <rect
+         style="fill:#cbcbcb;fill-opacity:1;stroke:#000000;stroke-width:0.99999970000000005;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.44262299999999999;stroke-dasharray:none;display:inline"
+         id="rect5310"
+         width="11"
+         height="7"
+         x="462.5"
+         y="702.5"
+         rx="0.99999958"
+         ry="1" />
+      <rect
+         style="fill:#cbcbcb;fill-opacity:1;stroke:#000000;stroke-width:0.99999976000000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.44262299999999999;stroke-dasharray:none;display:inline"
+         id="rect5312"
+         width="11"
+         height="7"
+         x="450.5"
+         y="702.5"
+         rx="0.99999958"
+         ry="1" />
+      <rect
+         style="fill:#cbcbcb;fill-opacity:1;stroke:#000000;stroke-width:0.99999970000000005;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.44262299999999999;stroke-dasharray:none;display:inline"
+         id="rect5314"
+         width="11"
+         height="7"
+         x="462.5"
+         y="710.5"
+         rx="0.99999958"
+         ry="1" />
+    </g>
+  </g>
+</svg>
diff --git a/data/theme/mosaic-view.svg b/data/theme/mosaic-view.svg
new file mode 100644
index 0000000..b6ec4c3
--- /dev/null
+++ b/data/theme/mosaic-view.svg
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="24"
+   height="16"
+   id="svg6503"
+   version="1.1"
+   inkscape:version="0.47pre4 r22446"
+   sodipodi:docname="New document 19">
+  <defs
+     id="defs6505">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 16 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="32 : 16 : 1"
+       inkscape:persp3d-origin="16 : 10.666667 : 1"
+       id="perspective6511" />
+    <inkscape:perspective
+       id="perspective6494"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.197802"
+     inkscape:cx="16"
+     inkscape:cy="16"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="1680"
+     inkscape:window-height="997"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata6508">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     transform="translate(0,-16)">
+    <g
+       style="display:inline"
+       transform="translate(-449.85476,-685.85869)"
+       id="g5306">
+      <rect
+         style="fill:#666666;fill-opacity:1;stroke:#000000;stroke-width:0.9999997;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.442623;stroke-dasharray:none"
+         id="rect5308"
+         width="11"
+         height="7"
+         x="450.5"
+         y="710.5"
+         rx="0.99999958"
+         ry="1" />
+      <rect
+         style="fill:#666666;fill-opacity:1;stroke:#000000;stroke-width:0.9999997;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.442623;stroke-dasharray:none;display:inline"
+         id="rect5310"
+         width="11"
+         height="7"
+         x="462.5"
+         y="702.5"
+         rx="0.99999958"
+         ry="1" />
+      <rect
+         style="fill:#666666;fill-opacity:1;stroke:#000000;stroke-width:0.99999976;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.442623;stroke-dasharray:none;display:inline"
+         id="rect5312"
+         width="11"
+         height="7"
+         x="450.5"
+         y="702.5"
+         rx="0.99999958"
+         ry="1" />
+      <rect
+         style="fill:#666666;fill-opacity:1;stroke:#000000;stroke-width:0.9999997;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.442623;stroke-dasharray:none;display:inline"
+         id="rect5314"
+         width="11"
+         height="7"
+         x="462.5"
+         y="710.5"
+         rx="0.99999958"
+         ry="1" />
+    </g>
+  </g>
+</svg>
diff --git a/data/theme/remove-workspace.svg b/data/theme/remove-workspace.svg
new file mode 100644
index 0000000..0063302
--- /dev/null
+++ b/data/theme/remove-workspace.svg
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="23"
+   height="15"
+   id="svg5501"
+   version="1.1"
+   inkscape:version="0.47pre4 r22446"
+   sodipodi:docname="add-workspace.svg">
+  <defs
+     id="defs5503">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 16 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="32 : 16 : 1"
+       inkscape:persp3d-origin="16 : 10.666667 : 1"
+       id="perspective5509" />
+    <inkscape:perspective
+       id="perspective5314"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.197802"
+     inkscape:cx="-0.074583208"
+     inkscape:cy="16"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="1680"
+     inkscape:window-height="997"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true" />
+  <metadata
+     id="metadata5506">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     transform="translate(0,-17)">
+    <g
+       style="display:inline"
+       id="g6239"
+       transform="translate(-953.97989,-657.32287)">
+      <rect
+         style="fill:#000000;fill-opacity:0.98770495;stroke:#666666;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
+         id="rect5318-6"
+         width="22"
+         height="14"
+         x="954.5"
+         y="675"
+         rx="0.49999979"
+         ry="0.5" />
+      <path
+         style="fill:none;stroke:#666666;stroke-width:1.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
+         d="m 968.71951,682 -6.43902,0"
+         id="path5324-5" />
+    </g>
+  </g>
+</svg>
diff --git a/data/theme/single-view-active.svg b/data/theme/single-view-active.svg
new file mode 100644
index 0000000..d7350ac
--- /dev/null
+++ b/data/theme/single-view-active.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="23"
+   height="15"
+   id="svg6446"
+   version="1.1"
+   inkscape:version="0.47pre4 r22446"
+   sodipodi:docname="single-view-active.svg">
+  <defs
+     id="defs6448">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 16 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="32 : 16 : 1"
+       inkscape:persp3d-origin="16 : 10.666667 : 1"
+       id="perspective6454" />
+    <inkscape:perspective
+       id="perspective6441"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.197802"
+     inkscape:cx="0.014720032"
+     inkscape:cy="16"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="1680"
+     inkscape:window-height="997"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata6451">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     transform="translate(0,-17)">
+    <rect
+       ry="0.5"
+       rx="0.49999979"
+       y="17.483809"
+       x="0.53483802"
+       height="14"
+       width="22"
+       id="rect5304"
+       style="fill:#cccccc;fill-opacity:1;stroke:#cccccc;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline" />
+  </g>
+</svg>
diff --git a/data/theme/single-view.svg b/data/theme/single-view.svg
new file mode 100644
index 0000000..c053e4f
--- /dev/null
+++ b/data/theme/single-view.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="23"
+   height="15"
+   id="svg6446"
+   version="1.1"
+   inkscape:version="0.47pre4 r22446"
+   sodipodi:docname="single-view.svg">
+  <defs
+     id="defs6448">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 16 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="32 : 16 : 1"
+       inkscape:persp3d-origin="16 : 10.666667 : 1"
+       id="perspective6454" />
+    <inkscape:perspective
+       id="perspective6441"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.197802"
+     inkscape:cx="0.014720032"
+     inkscape:cy="16"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="1680"
+     inkscape:window-height="997"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata6451">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     transform="translate(0,-17)">
+    <rect
+       ry="0.5"
+       rx="0.49999979"
+       y="17.483809"
+       x="0.53483802"
+       height="14"
+       width="22"
+       id="rect5304"
+       style="fill:#626262;fill-opacity:1;stroke:#cccccc;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline" />
+  </g>
+</svg>
diff --git a/data/theme/switch-scroll-hhandle.svg b/data/theme/switch-scroll-hhandle.svg
new file mode 100644
index 0000000..ae465d9
--- /dev/null
+++ b/data/theme/switch-scroll-hhandle.svg
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="300"
+   height="16"
+   id="svg5337"
+   version="1.1"
+   inkscape:version="0.47pre4 r22446"
+   sodipodi:docname="New document 2">
+  <defs
+     id="defs5339">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 16 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="32 : 16 : 1"
+       inkscape:persp3d-origin="16 : 10.666667 : 1"
+       id="perspective5345" />
+    <inkscape:perspective
+       id="perspective5323"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient356"
+       id="linearGradient5313"
+       gradientUnits="userSpaceOnUse"
+       x1="495.75"
+       y1="675.5"
+       x2="495.75"
+       y2="683" />
+    <linearGradient
+       id="linearGradient356">
+      <stop
+         style="stop-color:#999999;stop-opacity:0.60000002;"
+         offset="0"
+         id="stop357" />
+      <stop
+         style="stop-color:#fffff3;stop-opacity:0.0000000;"
+         offset="1.0000000"
+         id="stop358" />
+    </linearGradient>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.7994505"
+     inkscape:cx="111.6181"
+     inkscape:cy="33.67855"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="1680"
+     inkscape:window-height="997"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5342">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     transform="translate(0,-16)">
+    <g
+       style="display:inline"
+       id="g6342"
+       transform="translate(-334.52404,-658.85869)">
+      <rect
+         ry="8"
+         rx="8"
+         y="675.5"
+         x="335.5"
+         height="15"
+         width="299"
+         id="rect5457-5"
+         style="fill:url(#linearGradient5313);fill-opacity:1;stroke:#999999;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline" />
+      <g
+         transform="translate(0.5,0)"
+         id="g6337">
+        <path
+           style="fill:none;stroke:#666666;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.8"
+           d="m 480.5,680.3717 0,5.2566"
+           id="path6304" />
+        <path
+           style="fill:none;stroke:#666666;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.8;display:inline"
+           d="m 484.5,680.3717 0,5.2566"
+           id="path6304-0" />
+        <path
+           style="fill:none;stroke:#666666;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.8;display:inline"
+           d="m 488.5,680.3717 0,5.2566"
+           id="path6304-8" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am
index 4eb3883..9d54f74 100644
--- a/js/ui/Makefile.am
+++ b/js/ui/Makefile.am
@@ -30,4 +30,5 @@ dist_jsui_DATA =		\
 	widget.js		\
 	widgetBox.js		\
 	windowManager.js	\
-	workspaces.js
+	workspacesView.js	\
+	workspace.js
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 4749228..b9ba04f 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -19,7 +19,7 @@ const DND = imports.ui.dnd;
 const GenericDisplay = imports.ui.genericDisplay;
 const Main = imports.ui.main;
 const Search = imports.ui.search;
-const Workspaces = imports.ui.workspaces;
+const Workspace = imports.ui.workspace;
 
 const APPICON_SIZE = 48;
 const WELL_MAX_COLUMNS = 8;
@@ -697,7 +697,7 @@ AppIconMenu.prototype = {
     },
 
     _findMetaWindowForActor: function (actor) {
-        if (actor._delegate instanceof Workspaces.WindowClone)
+        if (actor._delegate instanceof Workspace.WindowClone)
             return actor._delegate.metaWindow;
         else if (actor.get_meta_window)
             return actor.get_meta_window();
@@ -1011,7 +1011,7 @@ AppWell.prototype = {
         let app = null;
         if (source instanceof AppDisplayItem) {
             app = this._appSystem.get_app(source.getId());
-        } else if (source instanceof Workspaces.WindowClone) {
+        } else if (source instanceof Workspace.WindowClone) {
             app = this._tracker.get_window_app(source.metaWindow);
         }
 
diff --git a/js/ui/overview.js b/js/ui/overview.js
index ce6fbfa..3ec61a1 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -8,6 +8,7 @@ const Mainloop = imports.mainloop;
 const Shell = imports.gi.Shell;
 const Signals = imports.signals;
 const Lang = imports.lang;
+const St = imports.gi.St;
 
 const AppDisplay = imports.ui.appDisplay;
 const DocDisplay = imports.ui.docDisplay;
@@ -16,7 +17,7 @@ const Main = imports.ui.main;
 const Panel = imports.ui.panel;
 const Dash = imports.ui.dash;
 const Tweener = imports.ui.tweener;
-const Workspaces = imports.ui.workspaces;
+const WorkspacesView = imports.ui.workspacesView;
 
 const ROOT_OVERVIEW_COLOR = new Clutter.Color();
 ROOT_OVERVIEW_COLOR.from_pixel(0x000000ff);
@@ -76,7 +77,6 @@ const NUMBER_OF_SECTIONS_IN_SEARCH = 2;
 let wideScreen = false;
 let displayGridColumnWidth = null;
 let displayGridRowHeight = null;
-let addRemoveButtonSize = null;
 
 function Overview() {
     this._init();
@@ -87,6 +87,9 @@ Overview.prototype = {
         this._group = new Clutter.Group();
         this._group._delegate = this;
 
+        this._workspacesViewSwitch = new WorkspacesView.WorkspacesViewSwitch();
+        this._workspacesViewSwitch.connect('view-changed', Lang.bind(this, this._onViewChanged));
+
         this.visible = false;
         this.animationInProgress = false;
         this._hideInProgress = false;
@@ -139,6 +142,45 @@ Overview.prototype = {
         this._workspaces = null;
     },
 
+    _createControlsBar: function() {
+        this._workspacesBar = new St.BoxLayout({ 'pack-start': true,
+                                                 style_class: 'workspaces-bar' });
+        this._workspacesBar.move_by(this._workspacesBarX, this._workspacesBarY);
+
+        let controlsBar = this._workspacesViewSwitch.createControlsBar();
+        let bar = this._workspaces.createControllerBar();
+        this._workspacesBar.add(bar, { expand: true, 'x-fill': true, 'y-fill': true,
+                                       y_align: St.Align.MIDDLE, x_align: St.Align.START });
+        this._workspacesBar.add(controlsBar, {x_align: St.Align.END});
+        this._workspacesBar.width = this._workspacesBarWidth;
+
+        this._group.add_actor(this._workspacesBar);
+        this._workspacesBar.raise(this._workspaces.actor);
+    },
+
+    _onViewChanged: function() {
+        if (!this.visible)
+            return;
+        //Remove old worspacesView
+        this._group.remove_actor(this._workspacesBar);
+        this._workspaces.hide();
+        this._group.remove_actor(this._workspaces.actor);
+        this._workspaces.destroy();
+        this._workspacesBar.destroy();
+
+        this._workspaces = this._workspacesViewSwitch.createCurrentWorkspaceView(this._workspacesWidth, this._workspacesHeight,
+                                                                             this._workspacesX, this._workspacesY, false);
+
+        //Show new workspacesView
+        this._group.add_actor(this._workspaces.actor);
+        this._dash.actor.raise(this._workspaces.actor);
+
+        this._createControlsBar();
+
+        // Set new position and scale to workspaces.
+        this.emit('showing');
+    },
+
     _recalculateGridSizes: function () {
         let primary = global.get_primary_monitor();
         wideScreen = (primary.width/primary.height > WIDE_SCREEN_CUT_OFF_RATIO) &&
@@ -188,9 +230,9 @@ Overview.prototype = {
         this._dash.searchResults.actor.height = this._workspacesHeight;
 
         // place the 'Add Workspace' button in the bottom row of the grid
-        addRemoveButtonSize = Math.floor(displayGridRowHeight * 3/5);
-        this._addButtonX = this._workspacesX + this._workspacesWidth - addRemoveButtonSize;
-        this._addButtonY = primary.height - Math.floor(displayGridRowHeight * 4/5);
+        this._workspacesBarX = this._workspacesX;
+        this._workspacesBarWidth = primary.width - this._workspacesBarX - WORKSPACE_GRID_PADDING;
+        this._workspacesBarY = primary.height - displayGridRowHeight + 5;
 
         // The parent (this._group) is positioned at the top left of the primary monitor
         // while this._backOver occupies the entire screen.
@@ -298,8 +340,8 @@ Overview.prototype = {
         this._dash.show();
 
         /* TODO: make this stuff dynamic */
-        this._workspaces = new Workspaces.Workspaces(this._workspacesWidth, this._workspacesHeight,
-                                                     this._workspacesX, this._workspacesY);
+        this._workspaces = this._workspacesViewSwitch.createCurrentWorkspaceView(this._workspacesWidth, this._workspacesHeight,
+                                                                             this._workspacesX, this._workspacesY, true);
         this._group.add_actor(this._workspaces.actor);
 
         // The workspaces actor is as big as the screen, so we have to raise the dash above it
@@ -307,11 +349,7 @@ Overview.prototype = {
         // be as big as the screen.
         this._dash.actor.raise(this._workspaces.actor);
 
-        // Create (+) button
-        this._addButton = new AddWorkspaceButton(addRemoveButtonSize, this._addButtonX, this._addButtonY, Lang.bind(this, this._acceptNewWorkspaceDrop));
-        this._addButton.actor.connect('button-release-event', Lang.bind(this, this._addNewWorkspace));
-        this._group.add_actor(this._addButton.actor);
-        this._addButton.actor.raise(this._workspaces.actor);
+        this._createControlsBar();
 
         // All the the actors in the window group are completely obscured,
         // hiding the group holding them while the Overview is displayed greatly
@@ -363,9 +401,8 @@ Overview.prototype = {
             this._activeDisplayPane.close();
         this._workspaces.hide();
 
-        this._addButton.actor.destroy();
-        this._addButton.actor = null;
-        this._addButton = null;
+        this._workspacesBar.destroy();
+        this._workspacesBar = null;
 
         // Create a zoom in effect by transforming the Overview group so that
         // the active workspace fills up the whole screen. The opposite
@@ -448,7 +485,7 @@ Overview.prototype = {
         this._dash.hide();
         this._group.hide();
 
-        this.visible = false; 
+        this.visible = false;
         this.animationInProgress = false;
         this._hideInProgress = false;
 
@@ -456,61 +493,6 @@ Overview.prototype = {
 
         Main.popModal(this._dash.actor);
         this.emit('hidden');
-    },
-
-    _addNewWorkspace: function() {
-        global.screen.append_new_workspace(false, global.get_current_time());
-    },
-
-    _acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) {
-        this._addNewWorkspace();
-        return this._workspaces.acceptNewWorkspaceDrop(source, dropActor, x, y, time);
     }
 };
 Signals.addSignalMethods(Overview.prototype);
-
-// Note that mutter has a compile-time limit of 36
-const MAX_WORKSPACES = 16;
-
-function AddWorkspaceButton(buttonSize, buttonX, buttonY, acceptDropCallback) {
-    this._init(buttonSize, buttonX, buttonY, acceptDropCallback);
-}
-
-AddWorkspaceButton.prototype = {
-    _init: function(buttonSize, buttonX, buttonY, acceptDropCallback) {
-        this.actor = new Clutter.Group({ x: buttonX,
-                                         y: buttonY,
-                                         width: global.screen_width - buttonX,
-                                         height: global.screen_height - buttonY,
-                                         reactive: true });
-        this.actor._delegate = this;
-        this._acceptDropCallback = acceptDropCallback;
-
-        let plus = new Clutter.Texture({ x: 0,
-                                         y: 0,
-                                         width: buttonSize,
-                                         height: buttonSize });
-        plus.set_from_file(global.imagedir + 'add-workspace.svg');
-        this.actor.add_actor(plus);
-
-        global.screen.connect('notify::n-workspaces', Lang.bind(this, this._nWorkspacesChanged));
-        this._nWorkspacesChanged();
-    },
-
-    _nWorkspacesChanged: function() {
-        let canAddAnother = global.screen.n_workspaces < MAX_WORKSPACES;
-
-        if (canAddAnother && !this.actor.reactive) {
-            this.actor.reactive = true;
-            this.actor.opacity = 255;
-        } else if (!canAddAnother && this.actor.reactive) {
-            this.actor.reactive = false;
-            this.actor.opacity = 85;
-        }
-    },
-
-    // Draggable target interface
-    acceptDrop: function(source, actor, x, y, time) {
-        return this.reactive && this._acceptDropCallback(source, actor, x, y, time);
-    }
-};
diff --git a/js/ui/workspaces.js b/js/ui/workspace.js
similarity index 70%
rename from js/ui/workspaces.js
rename to js/ui/workspace.js
index 427d313..49d0bed 100644
--- a/js/ui/workspaces.js
+++ b/js/ui/workspace.js
@@ -322,7 +322,7 @@ WindowOverlay.prototype = {
         this._parentActor = parentActor;
 
         let title = new St.Label({ style_class: "window-caption",
-                                   text : metaWindow.title });
+                                   text: metaWindow.title });
         title.connect('style-changed',
                       Lang.bind(this, this._onStyleChanged));
         title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
@@ -368,6 +368,8 @@ WindowOverlay.prototype = {
                                                   x, y);
         if (actor == this._windowClone.actor) {
             this.closeButton.show();
+            // Reposition the close button in case we've changed display modes
+            this._updatePositions();
         }
         this.title.show();
     },
@@ -375,7 +377,7 @@ WindowOverlay.prototype = {
     fadeIn: function() {
         this.title.opacity = 0;
         this.title.show();
-        this.title.raise_top();
+        this._parentActor.raise_top();
         Tweener.addTween(this.title,
                         { opacity: 255,
                           time: Overview.ANIMATION_TIME,
@@ -391,6 +393,12 @@ WindowOverlay.prototype = {
                this.title.height + this.title._spacing;
     },
 
+    _updatePositions: function() {
+        let [cloneX, cloneY] = this._windowClone.actor.get_transformed_position();
+        let [cloneWidth, cloneHeight] = this._windowClone.actor.get_transformed_size();
+        this.updatePositions(cloneX, cloneY, cloneWidth, cloneHeight);
+    },
+
     /**
      * @cloneX: x position of windowClone
      * @cloneY: y position of windowClone
@@ -460,7 +468,9 @@ WindowOverlay.prototype = {
     },
 
     _onEnter: function() {
-        this.closeButton.raise_top();
+        this._updatePositions();
+
+        this._parentActor.raise_top();
         this.closeButton.show();
         this.emit('show-close-button');
     },
@@ -510,6 +520,10 @@ WindowOverlay.prototype = {
 
 Signals.addSignalMethods(WindowOverlay.prototype);
 
+const WindowPositionFlags = {
+    ZOOM: 1 << 0,
+    ANIMATE: 1 << 1
+};
 
 /**
  * @workspaceNum: Workspace index
@@ -524,9 +538,14 @@ function Workspace(workspaceNum, parentActor) {
 Workspace.prototype = {
     _init : function(workspaceNum, parentActor) {
         this.workspaceNum = workspaceNum;
+        this._windowOverlaysGroup = new Clutter.Group();
+        // Without this the drop area will be overlapped.
+        this._windowOverlaysGroup.set_size(0, 0);
+
         this._metaWorkspace = global.screen.get_workspace_by_index(workspaceNum);
 
-        this.parentActor = parentActor;
+        parentActor.add_actor(this._windowOverlaysGroup);
+        this._parentActor = parentActor;
 
         this.actor = new Clutter.Group();
         this.actor._delegate = this;
@@ -578,7 +597,6 @@ Workspace.prototype = {
         this._windowRemovedId = this._metaWorkspace.connect('window-removed',
                                                             Lang.bind(this, this._windowRemoved));
 
-        this._removeButton = null;
         this._visible = false;
 
         this._frame = null;
@@ -586,51 +604,6 @@ Workspace.prototype = {
         this.leavingOverview = false;
     },
 
-    updateRemovable : function() {
-        let removable = (this._windows.length == 1 /* just desktop */ &&
-                         this.workspaceNum != 0 &&
-                         this.workspaceNum == global.screen.n_workspaces - 1);
-
-        if (removable) {
-            if (this._removeButton)
-                return;
-
-            this._removeButton = new Clutter.Texture({ width: Overview.addRemoveButtonSize,
-                                                       height: Overview.addRemoveButtonSize,
-                                                       reactive: true
-                                                     });
-            this._removeButton.set_from_file(global.imagedir + "remove-workspace.svg");
-            this._removeButton.connect('button-release-event', Lang.bind(this, this._removeSelf));
-
-            this.actor.add_actor(this._removeButton);
-            this._adjustRemoveButton();
-            this._adjustRemoveButtonId = this.actor.connect('notify::scale-x', Lang.bind(this, this._adjustRemoveButton));
-
-            if (this._visible) {
-                this._removeButton.set_opacity(0);
-                Tweener.addTween(this._removeButton,
-                                 { opacity: 255,
-                                   time: Overview.ANIMATION_TIME,
-                                   transition: "easeOutQuad"
-                                 });
-            }
-        } else {
-            if (!this._removeButton)
-                return;
-
-            if (this._visible) {
-                Tweener.addTween(this._removeButton,
-                                 { opacity: 0,
-                                   time: Overview.ANIMATION_TIME,
-                                   transition: "easeOutQuad",
-                                   onComplete: this._removeRemoveButton,
-                                   onCompleteScope: this
-                                 });
-            } else
-                this._removeRemoveButton();
-        }
-    },
-
     _lookupIndex: function (metaWindow) {
         let index, clone;
         for (let i = 0; i < this._windows.length; i++) {
@@ -661,7 +634,7 @@ Workspace.prototype = {
         this._showOnlyWindows = showOnlyWindows;
         this._resetCloneVisibility();
         if (reposition)
-            this.positionWindows(false);
+            this.positionWindows(WindowPositionFlags.ANIMATE);
     },
 
     /**
@@ -697,20 +670,6 @@ Workspace.prototype = {
         this._lightbox.highlight(actor);
     },
 
-    _adjustRemoveButton : function() {
-        this._removeButton.set_scale(1.0 / this.actor.scale_x,
-                                     1.0 / this.actor.scale_y);
-        this._removeButton.set_position(
-            (this.actor.width - this._removeButton.width / this.actor.scale_x) / 2,
-            (this.actor.height - this._removeButton.height / this.actor.scale_y) / 2);
-    },
-
-    _removeRemoveButton : function() {
-        this._removeButton.destroy();
-        this._removeButton = null;
-        this.actor.disconnect(this._adjustRemoveButtonId);
-    },
-
     // Mark the workspace selected/not-selected
     setSelected : function(selected) {
         // Don't draw a frame if we only have one workspace
@@ -1007,13 +966,18 @@ Workspace.prototype = {
 
     /**
      * positionWindows:
-     * @workspaceZooming: If true, then the workspace is moving at the same time and we need to take that into account.
+     * @flags:
+     *  ZOOM - workspace is moving at the same time and we need to take that into account.
+     *  ANIMATE - Indicates that we need animate changing position.
      */
-    positionWindows : function(workspaceZooming) {
+    positionWindows : function(flags) {
         let totalVisible = 0;
 
         let visibleWindows = this._getVisibleWindows();
 
+        let workspaceZooming = flags & WindowPositionFlags.ZOOM;
+        let animate = flags & WindowPositionFlags.ANIMATE;
+
         // Start the animations
         let slots = this._computeAllWindowSlots(visibleWindows.length);
         visibleWindows = this._orderWindowsByMotionAndStartup(visibleWindows, slots);
@@ -1028,18 +992,24 @@ Workspace.prototype = {
             let [x, y, scale] = this._computeWindowRelativeLayout(metaWindow, slot);
 
             overlay.hide();
-            Tweener.addTween(clone.actor,
-                             { x: x,
-                               y: y,
-                               scale_x: scale,
-                               scale_y: scale,
-                               workspace_relative: workspaceZooming ? this : null,
-                               time: Overview.ANIMATION_TIME,
-                               transition: "easeOutQuad",
-                               onComplete: Lang.bind(this, function() {
-                                  this._fadeInWindowOverlay(clone, overlay);
-                               })
-                             });
+            if (animate) {
+                Tweener.addTween(clone.actor,
+                                 { x: x,
+                                   y: y,
+                                   scale_x: scale,
+                                   scale_y: scale,
+                                   workspace_relative: workspaceZooming ? this : null,
+                                   time: Overview.ANIMATION_TIME,
+                                   transition: "easeOutQuad",
+                                   onComplete: Lang.bind(this, function() {
+                                      this._fadeInWindowOverlay(clone, overlay);
+                                   })
+                                 });
+            } else {
+                clone.actor.set_position(x, y);
+                clone.actor.set_scale(scale, scale);
+                this._fadeInWindowOverlay(clone, overlay);
+            }
         }
     },
 
@@ -1127,8 +1097,7 @@ Workspace.prototype = {
         }
         clone.destroy();
 
-        this.positionWindows(false);
-        this.updateRemovable();
+        this.positionWindows(WindowPositionFlags.ANIMATE);
     },
 
     _windowAdded : function(metaWorkspace, metaWin) {
@@ -1148,9 +1117,9 @@ Workspace.prototype = {
                                         }));
             return;
         }
-        
+
         if (!this._isOverviewWindow(win))
-            return;        
+            return;
 
         let clone = this._addWindowClone(win);
 
@@ -1164,29 +1133,19 @@ Workspace.prototype = {
             clone.actor.set_scale (scale, scale);
         }
 
-        this.positionWindows(false);
-        this.updateRemovable();
+        this.positionWindows(WindowPositionFlags.ANIMATE);
     },
 
     // Animate the full-screen to Overview transition.
-    zoomToOverview : function() {
+    zoomToOverview : function(animate) {
         this.actor.set_position(this.gridX, this.gridY);
         this.actor.set_scale(this.scale, this.scale);
 
         // Position and scale the windows.
-        this.positionWindows(true);
-
-        // Fade in the remove button if available, so that it doesn't appear
-        // too abrubtly and doesn't start at a too big size.
-        if (this._removeButton) {
-            Tweener.removeTweens(this._removeButton);
-            this._removeButton.opacity = 0;
-            Tweener.addTween(this._removeButton,
-                             { opacity: 255,
-                               time: Overview.ANIMATION_TIME,
-                               transition: 'easeOutQuad'
-                             });
-        }
+        if (animate)
+            this.positionWindows(WindowPositionFlags.ANIMATE | WindowPositionFlags.ZOOM);
+        else
+            this.positionWindows(WindowPositionFlags.ZOOM);
 
         this._visible = true;
     },
@@ -1200,17 +1159,6 @@ Workspace.prototype = {
         Main.overview.connect('hidden', Lang.bind(this,
                                                  this._doneLeavingOverview));
 
-        // Fade out the remove button if available, so that it doesn't
-        // disappear too abrubtly and doesn't become too big.
-        if (this._removeButton) {
-            Tweener.removeTweens(this._removeButton);
-            Tweener.addTween(this._removeButton,
-                             { opacity: 0,
-                               time: Overview.ANIMATION_TIME,
-                               transition: 'easeOutQuad'
-                             });
-        }
-
         // Position and scale the windows.
         for (let i = 1; i < this._windows.length; i++) {
             let clone = this._windows[i];
@@ -1226,7 +1174,7 @@ Workspace.prototype = {
                              });
         }
 
-        this._visible = false;        
+        this._visible = false;
     },
 
     // Animates grid shrinking/expanding when a row or column
@@ -1264,7 +1212,7 @@ Workspace.prototype = {
 
         this._visible = true;
     },
-    
+
     // Animates the removal of a workspace
     slideOut : function(onComplete) {
         let destX = this.actor.x, destY = this.actor.y;
@@ -1291,11 +1239,12 @@ Workspace.prototype = {
         // making its exit.
         this._desktop.reactive = false;
     },
-    
+
     destroy : function() {
         Tweener.removeTweens(this.actor);
         this.actor.destroy();
         this.actor = null;
+        this._windowOverlaysGroup.destroy();
 
         this._metaWorkspace.disconnect(this._windowAddedId);
         this._metaWorkspace.disconnect(this._windowRemovedId);
@@ -1321,7 +1270,7 @@ Workspace.prototype = {
     // Create a clone of a (non-desktop) window and add it to the window list
     _addWindowClone : function(win) {
         let clone = new WindowClone(win);
-        let overlay = new WindowOverlay(clone, this.parentActor);
+        let overlay = new WindowOverlay(clone, this._windowOverlaysGroup);
 
         clone.connect('selected',
                       Lang.bind(this, this._onCloneSelected));
@@ -1421,399 +1370,3 @@ Workspace.prototype = {
 };
 
 Signals.addSignalMethods(Workspace.prototype);
-
-function Workspaces(width, height, x, y) {
-    this._init(width, height, x, y);
-}
-
-Workspaces.prototype = {
-    _init : function(width, height, x, y) {
-        this.actor = new St.Bin({ style_class: "workspaces" });
-        this._actor = new Clutter.Group();
-
-        this.actor.add_actor(this._actor);
-
-        this._width = width;
-        this._height = height;
-        this._x = x;
-        this._y = y;
-
-        this._windowSelectionAppId = null;
-
-        this._workspaces = [];
-
-        this._highlightWindow = null;
-
-        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
-        let activeWorkspace;
-
-        // Create and position workspace objects
-        for (let w = 0; w < global.screen.n_workspaces; w++) {
-            this._addWorkspaceActor(w);
-            if (w == activeWorkspaceIndex) {
-                activeWorkspace = this._workspaces[w];
-                activeWorkspace.setSelected(true);
-            }
-        }
-        activeWorkspace.actor.raise_top();
-        this._positionWorkspaces();
-
-        let lastWorkspace = this._workspaces[this._workspaces.length - 1];
-        lastWorkspace.updateRemovable(true);
-
-        // Position/scale the desktop windows and their children after the
-        // workspaces have been created. This cannot be done first because
-        // window movement depends on the Workspaces object being accessible
-        // as an Overview member.
-        this._overviewShowingId =
-            Main.overview.connect('showing',
-                                 Lang.bind(this, function() {
-                this._onRestacked();
-                for (let w = 0; w < this._workspaces.length; w++)
-                    this._workspaces[w].zoomToOverview();
-        }));
-
-        // Track changes to the number of workspaces
-        this._nWorkspacesNotifyId =
-            global.screen.connect('notify::n-workspaces',
-                                  Lang.bind(this, this._workspacesChanged));
-        this._switchWorkspaceNotifyId =
-            global.window_manager.connect('switch-workspace',
-                                          Lang.bind(this, this._activeWorkspaceChanged));
-        this._restackedNotifyId =
-            global.screen.connect('restacked',
-                                  Lang.bind(this, this._onRestacked));
-    },
-
-    _lookupWorkspaceForMetaWindow: function (metaWindow) {
-        for (let i = 0; i < this._workspaces.length; i++) {
-            if (this._workspaces[i].containsMetaWindow(metaWindow))
-                return this._workspaces[i];
-        }
-        return null;
-    },
-
-    _lookupCloneForMetaWindow: function (metaWindow) {
-        for (let i = 0; i < this._workspaces.length; i++) {
-            let clone = this._workspaces[i].lookupCloneForMetaWindow(metaWindow);
-            if (clone)
-                return clone;
-        }
-        return null;
-    },
-
-    setHighlightWindow: function (metaWindow) {
-        // Looping over all workspaces is easier than keeping track of the last
-        // highlighted window while trying to handle the window or workspace possibly
-        // going away.
-        for (let i = 0; i < this._workspaces.length; i++) {
-            this._workspaces[i].setHighlightWindow(null);
-        }
-        if (metaWindow != null) {
-            let workspace = this._lookupWorkspaceForMetaWindow(metaWindow);
-            workspace.setHighlightWindow(metaWindow);
-        }
-    },
-
-    _clearApplicationWindowSelection: function(reposition) {
-        if (this._windowSelectionAppId == null)
-            return;
-        this._windowSelectionAppId = null;
-
-        for (let i = 0; i < this._workspaces.length; i++) {
-            this._workspaces[i].setLightboxMode(false);
-            this._workspaces[i].setShowOnlyWindows(null, reposition);
-        }
-    },
-
-    /**
-     * setApplicationWindowSelection:
-     * @appid: Application identifier string
-     *
-     * Enter a mode which shows only the windows owned by the
-     * given application, and allow highlighting of a specific
-     * window with setHighlightWindow().
-     */
-    setApplicationWindowSelection: function (appId) {
-        if (appId == null) {
-            this._clearApplicationWindowSelection(true);
-            return;
-        }
-
-        if (appId == this._windowSelectionAppId)
-            return;
-
-        this._windowSelectionAppId = appId;
-
-        let appSys = Shell.AppSystem.get_default();
-
-        let showOnlyWindows = {};
-        let app = appSys.get_app(appId);
-        let windows = app.get_windows();
-        for (let i = 0; i < windows.length; i++) {
-            showOnlyWindows[windows[i]] = 1;
-        }
-
-        for (let i = 0; i < this._workspaces.length; i++) {
-            this._workspaces[i].setLightboxMode(true);
-            this._workspaces[i].setShowOnlyWindows(showOnlyWindows, true);
-        }
-    },
-
-    /**
-     * activateWindowFromOverview:
-     * @metaWindow: A #MetaWindow
-     * @time: Integer even timestamp
-     *
-     * This function exits the overview, switching to the given @metaWindow.
-     * If an application filter is in effect, it will be cleared.
-     */
-    activateWindowFromOverview: function (metaWindow, time) {
-        if (this._windowSelectionAppId != null) {
-            this._clearApplicationWindowSelection(false);
-        }
-
-        Main.activateWindow(metaWindow, time);
-        Main.overview.hide();
-    },
-
-    hide : function() {
-        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
-        let activeWorkspace = this._workspaces[activeWorkspaceIndex];
-
-        this._positionWorkspaces();
-        activeWorkspace.actor.raise_top();
-
-        for (let w = 0; w < this._workspaces.length; w++)
-            this._workspaces[w].zoomFromOverview();
-    },
-
-    destroy : function() {
-        for (let w = 0; w < this._workspaces.length; w++)
-            this._workspaces[w].destroy();
-        this._workspaces = [];
-
-        this.actor.destroy();
-        this.actor = null;
-
-        Main.overview.disconnect(this._overviewShowingId);
-        global.screen.disconnect(this._nWorkspacesNotifyId);
-        global.window_manager.disconnect(this._switchWorkspaceNotifyId);
-        global.screen.disconnect(this._restackedNotifyId);
-    },
-
-    getScale : function() {
-        return this._workspaces[0].scale;
-    },
-
-    // Get the grid position of the active workspace.
-    getActiveWorkspacePosition : function() {
-        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
-        let activeWorkspace = this._workspaces[activeWorkspaceIndex];
-
-        return [activeWorkspace.gridX, activeWorkspace.gridY];
-    },
-
-    // Assign grid positions to workspaces. We can't just do a simple
-    // row-major or column-major numbering, because we don't want the
-    // existing workspaces to get rearranged when we add a row or
-    // column. So we alternate between adding to rows and adding to
-    // columns. (So, eg, when going from a 2x2 grid of 4 workspaces to
-    // a 3x2 grid of 5 workspaces, the 4 existing workspaces stay
-    // where they are, and the 5th one is added to the end of the
-    // first row.)
-    //
-    // FIXME: need to make the metacity internal layout agree with this!
-    _positionWorkspaces : function() {
-        let gridWidth = Math.ceil(Math.sqrt(this._workspaces.length));
-        let gridHeight = Math.ceil(this._workspaces.length / gridWidth);
-
-        let wsWidth = (this._width - (gridWidth - 1) * GRID_SPACING) / gridWidth;
-        let wsHeight = (this._height - (gridHeight - 1) * GRID_SPACING) / gridHeight;
-        let scale = wsWidth / global.screen_width;
-
-        let span = 1, n = 0, row = 0, col = 0, horiz = true;
-
-        for (let w = 0; w < this._workspaces.length; w++) {
-            let workspace = this._workspaces[w];
-
-            workspace.gridRow = row;
-            workspace.gridCol = col;
-
-            workspace.gridX = this._x + workspace.gridCol * (wsWidth + GRID_SPACING);
-            workspace.gridY = this._y + workspace.gridRow * (wsHeight + GRID_SPACING);
-            workspace.scale = scale;
-
-            if (horiz) {
-                col++;
-                if (col == span) {
-                    row = 0;
-                    horiz = false;
-                }
-            } else {
-                row++;
-                if (row == span) {
-                    col = 0;
-                    horiz = true;
-                    span++;
-                }
-            }
-        }
-    },
-
-    _workspacesChanged : function() {
-        let oldNumWorkspaces = this._workspaces.length;
-        let newNumWorkspaces = global.screen.n_workspaces;
-
-        if (oldNumWorkspaces == newNumWorkspaces)
-            return;
-
-        let oldScale = this._workspaces[0].scale;
-        let oldGridWidth = Math.ceil(Math.sqrt(oldNumWorkspaces));
-        let oldGridHeight = Math.ceil(oldNumWorkspaces / oldGridWidth);
-        let lostWorkspaces = [];
-
-        // The old last workspace is no longer removable.
-        this._workspaces[oldNumWorkspaces - 1].updateRemovable();
-
-        if (newNumWorkspaces > oldNumWorkspaces) {
-            // Create new workspace groups
-            for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
-                this._addWorkspaceActor(w);
-            }
-
-        } else {
-            // Truncate the list of workspaces
-            // FIXME: assumes that the workspaces are being removed from
-            // the end of the list, not the start/middle
-            lostWorkspaces = this._workspaces.splice(newNumWorkspaces);
-        }
-
-        // The new last workspace may be removable
-        let newLastWorkspace = this._workspaces[this._workspaces.length - 1];
-        newLastWorkspace.updateRemovable();
-
-        // Figure out the new layout
-        this._positionWorkspaces();
-        let newScale = this._workspaces[0].scale;
-        let newGridWidth = Math.ceil(Math.sqrt(newNumWorkspaces));
-        let newGridHeight = Math.ceil(newNumWorkspaces / newGridWidth);
-
-        if (newGridWidth != oldGridWidth || newGridHeight != oldGridHeight) {
-            // We need to resize/move the existing workspaces/windows
-            let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces);
-            for (let w = 0; w < existingWorkspaces; w++)
-                this._workspaces[w].resizeToGrid(oldScale);
-        }
-
-        if (newScale != oldScale) {
-            // The workspace scale affects window size/positioning because we clamp
-            // window size to a 1:1 ratio and never scale them up
-            let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces);
-            for (let w = 0; w < existingWorkspaces; w++)
-                this._workspaces[w].positionWindows(false);
-        }
-
-        if (newNumWorkspaces > oldNumWorkspaces) {
-            // Slide new workspaces in from offscreen
-            for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++)
-                this._workspaces[w].slideIn(oldScale);
-        } else {
-            // Slide old workspaces out
-            for (let w = 0; w < lostWorkspaces.length; w++) {
-                let workspace = lostWorkspaces[w];
-                workspace.slideOut(function () { workspace.destroy(); });
-            }
-
-            // FIXME: deal with windows on the lost workspaces
-        }
-
-        // Reset the selection state; if we went from > 1 workspace to 1,
-        // this has the side effect of removing the frame border
-        let activeIndex = global.screen.get_active_workspace_index();
-        this._workspaces[activeIndex].setSelected(true);
-    },
-
-    _activeWorkspaceChanged : function(wm, from, to, direction) {
-        this._workspaces[from].setSelected(false);
-        this._workspaces[to].setSelected(true);
-    },
-
-    _addWorkspaceActor : function(workspaceNum) {
-        let workspace  = new Workspace(workspaceNum, this._actor);
-        this._workspaces[workspaceNum] = workspace;
-        this._actor.add_actor(workspace.actor);
-    },
-
-    _onRestacked: function() {
-        let stack = global.get_windows();
-        let stackIndices = {};
-
-        for (let i = 0; i < stack.length; i++) {
-            // Use the stable sequence for an integer to use as a hash key
-            stackIndices[stack[i].get_meta_window().get_stable_sequence()] = i;
-        }
-
-        for (let i = 0; i < this._workspaces.length; i++)
-            this._workspaces[i].syncStacking(stackIndices);
-    },
-
-    // Handles a drop onto the (+) button; assumes the new workspace
-    // has already been added
-    acceptNewWorkspaceDrop : function(source, dropActor, x, y, time) {
-        return this._workspaces[this._workspaces.length - 1].acceptDrop(source, dropActor, x, y, time);
-    }
-};
-
-// Create a SpecialPropertyModifier to let us move windows in a
-// straight line on the screen even though their containing workspace
-// is also moving.
-Tweener.registerSpecialPropertyModifier("workspace_relative", _workspaceRelativeModifier, _workspaceRelativeGet);
-
-function _workspaceRelativeModifier(workspace) {
-    let [startX, startY] = Main.overview.getPosition();
-    let overviewPosX, overviewPosY, overviewScale;
-
-    if (!workspace)
-        return [];
-
-    if (workspace.leavingOverview) {
-        let [zoomedInX, zoomedInY] = Main.overview.getZoomedInPosition();
-        overviewPosX = { begin: startX, end: zoomedInX };
-        overviewPosY = { begin: startY, end: zoomedInY };
-        overviewScale = { begin: Main.overview.getScale(),
-                          end: Main.overview.getZoomedInScale() };
-    } else {
-        overviewPosX = { begin: startX, end: 0 };
-        overviewPosY = { begin: startY, end: 0 };
-        overviewScale = { begin: Main.overview.getScale(), end: 1 };
-    }
-
-    return [ { name: "x",
-               parameters: { workspacePos: workspace.gridX,
-                             overviewPos: overviewPosX,
-                             overviewScale: overviewScale } },
-             { name: "y",
-               parameters: { workspacePos: workspace.gridY,
-                             overviewPos: overviewPosY,
-                             overviewScale: overviewScale } }
-           ];
-}
-
-function _workspaceRelativeGet(begin, end, time, params) {
-    let curOverviewPos = (1 - time) * params.overviewPos.begin +
-                         time * params.overviewPos.end;
-    let curOverviewScale = (1 - time) * params.overviewScale.begin +
-                           time * params.overviewScale.end;
-
-    // Calculate the screen position of the window.
-    let screen = (1 - time) *
-                 ((begin + params.workspacePos) * params.overviewScale.begin +
-                  params.overviewPos.begin) +
-                 time *
-                 ((end + params.workspacePos) * params.overviewScale.end +
-                 params.overviewPos.end);
-
-    // Return the workspace coordinates.
-    return (screen - curOverviewPos) / curOverviewScale - params.workspacePos;
-}
diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js
new file mode 100644
index 0000000..d75e3e3
--- /dev/null
+++ b/js/ui/workspacesView.js
@@ -0,0 +1,956 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Big = imports.gi.Big;
+const Clutter = imports.gi.Clutter;
+const GdkPixbuf = imports.gi.GdkPixbuf;
+const Gdk = imports.gi.Gdk;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Meta = imports.gi.Meta;
+const Pango = imports.gi.Pango;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+const Signals = imports.signals;
+
+const DND = imports.ui.dnd;
+const Lightbox = imports.ui.lightbox;
+const Main = imports.ui.main;
+const Overview = imports.ui.overview;
+const Panel = imports.ui.panel;
+const Tweener = imports.ui.tweener;
+const Workspace = imports.ui.workspace;
+
+const WORKSPACE_SWITCH_TIME = 0.25;
+// Note that mutter has a compile-time limit of 36
+const MAX_WORKSPACES = 16;
+
+const GRID_SPACING = 15;
+
+const WorkspacesViewType = {
+    SINGLE: 0,
+    MOSAIC: 1
+};
+
+function GenericWorkspacesView(width, height, x, y, animate) {
+    this._init(width, height, x, y, animate);
+}
+
+GenericWorkspacesView.prototype = {
+    _init: function(width, height, x, y, animate) {
+        this.actor = new St.Bin({ style_class: "workspaces" });
+        this._actor = new Clutter.Group();
+
+        this.actor.add_actor(this._actor);
+
+        this._width = width;
+        this._height = height;
+        this._x = x;
+        this._y = y;
+
+        this._windowSelectionAppId = null;
+
+        this._workspaces = [];
+
+        this._highlightWindow = null;
+
+        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
+
+        // Create and position workspace objects
+        for (let w = 0; w < global.screen.n_workspaces; w++) {
+            this._addWorkspaceActor(w);
+        }
+        this._workspaces[activeWorkspaceIndex].actor.raise_top();
+        this._positionWorkspaces();
+
+        // Position/scale the desktop windows and their children after the
+        // workspaces have been created. This cannot be done first because
+        // window movement depends on the Workspaces object being accessible
+        // as an Overview member.
+        this._overviewShowingId =
+            Main.overview.connect('showing',
+                                 Lang.bind(this, function() {
+                this._onRestacked();
+                for (let w = 0; w < this._workspaces.length; w++)
+                    this._workspaces[w].zoomToOverview(animate);
+        }));
+
+        // Track changes to the number of workspaces
+        this._nWorkspacesNotifyId =
+            global.screen.connect('notify::n-workspaces',
+                                  Lang.bind(this, this._workspacesChanged));
+        this._switchWorkspaceNotifyId =
+            global.window_manager.connect('switch-workspace',
+                                          Lang.bind(this, this._activeWorkspaceChanged));
+        this._restackedNotifyId =
+            global.screen.connect('restacked',
+                                  Lang.bind(this, this._onRestacked));
+    },
+
+    _lookupWorkspaceForMetaWindow: function (metaWindow) {
+        for (let i = 0; i < this._workspaces.length; i++) {
+            if (this._workspaces[i].containsMetaWindow(metaWindow))
+                return this._workspaces[i];
+        }
+        return null;
+    },
+
+    _lookupCloneForMetaWindow: function (metaWindow) {
+        for (let i = 0; i < this._workspaces.length; i++) {
+            let clone = this._workspaces[i].lookupCloneForMetaWindow(metaWindow);
+            if (clone)
+                return clone;
+        }
+        return null;
+    },
+
+    setHighlightWindow: function (metaWindow) {
+        // Looping over all workspaces is easier than keeping track of the last
+        // highlighted window while trying to handle the window or workspace possibly
+        // going away.
+        for (let i = 0; i < this._workspaces.length; i++) {
+            this._workspaces[i].setHighlightWindow(null);
+        }
+        if (metaWindow != null) {
+            let workspace = this._lookupWorkspaceForMetaWindow(metaWindow);
+            workspace.setHighlightWindow(metaWindow);
+        }
+    },
+
+    _clearApplicationWindowSelection: function(reposition) {
+        if (this._windowSelectionAppId == null)
+            return;
+        this._windowSelectionAppId = null;
+
+        for (let i = 0; i < this._workspaces.length; i++) {
+            this._workspaces[i].setLightboxMode(false);
+            this._workspaces[i].setShowOnlyWindows(null, reposition);
+        }
+    },
+
+    /**
+     * setApplicationWindowSelection:
+     * @appid: Application identifier string
+     *
+     * Enter a mode which shows only the windows owned by the
+     * given application, and allow highlighting of a specific
+     * window with setHighlightWindow().
+     */
+    setApplicationWindowSelection: function (appId) {
+        if (appId == null) {
+            this._clearApplicationWindowSelection(true);
+            return;
+        }
+
+        if (appId == this._windowSelectionAppId)
+            return;
+
+        this._windowSelectionAppId = appId;
+
+        let appSys = Shell.AppSystem.get_default();
+
+        let showOnlyWindows = {};
+        let app = appSys.get_app(appId);
+        let windows = app.get_windows();
+        for (let i = 0; i < windows.length; i++) {
+            showOnlyWindows[windows[i]] = 1;
+        }
+
+        for (let i = 0; i < this._workspaces.length; i++) {
+            this._workspaces[i].setLightboxMode(true);
+            this._workspaces[i].setShowOnlyWindows(showOnlyWindows, true);
+        }
+    },
+
+    /**
+     * activateWindowFromOverview:
+     * @metaWindow: A #MetaWindow
+     * @time: Integer even timestamp
+     *
+     * This function exits the overview, switching to the given @metaWindow.
+     * If an application filter is in effect, it will be cleared.
+     */
+    activateWindowFromOverview: function (metaWindow, time) {
+        if (this._windowSelectionAppId != null) {
+            this._clearApplicationWindowSelection(false);
+        }
+
+        Main.activateWindow(metaWindow, time);
+        Main.overview.hide();
+    },
+
+    hide: function() {
+        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
+        let activeWorkspace = this._workspaces[activeWorkspaceIndex];
+
+        this._positionWorkspaces();
+        activeWorkspace.actor.raise_top();
+
+        for (let w = 0; w < this._workspaces.length; w++)
+            this._workspaces[w].zoomFromOverview();
+    },
+
+    destroy: function() {
+        for (let w = 0; w < this._workspaces.length; w++)
+            this._workspaces[w].destroy();
+        this._workspaces = [];
+
+        this.actor.destroy();
+        this.actor = null;
+
+        Main.overview.disconnect(this._overviewShowingId);
+        global.screen.disconnect(this._nWorkspacesNotifyId);
+        global.window_manager.disconnect(this._switchWorkspaceNotifyId);
+        global.screen.disconnect(this._restackedNotifyId);
+    },
+
+    getScale: function() {
+        return this._workspaces[0].scale;
+    },
+
+    _onRestacked: function() {
+        let stack = global.get_windows();
+        let stackIndices = {};
+
+        for (let i = 0; i < stack.length; i++) {
+            // Use the stable sequence for an integer to use as a hash key
+            stackIndices[stack[i].get_meta_window().get_stable_sequence()] = i;
+        }
+
+        for (let i = 0; i < this._workspaces.length; i++)
+            this._workspaces[i].syncStacking(stackIndices);
+    },
+
+    // Handles a drop onto the (+) button; assumes the new workspace
+    // has already been added
+    acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) {
+        return this._workspaces[this._workspaces.length - 1].acceptDrop(source, dropActor, x, y, time);
+    },
+
+    // Get the grid position of the active workspace.
+    getActiveWorkspacePosition: function() {
+        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
+        let activeWorkspace = this._workspaces[activeWorkspaceIndex];
+
+        return [activeWorkspace.gridX, activeWorkspace.gridY];
+    },
+
+    createControllerBar: function() {
+        throw new Error("Not implemented");
+    },
+
+    _positionWorkspaces: function() {
+        throw new Error("Not implemented");
+    },
+
+    _workspacesChanged: function() {
+        throw new Error("Not implemented");
+    },
+
+    _activeWorkspaceChanged: function() {
+        throw new Error("Not implemented");
+    },
+
+    _addWorkspaceActor: function() {
+        throw new Error("Not implemented");
+    }
+}
+
+function MosaicView(width, height, x, y, animate) {
+    this._init(width, height, x, y, animate);
+}
+
+MosaicView.prototype = {
+    __proto__: GenericWorkspacesView.prototype,
+
+    _init: function(width, height, x, y, animate) {
+        GenericWorkspacesView.prototype._init.call(this, width, height, x, y, animate);
+
+        this._workspaces[global.screen.get_active_workspace_index()].setSelected(true);
+
+        this._removeButton = null;
+        this._addButton = null;
+    },
+
+    // Assign grid positions to workspaces. We can't just do a simple
+    // row-major or column-major numbering, because we don't want the
+    // existing workspaces to get rearranged when we add a row or
+    // column. So we alternate between adding to rows and adding to
+    // columns. (So, eg, when going from a 2x2 grid of 4 workspaces to
+    // a 3x2 grid of 5 workspaces, the 4 existing workspaces stay
+    // where they are, and the 5th one is added to the end of the
+    // first row.)
+    //
+    // FIXME: need to make the metacity internal layout agree with this!
+    _positionWorkspaces: function() {
+        let gridWidth = Math.ceil(Math.sqrt(this._workspaces.length));
+        let gridHeight = Math.ceil(this._workspaces.length / gridWidth);
+
+        let wsWidth = (this._width - (gridWidth - 1) * GRID_SPACING) / gridWidth;
+        let wsHeight = (this._height - (gridHeight - 1) * GRID_SPACING) / gridHeight;
+        let scale = wsWidth / global.screen_width;
+
+        let span = 1, n = 0, row = 0, col = 0, horiz = true;
+
+        for (let w = 0; w < this._workspaces.length; w++) {
+            let workspace = this._workspaces[w];
+
+            workspace.gridRow = row;
+            workspace.gridCol = col;
+
+            workspace.gridX = this._x + workspace.gridCol * (wsWidth + GRID_SPACING);
+            workspace.gridY = this._y + workspace.gridRow * (wsHeight + GRID_SPACING);
+            workspace.scale = scale;
+
+            if (horiz) {
+                col++;
+                if (col == span) {
+                    row = 0;
+                    horiz = false;
+                }
+            } else {
+                row++;
+                if (row == span) {
+                    col = 0;
+                    horiz = true;
+                    span++;
+                }
+            }
+        }
+    },
+
+    _workspacesChanged: function() {
+        let oldNumWorkspaces = this._workspaces.length;
+        let newNumWorkspaces = global.screen.n_workspaces;
+
+        if (oldNumWorkspaces == newNumWorkspaces)
+            return;
+
+        let oldScale = this._workspaces[0].scale;
+        let oldGridWidth = Math.ceil(Math.sqrt(oldNumWorkspaces));
+        let oldGridHeight = Math.ceil(oldNumWorkspaces / oldGridWidth);
+        let lostWorkspaces = [];
+
+        // The old last workspace is no longer removable.
+
+        if (newNumWorkspaces > oldNumWorkspaces) {
+            // Create new workspace groups
+            for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
+                this._addWorkspaceActor(w);
+            }
+
+        } else {
+            // Truncate the list of workspaces
+            // FIXME: assumes that the workspaces are being removed from
+            // the end of the list, not the start/middle
+            lostWorkspaces = this._workspaces.splice(newNumWorkspaces);
+        }
+
+        // The new last workspace may be removable
+        let newLastWorkspace = this._workspaces[this._workspaces.length - 1];
+
+        // Figure out the new layout
+        this._positionWorkspaces();
+        let newScale = this._workspaces[0].scale;
+        let newGridWidth = Math.ceil(Math.sqrt(newNumWorkspaces));
+        let newGridHeight = Math.ceil(newNumWorkspaces / newGridWidth);
+
+        if (newGridWidth != oldGridWidth || newGridHeight != oldGridHeight) {
+            // We need to resize/move the existing workspaces/windows
+            let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces);
+            for (let w = 0; w < existingWorkspaces; w++)
+                this._workspaces[w].resizeToGrid(oldScale);
+        }
+
+        if (newScale != oldScale) {
+            // The workspace scale affects window size/positioning because we clamp
+            // window size to a 1:1 ratio and never scale them up
+            let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces);
+            for (let w = 0; w < existingWorkspaces; w++)
+                this._workspaces[w].positionWindows(Workspace.WindowPositionFlags.ANIMATE);
+        }
+
+        if (newNumWorkspaces > oldNumWorkspaces) {
+            // Slide new workspaces in from offscreen
+            for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++)
+                this._workspaces[w].slideIn(oldScale);
+        } else {
+            // Slide old workspaces out
+            for (let w = 0; w < lostWorkspaces.length; w++) {
+                let workspace = lostWorkspaces[w];
+                workspace.slideOut(function () { workspace.destroy(); });
+            }
+
+            // FIXME: deal with windows on the lost workspaces
+        }
+
+        // Reset the selection state; if we went from > 1 workspace to 1,
+        // this has the side effect of removing the frame border
+        let activeIndex = global.screen.get_active_workspace_index();
+        this._workspaces[activeIndex].setSelected(true);
+
+        this._updateButtonsVisibility();
+    },
+
+    _activeWorkspaceChanged: function(wm, from, to, direction) {
+        this._workspaces[from].setSelected(false);
+        this._workspaces[to].setSelected(true);
+    },
+
+    _addWorkspaceActor: function(workspaceNum) {
+        let workspace  = new Workspace.Workspace(workspaceNum, this._actor);
+        this._workspaces[workspaceNum] = workspace;
+        this._actor.add_actor(workspace.actor);
+    },
+
+    createControllerBar: function() {
+        let actor = new St.BoxLayout({ 'pack-start': true });
+        let bin = new St.Bin();
+        let addButton = new St.Button({ style_class: "single-view-add" });
+        this._addButton = addButton;
+        addButton.connect('clicked', Lang.bind(this, this._addNewWorkspace));
+        addButton._delegate = addButton;
+        addButton._delegate.acceptDrop = Lang.bind(this, function(source, actor, x, y, time) {
+            return this._acceptNewWorkspaceDrop(source, actor, x, y, time);
+        });
+        actor.add(bin, { x_align: St.Align.END });
+        bin.set_child(addButton);
+        bin.set_alignment(St.Align.END, St.Align.START);
+
+        bin = new St.Bin();
+        let removeButton = new St.Button({ style_class: "single-view-remove" });
+        this._removeButton = removeButton;
+        removeButton.connect('clicked', Lang.bind(this, function() {
+            if (this._workspaces.length <= 1)
+                return;
+            global.screen.remove_workspace(this._workspaces[this._workspaces.length - 1]._metaWorkspace, global.get_current_time());
+        }));
+        actor.add(bin, { expand: true, x_fill: true, x_align: St.Align.END });
+        this._updateButtonsVisibility();
+        bin.set_child(removeButton);
+        bin.set_alignment(St.Align.END, St.Align.START);
+
+        return actor;
+    },
+
+    _updateButtonsVisibility: function() {
+        //_removeButton may yet not exist.
+        if (this._removeButton == null)
+            return;
+        if (global.screen.n_workspaces == 1)
+            this._removeButton.hide();
+        else
+            this._removeButton.show();
+        if (this._addButton == null)
+            return;
+        if (global.screen.n_workspaces >= MAX_WORKSPACES)
+            this._addButton.hide();
+        else
+            this._addButton.show();
+    },
+
+    _addNewWorkspace: function() {
+        global.screen.append_new_workspace(false, global.get_current_time());
+    },
+
+    _acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) {
+        this._addNewWorkspace();
+        return this.acceptNewWorkspaceDrop(source, dropActor, x, y, time);
+    }
+};
+
+// Create a SpecialPropertyModifier to let us move windows in a
+// straight line on the screen even though their containing workspace
+// is also moving.
+Tweener.registerSpecialPropertyModifier("workspace_relative", _workspaceRelativeModifier, _workspaceRelativeGet);
+
+function SingleView(width, height, x, y, animate) {
+    this._init(width, height, x, y, animate);
+}
+
+SingleView.prototype = {
+    __proto__: GenericWorkspacesView.prototype,
+
+    _init: function(width, height, x, y, animate) {
+        this._scroll = null;
+        GenericWorkspacesView.prototype._init.call(this, width, height, x, y, animate);
+
+        this._actor.set_clip(x, y, width, height);
+        this._addButton = null;
+        this._removeButton = null;
+        this._indicatorsPanel = null;
+        this._indicatorsPanelWidth = null;
+
+        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
+        for (let w = 0; w < this._workspaces.length; w++) {
+            if (w != activeWorkspaceIndex) {
+                this._workspaces[w].actor.hide();
+                continue;
+            }
+            this._workspaces[w].actor.show();
+            this._workspaces[w]._windowOverlaysGroup.show();
+        }
+    },
+
+    _positionWorkspaces: function() {
+        let position = global.screen.get_active_workspace_index();
+        let scale = this._width / global.screen_width;
+
+        if (this._scroll != null)
+            position = this._scroll.adjustment.value;
+        let isInt = (Math.round(position) === position);
+
+        for (let w = 0; w < this._workspaces.length; w++) {
+            let workspace = this._workspaces[w];
+
+            workspace.gridRow = 0;
+            workspace.gridCol = 0;
+
+            workspace.scale = scale;
+            workspace.actor.set_scale(scale, scale);
+            workspace.gridX = this._x + (w - position) * workspace.actor.width;
+            workspace.gridY = this._y;
+            workspace.actor.set_position(workspace.gridX, workspace.gridY);
+            if (isInt) {
+                if (this.actor.get_stage() != null)
+                   workspace.positionWindows(0);
+                if (w == position) {
+                    workspace._windowOverlaysGroup.show();
+                    workspace.actor.show();
+                } else {
+                    workspace._windowOverlaysGroup.hide();
+                    workspace.actor.hide();
+                }
+            } else {
+                workspace._windowOverlaysGroup.hide();
+                if (Math.abs(w - position) <= 1)
+                    workspace.actor.show();
+                else
+                    workspace.actor.hide();
+            }
+        }
+    },
+
+    _workspacesChanged: function() {
+        let oldNumWorkspaces = this._workspaces.length;
+        let newNumWorkspaces = global.screen.n_workspaces;
+
+        if (oldNumWorkspaces == newNumWorkspaces)
+            return;
+
+        if (this._scroll != null) {
+            let adj = this._scroll.get_adjustment();
+            adj.upper = newNumWorkspaces;
+            this._scroll.adjustment = adj;
+        }
+        let lostWorkspaces = [];
+
+        if (newNumWorkspaces > oldNumWorkspaces) {
+            // Create new workspace groups
+            for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
+                this._addWorkspaceActor(w);
+                this._workspaces[w].actor.hide();
+            }
+
+        } else {
+            for (let i = 0; i < this._workspaces.length; i++)
+                this._workspaces[i].destroy();
+            this._actor.remove_all();
+
+            //Without this will be a lot of warnings
+            this._actor.hide();
+
+            this._workspaces = [];
+            let activeWorkspaceIndex = global.screen.get_active_workspace_index();
+            for (let w = 0; w < global.screen.n_workspaces; w++) {
+                this._addWorkspaceActor(w);
+                if (w == activeWorkspaceIndex) {
+                    this._workspaces[w].actor.show();
+                } else {
+                    this._workspaces[w].actor.hide();
+                }
+            }
+            this._actor.show();
+        }
+        this._positionWorkspaces();
+
+        // Reset the selection state; if we went from > 1 workspace to 1,
+        // this has the side effect of removing the frame border
+        let activeIndex = global.screen.get_active_workspace_index();
+        this._workspaces[activeIndex].actor.show();
+        this._workspaces[activeIndex]._windowOverlaysGroup.show();
+
+        this._updatePanelVisibility();
+    },
+
+    _activeWorkspaceChanged: function(wm, from, to, direction) {
+        this._updatePanelVisibility();
+        let showAnimation = true;
+
+        if (this._scroll != null) {
+            let adj = this._scroll.get_adjustment();
+            if (Math.round(adj.value - to) != adj.value - to)
+                showAnimation = false;
+            if (adj.value - to == 0)
+                showAnimation = false;
+            adj.value = to;
+            this._scroll.adjustment = adj;
+        }
+        if (showAnimation) {
+            let fx;
+            if (from > to) {
+                fx = this._workspaces[0].actor.width;
+            } else {
+                fx = -this._workspaces[0].actor.width;
+            }
+            this._workspaces[from]._windowOverlaysGroup.hide();
+            this._workspaces[to].actor.set_position(this._x - fx, this._workspaces[to].gridY);
+            this._workspaces[to].actor.show();
+            Tweener.addTween(this._workspaces[to].actor,
+                             { x: this._x,
+                               transition: 'easeOutQuad',
+                               time: WORKSPACE_SWITCH_TIME
+                              });
+
+            Tweener.addTween(this._workspaces[from].actor,
+                             { x: this._x + fx,
+                               transition: 'easeOutQuad',
+                               time: WORKSPACE_SWITCH_TIME,
+                               onComplete: this._positionWorkspaces,
+                               onCompleteScope: this
+                              });
+        } else
+            this._positionWorkspaces();
+    },
+
+    _addWorkspaceActor: function(workspaceNum) {
+        let workspace  = new Workspace.Workspace(workspaceNum, this._actor);
+        this._actor.add_actor(workspace.actor);
+        workspace._windowOverlaysGroup.hide();
+
+        this._workspaces[workspaceNum] = workspace;
+    },
+
+    createControllerBar: function() {
+        let panel = new St.BoxLayout({ 'pack-start': true, vertical: true });
+
+        let actor = new St.BoxLayout({ 'pack-start': true });
+        let adj = new St.Adjustment({ value: global.screen.get_active_workspace_index(),
+                                      lower: 0,
+                                      'page-increment': 1,
+                                      'page-size': 1,
+                                      'step-increment': 1,
+                                      upper: this._workspaces.length });
+        this._scroll = new St.ScrollBar({ adjustment: null, vertical: false, name: 'SwitchScroll' });
+
+        this._scroll.connect('notify::adjustment', Lang.bind(this, function() {
+            this._scroll.adjustment.connect('notify::value', Lang.bind(this, function () {
+                if (Math.abs(Math.round(this._scroll.adjustment.value) - this._scroll.adjustment.value) < 0.1) {
+                    this._scroll.adjustment.set_value (Math.round(this._scroll.adjustment.value));
+                    this._workspaces[Math.round(this._scroll.adjustment.value)]._metaWorkspace.activate(global.get_current_time());
+                } else
+                    this._positionWorkspaces();
+            }));
+        }));
+        this._scroll.adjustment = adj;
+
+        let addButton = new St.Button({ style_class: "single-view-add" });
+        this._addButton = addButton;
+        addButton.connect('clicked', Lang.bind(this, this._addNewWorkspace));
+        addButton._delegate = addButton;
+        addButton._delegate.acceptDrop = Lang.bind(this, function(source, actor, x, y, time) {
+            return this._acceptNewWorkspaceDrop(source, actor, x, y, time);
+        });
+        actor.add(addButton, {x_align: St.Align.END, y_align: St.Align.START, 'y-fill': false});
+
+        let removeButton = new St.Button({ style_class: "single-view-remove" });
+        this._removeButton = removeButton;
+        removeButton.connect('clicked', Lang.bind(this, function() {
+            if (this._workspaces.length <= 1)
+                return;
+            let index = global.screen.get_active_workspace_index();
+            if (index == 0)
+                return;
+            global.screen.remove_workspace(this._workspaces[index]._metaWorkspace, global.get_current_time());
+        }));
+        actor.add(removeButton, { x_align: St.Align.END, y_align: St.Align.START, 'y-fill': false });
+        this._updatePanelVisibility();
+
+        panel.add(this._createPositionalIndicator(), {expand: true, 'x-fill': true, 'y-fill': true});
+        panel.add(this._scroll, { expand: true,
+                                  'x-fill': true,
+                                  'y-fill': false,
+                                  y_align: St.Align.START });
+        // backward-stepper/forward-stepper has const width (= height)
+        let separator = new St.Button({ style_class: 'scroll-separator' });
+        actor.add(separator, {});
+
+        actor.add(panel, {expand: true, 'x-fill': true, 'y-fill': true});
+
+        separator = new St.Button({ style_class: 'scroll-separator' });
+        actor.add(separator, {});
+
+        return actor;
+    },
+
+    _addIndicatorClone: function(i, active) {
+        let actor = new St.Button({ style_class: 'workspace-indicator' });
+        if (active) {
+            actor.style_class = 'workspace-indicator-active';
+        }
+        actor.connect('button-release-event', Lang.bind(this, function() {
+            if (this._workspaces[i] != undefined)
+                this._workspaces[i]._metaWorkspace.activate(global.get_current_time());
+        }));
+
+        this._indicatorsPanel.add_actor(actor);
+
+        let [a, spacing] = actor.get_theme_node().get_length('border-spacing', false);
+        if (this._indicatorsPanelWidth < spacing * (i + 1) + actor.width * (i + 1))
+            actor.hide();
+        actor.x = spacing * i + actor.width * i;
+    },
+
+    _fillPositionalIndicator: function() {
+        if (this._indicatorsPanel == null || this._indicatorsPanelWidth == null)
+            return;
+        let width = this._indicatorsPanelWidth;
+        this._indicatorsPanel.remove_all();
+
+        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
+        for (let i = 0; i < this._workspaces.length; i++) {
+            this._addIndicatorClone(i, i == activeWorkspaceIndex);
+        }
+        this._indicatorsPanel.x = (this._indicatorsPanelWidth - this._indicatorsPanel.width) / 2;
+    },
+
+    _createPositionalIndicator: function() {
+        let actor = new St.Bin({ style_class: 'panel-button' });
+        let group = new Clutter.Group();
+
+        this._indicatorsPanel = new Shell.GenericContainer();
+        this._indicatorsPanel.connect('get-preferred-width', Lang.bind(this, function (actor, fh, alloc) {
+            let children = actor.get_children();
+            let width = 0;
+            for (let i = 0; i < children.length; i++) {
+                if (!children[i].visible)
+                    continue;
+                if (children[i].x + children[i].width <= width)
+                    continue;
+                width = children[i].x + children[i].width;
+            }
+            alloc.min_size = width;
+            alloc.nat_size = width;
+        }));
+        this._indicatorsPanel.connect('get-preferred-height', Lang.bind(this, function (actor, fw, alloc) {
+            let children = actor.get_children();
+            let height = 0;
+            if (children.length)
+                height = children[0].height;
+            alloc.min_size = height;
+            alloc.nat_size = height;
+        }));
+        this._indicatorsPanel.connect('allocate', Lang.bind(this, function (actor, box, flags) {
+            let children = actor.get_children();
+            for (let i = 0; i < children.length; i++) {
+                if (!children[i].visible)
+                    continue;
+                let childBox = new Clutter.ActorBox();
+                childBox.x1 = children[i].x;
+                childBox.y1 = 0;
+                childBox.x2 = children[i].x + children[i].width;
+                childBox.y2 = children[i].height;
+                children[i].allocate(childBox, flags);
+            }
+        }));
+
+        group.add_actor(this._indicatorsPanel);
+        actor.set_child(group);
+        actor.set_alignment(St.Align.START, St.Align.START);
+        actor.set_fill(true, true);
+        this._indicatorsPanel.hide();
+        actor.connect('notify::width', Lang.bind(this, function(actor) {
+            this._indicatorsPanelWidth = actor.width;
+            this._updatePanelVisibility();
+        }));
+        actor.connect('destroy', Lang.bind(this, function() {
+            this._indicatorsPanel = null;
+        }));
+        return actor;
+    },
+
+    _updatePanelVisibility: function() {
+        let n = global.screen.n_workspaces;
+        if (this._removeButton != null) {
+            // set opacity here, because if hide it, _scroll will fill this space.
+            if (global.screen.get_active_workspace_index() == 0)
+                this._removeButton.set_opacity(0);
+            else
+                this._removeButton.set_opacity(255);
+        }
+        if (this._addButton != null) {
+            // same here
+            this._addButton.set_opacity((global.screen.n_workspaces < MAX_WORKSPACES) * 255);
+        }
+        if (this._scroll != null) {
+            if (n > 1)
+                this._scroll.show();
+            else
+                this._scroll.hide();
+        }
+        if (this._indicatorsPanel != null) {
+            if (n == 1) {
+                this._indicatorsPanel.hide();
+            } else {
+                this._indicatorsPanel.show();
+            }
+        }
+        this._fillPositionalIndicator();
+    },
+
+    _addNewWorkspace: function() {
+        // Button with opacity 0 is clickable.
+        if (global.screen.n_workspaces >= MAX_WORKSPACES)
+            return;
+        global.screen.append_new_workspace(false, global.get_current_time());
+        this._workspaces[this._workspaces.length - 1]._metaWorkspace.activate(Clutter.get_current_event_time());
+    },
+
+    _acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) {
+        this._addNewWorkspace();
+        return this.acceptNewWorkspaceDrop(source, dropActor, x, y, time);
+    }
+};
+
+function _workspaceRelativeModifier(workspace) {
+    let [startX, startY] = Main.overview.getPosition();
+    let overviewPosX, overviewPosY, overviewScale;
+
+    if (!workspace)
+        return [];
+
+    if (workspace.leavingOverview) {
+        let [zoomedInX, zoomedInY] = Main.overview.getZoomedInPosition();
+        overviewPosX = { begin: startX, end: zoomedInX };
+        overviewPosY = { begin: startY, end: zoomedInY };
+        overviewScale = { begin: Main.overview.getScale(),
+                          end: Main.overview.getZoomedInScale() };
+    } else {
+        overviewPosX = { begin: startX, end: 0 };
+        overviewPosY = { begin: startY, end: 0 };
+        overviewScale = { begin: Main.overview.getScale(), end: 1 };
+    }
+
+    return [ { name: "x",
+               parameters: { workspacePos: workspace.gridX,
+                             overviewPos: overviewPosX,
+                             overviewScale: overviewScale } },
+             { name: "y",
+               parameters: { workspacePos: workspace.gridY,
+                             overviewPos: overviewPosY,
+                             overviewScale: overviewScale } }
+           ];
+}
+
+function _workspaceRelativeGet(begin, end, time, params) {
+    let curOverviewPos = (1 - time) * params.overviewPos.begin +
+                         time * params.overviewPos.end;
+    let curOverviewScale = (1 - time) * params.overviewScale.begin +
+                           time * params.overviewScale.end;
+
+    // Calculate the screen position of the window.
+    let screen = (1 - time) *
+                 ((begin + params.workspacePos) * params.overviewScale.begin +
+                  params.overviewPos.begin) +
+                 time *
+                 ((end + params.workspacePos) * params.overviewScale.end +
+                 params.overviewPos.end);
+
+    // Return the workspace coordinates.
+    return (screen - curOverviewPos) / curOverviewScale - params.workspacePos;
+}
+
+function WorkspacesViewSwitch() {
+    this._init();
+}
+
+WorkspacesViewSwitch.prototype = {
+    VIEW_KEY: 'view',
+
+    _init: function() {
+        this._gconf = Shell.GConf.get_default();
+        this._mosaicViewButton = null;
+        this._singleViewButton = null;
+        this._currentViewType = this._gconf.get_int(this.VIEW_KEY);
+        this._controlsBar = null;
+    },
+
+    _setView: function(view) {
+        this._mosaicViewButton.set_checked(WorkspacesViewType.MOSAIC == view);
+        this._singleViewButton.set_checked(WorkspacesViewType.SINGLE == view);
+
+        if (this._currentViewType == view)
+            return;
+        this._currentViewType = view;
+        this._gconf.set_int(this.VIEW_KEY, view);
+        this.emit('view-changed');
+    },
+
+    createCurrentWorkspaceView: function(width, height, x, y, animate) {
+        switch (this._currentViewType) {
+            case WorkspacesViewType.SINGLE:
+                return new SingleView(width, height, x, y, animate);
+            case WorkspacesViewType.MOSAIC:
+                return new MosaicView(width, height, x, y, animate);
+            default:
+                return new MosaicView(width, height, x, y, animate);
+        }
+    },
+
+    createControlsBar: function() {
+        let actor = new St.BoxLayout();
+
+        this._mosaicViewButton = new St.Button({ style_class: "switch-view-mosaic" });
+        this._mosaicViewButton.set_toggle_mode(true);
+        this._mosaicViewButton.connect('clicked', Lang.bind(this, function() {
+            this._setView(WorkspacesViewType.MOSAIC);
+        }));
+        actor.add(this._mosaicViewButton, {'y-fill' : false, 'y-align' : St.Align.START});
+
+        this._singleViewButton = new St.Button({ style_class: "switch-view-single" });
+        this._singleViewButton.set_toggle_mode(true);
+        this._singleViewButton.connect('clicked', Lang.bind(this, function() {
+            this._setView(WorkspacesViewType.SINGLE);
+        }));
+        actor.add(this._singleViewButton, {'y-fill' : false, 'y-align' : St.Align.START});
+
+        if (this._currentViewType == WorkspacesViewType.MOSAIC)
+            this._mosaicViewButton.set_checked(true);
+        else
+            this._singleViewButton.set_checked(true);
+
+        this._nWorkspacesNotifyId =
+            global.screen.connect('notify::n-workspaces',
+                                  Lang.bind(this, this._workspacesChanged));
+
+        actor.connect('destroy', Lang.bind(this, function() {
+            this._controlsBar = null;
+            global.screen.disconnect(this._nWorkspacesNotifyId);
+        }));
+
+        this._controlsBar = actor;
+        this._workspacesChanged();
+        return actor;
+    },
+
+    _workspacesChanged: function() {
+        if (this._controlsBar == null)
+            return;
+        if (global.screen.n_workspaces == 1)
+            this._controlsBar.set_opacity(0);
+        else
+            this._controlsBar.set_opacity(255);
+    }
+};
+
+Signals.addSignalMethods(WorkspacesViewSwitch.prototype);



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]