Completed
Push — master ( 381858...abd618 )
by Stephan
36s
created

cycle_monitors_all()   B

Complexity

Conditions 5

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
c 1
b 0
f 0
dl 0
loc 33
rs 8.0894
1
"""Available window-management commands"""
2
3
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
4
__license__ = "GNU GPL 2.0 or later"
5
6
import logging, time
7
from functools import wraps
8
from heapq import heappop, heappush
9
10
import gtk.gdk, wnck  # pylint: disable=import-error
11
12
from .util import fmt_table
13
14
class CommandRegistry(object):
15
    """Handles lookup and boilerplate for window management commands.
16
17
    Separated from WindowManager so its lifecycle is not tied to a specific
18
    GDK Screen object.
19
    """
20
21
    def __init__(self):
22
        self.commands = {}
23
        self.help = {}
24
25
    def __iter__(self):
26
        for x in self.commands:
0 ignored issues
show
Coding Style Naming introduced by
The name x 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...
27
            yield x
28
29
    def __str__(self):
30
        return fmt_table(self.help, ('Known Commands', 'desc'), group_by=1)
31
32
    def add(self, name, *p_args, **p_kwargs):
33
        """Decorator to wrap a function in boilerplate and add it to the
34
            command registry under the given name.
35
36
            @param name: The name to know the command by.
37
            @param p_args: Positional arguments to prepend to all calls made
38
                via C{name}.
39
            @param p_kwargs: Keyword arguments to prepend to all calls made
40
                via C{name}.
41
42
            @type name: C{str}
43
            """
44
45
        def decorate(func):
46
            """Closure used to allow decorator to take arguments"""
47
            @wraps(func)
48
            # pylint: disable=missing-docstring
49
            def wrapper(winman, window=None, *args, **kwargs):
50
51
                # Get Wnck and GDK window objects
52
                window = window or winman.screen.get_active_window()
53
                if isinstance(window, gtk.gdk.Window):
54
                    win = wnck.window_get(window.xid)  # pylint: disable=E1101
55
                else:
56
                    win = window
57
58
                # pylint: disable=no-member
59
                if not win:
60
                    logging.debug("Received no window object to manipulate.")
61
                    return None
62
                elif win.get_window_type() == wnck.WINDOW_DESKTOP:
63
                    logging.debug("Received desktop window object. Ignoring.")
64
                    return None
65
                else:
66
                    # FIXME: Make calls to win.get_* lazy in case --debug
67
                    #        wasn't passed.
68
                    logging.debug("Operating on window 0x%x with title \"%s\" "
69
                                  "and geometry %r",
70
                                  win.get_xid(), win.get_name(),
71
                                  win.get_geometry())
72
73
                monitor_id, monitor_geom = winman.get_monitor(window)
74
75
                use_area, use_rect = winman.get_workarea(
76
                    monitor_geom, winman.ignore_workarea)
77
78
                # TODO: Replace this MPlayer safety hack with a properly
79
                #       comprehensive exception catcher.
80
                if not use_rect:
81
                    logging.debug("Received a worthless value for largest "
82
                                  "rectangular subset of desktop (%r). Doing "
83
                                  "nothing.", use_rect)
84
                    return None
85
86
                state = {
87
                    "cmd_name": name,
88
                    "monitor_id": monitor_id,
89
                    "monitor_geom": monitor_geom,
90
                    "usable_region": use_area,
91
                    "usable_rect": use_rect,
92
                }
93
94
                args, kwargs = p_args + args, dict(p_kwargs, **kwargs)
95
                func(winman, win, state, *args, **kwargs)
96
97
            if name in self.commands:
98
                logging.warn("Redefining existing command: %s", name)
99
            self.commands[name] = wrapper
100
101
            help_str = func.__doc__.strip().split('\n')[0].split('. ')[0]
102
            self.help[name] = help_str.strip('.')
103
104
            # Return the unwrapped function so decorators can be stacked
105
            # to define multiple commands using the same code with different
106
            # arguments
107
            return func
108
        return decorate
109
110
    def add_many(self, command_map):
111
        """Convenience decorator to allow many commands to be defined from
112
           the same function with different arguments.
113
114
           @param command_map: A dict mapping command names to argument lists.
115
           @type command_map: C{dict}
116
           """
117
        def decorate(func):
118
            """Closure used to allow decorator to take arguments"""
119
            for cmd, arglist in command_map.items():
