~ubuntu-branches/ubuntu/vivid/mago/vivid

« back to all changes in this revision

Viewing changes to mago/xlib/xlib.py

  • Committer: Bazaar Package Importer
  • Author(s): Michael Vogt
  • Date: 2011-02-08 13:32:13 UTC
  • mfrom: (1.1.3 upstream)
  • Revision ID: james.westby@ubuntu.com-20110208133213-m1og7ey0m990chg6
Tags: 0.3+bzr20-0ubuntu1
* debian/rules:
  - updated to debhelper 7
  - use dh_python2 instead of python-central
* debian/pycompat:
  - removed, no longer needed
* debian/control:
  - dropped cdbs and python-central dependencies
* bzr snapshot of the current trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# PyWO - Python Window Organizer
 
3
# Copyright 2010, Wojciech 'KosciaK' Pietrzok
 
4
#
 
5
# This file is part of PyWO.
 
6
#
 
7
# PyWO is free software: you can redistribute it and/or modify
 
8
# it under the terms of the GNU General Public License as published by
 
9
# the Free Software Foundation, either version 3 of the License, or
 
10
# (at your option) any later version.
 
11
#
 
12
# PyWO is distributed in the hope that it will be useful,
 
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
15
# GNU General Public License for more details.
 
16
#
 
17
# You should have received a copy of the GNU General Public License
 
18
# along with PyWO.  If not, see <http://www.gnu.org/licenses/>.
 
19
#
 
20
 
 
21
"""core.py - an abstract layer between Xlib and the rest of aplication.
 
22
 
 
23
core module (with events module) encapsulates all comunication with X Server.
 
24
It contains objects representing Window Manager, Windows, and other basic
 
25
concepts needed for repositioning and resizing windows (size, position,
 
26
borders, gravity, etc).
 
27
 
 
28
"""
 
29
 
 
30
import logging
 
31
import re
 
32
import time
 
33
import threading
 
34
 
 
35
from sys import exit
 
36
try:
 
37
    from Xlib import X, XK, Xatom, protocol, error
 
38
    from Xlib.display import Display
 
39
except ImportError:
 
40
    print "Xlib support is required. Install the package python-xlib.\nExiting..."
 
41
    exit(1)
 
42
 
 
43
 
 
44
__author__ = "Wojciech 'KosciaK' Pietrzok <kosciak@kosciak.net>"
 
45
 
 
46
 
 
47
# Pattern matching simple calculations with floating numbers
 
48
_PATTERN = re.compile('^[ 0-9\.\+-/\*]+$')
 
49
 
 
50
# Predefined sizes that can be used in config files
 
51
_SIZES = {'FULL': '1.0',
 
52
          'HALF': '0.5',
 
53
          'THIRD': '1.0/3',
 
54
          'QUARTER': '0.25',
 
55
         }
 
56
 
 
57
# Predefined gravities, that can be used in config files
 
58
_GRAVITIES = {'TOP_LEFT': (0, 0), 'UP_LEFT': (0, 0),
 
59
              'TOP': (0.5, 0), 'UP': (0.5, 0),
 
60
              'TOP_RIGHT': (1, 0), 'UP_RIGHT': (1, 0),
 
61
              'LEFT': (0, 0.5),
 
62
              'MIDDLE': (0.5, 0.5), 'CENTER': (0.5, 0.5),
 
63
              'RIGHT': (1, 0.5),
 
64
              'BOTTOM_LEFT': (0, 1), 'DOWN_LEFT': (0, 1),
 
65
              'BOTTOM': (0.5, 1), 'DOWN': (0.5, 1),
 
66
              'BOTTOM_RIGHT': (1, 1), 'DOWN_RIGHT': (1, 1),
 
67
             }
 
68
 
 
69
class Gravity(object):
 
70
 
 
71
    """Gravity point as a percentage of width and height of the window."""
 
72
 
 
73
    def __init__(self, x, y):
 
74
        """
 
75
        x - percentage of width
 
76
        y - percentage of height
 
77
        """
 
78
        self.x = x
 
79
        self.y = y
 
80
        self.is_middle = (x == 1.0/2) and (y == 1.0/2)
 
81
 
 
82
 
 
83
    @property
 
84
    def is_top(self):
 
85
        """Return True if gravity is toward top."""
 
86
        return self.y < 1.0/2 or self.is_middle
 
87
 
 
88
    @property
 
89
    def is_bottom(self):
 
90
        """Return True if gravity is toward bottom."""
 
91
        return self.y > 1.0/2 or self.is_middle
 
92
 
 
93
    @property
 
94
    def is_left(self):
 
95
        """Return True if gravity is toward left."""
 
96
        return self.x < 1.0/2 or self.is_middle
 
97
 
 
98
    @property
 
99
    def is_right(self):
 
100
        """Return True if gravity is toward right."""
 
101
        return self.x > 1.0/2 or self.is_middle
 
102
 
 
103
    def invert(self, vertical=True, horizontal=True):
 
104
        """Invert the gravity (left becomes right, top becomes bottom)."""
 
105
        x, y = self.x, self.y
 
106
        if vertical:
 
107
            y = 1.0 - self.y
 
108
        if horizontal:
 
109
            x = 1.0 - self.x
 
110
        return Gravity(x, y)
 
111
 
 
112
    @staticmethod
 
113
    def parse(gravity):
 
114
        """Parse gravity string and return Gravity object.
 
115
 
 
116
        It can be one of predefined __GRAVITIES, or x and y values (floating
 
117
        numbers or those described in __SIZES).
 
118
 
 
119
        """
 
120
        if not gravity:
 
121
            return None
 
122
        if gravity in _GRAVITIES:
 
123
            x, y = _GRAVITIES[gravity]
 
124
        else:
 
125
            for name, value in _SIZES.items():
 
126
                gravity = gravity.replace(name, value)
 
