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