2
# PyWO - Python Window Organizer
3
# Copyright 2010, Wojciech 'KosciaK' Pietrzok
5
# This file is part of PyWO.
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.
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.
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/>.
21
"""core.py - an abstract layer between Xlib and the rest of aplication.
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).
37
from Xlib import X, XK, Xatom, protocol, error
38
from Xlib.display import Display
40
print "Xlib support is required. Install the package python-xlib.\nExiting..."
44
__author__ = "Wojciech 'KosciaK' Pietrzok <kosciak@kosciak.net>"
47
# Pattern matching simple calculations with floating numbers
48
_PATTERN = re.compile('^[ 0-9\.\+-/\*]+$')
50
# Predefined sizes that can be used in config files
51
_SIZES = {'FULL': '1.0',
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),
62
'MIDDLE': (0.5, 0.5), 'CENTER': (0.5, 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),
69
class Gravity(object):
71
"""Gravity point as a percentage of width and height of the window."""
73
def __init__(self, x, y):
75
x - percentage of width
76
y - percentage of height
80
self.is_middle = (x == 1.0/2) and (y == 1.0/2)
85
"""Return True if gravity is toward top."""
86
return self.y < 1.0/2 or self.is_middle
90
"""Return True if gravity is toward bottom."""
91
return self.y > 1.0/2 or self.is_middle
95
"""Return True if gravity is toward left."""
96
return self.x < 1.0/2 or self.is_middle
100
"""Return True if gravity is toward right."""
101
return self.x > 1.0/2 or self.is_middle
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
114
"""Parse gravity string and return Gravity object.
116
It can be one of predefined __GRAVITIES, or x and y values (floating
117
numbers or those described in __SIZES).
122
if gravity in _GRAVITIES:
123
x, y = _GRAVITIES[gravity]
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)]
132
def __eq__(self, other):
133
return ((self.x, self.y) ==
136
def __ne__(self, other):
137
return not self == other
140
return '(%.2f, %.2f)' % (self.x, self.y)
145
"""Size encapsulates width and height of the object."""
147
def __init__(self, width, height):
152
def parse(width, height):
153
"""Parse width and height strings and return Size object.
155
It can be float number (value will be evaluatedi, so 1.0/2 is valid)
156
or predefined value in __SIZES.
159
if not width or not height:
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)]
168
height = [eval(height) for height in height.split(',')
169
if _PATTERN.match(height)]
172
return Size(width, height)
174
def __eq__(self, other):
175
return ((self.width, self.height) == (other.width, other.height))
177
def __ne__(self, other):
178
return not self == other
181
string = 'width: %s, height: %s'
182
return string % (self.width, self.height)
185
class Position(object):
187
"""Position encapsulates Position of the object.
189
Position coordinates starts at top-left corner of the desktop.
193
def __init__(self, x, y):
197
def __eq__(self, other):
198
return ((self.x, self.y) == (other.x, other.y))
200
def __ne__(self, other):
201
return not self == other
204
string = 'x: %s, y: %s'
205
return string % (self.x, self.y)
208
class Geometry(Position, Size):
210
"""Geometry combines Size and Position of the object.
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.
217
__DEFAULT_GRAVITY = Gravity(0, 0)
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)
228
return self.x + self.width
232
return self.y + self.height
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
239
def __eq__(self, other):
240
return ((self.x, self.y, self.width, self.height) ==
241
(other.x, other.y, other.width, other.height))
243
def __ne__(self, other):
244
return not self == other
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,
253
class Borders(object):
255
"""Borders encapsulate Window borders (frames/decorations)."""
257
def __init__(self, left, right, top, bottom):
264
def horizontal(self):
265
"""Return sum of left and right borders."""
266
return self.left + self.right
270
"""Return sum of top and bottom borders."""
271
return self.top + self.bottom
274
string = 'left: %s, right: %s, top: %s, bottom %s'
275
return string % (self.left, self.right, self.top, self.bottom)
278
class EventDispatcher(object):
280
"""Checks the event queue and dispatches events to correct handlers.
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.
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, }, }
298
"""Perform event queue checking.
300
Every 50ms check event queue for pending events and dispatch them.
301
If there's no registered handlers stop running.
304
logging.debug('EventDispatcher started')
305
while self.__handlers:
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')
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
323
t = threading.Thread(target=self.run)
325
return set([handler.mask
326
for handler in self.__handlers[window.id].values()])
328
def unregister(self, window, handler=None):
329
"""Unregister event handler and return new window's event mask.
331
If handler is None all handlers will be unregistered.
334
if not handler and window.id in self.__handlers:
335
logging.debug('Unregistering all handlers for window %s' %
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]
348
return set([handler.mask
349
for handler in self.__handlers[window.id].values()])
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:
363
handlers = self.__handlers[self.__root]
365
logging.error('No handler for this event')
367
if not event.type in handlers:
368
# Just skip unwanted events' types
370
handlers[event.type].handle_event(event)
373
class XObject(object):
375
"""Abstract base class for classes communicating with X Server.
377
Encapsulates common methods for communication with X Server.
381
__DISPLAY = Display()
382
__EVENT_DISPATCHER = EventDispatcher(__DISPLAY)
383
__BAD_ACCESS = error.CatchError(error.BadAccess)
385
# List of recognized key modifiers
386
__KEY_MODIFIERS = {'Alt': X.Mod1Mask,
387
'Ctrl': X.ControlMask,
388
'Shift': X.ShiftMask,
390
'NumLock': X.Mod2Mask,
391
'CapsLock': X.LockMask}
395
def __init__(self, win_id=None):
397
win_id - id of the window to be created, if no id assume it's
398
Window Manager (root window)
400
self.__root = self.__DISPLAY.screen().root
403
self._win = self.__DISPLAY.create_resource_object('window', win_id)
406
# WindowManager, act as root window
407
self._win = self.__root
408
self.id = self._win.id
412
"""Return atom with given name."""
413
return cls.__DISPLAY.intern_atom(name)
416
def atom_name(cls, atom):
417
"""Return atom's name."""
418
return cls.__DISPLAY.get_atom_name(atom)
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)
426
def send_event(self, data, type, mask):
427
"""Send event from (to?) the root window."""
428
event = protocol.event.ClientMessage(
432
self.__root.send_event(event, event_mask=mask)
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)
439
def unlisten(self, event_handler=None):
440
"""Unregister event handler(s) and update event mask.
442
If event_handler is None all handlers will be unregistered.
445
masks = self.__EVENT_DISPATCHER.unregister(self, event_handler)
446
self.__set_event_mask(masks)
448
def __set_event_mask(self, masks):
449
"""Update event mask."""
451
logging.debug('Setting %s masks for window %s' %
452
([str(e) for e in masks], self.id))
454
event_mask = event_mask | mask
455
self._win.change_attributes(event_mask=event_mask)
457
def __grab_key(self, keycode, modifiers):
459
self._win.grab_key(keycode, modifiers,
460
1, X.GrabModeAsync, X.GrabModeAsync,
461
onerror=self.__BAD_ACCESS)
463
if self.__BAD_ACCESS.get_error():
464
logging.error("Can't use %s" %
465
self.keycode2str(modifiers, keycode))
467
def grab_key(self, modifiers, keycode, numlock, capslock):
470
Grab key alone, with CapsLock on and/or with NumLock on.
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)
482
def ungrab_key(self, modifiers, keycode, numlock, capslock):
485
Ungrab key alone, with CapsLock on and/or with NumLock on.
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)
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,
503
subwindow_mode=X.IncludeInferiors,)
504
self.__root.rectangle(gc, x, y, width, height)
506
def _translate_coords(self, x, y):
507
"""Return translated coordinates.
509
Untranslated coordinates are relative to window.
510
Translated coordinates are relative to desktop.
513
return self._win.translate_coords(self.__root, x, y)
516
def str2modifiers(cls, masks, splitted=False):
517
# TODO: Check this part... not sure why it looks like that...
519
masks = masks.split('-')
523
if not mask in cls.__KEY_MODIFIERS.keys():
525
modifiers = modifiers | cls.__KEY_MODIFIERS[mask]
527
modifiers = X.AnyModifier
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
539
def str2modifiers_keycode(cls, code, key=''):
540
"""Convert key as string(s) into (modifiers, keycode) pair.
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"
552
code = '-'.join([code,key])
553
code = code.split('-')
557
modifiers = cls.str2modifiers(masks, True)
558
keycode = cls.str2keycode(key)
559
return (modifiers, keycode)
562
def keycode2str(cls, modifiers, keycode):
563
"""Convert key as (modifiers, keycode) pair into string.
565
Works ONLY for already registered keycodes!
569
for name, code in cls.__KEY_MODIFIERS.items():
573
key.append(cls.__KEYCODES[keycode])
578
"""Flush request queue to X Server."""
579
cls.__DISPLAY.flush()
583
"""Flush request queue to X Server, wait until server processes them."""
587
class Window(XObject):
589
"""Window object (X Server client?)."""
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')
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')
615
# Mode values (for maximize and shade functions)
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'):
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', ]
634
"""Return list of window's type(s)."""
635
type = self.get_property('_NET_WM_WINDOW_TYPE')
637
return [Window.TYPE_NORMAL]
642
"""Return list of window's state(s)."""
643
state = self.get_property('_NET_WM_STATE')
650
"""Return window's parent id."""
651
parent = self._win.get_wm_transient_for()
659
"""Return window's parent."""
660
parent_id = self.parent_id
662
return Window(parent_id)
668
"""Return window's name."""
669
name = self.get_property('_NET_WM_NAME')
671
name = self._win.get_full_property(Xatom.WM_NAME, 0)
677
def class_name(self):
678
"""Return window's class name."""
679
class_name = self._win.get_wm_class()
684
"""Return desktop number the window is in."""
685
desktop = self.get_property('_NET_WM_DESKTOP')
688
# returns 0xFFFFFFFF when "show on all desktops"
689
return desktop.value[0]
692
"""Return raw borders info."""
693
extents = self.get_property('_NET_FRAME_EXTENTS')
696
# Hack for Blackbox, IceWM, Sawfish, Window Maker
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)
713
"""Return window's borders (frames/decorations)."""
714
borders = self.__borders()
715
return Borders(*borders)
717
def __geometry(self):
718
"""Return raw geometry info (translated if needed)."""
719
geometry = self._win.get_geometry()
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)
735
"""Return window's geometry.
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.
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
749
return Geometry(x, y,
750
width + left + right,
751
height + top + bottom)
753
def move_resize(self, geometry, on_resize=Gravity(0, 0)):
754
"""Move or resize window using provided geometry.
756
Postion and size must include window's borders.
759
left, right, top, bottom = self.__borders()
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:
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:
784
base = hints.base_width
786
base = current[2] % hints.width_inc
787
width = ((width - base) / hints.width_inc) * hints.width_inc
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
795
base = current[3] % hints.height_inc
796
height = ((height - base) / hints.height_inc) * hints.height_inc
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)
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)
817
def maximize(self, mode,
818
vert=STATE_MAXIMIZED_VERT,
819
horz=STATE_MAXIMIZED_HORZ):
820
"""Maximize window (both vertically and horizontally)."""
825
self.__change_state(data)
827
def shade(self, mode):
828
"""Shade window (if supported by window manager)."""
832
self.__change_state(data)
834
def fullscreen(self, mode):
835
"""Make window fullscreen (if supported by window manager)."""
837
Window.STATE_FULLSCREEN,
839
self.__change_state(data)
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)
847
def sticky(self, mode):
848
"""Make window fullscreen (if supported by window manager)."""
852
self.__change_state(data)
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)
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)
868
"""For 0.25 second show borderaround window."""
870
self.draw_rectangle(geo.x+10, geo.y+10,
871
geo.width-20, geo.height-20, 20)
874
self.draw_rectangle(geo.x+10, geo.y+10,
875
geo.width-20, geo.height-20, 20)
878
def __eq__(self, other):
879
return self.id == other.id
881
def __ne__(self, other):
882
return not self.id == other.id
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())
904
class WindowManager(XObject):
906
"""Window Manager (or root window in X programming terms).
908
WindowManager's self._win refers to the root window.
913
# Instance of the WindowManager class, make it Singleton.
918
return cls.__INSTANCE
919
manager = object.__new__(cls)
920
XObject.__init__(manager)
921
cls.__INSTANCE = manager
926
"""Return window manager's name.
928
'' is returned if window manager doesn't support EWMH.
931
win_id = self.get_property('_NET_SUPPORTING_WM_CHECK')
934
win = XObject(win_id.value[0])
935
name = win.get_property('_NET_WM_NAME')
943
"""Return number of desktops."""
944
number = self.get_property('_NET_NUMBER_OF_DESKTOPS')
947
return number.value[0]
951
"""Return current desktop number."""
952
desktop = self.get_property('_NET_CURRENT_DESKTOP')
953
return desktop.value[0]
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])
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])
970
"""Return position of current viewport.
972
If desktop is large it might be divided into several viewports.
975
viewport = self.get_property('_NET_DESKTOP_VIEWPORT').value
976
return Position(viewport[0], viewport[1])
978
def active_window_id(self):
979
"""Return only id of active window."""
980
win_id = self.get_property('_NET_ACTIVE_WINDOW').value[0]
983
def active_window(self):
984
"""Return active window."""
985
window_id = self.active_window_id()
986
return Window(window_id)
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
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]
999
windows = [window for window in windows if filter_method(window)]
1001
windows = self.__name_matcher(windows, match)
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')
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
1019
(window.desktop == desktop or \
1020
window.desktop == 0xFFFFFFFF):
1022
if geometry.x < workarea.x2 and \
1023
geometry.x2 > workarea.x and \
1024
geometry.y < workarea.y2 and \
1025
geometry.y2 > workarea.y:
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]
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)
1041
WM = WindowManager()
1044
def normal_on_same_filter(window):
1045
"""Default windows filter.
1047
Returns normal, not hidde, not shaded, not fullscreen, not maximized,
1048
placed on the same desktop (or sticky), and on the same workarea.
1051
if not Window.TYPE_NORMAL in window.type:
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