#!/usr/bin/env python
"""
PyPanel v0.9 - Lightweight panel/taskbar for X11 window managers
Copyright (c) 2003 Jon Gelo (ziljian@users.sourceforge.net)

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.
"""
#----------------------------------------------------------------------------
class PyPanel(object):
#----------------------------------------------------------------------------
    #---------------------------
    def __init__(self, display):
    #---------------------------
        """ Initialize and display the panel """
        self.display    = display                   # Display obj
        self.screen     = display.screen()          # Screen obj
        self.root       = self.screen.root          # Display root
        self.depth      = self.screen.root_depth    # Screen depth
        self.startx     = P_START                   # Starting x coord
        self.bgpm       = None                      # BG pixmap
        self.workspace  = -1                        # Current workspace
        self.tasks      = [{}]                      # Current tasks per WS
        self.colors     = {}                        # Alloc'd colors
                
        # Initialize the font, GC (Graphics Context) and default icon
        self.font = self.display.open_font(FONT)
        if not self.font:
            self.font = self.display.open_font("fixed")
        fontsize = self.font.query()
        height   = fontsize.font_ascent + fontsize.font_descent
        self.font_w = self.font.query_text_extents("m").overall_width
        self.font_y = (P_HEIGHT-height)/2 + height-1
        self.gc = self.root.create_gc(font = self.font,
                                      clip_y_origin=(P_HEIGHT-I_HEIGHT)/2)
        self.icon, self.mask = self.initIcon()
        
        # Set panel position and also set width to screen width if unset
        if P_LOCATION:
            self.location = self.screen.height_in_pixels - P_HEIGHT
        else:
            self.location = 0
        global P_WIDTH
        if not P_WIDTH:
            P_WIDTH = self.screen.width_in_pixels
        self.endx = P_WIDTH
        
        # Create the panel window
        self.window = self.screen.root.create_window(P_START,
            self.location, P_WIDTH, P_HEIGHT, 0,
            self.depth, X.InputOutput, X.CopyFromParent,
            colormap = X.CopyFromParent,
            event_mask = (X.ExposureMask | X.ButtonReleaseMask))
                        
        # Set the window properties and allocate room for extra workspaces
        self.setProps(self.display, self.window)
        for ws in range(15):
            self.tasks.append({})
        
        # Register the root window for events (ws changes, new tasks, etc.)
        self.root.change_attributes(event_mask=(X.PropertyChangeMask))
                
        # Finally, map the window and wait for events
        self.setBackground()
        self.window.map()
        self.loop(self.display, self.root, self.window, self.ewmh)
        
    #------------------
    def initIcon(self):
    #------------------
        """ Initialize the default icon - uses ppicon() to create a
            pixmap from an XPM using XpmCreatePixmapFromData(), required 
            libXpm at build time.
        """
        try:
            icon, mask, w, h = ppicon(self.root.id)
            icon = self.display.create_resource_object('pixmap', icon)
            mask = self.display.create_resource_object('pixmap', mask)
            return self.getIcon(icon, mask, w, h, self.depth)
        except:
            return None, None
        
    #-----------------------
    def setBackground(self):
    #-----------------------
        """ Set the panel background color or image and shade if configured.
            The method used for the transparency effect simply copies a 
            portion of the root into the panel - anything covering the
            root image where the panel is drawn will be included.
        """
        if not TRANSPARENCY:
            self.window.change_attributes(background_pixel = self.getColor(BG_COLOR))
            return
        
        img = self.root.get_image(P_START, self.location, P_WIDTH, P_HEIGHT,
                                  X.ZPixmap, -1).data
        if not self.bgpm:
            self.bgpm = self.root.create_pixmap(P_WIDTH, P_HEIGHT, self.depth)
        for format in self.bgpm.display.info.pixmap_formats:
            if format['depth'] == self.depth:
                bpp = format['bits_per_pixel']
        img = ppshade(img, bpp, P_WIDTH, P_HEIGHT, SHADE)
        self.bgpm.put_image(self.gc, 0, 0, P_WIDTH, P_HEIGHT, X.ZPixmap,
                            self.depth, 0, img)
        self.window.change_attributes(background_pixmap = self.bgpm)
        
    #----------------------------
    def setProps(self, dsp, win):
    #----------------------------
        """ Set general and wm specific properties/hints """
        self.ewmh    = dsp.intern_atom("_NET_SUPPORTING_WM_CHECK", 1)
        self.openbox = dsp.intern_atom("_OPENBOX_PID", 1)
        win.set_wm_name('PyPanel')
        win.set_wm_class('pypanel','PyPanel')
        win.set_wm_hints(flags = Xutil.InputHint, input = 0)
        win.set_wm_normal_hints(flags = (Xutil.PPosition | 
                                         Xutil.PSize     |
                                         Xutil.PMinSize),
                                min_width = P_WIDTH, min_height = P_HEIGHT)
        
        if self.ewmh:
            self._WORKSPACE_NAMES = dsp.intern_atom("_NET_DESKTOP_NAMES")        
            self._CURRENT_DESKTOP = dsp.intern_atom("_NET_CURRENT_DESKTOP")
            self._NUM_WORKSPACES  = dsp.intern_atom("_NET_NUMBER_OF_DESKTOPS")
            self._WORKSPACE       = dsp.intern_atom("_NET_WM_DESKTOP")
            self._CLIENT_LIST     = dsp.intern_atom("_NET_CLIENT_LIST")
            self._HIDDEN          = dsp.intern_atom("_NET_WM_STATE_HIDDEN")
            self._SHADED          = dsp.intern_atom("_NET_WM_STATE_SHADED")
            self._STATE           = dsp.intern_atom("_NET_WM_STATE")
            self._VISIBLE_NAME    = dsp.intern_atom("_NET_WM_VISIBLE_NAME")
            wmtype                = dsp.intern_atom("_NET_WM_WINDOW_TYPE")
            dock                  = dsp.intern_atom("_NET_WM_WINDOW_TYPE_DOCK")
            strut                 = dsp.intern_atom("_NET_WM_STRUT")
            strutp                = dsp.intern_atom("_NET_WM_STRUT_PARTIAL")
            
            if P_LOCATION:
                top = top_start = top_end = 0
                bottom       = P_HEIGHT
                bottom_start = P_START
                bottom_end   = P_START + P_WIDTH
            else:
                top       = P_HEIGHT
                top_start = P_START
                top_end   = P_START + P_WIDTH
                bottom = bottom_start = bottom_end = 0
            
            win.change_property(wmtype, Xatom.ATOM, 32, [dock])
            win.change_property(self._WORKSPACE, Xatom.CARDINAL, 32, [0xffffffffL])
            win.change_property(strut, Xatom.CARDINAL, 32, [0,0,top,bottom])
            win.change_property(strutp, Xatom.CARDINAL, 32,
                                [0, 0, top, bottom, 0, 0, 0, 0, top_start,
                                top_end, bottom_start, bottom_end])
                                
        else:
            self._WORKSPACE_NAMES = dsp.intern_atom("_WIN_WORKSPACE_NAMES") 
            self._NUM_WORKSPACES  = dsp.intern_atom("_WIN_WORKSPACE_COUNT")
            self._WORKSPACE       = dsp.intern_atom("_WIN_WORKSPACE")
            self._CLIENT_LIST     = dsp.intern_atom("_WIN_CLIENT_LIST")
            self._STATE           = dsp.intern_atom("_WIN_STATE")
            self._VISIBLE_NAME    = dsp.intern_atom("WM_NAME")
            motif                 = dsp.intern_atom("_MOTIF_WM_HINTS")
            layer                 = dsp.intern_atom("_WIN_LAYER")
            hints                 = dsp.intern_atom("_WIN_HINTS")
            win.change_property(self._WORKSPACE, Xatom.CARDINAL, 16, [0])
            win.change_property(self._STATE, Xatom.CARDINAL, 16, [1])
            win.change_property(layer, Xatom.CARDINAL, 32, [10])
            win.change_property(hints, Xatom.CARDINAL, 32, [39])
            win.change_property(motif, motif, 32, [0x2, 0x0, 0x0, 0x0, 0x0])
     
    #----------------------------
    def getWS(self, win, root=0):
    #----------------------------
        """ Return the workspace number of the given window object """
        try:
            if self.ewmh and root:
                return win.get_full_property(self._CURRENT_DESKTOP,
                                             Xatom.CARDINAL).value[0]
            return win.get_full_property(self._WORKSPACE,
                                         Xatom.CARDINAL).value[0]
        except:
            return None 
            
    #-------------------------
    def getColor(self, color):
    #-------------------------
        """ Function to get/convert/alloc a color given a single hex str """
        try:
            return self.colors[color]
        except:
            pass
            
        r = int('0x'+color[2:4],0)*257
        g = int('0x'+color[4:6],0)*257
        b = int('0x'+color[6:8],0)*257
        c = self.screen.default_colormap.alloc_color(r, g, b)
        
        if not c:
            sys.stderr.write("Error allocating color: %s\n" % color)
            return self.screen.white_pixel
        else:
            self.colors[color] = c.pixel
            return c.pixel
        
    #----------------------------------------------------------------
    def getIcon(self, icon=None, mask=None, w=0, h=0, d=0, obj=None):
    #----------------------------------------------------------------
        """ Get and/or scale an icon to match the configuration size """
        if not APPICONS:
            global I_WIDTH
            I_WIDTH = 0
            return None, None
        
        if not icon:
            # Get the icon and mask from the given obj
            try:
                icon = obj.get_wm_hints()['icon_pixmap']
                mask = obj.get_wm_hints()['icon_mask']
                geo  = icon.get_geometry()
                d    = geo._data['depth']
                w    = geo._data['width']
                h    = geo._data['height']
                try:
                    mask.get_geometry()
                except:
                    mask = None
                    
                if w == I_WIDTH and h == I_HEIGHT:
                    return icon, mask
            except:
                return (self.icon, self.mask)
                
        # Begin scaling
        sd   = self.depth
        gc   = self.gc
        npix = self.root.create_pixmap(I_WIDTH, I_HEIGHT, sd)
        if mask:
            nmsk = self.root.create_pixmap(I_WIDTH, I_HEIGHT, 1)
            mgc  = nmsk.create_gc(graphics_exposures = 0,
                                  subwindow_mode = X.IncludeInferiors)
        else:
            nmsk = None
        
        for y in range(I_HEIGHT-1, -1, -1):
            y2 = y*h/I_HEIGHT
            for x in range(I_WIDTH-1, -1, -1):
                x2 = x*w/I_WIDTH
                if mask:
                    nmsk.copy_area(mgc, mask, x2, y2, 1, 1, x, y)
                if d == sd:
                    npix.copy_area(gc, icon, x2, y2, 1, 1, x, y)
                else:
                    npix.copy_plane(gc, icon, x2, y2, 1, 1, x, y, 1)
                    
        if mask:
            mgc.free()
        return npix, nmsk
        
    #-----------------------------
    def addTasks(self, ws, total):
    #-----------------------------
        """ Add tasks to the panel """
        win = self.window
        gc  = self.gc
        self.curr_x = self.startx
        win.clear_area()
        
        if LEFT_OBJ:
            self.updatePanelObj(LEFT_OBJ, ws=ws)
        if RIGHT_OBJ:
            self.updatePanelObj(RIGHT_OBJ, ws=ws)
        
        if total:
            count      = 1
            icony      = (P_HEIGHT-I_HEIGHT)/2
            task_limit = (self.endx-self.startx) / total
            name_limit = int((task_limit/self.font_w) - 
                            ((P_SPACER*3+I_WIDTH) / self.font_w)) - 1
            # Begin adding tasks
            for t in self.tasks[ws].values():
                if not SHOWALL and t.state != Xutil.IconicState:
                    continue
                    
                x = self.curr_x + P_SPACER
                try:
                    ichk = t.obj.get_wm_hints()['icon_pixmap'].id
                except:
                    ichk = None
            
                if t.ichk != ichk:
                    t.icon, t.mask = self.getIcon(obj=t.obj)
                    t.ichk = ichk
                if t.icon:
                    if t.mask:
                        gc.change(clip_mask = t.mask, clip_x_origin = x) 
                    win.copy_area(gc, t.icon, 0, 0, I_WIDTH,I_HEIGHT,x,icony)
                    gc.change(clip_mask = X.NONE)  
                    x += I_WIDTH + P_SPACER
                gc.change(foreground = t.color) 
                win.draw_text(gc, x, self.font_y, t.name[:name_limit])
                if count < total:
                    l = self.curr_x + task_limit
                    self.drawLine(l)
                x2 = task_limit + self.curr_x
                self.tasks[ws][t.id].pos = (self.curr_x, x2)
                self.curr_x = x2 + 1
                count += 1
        
    #-----------------------------------------------
    def updatePanelObj(self, obj, clear=0, ws=None):
    #-----------------------------------------------
        """ Update the right or left side panel obj (clock/workspace) """
        side = "left"
        if obj == "clock":
            text  = time.strftime(CLOCK_FORMAT, time.localtime())
            color = CLOCK_COLOR
            if RIGHT_OBJ == "clock":
                side = "right"
        else:
            color = WS_COLOR
            self.workspace = ws
            if RIGHT_OBJ == "workspace":
                side = "right"
            try:
                names = self.root.get_full_property(self._WORKSPACE_NAMES,
                                                    X.AnyPropertyType)
                text  = names.value.split("\x00")[ws]
            except:
                text  = str(ws)

        size = self.font.query_text_extents(text).overall_width
        
        if side == "right":
            if clear:
                self.window.clear_area(self.endx, 0, P_WIDTH, P_HEIGHT)
            self.endx = (P_WIDTH - size - P_SPACER*2) 
            self.drawLine(self.endx)
            self.gc.change(foreground = self.getColor(color))
            self.window.draw_text(self.gc, self.endx+P_SPACER, self.font_y, text)
        else:
            if clear:
                self.window.clear_area(0, 0, self.startx, P_HEIGHT)
            self.gc.change(foreground = self.getColor(color))
            self.window.draw_text(self.gc, P_SPACER, self.font_y, text)
            self.curr_x = size + P_SPACER*2
            self.drawLine(self.curr_x)
            self.curr_x += 1 
            self.startx = self.curr_x
            
    #-----------------------------------------
    def updatePanel(self, dsp, root, id=None):
    #-----------------------------------------
        """ Perform necessary functions to update the panel items """
        #----------------------
        def setState(t, state):
        #----------------------
            """ Set a task obj state and text color according to state """
            if state == Xutil.IconicState:
                t.color = self.getColor(MINIMIZED_COLOR)
                t.state = Xutil.IconicState
            elif state == Xutil.NormalState:
                t.state = Xutil.NormalState
                if self.ewmh:
                    hints = t.obj.get_full_property(self._STATE, Xatom.ATOM).value
                    if self._SHADED in hints:
                        t.color = self.getColor(SHADED_COLOR)
                        return
                else:
                    ws = t.obj.get_full_property(self._STATE, Xatom.CARDINAL).value
                    if ws[0] == 32:
                        t.color = self.getColor(SHADED_COLOR)
                        return
                t.color = self.getColor(TASK_COLOR)
                
        # Update panel -
        tasks  = root.get_full_property(self._CLIENT_LIST, X.AnyPropertyType)
        update = 0
        check  = 1
        key    = time.time()
        cws    = self.getWS(root, 1)
        
        if id:
            try:
                t = self.tasks[cws][id]
                setState(t, t.obj.get_wm_state()['state'])
            except:
                pass
            update = 1
            check  = 0
        
        elif tasks and tasks.value:
            for task in tasks.value:
                obj = dsp.create_resource_object('window', task)   
                try:
                    tclass = obj.get_wm_class()
                    state  = obj.get_wm_state()['state']
                    name   = obj.get_full_property(self._VISIBLE_NAME,
                                                   X.AnyPropertyType)
                    if name:
                        name = name.value
                    else:
                        name = obj.get_wm_name() or ""
                except:
                    continue
                    
                if cws == self.getWS(obj):
                    obj.change_attributes(event_mask=(X.PropertyChangeMask |
                                                      X.StructureNotifyMask))
                    if task not in self.tasks[cws]:
                        update = 1
                        t = Task(task, obj, key, name, cws, tclass)
                        setState(t, state)
                        self.tasks[cws][task] = t
                    elif (task in self.tasks[cws]) and\
                         (name != self.tasks[cws][task].name):
                        update = 1
                        self.tasks[cws][task].name = name
                        self.tasks[cws][task].key  = key
                    else:
                        self.tasks[cws][task].key = key
        
        # Remove stale objs and reset task positions if updating
        total = 0
        for task in self.tasks[cws].keys():
            t = self.tasks[cws][task]
            if update:
                t.pos = (-1, -1)
            if check and t.key != key:
                update = 1
                del self.tasks[cws][task]
            elif SHOWALL or (not SHOWALL and t.state == Xutil.IconicState):
                total += 1
                    
        if update or cws != self.workspace:
            self.addTasks(cws, total)
            
    #---------------------
    def drawLine(self, x):
    #---------------------
        """ Draw a colored line at x """
        if SHOWLINES:
            self.gc.change(foreground = self.getColor(LINE_COLOR))
            self.window.poly_segment(self.gc, [(x, 0, x, P_HEIGHT)])
        
    #-------------------------------------
    def sendEvent(self, win, ctype, data):
    #-------------------------------------
        """ Send a ClientMessage event to the root """
        data = (data+[0]*(5-len(data)))[:5]
        ev = Xlib.protocol.event.ClientMessage(window      = win,
                                               client_type = ctype,
                                               data        = (32,(data)))
        self.root.send_event(ev, event_mask=(X.SubstructureRedirectMask |
                                             X.SubstructureNotifyMask))
                    
    #------------------------------
    def changeWorkspace(self, num):
    #------------------------------
        """ Increase/Decrease the current workspace number by num """
        cur = self.getWS(self.root, 1)
        tot = self.root.get_full_property(self._NUM_WORKSPACES,
                                          X.AnyPropertyType).value[0] - 1
            
        cur += num
        if cur < 0:
            cur = tot
        elif cur > tot:
            cur = 0
            
        if self.ewmh:
            self.sendEvent(self.root, self._CURRENT_DESKTOP, [cur])
        else:
            self.sendEvent(self.root, self._WORKSPACE, [cur])
            
    #------------------------------
    def toggleMinimize(self, task):
    #------------------------------
        """ Iconify/Deiconify a task """
        if self.ewmh and not self.openbox:
            self.sendEvent(task.obj, self._STATE, [2,self._HIDDEN])
        else:
            if task.obj.get_wm_state()['state'] == 1:
                cs = self.display.intern_atom("WM_CHANGE_STATE")
                self.sendEvent(task.obj, cs, [Xutil.IconicState])
            else:
                task.obj.map()
                
    #---------------------------
    def toggleShade(self, task):
    #---------------------------
        """ Shade/Unshade a task """
        if self.ewmh:
            self.sendEvent(task.obj, self._STATE, [2, self._SHADED])
        else:
            ws = task.obj.get_full_property(self._STATE, Xatom.CARDINAL)
            if ws.value[0] == 0:
                shade = 32
            else:
                shade = 0
            task.obj.change_property(self._STATE, Xatom.CARDINAL, 16, [shade])
            task.obj.unmap()
            task.obj.map()
            
    #----------------------------------
    def taskRaise(self, task, focus=0):
    #----------------------------------
        """ Raise a task """
        if task.obj.get_wm_state()['state'] == Xutil.NormalState:
            task.obj.configure(stack_mode = X.Above)
            if focus:
                task.obj.set_input_focus(X.RevertToNone, X.CurrentTime)
    
    #----------------------------------
    def taskLower(self, task, focus=0):
    #----------------------------------
        """ Lower a task """
        if task.obj.get_wm_state()['state'] == Xutil.NormalState:
            task.obj.configure(stack_mode = X.Below)
            if focus:
                task.obj.set_input_focus(X.RevertToNone, X.CurrentTime)
                    
    #------------------------------------
    def loop(self, dsp, root, win, ewmh):
    #------------------------------------
        """ Event loop - handle events as they occur until we're killed """
        mapped = 1
        state  = dsp.intern_atom("WM_STATE")
        name   = dsp.intern_atom("WM_ICON_NAME")
        rpm    = dsp.intern_atom("_XROOTPMAP_ID")
        cl     = self._CLIENT_LIST
        ws     = self._WORKSPACE
        
        if ewmh:
            cws = self._CURRENT_DESKTOP
            aw  = dsp.intern_atom("_NET_ACTIVE_WINDOW")
        else:
            cws = self._WORKSPACE
            aw  = 1
        
        while 1:
            if not mapped:
                self.setBackground()
                win.map()
                mapped = 1
                if ewmh:
                    self.sendEvent(win, self._STATE, [2,self._HIDDEN])
                    win.change_property(self._WORKSPACE, Xatom.CARDINAL,
                                        32, [0xffffffffL])
                                        
            while dsp.pending_events():
                e = dsp.next_event()
                if e.type == X.PropertyNotify:
                    if (e.atom == cl and not ewmh) or (ewmh and e.atom == aw):
                        self.updatePanel(dsp, root)                        
                    elif e.atom == state or e.atom == self._STATE:
                        self.updatePanel(dsp, root, e.window.id)
                    elif e.atom == name or e.atom == cws or e.atom == ws:
                        self.updatePanel(dsp, root)
                    elif e.atom == rpm and TRANSPARENCY:
                        if ewmh:
                            self.sendEvent(win,self._STATE,[2,self._HIDDEN])
                        win.unmap()
                        mapped = 0
                elif e.type == X.DestroyNotify:
                    self.updatePanel(dsp, root)
                elif (e.type==X.ButtonRelease):
                    if e.event_x > self.startx and e.event_x < self.endx:
                        ws = self.getWS(self.root, 1)
                        for t in self.tasks[ws].values():
                            if e.event_x >= t.pos[0] and e.event_x <= t.pos[1]:
                                taskButtonEvent(self, e.detail, t)
                    elif LEFT_OBJ and e.event_x < self.startx:
                        leftObjButtonEvent(self, e.detail)
                    elif RIGHT_OBJ and e.event_x > self.endx:
                        rightObjButtonEvent(self, e.detail)
                elif e.type == X.Expose:
                    if e.count == 0:
                        self.workspace = -1
                        self.updatePanel(dsp, root)
            
            rs, ws, es = select.select([dsp.display.socket], [], [], 60)
            if not rs:
                if LEFT_OBJ == "clock" or RIGHT_OBJ == "clock":
                    self.updatePanelObj("clock", clear=1)
                 