127
            x, y = [eval(xy) for xy in gravity.split(',')
 
128
                             if _PATTERN.match(xy)]
 
129
        return Gravity(x, y)
 
130
 
 
131
 
 
132
    def __eq__(self, other):
 
133
        return ((self.x, self.y) ==
 
134
                (other.x, other.y))
 
135
 
 
136
    def __ne__(self, other):
 
137
        return not self == other
 
138
 
 
139
    def __str__(self):
 
140
        return '(%.2f, %.2f)' % (self.x, self.y)
 
141
 
 
142
 
 
143
class Size(object):
 
144
 
 
145
    """Size encapsulates width and height of the object."""
 
146
 
 
147
    def __init__(self, width, height):
 
148
        self.width = width
 
149
        self.height = height
 
150
 
 
151
    @staticmethod
 
152
    def parse(width, height):
 
153
        """Parse width and height strings and return Size object.
 
154
 
 
155
        It can be float number (value will be evaluatedi, so 1.0/2 is valid) 
 
156
        or predefined value in __SIZES.
 
157
 
 
158
        """
 
159
        if not width or not height:
 
160
            return None
 
161
        for name, value in _SIZES.items():
 
162
            width = width.replace(name, value)
 
163
            height = height.replace(name, value)
 
164
        width = [eval(width) for width in width.split(',') 
 
165
                             if _PATTERN.match(width)]
 
166
        if len(width) == 1:
 
167
            width = width[0]
 
168
        height = [eval(height) for height in height.split(',')
 
169
                               if _PATTERN.match(height)]
 
170
        if len(height) == 1:
 
171
            height = height[0]
 
172
        return Size(width, height)
 
173
 
 
174
    def __eq__(self, other):
 
175
        return ((self.width, self.height) == (other.width, other.height))
 
176
 
 
177
    def __ne__(self, other):
 
178
        return not self == other
 
179
 
 
180
    def __str__(self):
 
181
        string = 'width: %s, height: %s' 
 
182
        return string % (self.width, self.height)
 
183
 
 
184
 
 
185
class Position(object):
 
186
 
 
187
    """Position encapsulates Position of the object.
 
188
 
 
189
    Position coordinates starts at top-left corner of the desktop.
 
190
 
 
191
    """
 
192
 
 
193
    def __init__(self, x, y):
 
194
        self.x = x
 
195
        self.y = y
 
196
 
 
197
    def __eq__(self, other):
 
198
        return ((self.x, self.y) == (other.x, other.y))
 
199
 
 
200
    def __ne__(self, other):
 
201
        return not self == other
 
202
 
 
203
    def __str__(self):
 
204
        string = 'x: %s, y: %s' 
 
205
        return string % (self.x, self.y)
 
206
 
 
207
 
 
208
class Geometry(Position, Size):
 
209
 
 
210
    """Geometry combines Size and Position of the object.
 
211
 
 
212
    Position coordinates (x, y) starts at top left corner of the desktop.
 
213
    (x2, y2) are the coordinates of the bottom-right corner of the object.
 
214
 
 
215
    """
 
216
 
 
217
    __DEFAULT_GRAVITY = Gravity(0, 0)
 
218
 
 
219
    def __init__(self, x, y, width, height,
 
220
                 gravity=__DEFAULT_GRAVITY):
 
221
        Size.__init__(self, int(width), int(height))
 
222
        x = int(x) - self.width * gravity.x
 
223
        y = int(y) - self.height * gravity.y
 
224
        Position.__init__(self, x, y)
 
225
 
 
226
    @property
 
227
    def x2(self):
 
228
        return self.x + self.width
 
229
 
 
230
    @property
 
231
    def y2(self):
 
232
        return self.y + self.height
 
233
 
 
234
    def set_position(self, x, y, gravity=__DEFAULT_GRAVITY):
 
235
        """Set position with (x,y) as gravity point."""
 
236
        self.x = x - self.width * gravity.x
 
237
        self.y = y - self.height * gravity.y
 
238
 
 
239
    def __eq__(self, other):
 
240
        return ((self.x, self.y, self.width, self.height) ==
 
241
                (other.x, other.y, other.width, other.height))
 
242
 
 
243
    def __ne__(self, other):
 
244
        return not self == other
 
245
 
 
246
    def __str__(self):
 
247
        string = 'x: %s, y: %s, width: %s, height: %s, x2: %s, y2: %s' 
 
248
        return string % (self.x, self.y, 
 
249
                         self.width, self.height, 
 
250
                         self.x2, self.y2)
 
251
 
 
252
 
 
253
class Borders(object):
 
254
 
 
255
    """Borders encapsulate Window borders (frames/decorations)."""
 
256
 
 
257
    def __init__(self, left, right, top, bottom):
 
258
        self.top = top
 
259
        self.bottom = bottom
 
260
        self.left = left
 
261
        self.right = right
 
262
 
 
263
    @property
 
264
    def horizontal(self):
 
265
        """Return sum of left and right borders."""
 
266
        return self.left + self.right
 
267
 
 
268
    @property
 
269
    def vertical(self):
 
270
        """Return sum of top and bottom borders."""
 
271
        return self.top + self.bottom
 
272
 
 
273
    def __str__(self):
 
274
        string = 'left: %s, right: %s, top: %s, bottom %s' 
 
275
        return string % (self.left, self.right, self.top, self.bottom)
 
276
 
 
277
 
 
278
class EventDispatcher(object):
 
279
 
 
280
    """Checks the event queue and dispatches events to correct handlers.
 
281
 
 
282
    EventDispatcher will run in separate thread.
 
283
    The self.__handlers attribute holds all registered EventHnadlers,
 
284
    it has structure as follows:
 
285
    self.__handlers = {win_id: {event_type: handler}} 
 
286
    That's why there can be only one handler per window/event_type.
 
287
 
 
288
    """
 
289
 
 
290
    def __init__(self, display):
 
