#! /usr/bin/env python """ kapager - Pager specially created for kahakai Copyright (C) 2003 Hyriand - See COPYING This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA A sample ~/.kahakai/kapager.rc: # Configuration and style file for KaPager # kapager.desktop.pixmap requires python-imlib2 kapager: withdrawn: 0 orientation: horizontal switchOnMove: 1 desktop: pixmap: 1 width: 128 height: 128 border: color: black active: color: grey passive: color: darkgrey window: active: color: #4444aa passive: color: #444488 """ from Xlib import display, Xatom, X, Xutil, rdb from Xlib.xobject.drawable import Window, Pixmap import Xlib.protocol.event import Xlib.protocol.rq import sys import struct import re import string import os HORIZONTAL = 0 VERTICAL = 1 config = { "withdrawn": 0, "orient": VERTICAL, "width": 64, "height": 64, "pixmap": 0, "switch": 0, "colors": { "desktop.border": "white", "desktop.passive": "darkgrey", "desktop.active": "grey", "window.passive": "#444488", "window.active": "#4444aa", }, } _re_sec = re.compile("(\\W*)(.+?)\\W*:$") _re_val = re.compile("(\\W*)(.+?)\\W*:\\W*(.+)") def parse_config(cfgfile): def set_config(c, sections, key, value): k = string.join(sections + [key], ".") c[k] = value c = {} try: f = open(os.path.expanduser(cfgfile), "r") d = f.read() f.close() except IOError: sys.stderr.write("* Warning cannot open %s\n" % cfgfile) return c except OSError: sys.stderr.write("* Warning cannot open %s\n" % cfgfile) return c lines = d.split("\n") indents = [] sections = [] for line in lines: line = line.split("#",1)[0].expandtabs().rstrip() if line.strip() == "": continue s = _re_sec.match(line) if s: indent = len(s.groups()[0]) section = s.groups()[1] if indent and indent in indents: ix = indents.index(indent) indents = indents[:ix] + [indent] sections = sections[:ix] + [section] elif indents and indent > indents[-1]: indents.append(indent) sections.append(section) elif indent: sys.stderr.write(" * WARNING: Indent doesn't match") continue elif not indent: sections = [section] indents = [0] continue s = _re_val.match(line) if s: indent = len(s.groups()[0]) key = s.groups()[1] value = s.groups()[2] if indents and indent > indents[-1]: set_config(c, sections, key, value) elif indent and indent in indents: ix = indents.index(indent) indents = indents[:ix] sections = sections[:ix] set_config(c, sections, key, value) elif indent: sys.stderr.write(" * WARNING: Unmatching indention: %s" % line) elif not indent: indents = [] sections = [] set_config(c, sections, key, value) return c def load_config(cfgfile): c = parse_config(cfgfile) if c.has_key("kapager.withdrawn"): if c["kapager.withdrawn"].lower() in ["1", "true"]: config["withdrawn"] = 1 else: config["withdrawn"] = 0 if c.has_key("kapager.switchOnMove"): if c["kapager.switchOnMove"].lower() in ["1", "true"]: config["switch"] = 1 else: config["switch"] = 0 if c.has_key("kapager.orientation"): if c["kapager.orientation"].lower()[0] == "h": config["orient"] = HORIZONTAL else: config["orient"] = VERTICAL if c.has_key("kapager.desktop.pixmap"): if c["kapager.desktop.pixmap"].lower() in ["1", "true"]: config["pixmap"] = 1 else: config["pixmap"] = 0 if c.has_key("kapager.desktop.width"): config["width"] = int(c["kapager.desktop.width"]) if c.has_key("kapager.desktop.height"): config["height"] = int(c["kapager.desktop.height"]) for i in ["desktop", "window"]: for j in ["border", "active", "passive"]: key = "kapager.%s.%s.color" % (i, j) if c.has_key(key): subkey = "%s.%s" % (i, j) config["colors"][subkey] = c[key] # Somewhat ugly re-implementation since python-xlib doesn't support initial state to be withdrawn WMHintsX = Xlib.protocol.rq.Struct( Xlib.protocol.rq.Card32('flags'), Xlib.protocol.rq.Card32('input', default = 0), Xlib.protocol.rq.Set('initial_state', 4, ( Xutil.WithdrawnState, Xutil.NormalState, Xutil.IconicState ), default = Xutil.NormalState), Xlib.protocol.rq.Pixmap('icon_pixmap', default = 0), Xlib.protocol.rq.Window('icon_window', default = 0), Xlib.protocol.rq.Int32('icon_x', default = 0), Xlib.protocol.rq.Int32('icon_y', default = 0), Xlib.protocol.rq.Pixmap('icon_mask', default = 0), Xlib.protocol.rq.Window('window_group', default = 0), ) class MyPixmap(Pixmap): def line(self, gc, x1, y1, x2, y2, onerror = None): Xlib.protocol.request.PolySegment(display = self.display, onerror = onerror, drawable = self.id, gc = gc, segments = [(x1, y1, x2, y2)]) class KaClient: def __init__(self, parent, win): self.client = win self.desktop = parent self.pager = parent.pager self.screen = parent.screen self.depth = self.pager.depth self.display = parent.display self.pixmap = None self.x = self.y = 0 self.w = self.h = 2 self.is_active = 0 self.move_x = self.move_y = 0 self.window = self.make_window() self.update_position() self.update_state() win.change_attributes(event_mask = ( X.PropertyChangeMask | X.StructureNotifyMask) ) def __del__(self): self.window.unmap() self.window.destroy() if self.pixmap: self.pixmap.free() def make_window(self): w = self.desktop.window.create_window( 0, 0, 2, 2, border_width = 0, depth = self.depth, window_class = X.InputOutput, event_mask = ( X.ButtonReleaseMask | X.ButtonPressMask | X.Button1MotionMask ), ) return w def render_bg(self): if self.pixmap: self.pixmap.free() w, h = self.w, self.h p = self.window.create_pixmap(w, h, self.depth) p = MyPixmap(self.display.display, p.id) if self.is_active: color = "window.active" else: color = "window.passive" gc = p.create_gc(foreground = self.pager.colors[color]) p.fill_rectangle(gc, 1, 1, w-2, h-2) gc.change(foreground = self.pager.colors[color + ".hi"]) p.line(gc, 0, 0, w - 1, 0) p.line(gc, 0, 1, 0, h - 1) gc.change(foreground = self.pager.colors[color + ".lo"]) p.line(gc, 1, h - 1, w - 1, h - 1) p.line(gc, w - 1, 1, w - 1, h - 2) gc.free() self.window.change_attributes( background_pixmap = p ) self.window.clear_area() self.pixmap = p def get_virtual_pos(self, window): pos = window.get_full_property(self.pager._WAIMEA_NET_VIRTUAL_POS, Xatom.INTEGER) if not pos: return 0, 0 return pos.value[0], pos.value[1] def set_virtual_pos(self, x, y): x = struct.unpack("I", struct.pack("i", x))[0] y = struct.unpack("I", struct.pack("i", y))[0] self.pager.send_client_message(self.client, self.pager._WAIMEA_NET_VIRTUAL_POS, 32, x, y) def update_position(self, width = None, height = None): x, y = self.get_virtual_pos(self.client) x = struct.unpack("i", struct.pack("I", x))[0] y = struct.unpack("i", struct.pack("I", y))[0] r_x = int(self.desktop.f_w * x) r_y = int(self.desktop.f_h * y) geom = self.client.get_geometry() if width is None: width = geom.width if height is None: height = geom.height r_w = int(self.desktop.f_w * width) r_h = int(self.desktop.f_h * height) if r_x < 0: r_w += r_x r_x = 0 if r_y < 0: r_h += r_y r_y = 0 if r_w + r_x >= self.desktop.w: r_w = self.desktop.w - r_x if r_h + r_y >= self.desktop.h: r_h = (self.desktop.h - r_y) if r_w < 2: r_w = 2 if r_h < 2: r_h = 2 self.x = self.desktop.x + r_x self.y = self.desktop.y + r_y resized = 0 if self.w != r_w or self.h != r_h: resized = 1 self.w = r_w self.h = r_h self.window.configure(x = self.x, y = self.y, width = r_w, height = r_h) if resized: self.render_bg() def validate_state(self): state = self.client.get_full_property(self.pager._NET_WM_STATE, 0) if not state: return 1 if self.pager._NET_WM_STATE_HIDDEN in state.value: return 0 if self.pager._NET_WM_STATE_SKIP_TASKBAR in state.value: return 0 return 1 def update_state(self): if self.validate_state(): self.window.map() else: self.window.unmap() def client_raise(self, switch = True): current = self.pager.get_current_desktop() == self.desktop.number if switch and not current: self.pager.raise_on_desktop = self self.pager.send_client_message(self.pager.root, self.pager._NET_CURRENT_DESKTOP, 32, self.desktop.number) else: self.client.configure(stack_mode = X.Above) if current: self.client.set_input_focus(X.RevertToNone, X.CurrentTime) def get_desktop_mask(self): return self.desktop.get_desktop_mask(self.client) def start_move(self, e): self.move_x = e.event_x self.move_y = e.event_y def move(self, e): self.x = self.x + e.event_x - self.move_x self.y = self.y + e.event_y - self.move_y self.window.configure(x = self.x, y = self.y, stack_mode = X.Above) def end_move(self, e): x = self.x + e.event_x y = self.y + e.event_y desktop = self.pager.find_desktop(x, y) if not desktop: return if desktop != self.desktop: new_mask = self.get_desktop_mask() new_mask -= new_mask & (1L << self.desktop.number) new_mask |= 1L << desktop.number self.pager.send_client_message(self.client, self.pager._WAIMEA_NET_WM_DESKTOP_MASK, 32, new_mask) self.desktop = desktop self.window.unmap() x -= self.move_x + desktop.x y -= self.move_y + desktop.y x /= desktop.f_w y /= desktop.f_h self.set_virtual_pos(int(x), int(y)) def active(self, active): if self.is_active == active: return self.is_active = active self.render_bg() class KaDesktop: def __init__(self, parent, number, x, y, w, h, d_w, d_h): self.pager = parent self.display = parent.display self.screen = parent.screen self.root = parent.root self.window = parent.window self.number = number self.x = x self.y = y self.w = w self.h = h self.d_w = d_w self.d_h = d_h self.f_w = float(w) / float(d_w) self.f_h = float(h) / float(d_h) self.clients = {} self.stack_atom_desktop = self.display.intern_atom("_KAHAKAI_NET_CLIENT_LIST_STACKING_DESKTOP_%d" % number) self.stack_atom = self.display.intern_atom("_NET_CLIENT_LIST_STACKING") def get_stacking_list(self): stack = self.root.get_full_property(self.stack_atom_desktop, Xatom.WINDOW) if stack is None: stack = self.root.get_full_property(self.stack_atom, Xatom.WINDOW) winlist = [] for w_id in stack.value: win = Window(self.display.display, w_id) winlist.append(win) return winlist def update(self): stack = self.get_stacking_list() id_stack = [win.id for win in stack] restack = [] for win in stack: mask = self.get_desktop_mask(win) if mask & (1L << self.number): if win.id in self.clients: self.clients[win.id].update_position() else: client = KaClient(self, win) self.clients[win.id] = client restack.append(self.clients[win.id].window) else: if win.id in self.clients: self.clients[win.id].window.unmap() del self.clients[win.id] for client_id in self.clients.keys()[:]: client = self.clients[client_id] client_win = client.window if not client_win in restack: del self.clients[client_id] restack.reverse() for win in restack: win.configure(stack_mode = X.Below) def get_desktop_mask(self, window): mask = window.get_full_property(self.pager._WAIMEA_NET_WM_DESKTOP_MASK, Xatom.CARDINAL) return mask.value[0] def update_state(self, window): if window.id in self.clients.keys(): self.clients[window.id].update_state() def update_position(self, window, width = None, height = None): if window.id in self.clients.keys(): self.clients[window.id].update_position(width, height) def window_raise(self, window): for client in self.clients.values(): if client.window.id == window.id: client.client_raise() self.update() def find_window(self, window): for client in self.clients.values(): if client.window.id == window.id: return client return None def active(self, window, desktop): for client in self.clients.values(): client.active(window == client.client.id and desktop == self.number) class KaPager: def __init__(self): self.display = display.Display() self.screen = self.display.screen() self.root = self.screen.root self.depth = self.screen.root_depth if config["pixmap"]: try: import imlib2 self.imlibx = imlib2.ImlibX() except: pass self.pmap = None self.active = None self.raise_on_desktop = None self.root.change_attributes(event_mask = X.PropertyChangeMask) self.get_atoms() if not self.wm_check(): raise RuntimeError, "WM does not support NET properties or no WM running" if not self.check_atoms(): raise RuntimeError, "Required properties not supported" num_desktops = self.get_num_desktops() if config["orient"] == VERTICAL: self.w = w = config["width"] + 2 self.h = h = (config["height"] + 1) * num_desktops + 1 else: self.w = w = (config["width"] + 1) * num_desktops + 1 self.h = h = config["height"] + 2 d_w, d_h = self.get_desktop_geometry() self.window = self.make_window(w, h) self.colors = {} colormap = self.window.list_installed_colormaps()[0] for color in config["colors"]: _color = colormap.alloc_named_color(config["colors"][color]) if not _color: sys.stderr.write(" * ERROR Couldn't allocate color %s" % color) self.colors[color] = self.screen.black_pixel self.colors[color+".hi"] = self.screen.white_pixel self.colors[color+".lo"] = self.screen.white_pixel else: self.colors[color] = _color.pixel if _color._data.has_key("exact_red"): _red = _color.exact_red _green = _color.exact_green _blue = _color.exact_blue else: _red = _color.red _green = _color.green _blue = _color.blue red = _red + (_red >> 1) if red > 65535: red = 65535 green = _green + (_green >> 1) if green > 65535: green = 65535 blue = _blue + (_blue >> 1) if blue > 65535: blue = 65535 _hi = colormap.alloc_color(red, green, blue) if not _hi: sys.stderr.write(" * ERROR Couldn't allocate high color for %s" % color) self.colors[color + ".hi"] = _color.pixel else: self.colors[color + ".hi"] = _hi.pixel red = (_red >> 2) + (_red >> 1) green = (_green >> 2) + (_green >> 1) blue = (_blue >> 2) + (_blue >> 1) _lo = colormap.alloc_color(red, green, blue) if not _lo: sys.stderr.write(" * ERROR Couldn't allocate low color for %s" % color) self.colors[color + ".lo"] = _color.pixel else: self.colors[color + ".lo"] = _lo.pixel self.desktops = [] for number in range(num_desktops): if config["orient"] == VERTICAL: x = 1 y = number * (config["height"] + 1) + 1 else: x = number * (config["width"] + 1) + 1 y = 1 desktop = KaDesktop(self, number, x, y, config["width"], config["height"], d_w, d_h) desktop.update() self.desktops.append(desktop) self.pixmap = self.make_bg_pixmap(w, h) self.render_desktops() def get_atoms(self): self._NET_SUPPORTED = self.display.intern_atom("_NET_SUPPORTED") self._NET_SUPPORTING_WM_CHECK = self.display.intern_atom("_NET_SUPPORTING_WM_CHECK") self._NET_CLIENT_LIST = self.display.intern_atom("_NET_CLIENT_LIST") self._NET_CLIENT_LIST_STACKING = self.display.intern_atom("_NET_CLIENT_LIST_STACKING") self._NET_NUMBER_OF_DESKTOPS = self.display.intern_atom("_NET_NUMBER_OF_DESKTOPS") self._NET_DESKTOP_GEOMETRY = self.display.intern_atom("_NET_DESKTOP_GEOMETRY") self._NET_CURRENT_DESKTOP = self.display.intern_atom("_NET_CURRENT_DESKTOP") self._WAIMEA_NET_VIRTUAL_POS = self.display.intern_atom('_WAIMEA_NET_VIRTUAL_POS') self._WAIMEA_NET_WM_DESKTOP_MASK = self.display.intern_atom("_WAIMEA_NET_WM_DESKTOP_MASK") self._WM_STATE = self.display.intern_atom("WM_STATE") self._NET_ACTIVE_WINDOW = self.display.intern_atom("_NET_ACTIVE_WINDOW") self._NET_WM_STATE = self.display.intern_atom("_NET_WM_STATE") self._NET_WM_STATE_HIDDEN = self.display.intern_atom("_NET_WM_STATE_HIDDEN") self._NET_WM_STATE_SKIP_TASKBAR = self.display.intern_atom("_NET_WM_STATE_SKIP_TASKBAR") self._XROOTPMAP_ID = self.display.intern_atom("_XROOTPMAP_ID") self.ESETROOT_PMAP_ID = self.display.intern_atom("ESETROOT_PMAP_ID") def get_current_desktop(self): desktop = self.root.get_full_property(self._NET_CURRENT_DESKTOP, Xatom.CARDINAL) return desktop.value[0] def wm_check(self): wm_check = self.root.get_full_property(self._NET_SUPPORTING_WM_CHECK, Xatom.WINDOW) if not wm_check: return False win = Window(self.display.display, wm_check.value[0]) wm_check = win.get_full_property(self._NET_SUPPORTING_WM_CHECK, Xatom.WINDOW) if win.id == wm_check.value[0]: return True return False def check_atoms(self): supported = self.root.get_full_property(self._NET_SUPPORTED, Xatom.ATOM) if self._NET_NUMBER_OF_DESKTOPS not in supported.value: return False return True def get_num_desktops(self): desktops = self.root.get_full_property(self._NET_NUMBER_OF_DESKTOPS, Xatom.CARDINAL) return desktops.value[0] def get_desktop_geometry(self): viewport = self.root.get_full_property(self._NET_DESKTOP_GEOMETRY, Xatom.CARDINAL) return viewport.value[0], viewport.value[1] def set_wm_hints(self, top, hints = {}, onerror = None, **keys): top._set_struct_prop(Xatom.WM_HINTS, Xatom.WM_HINTS, WMHintsX, hints, keys, onerror) def make_window(self, w, h): top = self.root.create_window( 0, 0, w, h, 0, self.depth, background_pixel = self.screen.black_pixel ) w = top.create_window( 0, 0, w, h, 0, self.depth, X.InputOutput, event_mask = ( X.ButtonPressMask | X.ButtonReleaseMask ) ) top.set_wm_name("KaPager") top.set_wm_icon_name("KaPager") top.set_wm_class("pager", "KaPager") if config["withdrawn"]: self.set_wm_hints(top, flags = Xutil.StateHint, initial_state = Xutil.WithdrawnState) top.map() w.map() return w def render_desktops(self): current = self.get_current_desktop() pmap = None if config["pixmap"]: pmap = self.root.get_full_property(self._XROOTPMAP_ID, Xatom.PIXMAP) if not pmap: pmap = self.root.get_full_property(self.ESETROOT_PMAP_ID, Xatom.PIXMAP) if pmap: pix = Pixmap(self.display.display, pmap.value[0]) geom = pix.get_geometry() img = self.imlibx.image_from_drawable(pmap.value[0], 0, 0, 0, geom.width, geom.height) img = img.scale(config["width"], config["height"]) gc = self.pixmap.create_gc() for desktop in self.desktops: if pmap: if self.pmap == pmap.value[0]: continue self.imlibx.render_image_on_drawable(img, self.pixmap.id, dest_rect = [desktop.x, desktop.y], ) else: self.pmap = None if desktop.number == current: color = "desktop.active" else: color = "desktop.passive" gc.change(foreground = self.colors[color]) self.pixmap.fill_rectangle(gc, desktop.x, desktop.y, desktop.w, desktop.h ) if pmap: self.pmap = pmap.value[0] else: self.pmap = None gc.free() self.window.clear_area() def make_bg_pixmap(self, w, h): p = self.window.create_pixmap(w, h, self.depth) gc = p.create_gc(foreground = self.colors["desktop.border"]) p.fill_rectangle(gc, 0, 0, w, h) gc.free() self.window.change_attributes( background_pixmap = p ) return p def send_client_message(self, window, type, fmt, *data): data = list(data[:5]) data += [0] * (160 / fmt - len(data)) ev = Xlib.protocol.event.ClientMessage(window = window, client_type = type, data = (fmt, data)) self.root.send_event(ev, event_mask=X.SubstructureRedirectMask) def find_desktop(self, x, y): for desktop in self.desktops: if x>=desktop.x and x<=desktop.x+desktop.w and y>=desktop.y and y<=desktop.y+desktop.w: return desktop return None def find_window(self, window): for desktop in self.desktops: win = desktop.find_window(window) if win: return win return None def switch_desktop(self, e): if e.window.id == self.window.id: desktop = self.find_desktop(e.event_x, e.event_y) if not desktop: return else: win = self.find_window(e.window) if not win: return desktop = win.desktop self.send_client_message(self.root, self._NET_CURRENT_DESKTOP, 32, desktop.number) def run(self): while 1: e = self.display.next_event() if e.type == X.PropertyNotify: if e.atom == self._NET_CLIENT_LIST_STACKING: for desktop in self.desktops: desktop.update() elif e.atom == self._WM_STATE: for desktop in self.desktops: desktop.update_state(e.window) elif e.atom == self._WAIMEA_NET_VIRTUAL_POS: for desktop in self.desktops: desktop.update_position(e.window) elif e.atom == self._NET_CURRENT_DESKTOP: if self.raise_on_desktop: self.raise_on_desktop.client_raise() self.raise_on_desktop = None self.render_desktops() self.window.clear_area() elif e.atom == self._NET_ACTIVE_WINDOW: win = self.root.get_full_property(e.atom, Xatom.WINDOW).value[0] for desktop in self.desktops: desktop.active(win, self.get_current_desktop()) elif e.atom == self._XROOTPMAP_ID or e.atom == self.ESETROOT_PMAP_ID: self.render_desktops() self.window.clear_area() elif e.type == X.ConfigureNotify: for desktop in self.desktops: desktop.update_position(e.window) elif e.type == X.ButtonPress and e.detail == 1: for desktop in self.desktops: win = desktop.find_window(e.window) if win: break else: self.active = None continue self.active = win win.start_move(e) self.motion = 0 elif e.type == X.MotionNotify and self.active: self.active.move(e) self.motion = 1 elif e.type == X.ButtonRelease: if e.detail == 1 and self.active: if self.motion: self.active.end_move(e) self.active.client_raise(config["switch"]) else: self.active.client_raise() elif e.detail == 3: self.switch_desktop(e) if __name__ == "__main__": load_config("~/.kahakai/kapager.rc") KaPager().run()