Merge lp:~gala-dev/gala/expose-natural into lp:gala
- expose-natural
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Scale feature in Gala for Luna
(Medium)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Rico Tzschichholz | Approve | ||
Review via email: mp+121001@code.launchpad.net |
Commit message
Description of the change
This replaces the old expose branch with a new one using the algorithm found in the natural-
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.
Cassidy James Blaede (cassidyjames) wrote : | # |
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
- 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
Tom Beckmann (tombeckmann) wrote : | # |
Should this renaming happen code intern as well? Or just on the user front?
- 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
Rico Tzschichholz (ricotz) : | # |
- 211. By Rico Tzschichholz
-
grid-placement: avoid empty window-slot which would make it look weird
Preview Diff
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 (); |
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 window- placement Gnome Shell extension (which is a /code.launchpad .net/~gala- dev/gala/ expose- natural/ +merge/ 121001 /code.launchpad .net/~gala- dev/gala/ expose- natural/ +merge/ 121001
> been updated.
>
> Description changed to:
>
> This replaces the old expose branch with a new one using the algorithm
> found in the natural-
> 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:/
> --
> https:/
> Your team Gala developers is requested to review the proposed merge of
> lp:~gala-dev/gala/expose-natural into lp:gala.
>