291
        # What about integration with gobject?
 
292
        # gobject.io_add_watch(root.display, gobject.IO_IN, handle_xevent)
 
293
        self.__display = display
 
294
        self.__root = display.screen().root
 
295
        self.__handlers = {} # {window.id: {handler.type: handler, }, }
 
296
 
 
297
    def run(self):
 
298
        """Perform event queue checking.
 
299
 
 
300
        Every 50ms check event queue for pending events and dispatch them.
 
301
        If there's no registered handlers stop running.
 
302
 
 
303
        """
 
304
        logging.debug('EventDispatcher started')
 
305
        while self.__handlers:
 
306
            time.sleep(0.05)
 
307
            while self.__display.pending_events():
 
308
                # Dispatch all pending events if present
 
309
                self.__dispatch(self.__display.next_event())
 
310
        logging.debug('EventDispatcher stopped')
 
311
 
 
312
    def register(self, window, handler):
 
313
        """Register event handler and return new window's event mask."""
 
314
        logging.debug('Registering %s (mask=%s, types=%s) for %s' %
 
315
                      (handler.__class__.__name__, 
 
316
                       handler.mask, handler.types, window.id))
 
317
        started = len(self.__handlers)
 
318
        if not window.id in self.__handlers:
 
319
            self.__handlers[window.id] = {}
 
320
        for type in handler.types:
 
321
            self.__handlers[window.id][type] = handler
 
322
        if not started:
 
323
            t = threading.Thread(target=self.run)
 
324
            t.start()
 
325
        return set([handler.mask 
 
326
                    for handler in self.__handlers[window.id].values()])
 
327
 
 
328
    def unregister(self, window, handler=None):
 
329
        """Unregister event handler and return new window's event mask.
 
330
        
 
331
        If handler is None all handlers will be unregistered.
 
332
        
 
333
        """
 
334
        if not handler and window.id in self.__handlers:
 
335
            logging.debug('Unregistering all handlers for window %s' % 
 
336
                          (window.id))
 
337
            self.__handlers[window.id] = {}
 
338
        elif window.id in self.__handlers:
 
339
            logging.debug('Unregistering %s (mask=%s, types=%s) for %s' %
 
340
                          (handler.__class__.__name__, 
 
341
                           handler.mask, handler.types, window.id))
 
342
            for type in handler.types:
 
343
                if type in self.__handlers[window.id]:
 
344
                    del self.__handlers[window.id][type]
 
345
        if not self.__handlers[window.id]:
 
346
            del self.__handlers[window.id]
 
347
            return []
 
348
        return set([handler.mask 
 
349
                    for handler in self.__handlers[window.id].values()])
 
350
 
 
351
    def __dispatch(self, event):
 
352
        """Dispatch raw X event to correct handler."""
 
353
        if hasattr(event, 'window') and \
 
354
           event.window.id in self.__handlers:
 
355
            # Try window the event is reported on (if present)
 
356
            handlers = self.__handlers[event.window.id]
 
357
        elif hasattr(event, 'event') and \
 
358
             event.event.id in self.__handlers:
 
359
            # Try window the event is reported for (if present)
 
360
            handlers = self.__handlers[event.event.id]
 
361
        elif self.__root in self.__handlers:
 
362
            # Try root window
 
363
            handlers = self.__handlers[self.__root]
 
364
        else:
 
365
            logging.error('No handler for this event')
 
366
            return
 
367
        if not event.type in handlers:
 
368
            # Just skip unwanted events' types
 
369
            return
 
370
        handlers[event.type].handle_event(event)
 
371
 
 
372
 
 
373
class XObject(object):
 
374
 
 
375
    """Abstract base class for classes communicating with X Server.
 
376
 
 
377
    Encapsulates common methods for communication with X Server.
 
378
 
 
379
    """
 
380
 
 
381
    __DISPLAY = Display()
 
382
    __EVENT_DISPATCHER = EventDispatcher(__DISPLAY)
 
383
    __BAD_ACCESS = error.CatchError(error.BadAccess)
 
384
 
 
385
    # List of recognized key modifiers
 
386
    __KEY_MODIFIERS = {'Alt': X.Mod1Mask,
 
387
                       'Ctrl': X.ControlMask,
 
388
                       'Shift': X.ShiftMask,
 
389
                       'Super': X.Mod4Mask,
 
390
                       'NumLock': X.Mod2Mask,
 
391
                       'CapsLock': X.LockMask}
 
392
 
 
393
    __KEYCODES = {}
 
394
 
 
395
    def __init__(self, win_id=None):
 
396
        """
 
397
        win_id - id of the window to be created, if no id assume it's 
 
398
                 Window Manager (root window)
 
399
        """
 
400
        self.__root = self.__DISPLAY.screen().root
 
401
        if win_id:
 
402
            # Normal window
 
403
            self._win = self.__DISPLAY.create_resource_object('window', win_id)
 
404
            self.id = win_id
 
405
        else:
 
406
            # WindowManager, act as root window
 
407
            self._win = self.__root 
 
408
            self.id = self._win.id
 
409
 
 
410
    @classmethod
 
411
    def atom(cls, name):
 
412
        """Return atom with given name."""
 
413
        return cls.__DISPLAY.intern_atom(name)
 
414
 
 
415
    @classmethod
 
416
    def atom_name(cls, atom):
 
417
        """Return atom's name."""
 
418
        return cls.__DISPLAY.get_atom_name(atom)
 
419
 
 
420
    def get_property(self, name):
 
421
        """Return property (None if there's no such property)."""
 
422
        atom = self.atom(name)
 
423
        property = self._win.get_full_property(atom, 0)
 
424
        return property
 
425
 
 
426
    def send_event(self, data, type, mask):
 
427
        """Send event from (to?) the root window."""
 
428
        event = protocol.event.ClientMessage(
 
429
                    window=self._win,
 
430
                    client_type=type,
 
431
                    data=(32, (data)))
 
