Merge lp:~gala-dev/gala/expose-natural into lp:gala

Proposed by Tom Beckmann
Status: Merged
Approved by: Rico Tzschichholz
Approved revision: 210
Merged at revision: 201
Proposed branch: lp:~gala-dev/gala/expose-natural
Merge into: lp:gala
Diff against target: 910 lines (+798/-4)
7 files modified
CMakeLists.txt (+2/-0)
data/org.pantheon.desktop.gala.gschema.xml (+15/-0)
src/Plugin.vala (+12/-1)
src/Settings.vala (+2/-0)
src/Widgets/WindowOverview.vala (+597/-0)
src/Widgets/WindowThumb.vala (+167/-0)
src/Widgets/WorkspaceView.vala (+3/-3)
To merge this branch: bzr merge lp:~gala-dev/gala/expose-natural
Reviewer Review Type Date Requested Status
Rico Tzschichholz Approve
Review via email: mp+121001@code.launchpad.net

Description of the change

This replaces the old expose branch with a new one using the algorithm found in the natural-window-placement Gnome Shell extension (which is a copy of the one in KWin)

This adds the long requested expose feature as alternative to alt-tab window switching. It's default shortcut right now is super+e and it can be chosen as hotcorner action.
At the moment, it isnt integrated to the workspace view and as said earlier only supports choosing the active window for simplicity and stability reasons, anything else would have created too much work, that way there are no corner cases to be handled, just simple expose.

To post a comment you must log in.
Revision history for this message
Cassidy James Blaede (cassidyjames) wrote :

I'd like to start calling this "Window Overview" rather than "expose" if
possible. 1. Expose doesn't really mean anything, 2. It's super close to
"Exposé," which is Apple's term for a similar feature, 3. I think "Window
Overview" makes more sense and is consistent with "Window Switcher,"
"Workspace Switcher," and "Workspace Overview."

Also, from what I saw in Tom's YouTube video, the placement needs work. It
feels a bit awkward right now (especially when there are more and more
windows).

Thanks,
Cassidy James

On Thu, Aug 23, 2012 at 10:33 AM, Rico Tzschichholz <email address hidden>wrote:

> The proposal to merge lp:~gala-dev/gala/expose-natural into lp:gala has
> been updated.
>
> Description changed to:
>
> This replaces the old expose branch with a new one using the algorithm
> found in the natural-window-placement Gnome Shell extension (which is a
> copy of the one in KWin)
>
> This adds the long requested expose feature as alternative to alt-tab
> window switching. It's default shortcut right now is super+e and it can be
> chosen as hotcorner action.
> At the moment, it isnt integrated to the workspace view and as said
> earlier only supports choosing the active window for simplicity and
> stability reasons, anything else would have created too much work, that way
> there are no corner cases to be handled, just simple expose.
>
> For more details, see:
> https://code.launchpad.net/~gala-dev/gala/expose-natural/+merge/121001
> --
> https://code.launchpad.net/~gala-dev/gala/expose-natural/+merge/121001
> Your team Gala developers is requested to review the proposed merge of
> lp:~gala-dev/gala/expose-natural into lp:gala.
>

Revision history for this message
Rico Tzschichholz (ricotz) wrote :

> I'd like to start calling this "Window Overview" rather than "expose" if
> possible. 1. Expose doesn't really mean anything, 2. It's super close to
> "Exposé," which is Apple's term for a similar feature, 3. I think "Window
> Overview" makes more sense and is consistent with "Window Switcher,"
> "Workspace Switcher," and "Workspace Overview."

+1

review: Needs Fixing
lp:~gala-dev/gala/expose-natural updated
194. By Tom Beckmann

Improve animations

195. By Tom Beckmann

Darken background while being in expose

196. By Tom Beckmann

Add a second, grid based algorithm, make this one the default one and add options in dconf to switch it

Revision history for this message
Tom Beckmann (tombeckmann) wrote :

Should this renaming happen code intern as well? Or just on the user front?

lp:~gala-dev/gala/expose-natural updated
197. By Tom Beckmann

Cleanup natural_placement

198. By Tom Beckmann

Replace Expo with WindowOverview

199. By Tom Beckmann

Fix problem with natural algo after cleanup

200. By Tom Beckmann

Cleanup grid_placement

201. By Tom Beckmann

Move code to better places, math related stuff to Utils namespace, some minor changes

202. By Tom Beckmann

Sort windows by monitor

203. By Rico Tzschichholz

Some minor fixes

204. By Rico Tzschichholz

Remove some useless methods

205. By Rico Tzschichholz

Use arrays instead of lists in natural-placement

206. By Tom Beckmann

Port the enlarging part for the natural placement algorithm

207. By Tom Beckmann

Some more cleanups for natural placement

208. By Rico Tzschichholz

Optimize a bit with the cost of behaviour change

209. By Tom Beckmann

Simplify area calculation

210. By Tom Beckmann

Rename ExposedWindow to WindowThumb