120
                self.add(cmd, *arglist)(func)
121
            return func
122
        return decorate
123
124
    def call(self, command, winman, *args, **kwargs):
125
        """Resolve a textual positioning command and execute it."""
126
        cmd = self.commands.get(command, None)
127
128
        if cmd:
129
            logging.debug("Executing command '%s' with arguments %r, %r",
130
                          command, args, kwargs)
131
            cmd(winman, *args, **kwargs)
132
        else:
133
            logging.error("Unrecognized command: %s", command)
134
135
136
#: The instance of L{CommandRegistry} to be used in 99.9% of use cases.
137
commands = CommandRegistry()
1 ignored issue
show
Coding Style Naming introduced by
The name commands 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...
138
139
def cycle_dimensions(winman, win, state, *dimensions):
140
    """Cycle the active window through a list of positions and shapes.
141
142
    Takes one step each time this function is called.
143
144
    If the window's dimensions are not within 100px (by euclidean distance)
145
    of an entry in the list, set them to the first list entry.
146
147
    @param dimensions: A list of tuples representing window geometries as
148
        floating-point values between 0 and 1, inclusive.
149
    @type dimensions: C{[(x, y, w, h), ...]}
150
    @type win: C{gtk.gdk.Window}
151
152
    @returns: The new window dimensions.
153
    @rtype: C{gtk.gdk.Rectangle}
154
    """
155
    win_geom = winman.get_geometry_rel(win, state['monitor_geom'])
156
    usable_region = state['usable_region']
157
158
    # Get the bounding box for the usable region (overlaps panels which
159
    # don't fill 100% of their edge of the screen)
160
    clip_box = usable_region.get_clipbox()
161
162
    logging.debug("Selected preset sequence:\n\t%r", dimensions)
163
164
    # Resolve proportional (eg. 0.5) and preserved (None) coordinates
165
    dims = []
166
    for tup in dimensions:
167
        current_dim = []
168
        for pos, val in enumerate(tup):
169
            if val is None:
170
                current_dim.append(tuple(win_geom)[pos])
171
            else:
172
                # FIXME: This is a bit of an ugly way to get (w, h, w, h)
173
                # from clip_box.
174
                current_dim.append(int(val * tuple(clip_box)[2 + pos % 2]))
175
176
        dims.append(current_dim)
177
178
    if not dims:
179
        return None
180
181
    logging.debug("Selected preset sequence resolves to these monitor-relative"
182
                  " pixel dimensions:\n\t%r", dims)
183
184
    # Calculate euclidean distances between the window's current geometry
185
    # and all presets and store them in a min heap.
186
    euclid_distance = []
187
    for pos, val in enumerate(dims):
188
        distance = sum([(wg - vv) ** 2 for (wg, vv)
189
                        in zip(tuple(win_geom), tuple(val))]) ** 0.5
190
        heappush(euclid_distance, (distance, pos))
191
192
    # If the window is already on one of the configured geometries, advance
193
    # to the next configuration. Otherwise, use the first configuration.
194
    min_distance = heappop(euclid_distance)
195
    if float(min_distance[0]) / tuple(clip_box)[2] < 0.1:
196
        pos = (min_distance[1] + 1) % len(dims)
197
    else:
198
        pos = 0
199
    result = gtk.gdk.Rectangle(*dims[pos])
200
201
    logging.debug("Target preset is %s relative to monitor %s",
202
                  result, clip_box)
203
    result.x += clip_box.x
204
    result.y += clip_box.y
205
206
    # If we're overlapping a panel, fall back to a monitor-specific
207
    # analogue to _NET_WORKAREA to prevent overlapping any panels and
208
    # risking the WM potentially meddling with the result of resposition()
209
    if not usable_region.rect_in(result) == gtk.gdk.OVERLAP_RECTANGLE_IN:
210
        result = result.intersect(state['usable_rect'])
211
        logging.debug("Result exceeds usable (non-rectangular) region of "
212
                      "desktop. (overlapped a non-fullwidth panel?) Reducing "
213
                      "to within largest usable rectangle: %s",
214
                      state['usable_rect'])
215
216
    logging.debug("Calling reposition() with default gravity and dimensions "
217
                  "%r", tuple(result))
218
    winman.reposition(win, result)
219
    return result