432
        self.__root.send_event(event, event_mask=mask)
 
433
 
 
434
    def listen(self, event_handler):
 
435
        """Register new event handler and update event mask."""
 
436
        masks = self.__EVENT_DISPATCHER.register(self, event_handler)
 
437
        self.__set_event_mask(masks)
 
438
 
 
439
    def unlisten(self, event_handler=None):
 
440
        """Unregister event handler(s) and update event mask.
 
441
        
 
442
        If event_handler is None all handlers will be unregistered.
 
443
 
 
444
        """
 
445
        masks = self.__EVENT_DISPATCHER.unregister(self, event_handler)
 
446
        self.__set_event_mask(masks)
 
447
 
 
448
    def __set_event_mask(self, masks):
 
449
        """Update event mask."""
 
450
        event_mask = 0
 
451
        logging.debug('Setting %s masks for window %s' % 
 
452
                      ([str(e) for e in masks], self.id))
 
453
        for mask in masks:
 
454
            event_mask = event_mask | mask
 
455
        self._win.change_attributes(event_mask=event_mask)
 
456
 
 
457
    def __grab_key(self, keycode, modifiers):
 
458
        """Grab key."""
 
459
        self._win.grab_key(keycode, modifiers, 
 
460
                           1, X.GrabModeAsync, X.GrabModeAsync,
 
461
                           onerror=self.__BAD_ACCESS)
 
462
        self.sync()
 
463
        if self.__BAD_ACCESS.get_error():
 
464
            logging.error("Can't use %s" % 
 
465
                              self.keycode2str(modifiers, keycode))
 
466
 
 
467
    def grab_key(self, modifiers, keycode, numlock, capslock):
 
468
        """Grab key.
 
469
 
 
470
        Grab key alone, with CapsLock on and/or with NumLock on.
 
471
 
 
472
        """
 
473
        if numlock in [0, 2] and capslock in [0, 2]:
 
474
            self.__grab_key(keycode, modifiers)
 
475
        if numlock in [0, 2] and capslock in [1, 2]:
 
476
            self.__grab_key(keycode, modifiers | X.LockMask)
 
477
        if numlock in [1, 2] and capslock in [0, 2]:
 
478
            self.__grab_key(keycode, modifiers | X.Mod2Mask)
 
479
        if numlock in [1, 2] and capslock in [1, 2]:
 
480
            self.__grab_key(keycode, modifiers | X.LockMask | X.Mod2Mask)
 
481
 
 
482
    def ungrab_key(self, modifiers, keycode, numlock, capslock):
 
483
        """Ungrab key.
 
484
 
 
485
        Ungrab key alone, with CapsLock on and/or with NumLock on.
 
486
 
 
487
        """
 
488
        if numlock in [0, 2] and capslock in [0, 1]:
 
489
            self._win.ungrab_key(keycode, modifiers)
 
490
        if numlock in [0, 2] and capslock in [1, 2]:
 
491
            self._win.ungrab_key(keycode, modifiers | X.LockMask)
 
492
        if numlock in [1, 2] and capslock in [0, 2]:
 
493
            self._win.ungrab_key(keycode, modifiers | X.Mod2Mask)
 
494
        if numlock in [1, 2] and capslock in [1, 2]:
 
495
            self._win.ungrab_key(keycode, modifiers | X.LockMask | X.Mod2Mask)
 
496
 
 
497
    def draw_rectangle(self, x, y, width, height, line):
 
498
        color = self.__DISPLAY.screen().black_pixel
 
499
        gc = self.__root.create_gc(line_width=line,
 
500
                                   #join_style=X.JoinRound,
 
501
                                   foreground=color,
 
502
                                   function=X.GXinvert,
 
503
                                   subwindow_mode=X.IncludeInferiors,)
 
504
        self.__root.rectangle(gc, x, y, width, height)
 
505
 
 
506
    def _translate_coords(self, x, y):
 
507
        """Return translated coordinates.
 
508
        
 
509
        Untranslated coordinates are relative to window.
 
510
        Translated coordinates are relative to desktop.
 
511
 
 
512
        """
 
513
        return self._win.translate_coords(self.__root, x, y)
 
514
 
 
515
    @classmethod
 
516
    def str2modifiers(cls, masks, splitted=False):
 
517
        # TODO: Check this part... not sure why it looks like that...
 
518
        if not splitted:
 
519
            masks = masks.split('-')
 
520
        modifiers = 0
 
521
        if masks[0]:
 
522
            for mask in masks:
 
523
                if not mask in cls.__KEY_MODIFIERS.keys():
 
524
                    continue
 
525
                modifiers = modifiers | cls.__KEY_MODIFIERS[mask]
 
526
        else:
 
527
            modifiers = X.AnyModifier
 
528
 
 
529
        return modifiers
 
530
 
 
531
    @classmethod
 
532
    def str2keycode(cls, key):
 
533
        keysym = XK.string_to_keysym(key)
 
534
        keycode = cls.__DISPLAY.keysym_to_keycode(keysym)
 
535
        cls.__KEYCODES[keycode] = key
 
536
        return keycode
 
537
 
 
538
    @classmethod
 
539
    def str2modifiers_keycode(cls, code, key=''):
 
540
        """Convert key as string(s) into (modifiers, keycode) pair.
 
541
        
 
542
        There must be both modifier(s) and key persent. If you send both
 
543
        modifier(s) and key in one string, they must be separated using '-'. 
 
544
        Modifiers must be separated using '-'.
 
545
        Keys are case insensitive.
 
546
        If you want to use upper case use Shift modifier.
 
547
        Only modifiers defined in __KEY_MODIFIERS are valid.
 
548
        For example: "Ctrl-A", "Super-Alt-x"
 
549
        
 
550
        """
 
551
        if key:
 
552
            code = '-'.join([code,key])
 
