1
|
|
|
"""Wrapper around libwnck for interacting with the window manager""" |
2
|
|
|
|
3
|
|
|
__author__ = "Stephan Sokolow (deitarion/SSokolow)" |
4
|
|
|
__license__ = "GNU GPL 2.0 or later" |
5
|
|
|
|
6
|
|
|
import logging |
7
|
|
|
from contextlib import contextmanager |
8
|
|
|
|
9
|
|
|
import gtk.gdk, wnck # pylint: disable=import-error |
10
|
|
|
from gtk.gdk import Rectangle # pylint: disable=import-error |
11
|
|
|
|
12
|
|
|
from .util import clamp_idx, EnumSafeDict, XInitError |
13
|
|
|
|
14
|
|
|
# Allow MyPy to work without depending on the `typing` package |
15
|
|
|
# (And silence complaints from only using the imported types in comments) |
16
|
|
|
try: |
17
|
|
|
# pylint: disable=unused-import |
18
|
|
|
from typing import List, Optional, Sequence, Tuple # NOQA |
19
|
|
|
from .util import Strut # NOQA |
20
|
|
|
except: # pylint: disable=bare-except |
21
|
|
|
pass |
|
|
|
|
22
|
|
|
|
23
|
|
|
#: Lookup table for internal window gravity support. |
24
|
|
|
#: (libwnck's support is either unreliable or broken) |
25
|
|
|
GRAVITY = EnumSafeDict({ |
26
|
|
|
'NORTH_WEST': (0.0, 0.0), |
27
|
|
|
'NORTH': (0.5, 0.0), |
28
|
|
|
'NORTH_EAST': (1.0, 0.0), |
29
|
|
|
'WEST': (0.0, 0.5), |
30
|
|
|
'CENTER': (0.5, 0.5), |
31
|
|
|
'EAST': (1.0, 0.5), |
32
|
|
|
'SOUTH_WEST': (0.0, 1.0), |
33
|
|
|
'SOUTH': (0.5, 1.0), |
34
|
|
|
'SOUTH_EAST': (1.0, 1.0), |
35
|
|
|
}) |
36
|
|
|
key, val = None, None # Safety cushion for the "del" line. |
|
|
|
|
37
|
|
|
for key, val in GRAVITY.items(): |
|
|
|
|
38
|
|
|
# Support GDK gravity constants |
39
|
|
|
GRAVITY[getattr(gtk.gdk, 'GRAVITY_%s' % key)] = val |
40
|
|
|
|
41
|
|
|
# Support libwnck gravity constants |
42
|
|
|
_name = 'WINDOW_GRAVITY_%s' % key.replace('_', '') |
43
|
|
|
GRAVITY[getattr(wnck, _name)] = val |
44
|
|
|
|
45
|
|
|
# Prevent these temporary variables from showing up in the apidocs |
46
|
|
|
del _name, key, val |
47
|
|
|
|
48
|
|
|
# --- |
49
|
|
|
|
50
|
|
|
@contextmanager |
51
|
|
|
def persist_maximization(win, keep_maximize=True): |
52
|
|
|
"""Context manager to persist maximization state after a reposition |
53
|
|
|
|
54
|
|
|
@param keep_maximize: If C{False}, this becomes a no-op to ease writing |
55
|
|
|
clean code which needs to support both behaviours. |
56
|
|
|
""" |
57
|
|
|
# Unmaximize and record the types we may need to restore |
58
|
|
|
max_types, maxed = ['', '_horizontally', '_vertically'], [] |
59
|
|
|
for maxtype in max_types: |
60
|
|
|
if getattr(win, 'is_maximized' + maxtype)(): |
61
|
|
|
maxed.append(maxtype) |
62
|
|
|
getattr(win, 'unmaximize' + maxtype)() |
63
|
|
|
|
64
|
|
|
yield |
65
|
|
|
|
66
|
|
|
# Restore maximization if asked |
67
|
|
|
if maxed and keep_maximize: |
68
|
|
|
for maxtype in maxed: |
69
|
|
|
getattr(win, 'maximize' + maxtype)() |
70
|
|
|
|
71
|
|
|
|
72
|
|
|
class WorkArea(object): |
73
|
|
|
"""Helper to calculate and query available workarea on the desktop.""" |
74
|
|
|
def __init__(self, gdk_screen, ignore_struts=False): |
75
|
|
|
# type: (gtk.gdk.Screen, bool) -> None |
76
|
|
|
self.gdk_screen = gdk_screen |
77
|
|
|
self.ignore_struts = ignore_struts |
78
|
|
|
|
79
|
|
|
def get_struts(self, root_win): # type: (gtk.gdk.Window) -> List[Strut] |
80
|
|
|
"""Retrieve the struts from the root window if supported.""" |
81
|
|
|
if not self.gdk_screen.supports_net_wm_hint("_NET_WM_STRUT_PARTIAL"): |
82
|
|
|
return [] |
83
|
|
|
|
84
|
|
|
# Gather all struts |
85
|
|
|
struts = [root_win.property_get("_NET_WM_STRUT_PARTIAL")] |
86
|
|
|
if self.gdk_screen.supports_net_wm_hint("_NET_CLIENT_LIST"): |
87
|
|
|
# Source: http://stackoverflow.com/a/11332614/435253 |
88
|
|
|
for wid in root_win.property_get('_NET_CLIENT_LIST')[2]: |
89
|
|
|
w = gtk.gdk.window_foreign_new(wid) |
|
|
|
|
90
|
|
|
struts.append(w.property_get("_NET_WM_STRUT_PARTIAL")) |
91
|
|
|
struts = [tuple(x[2]) for x in struts if x] |
92
|
|
|
|
93
|
|
|
logging.debug("Gathered _NET_WM_STRUT_PARTIAL values:\n\t%s", |
94
|
|
|
struts) |
95
|
|
|
return struts |
96
|
|
|
|
97
|
|
|
def subtract_struts(self, usable_region, # type: gtk.gdk.Region |
98
|
|
|
struts # type: Sequence[Strut] |
99
|
|
|
): # type: (...) -> Tuple[gtk.gdk.Region, Rectangle] |
100
|
|
|
"""Subtract the given struts from the given region.""" |
101
|
|
|
|
102
|
|
|
# Subtract the struts from the usable region |
103
|
|
|
_Sub = lambda *g: usable_region.subtract( |
|
|
|
|
104
|
|
|
gtk.gdk.region_rectangle(g)) |
105
|
|
|
_w, _h = self.gdk_screen.get_width(), self.gdk_screen.get_height() |
|
|
|
|
106
|
|
|
for g in struts: # pylint: disable=invalid-name |
107
|
|
|
# http://standards.freedesktop.org/wm-spec/1.5/ar01s05.html |
108
|
|
|
# XXX: Must not cache unless watching for notify events. |
109
|
|
|
_Sub(0, g[4], g[0], g[5] - g[4] + 1) # left |
110
|
|
|
_Sub(_w - g[1], g[6], g[1], g[7] - g[6] + 1) # right |
111
|
|
|
_Sub(g[8], 0, g[9] - g[8] + 1, g[2]) # top |
112
|
|
|
_Sub(g[10], _h - g[3], g[11] - g[10] + 1, g[3]) # bottom |
113
|
|
|
|
114
|
|
|
# Generate a more restrictive version used as a fallback |
115
|
|
|
usable_rect = usable_region.copy() |
116
|
|
|
_Sub = lambda *g: usable_rect.subtract(gtk.gdk.region_rectangle(g)) |
|
|
|
|
117
|
|
|
for geom in struts: |
118
|
|
|
# http://standards.freedesktop.org/wm-spec/1.5/ar01s05.html |
119
|
|
|
# XXX: Must not cache unless watching for notify events. |
120
|
|
|
_Sub(0, geom[4], geom[0], _h) # left |
121
|
|
|
_Sub(_w - geom[1], geom[6], geom[1], _h) # right |
122
|
|
|
_Sub(0, 0, _w, geom[2]) # top |
123
|
|
|
_Sub(0, _h - geom[3], _w, geom[3]) # bottom |
124
|
|
|
# TODO: The required "+ 1" in certain spots confirms that we're |
125
|
|
|
# going to need unit tests which actually check that the |
126
|
|
|
# WM's code for constraining windows to the usable area |
127
|
|
|
# doesn't cause off-by-one bugs. |
128
|
|
|
# TODO: Share this on http://stackoverflow.com/q/2598580/435253 |
129
|
|
|
return usable_rect.get_clipbox(), usable_region |
130
|
|
|
|
131
|
|
|
def get(self, monitor, ignore_struts=None): |
132
|
|
|
# type: (Rectangle, bool) -> Tuple[gtk.gdk.Region, Rectangle] |
133
|
|
|
"""Retrieve the usable area of the specified monitor using |
134
|
|
|
the most expressive method the window manager supports. |
135
|
|
|
|
136
|
|
|
@param monitor: The number or dimensions of the desired monitor. |
137
|
|
|
@param ignore_struts: If C{True}, just return the size of the whole |
138
|
|
|
monitor, allowing windows to overlap panels. |
139
|
|
|
|
140
|
|
|
@type monitor: C{gtk.gdk.Rectangle} |
141
|
|
|
@type ignore_struts: C{bool} |
142
|
|
|
|
143
|
|
|
@returns: The usable region and its largest rectangular subset. |
144
|
|
|
@rtype: C{gtk.gdk.Region}, C{gtk.gdk.Rectangle} |
145
|
|
|
""" |
146
|
|
|
|
147
|
|
|
# Get the region and return failure early if it's empty |
148
|
|
|
usable_rect, usable_region = monitor, gtk.gdk.region_rectangle(monitor) |
149
|
|
|
if not usable_region.get_rectangles(): |
150
|
|
|
logging.error("WorkArea.get_monitor_rect received " |
151
|
|
|
"an empty monitor region!") |
152
|
|
|
return None, None |
153
|
|
|
|
154
|
|
|
# Return early if asked to ignore struts |
155
|
|
|
if ignore_struts or (ignore_struts is None and self.ignore_struts): |
156
|
|
|
logging.debug("Panels ignored. Reported monitor geometry is:\n%s", |
157
|
|
|
usable_rect) |
158
|
|
|
return usable_region, usable_rect |
159
|
|
|
|
160
|
|
|
# Get the list of struts from the root window |
161
|
|
|
root_win = self.gdk_screen.get_root_window() |
162
|
|
|
struts = self.get_struts(root_win) |
163
|
|
|
|
164
|
|
|
# Fall back to _NET_WORKAREA if we couldn't get any struts |
165
|
|
|
if struts: |
166
|
|
|
usable_rect, usable_region = self.subtract_struts(usable_region, |
167
|
|
|
struts) |
168
|
|
|
elif self.gdk_screen.supports_net_wm_hint("_NET_WORKAREA"): |
169
|
|
|
desktop_geo = tuple(root_win.property_get('_NET_WORKAREA')[2][0:4]) |
170
|
|
|
logging.debug("Falling back to _NET_WORKAREA: %s", desktop_geo) |
171
|
|
|
usable_region.intersect(gtk.gdk.region_rectangle(desktop_geo)) |
172
|
|
|
usable_rect = usable_region.get_clipbox() |
173
|
|
|
|
174
|
|
|
# FIXME: Only call get_rectangles if --debug |
175
|
|
|
logging.debug("Usable region of monitor calculated as:\n" |
176
|
|
|
"\tRegion: %r\n\tRectangle: %r", |
177
|
|
|
usable_region.get_rectangles(), usable_rect) |
178
|
|
|
return usable_region, usable_rect |
179
|
|
|
|
180
|
|
|
|
181
|
|
|
class WindowManager(object): |
182
|
|
|
"""A simple API-wrapper class for manipulating window positioning.""" |
183
|
|
|
|
184
|
|
|
def __init__(self, screen=None, ignore_workarea=False): |
185
|
|
|
# type: (gtk.gdk.Screen, bool) -> None |
186
|
|
|
""" |
187
|
|
|
Initializes C{WindowManager}. |
188
|
|
|
|
189
|
|
|
@param screen: The X11 screen to operate on. If C{None}, the default |
190
|
|
|
screen as retrieved by C{gtk.gdk.screen_get_default} will be used. |
191
|
|
|
@type screen: C{gtk.gdk.Screen} |
192
|
|
|
|
193
|
|
|
@todo: Confirm that the root window only changes on X11 server |
194
|
|
|
restart. (Something which will crash QuickTile anyway since |
195
|
|
|
PyGTK makes X server disconnects uncatchable.) |
196
|
|
|
|
197
|
|
|
It could possibly change while toggling "allow desktop icons" |
198
|
|
|
in KDE 3.x. (Not sure what would be equivalent elsewhere) |
199
|
|
|
""" |
200
|
|
|
self.gdk_screen = screen or gtk.gdk.screen_get_default() |
201
|
|
|
if self.gdk_screen is None: |
202
|
|
|
raise XInitError("GTK+ could not open a connection to the X server" |
203
|
|
|
" (bad DISPLAY value?)") |
204
|
|
|
|
205
|
|
|
# pylint: disable=no-member |
206
|
|
|
self.screen = wnck.screen_get(self.gdk_screen.get_number()) |
207
|
|
|
self.workarea = WorkArea(self.gdk_screen, |
208
|
|
|
ignore_struts=ignore_workarea) |
209
|
|
|
|
210
|
|
|
@staticmethod |
211
|
|
|
def calc_win_gravity(geom, gravity): |
212
|
|
|
# (Rectangle, Tuple[float, float]) -> Tuple[int, int] |
213
|
|
|
"""Calculate the X and Y coordinates necessary to simulate non-topleft |
214
|
|
|
gravity on a window. |
215
|
|
|
|
216
|
|
|
@param geom: The window geometry to which to apply the corrections. |
217
|
|
|
@param gravity: A desired gravity chosen from L{GRAVITY}. |
218
|
|
|
@type geom: C{gtk.gdk.Rectangle} |
219
|
|
|
@type gravity: C{wnck.WINDOW_GRAVITY_*} or C{gtk.gdk.GRAVITY_*} |
220
|
|
|
|
221
|
|
|
@returns: The coordinates to be used to achieve the desired position. |
222
|
|
|
@rtype: C{(x, y)} |
223
|
|
|
|
224
|
|
|
This exists because, for whatever reason, whether it's wnck, Openbox, |
225
|
|
|
or both at fault, the WM's support for window gravities seems to have |
226
|
|
|
no effect beyond double-compensating for window border thickness unless |
227
|
|
|
using WINDOW_GRAVITY_STATIC. |
228
|
|
|
|
229
|
|
|
My best guess is that the gravity modifiers are being applied to the |
230
|
|
|
window frame rather than the window itself, hence static gravity would |
231
|
|
|
position correctly and north-west gravity would double-compensate for |
232
|
|
|
the titlebar and border dimensions. |
233
|
|
|
|
234
|
|
|
...however, that still doesn't explain why the non-topleft gravities |
235
|
|
|
have no effect. I'm guessing something's just broken. |
236
|
|
|
""" |
237
|
|
|
grav_x, grav_y = GRAVITY[gravity] |
238
|
|
|
|
239
|
|
|
return ( |
240
|
|
|
int(geom.x - (geom.width * grav_x)), |
241
|
|
|
int(geom.y - (geom.height * grav_y)) |
242
|
|
|
) |
243
|
|
|
|
244
|
|
|
@staticmethod |
245
|
|
|
def get_geometry_rel(window, monitor_geom): |
246
|
|
|
# type: (wnck.Window, Rectangle) -> Rectangle |
247
|
|
|
"""Get window position relative to the monitor rather than the desktop. |
248
|
|
|
|
249
|
|
|
@param monitor_geom: The rectangle returned by |
250
|
|
|
C{gdk.Screen.get_monitor_geometry} |
251
|
|
|
@type window: C{wnck.Window} |
252
|
|
|
@type monitor_geom: C{gtk.gdk.Rectangle} |
253
|
|
|
|
254
|
|
|
@rtype: C{gtk.gdk.Rectangle} |
255
|
|
|
""" |
256
|
|
|
win_geom = Rectangle(*window.get_geometry()) |
257
|
|
|
win_geom.x -= monitor_geom.x |
258
|
|
|
win_geom.y -= monitor_geom.y |
259
|
|
|
|
260
|
|
|
return win_geom |
261
|
|
|
|
262
|
|
|
def get_monitor(self, win): |
263
|
|
|
# type: (wnck.Window) -> Tuple[int, Rectangle] |
264
|
|
|
"""Given a C{wnck.Window}, retrieve the monitor ID and geometry. |
265
|
|
|
|
266
|
|
|
@type win: C{wnck.Window} |
267
|
|
|
@returns: A tuple containing the monitor ID and geometry. |
268
|
|
|
@rtype: C{(int, gtk.gdk.Rectangle)} |
269
|
|
|
""" |
270
|
|
|
# TODO: Look for a way to get the monitor ID without having |
271
|
|
|
# to instantiate a gtk.gdk.Window |
272
|
|
|
if not isinstance(win, gtk.gdk.Window): |
273
|
|
|
win = gtk.gdk.window_foreign_new(win.get_xid()) |
274
|
|
|
|
275
|
|
|
# TODO: How do I retrieve the root window from a given one? |
276
|
|
|
monitor_id = self.gdk_screen.get_monitor_at_window(win) |
277
|
|
|
monitor_geom = self.gdk_screen.get_monitor_geometry(monitor_id) |
278
|
|
|
|
279
|
|
|
logging.debug(" Window is on monitor %s, which has geometry %s", |
280
|
|
|
monitor_id, monitor_geom) |
281
|
|
|
return monitor_id, monitor_geom |
282
|
|
|
|
283
|
|
|
def get_relevant_windows(self, workspace): |
284
|
|
|
"""C{wnck.Screen.get_windows} without WINDOW_DESKTOP/DOCK windows.""" |
285
|
|
|
|
286
|
|
|
for window in self.screen.get_windows(): |
287
|
|
|
# Skip windows on other virtual desktops for intuitiveness |
288
|
|
|
if workspace and not window.is_on_workspace(workspace): |
289
|
|
|
logging.debug("Skipping window on other workspace: %r", window) |
290
|
|
|
continue |
291
|
|
|
|
292
|
|
|
# Don't cycle elements of the desktop |
293
|
|
|
if not self.is_relevant(window): |
294
|
|
|
continue |
295
|
|
|
|
296
|
|
|
yield window |
297
|
|
|
|
298
|
|
|
def get_workspace(self, |
299
|
|
|
window=None, # type: wnck.Window |
300
|
|
|
direction=None, # type: wnck.MotionDirection |
301
|
|
|
wrap_around=True # type: bool |
302
|
|
|
): # type: (...) -> wnck.Workspace |
303
|
|
|
"""Get a workspace relative to either a window or the active one. |
304
|
|
|
|
305
|
|
|
@param window: The point of reference. C{None} for the active workspace |
306
|
|
|
@param direction: The direction in which to look, relative to the point |
307
|
|
|
of reference. Accepts the following types: |
308
|
|
|
- C{wnck.MotionDirection}: Non-cycling direction |
309
|
|
|
- C{int}: Relative index in the list of workspaces |
310
|
|
|
- C{None}: Just get the workspace object for the point of |
311
|
|
|
reference |
312
|
|
|
@param wrap_around: Whether relative indexes should wrap around. |
313
|
|
|
|
314
|
|
|
@type window: C{wnck.Window} or C{None} |
315
|
|
|
@type wrap_around: C{bool} |
316
|
|
|
@rtype: C{wnck.Workspace} or C{None} |
317
|
|
|
@returns: The workspace object or C{None} if no match could be found. |
318
|
|
|
""" |
319
|
|
|
if window: |
320
|
|
|
cur = window.get_workspace() |
321
|
|
|
else: |
322
|
|
|
cur = self.screen.get_active_workspace() |
323
|
|
|
|
324
|
|
|
if not cur: |
325
|
|
|
return None # It's either pinned or on no workspaces |
326
|
|
|
|
327
|
|
|
# pylint: disable=no-member |
328
|
|
|
if isinstance(direction, wnck.MotionDirection): |
329
|
|
|
nxt = cur.get_neighbor(direction) |
330
|
|
|
elif isinstance(direction, int): |
331
|
|
|
# TODO: Deduplicate with the wrapping code in commands.py |
332
|
|
|
n_spaces = self.screen.get_workspace_count() |
333
|
|
|
|
334
|
|
|
nxt = self.screen.get_workspace( |
335
|
|
|
clamp_idx(cur.get_number() + direction, n_spaces, wrap_around)) |
336
|
|
|
|
337
|
|
|
elif direction is None: |
338
|
|
|
nxt = cur |
339
|
|
|
else: |
340
|
|
|
nxt = None |
341
|
|
|
logging.warn("Unrecognized direction: %r", direction) |
342
|
|
|
|
343
|
|
|
return nxt |
344
|
|
|
|
345
|
|
|
def is_relevant(self, window): |
|
|
|
|
346
|
|
|
# type: (wnck.Window) -> bool |
347
|
|
|
"""Return False if the window should be ignored. |
348
|
|
|
|
349
|
|
|
(eg. If it's the desktop or a panel) |
350
|
|
|
""" |
351
|
|
|
if not window: |
352
|
|
|
logging.debug("Received no window object to manipulate") |
353
|
|
|
return False |
354
|
|
|
|
355
|
|
|
if window.get_window_type() in [ |
356
|
|
|
wnck.WINDOW_DESKTOP, # pylint: disable=E1101 |
357
|
|
|
wnck.WINDOW_DOCK]: # pylint: disable=E1101 |
358
|
|
|
logging.debug("Irrelevant window: %r", window) |
359
|
|
|
return False |
360
|
|
|
return True |
361
|
|
|
|
362
|
|
|
def reposition(self, |
363
|
|
|
win, # type: wnck.Window |
364
|
|
|
geom=None, # type: Optional[Rectangle] |
365
|
|
|
monitor=Rectangle(0, 0, 0, 0), # type: Rectangle |
366
|
|
|
keep_maximize=False, # type: bool |
367
|
|
|
gravity=wnck.WINDOW_GRAVITY_NORTHWEST, |
368
|
|
|
geometry_mask=wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y | |
369
|
|
|
wnck.WINDOW_CHANGE_WIDTH | |
370
|
|
|
wnck.WINDOW_CHANGE_HEIGHT # type: wnck.WindowMoveResizeMask |
371
|
|
|
): # pylint: disable=no-member,too-many-arguments |
372
|
|
|
# type: (...) -> None |
373
|
|
|
# TODO: Complete MyPy type signature |
374
|
|
|
# pylint:disable=line-too-long |
375
|
|
|
""" |
376
|
|
|
Position and size a window, decorations inclusive, according to the |
377
|
|
|
provided target window and monitor geometry rectangles. |
378
|
|
|
|
379
|
|
|
If no monitor rectangle is specified, position relative to the desktop |
380
|
|
|
as a whole. |
381
|
|
|
|
382
|
|
|
@param win: The C{wnck.Window} to operate on. |
383
|
|
|
@param geom: The new geometry for the window. Can be left unspecified |
384
|
|
|
if the intent is to move the window to another monitor without |
385
|
|
|
repositioning it. |
386
|
|
|
@param monitor: The frame relative to which C{geom} should be |
387
|
|
|
interpreted. The whole desktop if unspecified. |
388
|
|
|
@param keep_maximize: Whether to re-maximize a maximized window after |
389
|
|
|
un-maximizing it to move it. |
390
|
|
|
@param gravity: A constant specifying which point on the window is |
391
|
|
|
referred to by the X and Y coordinates in C{geom}. |
392
|
|
|
@param geometry_mask: A set of flags determining which aspects of the |
393
|
|
|
requested geometry should actually be applied to the window. |
394
|
|
|
(Allows the same geometry definition to easily be shared between |
395
|
|
|
operations like move and resize.) |
396
|
|
|
@type win: C{wnck.Window} |
397
|
|
|
@type geom: C{gtk.gdk.Rectangle} or C{None} |
398
|
|
|
@type monitor: C{gtk.gdk.Rectangle} |
399
|
|
|
@type keep_maximize: C{bool} |
400
|
|
|
@type gravity: U{WnckWindowGravity<https://developer.gnome.org/libwnck/stable/WnckWindow.html#WnckWindowGravity>} or U{GDK Gravity Constant<http://www.pygtk.org/docs/pygtk/gdk-constants.html#gdk-gravity-constants>} |
401
|
|
|
@type geometry_mask: U{WnckWindowMoveResizeMask<https://developer.gnome.org/libwnck/2.30/WnckWindow.html#WnckWindowMoveResizeMask>} |
402
|
|
|
|
403
|
|
|
@todo 1.0.0: Look for a way to accomplish this with a cleaner method |
404
|
|
|
signature. This is getting a little hairy. (API-breaking change) |
405
|
|
|
""" # NOQA |
406
|
|
|
|
407
|
|
|
# We need to ensure that ignored values are still present for |
408
|
|
|
# gravity calculations. |
409
|
|
|
old_geom = self.get_geometry_rel(win, self.get_monitor(win)[1]) |
410
|
|
|
if geom: |
411
|
|
|
for attr in ('x', 'y', 'width', 'height'): |
412
|
|
|
if not geometry_mask & getattr(wnck, |
413
|
|
|
'WINDOW_CHANGE_%s' % attr.upper()): |
414
|
|
|
setattr(geom, attr, getattr(old_geom, attr)) |
415
|
|
|
else: |
416
|
|
|
geom = old_geom |
417
|
|
|
|
418
|
|
|
with persist_maximization(win, keep_maximize): |
419
|
|
|
# Apply gravity and resolve to absolute desktop coordinates. |
420
|
|
|
new_x, new_y = self.calc_win_gravity(geom, gravity) |
421
|
|
|
new_x += monitor.x |
422
|
|
|
new_y += monitor.y |
423
|
|
|
|
424
|
|
|
logging.debug(" Repositioning to (%d, %d, %d, %d)\n", |
425
|
|
|
new_x, new_y, geom.width, geom.height) |
426
|
|
|
|
427
|
|
|
# See the calc_win_gravity docstring for the rationale here |
428
|
|
|
win.set_geometry(wnck.WINDOW_GRAVITY_STATIC, geometry_mask, |
429
|
|
|
new_x, new_y, geom.width, geom.height) |
430
|
|
|
|
Except handlers which only contain
pass
and do not have anelse
clause can usually simply be removed: