Completed
Push — master ( 9d96a1...31b814 )
by Stephan
28s
created

WorkArea.get_struts()   B

Complexity

Conditions 6

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
c 1
b 0
f 0
dl 0
loc 17
rs 8
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.
0 ignored issues
show
Coding Style Naming introduced by
The name key does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

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.

Loading history...
Coding Style Naming introduced by
The name val does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

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.

Loading history...
26
for key, val in GRAVITY.items():
0 ignored issues
show
Bug introduced by
The Instance of EnumSafeDict does not seem to have a member named items.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
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)
0 ignored issues
show
Coding Style Naming introduced by
The name w does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

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.

Loading history...
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(
0 ignored issues
show
Coding Style Naming introduced by
The name _Sub does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

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.

Loading history...
89
            gtk.gdk.region_rectangle(g))
90
        _w, _h = self.gdk_screen.get_width(), self.gdk_screen.get_height()
0 ignored issues
show
Coding Style Naming introduced by
The name _w does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

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.

Loading history...
Coding Style Naming introduced by
The name _h does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

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.

Loading history...
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))
0 ignored issues
show
Coding Style Naming introduced by
The name _Sub does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

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.

Loading history...
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