553
        code = code.split('-')
 
554
        key = code[-1]
 
555
        masks = code[:-1]
 
556
        
 
557
        modifiers = cls.str2modifiers(masks, True)
 
558
        keycode = cls.str2keycode(key)
 
559
        return (modifiers, keycode)
 
560
 
 
561
    @classmethod
 
562
    def keycode2str(cls, modifiers, keycode):
 
563
        """Convert key as (modifiers, keycode) pair into string.
 
564
        
 
565
        Works ONLY for already registered keycodes!
 
566
        
 
567
        """
 
568
        key = []
 
569
        for name, code in cls.__KEY_MODIFIERS.items():
 
570
            if modifiers & code:
 
571
                key.append(name)
 
572
 
 
573
        key.append(cls.__KEYCODES[keycode])
 
574
        return '-'.join(key)
 
575
 
 
576
    @classmethod
 
577
    def flush(cls):
 
578
        """Flush request queue to X Server."""
 
579
        cls.__DISPLAY.flush()
 
580
 
 
581
    @classmethod
 
582
    def sync(cls):
 
583
        """Flush request queue to X Server, wait until server processes them."""
 
584
        cls.__DISPLAY.sync()
 
585
 
 
586
 
 
587
class Window(XObject):
 
588
 
 
589
    """Window object (X Server client?)."""
 
590
 
 
591
    # List of window types
 
592
    TYPE_DESKTOP = XObject.atom('_NET_WM_WINDOW_TYPE_DESKTOP')
 
593
    TYPE_DOCK = XObject.atom('_NET_WM_WINDOW_TYPE_DOCK')
 
594
    TYPE_TOOLBAR = XObject.atom('_NET_WM_WINDOW_TYPE_TOOLBAR')
 
595
    TYPE_MENU = XObject.atom('_NET_WM_WINDOW_TYPE_MENU')
 
596
    TYPE_UTILITY = XObject.atom('_NET_WM_WINDOW_TYPE_UTILITY')
 
597
    TYPE_SPLASH = XObject.atom('_NET_WM_WINDOW_TYPE_SPLASH')
 
598
    TYPE_DIALOG = XObject.atom('_NET_WM_WINDOW_TYPE_DIALOG')
 
599
    TYPE_NORMAL = XObject.atom('_NET_WM_WINDOW_TYPE_NORMAL')
 
600
 
 
601
    # List of window states
 
602
    STATE_MODAL = XObject.atom('_NET_WM_STATE_MODAL')
 
603
    STATE_STICKY = XObject.atom('_NET_WM_STATE_STICKY')
 
604
    STATE_MAXIMIZED_VERT = XObject.atom('_NET_WM_STATE_MAXIMIZED_VERT')
 
605
    STATE_MAXIMIZED_HORZ = XObject.atom('_NET_WM_STATE_MAXIMIZED_HORZ')
 
606
    STATE_SHADED = XObject.atom('_NET_WM_STATE_SHADED')
 
607
    STATE_SKIP_TASKBAR = XObject.atom('_NET_WM_STATE_SKIP_TASKBAR')
 
608
    STATE_SKIP_PAGER = XObject.atom('_NET_WM_STATE_SKIP_PAGER')
 
609
    STATE_HIDDEN = XObject.atom('_NET_WM_STATE_HIDDEN')
 
610
    STATE_FULLSCREEN = XObject.atom('_NET_WM_STATE_FULLSCREEN')
 
611
    STATE_ABOVE = XObject.atom('_NET_WM_STATE_ABOVE')
 
612
    STATE_BELOW = XObject.atom('_NET_WM_STATE_BELOW')
 
613
    STATE_DEMANDS_ATTENTION = XObject.atom('_NET_WM_STATE_DEMANDS_ATTENTION')
 
614
 
 
615
    # Mode values (for maximize and shade functions)
 
616
    MODE_UNSET = 0
 
617
    MODE_SET = 1
 
618
    MODE_TOGGLE = 2
 
619
 
 
620
    def __init__(self, win_id):
 
621
        XObject.__init__(self, win_id)
 
622
        # Here comes the hacks for WMs strange behaviours....
 
623
        wm_name = WindowManager().name.lower()
 
624
        if wm_name.startswith('icewm'):
 
625
            wm_name = 'icewm'
 
626
        self.__translate_coords =  \
 
627
                wm_name not in ['compiz', 'fluxbox', 'window maker', ]
 
628
        self.__adjust_geometry =  \
 
629
                wm_name in ['compiz', 'kwin', 'e16', 'icewm', 'blackbox', ]
 
630
        self.__parent_xy = wm_name in ['fluxbox', 'window maker', ]
 
631
 
 
632
    @property
 
633
    def type(self):
 
634
        """Return list of window's type(s)."""
 
635
        type = self.get_property('_NET_WM_WINDOW_TYPE')
 
636
        if not type:
 
637
            return [Window.TYPE_NORMAL]
 
638
        return type.value
 
639
 
 
640
    @property
 
641
    def state(self):
 
642
        """Return list of window's state(s)."""
 
643
        state = self.get_property('_NET_WM_STATE')
 
644
        if not state:
 
645
            return []
 
646
        return state.value
 
647
 
 
648
    @property
 
649
    def parent_id(self):
 
650
        """Return window's parent id."""
 
651
        parent = self._win.get_wm_transient_for()
 
652
        if parent:
 
653
            return parent.id
 
654
        else:
 
655
            return None
 
656
 
 
657
    @property
 
658
    def parent(self):
 
659
        """Return window's parent."""
 
660
        parent_id = self.parent_id
 
661
        if parent_id:
 
662
            return Window(parent_id)
 
663
        else:
 
664
            return None
 
665
 
 
666
    @property
 
667
    def name(self):
 
668
        """Return window's name."""
 
669
        name = self.get_property('_NET_WM_NAME')
 
670
        if not name:
 