220
221
@commands.add('monitor-switch')
222
@commands.add('monitor-next', 1)
223
@commands.add('monitor-prev', -1)
224
def cycle_monitors(winman, win, state, step=1):
225
    """Cycle the active window between monitors while preserving position.
226
227
    @todo 1.0.0: Remove C{monitor-switch} in favor of C{monitor-next}
228
        (API-breaking change)
229
    """
230
    mon_id = state['monitor_id']
231
    new_mon_id = (mon_id + step) % winman.gdk_screen.get_n_monitors()
232
233
    new_mon_geom = winman.gdk_screen.get_monitor_geometry(new_mon_id)
234
    logging.debug("Moving window to monitor %s, which has geometry %s",
235
                  new_mon_id, new_mon_geom)
236
237
    winman.reposition(win, None, new_mon_geom, keep_maximize=True)
238
239
@commands.add('monitor-prev-all', -1)
240
@commands.add('monitor-next-all', 1)
241
def cycle_monitors_all(winman, win, state, step=1):
242
    """Cycle all windows between monitors while preserving position."""
243
    n_monitors = winman.gdk_screen.get_n_monitors()
244
    curr_workspace = win.get_workspace()
245
246
    if not curr_workspace:
247
        logging.debug("get_workspace() returned None")
248
        return
249
250
    for window in winman.screen.get_windows():
251
        # Skip windows on other virtual desktops for intuitiveness
252
        if not window.is_on_workspace(curr_workspace):
253
            logging.debug("Skipping window on other workspace")
254
            continue
255
256
        # Don't cycle elements of the desktop
257
        if window.get_window_type() in [
258
              wnck.WINDOW_DESKTOP, wnck.WINDOW_DOCK]:  # pylint: disable=E1101
259
            logging.debug("Skipping desktop/dock window")
260
            continue
261
262
        gdkwin = gtk.gdk.window_foreign_new(window.get_xid())
263
        mon_id = winman.gdk_screen.get_monitor_at_window(gdkwin)
264
        new_mon_id = (mon_id + step) % n_monitors
265
266
        new_mon_geom = winman.gdk_screen.get_monitor_geometry(new_mon_id)
267
        logging.debug(
268
            "Moving window %s to monitor 0x%d, which has geometry %s",
269
            hex(window.get_xid()), new_mon_id, new_mon_geom)
270
271
        winman.reposition(window, None, new_mon_geom, keep_maximize=True)