#----------------------------------------------------------------------------                
class Task(object):
#----------------------------------------------------------------------------
    """ PyPanel task object """
    #--------------------------------------------------
    def __init__(self, id, obj, key, name, ws, tclass):
    #--------------------------------------------------
        self.id     = id
        self.obj    = obj
        self.key    = key
        self.name   = name
        self.ws     = ws
        self.tclass = tclass
        self.color  = None
        self.state  = None
        self.icon   = None
        self.mask   = None
        self.ichk   = None
        self.pos    = (-1, -1)
                            
#----------------------------------------------------------------------------
#                                  Main
#----------------------------------------------------------------------------
from distutils import sysconfig
from ppmodule import ppshade, ppicon
from Xlib import X, display, Xatom, Xutil
import Xlib.protocol.event
import os, pwd, select, sys, time

P_WIDTH = 0

if __name__ == '__main__':
    try:
        home = pwd.getpwuid(os.getuid())[5]
        if not os.path.isfile("%s/.pypanelrc" % home):
            import shutil
            src = "%s/pypanel/pypanelrc" % sysconfig.get_python_lib()
            dst = "%s/.pypanelrc" % home
            shutil.copyfile(src, dst)
            del src, dst
        execfile("%s/.pypanelrc" % home)
        del home
    except StandardError, e:
        sys.stderr.write("\nFailed to open ~/.pypanelrc -\n\n")
        sys.stderr.write(str(e)+"\n\n")
        sys.exit()
    PyPanel(display.Display())