671
            name = self._win.get_full_property(Xatom.WM_NAME, 0)
 
672
            if not name:        
 
673
                return ''
 
674
        return name.value
 
675
 
 
676
    @property
 
677
    def class_name(self):
 
678
        """Return window's class name."""
 
679
        class_name = self._win.get_wm_class()
 
680
        return class_name
 
681
 
 
682
    @property
 
683
    def desktop(self):
 
684
        """Return desktop number the window is in."""
 
685
        desktop = self.get_property('_NET_WM_DESKTOP')
 
686
        if not desktop:
 
687
            return 0
 
688
        # returns 0xFFFFFFFF when "show on all desktops"
 
689
        return desktop.value[0]
 
690
 
 
691
    def __borders(self):
 
692
        """Return raw borders info."""
 
693
        extents = self.get_property('_NET_FRAME_EXTENTS')
 
694
        if extents:
 
695
            return extents.value
 
696
        # Hack for Blackbox, IceWM, Sawfish, Window Maker
 
697
        win = self._win
 
698
        parent = win.query_tree().parent
 
699
        if win.get_geometry().width == parent.get_geometry().width and \
 
700
           win.get_geometry().height == parent.get_geometry().height:
 
701
            win, parent = parent, parent.query_tree().parent
 
702
        win_geo = win.get_geometry()
 
703
        parent_geo = parent.get_geometry()
 
704
        border_widths = win_geo.border_width + parent_geo.border_width
 
705
        left = win_geo.x + border_widths
 
706
        top = win_geo.y + border_widths
 
707
        right = parent_geo.width - win_geo.width - left + parent_geo.border_width*2
 
708
        bottom = parent_geo.height - win_geo.height - top + parent_geo.border_width*2
 
709
        return (left, right, top, bottom)
 
710
 
 
711
    @property
 
712
    def borders(self):
 
713
        """Return window's borders (frames/decorations)."""
 
714
        borders = self.__borders()
 
715
        return Borders(*borders)
 
716
 
 
717
    def __geometry(self):
 
718
        """Return raw geometry info (translated if needed)."""
 
719
        geometry = self._win.get_geometry()
 
720
        if self.__parent_xy:
 
721
            # Hack for Fluxbox, Window Maker
 
722
            parent_geo = self._win.query_tree().parent.get_geometry()
 
723
            geometry.x = parent_geo.x
 
724
            geometry.y = parent_geo.y
 
725
        if self.__translate_coords:
 
726
            # if neeeded translate coords and multiply them by -1
 
727
            translated = self._translate_coords(geometry.x, geometry.y)
 
728
            return (-translated.x, -translated.y, 
 
729
                    geometry.width, geometry.height)
 
730
        return (geometry.x, geometry.y, 
 
731
                geometry.width, geometry.height)
 
732
 
 
733
    @property
 
734
    def geometry(self):
 
735
        """Return window's geometry.
 
736
 
 
737
        (x, y) coordinates are the top-left corner of the window,
 
738
        relative to the left-top corner of desktop (workarea?).
 
739
        Position and size *includes* window's borders!
 
740
        Position is translated if needed.
 
741
 
 
742
        """
 
743
        x, y, width, height = self.__geometry()
 
744
        left, right, top, bottom = self.__borders()
 
745
        if self.__adjust_geometry:
 
746
            # Used in Compiz, KWin, E16, IceWM, Blackbox
 
747
            x -= left
 
748
            y -= top
 
749
        return Geometry(x, y,
 
750
                        width + left + right,
 
751
                        height + top + bottom)
 
752
 
 
753
    def move_resize(self, geometry, on_resize=Gravity(0, 0)):
 
754
        """Move or resize window using provided geometry.
 
755
 
 
756
        Postion and size must include window's borders. 
 
757
 
 
758
        """
 
759
        left, right, top, bottom = self.__borders()
 
760
        x = geometry.x
 
761
        y = geometry.y
 
762
        width = geometry.width - (left + right)
 
763
        height = geometry.height - (top + bottom)
 
764
        geometry_size = (width, height)
 
765
        current = self.__geometry()
 
766
        hints = self._win.get_wm_normal_hints()
 
767
        # This is a fix for WINE, OpenOffice and KeePassX windows
 
768
        if hints and hints.win_gravity == X.StaticGravity:
 
769
            x += left
 
770
            y += top
 
771
        # Reduce size to maximal allowed value
 
772
        if hints and hints.max_width: 
 
773
            width = min([width, hints.max_width])
 
774
        if hints and hints.max_height:
 
775
            height = min([height, hints.max_height])
 
776
        # Don't try to set size lower then minimal
 
777
        if hints and hints.min_width: 
 
778
            width = max([width, hints.min_width])
 
779
        if hints and hints.min_height:
 
780
            height = max([height, hints.min_height])
 
781
        # Set correct size if it is incremental, take base in account
 
782
        if hints and hints.width_inc: 
 
783
            if hints.base_width:
 
784
                base = hints.base_width
 
785
            else:
 
786
                base = current[2] % hints.width_inc
 
787
            width = ((width - base) / hints.width_inc) * hints.width_inc
 
788
            width += base
 
789
            if hints.min_width and width < hints.min_width:
 
790
                width += hints.width_inc
 
791
        if hints and hints.height_inc:
 
792
            if hints.base_height:
 
793
                base = hints.base_height
 
794
            else:
 
795
                base = current[3] % hints.height_inc
 
796
            height = ((height - base) / hints.height_inc) * hints.height_inc
 
797
            height += base
 
798
            if hints.height_inc and height < hints.min_height:
 
799
                height += hints.height_inc
 
800
        # Adjust position after size change
 
801
        if (width, height) != geometry_size:
 
802
            x = x + (geometry_size[0] - width) * on_resize.x
 
803
            y = y + (geometry_size[1] - height) * on_resize.y
 