Revision history for this message
Rico Tzschichholz (ricotz) :
review: Approve
lp:~gala-dev/gala/expose-natural updated
211. By Rico Tzschichholz

grid-placement: avoid empty window-slot which would make it look weird

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2012-08-23 10:03:20 +0000
3+++ CMakeLists.txt 2012-08-27 21:04:19 +0000
4@@ -54,7 +54,9 @@
5 src/TextShadowEffect.vala
6 src/Utils.vala
7 src/Widgets/AppIcon.vala
8+ src/Widgets/WindowOverview.vala
9 src/Widgets/WindowSwitcher.vala
10+ src/Widgets/WindowThumb.vala
11 src/Widgets/WorkspaceThumb.vala
12 src/Widgets/WorkspaceView.vala
13 ${CMAKE_BINARY_DIR}/src/Config.vala
14
15=== modified file 'data/org.pantheon.desktop.gala.gschema.xml'
16--- data/org.pantheon.desktop.gala.gschema.xml 2012-07-28 06:40:12 +0000
17+++ data/org.pantheon.desktop.gala.gschema.xml 2012-08-27 21:04:19 +0000
18@@ -7,6 +7,11 @@
19 <value nick="minimize-current" value="3" />
20 <value nick="open-launcher" value="4" />
21 <value nick="custom-command" value="5" />
22+ <value nick="window-overview" value="6" />
23+ </enum>
24+ <enum id="GalaWindowOverviewType">
25+ <value nick='grid' value='0'/>
26+ <value nick='natural' value='1'/>
27 </enum>
28
29 <schema path="/org/pantheon/desktop/gala/behavior/" id="org.pantheon.desktop.gala.behavior" gettext-domain="gala">
30@@ -15,6 +20,11 @@
31 <summary>Action for the top left corner</summary>
32 <description></description>
33 </key>
34+ <key enum="GalaWindowOverviewType" name="window-overview-type">
35+ <default>'grid'</default>
36+ <summary>Algorithm for window overview layout</summary>
37+ <description>Choose the algorithm used for exposing the windows</description>
38+ </key>
39 <key enum="GalaActionType" name="hotcorner-topright">
40 <default>'none'</default>
41 <summary>Action for the top right corner</summary>
42@@ -65,6 +75,11 @@
43 <summary>Shortcut to move to last workspace</summary>
44 <description></description>
45 </key>
46+ <key type="as" name="expose-windows">
47+ <default><![CDATA[['<Super>e']]]></default>
48+ <summary>Shortcut to move to last workspace</summary>
49+ <description></description>
50+ </key>
51 </schema>
52
53 <schema path="/org/pantheon/desktop/gala/appearance/" id="org.pantheon.desktop.gala.appearance" gettext-domain="gala">
54
55=== modified file 'src/Plugin.vala'
56--- src/Plugin.vala 2012-08-24 19:39:01 +0000
57+++ src/Plugin.vala 2012-08-27 21:04:19 +0000
58@@ -26,7 +26,8 @@
59 MAXIMIZE_CURRENT,
60 MINIMIZE_CURRENT,
61 OPEN_LAUNCHER,
62- CUSTOM_COMMAND
63+ CUSTOM_COMMAND,
64+ WINDOW_OVERVIEW
65 }
66
67 public enum InputArea {
68@@ -39,6 +40,7 @@
69 {
70 WindowSwitcher winswitcher;
71 WorkspaceView workspace_view;
72+ WindowOverview window_overview;
73
74 Window? moving; //place for the window that is being moved over
75
76@@ -77,11 +79,17 @@
77
78 winswitcher = new WindowSwitcher (this);
79
80+ window_overview = new WindowOverview (this);
81+
82 stage.add_child (workspace_view);
83 stage.add_child (winswitcher);
84+ stage.add_child (window_overview);
85
86 /*keybindings*/
87
88+ screen.get_display ().add_keybinding ("expose-windows", BehaviorSettings.get_default ().schema, 0, () => {
89+ window_overview.open (true);
90+ });
91 screen.get_display ().add_keybinding ("move-to-workspace-first", BehaviorSettings.get_default ().schema, 0, () => {
92 screen.get_workspace_by_index (0).activate (screen.get_display ().get_current_time ());
93 });
94@@ -280,6 +288,9 @@
95 warning (e.message);
96 }
97 break;
98+ case ActionType.WINDOW_OVERVIEW:
99+ window_overview.open (true);
100+ break;
101 default:
102 warning ("Trying to run unknown action");
103 break;
104
105=== modified file 'src/Settings.vala'
106--- src/Settings.vala 2012-07-26 12:54:56 +0000
107+++ src/Settings.vala 2012-08-27 21:04:19 +0000
108@@ -25,6 +25,8 @@
109 public string overlay_action { get; set; }
110 public string hotcorner_custom_command { get; set; }
111
112+ public WindowOverviewType window_overview_type { get; set; }
113+
114 public ActionType hotcorner_topleft { get; set; }
115 public ActionType hotcorner_topright { get; set; }
116 public ActionType hotcorner_bottomleft { get; set; }
117
118=== added file 'src/Widgets/WindowOverview.vala'
119--- src/Widgets/WindowOverview.vala 1970-01-01 00:00:00 +0000
120+++ src/Widgets/WindowOverview.vala 2012-08-27 21:04:19 +0000
121@@ -0,0 +1,597 @@
122+//
123+// Copyright (C) 2012 Tom Beckmann
124+//
125+// This program is free software: you can redistribute it and/or modify
126+// it under the terms of the GNU General Public License as published by
127+// the Free Software Foundation, either version 3 of the License, or
128+// (at your option) any later version.
129+//
130+// This program is distributed in the hope that it will be useful,
131+// but WITHOUT ANY WARRANTY; without even the implied warranty of
132+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
133+// GNU General Public License for more details.
134+//
135+// You should have received a copy of the GNU General Public License
136+// along with this program. If not, see <http://www.gnu.org/licenses/>.
137+//
138+
139+using Meta;
140+using Clutter;
141+
142+namespace Gala
143+{
144+
145+ public enum WindowOverviewType
146+ {
147+ GRID = 0,
148+ NATURAL
149+ }
150+
151+ public class WindowOverview : Actor
152+ {
153+ Plugin plugin;
154+ Screen screen;
155+
156+ bool ready;
157+
158+ static const int PADDING = 50;
159+
160+ public WindowOverview (Plugin _plugin)
161+ {
162+ plugin = _plugin;
163+ screen = plugin.get_screen ();
164+
165+ screen.workspace_switched.connect (() => close (false));
166+
167+ visible = false;
168+ ready = true;
169+ }
170+
171+ public override bool key_press_event (Clutter.KeyEvent event)
172+ {
173+ //FIXME need to figure out the actual keycombo, for now leave it by
174+ // default and others will close it by selecting a window!
175+ if (event.keyval == Clutter.Key.e || event.keyval == Clutter.Key.Escape) {
176+ close (true);
177+
178+ return true;
179+ }
180+
181+ return false;
182+ }
183+
184+ public override void key_focus_out ()
185+ {
186+ close (false);
187+ }
188+
189+ /**
190+ * Code ported from KWin present windows effect
191+ * https://projects.kde.org/projects/kde/kde-workspace/repository/revisions/master/entry/kwin/effects/presentwindows/presentwindows.cpp
192+ **/
193+
194+ //constants, mainly for natural expo
195+ const int GAPS = 10;
196+ const int MAX_TRANSLATIONS = 100000;
197+ const int ACCURACY = 20;
198+ const int BORDER = 10;
199+ const int TOP_GAP = 20;
200+ const int BOTTOM_GAP = 100;
201+
202+ //some math utilities
203+ int squared_distance (Gdk.Point a, Gdk.Point b)
204+ {
205+ var k1 = b.x - a.x;
206+ var k2 = b.y - a.y;
207+
208+ return k1*k1 + k2*k2;
209+ }
210+
211+ bool rect_is_overlapping_any (Meta.Rectangle rect, Meta.Rectangle[] rects, Meta.Rectangle border)
212+ {
213+ if (!border.contains_rect (rect))
214+ return true;
215+ foreach (var comp in rects) {
216+ if (comp == rect)
217+ continue;
218+
219+ if (rect.overlap (comp))
220+ return true;
221+ }
222+
223+ return false;
224+ }
225+
226+ Meta.Rectangle rect_adjusted (Meta.Rectangle rect, int dx1, int dy1, int dx2, int dy2)
227+ {
228+ return {rect.x + dx1, rect.y + dy1, rect.width + (-dx1 + dx2), rect.height + (-dy1 + dy2)};
229+ }
230+
231+ Gdk.Point rect_center (Meta.Rectangle rect)
232+ {
233+ return {rect.x + rect.width / 2, rect.y + rect.height / 2};
234+ }
235+
236+
237+ void calculate_places (List<Actor> windows)
238+ {
239+ var clones = windows.copy ();
240+ clones.sort ((a, b) => {
241+ return (int)(a as WindowThumb).window.get_stable_sequence () -
242+ (int)(b as WindowThumb).window.get_stable_sequence ();
243+ });
244+
245+ //sort windows by monitor
246+ List<Actor>[] monitors = {};
247+ monitors.resize (screen.get_n_monitors ());
248+
249+ foreach (var clone in clones)
250+ monitors[(clone as WindowThumb).window.get_monitor ()].append (clone);
251+
252+ for (var i = 0; i < screen.get_n_monitors (); i++) {
253+ if (monitors[i].length () == 0)
254+ continue;
255+
256+ // get the area used by the expo algorithms together
257+ var geom = screen.get_monitor_geometry (i);
258+ Meta.Rectangle area = {(int)Math.floorf (geom.x + BORDER),
259+ (int)Math.floorf (geom.y + TOP_GAP),
260+ (int)Math.floorf (geom.width - BORDER * 2),
261+ (int)Math.floorf (geom.height - BOTTOM_GAP)};
262+
263+ if (BehaviorSettings.get_default ().schema.get_enum ("window-overview-type") == WindowOverviewType.GRID)
264+ grid_placement (area, monitors[i]);
265+ else
266+ natural_placement (area, monitors[i]);
267+ }
268+ }
269+
270+ void grid_placement (Meta.Rectangle area, List<Actor> clones)
271+ {
272+ int columns = (int)Math.ceil (Math.sqrt (clones.length ()));
273+ int rows = (int)Math.ceil (clones.length () / (double)columns);
274+
275+ // Assign slots
276+ int slot_width = area.width / columns;
277+ int slot_height = area.height / rows;
278+
279+ WindowThumb[] taken_slots = {};
280+ taken_slots.resize (rows * columns);
281+
282+ // precalculate all slot centers
283+ Gdk.Point[] slot_centers = {};
284+ slot_centers.resize (rows * columns);
285+ for (int x = 0; x < columns; x++) {
286+ for (int y = 0; y < rows; y++) {
287+ slot_centers[x + y * columns] = {area.x + slot_width * x + slot_width / 2,
288+ area.y + slot_height * y + slot_height / 2};
289+ }
290+ }
291+
292+ // Assign each window to the closest available slot
293+ var tmplist = clones.copy ();
294+ var window_count = tmplist.length ();
295+ while (tmplist.length () > 0) {
296+ var window = tmplist.nth_data (0) as WindowThumb;
297+ var rect = window.window.get_outer_rect ();
298+
299+ var slot_candidate = -1;
300+ var slot_candidate_distance = int.MAX;
301+ var pos = rect_center (rect);
302+
303+ // all slots
304+ for (int i = 0; i < columns * rows; i++) {
305+ if (i > window_count - 1)
306+ break;
307+
308+ var dist = squared_distance (pos, slot_centers[i]);
309+
310+ if (dist < slot_candidate_distance) {
311+ // window is interested in this slot
312+ WindowThumb occupier = taken_slots[i];
313+ if (occupier == window)
314+ continue;
315+
316+ if (occupier == null || dist < squared_distance (rect_center (occupier.window.get_outer_rect ()), slot_centers[i])) {
317+ // either nobody lives here, or we're better - takeover the slot if it's our best
318+ slot_candidate = i;
319+ slot_candidate_distance = dist;
320+ }
321+ }
322+ }
323+
324+ if (slot_candidate == -1)
325+ continue;
326+
327+ if (taken_slots[slot_candidate] != null)
328+ tmplist.prepend (taken_slots[slot_candidate]);
329+
330+ tmplist.remove_all (window);
331+ taken_slots[slot_candidate] = window;
332+ }
333+
334+ for (int slot = 0; slot < columns * rows; slot++) {
335+ var window = taken_slots[slot];
336+ // some slots might be empty
337+ if (window == null)
338+ continue;
339+
340+ var rect = window.window.get_outer_rect ();
341+
342+ // Work out where the slot is
343+ Meta.Rectangle target = {area.x + (slot % columns) * slot_width,
344+ area.y + (slot / columns) * slot_height,
345+ slot_width,
346+ slot_height};
347+ target = rect_adjusted (target, 10, 10, -10, -10);
348+
349+ float scale;
350+ if (target.width / (double)rect.width < target.height / (double)rect.height) {
351+ // Center vertically
352+ scale = target.width / (float)rect.width;
353+ target.y += (target.height - (int)(rect.height * scale)) / 2;
354+ target.height = (int)Math.floorf (rect.height * scale);
355+ } else {
356+ // Center horizontally
357+ scale = target.height / (float)window.height;
358+ target.x += (target.width - (int)(rect.width * scale)) / 2;
359+ target.width = (int)Math.floorf (rect.width * scale);
360+ }
361+
362+ // Don't scale the windows too much
363+ if (scale > 2.0 || (scale > 1.0 && (rect.width > 300 || rect.height > 300))) {
364+ scale = (rect.width > 300 || rect.height > 300) ? 1.0f : 2.0f;
365+ target = {rect_center (target).x - (int)Math.floorf (rect.width * scale) / 2,
366+ rect_center (target).y - (int)Math.floorf (rect.height * scale) / 2,
367+ (int)Math.floorf (scale * rect.width),
368+ (int)Math.floorf (scale * rect.height)};
369+ }
370+
371+ place_window (window, target);
372+ }
373+ }
374+
375+ void natural_placement (Meta.Rectangle area, List<Actor> clones)
376+ {
377+ Meta.Rectangle bounds = {area.x, area.y, area.width, area.height};
378+
379+ var direction = 0;
380+ int[] directions = new int[clones.length ()];
381+ Meta.Rectangle[] rects = new Meta.Rectangle[clones.length ()];
382+
383+ for (int i = 0; i < clones.length (); i++) {
384+ // save rectangles into 4-dimensional arrays representing two corners of the rectangular: [left_x, top_y, right_x, bottom_y]
385+ var rect = (clones.nth_data (i) as WindowThumb).window.get_outer_rect ();
386+ rect = rect_adjusted(rect, -GAPS, -GAPS, GAPS, GAPS);
387+ rects[i] = rect;
388+ bounds = bounds.union (rect);
389+
390+ // This is used when the window is on the edge of the screen to try to use as much screen real estate as possible.
391+ directions[i] = direction;
392+ direction++;
393+ if (direction == 4)
394+ direction = 0;
395+ }
396+
397+ var loop_counter = 0;
398+ var overlap = false;
399+ do {
400+ overlap = false;
401+ for (var i = 0; i < rects.length; i++) {
402+ for (var j = 0; j < rects.length; j++) {
403+ if (i == j)
404+ continue;
405+
406+ var rect = rects[i];
407+ var comp = rects[j];
408+
409+ if (!rect.overlap (comp))
410+ continue;
411+
412+ loop_counter ++;
413+ overlap = true;
414+
415+ // Determine pushing direction
416+ Gdk.Point i_center = rect_center (rect);
417+ Gdk.Point j_center = rect_center (comp);
418+ Gdk.Point diff = {j_center.x - i_center.x, j_center.y - i_center.y};
419+
420+ // Prevent dividing by zero and non-movement
421+ if (diff.x == 0 && diff.y == 0)
422+ diff.x = 1;
423+
424+ // Approximate a vector of between 10px and 20px in magnitude in the same direction
425+ var length = Math.sqrtf (diff.x * diff.x + diff.y * diff.y);
426+ diff.x = (int)Math.floorf (diff.x * ACCURACY / length);
427+ diff.y = (int)Math.floorf (diff.y * ACCURACY / length);
428+ // Move both windows apart
429+ rect.x += -diff.x;
430+ rect.y += -diff.y;
431+ comp.x += diff.x;
432+ comp.y += diff.y;
433+
434+ // Try to keep the bounding rect the same aspect as the screen so that more
435+ // screen real estate is utilised. We do this by splitting the screen into nine
436+ // equal sections, if the window center is in any of the corner sections pull the
437+ // window towards the outer corner. If it is in any of the other edge sections
438+ // alternate between each corner on that edge. We don't want to determine it
439+ // randomly as it will not produce consistant locations when using the filter.
440+ // Only move one window so we don't cause large amounts of unnecessary zooming
441+ // in some situations. We need to do this even when expanding later just in case
442+ // all windows are the same size.
443+ // (We are using an old bounding rect for this, hopefully it doesn't matter)
444+ var x_section = (int)Math.roundf ((rect.x - bounds.x) / (bounds.width / 3.0f));
445+ var y_section = (int)Math.roundf ((comp.y - bounds.y) / (bounds.height / 3.0f));
446+
447+ i_center = rect_center (rect);
448+ diff.x = 0;
449+ diff.y = 0;
450+ if (x_section != 1 || y_section != 1) { // Remove this if you want the center to pull as well
451+ if (x_section == 1)
452+ x_section = (directions[i] / 2 == 1 ? 2 : 0);
453+ if (y_section == 1)
454+ y_section = (directions[i] % 2 == 1 ? 2 : 0);
455+ }
456+ if (x_section == 0 && y_section == 0) {
457+ diff.x = bounds.x - i_center.x;
458+ diff.y = bounds.y - i_center.y;
459+ }
460+ if (x_section == 2 && y_section == 0) {
461+ diff.x = bounds.x + bounds.width - i_center.x;
462+ diff.y = bounds.y - i_center.y;
463+ }
464+ if (x_section == 2 && y_section == 2) {
465+ diff.x = bounds.x + bounds.width - i_center.x;
466+ diff.y = bounds.y + bounds.height - i_center.y;
467+ }
468+ if (x_section == 0 && y_section == 2) {
469+ diff.x = bounds.x - i_center.x;
470+ diff.y = bounds.y + bounds.height - i_center.y;
471+ }
472+ if (diff.x != 0 || diff.y != 0) {
473+ length = Math.sqrtf (diff.x * diff.x + diff.y * diff.y);
474+ diff.x *= (int)Math.floorf (ACCURACY / length / 2.0f);
475+ diff.y *= (int)Math.floorf (ACCURACY / length / 2.0f);
476+ rect.x += diff.x;
477+ rect.y += diff.y;
478+ }
479+
480+ // Update bounding rect
481+ bounds = bounds.union(rect);
482+ bounds = bounds.union(comp);
483+
484+ //we took copies from the rects from our list so we need to reassign them
485+ rects[i] = rect;
486+ rects[j] = comp;
487+ }
488+ }
489+ } while (overlap && loop_counter < MAX_TRANSLATIONS);
490+
491+ // Work out scaling by getting the most top-left and most bottom-right window coords.
492+ float scale = Math.fminf (Math.fminf (area.width / (float)bounds.width, area.height / (float)bounds.height), 1.0f);
493+
494+ // Make bounding rect fill the screen size for later steps
495+ bounds.x = (int)Math.floorf (bounds.x - (area.width - bounds.width * scale) / 2);
496+ bounds.y = (int)Math.floorf (bounds.y - (area.height - bounds.height * scale) / 2);
497+ bounds.width = (int)Math.floorf (area.width / scale);
498+ bounds.height = (int)Math.floorf (area.height / scale);
499+
500+ // Move all windows back onto the screen and set their scale
501+ var index = 0;
502+ foreach (var rect in rects) {
503+ rect = {(int)Math.floorf ((rect.x - bounds.x) * scale + area.x),
504+ (int)Math.floorf ((rect.y - bounds.y) * scale + area.y),
505+ (int)Math.floorf (rect.width * scale),
506+ (int)Math.floorf (rect.height * scale)};
507+
508+ rects[index] = rect;
509+ index++;
510+ }
511+
512+ // fill gaps by enlarging windows
513+ bool moved = false;
514+ Meta.Rectangle border = area;
515+ do {
516+ moved = false;
517+
518+ index = 0;
519+ foreach (var rect in rects) {
520+
521+ int width_diff = ACCURACY;
522+ int height_diff = (int)Math.floorf ((((rect.width + width_diff) - rect.height) /
523+ (float)rect.width) * rect.height);
524+ int x_diff = width_diff / 2;
525+ int y_diff = height_diff / 2;
526+
527+ //top right
528+ Meta.Rectangle old = rect;
529+ rect = {rect.x + x_diff, rect.y - y_diff - height_diff, rect.width + width_diff, rect.height + width_diff};
530+ if (rect_is_overlapping_any (rect, rects, border))
531+ rect = old;
532+ else
533+ moved = true;
534+
535+ //bottom right
536+ old = rect;
537+ rect = {rect.x + x_diff, rect.y + y_diff, rect.width + width_diff, rect.height + width_diff};
538+ if (rect_is_overlapping_any (rect, rects, border))
539+ rect = old;
540+ else
541+ moved = true;
542+
543+ //bottom left
544+ old = rect;
545+ rect = {rect.x - x_diff, rect.y + y_diff, rect.width + width_diff, rect.height + width_diff};
546+ if (rect_is_overlapping_any (rect, rects, border))
547+ rect = old;
548+ else
549+ moved = true;
550+
551+ //top left
552+ old = rect;
553+ rect = {rect.x - x_diff, rect.y - y_diff - height_diff, rect.width + width_diff, rect.height + width_diff};
554+ if (rect_is_overlapping_any (rect, rects, border))
555+ rect = old;
556+ else
557+ moved = true;
558+
559+ rects[index] = rect;
560+ index++;
561+ }
562+ } while (moved);
563+
564+ index = 0;
565+ foreach (var rect in rects) {
566+ var window = clones.nth_data (index) as WindowThumb;
567+ var window_rect = window.window.get_outer_rect ();
568+
569+ rect = rect_adjusted(rect, GAPS, GAPS, -GAPS, -GAPS);
570+ scale = rect.width / (float)window_rect.width;
571+
572+ if (scale > 2.0 || (scale > 1.0 && (window_rect.width > 300 || window_rect.height > 300))) {
573+ scale = (window_rect.width > 300 || window_rect.height > 300) ? 1.0f : 2.0f;
574+ rect = {rect_center (rect).x - (int)Math.floorf (window_rect.width * scale) / 2,
575+ rect_center (rect).y - (int)Math.floorf (window_rect.height * scale) / 2,
576+ (int)Math.floorf (window_rect.width * scale),
577+ (int)Math.floorf (window_rect.height * scale)};
578+ }
579+
580+ place_window (window, rect);
581+ index++;
582+ }
583+ }
584+
585+ // animate a window to the given position
586+ void place_window (WindowThumb clone, Meta.Rectangle rect)
587+ {
588+ var fscale = rect.width / clone.width;
589+
590+ //animate the windows and icons to the calculated positions
591+ clone.icon.x = rect.x + Math.floorf (clone.width * fscale / 2.0f - clone.icon.width / 2.0f);
592+ clone.icon.y = rect.y + Math.floorf (clone.height * fscale - 50.0f);
593+ clone.icon.get_parent ().set_child_above_sibling (clone.icon, null);
594+
595+ clone.close_button.x = rect.x - 10;
596+ clone.close_button.y = rect.y - 10;
597+
598+ clone.animate (Clutter.AnimationMode.EASE_OUT_CUBIC, 250, scale_x:fscale, scale_y:fscale, x:rect.x+0.0f, y:rect.y+0.0f)
599+ .completed.connect (() => ready = true );
600+ clone.icon.opacity = 0;
601+ clone.icon.animate (Clutter.AnimationMode.EASE_OUT_CUBIC, 350, scale_x:1.0f, scale_y:1.0f, opacity:255);
602+ }
603+
604+ public void open (bool animate = true)
605+ {
606+ if (!ready)
607+ return;
608+
609+ if (visible) {
610+ close (true);
611+ return;
612+ }
613+
614+ ready = false;
615+
616+ var used_windows = new SList<Window> ();
617+
618+ foreach (var window in screen.get_active_workspace ().list_windows ()) {
619+ if (window.window_type != WindowType.NORMAL && window.window_type != WindowType.DOCK) {
620+ (window.get_compositor_private () as Actor).hide ();
621+ continue;
622+ }
623+ if (window.window_type == WindowType.DOCK)
624+ continue;
625+
626+ used_windows.append (window);
627+ }
628+
629+ var n_windows = used_windows.length ();
630+ if (n_windows == 0)
631+ return;
632+
633+ Compositor.get_background_actor_for_screen (screen).
634+ animate (AnimationMode.EASE_OUT_QUAD, 1000, dim_factor : 0.4);
635+
636+ // sort windows by stacking order
637+ var windows = screen.get_display ().sort_windows_by_stacking (used_windows);
638+
639+ grab_key_focus ();
640+
641+ plugin.begin_modal ();
642+ Utils.set_input_area (screen, InputArea.FULLSCREEN);
643+
644+ visible = true;
645+
646+ foreach (var window in windows) {
647+ var actor = window.get_compositor_private () as WindowActor;
648+ if (actor == null)
649+ return;
650+ actor.hide ();
651+
652+ var clone = new WindowThumb (window);
653+ clone.x = actor.x;
654+ clone.y = actor.y;
655+
656+ clone.selected.connect (selected);
657+ clone.reposition.connect (reposition);
658+
659+ add_child (clone);
660+ }
661+
662+ calculate_places (get_children ());
663+ }
664+
665+ //called when a window has been closed
666+ void reposition (WindowThumb removed)
667+ {
668+ var children = get_children ().copy ();
669+ children.remove (removed);
670+ calculate_places (children);
671+ }
672+
673+ void selected (Window window)
674+ {
675+ window.activate (screen.get_display ().get_current_time ());
676+
677+ close (true);
678+ }
679+
680+ void close (bool animate)
681+ {
682+ if (!visible || !ready)
683+ return;
684+
685+ ready = false;
686+
687+ plugin.end_modal ();
688+ plugin.update_input_area ();
689+
690+ foreach (var child in get_children ()) {
691+ var exposed = child as WindowThumb;
692+ exposed.close (animate);
693+ exposed.selected.disconnect (selected);
694+ }
695+
696+ Compositor.get_background_actor_for_screen (screen).
697+ animate (AnimationMode.EASE_OUT_QUAD, 500, dim_factor : 1.0);
698+
699+ if (animate) {
700+ Timeout.add (300, () => {
701+ visible = false;
702+ ready = true;
703+
704+ foreach (var window in screen.get_active_workspace ().list_windows ())
705+ (window.get_compositor_private () as Actor).show ();
706+
707+ return false;
708+ });
709+ } else {
710+ ready = true;
711+ visible = false;
712+
713+ foreach (var window in screen.get_active_workspace ().list_windows ())
714+ (window.get_compositor_private () as Actor).show ();
715+ }
716+ }
717+ }
718+}
719
720=== added file 'src/Widgets/WindowThumb.vala'
721--- src/Widgets/WindowThumb.vala 1970-01-01 00:00:00 +0000
722+++ src/Widgets/WindowThumb.vala 2012-08-27 21:04:19 +0000
723@@ -0,0 +1,167 @@
724+//
725+// Copyright (C) 2012 Tom Beckmann
726+//
727+// This program is free software: you can redistribute it and/or modify
728+// it under the terms of the GNU General Public License as published by
729+// the Free Software Foundation, either version 3 of the License, or
730+// (at your option) any later version.
731+//
732+// This program is distributed in the hope that it will be useful,
733+// but WITHOUT ANY WARRANTY; without even the implied warranty of
734+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
735+// GNU General Public License for more details.
736+//
737+// You should have received a copy of the GNU General Public License
738+// along with this program. If not, see <http://www.gnu.org/licenses/>.
739+//
740+
741+using Meta;
742+using Clutter;
743+
744+namespace Gala
745+{
746+ public class WindowThumb : Actor
747+ {
748+ public weak Window window;
749+ Clone clone;
750+ public GtkClutter.Texture icon;
751+ public GtkClutter.Texture close_button;
752+
753+ public signal void selected (Window window);
754+ public signal void reposition ();
755+
756+ public WindowThumb (Window _window)
757+ {
758+ window = _window;
759+
760+ reactive = true;
761+
762+ var actor = window.get_compositor_private () as WindowActor;
763+ clone = new Clone (actor.get_texture ());
764+
765+ icon = new GtkClutter.Texture ();
766+ icon.scale_x = 0.0f;
767+ icon.scale_y = 0.0f;
768+ icon.scale_gravity = Gravity.CENTER;
769+
770+ try {
771+ icon.set_from_pixbuf (Utils.get_icon_for_window (window, 64));
772+ } catch (Error e) { warning (e.message); }
773+
774+ close_button = new GtkClutter.Texture ();
775+ close_button.reactive = true;
776+ close_button.visible = false;
777+ close_button.scale_x = 0.0f;
778+ close_button.scale_y = 0.0f;
779+ close_button.scale_gravity = Gravity.CENTER;
780+ close_button.button_press_event.connect (close_clicked);
781+ close_button.leave_event.connect ((e) => leave_event (e));
782+
783+ try {
784+ close_button.set_from_pixbuf (Granite.Widgets.Utils.get_close_pixbuf ());
785+ } catch (Error e) { warning (e.message); }
786+
787+ add_child (clone);
788+
789+ var stage = Compositor.get_stage_for_screen (window.get_screen ());
790+ stage.add_child (icon);
791+ stage.add_child (close_button);
792+ }
793+
794+ bool close_clicked (ButtonEvent event)
795+ {
796+ if (event.button != 1)
797+ return false;
798+
799+ //make sure we dont see a window closing animation in the background
800+ (window.get_compositor_private () as Actor).opacity = 0;
801+ get_parent ().set_child_below_sibling (this, null);
802+ animate (AnimationMode.EASE_IN_CUBIC, 200, depth : -50.0f, opacity : 0).completed.connect (() => {
803+ destroy ();
804+ window.delete (window.get_screen ().get_display ().get_current_time ());
805+ });
806+
807+ close_button.destroy ();
808+ icon.destroy ();
809+
810+ reposition ();
811+
812+ return true;
813+ }
814+
815+ public override bool enter_event (CrossingEvent event)
816+ {
817+ //if we're still animating don't show the close button
818+ if (get_animation () != null)
819+ return false;
820+
821+ close_button.visible = true;
822+ close_button.animate (AnimationMode.EASE_OUT_ELASTIC, 400, scale_x : 1.0f, scale_y : 1.0f);
823+
824+ return true;
825+ }
826+
827+ public override bool motion_event (MotionEvent event)
828+ {
829+ if (get_animation () != null)
830+ return false;
831+
832+ close_button.visible = true;
833+ close_button.animate (AnimationMode.EASE_OUT_ELASTIC, 400, scale_x : 1.0f, scale_y : 1.0f);
834+
835+ return true;
836+ }
837+
838+ public override bool leave_event (CrossingEvent event)
839+ {
840+ if (event.related == close_button)
841+ return false;
842+
843+ close_button.animate (AnimationMode.EASE_IN_QUAD, 200, scale_x : 0.0f, scale_y : 0.0f)
844+ .completed.connect (() => close_button.visible = false );
845+
846+ return true;
847+ }
848+
849+ public override bool button_press_event (ButtonEvent event)
850+ {
851+ get_parent ().set_child_above_sibling (this, null);
852+ selected (window);
853+
854+ return true;
855+ }
856+
857+ public void close (bool do_animate=true)
858+ {
859+ unowned Meta.Rectangle rect = window.get_outer_rect ();
860+
861+ //FIXME need to subtract 10 here to remove jump for most windows, but adds jump for maximized ones
862+ float delta = window.maximized_horizontally || window.maximized_vertically ? 0 : 10;
863+
864+ float dest_x = rect.x - delta;
865+ float dest_y = rect.y - delta;
866+
867+ //stop all running animations
868+ detach_animation ();
869+ icon.detach_animation ();
870+
871+ if (do_animate) {
872+ icon.animate (AnimationMode.EASE_IN_CUBIC, 100, scale_x:0.0f, scale_y:0.0f).completed.connect ( () => {
873+ icon.destroy ();
874+ });
875+
876+ animate (AnimationMode.EASE_IN_OUT_CUBIC, 300, scale_x:1.0f, scale_y:1.0f, x:dest_x, y:dest_y).completed.connect (() => {
877+ (window.get_compositor_private () as Actor).show ();
878+ destroy ();
879+ });
880+ } else {
881+ (window.get_compositor_private () as Actor).show ();
882+
883+ destroy ();
884+ icon.destroy ();
885+ }
886+
887+ close_button.destroy ();
888+ }
889+ }
890+}
891
892=== modified file 'src/Widgets/WorkspaceView.vala'
893--- src/Widgets/WorkspaceView.vala 2012-08-09 15:20:04 +0000
894+++ src/Widgets/WorkspaceView.vala 2012-08-27 21:04:19 +0000
895@@ -320,12 +320,12 @@
896
897 var screen = plugin.get_screen ();
898
899+ visible = true;
900+ grab_key_focus ();
901+
902 Utils.set_input_area (screen, InputArea.FULLSCREEN);
903 plugin.begin_modal ();
904
905- visible = true;
906- grab_key_focus ();
907-
908 if (wait) {
909 timeout = Timeout.add (1000, () => {
910 show_elements ();

Subscribers

People subscribed via source and target branches