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