804
        self._win.configure(x=x, y=y, width=width, height=height)
 
805
 
 
806
    def activate(self):
 
807
        """Make this window active (unshade, unminimize)."""
 
808
        type = self.atom('_NET_ACTIVE_WINDOW')
 
809
        mask = X.SubstructureRedirectMask
 
810
        data = [0, 0, 0, 0, 0]
 
811
        self.send_event(data, type, mask)
 
812
        # NOTE: Previously used for activating (didn't unshade/unminimize)
 
813
        #       Need to test if setting X.Above is needed in various WMs
 
814
        #self._win.set_input_focus(X.RevertToNone, X.CurrentTime)
 
815
        #self._win.configure(stack_mode=X.Above)
 
816
 
 
817
    def maximize(self, mode,
 
818
                 vert=STATE_MAXIMIZED_VERT, 
 
819
                 horz=STATE_MAXIMIZED_HORZ):
 
820
        """Maximize window (both vertically and horizontally)."""
 
821
        data = [mode, 
 
822
                horz,
 
823
                vert,
 
824
                0, 0]
 
825
        self.__change_state(data)
 
826
 
 
827
    def shade(self, mode):
 
828
        """Shade window (if supported by window manager)."""
 
829
        data = [mode, 
 
830
                Window.STATE_SHADED,
 
831
                0, 0, 0]
 
832
        self.__change_state(data)
 
833
 
 
834
    def fullscreen(self, mode):
 
835
        """Make window fullscreen (if supported by window manager)."""
 
836
        data = [mode, 
 
837
                Window.STATE_FULLSCREEN,
 
838
                0, 0, 0]
 
839
        self.__change_state(data)
 
840
 
 
841
    def reset(self):
 
842
        """Unmaximize (horizontally and vertically), unshade, unfullscreen."""
 
843
        self.fullscreen(self.MODE_UNSET)
 
844
        self.maximize(self.MODE_UNSET)
 
845
        self.shade(self.MODE_UNSET)
 
846
 
 
847
    def sticky(self, mode):
 
848
        """Make window fullscreen (if supported by window manager)."""
 
849
        data = [mode, 
 
850
                Window.STATE_STICKY,
 
851
                0, 0, 0]
 
852
        self.__change_state(data)
 
853
 
 
854
    def close(self):
 
855
        """Close window."""
 
856
        type = self.atom('_NET_CLOSE_WINDOW')
 
857
        mask = X.SubstructureRedirectMask
 
858
        data = [0, 0, 0, 0, 0]
 
859
        self.send_event(data, type, mask)
 
860
 
 
861
    def __change_state(self, data):
 
862
        """Send _NET_WM_STATE event to the root window."""
 
863
        type = self.atom('_NET_WM_STATE')
 
864
        mask = X.SubstructureRedirectMask
 
865
        self.send_event(data, type, mask)
 
866
 
 
867
    def blink(self):
 
868
        """For 0.25 second show borderaround window."""
 
869
        geo = self.geometry
 
870
        self.draw_rectangle(geo.x+10, geo.y+10, 
 
871
                            geo.width-20, geo.height-20, 20)
 
872
        self.flush()
 
873
        time.sleep(0.25)
 
874
        self.draw_rectangle(geo.x+10, geo.y+10, 
 
875
                            geo.width-20, geo.height-20, 20)
 
876
        self.flush()
 
877
 
 
878
    def __eq__(self, other):
 
879
        return self.id == other.id
 
880
 
 
881
    def __ne__(self, other):
 
882
        return not self.id == other.id
 
883
 
 
884
    def debug_info(self):
 
885
        """Print full window's info, for debug use only."""
 
886
        logging.info('ID=%s' % self.id)
 
887
        logging.info('Name=%s' % self.name)
 
888
        logging.info('Class=%s' % [str(e) for e in self.class_name])
 
889
        #logging.info('Type=%s' % [str(e) for e in self.type])
 
890
        logging.info('Type=%s' % [self.atom_name(e) for e in self.type])
 
891
        #logging.info('State=%s' % [str(e) for e in self.state])
 
892
        logging.info('State=%s' % [self.atom_name(e) for e in self.state])
 
893
        logging.info('Desktop=%s' % self.desktop)
 
894
        logging.info('Borders=%s' % self.borders)
 
895
        logging.info('Borders_raw=%s' % [str(e) for e in self.__borders()])
 
896
        logging.info('Geometry=%s' % self.geometry)
 
897
        logging.info('Geometry_raw=%s' % self._win.get_geometry())
 
898
        logging.info('Parent=%s %s' % (self.parent_id, self.parent))
 
899
        logging.info('Normal_hints=%s' % self._win.get_wm_normal_hints())
 
900
        logging.info('Attributes=%s' % self._win.get_attributes())
 
901
        logging.info('Query_tree=%s' % self._win.query_tree())
 
902
 
 
903
 
 
904
class WindowManager(XObject):
 
905
    
 
906
    """Window Manager (or root window in X programming terms).
 
907
    
 
908
    WindowManager's self._win refers to the root window.
 
909
    It is Singleton.
 
910
 
 
911
    """
 
912
 
 
913
    # Instance of the WindowManager class, make it Singleton.
 
914
    __INSTANCE = None
 
915
 
 
916
    def __new__(cls):
 
917
        if cls.__INSTANCE:
 
918
            return cls.__INSTANCE
 
919
        manager = object.__new__(cls)
 
920
        XObject.__init__(manager)
 
921
        cls.__INSTANCE = manager
 
922
        return manager
 
923
 
 
924
    @property
 
925
    def name(self):
 
926
        """Return window manager's name.
 
927
 
 
928
        '' is returned if window manager doesn't support EWMH.
 
929
 
 
930
        """
 
931
        win_id = self.get_property('_NET_SUPPORTING_WM_CHECK')
 
932
        if not win_id:
 
933
            return ''
 
