Completed
Push — master ( 39bbc3...ba7203 )
by Stephan
49s
created

WindowManager.del_property()   A

Complexity

Conditions 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 8
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
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
MYPY = False
17
if MYPY:
18
    # pylint: disable=unused-import
19
    from typing import Any, List, Optional, Sequence, Tuple, Union  # NOQA
20
    from .util import Strut  # NOQA
21
del MYPY
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.
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...
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
    If C{keep_maximize=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)
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...
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(
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...
104
            gtk.gdk.region_rectangle(g))
105
        _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...
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))
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...
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_win_for_prop(self, window=None):
284
        # type: (Optional[wnck.Window]) -> gtk.gdk.Window
285
        """Retrieve a GdkWindow for a given WnckWindow, or the root if None."""
286
        if window:
287
            return gtk.gdk.window_foreign_new(window.get_xid())
288
        else:
289
            return self.gdk_screen.get_root_window()
290
291
    def get_property(self, key, window=None):
0 ignored issues
show
Comprehensibility Bug introduced by
key is re-defining a name which is already available in the outer-scope (previously defined on line 36).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
292
        # type: (str, Optional[wnck.Window]) -> Any
293
        """Retrieve the value of a property on the given window.
294
295
        @param window: If unset, the root window will be queried.
296
        @type window: C{wnck.Window} or C{None}
297
        """
298
        return self._get_win_for_prop(window).property_get(key)
299
300
    def set_property(self, key,   # type: str
0 ignored issues
show
Comprehensibility Bug introduced by
key is re-defining a name which is already available in the outer-scope (previously defined on line 36).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
301
                     value,       # type: Union[Sequence[int], int, str]
302
                     window=None  # type: Optional[wnck.Window]
303
                     ):           # type: (...) -> None
304
        """Set the value of a property on the given window.
305
306
        @param window: If unset, the root window will be queried.
307
        @type window: C{wnck.Window} or C{None}
308
        """
309
310
        if isinstance(value, basestring):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'basestring'
Loading history...
311
            prop_format = 8
312
            prop_type = "STRING"
313
        else:
314
            prop_format = 32
315
            prop_type = "CARDINAL"
316
            if isinstance(value, int):
317
                value = [value]
318
319
        self._get_win_for_prop(window).property_change(
320
            key, prop_type,
321
            prop_format, gtk.gdk.PROP_MODE_REPLACE, value)
322
323
    def del_property(self, key, window=None):
0 ignored issues
show
Comprehensibility Bug introduced by
key is re-defining a name which is already available in the outer-scope (previously defined on line 36).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
324
        # type: (str, Optional[wnck.Window]) -> None
325
        """Unset a property on the given window.
326
327
        @param window: If unset, the root window will be queried.
328
        @type window: C{wnck.Window} or C{None}
329
        """
330
        self._get_win_for_prop(window).property_delete(key)
331
332
    def get_relevant_windows(self, workspace):
333
        """C{wnck.Screen.get_windows} without WINDOW_DESKTOP/DOCK windows."""
334
335
        for window in self.screen.get_windows():
336
            # Skip windows on other virtual desktops for intuitiveness
337
            if workspace and not window.is_on_workspace(workspace):
338
                logging.debug("Skipping window on other workspace: %r", window)
339
                continue
340
341
            # Don't cycle elements of the desktop
342
            if not self.is_relevant(window):
343
                continue
344
345
            yield window
346
347
    def get_workspace(self,
348
                      window=None,      # type: wnck.Window
349
                      direction=None,   # type: wnck.MotionDirection
350
                      wrap_around=True  # type: bool
351
                      ):                # type: (...) -> wnck.Workspace
352
        """Get a workspace relative to either a window or the active one.
353
354
        @param window: The point of reference. C{None} for the active workspace
355
        @param direction: The direction in which to look, relative to the point
356
            of reference. Accepts the following types:
357
             - C{wnck.MotionDirection}: Non-cycling direction
358
             - C{int}: Relative index in the list of workspaces
359
             - C{None}: Just get the workspace object for the point of
360
               reference
361
        @param wrap_around: Whether relative indexes should wrap around.
362
363
        @type window: C{wnck.Window} or C{None}
364
        @type wrap_around: C{bool}
365
        @rtype: C{wnck.Workspace} or C{None}
366
        @returns: The workspace object or C{None} if no match could be found.
367
        """
368
        if window:
369
            cur = window.get_workspace()
370
        else:
371
            cur = self.screen.get_active_workspace()
372
373
        if not cur:
374
            return None  # It's either pinned or on no workspaces
375
376
        # pylint: disable=no-member
377
        if isinstance(direction, wnck.MotionDirection):
378
            nxt = cur.get_neighbor(direction)
