Completed
Push — master ( 93dd71...36e57a )
by Stephan
32s
created

WindowManager.is_relevant()   A

Complexity

Conditions 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 16
rs 9.4285
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
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
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.
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...
36
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...
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)
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...
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(
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...
81
            gtk.gdk.region_rectangle(g))
82
        _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...
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))
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...
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):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
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