934
        win = XObject(win_id.value[0])
 
935
        name = win.get_property('_NET_WM_NAME')
 
936
        if name:
 
937
            return name.value
 
938
        else:
 
939
            return ''
 
940
 
 
941
    @property
 
942
    def desktops(self):
 
943
        """Return number of desktops."""
 
944
        number = self.get_property('_NET_NUMBER_OF_DESKTOPS')
 
945
        if not number:
 
946
            return 1
 
947
        return number.value[0]
 
948
 
 
949
    @property
 
950
    def desktop(self):
 
951
        """Return current desktop number."""
 
952
        desktop = self.get_property('_NET_CURRENT_DESKTOP')
 
953
        return desktop.value[0]
 
954
 
 
955
    @property
 
956
    def desktop_size(self):
 
957
        """Return size of current desktop."""
 
958
        geometry = self.get_property('_NET_DESKTOP_GEOMETRY').value
 
959
        return Size(geometry[0], geometry[1])
 
960
 
 
961
    @property
 
962
    def workarea_geometry(self):
 
963
        """Return geometry of current workarea (desktop without panels)."""
 
964
        workarea = self.get_property('_NET_WORKAREA').value
 
965
        return Geometry(workarea[0], workarea[1], 
 
966
                        workarea[2], workarea[3])
 
967
 
 
968
    @property
 
969
    def viewport(self):
 
970
        """Return position of current viewport. 
 
971
 
 
972
        If desktop is large it might be divided into several viewports.
 
973
 
 
974
        """
 
975
        viewport = self.get_property('_NET_DESKTOP_VIEWPORT').value
 
976
        return Position(viewport[0], viewport[1])
 
977
 
 
978
    def active_window_id(self):
 
979
        """Return only id of active window."""
 
980
        win_id = self.get_property('_NET_ACTIVE_WINDOW').value[0]
 
981
        return win_id
 
982
 
 
983
    def active_window(self):
 
984
        """Return active window."""
 
985
        window_id = self.active_window_id()
 
986
        return Window(window_id)
 
987
 
 
988
    def windows_ids(self):
 
989
        """Return list of all windows' ids (with bottom-top stacking order)."""
 
990
        windows_ids = self.get_property('_NET_CLIENT_LIST_STACKING').value
 
991
        return windows_ids
 
992
 
 
993
    def windows(self, filter_method=None, match=''):
 
994
        """Return list of all windows (with top-bottom stacking order)."""
 
995
        # TODO: regexp matching?
 
996
        windows_ids = self.windows_ids()
 
997
        windows = [Window(win_id) for win_id in windows_ids]
 
998
        if filter_method:
 
999
            windows = [window for window in windows if filter_method(window)]
 
1000
        if match:
 
1001
            windows = self.__name_matcher(windows, match)
 
1002
        windows.reverse()
 
1003
        return windows
 
1004
 
 
1005
    def __name_matcher(self, windows, match):
 
1006
        match = match.strip().lower()
 
1007
        desktop = self.desktop
 
1008
        workarea = self.workarea_geometry
 
1009
        def mapper(window, points=0):
 
1010
            name = window.name.lower().decode('utf-8')
 
1011
            if name == match:
 
1012
                points += 200
 
1013
            elif match in name:
 
1014
                left = name.find(match)
 
1015
                right = (name.rfind(match) - len(name) + len(match)) * -1
 
1016
                points += 150 - min(left, right)
 
1017
            geometry = window.geometry
 
1018
            if points and \
 
1019
               (window.desktop == desktop or \
 
1020
                window.desktop == 0xFFFFFFFF):
 
1021
                points += 50
 
1022
                if geometry.x < workarea.x2 and \
 
1023
                   geometry.x2 > workarea.x and \
 
1024
                   geometry.y < workarea.y2 and \
 
1025
                   geometry.y2 > workarea.y:
 
1026
                    points += 100
 
1027
            return (window, points)
 
1028
        windows = map(mapper, windows)
 
1029
        windows.sort(key=lambda win: win[1])
 
1030
        windows = [win for win, points in windows if points]
 
1031
        return windows
 
1032
 
 
1033
    def debug_info(self):
 
1034
        """Print full windows manager's info, for debug use only."""
 
1035
        logging.info('WindowManager=%s' % self.name)
 
1036
        logging.info('Desktops=%s, current=%s' % (self.desktops, self.desktop))
 
1037
        logging.info('Desktop=%s' % self.desktop_size)
 
1038
        logging.info('Viewport=%s' % self.viewport)
 
1039
        logging.info('Workarea=%s' % self.workarea_geometry)
 
1040
 
 
1041
WM = WindowManager()
 
1042
 
 
1043
 
 
1044
def normal_on_same_filter(window):
 
1045
    """Default windows filter.
 
1046
 
 
1047
    Returns normal, not hidde, not shaded, not fullscreen, not maximized,
 
1048
    placed on the same desktop (or sticky), and on the same workarea.
 
1049
 
 
1050
    """
 
1051
    if not Window.TYPE_NORMAL in window.type:
 
1052
        return False
 
1053
    state = window.state
 
1054
    geometry = window.geometry
 
1055
    workarea = WM.workarea_geometry
 
1056
    return Window.STATE_HIDDEN not in state and \
 
1057
           Window.STATE_SHADED not in state and \
 
1058
           Window.STATE_FULLSCREEN not in state and \
 
1059
           not (Window.STATE_MAXIMIZED_VERT in state and \
 
1060
                Window.STATE_MAXIMIZED_HORZ in state) and \
 
1061
           (window.desktop == WM.desktop or \
 
1062
            window.desktop == 0xFFFFFFFF) and \
 
1063
           geometry.x < workarea.x2 and \
 
1064
           geometry.x2 > workarea.x and \
 
1065
           geometry.y < workarea.y2 and \
 
1066
           geometry.y2 > workarea.y
 
1067
 
 
1068