379
        elif isinstance(direction, int):
380
            # TODO: Deduplicate with the wrapping code in commands.py
381
            n_spaces = self.screen.get_workspace_count()
382
383
            nxt = self.screen.get_workspace(
384
                clamp_idx(cur.get_number() + direction, n_spaces, wrap_around))
385
386
        elif direction is None:
387
            nxt = cur
388
        else:
389
            nxt = None
390
            logging.warn("Unrecognized direction: %r", direction)
391
392
        return nxt
393
394
    @staticmethod
395
    def is_relevant(window):
396
        # type: (wnck.Window) -> bool
397
        """Return False if the window should be ignored.
398
399
        (eg. If it's the desktop or a panel)
400
        """
401
        if not window:
402
            logging.debug("Received no window object to manipulate")
403
            return False
404
405
        if window.get_window_type() in [
406
                wnck.WINDOW_DESKTOP,  # pylint: disable=E1101
407
                wnck.WINDOW_DOCK]:    # pylint: disable=E1101
408
            logging.debug("Irrelevant window: %r", window)
409
            return False
410
411
        # TODO: Support customizations to exclude things like my Conky window
412
        # (Which I can't make a `desktop` window because I sometimes drag it)
413
414
        return True
415
416
    def reposition(self,
417
            win,                                    # type: wnck.Window
418
            geom=None,                              # type: Optional[Rectangle]
419
            monitor=Rectangle(0, 0, 0, 0),          # type: Rectangle
420
            keep_maximize=False,                    # type: bool
421
            gravity=wnck.WINDOW_GRAVITY_NORTHWEST,
422
            geometry_mask=wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y |
423
                wnck.WINDOW_CHANGE_WIDTH |
424
                wnck.WINDOW_CHANGE_HEIGHT  # type: wnck.WindowMoveResizeMask
425
                   ):  # pylint: disable=no-member,too-many-arguments
426
        # type: (...) -> None
427
        # TODO: Complete MyPy type signature
428
        # pylint:disable=line-too-long
429
        """
430
        Position and size a window, decorations inclusive, according to the
431
        provided target window and monitor geometry rectangles.
432
433
        If no monitor rectangle is specified, position relative to the desktop
434
        as a whole.
435
436
        @param win: The C{wnck.Window} to operate on.
437
        @param geom: The new geometry for the window. Can be left unspecified
438
            if the intent is to move the window to another monitor without
439
            repositioning it.
440
        @param monitor: The frame relative to which C{geom} should be
441
            interpreted. The whole desktop if unspecified.
442
        @param keep_maximize: Whether to re-maximize a maximized window after
443
            un-maximizing it to move it.
444
        @param gravity: A constant specifying which point on the window is
445
            referred to by the X and Y coordinates in C{geom}.
446
        @param geometry_mask: A set of flags determining which aspects of the
447
            requested geometry should actually be applied to the window.
448
            (Allows the same geometry definition to easily be shared between
449
            operations like move and resize.)
450
        @type win: C{wnck.Window}
451
        @type geom: C{gtk.gdk.Rectangle} or C{None}
452
        @type monitor: C{gtk.gdk.Rectangle}
453
        @type keep_maximize: C{bool}
454
        @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>}
455
        @type geometry_mask: U{WnckWindowMoveResizeMask<https://developer.gnome.org/libwnck/2.30/WnckWindow.html#WnckWindowMoveResizeMask>}
456
457
        @todo 1.0.0: Look for a way to accomplish this with a cleaner method
458
            signature. This is getting a little hairy. (API-breaking change)
459
        """  # NOQA
460
461
        # We need to ensure that ignored values are still present for
462
        # gravity calculations.
463
        old_geom = self.get_geometry_rel(win, self.get_monitor(win)[1])
464
        if geom:
465
            for attr in ('x', 'y', 'width', 'height'):
466
                if not geometry_mask & getattr(wnck,
467
                        'WINDOW_CHANGE_%s' % attr.upper()):
468
                    setattr(geom, attr, getattr(old_geom, attr))
469
        else:
470
            geom = old_geom
471
472
        with persist_maximization(win, keep_maximize):
473
            # Apply gravity and resolve to absolute desktop coordinates.
474
            new_x, new_y = self.calc_win_gravity(geom, gravity)
475
            new_x += monitor.x
476
            new_y += monitor.y
477
478
            logging.debug(" Repositioning to (%d, %d, %d, %d)\n",
479
                    new_x, new_y, geom.width, geom.height)
480
481
            # See the calc_win_gravity docstring for the rationale here
482
            win.set_geometry(wnck.WINDOW_GRAVITY_STATIC, geometry_mask,
483
                    new_x, new_y, geom.width, geom.height)
484