272
273
# pylint: disable=no-member
274
MOVE_TO_COMMANDS = {
275
    'move-to-top-left': [wnck.WINDOW_GRAVITY_NORTHWEST,
276
                         wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
277
    'move-to-top': [wnck.WINDOW_GRAVITY_NORTH, wnck.WINDOW_CHANGE_Y],
278
    'move-to-top-right': [wnck.WINDOW_GRAVITY_NORTHEAST,
279
                          wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
280
    'move-to-left': [wnck.WINDOW_GRAVITY_WEST, wnck.WINDOW_CHANGE_X],
281
    'move-to-center': [wnck.WINDOW_GRAVITY_CENTER,
282
                       wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
283
    'move-to-right': [wnck.WINDOW_GRAVITY_EAST, wnck.WINDOW_CHANGE_X],
284
    'move-to-bottom-left': [wnck.WINDOW_GRAVITY_SOUTHWEST,
285
                            wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
286
    'move-to-bottom': [wnck.WINDOW_GRAVITY_SOUTH, wnck.WINDOW_CHANGE_Y],
287
    'move-to-bottom-right': [wnck.WINDOW_GRAVITY_SOUTHEAST,
288
                             wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
289
}
290
291
@commands.add_many(MOVE_TO_COMMANDS)
292
def move_to_position(winman, win, state, gravity, gravity_mask):
293
    """Move window to a position on the screen, preserving its dimensions."""
294
    use_rect = state['usable_rect']
295
296
    grav_x, grav_y = winman.gravities[gravity]
297
    dims = (int(use_rect.width * grav_x), int(use_rect.height * grav_y), 0, 0)
298
    result = gtk.gdk.Rectangle(*dims)
299
    logging.debug("Calling reposition() with %r gravity and dimensions %r",
300
                  gravity, tuple(result))
301
302
    # pylint: disable=no-member
303
    winman.reposition(win, result, use_rect, gravity=gravity,
304
            geometry_mask=gravity_mask)
305
306
@commands.add('bordered')
307
def toggle_decorated(winman, win, state):  # pylint: disable=unused-argument
308
    """Toggle window state on the active window."""
309
    win = gtk.gdk.window_foreign_new(win.get_xid())
310
    win.set_decorations(not win.get_decorations())
311
312
@commands.add('show-desktop')
313
def toggle_desktop(winman, win, state):  # pylint: disable=unused-argument
314
    """Toggle "all windows minimized" to view the desktop"""
315
    target = not winman.screen.get_showing_desktop()
316
    winman.screen.toggle_showing_desktop(target)
317
318
@commands.add('all-desktops', 'pin', 'is_pinned')
319
@commands.add('fullscreen', 'set_fullscreen', 'is_fullscreen', True)
320
@commands.add('vertical-maximize', 'maximize_vertically',
321
                                   'is_maximized_vertically')
322
@commands.add('horizontal-maximize', 'maximize_horizontally',
323
                                     'is_maximized_horizontally')
324
@commands.add('maximize', 'maximize', 'is_maximized')
325
@commands.add('minimize', 'minimize', 'is_minimized')
326
@commands.add('always-above', 'make_above', 'is_above')
327
@commands.add('always-below', 'make_below', 'is_below')
328
@commands.add('shade', 'shade', 'is_shaded')
329
# pylint: disable=unused-argument,too-many-arguments
330
def toggle_state(winman, win, state, command, check, takes_bool=False):
331
    """Toggle window state on the active window.
332
333
    @param command: The C{wnck.Window} method name to be conditionally prefixed
334
        with "un", resolved, and called.
335
    @param check: The C{wnck.Window} method name to be called to check
336
        whether C{command} should be prefixed with "un".
337
    @param takes_bool: If C{True}, pass C{True} or C{False} to C{check} rather
338
        thank conditionally prefixing it with C{un} before resolving.
339
    @type command: C{str}
340
    @type check: C{str}
341
    @type takes_bool: C{bool}
342
343
    @todo 1.0.0: Rename C{vertical-maximize} and C{horizontal-maximize} to
344
        C{maximize-vertical} and C{maximize-horizontal}. (API-breaking change)
345
    """
346
    target = not getattr(win, check)()
347
348
    logging.debug("Calling action '%s' with state '%s'", command, target)
349
    if takes_bool:
350
        getattr(win, command)(target)
351
    else:
352
        getattr(win, ('' if target else 'un') + command)()
353
354
@commands.add('trigger-move', 'move')
355
@commands.add('trigger-resize', 'size')
356
# pylint: disable=unused-argument
357
def trigger_keyboard_action(winman, win, state, command):
358
    """Ask the Window Manager to begin a keyboard-driven operation."""
359
    getattr(win, 'keyboard_' + command)()
360
361
@commands.add('workspace-go-next', 1)
362
@commands.add('workspace-go-prev', -1)
363
@commands.add('workspace-go-up', wnck.MOTION_UP)        # pylint: disable=E1101
364
@commands.add('workspace-go-down', wnck.MOTION_DOWN)    # pylint: disable=E1101
365
@commands.add('workspace-go-left', wnck.MOTION_LEFT)    # pylint: disable=E1101
366
@commands.add('workspace-go-right', wnck.MOTION_RIGHT)  # pylint: disable=E1101
367
def workspace_go(winman, win, state, motion):  # pylint: disable=W0613
368
    """Switch the active workspace (next/prev wrap around)"""
369
    target = winman.get_workspace(None, motion)
370
    if not target:
371
        return  # It's either pinned, on no workspaces, or there is no match
372
    target.activate(int(time.time()))
373
374
@commands.add('workspace-send-next', 1)
375
@commands.add('workspace-send-prev', -1)
376
@commands.add('workspace-send-up', wnck.MOTION_UP)      # pylint: disable=E1101
377
@commands.add('workspace-send-down', wnck.MOTION_DOWN)  # pylint: disable=E1101
378
@commands.add('workspace-send-left', wnck.MOTION_LEFT)  # pylint: disable=E1101
379
# pylint: disable=E1101
380
@commands.add('workspace-send-right', wnck.MOTION_RIGHT)
381
# pylint: disable=unused-argument
382
def workspace_send_window(winman, win, state, motion):
383
    """Move the active window to another workspace (next/prev wrap around)"""
384
    target = winman.get_workspace(win, motion)
385
    if not target:
386
        return  # It's either pinned, on no workspaces, or there is no match
387
388
    win.move_to_workspace(target)
389
390
# vim: set sw=4 sts=4 expandtab :
391