From: Stefanos Harhalakis Date: Thu, 24 Jun 2010 15:33:42 +0000 (+0000) Subject: Moved into src. X-Git-Url: http://vcs.maemo.org/git/?a=commitdiff_plain;h=15a43ab3378c0aa46af8ce75b3303d15947ac4a6;hp=3e41d8ca09bf165367e362baee40ac64c9c0fcec;p=drlaunch Moved into src. --- diff --git a/0.py b/0.py deleted file mode 100755 index 2b790f5..0000000 --- a/0.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/apps.py b/apps.py deleted file mode 100755 index 28673cd..0000000 --- a/apps.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -import os - -from gettext import translation -#import locale - -#from xdg.IconTheme import getIconPath - -appdir="/usr/share/applications/hildon" - -def readOneFn(fn): - global appdir - - fn2=appdir + '/' + fn - - f=open(fn2, 'rt') - - ret={ - 'id': fn[:-8], - 'name': None, - 'exec': None, - 'icon': None, - 'iconpath': None, - 'domain': None, - } - inde=False - for line in f: - line=line.strip() - if line=='[Desktop Entry]': - inde=True - continue - - if inde==False: - continue - - # Reached another block - if line.startswith('[') and inde: - break - - elif line.startswith('Name='): - l=line[5:] - ret['name']=l - elif line.startswith('Exec='): - l=line[5:] - ret['exec']=l - elif line.startswith('Icon='): - l=line[5:] - ret['icon']=l - # ret['iconpath']=getIconPath(l) - elif line.startswith('X-Text-Domain='): - l=line[14:] - ret['domain']=l - - if ret['domain']!=None: - try: - c=translation(ret['domain']) - except IOError, e: - c=None - - if c!=None: - ret['name0']=ret['name'] - ret['name']=c.gettext(ret['name0']) - - return(ret) - -def readOne(name): - fn=name + ".desktop" - - ret=readOneFn(fn) - - return(ret) - -def scan(): - global appdir - - files=os.listdir(appdir) - - ret={} - - for f in files: - if not f.endswith('.desktop'): - continue - if f.startswith('catorise-'): - continue - dt=readOneFn(f) - t=f[:-8] - ret[t]=dt - - return(ret) - -if __name__=="__main__": - #locale.setlocale(locale.LC_ALL, '') - print scan() - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/config.py b/config.py deleted file mode 100755 index 36a33be..0000000 --- a/config.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -import os -import pickle - -try: - from glib import get_user_config_dir -except: - def get_user_config_dir(): - home=os.environ['HOME'] - if home=='': - home="/home/user" - cfg="%s/.config" % (home) - - return(cfg) - -size = 2 -iconsize = 64 -iconspace = 42 -apps=None - -def setSize(sz): - global size - - size=sz - -def getSize(): - global size - - return(size) - -def setApps(aps): - """ apps is a dictionary of (x,y)=>appname """ - global apps - - apps=aps - -def getApps(): - global apps - - if apps==None: - tmp={ - (0,0): 'rtcom-call-ui', - (0,1): 'rtcom-messaging-ui', - (1,0): 'browser', - (1,1): 'osso-addressbook', - } - setApps(tmp) - - return(apps) - -def ensure_dir(): - dir0=get_user_config_dir() - dir=dir0+'/drlaunch' - if not os.path.exists(dir): - os.mkdir(dir) - if not os.path.isdir(dir): - raise Exception('Failed to ensure directory' + dir) - - return(dir) - -def get_config_fn(): - dir=ensure_dir() - ret=dir + '/config' - - return(ret) - -def save(): - fn=get_config_fn() - print "save", fn - - dt={ - 'version': 1, - 'size': getSize(), - 'apps': getApps() - } - - print "save:", dt - - st=pickle.dumps(dt) - f=file(fn, 'w') - f.write(st) - f.close() - -def load(): - fn=get_config_fn() - print "load", fn - - try: - f=file(fn, 'r') - st=f.read() - f.close() - dt=pickle.loads(st) - except: - return - - print "load:", dt - - setSize(dt['size']) - setApps(dt['apps']) - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/icon.py b/icon.py deleted file mode 100755 index f050463..0000000 --- a/icon.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -import gtk -import gobject -import hildon -from hildondesktop import * -from gtk import gdk -from math import pi -import cairo -import time - -from portrait import FremantleRotation -import launcher -from xdg.IconTheme import getIconPath - - -import config -import apps - -def getIcon(name): - ico=getIconPath(name, config.iconsize) - ret=gtk.gdk.pixbuf_new_from_file_at_size(ico, config.iconsize, - config.iconsize) - - return(ret) - -class Icon(gobject.GObject): - def __init__(self, isconfig): - self.__gobject_init__() - - self.isconfig=isconfig - - self.name=None - self.icon=None - self.lastpress=0 - self.ispressed=False - - self.x=0 - self.y=0 - - self.presstime=0.25 - - self.window=None - - self.clickcount=0 - - def __del__(self): - print "del" - - def timePressed(self): - """ return how much time a button is pressed """ - dt=time.time() - self.lastpress - - return(dt) - - def setApp(self, dt): - self.name=dt['id'] - self.icon=dt['icon2'] - self.invalidate() - - def getSize(self): - return(config.iconsize+config.iconspace) - - def draw(self, cr, x, y, mode): - #print "draw", x, y, mode - self.x=x - self.y=y - - if self.icon==None and not self.isconfig: - return - - cr.save() - cr.set_source_rgba(0.1, 0.1, 0.1, 1) - cr.set_line_width(5) - - if self.ispressed: - t=1.0 * min(self.timePressed(), self.presstime) / self.presstime - g=0.3+0.5*t - b=0.3+0.7*t - cr.set_source_rgba(0, g, b, 0.7) - else: - cr.set_source_rgba(0.3, 0.3, 0.3, 0.7) - - x3=x + (config.iconspace/6) - y3=y + (config.iconspace/6) - - r=10 # Radius - w=config.iconsize+(config.iconspace*2/3) - - cr.move_to(x3+r, y3) - cr.arc(x3+w-r, y3+r, r, pi*1.5, pi*2) - cr.arc(x3+w-r, y3+w-r, r, 0, pi*0.5) - cr.arc(x3+r, y3+w-r, r, pi*0.5, pi) - cr.arc(x3+r, y3+r, r, pi, pi*1.5) - - cr.stroke_preserve() - cr.fill() - cr.clip() - cr.paint() - cr.restore() - - if self.icon==None: - return - - icon=self.icon - - if mode=='l': - icon2=icon - else: - icon2=icon.rotate_simple(gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) - - cr.save() - x3=x + (config.iconspace/2) - y3=y + (config.iconspace/2) - cr.set_source_pixbuf(icon2, x3, y3) - cr.paint() - cr.restore() - - return(False) - - def timerPressed(self): - #print "timer" - - if not self.ispressed: - return(False) - - self.invalidate() - - if self.timePressed()>self.presstime: - ret=False - else: - ret=True - - return(ret) - - def doPress(self): - #print "doPress()" - # Double-time: time for pressed and time for not-pressed - if time.time() - self.lastpress > self.presstime*2: - self.clickcount=0 - - self.lastpress=time.time() - self.ispressed=True - gobject.timeout_add(20, self.timerPressed) - #print "doPress() end" - - def doRelease(self): - print "doRelease()" - dt=time.time() - self.lastpress - self.ispressed=False - self.invalidate() - if dt<=self.presstime: - self.clickcount+=1 - if self.clickcount==1: - print "click" - self.emit('click') - elif self.clickcount==2: - print "double-click" - self.emit('double-click') - if self.clickcount==3: - print "tripple-click" - self.emit('tripple-click') - self.clickcount=0 - elif dt>self.presstime and dt<2: - print "long-press" - self.emit('long-press') - - def doCancel(self): - print "doCancel()" - self.ispressed=False - - def invalidate(self, window=None): - if window==None: - window=self.window - else: - self.window=window - - if window==None: - return - - w=config.iconsize + config.iconspace - rect=gdk.Rectangle(self.x, self.y, w, w) - #print "rect", self.x, self.y, w, w - gdk.Window.invalidate_rect(window, rect, True) - -gobject.type_register(Icon) -signals=['click', 'double-click', 'tripple-click', 'long-press'] -for s in signals: - gobject.signal_new(s, Icon, gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ()) - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/icongrid.py b/icongrid.py deleted file mode 100755 index 1308bb0..0000000 --- a/icongrid.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -import gtk -import gobject -import hildon -from hildondesktop import * -from gtk import gdk -from math import pi -import cairo -import time - -from portrait import FremantleRotation -from xdg.IconTheme import getIconPath - -import config -import apps -import icon -from icon import Icon -from icons import Icons - -def getIcon(name): - ico=getIconPath(name, config.iconsize) - ret=gtk.gdk.pixbuf_new_from_file_at_size(ico, config.iconsize, - config.iconsize) - - return(ret) - -#class IconGrid(gtk.Widget, FremantleRotation): -class IconGrid(object): #(gobject.GObject): - def __init__(self, isconfig=False): -# self.__gobject_init__() - - self.size=0 - - self.isconfig=isconfig - - self.icons=Icons(isconfig) - - self.setMode('l') - - # Maybe fix those: -# w=(config.getSize() * config.iconsize) + \ -# (config.getSize()) * config.iconspace - #self.set_size_request(w, w) - -# self.setSize(config.getSize()) - self.setSize(4) - - self.lasticon=None # The last icon that got selected - - self.reloadIcons() - - def connect(self, what, *args): - if what in icon.signals: - self.icons.connect(what, *args) - else: - super(IconGrid, self).connect(what, *args) - - def setSize(self, size): - print "igw::setSize", size -# config.setSize(size) - self.size=size - self.icons.setSize(size) - - def setMode(self, mode): - self.mode=mode - if isinstance(self, gtk.Widget): - self.queue_draw() - - def iconAt(self, x, y): - """ Get icon at coordinates x,y. X and Y are in pixels """ - - w=config.iconsize + config.iconspace - - if self.mode=='l': - x2=int(x / w) - y2=int(y / w) - else: - x2=self.size - int(y/w) - 1 - y2=int(x/w) - - print "x2,y2", x2, y2 - ret=self.get(x2,y2) - - return(ret) - - def get(self, x, y): - ret=self.icons.get(x,y) - - return(ret) - - def _draw(self, cr, event): -# print "mode:", self.mode -# print "icons", len(self.icons) - - w=config.iconsize + config.iconspace - for x,y in self.icons: -# print x, y - - if self.mode=='l': - x2=x * (config.iconsize + config.iconspace) - y2=y * (config.iconsize + config.iconspace) - else: - x2=y * (config.iconsize + config.iconspace) - y2=(self.size-x-1) * (config.iconsize + config.iconspace) - - # Only repaint the needed icons - rect=gdk.Rectangle(x2, y2, w, w) - t=rect.intersect(event.area) - if t.width==0 and t.height==0: - continue - -# print "draw:", x, y - ico=self.icons.get(x,y) - ico.draw(cr, x2, y2, self.mode) - - def do_expose_event(self, event): - cr=self.window.cairo_create() - - cr.rectangle(event.area.x, event.area.y, - event.area.width, event.area.height) - - cr.clip() - - self._draw(cr, event) - - def setLastIcon(self, icon): - if icon==self.lasticon: - return - - if self.lasticon!=None: - self.lasticon.doCancel() - self.lasticon.invalidate(self.window) - self.lasticon=icon - - def do_button_press_event(self, event): - print "press", event.type - icon=self.iconAt(event.x, event.y) - if icon==None: - return -# rect=gdk.Rectangle(event.x,event.y,1,1) -# rect=gdk.Rectangle(0, 0, 100, 100) - icon.doPress() - icon.invalidate(self.window) - self.setLastIcon(icon) - -# gdk.Window.invalidate_rect(self.window, rect, True) - - return(True) - - def do_button_release_event(self, event): - print "release" - if self.lasticon!=None: - self.lasticon.invalidate(self.window) - self.lasticon.doRelease() - - self.setLastIcon(None) - - return(True) - - def do_leave_notify_event(self, event): - print "leave" - #print "leave", event.x, event.y - self.setLastIcon(None) - return(True) - - def do_pproperty_notify_event(self, event): - print "property" - icon=self.iconAt(event.x, event.y) - if icon==None: - return - icon.doCancel() - icon.invalidate(self.window) - return(True) - - def do_motion_notify_event(self, event): - print "motion" - icon=self.iconAt(event.x, event.y) - if self.lasticon==icon: - return(True) - - self.setLastIcon(None) - icon.doCancel() - icon.invalidate(self.window) - return(True) - - def do_button_press_event_old(self, event): - #print "press" - if event.type==gdk.BUTTON_PRESS: - print "press", event.type - if self.mode=='p': - self.setMode('l') - else: - self.setMode('p') - self.queue_draw() - return True - - # For debugging - def do_event1(self, event): - print "event:", event, event.type - - def butTest(self, arg): - print "but", arg - - def reloadIcons(self): - self.icons.load() - -# def on_orientation_changed(self, orientation): -# print "orch:", orientation -# o=orientation[0] -# self.setMode(o) - -class IconGridWidget(IconGrid, gtk.Widget): - def __init__(self, isconfig): - IconGrid.__init__(self, isconfig) - gtk.Widget.__init__(self) - - if isconfig: - w=4 * (config.iconsize + config.iconspace) - else: - w=self.size * (config.iconsize + config.iconspace) - - self.set_size_request(w, w) - - def do_realize(self): - screen=self.get_screen() - self.set_colormap(screen.get_rgba_colormap()) - self.set_app_paintable(True) - - self.set_flags(self.flags() | gtk.REALIZED) - - self.window=gdk.Window( - self.get_parent_window(), - width=self.allocation.width, - height=self.allocation.height, - window_type=gdk.WINDOW_CHILD, - wclass=gdk.INPUT_OUTPUT, - event_mask=self.get_events() | gdk.EXPOSURE_MASK - | gdk.BUTTON_PRESS_MASK - | gdk.BUTTON_RELEASE_MASK - | gdk.BUTTON_MOTION_MASK - | gdk.POINTER_MOTION_MASK - | gdk.POINTER_MOTION_HINT_MASK - | gdk.ENTER_NOTIFY_MASK - | gdk.LEAVE_NOTIFY_MASK ) - - self.window.set_user_data(self) - self.style.attach(self.window) - -# self.style.set_background(self.window, gtk.STATE_NORMAL) - self.window.move_resize(*self.allocation) - -# self.pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d( -# self.window, self.style.bg[gtk.STATE_NORMAL], STAR_PIXMAP) - -# self.gc = self.style.fg_gc[gtk.STATE_NORMAL] - - #gtk.Widget.do_realize(self) - #HomePluginItem.do_realize(self) - -# screen=self.get_screen() -# self.set_colormap(screen.get_rgba_colormap()) -# self.set_app_paintable(True) - - def do_unrealize(self): - #self.window.set_user_data(None) - self.window.destroy() - -#gobject.type_register(IconGrid) -gobject.type_register(IconGridWidget) - - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/icons.py b/icons.py deleted file mode 100755 index 72812de..0000000 --- a/icons.py +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -import config -import apps -import icon -from icon import getIcon, Icon - -import gobject - -class IconIter: - def __init__(self, items): - self.iter=items.__iter__() - - def __iter__(self): - ret=self.iter.__iter__() - return(ret) - - def next(self): - ret=self.iter.next() - return(ret) - -class Icons(gobject.GObject): - def __init__(self, isconfig): - self.__gobject_init__() - self.icons={} - self.allicons={} - self.size=0 - self.isconfig=isconfig - - # signal handlers - self.h={} - - # setup allicons - maxsz=4 - for x in xrange(maxsz): - for y in xrange(maxsz): - k=(x,y) - ico=Icon(self.isconfig) - self.allicons[k]=ico - self.connect_one(ico) - - @classmethod - def register_signals(cls): - signals=icon.signals - for s in signals: - gobject.signal_new(s, cls, gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, (Icon,)) - - def __iter__(self): - return(IconIter(self.icons)) - - def connect_one(self, which): - self.h[which]={ - 'longpress': which.connect('long-press', self.signalLongpress), - 'click': which.connect('click', self.signalClick), - 'tripple': which.connect('tripple-click', - self.signalTrippleClick) - } - - def disconnect_one(self, which): - for i in self.h[which]: - which.disconnect(self.h[which][i]) - self.h.pop(which) -# which.disconnect(self.h[which]_longpress) -# which.disconnect(self.h_click) -# which.disconnect(self.h_tripple) - - def setSize(self, sz): -# print "sz:", sz, self.size - - if sz==self.size: - return - - old=self.icons - self.icons={} - - for x in xrange(sz): - for y in xrange(sz): - k=(x,y) - ico=self.allicons[k] - self.icons[k]=ico -# if old.has_key(k): -# old.pop(k) # Re-used -# else: -# self.connect_one(ico) - - # Disconnect signals -# for i in old: -# self.disconnect_one(old[i]) - - self.size=sz - - def signalLongpress(self, icon): - print "signalLongpress()", icon - self.emit('long-press', icon) - - def signalClick(self, icon): - print "signalClick()", icon - self.emit('click', icon) - - def signalTrippleClick(self, icon): - print "signalTrippleClick()", icon - self.emit('tripple-click', icon) - - def get(self, x, y): - k=(x,y) - if self.icons.has_key(k): - ret=self.icons[k] - else: - ret=None - - return(ret) - - def load(self): -# x=0 -# y=0 -# fn=["maegirls", "wifieye", 'battery-eye', 'image-viewer', -# 'tecnoballz', 'ncalc', 'rtcom-call-ui', 'rtcom-messaging-ui', -# 'extcalllog', 'browser', 'modest', 'osso-addressbook'] - - wapps=config.getApps() - sz=config.getSize() - - for k in wapps: - x,y=k - if x>=sz or y>=sz: - continue - - appname=wapps[k] - if appname!=None: - app=apps.readOne(appname) - app['icon2']=getIcon(app['icon']) - self.get(x,y).setApp(app) - -# for f in fn: -# dt=apps.readOne(f) -# dt['icon2']=getIcon(dt['icon']) -# print x, y, dt -# self.get(x,y).setApp(dt) -# x+=1 -# if x>=config.getSize(getSize()): -# x=0 -# y+=1 -## self.icons.append(p) - - print "end of Icons init" - - -gobject.type_register(Icons) -Icons.register_signals() - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/launcher.py b/launcher.py deleted file mode 100755 index dacde19..0000000 --- a/launcher.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -import dbus - -bus=None -proxy=None - -def init(): - global bus, proxy - - bus=dbus.SessionBus() - - proxy=bus.get_object('com.nokia.HildonDesktop.AppMgr', - '/com/nokia/HildonDesktop/AppMgr') - -def launch(prog): - global bus, proxy - - proxy.LaunchApplication(prog, - dbus_interface='com.nokia.HildonDesktop.AppMgr') - -if __name__=="__main__": - init() - launch('wifieye') - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/portrait.py b/portrait.py deleted file mode 100644 index 45043c5..0000000 --- a/portrait.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -# -# gPodder - A media aggregator and podcast client -# Copyright (c) 2005-2010 Thomas Perl and the gPodder Team -# -# gPodder 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 3 of the License, or -# (at your option) any later version. -# -# gPodder 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, see . -# - -import dbus -import dbus.glib - -import hildon -import osso - -# Replace this with your own gettext() functionality -#import gpodder -#_ = gpodder.gettext - - -class FremantleRotation(object): - """thp's screen rotation for Maemo 5 - - Simply instantiate an object of this class and let it auto-rotate - your StackableWindows depending on the device orientation. - - If you need to relayout a window, connect to its "configure-event" - signal and measure the ratio of width/height and relayout for that. - - You can set the mode for rotation to AUTOMATIC (default), NEVER or - ALWAYS with the set_mode() method. - """ - - AUTOMATIC, NEVER, ALWAYS = range(3) - - MODE_CAPTIONS = ('Automatic', 'Landscape', 'Portrait') - - # Privately-used constants - _PORTRAIT, _LANDSCAPE = ('portrait', 'landscape') - _ENABLE_ACCEL = 'req_accelerometer_enable' - _DISABLE_ACCEL = 'req_accelerometer_disable' - - # Defined in mce/dbus-names.h - _MCE_SERVICE = 'com.nokia.mce' - _MCE_REQUEST_PATH = '/com/nokia/mce/request' - _MCE_REQUEST_IF = 'com.nokia.mce.request' - - # sysfs device name for the keyboard slider switch - KBD_SLIDER = '/sys/devices/platform/gpio-switch/slide/state' - _KBD_OPEN = 'open' - _KBD_CLOSED = 'closed' - - def __init__(self, app_name, main_window=None, version='1.0', mode=0): - """Create a new rotation manager - - app_name ... The name of your application (for osso.Context) - main_window ... The root window (optional, hildon.StackableWindow) - version ... The version of your application (optional, string) - mode ... Initial mode for this manager (default: AUTOMATIC) - """ - self._orientation = None - self._main_window = main_window - self._mode = -1 - self._last_dbus_orientation = None - self._keyboard_state = self._get_keyboard_state() - app_id = '-'.join((app_name, self.__class__.__name__)) - self._osso_context = osso.Context(app_id, version, False) - program = hildon.Program.get_instance() - program.connect('notify::is-topmost', self._on_topmost_changed) - system_bus = dbus.Bus.get_system() - system_bus.add_signal_receiver(self._on_orientation_signal, \ - signal_name='sig_device_orientation_ind', \ - dbus_interface='com.nokia.mce.signal', \ - path='/com/nokia/mce/signal') - system_bus.add_signal_receiver(self._on_keyboard_signal, \ - signal_name='Condition', \ - dbus_interface='org.freedesktop.Hal.Device', \ - path='/org/freedesktop/Hal/devices/platform_slide') - self.set_mode(mode) - - self._send_mce_request(self._ENABLE_ACCEL) - - def set_mode(self, new_mode): - self._mode=new_mode - - def get_mode(self): - """Get the currently-set rotation mode - - This will return one of three values: AUTOMATIC, ALWAYS or NEVER. - """ - return self._mode - - def _send_mce_request(self, request): - rpc = osso.Rpc(self._osso_context) - rpc.rpc_run(self._MCE_SERVICE, \ - self._MCE_REQUEST_PATH, \ - self._MCE_REQUEST_IF, \ - request, \ - use_system_bus=True) - - def _on_topmost_changed(self, program, property_spec): - # XXX: This seems to never get called on Fremantle(?) - if self._mode == self.AUTOMATIC: - if program.get_is_topmost(): - self._send_mce_request(self._ENABLE_ACCEL) - else: - self._send_mce_request(self._DISABLE_ACCEL) - - def NO_get_main_window(self): - if self._main_window: - # If we have gotten the main window as parameter, return it and - # don't try "harder" to find another window using the stack - return self._main_window - else: - # The main window is at the "bottom" of the window stack, and as - # the list we get with get_windows() is sorted "topmost first", we - # simply take the last item of the list to get our main window - windows = self._stack.get_windows() - if windows: - return windows[-1] - else: - return None - - def _orientation_changed(self, orientation): - if self._orientation == orientation: - # Ignore repeated requests - return - - flags = 0 - - if orientation != self._LANDSCAPE: - flags |= hildon.PORTRAIT_MODE_SUPPORT - - if orientation == self._PORTRAIT: - flags |= hildon.PORTRAIT_MODE_REQUEST - -# window = self._get_main_window() -# if window is not None: -# hildon.hildon_gtk_window_set_portrait_flags(window, flags) - - self._orientation = orientation - - self.on_orientation_changed(orientation) - - def _get_keyboard_state(self): - # For sbox, if the device does not exist assume that it's closed - try: - return open(self.KBD_SLIDER).read().strip() - except IOError: - return self._KBD_CLOSED - - def _keyboard_state_changed(self): - state = self._get_keyboard_state() - - if state == self._KBD_OPEN: - self._orientation_changed(self._LANDSCAPE) - elif state == self._KBD_CLOSED: - if self._mode == self.AUTOMATIC: - self._orientation_changed(self._last_dbus_orientation) - elif self._mode == self.ALWAYS: - self._orientation_changed(self._PORTRAIT) - - self._keyboard_state = state - - def _on_keyboard_signal(self, condition, button_name): - if condition == 'ButtonPressed' and button_name == 'cover': - self._keyboard_state_changed() - - def _on_orientation_signal(self, orientation, stand, face, x, y, z): - print "orsignal" - if orientation in (self._PORTRAIT, self._LANDSCAPE): - if self._mode == self.AUTOMATIC and \ - self._keyboard_state != self._KBD_OPEN: - # Automatically set the rotation based on hardware orientation - self._orientation_changed(orientation) - - # Save the current orientation for "automatic" mode later on - self._last_dbus_orientation = orientation - diff --git a/src/0.py b/src/0.py new file mode 100755 index 0000000..2b790f5 --- /dev/null +++ b/src/0.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/apps.py b/src/apps.py new file mode 100755 index 0000000..28673cd --- /dev/null +++ b/src/apps.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +import os + +from gettext import translation +#import locale + +#from xdg.IconTheme import getIconPath + +appdir="/usr/share/applications/hildon" + +def readOneFn(fn): + global appdir + + fn2=appdir + '/' + fn + + f=open(fn2, 'rt') + + ret={ + 'id': fn[:-8], + 'name': None, + 'exec': None, + 'icon': None, + 'iconpath': None, + 'domain': None, + } + inde=False + for line in f: + line=line.strip() + if line=='[Desktop Entry]': + inde=True + continue + + if inde==False: + continue + + # Reached another block + if line.startswith('[') and inde: + break + + elif line.startswith('Name='): + l=line[5:] + ret['name']=l + elif line.startswith('Exec='): + l=line[5:] + ret['exec']=l + elif line.startswith('Icon='): + l=line[5:] + ret['icon']=l + # ret['iconpath']=getIconPath(l) + elif line.startswith('X-Text-Domain='): + l=line[14:] + ret['domain']=l + + if ret['domain']!=None: + try: + c=translation(ret['domain']) + except IOError, e: + c=None + + if c!=None: + ret['name0']=ret['name'] + ret['name']=c.gettext(ret['name0']) + + return(ret) + +def readOne(name): + fn=name + ".desktop" + + ret=readOneFn(fn) + + return(ret) + +def scan(): + global appdir + + files=os.listdir(appdir) + + ret={} + + for f in files: + if not f.endswith('.desktop'): + continue + if f.startswith('catorise-'): + continue + dt=readOneFn(f) + t=f[:-8] + ret[t]=dt + + return(ret) + +if __name__=="__main__": + #locale.setlocale(locale.LC_ALL, '') + print scan() + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/config.py b/src/config.py new file mode 100755 index 0000000..36a33be --- /dev/null +++ b/src/config.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +import os +import pickle + +try: + from glib import get_user_config_dir +except: + def get_user_config_dir(): + home=os.environ['HOME'] + if home=='': + home="/home/user" + cfg="%s/.config" % (home) + + return(cfg) + +size = 2 +iconsize = 64 +iconspace = 42 +apps=None + +def setSize(sz): + global size + + size=sz + +def getSize(): + global size + + return(size) + +def setApps(aps): + """ apps is a dictionary of (x,y)=>appname """ + global apps + + apps=aps + +def getApps(): + global apps + + if apps==None: + tmp={ + (0,0): 'rtcom-call-ui', + (0,1): 'rtcom-messaging-ui', + (1,0): 'browser', + (1,1): 'osso-addressbook', + } + setApps(tmp) + + return(apps) + +def ensure_dir(): + dir0=get_user_config_dir() + dir=dir0+'/drlaunch' + if not os.path.exists(dir): + os.mkdir(dir) + if not os.path.isdir(dir): + raise Exception('Failed to ensure directory' + dir) + + return(dir) + +def get_config_fn(): + dir=ensure_dir() + ret=dir + '/config' + + return(ret) + +def save(): + fn=get_config_fn() + print "save", fn + + dt={ + 'version': 1, + 'size': getSize(), + 'apps': getApps() + } + + print "save:", dt + + st=pickle.dumps(dt) + f=file(fn, 'w') + f.write(st) + f.close() + +def load(): + fn=get_config_fn() + print "load", fn + + try: + f=file(fn, 'r') + st=f.read() + f.close() + dt=pickle.loads(st) + except: + return + + print "load:", dt + + setSize(dt['size']) + setApps(dt['apps']) + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/icon.py b/src/icon.py new file mode 100755 index 0000000..f050463 --- /dev/null +++ b/src/icon.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +import gtk +import gobject +import hildon +from hildondesktop import * +from gtk import gdk +from math import pi +import cairo +import time + +from portrait import FremantleRotation +import launcher +from xdg.IconTheme import getIconPath + + +import config +import apps + +def getIcon(name): + ico=getIconPath(name, config.iconsize) + ret=gtk.gdk.pixbuf_new_from_file_at_size(ico, config.iconsize, + config.iconsize) + + return(ret) + +class Icon(gobject.GObject): + def __init__(self, isconfig): + self.__gobject_init__() + + self.isconfig=isconfig + + self.name=None + self.icon=None + self.lastpress=0 + self.ispressed=False + + self.x=0 + self.y=0 + + self.presstime=0.25 + + self.window=None + + self.clickcount=0 + + def __del__(self): + print "del" + + def timePressed(self): + """ return how much time a button is pressed """ + dt=time.time() - self.lastpress + + return(dt) + + def setApp(self, dt): + self.name=dt['id'] + self.icon=dt['icon2'] + self.invalidate() + + def getSize(self): + return(config.iconsize+config.iconspace) + + def draw(self, cr, x, y, mode): + #print "draw", x, y, mode + self.x=x + self.y=y + + if self.icon==None and not self.isconfig: + return + + cr.save() + cr.set_source_rgba(0.1, 0.1, 0.1, 1) + cr.set_line_width(5) + + if self.ispressed: + t=1.0 * min(self.timePressed(), self.presstime) / self.presstime + g=0.3+0.5*t + b=0.3+0.7*t + cr.set_source_rgba(0, g, b, 0.7) + else: + cr.set_source_rgba(0.3, 0.3, 0.3, 0.7) + + x3=x + (config.iconspace/6) + y3=y + (config.iconspace/6) + + r=10 # Radius + w=config.iconsize+(config.iconspace*2/3) + + cr.move_to(x3+r, y3) + cr.arc(x3+w-r, y3+r, r, pi*1.5, pi*2) + cr.arc(x3+w-r, y3+w-r, r, 0, pi*0.5) + cr.arc(x3+r, y3+w-r, r, pi*0.5, pi) + cr.arc(x3+r, y3+r, r, pi, pi*1.5) + + cr.stroke_preserve() + cr.fill() + cr.clip() + cr.paint() + cr.restore() + + if self.icon==None: + return + + icon=self.icon + + if mode=='l': + icon2=icon + else: + icon2=icon.rotate_simple(gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) + + cr.save() + x3=x + (config.iconspace/2) + y3=y + (config.iconspace/2) + cr.set_source_pixbuf(icon2, x3, y3) + cr.paint() + cr.restore() + + return(False) + + def timerPressed(self): + #print "timer" + + if not self.ispressed: + return(False) + + self.invalidate() + + if self.timePressed()>self.presstime: + ret=False + else: + ret=True + + return(ret) + + def doPress(self): + #print "doPress()" + # Double-time: time for pressed and time for not-pressed + if time.time() - self.lastpress > self.presstime*2: + self.clickcount=0 + + self.lastpress=time.time() + self.ispressed=True + gobject.timeout_add(20, self.timerPressed) + #print "doPress() end" + + def doRelease(self): + print "doRelease()" + dt=time.time() - self.lastpress + self.ispressed=False + self.invalidate() + if dt<=self.presstime: + self.clickcount+=1 + if self.clickcount==1: + print "click" + self.emit('click') + elif self.clickcount==2: + print "double-click" + self.emit('double-click') + if self.clickcount==3: + print "tripple-click" + self.emit('tripple-click') + self.clickcount=0 + elif dt>self.presstime and dt<2: + print "long-press" + self.emit('long-press') + + def doCancel(self): + print "doCancel()" + self.ispressed=False + + def invalidate(self, window=None): + if window==None: + window=self.window + else: + self.window=window + + if window==None: + return + + w=config.iconsize + config.iconspace + rect=gdk.Rectangle(self.x, self.y, w, w) + #print "rect", self.x, self.y, w, w + gdk.Window.invalidate_rect(window, rect, True) + +gobject.type_register(Icon) +signals=['click', 'double-click', 'tripple-click', 'long-press'] +for s in signals: + gobject.signal_new(s, Icon, gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ()) + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/icongrid.py b/src/icongrid.py new file mode 100755 index 0000000..1308bb0 --- /dev/null +++ b/src/icongrid.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +import gtk +import gobject +import hildon +from hildondesktop import * +from gtk import gdk +from math import pi +import cairo +import time + +from portrait import FremantleRotation +from xdg.IconTheme import getIconPath + +import config +import apps +import icon +from icon import Icon +from icons import Icons + +def getIcon(name): + ico=getIconPath(name, config.iconsize) + ret=gtk.gdk.pixbuf_new_from_file_at_size(ico, config.iconsize, + config.iconsize) + + return(ret) + +#class IconGrid(gtk.Widget, FremantleRotation): +class IconGrid(object): #(gobject.GObject): + def __init__(self, isconfig=False): +# self.__gobject_init__() + + self.size=0 + + self.isconfig=isconfig + + self.icons=Icons(isconfig) + + self.setMode('l') + + # Maybe fix those: +# w=(config.getSize() * config.iconsize) + \ +# (config.getSize()) * config.iconspace + #self.set_size_request(w, w) + +# self.setSize(config.getSize()) + self.setSize(4) + + self.lasticon=None # The last icon that got selected + + self.reloadIcons() + + def connect(self, what, *args): + if what in icon.signals: + self.icons.connect(what, *args) + else: + super(IconGrid, self).connect(what, *args) + + def setSize(self, size): + print "igw::setSize", size +# config.setSize(size) + self.size=size + self.icons.setSize(size) + + def setMode(self, mode): + self.mode=mode + if isinstance(self, gtk.Widget): + self.queue_draw() + + def iconAt(self, x, y): + """ Get icon at coordinates x,y. X and Y are in pixels """ + + w=config.iconsize + config.iconspace + + if self.mode=='l': + x2=int(x / w) + y2=int(y / w) + else: + x2=self.size - int(y/w) - 1 + y2=int(x/w) + + print "x2,y2", x2, y2 + ret=self.get(x2,y2) + + return(ret) + + def get(self, x, y): + ret=self.icons.get(x,y) + + return(ret) + + def _draw(self, cr, event): +# print "mode:", self.mode +# print "icons", len(self.icons) + + w=config.iconsize + config.iconspace + for x,y in self.icons: +# print x, y + + if self.mode=='l': + x2=x * (config.iconsize + config.iconspace) + y2=y * (config.iconsize + config.iconspace) + else: + x2=y * (config.iconsize + config.iconspace) + y2=(self.size-x-1) * (config.iconsize + config.iconspace) + + # Only repaint the needed icons + rect=gdk.Rectangle(x2, y2, w, w) + t=rect.intersect(event.area) + if t.width==0 and t.height==0: + continue + +# print "draw:", x, y + ico=self.icons.get(x,y) + ico.draw(cr, x2, y2, self.mode) + + def do_expose_event(self, event): + cr=self.window.cairo_create() + + cr.rectangle(event.area.x, event.area.y, + event.area.width, event.area.height) + + cr.clip() + + self._draw(cr, event) + + def setLastIcon(self, icon): + if icon==self.lasticon: + return + + if self.lasticon!=None: + self.lasticon.doCancel() + self.lasticon.invalidate(self.window) + self.lasticon=icon + + def do_button_press_event(self, event): + print "press", event.type + icon=self.iconAt(event.x, event.y) + if icon==None: + return +# rect=gdk.Rectangle(event.x,event.y,1,1) +# rect=gdk.Rectangle(0, 0, 100, 100) + icon.doPress() + icon.invalidate(self.window) + self.setLastIcon(icon) + +# gdk.Window.invalidate_rect(self.window, rect, True) + + return(True) + + def do_button_release_event(self, event): + print "release" + if self.lasticon!=None: + self.lasticon.invalidate(self.window) + self.lasticon.doRelease() + + self.setLastIcon(None) + + return(True) + + def do_leave_notify_event(self, event): + print "leave" + #print "leave", event.x, event.y + self.setLastIcon(None) + return(True) + + def do_pproperty_notify_event(self, event): + print "property" + icon=self.iconAt(event.x, event.y) + if icon==None: + return + icon.doCancel() + icon.invalidate(self.window) + return(True) + + def do_motion_notify_event(self, event): + print "motion" + icon=self.iconAt(event.x, event.y) + if self.lasticon==icon: + return(True) + + self.setLastIcon(None) + icon.doCancel() + icon.invalidate(self.window) + return(True) + + def do_button_press_event_old(self, event): + #print "press" + if event.type==gdk.BUTTON_PRESS: + print "press", event.type + if self.mode=='p': + self.setMode('l') + else: + self.setMode('p') + self.queue_draw() + return True + + # For debugging + def do_event1(self, event): + print "event:", event, event.type + + def butTest(self, arg): + print "but", arg + + def reloadIcons(self): + self.icons.load() + +# def on_orientation_changed(self, orientation): +# print "orch:", orientation +# o=orientation[0] +# self.setMode(o) + +class IconGridWidget(IconGrid, gtk.Widget): + def __init__(self, isconfig): + IconGrid.__init__(self, isconfig) + gtk.Widget.__init__(self) + + if isconfig: + w=4 * (config.iconsize + config.iconspace) + else: + w=self.size * (config.iconsize + config.iconspace) + + self.set_size_request(w, w) + + def do_realize(self): + screen=self.get_screen() + self.set_colormap(screen.get_rgba_colormap()) + self.set_app_paintable(True) + + self.set_flags(self.flags() | gtk.REALIZED) + + self.window=gdk.Window( + self.get_parent_window(), + width=self.allocation.width, + height=self.allocation.height, + window_type=gdk.WINDOW_CHILD, + wclass=gdk.INPUT_OUTPUT, + event_mask=self.get_events() | gdk.EXPOSURE_MASK + | gdk.BUTTON_PRESS_MASK + | gdk.BUTTON_RELEASE_MASK + | gdk.BUTTON_MOTION_MASK + | gdk.POINTER_MOTION_MASK + | gdk.POINTER_MOTION_HINT_MASK + | gdk.ENTER_NOTIFY_MASK + | gdk.LEAVE_NOTIFY_MASK ) + + self.window.set_user_data(self) + self.style.attach(self.window) + +# self.style.set_background(self.window, gtk.STATE_NORMAL) + self.window.move_resize(*self.allocation) + +# self.pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d( +# self.window, self.style.bg[gtk.STATE_NORMAL], STAR_PIXMAP) + +# self.gc = self.style.fg_gc[gtk.STATE_NORMAL] + + #gtk.Widget.do_realize(self) + #HomePluginItem.do_realize(self) + +# screen=self.get_screen() +# self.set_colormap(screen.get_rgba_colormap()) +# self.set_app_paintable(True) + + def do_unrealize(self): + #self.window.set_user_data(None) + self.window.destroy() + +#gobject.type_register(IconGrid) +gobject.type_register(IconGridWidget) + + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/icons.py b/src/icons.py new file mode 100755 index 0000000..72812de --- /dev/null +++ b/src/icons.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +import config +import apps +import icon +from icon import getIcon, Icon + +import gobject + +class IconIter: + def __init__(self, items): + self.iter=items.__iter__() + + def __iter__(self): + ret=self.iter.__iter__() + return(ret) + + def next(self): + ret=self.iter.next() + return(ret) + +class Icons(gobject.GObject): + def __init__(self, isconfig): + self.__gobject_init__() + self.icons={} + self.allicons={} + self.size=0 + self.isconfig=isconfig + + # signal handlers + self.h={} + + # setup allicons + maxsz=4 + for x in xrange(maxsz): + for y in xrange(maxsz): + k=(x,y) + ico=Icon(self.isconfig) + self.allicons[k]=ico + self.connect_one(ico) + + @classmethod + def register_signals(cls): + signals=icon.signals + for s in signals: + gobject.signal_new(s, cls, gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, (Icon,)) + + def __iter__(self): + return(IconIter(self.icons)) + + def connect_one(self, which): + self.h[which]={ + 'longpress': which.connect('long-press', self.signalLongpress), + 'click': which.connect('click', self.signalClick), + 'tripple': which.connect('tripple-click', + self.signalTrippleClick) + } + + def disconnect_one(self, which): + for i in self.h[which]: + which.disconnect(self.h[which][i]) + self.h.pop(which) +# which.disconnect(self.h[which]_longpress) +# which.disconnect(self.h_click) +# which.disconnect(self.h_tripple) + + def setSize(self, sz): +# print "sz:", sz, self.size + + if sz==self.size: + return + + old=self.icons + self.icons={} + + for x in xrange(sz): + for y in xrange(sz): + k=(x,y) + ico=self.allicons[k] + self.icons[k]=ico +# if old.has_key(k): +# old.pop(k) # Re-used +# else: +# self.connect_one(ico) + + # Disconnect signals +# for i in old: +# self.disconnect_one(old[i]) + + self.size=sz + + def signalLongpress(self, icon): + print "signalLongpress()", icon + self.emit('long-press', icon) + + def signalClick(self, icon): + print "signalClick()", icon + self.emit('click', icon) + + def signalTrippleClick(self, icon): + print "signalTrippleClick()", icon + self.emit('tripple-click', icon) + + def get(self, x, y): + k=(x,y) + if self.icons.has_key(k): + ret=self.icons[k] + else: + ret=None + + return(ret) + + def load(self): +# x=0 +# y=0 +# fn=["maegirls", "wifieye", 'battery-eye', 'image-viewer', +# 'tecnoballz', 'ncalc', 'rtcom-call-ui', 'rtcom-messaging-ui', +# 'extcalllog', 'browser', 'modest', 'osso-addressbook'] + + wapps=config.getApps() + sz=config.getSize() + + for k in wapps: + x,y=k + if x>=sz or y>=sz: + continue + + appname=wapps[k] + if appname!=None: + app=apps.readOne(appname) + app['icon2']=getIcon(app['icon']) + self.get(x,y).setApp(app) + +# for f in fn: +# dt=apps.readOne(f) +# dt['icon2']=getIcon(dt['icon']) +# print x, y, dt +# self.get(x,y).setApp(dt) +# x+=1 +# if x>=config.getSize(getSize()): +# x=0 +# y+=1 +## self.icons.append(p) + + print "end of Icons init" + + +gobject.type_register(Icons) +Icons.register_signals() + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/launcher.py b/src/launcher.py new file mode 100755 index 0000000..dacde19 --- /dev/null +++ b/src/launcher.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +import dbus + +bus=None +proxy=None + +def init(): + global bus, proxy + + bus=dbus.SessionBus() + + proxy=bus.get_object('com.nokia.HildonDesktop.AppMgr', + '/com/nokia/HildonDesktop/AppMgr') + +def launch(prog): + global bus, proxy + + proxy.LaunchApplication(prog, + dbus_interface='com.nokia.HildonDesktop.AppMgr') + +if __name__=="__main__": + init() + launch('wifieye') + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/portrait.py b/src/portrait.py new file mode 100644 index 0000000..45043c5 --- /dev/null +++ b/src/portrait.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# +# gPodder - A media aggregator and podcast client +# Copyright (c) 2005-2010 Thomas Perl and the gPodder Team +# +# gPodder 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 3 of the License, or +# (at your option) any later version. +# +# gPodder 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, see . +# + +import dbus +import dbus.glib + +import hildon +import osso + +# Replace this with your own gettext() functionality +#import gpodder +#_ = gpodder.gettext + + +class FremantleRotation(object): + """thp's screen rotation for Maemo 5 + + Simply instantiate an object of this class and let it auto-rotate + your StackableWindows depending on the device orientation. + + If you need to relayout a window, connect to its "configure-event" + signal and measure the ratio of width/height and relayout for that. + + You can set the mode for rotation to AUTOMATIC (default), NEVER or + ALWAYS with the set_mode() method. + """ + + AUTOMATIC, NEVER, ALWAYS = range(3) + + MODE_CAPTIONS = ('Automatic', 'Landscape', 'Portrait') + + # Privately-used constants + _PORTRAIT, _LANDSCAPE = ('portrait', 'landscape') + _ENABLE_ACCEL = 'req_accelerometer_enable' + _DISABLE_ACCEL = 'req_accelerometer_disable' + + # Defined in mce/dbus-names.h + _MCE_SERVICE = 'com.nokia.mce' + _MCE_REQUEST_PATH = '/com/nokia/mce/request' + _MCE_REQUEST_IF = 'com.nokia.mce.request' + + # sysfs device name for the keyboard slider switch + KBD_SLIDER = '/sys/devices/platform/gpio-switch/slide/state' + _KBD_OPEN = 'open' + _KBD_CLOSED = 'closed' + + def __init__(self, app_name, main_window=None, version='1.0', mode=0): + """Create a new rotation manager + + app_name ... The name of your application (for osso.Context) + main_window ... The root window (optional, hildon.StackableWindow) + version ... The version of your application (optional, string) + mode ... Initial mode for this manager (default: AUTOMATIC) + """ + self._orientation = None + self._main_window = main_window + self._mode = -1 + self._last_dbus_orientation = None + self._keyboard_state = self._get_keyboard_state() + app_id = '-'.join((app_name, self.__class__.__name__)) + self._osso_context = osso.Context(app_id, version, False) + program = hildon.Program.get_instance() + program.connect('notify::is-topmost', self._on_topmost_changed) + system_bus = dbus.Bus.get_system() + system_bus.add_signal_receiver(self._on_orientation_signal, \ + signal_name='sig_device_orientation_ind', \ + dbus_interface='com.nokia.mce.signal', \ + path='/com/nokia/mce/signal') + system_bus.add_signal_receiver(self._on_keyboard_signal, \ + signal_name='Condition', \ + dbus_interface='org.freedesktop.Hal.Device', \ + path='/org/freedesktop/Hal/devices/platform_slide') + self.set_mode(mode) + + self._send_mce_request(self._ENABLE_ACCEL) + + def set_mode(self, new_mode): + self._mode=new_mode + + def get_mode(self): + """Get the currently-set rotation mode + + This will return one of three values: AUTOMATIC, ALWAYS or NEVER. + """ + return self._mode + + def _send_mce_request(self, request): + rpc = osso.Rpc(self._osso_context) + rpc.rpc_run(self._MCE_SERVICE, \ + self._MCE_REQUEST_PATH, \ + self._MCE_REQUEST_IF, \ + request, \ + use_system_bus=True) + + def _on_topmost_changed(self, program, property_spec): + # XXX: This seems to never get called on Fremantle(?) + if self._mode == self.AUTOMATIC: + if program.get_is_topmost(): + self._send_mce_request(self._ENABLE_ACCEL) + else: + self._send_mce_request(self._DISABLE_ACCEL) + + def NO_get_main_window(self): + if self._main_window: + # If we have gotten the main window as parameter, return it and + # don't try "harder" to find another window using the stack + return self._main_window + else: + # The main window is at the "bottom" of the window stack, and as + # the list we get with get_windows() is sorted "topmost first", we + # simply take the last item of the list to get our main window + windows = self._stack.get_windows() + if windows: + return windows[-1] + else: + return None + + def _orientation_changed(self, orientation): + if self._orientation == orientation: + # Ignore repeated requests + return + + flags = 0 + + if orientation != self._LANDSCAPE: + flags |= hildon.PORTRAIT_MODE_SUPPORT + + if orientation == self._PORTRAIT: + flags |= hildon.PORTRAIT_MODE_REQUEST + +# window = self._get_main_window() +# if window is not None: +# hildon.hildon_gtk_window_set_portrait_flags(window, flags) + + self._orientation = orientation + + self.on_orientation_changed(orientation) + + def _get_keyboard_state(self): + # For sbox, if the device does not exist assume that it's closed + try: + return open(self.KBD_SLIDER).read().strip() + except IOError: + return self._KBD_CLOSED + + def _keyboard_state_changed(self): + state = self._get_keyboard_state() + + if state == self._KBD_OPEN: + self._orientation_changed(self._LANDSCAPE) + elif state == self._KBD_CLOSED: + if self._mode == self.AUTOMATIC: + self._orientation_changed(self._last_dbus_orientation) + elif self._mode == self.ALWAYS: + self._orientation_changed(self._PORTRAIT) + + self._keyboard_state = state + + def _on_keyboard_signal(self, condition, button_name): + if condition == 'ButtonPressed' and button_name == 'cover': + self._keyboard_state_changed() + + def _on_orientation_signal(self, orientation, stand, face, x, y, z): + print "orsignal" + if orientation in (self._PORTRAIT, self._LANDSCAPE): + if self._mode == self.AUTOMATIC and \ + self._keyboard_state != self._KBD_OPEN: + # Automatically set the rotation based on hardware orientation + self._orientation_changed(orientation) + + # Save the current orientation for "automatic" mode later on + self._last_dbus_orientation = orientation + diff --git a/src/widget.py b/src/widget.py new file mode 100755 index 0000000..34e73c7 --- /dev/null +++ b/src/widget.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +import gtk +import gobject +import hildon +from hildondesktop import * +from gtk import gdk +from math import pi +import cairo +import time + +from portrait import FremantleRotation +import launcher +from xdg.IconTheme import getIconPath +from win_config import WinConfig + +import config +import apps +from icon import Icon +from icongrid import IconGrid + +# IconGrid must be before HomePluginItem for its connect() +# and do_button_*() to override those of HomePluginItem +class DrlaunchPlugin(IconGrid, HomePluginItem, FremantleRotation): + def __init__(self): + IconGrid.__init__(self) + HomePluginItem.__init__(self) + FremantleRotation.__init__(self, 'DrlaunchPlugin') + + launcher.init() + config.load() + self.setSize(config.getSize()) + self.reloadIcons() + + self.set_settings(True) + self.connect('show-settings', self.slot_show_settings) + self.connect('long-press', self.signalLongpress) + + def do_realize(self): + screen=self.get_screen() + self.set_colormap(screen.get_rgba_colormap()) + self.set_app_paintable(True) + + HomePluginItem.do_realize(self) + + def on_orientation_changed(self, orientation): + print "orch:", orientation + o=orientation[0] + self.setMode(o) +# self.queue_draw() + + def do_expose_event(self, event): + IconGrid.do_expose_event(self, event) + HomePluginItem.do_expose_event(self, event) + + def do_buttonn_press_event(self, event): + print "press0" + + def slot_show_settings(self, dt): + print "settings", dt + s=WinConfig() + s.show_all() + s.connect('destroy', self.slotConfigDestroy) + + def slotConfigDestroy(self, sender): + print "destroy", sender + dt=sender.getData() + print "dt:", dt + config.setSize(dt['size']) + config.setApps(dt['apps']) + config.save() + + # Resize widget + self.setSize(dt['size']) + self.reloadIcons() + +# self.queue_draw() + + def signalLongpress(self, sender, icon): + print "launch:", icon.name + launcher.launch(icon.name) + + def resize(self): + w=(self.size * config.iconsize) + \ + (self.size * config.iconspace) + self.set_size_request(w, w) + + def setSize(self, size): + IconGrid.setSize(self, size) + self.resize() + +hd_plugin_type = DrlaunchPlugin + +def do1(): +# gobject.type_register(MyQ) + gobject.type_register(hd_plugin_type) + obj=gobject.new(hd_plugin_type, plugin_id="plugin_id") + obj.show_all() + gtk.main() + +def do2(): + win=DrlaunchPlugin() + win.connect('delete-event', gtk.main_quit) + + print "win:", win + +# t=DrlaunchPlugin() +# win.add(t) + + win.show_all() + gtk.main() + +if __name__=="__main__": + do1() + + + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/win_config.py b/src/win_config.py new file mode 100755 index 0000000..47b7436 --- /dev/null +++ b/src/win_config.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python +# coding=UTF-8 +# +# Copyright (C) 2010 Stefanos Harhalakis +# +# This file is part of wifieye. +# +# wifieye 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 3 of the License, or +# (at your option) any later version. +# +# wifieye 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 wifieye. If not, see . +# +# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ + +__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" + +import gtk +import gobject +import hildon +import time + +from hildon import StackableWindow +#from portrait import FremantleRotation +#from xdg.IconTheme import getIconPath + +import config +import apps +from icon import Icon, getIcon +from icongrid import IconGridWidget + +class WinConfig(StackableWindow): + def __init__(self, *args): + StackableWindow.__init__(self) + + self.setupUi() + + def setupUi(self): + self.igw=IconGridWidget(True) +# self.igw.setSize(config.getSize()) + + hbox=gtk.HBox() + self.add(hbox) + + # Add the icongrid + hbox.add(self.igw) + + # Now go for the right side + al=gtk.Alignment(yscale=0) + hbox.add(al) + + vbox=gtk.VBox() +# hbox.add(vbox) + al.add(vbox) + + vbox.add(gtk.Label('Grid size:')) + + self.butsSize=[] + for i in xrange(4): + but=gtk.ToggleButton("%sx%s" % (i+1,i+1)) + but.set_size_request(160, 90) + self.butsSize.append(but) + but.connect('toggled', self.slotButtonSize, i) + + hbox2=gtk.HBox() + vbox.add(hbox2) + hbox2.add(self.butsSize[0]) + hbox2.add(self.butsSize[1]) + hbox2=gtk.HBox() + vbox.add(hbox2) + hbox2.add(self.butsSize[2]) + hbox2.add(self.butsSize[3]) + + self.igw.connect('long-press', self.slotLongpress) + + self.ignore_toggle=False + + self.setSize(config.getSize()) + + def slotLongpress(self, sender, icon): + print "slp", icon + self.doConfig(icon) + + def slotButtonSize(self, sender, id): + print "size:", id + + self.setSize(id+1) + + def setSize(self, sz): + if self.ignore_toggle: + return + + self.ignore_toggle=True + + id=sz-1 + + for i in xrange(4): + but=self.butsSize[i] + but.set_active(i==id) + + self.ignore_toggle=False + + self.igw.setSize(sz) + + self.igw.queue_draw() + + + def doConfig(self, icon): + aps=apps.scan() + + lst=[aps[x]['name'] for x in aps] + lst.sort() + + dialog=gtk.Dialog('App select', None, + gtk.DIALOG_DESTROY_WITH_PARENT, buttons=()) + + selector=hildon.TouchSelectorEntry(text=True) + selector.set_column_selection_mode( + hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE) + + dialog.vbox.pack_start(selector, True, True, 0) + dialog.set_size_request(0,900) + dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) + dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) + + selector.append_text('None') + + idx=0 + cnt=1 + for app in lst: + if app==None: + continue + selector.append_text(app) + if icon.name!=None and aps[icon.name]['name']==app: + idx=cnt + cnt+=1 + + selector.set_active(0, idx) + + dialog.show_all() + + app=None + + r=dialog.run() + + if r==gtk.RESPONSE_OK: + cur=selector.get_current_text() + if cur=='None': + app=None + else: + for i in aps: + if aps[i]['name']==cur: + app=aps[i] + break + if app!=None: + app['icon2']=getIcon(app['icon']) + else: + app={ + 'id': None, + 'icon2': None, + } + icon.setApp(app) + + dialog.destroy() + + def getData(self): + sz=0 + for but in self.butsSize: + sz+=1 + if but.get_active()==True: + break + + print "conf: sz=", sz + + wapps={} + + for x in xrange(sz): + for y in xrange(sz): + ico=self.igw.get(x,y) + k=(x,y) + wapps[k]=ico.name + + ret={ + 'size': sz, + 'apps': wapps + } + + print "ret:", ret + + return(ret) + +if __name__=="__main__": + win=WinConfig() + win.connect('delete-event', gtk.main_quit) + + win.show_all() + gtk.main() + + + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/src/xdg/BaseDirectory.py b/src/xdg/BaseDirectory.py new file mode 100644 index 0000000..6f532c9 --- /dev/null +++ b/src/xdg/BaseDirectory.py @@ -0,0 +1,97 @@ +""" +This module is based on a rox module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log + +The freedesktop.org Base Directory specification provides a way for +applications to locate shared data and configuration: + + http://standards.freedesktop.org/basedir-spec/ + +(based on version 0.6) + +This module can be used to load and save from and to these directories. + +Typical usage: + + from rox import basedir + + for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'): + print "Load settings from", dir + + dir = basedir.save_config_path('mydomain.org', 'MyProg') + print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2" + +Note: see the rox.Options module for a higher-level API for managing options. +""" + +from __future__ import generators +import os + +_home = os.environ.get('HOME', '/') +xdg_data_home = os.environ.get('XDG_DATA_HOME', + os.path.join(_home, '.local', 'share')) + +xdg_data_dirs = [xdg_data_home] + \ + os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') + +xdg_config_home = os.environ.get('XDG_CONFIG_HOME', + os.path.join(_home, '.config')) + +xdg_config_dirs = [xdg_config_home] + \ + os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':') + +xdg_cache_home = os.environ.get('XDG_CACHE_HOME', + os.path.join(_home, '.cache')) + +xdg_data_dirs = filter(lambda x: x, xdg_data_dirs) +xdg_config_dirs = filter(lambda x: x, xdg_config_dirs) + +def save_config_path(*resource): + """Ensure $XDG_CONFIG_HOME// exists, and return its path. + 'resource' should normally be the name of your application. Use this + when SAVING configuration settings. Use the xdg_config_dirs variable + for loading.""" + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_config_home, resource) + if not os.path.isdir(path): + os.makedirs(path, 0700) + return path + +def save_data_path(*resource): + """Ensure $XDG_DATA_HOME// exists, and return its path. + 'resource' is the name of some shared resource. Use this when updating + a shared (between programs) database. Use the xdg_data_dirs variable + for loading.""" + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_data_home, resource) + if not os.path.isdir(path): + os.makedirs(path) + return path + +def load_config_paths(*resource): + """Returns an iterator which gives each directory named 'resource' in the + configuration search path. Information provided by earlier directories should + take precedence over later ones (ie, the user's config dir comes first).""" + resource = os.path.join(*resource) + for config_dir in xdg_config_dirs: + path = os.path.join(config_dir, resource) + if os.path.exists(path): yield path + +def load_first_config(*resource): + """Returns the first result from load_config_paths, or None if there is nothing + to load.""" + for x in load_config_paths(*resource): + return x + return None + +def load_data_paths(*resource): + """Returns an iterator which gives each directory named 'resource' in the + shared data search path. Information provided by earlier directories should + take precedence over later ones.""" + resource = os.path.join(*resource) + for data_dir in xdg_data_dirs: + path = os.path.join(data_dir, resource) + if os.path.exists(path): yield path diff --git a/src/xdg/Config.py b/src/xdg/Config.py new file mode 100644 index 0000000..e2fbe64 --- /dev/null +++ b/src/xdg/Config.py @@ -0,0 +1,39 @@ +""" +Functions to configure Basic Settings +""" + +language = "C" +windowmanager = None +icon_theme = "highcolor" +icon_size = 48 +cache_time = 5 +root_mode = False + +def setWindowManager(wm): + global windowmanager + windowmanager = wm + +def setIconTheme(theme): + global icon_theme + icon_theme = theme + import xdg.IconTheme + xdg.IconTheme.themes = [] + +def setIconSize(size): + global icon_size + icon_size = size + +def setCacheTime(time): + global cache_time + cache_time = time + +def setLocale(lang): + import locale + lang = locale.normalize(lang) + locale.setlocale(locale.LC_ALL, lang) + import xdg.Locale + xdg.Locale.update(lang) + +def setRootMode(boolean): + global root_mode + root_mode = boolean diff --git a/src/xdg/DesktopEntry.py b/src/xdg/DesktopEntry.py new file mode 100644 index 0000000..8626d7f --- /dev/null +++ b/src/xdg/DesktopEntry.py @@ -0,0 +1,397 @@ +""" +Complete implementation of the XDG Desktop Entry Specification Version 0.9.4 +http://standards.freedesktop.org/desktop-entry-spec/ + +Not supported: +- Encoding: Legacy Mixed +- Does not check exec parameters +- Does not check URL's +- Does not completly validate deprecated/kde items +- Does not completly check categories +""" + +from xdg.IniFile import * +from xdg.BaseDirectory import * +import os.path + +class DesktopEntry(IniFile): + "Class to parse and validate DesktopEntries" + + defaultGroup = 'Desktop Entry' + + def __init__(self, filename=None): + self.content = dict() + if filename and os.path.exists(filename): + self.parse(filename) + elif filename: + self.new(filename) + + def __str__(self): + return self.getName() + + def parse(self, file): + IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) + + # start standard keys + def getType(self): + return self.get('Type') + """ @deprecated, use getVersionString instead """ + def getVersion(self): + return self.get('Version', type="numeric") + def getVersionString(self): + return self.get('Version') + def getName(self): + return self.get('Name', locale=True) + def getGenericName(self): + return self.get('GenericName', locale=True) + def getNoDisplay(self): + return self.get('NoDisplay', type="boolean") + def getComment(self): + return self.get('Comment', locale=True) + def getIcon(self): + return self.get('Icon', locale=True) + def getHidden(self): + return self.get('Hidden', type="boolean") + def getOnlyShowIn(self): + return self.get('OnlyShowIn', list=True) + def getNotShowIn(self): + return self.get('NotShowIn', list=True) + def getTryExec(self): + return self.get('TryExec') + def getExec(self): + return self.get('Exec') + def getPath(self): + return self.get('Path') + def getTerminal(self): + return self.get('Terminal', type="boolean") + """ @deprecated, use getMimeTypes instead """ + def getMimeType(self): + return self.get('MimeType', list=True, type="regex") + def getMimeTypes(self): + return self.get('MimeType', list=True) + def getCategories(self): + return self.get('Categories', list=True) + def getStartupNotify(self): + return self.get('StartupNotify', type="boolean") + def getStartupWMClass(self): + return self.get('StartupWMClass') + def getURL(self): + return self.get('URL') + # end standard keys + + # start kde keys + def getServiceTypes(self): + return self.get('ServiceTypes', list=True) + def getDocPath(self): + return self.get('DocPath') + def getKeywords(self): + return self.get('Keywords', list=True, locale=True) + def getInitialPreference(self): + return self.get('InitialPreference') + def getDev(self): + return self.get('Dev') + def getFSType(self): + return self.get('FSType') + def getMountPoint(self): + return self.get('MountPoint') + def getReadonly(self): + return self.get('ReadOnly', type="boolean") + def getUnmountIcon(self): + return self.get('UnmountIcon', locale=True) + # end kde keys + + # start deprecated keys + def getMiniIcon(self): + return self.get('MiniIcon', locale=True) + def getTerminalOptions(self): + return self.get('TerminalOptions') + def getDefaultApp(self): + return self.get('DefaultApp') + def getProtocols(self): + return self.get('Protocols', list=True) + def getExtensions(self): + return self.get('Extensions', list=True) + def getBinaryPattern(self): + return self.get('BinaryPattern') + def getMapNotify(self): + return self.get('MapNotify') + def getEncoding(self): + return self.get('Encoding') + def getSwallowTitle(self): + return self.get('SwallowTitle', locale=True) + def getSwallowExec(self): + return self.get('SwallowExec') + def getSortOrder(self): + return self.get('SortOrder', list=True) + def getFilePattern(self): + return self.get('FilePattern', type="regex") + def getActions(self): + return self.get('Actions', list=True) + # end deprecated keys + + # desktop entry edit stuff + def new(self, filename): + if os.path.splitext(filename)[1] == ".desktop": + type = "Application" + elif os.path.splitext(filename)[1] == ".directory": + type = "Directory" + else: + raise ParsingError("Unknown extension", filename) + + self.content = dict() + self.addGroup(self.defaultGroup) + self.set("Type", type) + self.filename = filename + # end desktop entry edit stuff + + # validation stuff + def checkExtras(self): + # header + if self.defaultGroup == "KDE Desktop Entry": + self.warnings.append('[KDE Desktop Entry]-Header is deprecated') + + # file extension + if self.fileExtension == ".kdelnk": + self.warnings.append("File extension .kdelnk is deprecated") + elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": + self.warnings.append('Unknown File extension') + + # Type + try: + self.type = self.content[self.defaultGroup]["Type"] + except KeyError: + self.errors.append("Key 'Type' is missing") + + # Name + try: + self.name = self.content[self.defaultGroup]["Name"] + except KeyError: + self.errors.append("Key 'Name' is missing") + + def checkGroup(self, group): + # check if group header is valid + if not (group == self.defaultGroup \ + or re.match("^\Desktop Action [a-zA-Z]+\$", group) \ + or (re.match("^\X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group)): + self.errors.append("Invalid Group name: %s" % group) + else: + #OnlyShowIn and NotShowIn + if self.content[group].has_key("OnlyShowIn") and self.content[group].has_key("NotShowIn"): + self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") + + def checkKey(self, key, value, group): + # standard keys + if key == "Type": + if value == "ServiceType" or value == "Service" or value == "FSDevice": + self.warnings.append("Type=%s is a KDE extension" % key) + elif value == "MimeType": + self.warnings.append("Type=MimeType is deprecated") + elif not (value == "Application" or value == "Link" or value == "Directory"): + self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value) + + if self.fileExtension == ".directory" and not value == "Directory": + self.warnings.append("File extension is .directory, but Type is '%s'" % value) + elif self.fileExtension == ".desktop" and value == "Directory": + self.warnings.append("Files with Type=Directory should have the extension .directory") + + if value == "Application": + if not self.content[group].has_key("Exec"): + self.warnings.append("Type=Application needs 'Exec' key") + if value == "Link": + if not self.content[group].has_key("URL"): + self.warnings.append("Type=Application needs 'Exec' key") + + elif key == "Version": + self.checkValue(key, value) + + elif re.match("^Name"+xdg.Locale.regex+"$", key): + pass # locale string + + elif re.match("^GenericName"+xdg.Locale.regex+"$", key): + pass # locale string + + elif key == "NoDisplay": + self.checkValue(key, value, type="boolean") + + elif re.match("^Comment"+xdg.Locale.regex+"$", key): + pass # locale string + + elif re.match("^Icon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + + elif key == "Hidden": + self.checkValue(key, value, type="boolean") + + elif key == "OnlyShowIn": + self.checkValue(key, value, list=True) + self.checkOnlyShowIn(value) + + elif key == "NotShowIn": + self.checkValue(key, value, list=True) + self.checkOnlyShowIn(value) + + elif key == "TryExec": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Exec": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Path": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Terminal": + self.checkValue(key, value, type="boolean") + self.checkType(key, "Application") + + elif key == "MimeType": + self.checkValue(key, value, list=True) + self.checkType(key, "Application") + + elif key == "Categories": + self.checkValue(key, value) + self.checkType(key, "Application") + self.checkCategorie(value) + + elif key == "StartupNotify": + self.checkValue(key, value, type="boolean") + self.checkType(key, "Application") + + elif key == "StartupWMClass": + self.checkType(key, "Application") + + elif key == "URL": + self.checkValue(key, value) + self.checkType(key, "URL") + + # kde extensions + elif key == "ServiceTypes": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "DocPath": + self.checkValue(key, value) + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif re.match("^Keywords"+xdg.Locale.regex+"$", key): + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "InitialPreference": + self.checkValue(key, value, type="numeric") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "Dev": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "FSType": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "MountPoint": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "ReadOnly": + self.checkValue(key, value, type="boolean") + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + # deprecated keys + elif key == "Encoding": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "TerminalOptions": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "DefaultApp": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "Protocols": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "Extensions": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "BinaryPattern": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "MapNotify": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "SwallowExec": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "FilePattern": + self.checkValue(key, value, type="regex", list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "SortOrder": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "Actions": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + # "X-" extensions + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + + else: + self.errors.append("Invalid key: %s" % key) + + def checkType(self, key, type): + if not self.getType() == type: + self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) + + def checkOnlyShowIn(self, value): + values = self.getList(value) + valid = ["GNOME", "KDE", "ROX", "XFCE", "Old", "LXDE"] + for item in values: + if item not in valid and item[0:2] != "X-": + self.errors.append("'%s' is not a registered OnlyShowIn value" % item); + + def checkCategorie(self, value): + values = self.getList(value) + + main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Settings", "System", "Utility"] + hasmain = False + for item in values: + if item in main: + hasmain = True + if hasmain == False: + self.errors.append("Missing main category") + + additional = ["Building", "Debugger", "IDE", "GUIDesigner", "Profiling", "RevisionControl", "Translation", "Calendar", "ContactManagement", "Database", "Dictionary", "Chart", "Email", "Finance", "FlowChart", "PDA", "ProjectManagement", "Presentation", "Spreadsheet", "WordProcessor", "2DGraphics", "VectorGraphics", "3DGraphics", "RasterGraphics", "Scanning", "OCR", "Photography", "Publishing", "Viewer", "TextTools", "DesktopSettings", "HardwareSettings", "Printing", "PackageManager", "Dialup", "InstantMessaging", "Chat", "IRCClient", "FileTransfer", "HamRadio", "News", "P2P", "RemoteAccess", "Telephony", "TelephonyTools", "VideoConference", "WebBrowser", "WebDevelopment", "Midi", "Mixer", "Sequencer", "Tuner", "TV", "AudioVideoEditing", "Player", "Recorder", "DiscBurning", "ActionGame", "AdventureGame", "ArcadeGame", "BoardGame", "BlocksGame", "CardGame", "KidsGame", "LogicGame", "RolePlaying", "Simulation", "SportsGame", "StrategyGame", "Art", "Construction", "Music", "Languages", "Science", "ArtificialIntelligence", "Astronomy", "Biology", "Chemistry", "ComputerScience", "DataVisualization", "Economy", "Electricity", "Geography", "Geology", "Geoscience", "History", "ImageProcessing", "Literature", "Math", "NumericalAnalysis", "MedicalSoftware", "Physics", "Robotics", "Sports", "ParallelComputing", "Amusement", "Archiving", "Compression", "Electronics", "Emulator", "Engineering", "FileTools", "FileManager", "TerminalEmulator", "Filesystem", "Monitor", "Security", "Accessibility", "Calculator", "Clock", "TextEditor", "Documentation", "Core", "KDE", "GNOME", "GTK", "Qt", "Motif", "Java", "ConsoleOnly", "Screensaver", "TrayIcon", "Applet", "Shell"] + + for item in values: + if item not in additional + main and item[0:2] != "X-": + self.errors.append("'%s' is not a registered Category" % item); + diff --git a/src/xdg/Exceptions.py b/src/xdg/Exceptions.py new file mode 100644 index 0000000..f7d08be --- /dev/null +++ b/src/xdg/Exceptions.py @@ -0,0 +1,51 @@ +""" +Exception Classes for the xdg package +""" + +debug = False + +class Error(Exception): + def __init__(self, msg): + self.msg = msg + Exception.__init__(self, msg) + def __str__(self): + return self.msg + +class ValidationError(Error): + def __init__(self, msg, file): + self.msg = msg + self.file = file + Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) + +class ParsingError(Error): + def __init__(self, msg, file): + self.msg = msg + self.file = file + Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) + +class NoKeyError(Error): + def __init__(self, key, group, file): + Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) + self.key = key + self.group = group + +class DuplicateKeyError(Error): + def __init__(self, key, group, file): + Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) + self.key = key + self.group = group + +class NoGroupError(Error): + def __init__(self, group, file): + Error.__init__(self, "No group: %s in file %s" % (group, file)) + self.group = group + +class DuplicateGroupError(Error): + def __init__(self, group, file): + Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) + self.group = group + +class NoThemeError(Error): + def __init__(self, theme): + Error.__init__(self, "No such icon-theme: %s" % theme) + self.theme = theme diff --git a/src/xdg/IconTheme.py b/src/xdg/IconTheme.py new file mode 100644 index 0000000..1f1fa18 --- /dev/null +++ b/src/xdg/IconTheme.py @@ -0,0 +1,391 @@ +""" +Complete implementation of the XDG Icon Spec Version 0.8 +http://standards.freedesktop.org/icon-theme-spec/ +""" + +import os, sys, time + +from xdg.IniFile import * +from xdg.BaseDirectory import * +from xdg.Exceptions import * + +import xdg.Config + +class IconTheme(IniFile): + "Class to parse and validate IconThemes" + def __init__(self): + IniFile.__init__(self) + + def __repr__(self): + return self.name + + def parse(self, file): + IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"]) + self.dir = os.path.dirname(file) + (nil, self.name) = os.path.split(self.dir) + + def getDir(self): + return self.dir + + # Standard Keys + def getName(self): + return self.get('Name', locale=True) + def getComment(self): + return self.get('Comment', locale=True) + def getInherits(self): + return self.get('Inherits', list=True) + def getDirectories(self): + return self.get('Directories', list=True) + def getHidden(self): + return self.get('Hidden', type="boolean") + def getExample(self): + return self.get('Example') + + # Per Directory Keys + def getSize(self, directory): + return self.get('Size', type="integer", group=directory) + def getContext(self, directory): + return self.get('Context', group=directory) + def getType(self, directory): + value = self.get('Type', group=directory) + if value: + return value + else: + return "Threshold" + def getMaxSize(self, directory): + value = self.get('MaxSize', type="integer", group=directory) + if value or value == 0: + return value + else: + return self.getSize(directory) + def getMinSize(self, directory): + value = self.get('MinSize', type="integer", group=directory) + if value or value == 0: + return value + else: + return self.getSize(directory) + def getThreshold(self, directory): + value = self.get('Threshold', type="integer", group=directory) + if value or value == 0: + return value + else: + return 2 + + # validation stuff + def checkExtras(self): + # header + if self.defaultGroup == "KDE Icon Theme": + self.warnings.append('[KDE Icon Theme]-Header is deprecated') + + # file extension + if self.fileExtension == ".theme": + pass + elif self.fileExtension == ".desktop": + self.warnings.append('.desktop fileExtension is deprecated') + else: + self.warnings.append('Unknown File extension') + + # Check required keys + # Name + try: + self.name = self.content[self.defaultGroup]["Name"] + except KeyError: + self.errors.append("Key 'Name' is missing") + + # Comment + try: + self.comment = self.content[self.defaultGroup]["Comment"] + except KeyError: + self.errors.append("Key 'Comment' is missing") + + # Directories + try: + self.directories = self.content[self.defaultGroup]["Directories"] + except KeyError: + self.errors.append("Key 'Directories' is missing") + + def checkGroup(self, group): + # check if group header is valid + if group == self.defaultGroup: + pass + elif group in self.getDirectories(): + try: + self.type = self.content[group]["Type"] + except KeyError: + self.type = "Threshold" + try: + self.name = self.content[group]["Name"] + except KeyError: + self.errors.append("Key 'Name' in Group '%s' is missing" % group) + elif not (re.match("^\[X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group): + self.errors.append("Invalid Group name: %s" % group) + + def checkKey(self, key, value, group): + # standard keys + if group == self.defaultGroup: + if re.match("^Name"+xdg.Locale.regex+"$", key): + pass + elif re.match("^Comment"+xdg.Locale.regex+"$", key): + pass + elif key == "Inherits": + self.checkValue(key, value, list=True) + elif key == "Directories": + self.checkValue(key, value, list=True) + elif key == "Hidden": + self.checkValue(key, value, type="boolean") + elif key == "Example": + self.checkValue(key, value) + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + elif group in self.getDirectories(): + if key == "Size": + self.checkValue(key, value, type="integer") + elif key == "Context": + self.checkValue(key, value) + elif key == "Type": + self.checkValue(key, value) + if value not in ["Fixed", "Scalable", "Threshold"]: + self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value) + elif key == "MaxSize": + self.checkValue(key, value, type="integer") + if self.type != "Scalable": + self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type) + elif key == "MinSize": + self.checkValue(key, value, type="integer") + if self.type != "Scalable": + self.errors.append("Key 'MinSize' give, but Type is %s" % self.type) + elif key == "Threshold": + self.checkValue(key, value, type="integer") + if self.type != "Threshold": + self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + + +class IconData(IniFile): + "Class to parse and validate IconData Files" + def __init__(self): + IniFile.__init__(self) + + def __repr__(self): + return self.getDisplayName() + + def parse(self, file): + IniFile.parse(self, file, ["Icon Data"]) + + # Standard Keys + def getDisplayName(self): + return self.get('DisplayName', locale=True) + def getEmbeddedTextRectangle(self): + return self.get('EmbeddedTextRectangle', list=True) + def getAttachPoints(self): + return self.get('AttachPoints', type="point", list=True) + + # validation stuff + def checkExtras(self): + # file extension + if self.fileExtension != ".icon": + self.warnings.append('Unknown File extension') + + def checkGroup(self, group): + # check if group header is valid + if not (group == self.defaultGroup \ + or (re.match("^\[X-", group) and group.encode("ascii", 'ignore') == group)): + self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace")) + + def checkKey(self, key, value, group): + # standard keys + if re.match("^DisplayName"+xdg.Locale.regex+"$", key): + pass + elif key == "EmbeddedTextRectangle": + self.checkValue(key, value, type="integer", list=True) + elif key == "AttachPoints": + self.checkValue(key, value, type="point", list=True) + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + + + +icondirs = [] +for basedir in xdg_data_dirs: + icondirs.append(os.path.join(basedir, "icons")) + icondirs.append(os.path.join(basedir, "pixmaps")) +icondirs.append(os.path.expanduser("~/.icons")) + +# just cache variables, they give a 10x speed improvement +themes = [] +cache = dict() +dache = dict() +eache = dict() + +def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): + global themes + + if size == None: + size = xdg.Config.icon_size + if theme == None: + theme = xdg.Config.icon_theme + + # if we have an absolute path, just return it + if os.path.isabs(iconname): + return iconname + + # check if it has an extension and strip it + if os.path.splitext(iconname)[1][1:] in extensions: + iconname = os.path.splitext(iconname)[0] + + # parse theme files + try: + if themes[0].name != theme: + themes = [] + __addTheme(theme) + except IndexError: + __addTheme(theme) + + # more caching (icon looked up in the last 5 seconds?) + tmp = "".join([iconname, str(size), theme, "".join(extensions)]) + if eache.has_key(tmp): + if int(time.time() - eache[tmp][0]) >= xdg.Config.cache_time: + del eache[tmp] + else: + return eache[tmp][1] + + for thme in themes: + icon = LookupIcon(iconname, size, thme, extensions) + if icon: + eache[tmp] = [time.time(), icon] + return icon + + # cache stuff again (directories lookuped up in the last 5 seconds?) + for directory in icondirs: + if (not dache.has_key(directory) \ + or (int(time.time() - dache[directory][1]) >= xdg.Config.cache_time \ + and dache[directory][2] < os.path.getmtime(directory))) \ + and os.path.isdir(directory): + dache[directory] = [os.listdir(directory), time.time(), os.path.getmtime(directory)] + + for dir, values in dache.items(): + for extension in extensions: + try: + if iconname + "." + extension in values[0]: + icon = os.path.join(dir, iconname + "." + extension) + eache[tmp] = [time.time(), icon] + return icon + except UnicodeDecodeError, e: + if debug: + raise e + else: + pass + + # we haven't found anything? "hicolor" is our fallback + if theme != "hicolor": + icon = getIconPath(iconname, size, "hicolor") + eache[tmp] = [time.time(), icon] + return icon + +def getIconData(path): + if os.path.isfile(path): + dirname = os.path.dirname(path) + basename = os.path.basename(path) + if os.path.isfile(os.path.join(dirname, basename + ".icon")): + data = IconData() + data.parse(os.path.join(dirname, basename + ".icon")) + return data + +def __addTheme(theme): + for dir in icondirs: + if os.path.isfile(os.path.join(dir, theme, "index.theme")): + __parseTheme(os.path.join(dir,theme, "index.theme")) + break + elif os.path.isfile(os.path.join(dir, theme, "index.desktop")): + __parseTheme(os.path.join(dir,theme, "index.desktop")) + break + else: + if debug: + raise NoThemeError(theme) + +def __parseTheme(file): + theme = IconTheme() + theme.parse(file) + themes.append(theme) + for subtheme in theme.getInherits(): + __addTheme(subtheme) + +def LookupIcon(iconname, size, theme, extensions): + # look for the cache + if not cache.has_key(theme.name): + cache[theme.name] = [] + cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup + cache[theme.name].append(0) # [1] mtime + cache[theme.name].append(dict()) # [2] dir: [subdir, [items]] + + # cache stuff (directory lookuped up the in the last 5 seconds?) + if int(time.time() - cache[theme.name][0]) >= xdg.Config.cache_time: + cache[theme.name][0] = time.time() + for subdir in theme.getDirectories(): + for directory in icondirs: + dir = os.path.join(directory,theme.name,subdir) + if (not cache[theme.name][2].has_key(dir) \ + or cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \ + and subdir != "" \ + and os.path.isdir(dir): + cache[theme.name][2][dir] = [subdir, os.listdir(dir)] + cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name)) + + for dir, values in cache[theme.name][2].items(): + if DirectoryMatchesSize(values[0], size, theme): + for extension in extensions: + if iconname + "." + extension in values[1]: + return os.path.join(dir, iconname + "." + extension) + + minimal_size = sys.maxint + closest_filename = "" + for dir, values in cache[theme.name][2].items(): + distance = DirectorySizeDistance(values[0], size, theme) + if distance < minimal_size: + for extension in extensions: + if iconname + "." + extension in values[1]: + closest_filename = os.path.join(dir, iconname + "." + extension) + minimal_size = distance + + return closest_filename + +def DirectoryMatchesSize(subdir, iconsize, theme): + Type = theme.getType(subdir) + Size = theme.getSize(subdir) + Threshold = theme.getThreshold(subdir) + MinSize = theme.getMinSize(subdir) + MaxSize = theme.getMaxSize(subdir) + if Type == "Fixed": + return Size == iconsize + elif Type == "Scaleable": + return MinSize <= iconsize <= MaxSize + elif Type == "Threshold": + return Size - Threshold <= iconsize <= Size + Threshold + +def DirectorySizeDistance(subdir, iconsize, theme): + Type = theme.getType(subdir) + Size = theme.getSize(subdir) + Threshold = theme.getThreshold(subdir) + MinSize = theme.getMinSize(subdir) + MaxSize = theme.getMaxSize(subdir) + if Type == "Fixed": + return abs(Size - iconsize) + elif Type == "Scalable": + if iconsize < MinSize: + return MinSize - iconsize + elif iconsize > MaxSize: + return MaxSize - iconsize + return 0 + elif Type == "Threshold": + if iconsize < Size - Threshold: + return MinSize - iconsize + elif iconsize > Size + Threshold: + return iconsize - MaxSize + return 0 diff --git a/src/xdg/IniFile.py b/src/xdg/IniFile.py new file mode 100644 index 0000000..f3f08c7 --- /dev/null +++ b/src/xdg/IniFile.py @@ -0,0 +1,406 @@ +""" +Base Class for DesktopEntry, IconTheme and IconData +""" + +import re, os, stat, codecs +from Exceptions import * +import xdg.Locale + +class IniFile: + defaultGroup = '' + fileExtension = '' + + filename = '' + + tainted = False + + def __init__(self, filename=None): + self.content = dict() + if filename: + self.parse(filename) + + def __cmp__(self, other): + return cmp(self.content, other.content) + + def parse(self, filename, headers=None): + # for performance reasons + content = self.content + + if not os.path.isfile(filename): + raise ParsingError("File not found", filename) + + try: + fd = file(filename, 'r') + except IOError, e: + if debug: + raise e + else: + return + + # parse file + for line in fd: + line = line.strip() + # empty line + if not line: + continue + # comment + elif line[0] == '#': + continue + # new group + elif line[0] == '[': + currentGroup = line.lstrip("[").rstrip("]") + if debug and self.hasGroup(currentGroup): + raise DuplicateGroupError(currentGroup, filename) + else: + content[currentGroup] = {} + # key + else: + index = line.find("=") + key = line[0:index].strip() + value = line[index+1:].strip() + try: + if debug and self.hasKey(key, currentGroup): + raise DuplicateKeyError(key, currentGroup, filename) + else: + content[currentGroup][key] = value + except (IndexError, UnboundLocalError): + raise ParsingError("Parsing error on key, group missing", filename) + + fd.close() + + self.filename = filename + self.tainted = False + + # check header + if headers: + for header in headers: + if content.has_key(header): + self.defaultGroup = header + break + else: + raise ParsingError("[%s]-Header missing" % headers[0], filename) + + # start stuff to access the keys + def get(self, key, group=None, locale=False, type="string", list=False): + # set default group + if not group: + group = self.defaultGroup + + # return key (with locale) + if self.content.has_key(group) and self.content[group].has_key(key): + if locale: + value = self.content[group][self.__addLocale(key, group)] + else: + value = self.content[group][key] + else: + if debug: + if not self.content.has_key(group): + raise NoGroupError(group, self.filename) + elif not self.content[group].has_key(key): + raise NoKeyError(key, group, self.filename) + else: + value = "" + + if list == True: + values = self.getList(value) + result = [] + else: + values = [value] + + for value in values: + if type == "string" and locale == True: + value = value.decode("utf-8", "ignore") + elif type == "boolean": + value = self.__getBoolean(value) + elif type == "integer": + try: + value = int(value) + except ValueError: + value = 0 + elif type == "numeric": + try: + value = float(value) + except ValueError: + value = 0.0 + elif type == "regex": + value = re.compile(value) + elif type == "point": + value = value.split(",") + + if list == True: + result.append(value) + else: + result = value + + return result + # end stuff to access the keys + + # start subget + def getList(self, string): + if re.search(r"(? 0: + key = key + "[" + xdg.Locale.langs[0] + "]" + + try: + if isinstance(value, unicode): + self.content[group][key] = value.encode("utf-8", "ignore") + else: + self.content[group][key] = value + except KeyError: + raise NoGroupError(group, self.filename) + + self.tainted = (value == self.get(key, group)) + + def addGroup(self, group): + if self.hasGroup(group): + if debug: + raise DuplicateGroupError(group, self.filename) + else: + pass + else: + self.content[group] = {} + self.tainted = True + + def removeGroup(self, group): + existed = group in self.content + if existed: + del self.content[group] + self.tainted = True + else: + if debug: + raise NoGroupError(group, self.filename) + return existed + + def removeKey(self, key, group=None, locales=True): + # set default group + if not group: + group = self.defaultGroup + + try: + if locales: + for (name, value) in self.content[group].items(): + if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key: + value = self.content[group][name] + del self.content[group][name] + value = self.content[group][key] + del self.content[group][key] + self.tainted = True + return value + except KeyError, e: + if debug: + if e == group: + raise NoGroupError(group, self.filename) + else: + raise NoKeyError(key, group, self.filename) + else: + return "" + + # misc + def groups(self): + return self.content.keys() + + def hasGroup(self, group): + if self.content.has_key(group): + return True + else: + return False + + def hasKey(self, key, group=None): + # set default group + if not group: + group = self.defaultGroup + + if self.content[group].has_key(key): + return True + else: + return False + + def getFileName(self): + return self.filename diff --git a/src/xdg/Locale.py b/src/xdg/Locale.py new file mode 100644 index 0000000..d30d91a --- /dev/null +++ b/src/xdg/Locale.py @@ -0,0 +1,79 @@ +""" +Helper Module for Locale settings + +This module is based on a ROX module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log +""" + +import os +from locale import normalize + +regex = "(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z\-0-9]+)?(@[a-zA-Z]+)?\])?" + +def _expand_lang(locale): + locale = normalize(locale) + COMPONENT_CODESET = 1 << 0 + COMPONENT_MODIFIER = 1 << 1 + COMPONENT_TERRITORY = 1 << 2 + # split up the locale into its base components + mask = 0 + pos = locale.find('@') + if pos >= 0: + modifier = locale[pos:] + locale = locale[:pos] + mask |= COMPONENT_MODIFIER + else: + modifier = '' + pos = locale.find('.') + codeset = '' + if pos >= 0: + locale = locale[:pos] + pos = locale.find('_') + if pos >= 0: + territory = locale[pos:] + locale = locale[:pos] + mask |= COMPONENT_TERRITORY + else: + territory = '' + language = locale + ret = [] + for i in range(mask+1): + if not (i & ~mask): # if all components for this combo exist ... + val = language + if i & COMPONENT_TERRITORY: val += territory + if i & COMPONENT_CODESET: val += codeset + if i & COMPONENT_MODIFIER: val += modifier + ret.append(val) + ret.reverse() + return ret + +def expand_languages(languages=None): + # Get some reasonable defaults for arguments that were not supplied + if languages is None: + languages = [] + for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): + val = os.environ.get(envar) + if val: + languages = val.split(':') + break + #if 'C' not in languages: + # languages.append('C') + + # now normalize and expand the languages + nelangs = [] + for lang in languages: + for nelang in _expand_lang(lang): + if nelang not in nelangs: + nelangs.append(nelang) + return nelangs + +def update(language=None): + global langs + if language: + langs = expand_languages([language]) + else: + langs = expand_languages() + +langs = [] +update() diff --git a/src/xdg/Menu.py b/src/xdg/Menu.py new file mode 100644 index 0000000..d437ee4 --- /dev/null +++ b/src/xdg/Menu.py @@ -0,0 +1,1074 @@ +""" +Implementation of the XDG Menu Specification Version 1.0.draft-1 +http://standards.freedesktop.org/menu-spec/ +""" + +from __future__ import generators +import locale, os, xml.dom.minidom + +from xdg.BaseDirectory import * +from xdg.DesktopEntry import * +from xdg.Exceptions import * + +import xdg.Locale +import xdg.Config + +ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE + +# for python <= 2.3 +try: + reversed = reversed +except NameError: + def reversed(x): + return x[::-1] + +class Menu: + def __init__(self): + # Public stuff + self.Name = "" + self.Directory = None + self.Entries = [] + self.Doc = "" + self.Filename = "" + self.Depth = 0 + self.Parent = None + self.NotInXml = False + + # Can be one of Deleted/NoDisplay/Hidden/Empty/NotShowIn or True + self.Show = True + self.Visible = 0 + + # Private stuff, only needed for parsing + self.AppDirs = [] + self.DefaultLayout = None + self.Deleted = "notset" + self.Directories = [] + self.DirectoryDirs = [] + self.Layout = None + self.MenuEntries = [] + self.Moves = [] + self.OnlyUnallocated = "notset" + self.Rules = [] + self.Submenus = [] + + def __str__(self): + return self.Name + + def __add__(self, other): + for dir in other.AppDirs: + self.AppDirs.append(dir) + + for dir in other.DirectoryDirs: + self.DirectoryDirs.append(dir) + + for directory in other.Directories: + self.Directories.append(directory) + + if other.Deleted != "notset": + self.Deleted = other.Deleted + + if other.OnlyUnallocated != "notset": + self.OnlyUnallocated = other.OnlyUnallocated + + if other.Layout: + self.Layout = other.Layout + + if other.DefaultLayout: + self.DefaultLayout = other.DefaultLayout + + for rule in other.Rules: + self.Rules.append(rule) + + for move in other.Moves: + self.Moves.append(move) + + for submenu in other.Submenus: + self.addSubmenu(submenu) + + return self + + # FIXME: Performance: cache getName() + def __cmp__(self, other): + return locale.strcoll(self.getName(), other.getName()) + + def __eq__(self, other): + if self.Name == str(other): + return True + else: + return False + + """ PUBLIC STUFF """ + def getEntries(self, hidden=False): + for entry in self.Entries: + if hidden == True: + yield entry + elif entry.Show == True: + yield entry + + # FIXME: Add searchEntry/seaqrchMenu function + # search for name/comment/genericname/desktopfileide + # return multiple items + + def getMenuEntry(self, desktopfileid, deep = False): + for menuentry in self.MenuEntries: + if menuentry.DesktopFileID == desktopfileid: + return menuentry + if deep == True: + for submenu in self.Submenus: + submenu.getMenuEntry(desktopfileid, deep) + + def getMenu(self, path): + array = path.split("/", 1) + for submenu in self.Submenus: + if submenu.Name == array[0]: + if len(array) > 1: + return submenu.getMenu(array[1]) + else: + return submenu + + def getPath(self, org=False, toplevel=False): + parent = self + names=[] + while 1: + if org: + names.append(parent.Name) + else: + names.append(parent.getName()) + if parent.Depth > 0: + parent = parent.Parent + else: + break + names.reverse() + path = "" + if toplevel == False: + names.pop(0) + for name in names: + path = os.path.join(path, name) + return path + + def getName(self): + try: + return self.Directory.DesktopEntry.getName() + except AttributeError: + return self.Name + + def getGenericName(self): + try: + return self.Directory.DesktopEntry.getGenericName() + except AttributeError: + return "" + + def getComment(self): + try: + return self.Directory.DesktopEntry.getComment() + except AttributeError: + return "" + + def getIcon(self): + try: + return self.Directory.DesktopEntry.getIcon() + except AttributeError: + return "" + + """ PRIVATE STUFF """ + def addSubmenu(self, newmenu): + for submenu in self.Submenus: + if submenu == newmenu: + submenu += newmenu + break + else: + self.Submenus.append(newmenu) + newmenu.Parent = self + newmenu.Depth = self.Depth + 1 + +class Move: + "A move operation" + def __init__(self, node=None): + if node: + self.parseNode(node) + else: + self.Old = "" + self.New = "" + + def __cmp__(self, other): + return cmp(self.Old, other.Old) + + def parseNode(self, node): + for child in node.childNodes: + if child.nodeType == ELEMENT_NODE: + if child.tagName == "Old": + try: + self.parseOld(child.childNodes[0].nodeValue) + except IndexError: + raise ValidationError('Old cannot be empty', '??') + elif child.tagName == "New": + try: + self.parseNew(child.childNodes[0].nodeValue) + except IndexError: + raise ValidationError('New cannot be empty', '??') + + def parseOld(self, value): + self.Old = value + def parseNew(self, value): + self.New = value + + +class Layout: + "Menu Layout class" + def __init__(self, node=None): + self.order = [] + if node: + self.show_empty = node.getAttribute("show_empty") or "false" + self.inline = node.getAttribute("inline") or "false" + self.inline_limit = node.getAttribute("inline_limit") or 4 + self.inline_header = node.getAttribute("inline_header") or "true" + self.inline_alias = node.getAttribute("inline_alias") or "false" + self.inline_limit = int(self.inline_limit) + self.parseNode(node) + else: + self.show_empty = "false" + self.inline = "false" + self.inline_limit = 4 + self.inline_header = "true" + self.inline_alias = "false" + self.order.append(["Merge", "menus"]) + self.order.append(["Merge", "files"]) + + def parseNode(self, node): + for child in node.childNodes: + if child.nodeType == ELEMENT_NODE: + if child.tagName == "Menuname": + try: + self.parseMenuname( + child.childNodes[0].nodeValue, + child.getAttribute("show_empty") or "false", + child.getAttribute("inline") or "false", + child.getAttribute("inline_limit") or 4, + child.getAttribute("inline_header") or "true", + child.getAttribute("inline_alias") or "false" ) + except IndexError: + raise ValidationError('Menuname cannot be empty', "") + elif child.tagName == "Separator": + self.parseSeparator() + elif child.tagName == "Filename": + try: + self.parseFilename(child.childNodes[0].nodeValue) + except IndexError: + raise ValidationError('Filename cannot be empty', "") + elif child.tagName == "Merge": + self.parseMerge(child.getAttribute("type") or "all") + + def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"): + self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias]) + self.order[-1][4] = int(self.order[-1][4]) + + def parseSeparator(self): + self.order.append(["Separator"]) + + def parseFilename(self, value): + self.order.append(["Filename", value]) + + def parseMerge(self, type="all"): + self.order.append(["Merge", type]) + + +class Rule: + "Inlcude / Exclude Rules Class" + def __init__(self, type, node=None): + # Type is Include or Exclude + self.Type = type + # Rule is a python expression + self.Rule = "" + + # Private attributes, only needed for parsing + self.Depth = 0 + self.Expr = [ "or" ] + self.New = True + + # Begin parsing + if node: + self.parseNode(node) + self.compile() + + def __str__(self): + return self.Rule + + def compile(self): + exec(""" +def do(menuentries, type, run): + for menuentry in menuentries: + if run == 2 and ( menuentry.MatchedInclude == True \ + or menuentry.Allocated == True ): + continue + elif %s: + if type == "Include": + menuentry.Add = True + menuentry.MatchedInclude = True + else: + menuentry.Add = False + return menuentries +""" % self.Rule) in self.__dict__ + + def parseNode(self, node): + for child in node.childNodes: + if child.nodeType == ELEMENT_NODE: + if child.tagName == 'Filename': + try: + self.parseFilename(child.childNodes[0].nodeValue) + except IndexError: + raise ValidationError('Filename cannot be empty', "???") + elif child.tagName == 'Category': + try: + self.parseCategory(child.childNodes[0].nodeValue) + except IndexError: + raise ValidationError('Category cannot be empty', "???") + elif child.tagName == 'All': + self.parseAll() + elif child.tagName == 'And': + self.parseAnd(child) + elif child.tagName == 'Or': + self.parseOr(child) + elif child.tagName == 'Not': + self.parseNot(child) + + def parseNew(self, set=True): + if not self.New: + self.Rule += " " + self.Expr[self.Depth] + " " + if not set: + self.New = True + elif set: + self.New = False + + def parseFilename(self, value): + self.parseNew() + self.Rule += "menuentry.DesktopFileID == '%s'" % value.strip().replace("\\", r"\\").replace("'", r"\'") + + def parseCategory(self, value): + self.parseNew() + self.Rule += "'%s' in menuentry.Categories" % value.strip() + + def parseAll(self): + self.parseNew() + self.Rule += "True" + + def parseAnd(self, node): + self.parseNew(False) + self.Rule += "(" + self.Depth += 1 + self.Expr.append("and") + self.parseNode(node) + self.Depth -= 1 + self.Expr.pop() + self.Rule += ")" + + def parseOr(self, node): + self.parseNew(False) + self.Rule += "(" + self.Depth += 1 + self.Expr.append("or") + self.parseNode(node) + self.Depth -= 1 + self.Expr.pop() + self.Rule += ")" + + def parseNot(self, node): + self.parseNew(False) + self.Rule += "not (" + self.Depth += 1 + self.Expr.append("or") + self.parseNode(node) + self.Depth -= 1 + self.Expr.pop() + self.Rule += ")" + + +class MenuEntry: + "Wrapper for 'Menu Style' Desktop Entries" + def __init__(self, filename, dir="", prefix=""): + # Create entry + self.DesktopEntry = DesktopEntry(os.path.join(dir,filename)) + self.setAttributes(filename, dir, prefix) + + # Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True + self.Show = True + + # Semi-Private + self.Original = None + self.Parents = [] + + # Private Stuff + self.Allocated = False + self.Add = False + self.MatchedInclude = False + + # Caching + self.Categories = self.DesktopEntry.getCategories() + + def save(self): + if self.DesktopEntry.tainted == True: + self.DesktopEntry.write() + + def getDir(self): + return self.DesktopEntry.filename.replace(self.Filename, '') + + def getType(self): + # Can be one of System/User/Both + if xdg.Config.root_mode == False: + if self.Original: + return "Both" + elif xdg_data_dirs[0] in self.DesktopEntry.filename: + return "User" + else: + return "System" + else: + return "User" + + def setAttributes(self, filename, dir="", prefix=""): + self.Filename = filename + self.Prefix = prefix + self.DesktopFileID = os.path.join(prefix,filename).replace("/", "-") + + if not os.path.isabs(self.DesktopEntry.filename): + self.__setFilename() + + def updateAttributes(self): + if self.getType() == "System": + self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) + self.__setFilename() + + def __setFilename(self): + if xdg.Config.root_mode == False: + path = xdg_data_dirs[0] + else: + path= xdg_data_dirs[1] + + if self.DesktopEntry.getType() == "Application": + dir = os.path.join(path, "applications") + else: + dir = os.path.join(path, "desktop-directories") + + self.DesktopEntry.filename = os.path.join(dir, self.Filename) + + def __cmp__(self, other): + return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) + + def __eq__(self, other): + if self.DesktopFileID == str(other): + return True + else: + return False + + def __repr__(self): + return self.DesktopFileID + + +class Separator: + "Just a dummy class for Separators" + def __init__(self, parent): + self.Parent = parent + self.Show = True + + +class Header: + "Class for Inline Headers" + def __init__(self, name, generic_name, comment): + self.Name = name + self.GenericName = generic_name + self.Comment = comment + + def __str__(self): + return self.Name + + +tmp = {} + +def __getFileName(filename): + dirs = xdg_config_dirs[:] + if xdg.Config.root_mode == True: + dirs.pop(0) + + for dir in dirs: + menuname = os.path.join (dir, "menus" , filename) + if os.path.isdir(dir) and os.path.isfile(menuname): + return menuname + +def parse(filename=None): + # conver to absolute path + if filename and not os.path.isabs(filename): + filename = __getFileName(filename) + + # use default if no filename given + if not filename: + candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" + filename = __getFileName(candidate) + + if not filename: + raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) + + # check if it is a .menu file + if not os.path.splitext(filename)[1] == ".menu": + raise ParsingError('Not a .menu file', filename) + + # create xml parser + try: + doc = xml.dom.minidom.parse(filename) + except xml.parsers.expat.ExpatError: + raise ParsingError('Not a valid .menu file', filename) + + # parse menufile + tmp["Root"] = "" + tmp["mergeFiles"] = [] + tmp["DirectoryDirs"] = [] + tmp["cache"] = MenuEntryCache() + + __parse(doc, filename, tmp["Root"]) + __parsemove(tmp["Root"]) + __postparse(tmp["Root"]) + + tmp["Root"].Doc = doc + tmp["Root"].Filename = filename + + # generate the menu + __genmenuNotOnlyAllocated(tmp["Root"]) + __genmenuOnlyAllocated(tmp["Root"]) + + # and finally sort + sort(tmp["Root"]) + + return tmp["Root"] + + +def __parse(node, filename, parent=None): + for child in node.childNodes: + if child.nodeType == ELEMENT_NODE: + if child.tagName == 'Menu': + __parseMenu(child, filename, parent) + elif child.tagName == 'AppDir': + try: + __parseAppDir(child.childNodes[0].nodeValue, filename, parent) + except IndexError: + raise ValidationError('AppDir cannot be empty', filename) + elif child.tagName == 'DefaultAppDirs': + __parseDefaultAppDir(filename, parent) + elif child.tagName == 'DirectoryDir': + try: + __parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent) + except IndexError: + raise ValidationError('DirectoryDir cannot be empty', filename) + elif child.tagName == 'DefaultDirectoryDirs': + __parseDefaultDirectoryDir(filename, parent) + elif child.tagName == 'Name' : + try: + parent.Name = child.childNodes[0].nodeValue + except IndexError: + raise ValidationError('Name cannot be empty', filename) + elif child.tagName == 'Directory' : + try: + parent.Directories.append(child.childNodes[0].nodeValue) + except IndexError: + raise ValidationError('Directory cannot be empty', filename) + elif child.tagName == 'OnlyUnallocated': + parent.OnlyUnallocated = True + elif child.tagName == 'NotOnlyUnallocated': + parent.OnlyUnallocated = False + elif child.tagName == 'Deleted': + parent.Deleted = True + elif child.tagName == 'NotDeleted': + parent.Deleted = False + elif child.tagName == 'Include' or child.tagName == 'Exclude': + parent.Rules.append(Rule(child.tagName, child)) + elif child.tagName == 'MergeFile': + try: + if child.getAttribute("type") == "parent": + __parseMergeFile("applications.menu", child, filename, parent) + else: + __parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent) + except IndexError: + raise ValidationError('MergeFile cannot be empty', filename) + elif child.tagName == 'MergeDir': + try: + __parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent) + except IndexError: + raise ValidationError('MergeDir cannot be empty', filename) + elif child.tagName == 'DefaultMergeDirs': + __parseDefaultMergeDirs(child, filename, parent) + elif child.tagName == 'Move': + parent.Moves.append(Move(child)) + elif child.tagName == 'Layout': + if len(child.childNodes) > 1: + parent.Layout = Layout(child) + elif child.tagName == 'DefaultLayout': + if len(child.childNodes) > 1: + parent.DefaultLayout = Layout(child) + elif child.tagName == 'LegacyDir': + try: + __parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent) + except IndexError: + raise ValidationError('LegacyDir cannot be empty', filename) + elif child.tagName == 'KDELegacyDirs': + __parseKDELegacyDirs(filename, parent) + +def __parsemove(menu): + for submenu in menu.Submenus: + __parsemove(submenu) + + # parse move operations + for move in menu.Moves: + move_from_menu = menu.getMenu(move.Old) + if move_from_menu: + move_to_menu = menu.getMenu(move.New) + + menus = move.New.split("/") + oldparent = None + while len(menus) > 0: + if not oldparent: + oldparent = menu + newmenu = oldparent.getMenu(menus[0]) + if not newmenu: + newmenu = Menu() + newmenu.Name = menus[0] + if len(menus) > 1: + newmenu.NotInXml = True + oldparent.addSubmenu(newmenu) + oldparent = newmenu + menus.pop(0) + + newmenu += move_from_menu + move_from_menu.Parent.Submenus.remove(move_from_menu) + +def __postparse(menu): + # unallocated / deleted + if menu.Deleted == "notset": + menu.Deleted = False + if menu.OnlyUnallocated == "notset": + menu.OnlyUnallocated = False + + # Layout Tags + if not menu.Layout or not menu.DefaultLayout: + if menu.DefaultLayout: + menu.Layout = menu.DefaultLayout + elif menu.Layout: + if menu.Depth > 0: + menu.DefaultLayout = menu.Parent.DefaultLayout + else: + menu.DefaultLayout = Layout() + else: + if menu.Depth > 0: + menu.Layout = menu.Parent.DefaultLayout + menu.DefaultLayout = menu.Parent.DefaultLayout + else: + menu.Layout = Layout() + menu.DefaultLayout = Layout() + + # add parent's app/directory dirs + if menu.Depth > 0: + menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs + menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs + + # remove duplicates + menu.Directories = __removeDuplicates(menu.Directories) + menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs) + menu.AppDirs = __removeDuplicates(menu.AppDirs) + + # go recursive through all menus + for submenu in menu.Submenus: + __postparse(submenu) + + # reverse so handling is easier + menu.Directories.reverse() + menu.DirectoryDirs.reverse() + menu.AppDirs.reverse() + + # get the valid .directory file out of the list + for directory in menu.Directories: + for dir in menu.DirectoryDirs: + if os.path.isfile(os.path.join(dir, directory)): + menuentry = MenuEntry(directory, dir) + if not menu.Directory: + menu.Directory = menuentry + elif menuentry.getType() == "System": + if menu.Directory.getType() == "User": + menu.Directory.Original = menuentry + if menu.Directory: + break + + +# Menu parsing stuff +def __parseMenu(child, filename, parent): + m = Menu() + __parse(child, filename, m) + if parent: + parent.addSubmenu(m) + else: + tmp["Root"] = m + +# helper function +def __check(value, filename, type): + path = os.path.dirname(filename) + + if not os.path.isabs(value): + value = os.path.join(path, value) + + value = os.path.abspath(value) + + if type == "dir" and os.path.exists(value) and os.path.isdir(value): + return value + elif type == "file" and os.path.exists(value) and os.path.isfile(value): + return value + else: + return False + +# App/Directory Dir Stuff +def __parseAppDir(value, filename, parent): + value = __check(value, filename, "dir") + if value: + parent.AppDirs.append(value) + +def __parseDefaultAppDir(filename, parent): + for dir in reversed(xdg_data_dirs): + __parseAppDir(os.path.join(dir, "applications"), filename, parent) + +def __parseDirectoryDir(value, filename, parent): + value = __check(value, filename, "dir") + if value: + parent.DirectoryDirs.append(value) + +def __parseDefaultDirectoryDir(filename, parent): + for dir in reversed(xdg_data_dirs): + __parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent) + +# Merge Stuff +def __parseMergeFile(value, child, filename, parent): + if child.getAttribute("type") == "parent": + for dir in xdg_config_dirs: + rel_file = filename.replace(dir, "").strip("/") + if rel_file != filename: + for p in xdg_config_dirs: + if dir == p: + continue + if os.path.isfile(os.path.join(p,rel_file)): + __mergeFile(os.path.join(p,rel_file),child,parent) + break + else: + value = __check(value, filename, "file") + if value: + __mergeFile(value, child, parent) + +def __parseMergeDir(value, child, filename, parent): + value = __check(value, filename, "dir") + if value: + for item in os.listdir(value): + try: + if os.path.splitext(item)[1] == ".menu": + __mergeFile(os.path.join(value, item), child, parent) + except UnicodeDecodeError: + continue + +def __parseDefaultMergeDirs(child, filename, parent): + basename = os.path.splitext(os.path.basename(filename))[0] + for dir in reversed(xdg_config_dirs): + __parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent) + +def __mergeFile(filename, child, parent): + # check for infinite loops + if filename in tmp["mergeFiles"]: + if debug: + raise ParsingError('Infinite MergeFile loop detected', filename) + else: + return + + tmp["mergeFiles"].append(filename) + + # load file + try: + doc = xml.dom.minidom.parse(filename) + except IOError: + if debug: + raise ParsingError('File not found', filename) + else: + return + except xml.parsers.expat.ExpatError: + if debug: + raise ParsingError('Not a valid .menu file', filename) + else: + return + + # append file + for child in doc.childNodes: + if child.nodeType == ELEMENT_NODE: + __parse(child,filename,parent) + break + +# Legacy Dir Stuff +def __parseLegacyDir(dir, prefix, filename, parent): + m = __mergeLegacyDir(dir,prefix,filename,parent) + if m: + parent += m + +def __mergeLegacyDir(dir, prefix, filename, parent): + dir = __check(dir,filename,"dir") + if dir and dir not in tmp["DirectoryDirs"]: + tmp["DirectoryDirs"].append(dir) + + m = Menu() + m.AppDirs.append(dir) + m.DirectoryDirs.append(dir) + m.Name = os.path.basename(dir) + m.NotInXml = True + + for item in os.listdir(dir): + try: + if item == ".directory": + m.Directories.append(item) + elif os.path.isdir(os.path.join(dir,item)): + m.addSubmenu(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent)) + except UnicodeDecodeError: + continue + + tmp["cache"].addMenuEntries([dir],prefix, True) + menuentries = tmp["cache"].getMenuEntries([dir], False) + + for menuentry in menuentries: + categories = menuentry.Categories + if len(categories) == 0: + r = Rule("Include") + r.parseFilename(menuentry.DesktopFileID) + r.compile() + m.Rules.append(r) + if not dir in parent.AppDirs: + categories.append("Legacy") + menuentry.Categories = categories + + return m + +def __parseKDELegacyDirs(filename, parent): + f=os.popen3("kde-config --path apps") + output = f[1].readlines() + try: + for dir in output[0].split(":"): + __parseLegacyDir(dir,"kde", filename, parent) + except IndexError: + pass + +# remove duplicate entries from a list +def __removeDuplicates(list): + set = {} + list.reverse() + list = [set.setdefault(e,e) for e in list if e not in set] + list.reverse() + return list + +# Finally generate the menu +def __genmenuNotOnlyAllocated(menu): + for submenu in menu.Submenus: + __genmenuNotOnlyAllocated(submenu) + + if menu.OnlyUnallocated == False: + tmp["cache"].addMenuEntries(menu.AppDirs) + menuentries = [] + for rule in menu.Rules: + menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 1) + for menuentry in menuentries: + if menuentry.Add == True: + menuentry.Parents.append(menu) + menuentry.Add = False + menuentry.Allocated = True + menu.MenuEntries.append(menuentry) + +def __genmenuOnlyAllocated(menu): + for submenu in menu.Submenus: + __genmenuOnlyAllocated(submenu) + + if menu.OnlyUnallocated == True: + tmp["cache"].addMenuEntries(menu.AppDirs) + menuentries = [] + for rule in menu.Rules: + menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 2) + for menuentry in menuentries: + if menuentry.Add == True: + menuentry.Parents.append(menu) + # menuentry.Add = False + # menuentry.Allocated = True + menu.MenuEntries.append(menuentry) + +# And sorting ... +def sort(menu): + menu.Entries = [] + menu.Visible = 0 + + for submenu in menu.Submenus: + sort(submenu) + + tmp_s = [] + tmp_e = [] + + for order in menu.Layout.order: + if order[0] == "Filename": + tmp_e.append(order[1]) + elif order[0] == "Menuname": + tmp_s.append(order[1]) + + for order in menu.Layout.order: + if order[0] == "Separator": + separator = Separator(menu) + if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator): + separator.Show = False + menu.Entries.append(separator) + elif order[0] == "Filename": + menuentry = menu.getMenuEntry(order[1]) + if menuentry: + menu.Entries.append(menuentry) + elif order[0] == "Menuname": + submenu = menu.getMenu(order[1]) + if submenu: + __parse_inline(submenu, menu) + elif order[0] == "Merge": + if order[1] == "files" or order[1] == "all": + menu.MenuEntries.sort() + for menuentry in menu.MenuEntries: + if menuentry not in tmp_e: + menu.Entries.append(menuentry) + elif order[1] == "menus" or order[1] == "all": + menu.Submenus.sort() + for submenu in menu.Submenus: + if submenu.Name not in tmp_s: + __parse_inline(submenu, menu) + + # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec + for entry in menu.Entries: + entry.Show = True + menu.Visible += 1 + if isinstance(entry, Menu): + if entry.Deleted == True: + entry.Show = "Deleted" + menu.Visible -= 1 + elif isinstance(entry.Directory, MenuEntry): + if entry.Directory.DesktopEntry.getNoDisplay() == True: + entry.Show = "NoDisplay" + menu.Visible -= 1 + elif entry.Directory.DesktopEntry.getHidden() == True: + entry.Show = "Hidden" + menu.Visible -= 1 + elif isinstance(entry, MenuEntry): + if entry.DesktopEntry.getNoDisplay() == True: + entry.Show = "NoDisplay" + menu.Visible -= 1 + elif entry.DesktopEntry.getHidden() == True: + entry.Show = "Hidden" + menu.Visible -= 1 + elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()): + entry.Show = "NoExec" + menu.Visible -= 1 + elif xdg.Config.windowmanager: + if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \ + or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn(): + entry.Show = "NotShowIn" + menu.Visible -= 1 + elif isinstance(entry,Separator): + menu.Visible -= 1 + + # remove separators at the beginning and at the end + if len(menu.Entries) > 0: + if isinstance(menu.Entries[0], Separator): + menu.Entries[0].Show = False + if len(menu.Entries) > 1: + if isinstance(menu.Entries[-1], Separator): + menu.Entries[-1].Show = False + + # show_empty tag + for entry in menu.Entries: + if isinstance(entry,Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0: + entry.Show = "Empty" + menu.Visible -= 1 + if entry.NotInXml == True: + menu.Entries.remove(entry) + +def __try_exec(executable): + paths = os.environ['PATH'].split(os.pathsep) + if not os.path.isfile(executable): + for p in paths: + f = os.path.join(p, executable) + if os.path.isfile(f): + if os.access(f, os.X_OK): + return True + else: + if os.access(executable, os.X_OK): + return True + return False + +# inline tags +def __parse_inline(submenu, menu): + if submenu.Layout.inline == "true": + if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true": + menuentry = submenu.Entries[0] + menuentry.DesktopEntry.set("Name", submenu.getName(), locale = True) + menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale = True) + menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale = True) + menu.Entries.append(menuentry) + elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: + if submenu.Layout.inline_header == "true": + header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) + menu.Entries.append(header) + for entry in submenu.Entries: + menu.Entries.append(entry) + else: + menu.Entries.append(submenu) + else: + menu.Entries.append(submenu) + +class MenuEntryCache: + "Class to cache Desktop Entries" + def __init__(self): + self.cacheEntries = {} + self.cacheEntries['legacy'] = [] + self.cache = {} + + def addMenuEntries(self, dirs, prefix="", legacy=False): + for dir in dirs: + if not self.cacheEntries.has_key(dir): + self.cacheEntries[dir] = [] + self.__addFiles(dir, "", prefix, legacy) + + def __addFiles(self, dir, subdir, prefix, legacy): + for item in os.listdir(os.path.join(dir,subdir)): + if os.path.splitext(item)[1] == ".desktop": + try: + menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix) + except ParsingError: + continue + + self.cacheEntries[dir].append(menuentry) + if legacy == True: + self.cacheEntries['legacy'].append(menuentry) + elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False: + self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy) + + def getMenuEntries(self, dirs, legacy=True): + list = [] + ids = [] + # handle legacy items + appdirs = dirs[:] + if legacy == True: + appdirs.append("legacy") + # cache the results again + key = "".join(appdirs) + try: + return self.cache[key] + except KeyError: + pass + for dir in appdirs: + for menuentry in self.cacheEntries[dir]: + try: + if menuentry.DesktopFileID not in ids: + ids.append(menuentry.DesktopFileID) + list.append(menuentry) + elif menuentry.getType() == "System": + # FIXME: This is only 99% correct, but still... + i = list.index(menuentry) + e = list[i] + if e.getType() == "User": + e.Original = menuentry + except UnicodeDecodeError: + continue + self.cache[key] = list + return list diff --git a/src/xdg/MenuEditor.py b/src/xdg/MenuEditor.py new file mode 100644 index 0000000..cc5ce54 --- /dev/null +++ b/src/xdg/MenuEditor.py @@ -0,0 +1,511 @@ +""" CLass to edit XDG Menus """ + +from xdg.Menu import * +from xdg.BaseDirectory import * +from xdg.Exceptions import * +from xdg.DesktopEntry import * +from xdg.Config import * + +import xml.dom.minidom +import os +import re + +# XML-Cleanups: Move / Exclude +# FIXME: proper reverte/delete +# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions +# FIXME: catch Exceptions +# FIXME: copy functions +# FIXME: More Layout stuff +# FIXME: unod/redo function / remove menu... +# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile +# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs + +class MenuEditor: + def __init__(self, menu=None, filename=None, root=False): + self.menu = None + self.filename = None + self.doc = None + self.parse(menu, filename, root) + + # fix for creating two menus with the same name on the fly + self.filenames = [] + + def parse(self, menu=None, filename=None, root=False): + if root == True: + setRootMode(True) + + if isinstance(menu, Menu): + self.menu = menu + elif menu: + self.menu = parse(menu) + else: + self.menu = parse() + + if root == True: + self.filename = self.menu.Filename + elif filename: + self.filename = filename + else: + self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1]) + + try: + self.doc = xml.dom.minidom.parse(self.filename) + except IOError: + self.doc = xml.dom.minidom.parseString('Applications'+self.menu.Filename+'') + except xml.parsers.expat.ExpatError: + raise ParsingError('Not a valid .menu file', self.filename) + + self.__remove_whilespace_nodes(self.doc) + + def save(self): + self.__saveEntries(self.menu) + self.__saveMenu() + + def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None): + menuentry = MenuEntry(self.__getFileName(name, ".desktop")) + menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal) + + self.__addEntry(parent, menuentry, after, before) + + sort(self.menu) + + return menuentry + + def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None): + menu = Menu() + + menu.Parent = parent + menu.Depth = parent.Depth + 1 + menu.Layout = parent.DefaultLayout + menu.DefaultLayout = parent.DefaultLayout + + menu = self.editMenu(menu, name, genericname, comment, icon) + + self.__addEntry(parent, menu, after, before) + + sort(self.menu) + + return menu + + def createSeparator(self, parent, after=None, before=None): + separator = Separator(parent) + + self.__addEntry(parent, separator, after, before) + + sort(self.menu) + + return separator + + def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): + self.__deleteEntry(oldparent, menuentry, after, before) + self.__addEntry(newparent, menuentry, after, before) + + sort(self.menu) + + return menuentry + + def moveMenu(self, menu, oldparent, newparent, after=None, before=None): + self.__deleteEntry(oldparent, menu, after, before) + self.__addEntry(newparent, menu, after, before) + + root_menu = self.__getXmlMenu(self.menu.Name) + if oldparent.getPath(True) != newparent.getPath(True): + self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name)) + + sort(self.menu) + + return menu + + def moveSeparator(self, separator, parent, after=None, before=None): + self.__deleteEntry(parent, separator, after, before) + self.__addEntry(parent, separator, after, before) + + sort(self.menu) + + return separator + + def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): + self.__addEntry(newparent, menuentry, after, before) + + sort(self.menu) + + return menuentry + + def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None): + deskentry = menuentry.DesktopEntry + + if name: + if not deskentry.hasKey("Name"): + deskentry.set("Name", name) + deskentry.set("Name", name, locale = True) + if comment: + if not deskentry.hasKey("Comment"): + deskentry.set("Comment", comment) + deskentry.set("Comment", comment, locale = True) + if genericname: + if not deskentry.hasKey("GnericNe"): + deskentry.set("GenericName", genericname) + deskentry.set("GenericName", genericname, locale = True) + if command: + deskentry.set("Exec", command) + if icon: + deskentry.set("Icon", icon) + + if terminal == True: + deskentry.set("Terminal", "true") + elif terminal == False: + deskentry.set("Terminal", "false") + + if nodisplay == True: + deskentry.set("NoDisplay", "true") + elif nodisplay == False: + deskentry.set("NoDisplay", "false") + + if hidden == True: + deskentry.set("Hidden", "true") + elif hidden == False: + deskentry.set("Hidden", "false") + + menuentry.updateAttributes() + + if len(menuentry.Parents) > 0: + sort(self.menu) + + return menuentry + + def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None): + # Hack for legacy dirs + if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory": + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory") + menu.Directory.setAttributes(menu.Name + ".directory") + # Hack for New Entries + elif not isinstance(menu.Directory, MenuEntry): + if not name: + name = menu.Name + filename = self.__getFileName(name, ".directory").replace("/", "") + if not menu.Name: + menu.Name = filename.replace(".directory", "") + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + self.__addXmlTextElement(xml_menu, 'Directory', filename) + menu.Directory = MenuEntry(filename) + + deskentry = menu.Directory.DesktopEntry + + if name: + if not deskentry.hasKey("Name"): + deskentry.set("Name", name) + deskentry.set("Name", name, locale = True) + if genericname: + if not deskentry.hasKey("GenericName"): + deskentry.set("GenericName", genericname) + deskentry.set("GenericName", genericname, locale = True) + if comment: + if not deskentry.hasKey("Comment"): + deskentry.set("Comment", comment) + deskentry.set("Comment", comment, locale = True) + if icon: + deskentry.set("Icon", icon) + + if nodisplay == True: + deskentry.set("NoDisplay", "true") + elif nodisplay == False: + deskentry.set("NoDisplay", "false") + + if hidden == True: + deskentry.set("Hidden", "true") + elif hidden == False: + deskentry.set("Hidden", "false") + + menu.Directory.updateAttributes() + + if isinstance(menu.Parent, Menu): + sort(self.menu) + + return menu + + def hideMenuEntry(self, menuentry): + self.editMenuEntry(menuentry, nodisplay = True) + + def unhideMenuEntry(self, menuentry): + self.editMenuEntry(menuentry, nodisplay = False, hidden = False) + + def hideMenu(self, menu): + self.editMenu(menu, nodisplay = True) + + def unhideMenu(self, menu): + self.editMenu(menu, nodisplay = False, hidden = False) + xml_menu = self.__getXmlMenu(menu.getPath(True,True), False) + for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu): + node.parentNode.removeChild(node) + + def deleteMenuEntry(self, menuentry): + if self.getAction(menuentry) == "delete": + self.__deleteFile(menuentry.DesktopEntry.filename) + for parent in menuentry.Parents: + self.__deleteEntry(parent, menuentry) + sort(self.menu) + return menuentry + + def revertMenuEntry(self, menuentry): + if self.getAction(menuentry) == "revert": + self.__deleteFile(menuentry.DesktopEntry.filename) + menuentry.Original.Parents = [] + for parent in menuentry.Parents: + index = parent.Entries.index(menuentry) + parent.Entries[index] = menuentry.Original + index = parent.MenuEntries.index(menuentry) + parent.MenuEntries[index] = menuentry.Original + menuentry.Original.Parents.append(parent) + sort(self.menu) + return menuentry + + def deleteMenu(self, menu): + if self.getAction(menu) == "delete": + self.__deleteFile(menu.Directory.DesktopEntry.filename) + self.__deleteEntry(menu.Parent, menu) + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + xml_menu.parentNode.removeChild(xml_menu) + sort(self.menu) + return menu + + def revertMenu(self, menu): + if self.getAction(menu) == "revert": + self.__deleteFile(menu.Directory.DesktopEntry.filename) + menu.Directory = menu.Directory.Original + sort(self.menu) + return menu + + def deleteSeparator(self, separator): + self.__deleteEntry(separator.Parent, separator, after=True) + + sort(self.menu) + + return separator + + """ Private Stuff """ + def getAction(self, entry): + if isinstance(entry, Menu): + if not isinstance(entry.Directory, MenuEntry): + return "none" + elif entry.Directory.getType() == "Both": + return "revert" + elif entry.Directory.getType() == "User" \ + and (len(entry.Submenus) + len(entry.MenuEntries)) == 0: + return "delete" + + elif isinstance(entry, MenuEntry): + if entry.getType() == "Both": + return "revert" + elif entry.getType() == "User": + return "delete" + else: + return "none" + + return "none" + + def __saveEntries(self, menu): + if not menu: + menu = self.menu + if isinstance(menu.Directory, MenuEntry): + menu.Directory.save() + for entry in menu.getEntries(hidden=True): + if isinstance(entry, MenuEntry): + entry.save() + elif isinstance(entry, Menu): + self.__saveEntries(entry) + + def __saveMenu(self): + if not os.path.isdir(os.path.dirname(self.filename)): + os.makedirs(os.path.dirname(self.filename)) + fd = open(self.filename, 'w') + fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*\n', ''))) + fd.close() + + def __getFileName(self, name, extension): + postfix = 0 + while 1: + if postfix == 0: + filename = name + extension + else: + filename = name + "-" + str(postfix) + extension + if extension == ".desktop": + dir = "applications" + elif extension == ".directory": + dir = "desktop-directories" + if not filename in self.filenames and not \ + os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)): + self.filenames.append(filename) + break + else: + postfix += 1 + + return filename + + def __getXmlMenu(self, path, create=True, element=None): + if not element: + element = self.doc + + if "/" in path: + (name, path) = path.split("/", 1) + else: + name = path + path = "" + + found = None + for node in self.__getXmlNodesByName("Menu", element): + for child in self.__getXmlNodesByName("Name", node): + if child.childNodes[0].nodeValue == name: + if path: + found = self.__getXmlMenu(path, create, node) + else: + found = node + break + if found: + break + if not found and create == True: + node = self.__addXmlMenuElement(element, name) + if path: + found = self.__getXmlMenu(path, create, node) + else: + found = node + + return found + + def __addXmlMenuElement(self, element, name): + node = self.doc.createElement('Menu') + self.__addXmlTextElement(node, 'Name', name) + return element.appendChild(node) + + def __addXmlTextElement(self, element, name, text): + node = self.doc.createElement(name) + text = self.doc.createTextNode(text) + node.appendChild(text) + return element.appendChild(node) + + def __addXmlFilename(self, element, filename, type = "Include"): + # remove old filenames + for node in self.__getXmlNodesByName(["Include", "Exclude"], element): + if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename: + element.removeChild(node) + + # add new filename + node = self.doc.createElement(type) + node.appendChild(self.__addXmlTextElement(node, 'Filename', filename)) + return element.appendChild(node) + + def __addXmlMove(self, element, old, new): + node = self.doc.createElement("Move") + node.appendChild(self.__addXmlTextElement(node, 'Old', old)) + node.appendChild(self.__addXmlTextElement(node, 'New', new)) + return element.appendChild(node) + + def __addXmlLayout(self, element, layout): + # remove old layout + for node in self.__getXmlNodesByName("Layout", element): + element.removeChild(node) + + # add new layout + node = self.doc.createElement("Layout") + for order in layout.order: + if order[0] == "Separator": + child = self.doc.createElement("Separator") + node.appendChild(child) + elif order[0] == "Filename": + child = self.__addXmlTextElement(node, "Filename", order[1]) + elif order[0] == "Menuname": + child = self.__addXmlTextElement(node, "Menuname", order[1]) + elif order[0] == "Merge": + child = self.doc.createElement("Merge") + child.setAttribute("type", order[1]) + node.appendChild(child) + return element.appendChild(node) + + def __getXmlNodesByName(self, name, element): + for child in element.childNodes: + if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name: + yield child + + def __addLayout(self, parent): + layout = Layout() + layout.order = [] + layout.show_empty = parent.Layout.show_empty + layout.inline = parent.Layout.inline + layout.inline_header = parent.Layout.inline_header + layout.inline_alias = parent.Layout.inline_alias + layout.inline_limit = parent.Layout.inline_limit + + layout.order.append(["Merge", "menus"]) + for entry in parent.Entries: + if isinstance(entry, Menu): + layout.parseMenuname(entry.Name) + elif isinstance(entry, MenuEntry): + layout.parseFilename(entry.DesktopFileID) + elif isinstance(entry, Separator): + layout.parseSeparator() + layout.order.append(["Merge", "files"]) + + parent.Layout = layout + + return layout + + def __addEntry(self, parent, entry, after=None, before=None): + if after or before: + if after: + index = parent.Entries.index(after) + 1 + elif before: + index = parent.Entries.index(before) + parent.Entries.insert(index, entry) + else: + parent.Entries.append(entry) + + xml_parent = self.__getXmlMenu(parent.getPath(True, True)) + + if isinstance(entry, MenuEntry): + parent.MenuEntries.append(entry) + entry.Parents.append(parent) + self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include") + elif isinstance(entry, Menu): + parent.addSubmenu(entry) + + if after or before: + self.__addLayout(parent) + self.__addXmlLayout(xml_parent, parent.Layout) + + def __deleteEntry(self, parent, entry, after=None, before=None): + parent.Entries.remove(entry) + + xml_parent = self.__getXmlMenu(parent.getPath(True, True)) + + if isinstance(entry, MenuEntry): + entry.Parents.remove(parent) + parent.MenuEntries.remove(entry) + self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude") + elif isinstance(entry, Menu): + parent.Submenus.remove(entry) + + if after or before: + self.__addLayout(parent) + self.__addXmlLayout(xml_parent, parent.Layout) + + def __deleteFile(self, filename): + try: + os.remove(filename) + except OSError: + pass + try: + self.filenames.remove(filename) + except ValueError: + pass + + def __remove_whilespace_nodes(self, node): + remove_list = [] + for child in node.childNodes: + if child.nodeType == xml.dom.minidom.Node.TEXT_NODE: + child.data = child.data.strip() + if not child.data.strip(): + remove_list.append(child) + elif child.hasChildNodes(): + self.__remove_whilespace_nodes(child) + for node in remove_list: + node.parentNode.removeChild(node) diff --git a/src/xdg/Mime.py b/src/xdg/Mime.py new file mode 100644 index 0000000..04fe0d2 --- /dev/null +++ b/src/xdg/Mime.py @@ -0,0 +1,474 @@ +""" +This module is based on a rox module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log + +This module provides access to the shared MIME database. + +types is a dictionary of all known MIME types, indexed by the type name, e.g. +types['application/x-python'] + +Applications can install information about MIME types by storing an +XML file as /packages/.xml and running the +update-mime-database command, which is provided by the freedesktop.org +shared mime database package. + +See http://www.freedesktop.org/standards/shared-mime-info-spec/ for +information about the format of these files. + +(based on version 0.13) +""" + +import os +import stat +import fnmatch + +import xdg.BaseDirectory +import xdg.Locale + +from xml.dom import Node, minidom, XML_NAMESPACE + +FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info' + +types = {} # Maps MIME names to type objects + +exts = None # Maps extensions to types +globs = None # List of (glob, type) pairs +literals = None # Maps liternal names to types +magic = None + +def _get_node_data(node): + """Get text of XML node""" + return ''.join([n.nodeValue for n in node.childNodes]).strip() + +def lookup(media, subtype = None): + "Get the MIMEtype object for this type, creating a new one if needed." + if subtype is None and '/' in media: + media, subtype = media.split('/', 1) + if (media, subtype) not in types: + types[(media, subtype)] = MIMEtype(media, subtype) + return types[(media, subtype)] + +class MIMEtype: + """Type holding data about a MIME type""" + def __init__(self, media, subtype): + "Don't use this constructor directly; use mime.lookup() instead." + assert media and '/' not in media + assert subtype and '/' not in subtype + assert (media, subtype) not in types + + self.media = media + self.subtype = subtype + self._comment = None + + def _load(self): + "Loads comment for current language. Use get_comment() instead." + resource = os.path.join('mime', self.media, self.subtype + '.xml') + for path in xdg.BaseDirectory.load_data_paths(resource): + doc = minidom.parse(path) + if doc is None: + continue + for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'): + lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en' + goodness = 1 + (lang in xdg.Locale.langs) + if goodness > self._comment[0]: + self._comment = (goodness, _get_node_data(comment)) + if goodness == 2: return + + # FIXME: add get_icon method + def get_comment(self): + """Returns comment for current language, loading it if needed.""" + # Should we ever reload? + if self._comment is None: + self._comment = (0, str(self)) + self._load() + return self._comment[1] + + def __str__(self): + return self.media + '/' + self.subtype + + def __repr__(self): + return '[%s: %s]' % (self, self._comment or '(comment not loaded)') + +class MagicRule: + def __init__(self, f): + self.next=None + self.prev=None + + #print line + ind='' + while True: + c=f.read(1) + if c=='>': + break + ind+=c + if not ind: + self.nest=0 + else: + self.nest=int(ind) + + start='' + while True: + c=f.read(1) + if c=='=': + break + start+=c + self.start=int(start) + + hb=f.read(1) + lb=f.read(1) + self.lenvalue=ord(lb)+(ord(hb)<<8) + + self.value=f.read(self.lenvalue) + + c=f.read(1) + if c=='&': + self.mask=f.read(self.lenvalue) + c=f.read(1) + else: + self.mask=None + + if c=='~': + w='' + while c!='+' and c!='\n': + c=f.read(1) + if c=='+' or c=='\n': + break + w+=c + + self.word=int(w) + else: + self.word=1 + + if c=='+': + r='' + while c!='\n': + c=f.read(1) + if c=='\n': + break + r+=c + #print r + self.range=int(r) + else: + self.range=1 + + if c!='\n': + raise 'Malformed MIME magic line' + + def getLength(self): + return self.start+self.lenvalue+self.range + + def appendRule(self, rule): + if self.nest%d=[%d]%s&%s~%d+%d>' % (self.nest, + self.start, + self.lenvalue, + `self.value`, + `self.mask`, + self.word, + self.range) + +class MagicType: + def __init__(self, mtype): + self.mtype=mtype + self.top_rules=[] + self.last_rule=None + + def getLine(self, f): + nrule=MagicRule(f) + + if nrule.nest and self.last_rule: + self.last_rule.appendRule(nrule) + else: + self.top_rules.append(nrule) + + self.last_rule=nrule + + return nrule + + def match(self, buffer): + for rule in self.top_rules: + if rule.match(buffer): + return self.mtype + + def __repr__(self): + return '' % self.mtype + +class MagicDB: + def __init__(self): + self.types={} # Indexed by priority, each entry is a list of type rules + self.maxlen=0 + + def mergeFile(self, fname): + f=file(fname, 'r') + line=f.readline() + if line!='MIME-Magic\0\n': + raise 'Not a MIME magic file' + + while True: + shead=f.readline() + #print shead + if not shead: + break + if shead[0]!='[' or shead[-2:]!=']\n': + raise 'Malformed section heading' + pri, tname=shead[1:-2].split(':') + #print shead[1:-2] + pri=int(pri) + mtype=lookup(tname) + + try: + ents=self.types[pri] + except: + ents=[] + self.types[pri]=ents + + magictype=MagicType(mtype) + #print tname + + #rline=f.readline() + c=f.read(1) + f.seek(-1, 1) + while c and c!='[': + rule=magictype.getLine(f) + #print rule + if rule and rule.getLength()>self.maxlen: + self.maxlen=rule.getLength() + + c=f.read(1) + f.seek(-1, 1) + + ents.append(magictype) + #self.types[pri]=ents + if not c: + break + + def match_data(self, data, max_pri=100, min_pri=0): + pris=self.types.keys() + pris.sort(lambda a, b: -cmp(a, b)) + for pri in pris: + #print pri, max_pri, min_pri + if pri>max_pri: + continue + if pri' % self.types + + +# Some well-known types +text = lookup('text', 'plain') +inode_block = lookup('inode', 'blockdevice') +inode_char = lookup('inode', 'chardevice') +inode_dir = lookup('inode', 'directory') +inode_fifo = lookup('inode', 'fifo') +inode_socket = lookup('inode', 'socket') +inode_symlink = lookup('inode', 'symlink') +inode_door = lookup('inode', 'door') +app_exe = lookup('application', 'executable') + +_cache_uptodate = False + +def _cache_database(): + global exts, globs, literals, magic, _cache_uptodate + + _cache_uptodate = True + + exts = {} # Maps extensions to types + globs = [] # List of (glob, type) pairs + literals = {} # Maps liternal names to types + magic = MagicDB() + + def _import_glob_file(path): + """Loads name matching information from a MIME directory.""" + for line in file(path): + if line.startswith('#'): continue + line = line[:-1] + + type_name, pattern = line.split(':', 1) + mtype = lookup(type_name) + + if pattern.startswith('*.'): + rest = pattern[2:] + if not ('*' in rest or '[' in rest or '?' in rest): + exts[rest] = mtype + continue + if '*' in pattern or '[' in pattern or '?' in pattern: + globs.append((pattern, mtype)) + else: + literals[pattern] = mtype + + for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'globs')): + _import_glob_file(path) + for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'magic')): + magic.mergeFile(path) + + # Sort globs by length + globs.sort(lambda a, b: cmp(len(b[0]), len(a[0]))) + +def get_type_by_name(path): + """Returns type of file by its name, or None if not known""" + if not _cache_uptodate: + _cache_database() + + leaf = os.path.basename(path) + if leaf in literals: + return literals[leaf] + + lleaf = leaf.lower() + if lleaf in literals: + return literals[lleaf] + + ext = leaf + while 1: + p = ext.find('.') + if p < 0: break + ext = ext[p + 1:] + if ext in exts: + return exts[ext] + ext = lleaf + while 1: + p = ext.find('.') + if p < 0: break + ext = ext[p+1:] + if ext in exts: + return exts[ext] + for (glob, mime_type) in globs: + if fnmatch.fnmatch(leaf, glob): + return mime_type + if fnmatch.fnmatch(lleaf, glob): + return mime_type + return None + +def get_type_by_contents(path, max_pri=100, min_pri=0): + """Returns type of file by its contents, or None if not known""" + if not _cache_uptodate: + _cache_database() + + return magic.match(path, max_pri, min_pri) + +def get_type_by_data(data, max_pri=100, min_pri=0): + """Returns type of the data""" + if not _cache_uptodate: + _cache_database() + + return magic.match_data(data, max_pri, min_pri) + +def get_type(path, follow=1, name_pri=100): + """Returns type of file indicated by path. + path - pathname to check (need not exist) + follow - when reading file, follow symbolic links + name_pri - Priority to do name matches. 100=override magic""" + if not _cache_uptodate: + _cache_database() + + try: + if follow: + st = os.stat(path) + else: + st = os.lstat(path) + except: + t = get_type_by_name(path) + return t or text + + if stat.S_ISREG(st.st_mode): + t = get_type_by_contents(path, min_pri=name_pri) + if not t: t = get_type_by_name(path) + if not t: t = get_type_by_contents(path, max_pri=name_pri) + if t is None: + if stat.S_IMODE(st.st_mode) & 0111: + return app_exe + else: + return text + return t + elif stat.S_ISDIR(st.st_mode): return inode_dir + elif stat.S_ISCHR(st.st_mode): return inode_char + elif stat.S_ISBLK(st.st_mode): return inode_block + elif stat.S_ISFIFO(st.st_mode): return inode_fifo + elif stat.S_ISLNK(st.st_mode): return inode_symlink + elif stat.S_ISSOCK(st.st_mode): return inode_socket + return inode_door + +def install_mime_info(application, package_file): + """Copy 'package_file' as ~/.local/share/mime/packages/.xml. + If package_file is None, install /.xml. + If already installed, does nothing. May overwrite an existing + file with the same name (if the contents are different)""" + application += '.xml' + + new_data = file(package_file).read() + + # See if the file is already installed + package_dir = os.path.join('mime', 'packages') + resource = os.path.join(package_dir, application) + for x in xdg.BaseDirectory.load_data_paths(resource): + try: + old_data = file(x).read() + except: + continue + if old_data == new_data: + return # Already installed + + global _cache_uptodate + _cache_uptodate = False + + # Not already installed; add a new copy + # Create the directory structure... + new_file = os.path.join(xdg.BaseDirectory.save_data_path(package_dir), application) + + # Write the file... + file(new_file, 'w').write(new_data) + + # Update the database... + command = 'update-mime-database' + if os.spawnlp(os.P_WAIT, command, command, xdg.BaseDirectory.save_data_path('mime')): + os.unlink(new_file) + raise Exception("The '%s' command returned an error code!\n" \ + "Make sure you have the freedesktop.org shared MIME package:\n" \ + "http://standards.freedesktop.org/shared-mime-info/") % command diff --git a/src/xdg/RecentFiles.py b/src/xdg/RecentFiles.py new file mode 100644 index 0000000..6c2cd85 --- /dev/null +++ b/src/xdg/RecentFiles.py @@ -0,0 +1,159 @@ +""" +Implementation of the XDG Recent File Storage Specification Version 0.2 +http://standards.freedesktop.org/recent-file-spec +""" + +import xml.dom.minidom, xml.sax.saxutils +import os, time, fcntl +from xdg.Exceptions import * + +class RecentFiles: + def __init__(self): + self.RecentFiles = [] + self.filename = "" + + def parse(self, filename=None): + if not filename: + filename = os.path.join(os.getenv("HOME"), ".recently-used") + + try: + doc = xml.dom.minidom.parse(filename) + except IOError: + raise ParsingError('File not found', filename) + except xml.parsers.expat.ExpatError: + raise ParsingError('Not a valid .menu file', filename) + + self.filename = filename + + for child in doc.childNodes: + if child.nodeType == xml.dom.Node.ELEMENT_NODE: + if child.tagName == "RecentFiles": + for recent in child.childNodes: + if recent.nodeType == xml.dom.Node.ELEMENT_NODE: + if recent.tagName == "RecentItem": + self.__parseRecentItem(recent) + + self.sort() + + def __parseRecentItem(self, item): + recent = RecentFile() + self.RecentFiles.append(recent) + + for attribute in item.childNodes: + if attribute.nodeType == xml.dom.Node.ELEMENT_NODE: + if attribute.tagName == "URI": + recent.URI = attribute.childNodes[0].nodeValue + elif attribute.tagName == "Mime-Type": + recent.MimeType = attribute.childNodes[0].nodeValue + elif attribute.tagName == "Timestamp": + recent.Timestamp = attribute.childNodes[0].nodeValue + elif attribute.tagName == "Private": + recent.Prviate = True + elif attribute.tagName == "Groups": + + for group in attribute.childNodes: + if group.nodeType == xml.dom.Node.ELEMENT_NODE: + if group.tagName == "Group": + recent.Groups.append(group.childNodes[0].nodeValue) + + def write(self, filename=None): + if not filename and not self.filename: + raise ParsingError('File not found', filename) + elif not filename: + filename = self.filename + + f = open(filename, "w") + fcntl.lockf(f, fcntl.LOCK_EX) + f.write('\n') + f.write("\n") + + for r in self.RecentFiles: + f.write(" \n") + f.write(" %s\n" % xml.sax.saxutils.escape(r.URI)) + f.write(" %s\n" % r.MimeType) + f.write(" %s\n" % r.Timestamp) + if r.Private == True: + f.write(" \n") + if len(r.Groups) > 0: + f.write(" \n") + for group in r.Groups: + f.write(" %s\n" % group) + f.write(" \n") + f.write(" \n") + + f.write("\n") + fcntl.lockf(f, fcntl.LOCK_UN) + f.close() + + def getFiles(self, mimetypes=None, groups=None, limit=0): + tmp = [] + i = 0 + for item in self.RecentFiles: + if groups: + for group in groups: + if group in item.Groups: + tmp.append(item) + i += 1 + elif mimetypes: + for mimetype in mimetypes: + if mimetype == item.MimeType: + tmp.append(item) + i += 1 + else: + if item.Private == False: + tmp.append(item) + i += 1 + if limit != 0 and i == limit: + break + + return tmp + + def addFile(self, item, mimetype, groups=None, private=False): + # check if entry already there + if item in self.RecentFiles: + index = self.RecentFiles.index(item) + recent = self.RecentFiles[index] + else: + # delete if more then 500 files + if len(self.RecentFiles) == 500: + self.RecentFiles.pop() + # add entry + recent = RecentFile() + self.RecentFiles.append(recent) + + recent.URI = item + recent.MimeType = mimetype + recent.Timestamp = int(time.time()) + recent.Private = private + recent.Groups = groups + + self.sort() + + def deleteFile(self, item): + if item in self.RecentFiles: + self.RecentFiles.remove(item) + + def sort(self): + self.RecentFiles.sort() + self.RecentFiles.reverse() + + +class RecentFile: + def __init__(self): + self.URI = "" + self.MimeType = "" + self.Timestamp = "" + self.Private = False + self.Groups = [] + + def __cmp__(self, other): + return cmp(self.Timestamp, other.Timestamp) + + def __eq__(self, other): + if self.URI == str(other): + return True + else: + return False + + def __str__(self): + return self.URI diff --git a/src/xdg/__init__.py b/src/xdg/__init__.py new file mode 100644 index 0000000..870cd00 --- /dev/null +++ b/src/xdg/__init__.py @@ -0,0 +1 @@ +__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] diff --git a/widget.py b/widget.py deleted file mode 100755 index 34e73c7..0000000 --- a/widget.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -import gtk -import gobject -import hildon -from hildondesktop import * -from gtk import gdk -from math import pi -import cairo -import time - -from portrait import FremantleRotation -import launcher -from xdg.IconTheme import getIconPath -from win_config import WinConfig - -import config -import apps -from icon import Icon -from icongrid import IconGrid - -# IconGrid must be before HomePluginItem for its connect() -# and do_button_*() to override those of HomePluginItem -class DrlaunchPlugin(IconGrid, HomePluginItem, FremantleRotation): - def __init__(self): - IconGrid.__init__(self) - HomePluginItem.__init__(self) - FremantleRotation.__init__(self, 'DrlaunchPlugin') - - launcher.init() - config.load() - self.setSize(config.getSize()) - self.reloadIcons() - - self.set_settings(True) - self.connect('show-settings', self.slot_show_settings) - self.connect('long-press', self.signalLongpress) - - def do_realize(self): - screen=self.get_screen() - self.set_colormap(screen.get_rgba_colormap()) - self.set_app_paintable(True) - - HomePluginItem.do_realize(self) - - def on_orientation_changed(self, orientation): - print "orch:", orientation - o=orientation[0] - self.setMode(o) -# self.queue_draw() - - def do_expose_event(self, event): - IconGrid.do_expose_event(self, event) - HomePluginItem.do_expose_event(self, event) - - def do_buttonn_press_event(self, event): - print "press0" - - def slot_show_settings(self, dt): - print "settings", dt - s=WinConfig() - s.show_all() - s.connect('destroy', self.slotConfigDestroy) - - def slotConfigDestroy(self, sender): - print "destroy", sender - dt=sender.getData() - print "dt:", dt - config.setSize(dt['size']) - config.setApps(dt['apps']) - config.save() - - # Resize widget - self.setSize(dt['size']) - self.reloadIcons() - -# self.queue_draw() - - def signalLongpress(self, sender, icon): - print "launch:", icon.name - launcher.launch(icon.name) - - def resize(self): - w=(self.size * config.iconsize) + \ - (self.size * config.iconspace) - self.set_size_request(w, w) - - def setSize(self, size): - IconGrid.setSize(self, size) - self.resize() - -hd_plugin_type = DrlaunchPlugin - -def do1(): -# gobject.type_register(MyQ) - gobject.type_register(hd_plugin_type) - obj=gobject.new(hd_plugin_type, plugin_id="plugin_id") - obj.show_all() - gtk.main() - -def do2(): - win=DrlaunchPlugin() - win.connect('delete-event', gtk.main_quit) - - print "win:", win - -# t=DrlaunchPlugin() -# win.add(t) - - win.show_all() - gtk.main() - -if __name__=="__main__": - do1() - - - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/win_config.py b/win_config.py deleted file mode 100755 index 47b7436..0000000 --- a/win_config.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python -# coding=UTF-8 -# -# Copyright (C) 2010 Stefanos Harhalakis -# -# This file is part of wifieye. -# -# wifieye 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 3 of the License, or -# (at your option) any later version. -# -# wifieye 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 wifieye. If not, see . -# -# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $ - -__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $" - -import gtk -import gobject -import hildon -import time - -from hildon import StackableWindow -#from portrait import FremantleRotation -#from xdg.IconTheme import getIconPath - -import config -import apps -from icon import Icon, getIcon -from icongrid import IconGridWidget - -class WinConfig(StackableWindow): - def __init__(self, *args): - StackableWindow.__init__(self) - - self.setupUi() - - def setupUi(self): - self.igw=IconGridWidget(True) -# self.igw.setSize(config.getSize()) - - hbox=gtk.HBox() - self.add(hbox) - - # Add the icongrid - hbox.add(self.igw) - - # Now go for the right side - al=gtk.Alignment(yscale=0) - hbox.add(al) - - vbox=gtk.VBox() -# hbox.add(vbox) - al.add(vbox) - - vbox.add(gtk.Label('Grid size:')) - - self.butsSize=[] - for i in xrange(4): - but=gtk.ToggleButton("%sx%s" % (i+1,i+1)) - but.set_size_request(160, 90) - self.butsSize.append(but) - but.connect('toggled', self.slotButtonSize, i) - - hbox2=gtk.HBox() - vbox.add(hbox2) - hbox2.add(self.butsSize[0]) - hbox2.add(self.butsSize[1]) - hbox2=gtk.HBox() - vbox.add(hbox2) - hbox2.add(self.butsSize[2]) - hbox2.add(self.butsSize[3]) - - self.igw.connect('long-press', self.slotLongpress) - - self.ignore_toggle=False - - self.setSize(config.getSize()) - - def slotLongpress(self, sender, icon): - print "slp", icon - self.doConfig(icon) - - def slotButtonSize(self, sender, id): - print "size:", id - - self.setSize(id+1) - - def setSize(self, sz): - if self.ignore_toggle: - return - - self.ignore_toggle=True - - id=sz-1 - - for i in xrange(4): - but=self.butsSize[i] - but.set_active(i==id) - - self.ignore_toggle=False - - self.igw.setSize(sz) - - self.igw.queue_draw() - - - def doConfig(self, icon): - aps=apps.scan() - - lst=[aps[x]['name'] for x in aps] - lst.sort() - - dialog=gtk.Dialog('App select', None, - gtk.DIALOG_DESTROY_WITH_PARENT, buttons=()) - - selector=hildon.TouchSelectorEntry(text=True) - selector.set_column_selection_mode( - hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE) - - dialog.vbox.pack_start(selector, True, True, 0) - dialog.set_size_request(0,900) - dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) - dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - - selector.append_text('None') - - idx=0 - cnt=1 - for app in lst: - if app==None: - continue - selector.append_text(app) - if icon.name!=None and aps[icon.name]['name']==app: - idx=cnt - cnt+=1 - - selector.set_active(0, idx) - - dialog.show_all() - - app=None - - r=dialog.run() - - if r==gtk.RESPONSE_OK: - cur=selector.get_current_text() - if cur=='None': - app=None - else: - for i in aps: - if aps[i]['name']==cur: - app=aps[i] - break - if app!=None: - app['icon2']=getIcon(app['icon']) - else: - app={ - 'id': None, - 'icon2': None, - } - icon.setApp(app) - - dialog.destroy() - - def getData(self): - sz=0 - for but in self.butsSize: - sz+=1 - if but.get_active()==True: - break - - print "conf: sz=", sz - - wapps={} - - for x in xrange(sz): - for y in xrange(sz): - ico=self.igw.get(x,y) - k=(x,y) - wapps[k]=ico.name - - ret={ - 'size': sz, - 'apps': wapps - } - - print "ret:", ret - - return(ret) - -if __name__=="__main__": - win=WinConfig() - win.connect('delete-event', gtk.main_quit) - - win.show_all() - gtk.main() - - - -# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: - diff --git a/xdg/BaseDirectory.py b/xdg/BaseDirectory.py deleted file mode 100644 index 6f532c9..0000000 --- a/xdg/BaseDirectory.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -This module is based on a rox module (LGPL): - -http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log - -The freedesktop.org Base Directory specification provides a way for -applications to locate shared data and configuration: - - http://standards.freedesktop.org/basedir-spec/ - -(based on version 0.6) - -This module can be used to load and save from and to these directories. - -Typical usage: - - from rox import basedir - - for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'): - print "Load settings from", dir - - dir = basedir.save_config_path('mydomain.org', 'MyProg') - print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2" - -Note: see the rox.Options module for a higher-level API for managing options. -""" - -from __future__ import generators -import os - -_home = os.environ.get('HOME', '/') -xdg_data_home = os.environ.get('XDG_DATA_HOME', - os.path.join(_home, '.local', 'share')) - -xdg_data_dirs = [xdg_data_home] + \ - os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') - -xdg_config_home = os.environ.get('XDG_CONFIG_HOME', - os.path.join(_home, '.config')) - -xdg_config_dirs = [xdg_config_home] + \ - os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':') - -xdg_cache_home = os.environ.get('XDG_CACHE_HOME', - os.path.join(_home, '.cache')) - -xdg_data_dirs = filter(lambda x: x, xdg_data_dirs) -xdg_config_dirs = filter(lambda x: x, xdg_config_dirs) - -def save_config_path(*resource): - """Ensure $XDG_CONFIG_HOME// exists, and return its path. - 'resource' should normally be the name of your application. Use this - when SAVING configuration settings. Use the xdg_config_dirs variable - for loading.""" - resource = os.path.join(*resource) - assert not resource.startswith('/') - path = os.path.join(xdg_config_home, resource) - if not os.path.isdir(path): - os.makedirs(path, 0700) - return path - -def save_data_path(*resource): - """Ensure $XDG_DATA_HOME// exists, and return its path. - 'resource' is the name of some shared resource. Use this when updating - a shared (between programs) database. Use the xdg_data_dirs variable - for loading.""" - resource = os.path.join(*resource) - assert not resource.startswith('/') - path = os.path.join(xdg_data_home, resource) - if not os.path.isdir(path): - os.makedirs(path) - return path - -def load_config_paths(*resource): - """Returns an iterator which gives each directory named 'resource' in the - configuration search path. Information provided by earlier directories should - take precedence over later ones (ie, the user's config dir comes first).""" - resource = os.path.join(*resource) - for config_dir in xdg_config_dirs: - path = os.path.join(config_dir, resource) - if os.path.exists(path): yield path - -def load_first_config(*resource): - """Returns the first result from load_config_paths, or None if there is nothing - to load.""" - for x in load_config_paths(*resource): - return x - return None - -def load_data_paths(*resource): - """Returns an iterator which gives each directory named 'resource' in the - shared data search path. Information provided by earlier directories should - take precedence over later ones.""" - resource = os.path.join(*resource) - for data_dir in xdg_data_dirs: - path = os.path.join(data_dir, resource) - if os.path.exists(path): yield path diff --git a/xdg/Config.py b/xdg/Config.py deleted file mode 100644 index e2fbe64..0000000 --- a/xdg/Config.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Functions to configure Basic Settings -""" - -language = "C" -windowmanager = None -icon_theme = "highcolor" -icon_size = 48 -cache_time = 5 -root_mode = False - -def setWindowManager(wm): - global windowmanager - windowmanager = wm - -def setIconTheme(theme): - global icon_theme - icon_theme = theme - import xdg.IconTheme - xdg.IconTheme.themes = [] - -def setIconSize(size): - global icon_size - icon_size = size - -def setCacheTime(time): - global cache_time - cache_time = time - -def setLocale(lang): - import locale - lang = locale.normalize(lang) - locale.setlocale(locale.LC_ALL, lang) - import xdg.Locale - xdg.Locale.update(lang) - -def setRootMode(boolean): - global root_mode - root_mode = boolean diff --git a/xdg/DesktopEntry.py b/xdg/DesktopEntry.py deleted file mode 100644 index 8626d7f..0000000 --- a/xdg/DesktopEntry.py +++ /dev/null @@ -1,397 +0,0 @@ -""" -Complete implementation of the XDG Desktop Entry Specification Version 0.9.4 -http://standards.freedesktop.org/desktop-entry-spec/ - -Not supported: -- Encoding: Legacy Mixed -- Does not check exec parameters -- Does not check URL's -- Does not completly validate deprecated/kde items -- Does not completly check categories -""" - -from xdg.IniFile import * -from xdg.BaseDirectory import * -import os.path - -class DesktopEntry(IniFile): - "Class to parse and validate DesktopEntries" - - defaultGroup = 'Desktop Entry' - - def __init__(self, filename=None): - self.content = dict() - if filename and os.path.exists(filename): - self.parse(filename) - elif filename: - self.new(filename) - - def __str__(self): - return self.getName() - - def parse(self, file): - IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) - - # start standard keys - def getType(self): - return self.get('Type') - """ @deprecated, use getVersionString instead """ - def getVersion(self): - return self.get('Version', type="numeric") - def getVersionString(self): - return self.get('Version') - def getName(self): - return self.get('Name', locale=True) - def getGenericName(self): - return self.get('GenericName', locale=True) - def getNoDisplay(self): - return self.get('NoDisplay', type="boolean") - def getComment(self): - return self.get('Comment', locale=True) - def getIcon(self): - return self.get('Icon', locale=True) - def getHidden(self): - return self.get('Hidden', type="boolean") - def getOnlyShowIn(self): - return self.get('OnlyShowIn', list=True) - def getNotShowIn(self): - return self.get('NotShowIn', list=True) - def getTryExec(self): - return self.get('TryExec') - def getExec(self): - return self.get('Exec') - def getPath(self): - return self.get('Path') - def getTerminal(self): - return self.get('Terminal', type="boolean") - """ @deprecated, use getMimeTypes instead """ - def getMimeType(self): - return self.get('MimeType', list=True, type="regex") - def getMimeTypes(self): - return self.get('MimeType', list=True) - def getCategories(self): - return self.get('Categories', list=True) - def getStartupNotify(self): - return self.get('StartupNotify', type="boolean") - def getStartupWMClass(self): - return self.get('StartupWMClass') - def getURL(self): - return self.get('URL') - # end standard keys - - # start kde keys - def getServiceTypes(self): - return self.get('ServiceTypes', list=True) - def getDocPath(self): - return self.get('DocPath') - def getKeywords(self): - return self.get('Keywords', list=True, locale=True) - def getInitialPreference(self): - return self.get('InitialPreference') - def getDev(self): - return self.get('Dev') - def getFSType(self): - return self.get('FSType') - def getMountPoint(self): - return self.get('MountPoint') - def getReadonly(self): - return self.get('ReadOnly', type="boolean") - def getUnmountIcon(self): - return self.get('UnmountIcon', locale=True) - # end kde keys - - # start deprecated keys - def getMiniIcon(self): - return self.get('MiniIcon', locale=True) - def getTerminalOptions(self): - return self.get('TerminalOptions') - def getDefaultApp(self): - return self.get('DefaultApp') - def getProtocols(self): - return self.get('Protocols', list=True) - def getExtensions(self): - return self.get('Extensions', list=True) - def getBinaryPattern(self): - return self.get('BinaryPattern') - def getMapNotify(self): - return self.get('MapNotify') - def getEncoding(self): - return self.get('Encoding') - def getSwallowTitle(self): - return self.get('SwallowTitle', locale=True) - def getSwallowExec(self): - return self.get('SwallowExec') - def getSortOrder(self): - return self.get('SortOrder', list=True) - def getFilePattern(self): - return self.get('FilePattern', type="regex") - def getActions(self): - return self.get('Actions', list=True) - # end deprecated keys - - # desktop entry edit stuff - def new(self, filename): - if os.path.splitext(filename)[1] == ".desktop": - type = "Application" - elif os.path.splitext(filename)[1] == ".directory": - type = "Directory" - else: - raise ParsingError("Unknown extension", filename) - - self.content = dict() - self.addGroup(self.defaultGroup) - self.set("Type", type) - self.filename = filename - # end desktop entry edit stuff - - # validation stuff - def checkExtras(self): - # header - if self.defaultGroup == "KDE Desktop Entry": - self.warnings.append('[KDE Desktop Entry]-Header is deprecated') - - # file extension - if self.fileExtension == ".kdelnk": - self.warnings.append("File extension .kdelnk is deprecated") - elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": - self.warnings.append('Unknown File extension') - - # Type - try: - self.type = self.content[self.defaultGroup]["Type"] - except KeyError: - self.errors.append("Key 'Type' is missing") - - # Name - try: - self.name = self.content[self.defaultGroup]["Name"] - except KeyError: - self.errors.append("Key 'Name' is missing") - - def checkGroup(self, group): - # check if group header is valid - if not (group == self.defaultGroup \ - or re.match("^\Desktop Action [a-zA-Z]+\$", group) \ - or (re.match("^\X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group)): - self.errors.append("Invalid Group name: %s" % group) - else: - #OnlyShowIn and NotShowIn - if self.content[group].has_key("OnlyShowIn") and self.content[group].has_key("NotShowIn"): - self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") - - def checkKey(self, key, value, group): - # standard keys - if key == "Type": - if value == "ServiceType" or value == "Service" or value == "FSDevice": - self.warnings.append("Type=%s is a KDE extension" % key) - elif value == "MimeType": - self.warnings.append("Type=MimeType is deprecated") - elif not (value == "Application" or value == "Link" or value == "Directory"): - self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value) - - if self.fileExtension == ".directory" and not value == "Directory": - self.warnings.append("File extension is .directory, but Type is '%s'" % value) - elif self.fileExtension == ".desktop" and value == "Directory": - self.warnings.append("Files with Type=Directory should have the extension .directory") - - if value == "Application": - if not self.content[group].has_key("Exec"): - self.warnings.append("Type=Application needs 'Exec' key") - if value == "Link": - if not self.content[group].has_key("URL"): - self.warnings.append("Type=Application needs 'Exec' key") - - elif key == "Version": - self.checkValue(key, value) - - elif re.match("^Name"+xdg.Locale.regex+"$", key): - pass # locale string - - elif re.match("^GenericName"+xdg.Locale.regex+"$", key): - pass # locale string - - elif key == "NoDisplay": - self.checkValue(key, value, type="boolean") - - elif re.match("^Comment"+xdg.Locale.regex+"$", key): - pass # locale string - - elif re.match("^Icon"+xdg.Locale.regex+"$", key): - self.checkValue(key, value) - - elif key == "Hidden": - self.checkValue(key, value, type="boolean") - - elif key == "OnlyShowIn": - self.checkValue(key, value, list=True) - self.checkOnlyShowIn(value) - - elif key == "NotShowIn": - self.checkValue(key, value, list=True) - self.checkOnlyShowIn(value) - - elif key == "TryExec": - self.checkValue(key, value) - self.checkType(key, "Application") - - elif key == "Exec": - self.checkValue(key, value) - self.checkType(key, "Application") - - elif key == "Path": - self.checkValue(key, value) - self.checkType(key, "Application") - - elif key == "Terminal": - self.checkValue(key, value, type="boolean") - self.checkType(key, "Application") - - elif key == "MimeType": - self.checkValue(key, value, list=True) - self.checkType(key, "Application") - - elif key == "Categories": - self.checkValue(key, value) - self.checkType(key, "Application") - self.checkCategorie(value) - - elif key == "StartupNotify": - self.checkValue(key, value, type="boolean") - self.checkType(key, "Application") - - elif key == "StartupWMClass": - self.checkType(key, "Application") - - elif key == "URL": - self.checkValue(key, value) - self.checkType(key, "URL") - - # kde extensions - elif key == "ServiceTypes": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "DocPath": - self.checkValue(key, value) - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif re.match("^Keywords"+xdg.Locale.regex+"$", key): - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "InitialPreference": - self.checkValue(key, value, type="numeric") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "Dev": - self.checkValue(key, value) - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "FSType": - self.checkValue(key, value) - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "MountPoint": - self.checkValue(key, value) - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "ReadOnly": - self.checkValue(key, value, type="boolean") - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): - self.checkValue(key, value) - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - # deprecated keys - elif key == "Encoding": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "TerminalOptions": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "DefaultApp": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "Protocols": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "Extensions": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "BinaryPattern": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "MapNotify": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "SwallowExec": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "FilePattern": - self.checkValue(key, value, type="regex", list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "SortOrder": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "Actions": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - # "X-" extensions - elif re.match("^X-[a-zA-Z0-9-]+", key): - pass - - else: - self.errors.append("Invalid key: %s" % key) - - def checkType(self, key, type): - if not self.getType() == type: - self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) - - def checkOnlyShowIn(self, value): - values = self.getList(value) - valid = ["GNOME", "KDE", "ROX", "XFCE", "Old", "LXDE"] - for item in values: - if item not in valid and item[0:2] != "X-": - self.errors.append("'%s' is not a registered OnlyShowIn value" % item); - - def checkCategorie(self, value): - values = self.getList(value) - - main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Settings", "System", "Utility"] - hasmain = False - for item in values: - if item in main: - hasmain = True - if hasmain == False: - self.errors.append("Missing main category") - - additional = ["Building", "Debugger", "IDE", "GUIDesigner", "Profiling", "RevisionControl", "Translation", "Calendar", "ContactManagement", "Database", "Dictionary", "Chart", "Email", "Finance", "FlowChart", "PDA", "ProjectManagement", "Presentation", "Spreadsheet", "WordProcessor", "2DGraphics", "VectorGraphics", "3DGraphics", "RasterGraphics", "Scanning", "OCR", "Photography", "Publishing", "Viewer", "TextTools", "DesktopSettings", "HardwareSettings", "Printing", "PackageManager", "Dialup", "InstantMessaging", "Chat", "IRCClient", "FileTransfer", "HamRadio", "News", "P2P", "RemoteAccess", "Telephony", "TelephonyTools", "VideoConference", "WebBrowser", "WebDevelopment", "Midi", "Mixer", "Sequencer", "Tuner", "TV", "AudioVideoEditing", "Player", "Recorder", "DiscBurning", "ActionGame", "AdventureGame", "ArcadeGame", "BoardGame", "BlocksGame", "CardGame", "KidsGame", "LogicGame", "RolePlaying", "Simulation", "SportsGame", "StrategyGame", "Art", "Construction", "Music", "Languages", "Science", "ArtificialIntelligence", "Astronomy", "Biology", "Chemistry", "ComputerScience", "DataVisualization", "Economy", "Electricity", "Geography", "Geology", "Geoscience", "History", "ImageProcessing", "Literature", "Math", "NumericalAnalysis", "MedicalSoftware", "Physics", "Robotics", "Sports", "ParallelComputing", "Amusement", "Archiving", "Compression", "Electronics", "Emulator", "Engineering", "FileTools", "FileManager", "TerminalEmulator", "Filesystem", "Monitor", "Security", "Accessibility", "Calculator", "Clock", "TextEditor", "Documentation", "Core", "KDE", "GNOME", "GTK", "Qt", "Motif", "Java", "ConsoleOnly", "Screensaver", "TrayIcon", "Applet", "Shell"] - - for item in values: - if item not in additional + main and item[0:2] != "X-": - self.errors.append("'%s' is not a registered Category" % item); - diff --git a/xdg/Exceptions.py b/xdg/Exceptions.py deleted file mode 100644 index f7d08be..0000000 --- a/xdg/Exceptions.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Exception Classes for the xdg package -""" - -debug = False - -class Error(Exception): - def __init__(self, msg): - self.msg = msg - Exception.__init__(self, msg) - def __str__(self): - return self.msg - -class ValidationError(Error): - def __init__(self, msg, file): - self.msg = msg - self.file = file - Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) - -class ParsingError(Error): - def __init__(self, msg, file): - self.msg = msg - self.file = file - Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) - -class NoKeyError(Error): - def __init__(self, key, group, file): - Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) - self.key = key - self.group = group - -class DuplicateKeyError(Error): - def __init__(self, key, group, file): - Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) - self.key = key - self.group = group - -class NoGroupError(Error): - def __init__(self, group, file): - Error.__init__(self, "No group: %s in file %s" % (group, file)) - self.group = group - -class DuplicateGroupError(Error): - def __init__(self, group, file): - Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) - self.group = group - -class NoThemeError(Error): - def __init__(self, theme): - Error.__init__(self, "No such icon-theme: %s" % theme) - self.theme = theme diff --git a/xdg/IconTheme.py b/xdg/IconTheme.py deleted file mode 100644 index 1f1fa18..0000000 --- a/xdg/IconTheme.py +++ /dev/null @@ -1,391 +0,0 @@ -""" -Complete implementation of the XDG Icon Spec Version 0.8 -http://standards.freedesktop.org/icon-theme-spec/ -""" - -import os, sys, time - -from xdg.IniFile import * -from xdg.BaseDirectory import * -from xdg.Exceptions import * - -import xdg.Config - -class IconTheme(IniFile): - "Class to parse and validate IconThemes" - def __init__(self): - IniFile.__init__(self) - - def __repr__(self): - return self.name - - def parse(self, file): - IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"]) - self.dir = os.path.dirname(file) - (nil, self.name) = os.path.split(self.dir) - - def getDir(self): - return self.dir - - # Standard Keys - def getName(self): - return self.get('Name', locale=True) - def getComment(self): - return self.get('Comment', locale=True) - def getInherits(self): - return self.get('Inherits', list=True) - def getDirectories(self): - return self.get('Directories', list=True) - def getHidden(self): - return self.get('Hidden', type="boolean") - def getExample(self): - return self.get('Example') - - # Per Directory Keys - def getSize(self, directory): - return self.get('Size', type="integer", group=directory) - def getContext(self, directory): - return self.get('Context', group=directory) - def getType(self, directory): - value = self.get('Type', group=directory) - if value: - return value - else: - return "Threshold" - def getMaxSize(self, directory): - value = self.get('MaxSize', type="integer", group=directory) - if value or value == 0: - return value - else: - return self.getSize(directory) - def getMinSize(self, directory): - value = self.get('MinSize', type="integer", group=directory) - if value or value == 0: - return value - else: - return self.getSize(directory) - def getThreshold(self, directory): - value = self.get('Threshold', type="integer", group=directory) - if value or value == 0: - return value - else: - return 2 - - # validation stuff - def checkExtras(self): - # header - if self.defaultGroup == "KDE Icon Theme": - self.warnings.append('[KDE Icon Theme]-Header is deprecated') - - # file extension - if self.fileExtension == ".theme": - pass - elif self.fileExtension == ".desktop": - self.warnings.append('.desktop fileExtension is deprecated') - else: - self.warnings.append('Unknown File extension') - - # Check required keys - # Name - try: - self.name = self.content[self.defaultGroup]["Name"] - except KeyError: - self.errors.append("Key 'Name' is missing") - - # Comment - try: - self.comment = self.content[self.defaultGroup]["Comment"] - except KeyError: - self.errors.append("Key 'Comment' is missing") - - # Directories - try: - self.directories = self.content[self.defaultGroup]["Directories"] - except KeyError: - self.errors.append("Key 'Directories' is missing") - - def checkGroup(self, group): - # check if group header is valid - if group == self.defaultGroup: - pass - elif group in self.getDirectories(): - try: - self.type = self.content[group]["Type"] - except KeyError: - self.type = "Threshold" - try: - self.name = self.content[group]["Name"] - except KeyError: - self.errors.append("Key 'Name' in Group '%s' is missing" % group) - elif not (re.match("^\[X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group): - self.errors.append("Invalid Group name: %s" % group) - - def checkKey(self, key, value, group): - # standard keys - if group == self.defaultGroup: - if re.match("^Name"+xdg.Locale.regex+"$", key): - pass - elif re.match("^Comment"+xdg.Locale.regex+"$", key): - pass - elif key == "Inherits": - self.checkValue(key, value, list=True) - elif key == "Directories": - self.checkValue(key, value, list=True) - elif key == "Hidden": - self.checkValue(key, value, type="boolean") - elif key == "Example": - self.checkValue(key, value) - elif re.match("^X-[a-zA-Z0-9-]+", key): - pass - else: - self.errors.append("Invalid key: %s" % key) - elif group in self.getDirectories(): - if key == "Size": - self.checkValue(key, value, type="integer") - elif key == "Context": - self.checkValue(key, value) - elif key == "Type": - self.checkValue(key, value) - if value not in ["Fixed", "Scalable", "Threshold"]: - self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value) - elif key == "MaxSize": - self.checkValue(key, value, type="integer") - if self.type != "Scalable": - self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type) - elif key == "MinSize": - self.checkValue(key, value, type="integer") - if self.type != "Scalable": - self.errors.append("Key 'MinSize' give, but Type is %s" % self.type) - elif key == "Threshold": - self.checkValue(key, value, type="integer") - if self.type != "Threshold": - self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) - elif re.match("^X-[a-zA-Z0-9-]+", key): - pass - else: - self.errors.append("Invalid key: %s" % key) - - -class IconData(IniFile): - "Class to parse and validate IconData Files" - def __init__(self): - IniFile.__init__(self) - - def __repr__(self): - return self.getDisplayName() - - def parse(self, file): - IniFile.parse(self, file, ["Icon Data"]) - - # Standard Keys - def getDisplayName(self): - return self.get('DisplayName', locale=True) - def getEmbeddedTextRectangle(self): - return self.get('EmbeddedTextRectangle', list=True) - def getAttachPoints(self): - return self.get('AttachPoints', type="point", list=True) - - # validation stuff - def checkExtras(self): - # file extension - if self.fileExtension != ".icon": - self.warnings.append('Unknown File extension') - - def checkGroup(self, group): - # check if group header is valid - if not (group == self.defaultGroup \ - or (re.match("^\[X-", group) and group.encode("ascii", 'ignore') == group)): - self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace")) - - def checkKey(self, key, value, group): - # standard keys - if re.match("^DisplayName"+xdg.Locale.regex+"$", key): - pass - elif key == "EmbeddedTextRectangle": - self.checkValue(key, value, type="integer", list=True) - elif key == "AttachPoints": - self.checkValue(key, value, type="point", list=True) - elif re.match("^X-[a-zA-Z0-9-]+", key): - pass - else: - self.errors.append("Invalid key: %s" % key) - - - -icondirs = [] -for basedir in xdg_data_dirs: - icondirs.append(os.path.join(basedir, "icons")) - icondirs.append(os.path.join(basedir, "pixmaps")) -icondirs.append(os.path.expanduser("~/.icons")) - -# just cache variables, they give a 10x speed improvement -themes = [] -cache = dict() -dache = dict() -eache = dict() - -def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): - global themes - - if size == None: - size = xdg.Config.icon_size - if theme == None: - theme = xdg.Config.icon_theme - - # if we have an absolute path, just return it - if os.path.isabs(iconname): - return iconname - - # check if it has an extension and strip it - if os.path.splitext(iconname)[1][1:] in extensions: - iconname = os.path.splitext(iconname)[0] - - # parse theme files - try: - if themes[0].name != theme: - themes = [] - __addTheme(theme) - except IndexError: - __addTheme(theme) - - # more caching (icon looked up in the last 5 seconds?) - tmp = "".join([iconname, str(size), theme, "".join(extensions)]) - if eache.has_key(tmp): - if int(time.time() - eache[tmp][0]) >= xdg.Config.cache_time: - del eache[tmp] - else: - return eache[tmp][1] - - for thme in themes: - icon = LookupIcon(iconname, size, thme, extensions) - if icon: - eache[tmp] = [time.time(), icon] - return icon - - # cache stuff again (directories lookuped up in the last 5 seconds?) - for directory in icondirs: - if (not dache.has_key(directory) \ - or (int(time.time() - dache[directory][1]) >= xdg.Config.cache_time \ - and dache[directory][2] < os.path.getmtime(directory))) \ - and os.path.isdir(directory): - dache[directory] = [os.listdir(directory), time.time(), os.path.getmtime(directory)] - - for dir, values in dache.items(): - for extension in extensions: - try: - if iconname + "." + extension in values[0]: - icon = os.path.join(dir, iconname + "." + extension) - eache[tmp] = [time.time(), icon] - return icon - except UnicodeDecodeError, e: - if debug: - raise e - else: - pass - - # we haven't found anything? "hicolor" is our fallback - if theme != "hicolor": - icon = getIconPath(iconname, size, "hicolor") - eache[tmp] = [time.time(), icon] - return icon - -def getIconData(path): - if os.path.isfile(path): - dirname = os.path.dirname(path) - basename = os.path.basename(path) - if os.path.isfile(os.path.join(dirname, basename + ".icon")): - data = IconData() - data.parse(os.path.join(dirname, basename + ".icon")) - return data - -def __addTheme(theme): - for dir in icondirs: - if os.path.isfile(os.path.join(dir, theme, "index.theme")): - __parseTheme(os.path.join(dir,theme, "index.theme")) - break - elif os.path.isfile(os.path.join(dir, theme, "index.desktop")): - __parseTheme(os.path.join(dir,theme, "index.desktop")) - break - else: - if debug: - raise NoThemeError(theme) - -def __parseTheme(file): - theme = IconTheme() - theme.parse(file) - themes.append(theme) - for subtheme in theme.getInherits(): - __addTheme(subtheme) - -def LookupIcon(iconname, size, theme, extensions): - # look for the cache - if not cache.has_key(theme.name): - cache[theme.name] = [] - cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup - cache[theme.name].append(0) # [1] mtime - cache[theme.name].append(dict()) # [2] dir: [subdir, [items]] - - # cache stuff (directory lookuped up the in the last 5 seconds?) - if int(time.time() - cache[theme.name][0]) >= xdg.Config.cache_time: - cache[theme.name][0] = time.time() - for subdir in theme.getDirectories(): - for directory in icondirs: - dir = os.path.join(directory,theme.name,subdir) - if (not cache[theme.name][2].has_key(dir) \ - or cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \ - and subdir != "" \ - and os.path.isdir(dir): - cache[theme.name][2][dir] = [subdir, os.listdir(dir)] - cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name)) - - for dir, values in cache[theme.name][2].items(): - if DirectoryMatchesSize(values[0], size, theme): - for extension in extensions: - if iconname + "." + extension in values[1]: - return os.path.join(dir, iconname + "." + extension) - - minimal_size = sys.maxint - closest_filename = "" - for dir, values in cache[theme.name][2].items(): - distance = DirectorySizeDistance(values[0], size, theme) - if distance < minimal_size: - for extension in extensions: - if iconname + "." + extension in values[1]: - closest_filename = os.path.join(dir, iconname + "." + extension) - minimal_size = distance - - return closest_filename - -def DirectoryMatchesSize(subdir, iconsize, theme): - Type = theme.getType(subdir) - Size = theme.getSize(subdir) - Threshold = theme.getThreshold(subdir) - MinSize = theme.getMinSize(subdir) - MaxSize = theme.getMaxSize(subdir) - if Type == "Fixed": - return Size == iconsize - elif Type == "Scaleable": - return MinSize <= iconsize <= MaxSize - elif Type == "Threshold": - return Size - Threshold <= iconsize <= Size + Threshold - -def DirectorySizeDistance(subdir, iconsize, theme): - Type = theme.getType(subdir) - Size = theme.getSize(subdir) - Threshold = theme.getThreshold(subdir) - MinSize = theme.getMinSize(subdir) - MaxSize = theme.getMaxSize(subdir) - if Type == "Fixed": - return abs(Size - iconsize) - elif Type == "Scalable": - if iconsize < MinSize: - return MinSize - iconsize - elif iconsize > MaxSize: - return MaxSize - iconsize - return 0 - elif Type == "Threshold": - if iconsize < Size - Threshold: - return MinSize - iconsize - elif iconsize > Size + Threshold: - return iconsize - MaxSize - return 0 diff --git a/xdg/IniFile.py b/xdg/IniFile.py deleted file mode 100644 index f3f08c7..0000000 --- a/xdg/IniFile.py +++ /dev/null @@ -1,406 +0,0 @@ -""" -Base Class for DesktopEntry, IconTheme and IconData -""" - -import re, os, stat, codecs -from Exceptions import * -import xdg.Locale - -class IniFile: - defaultGroup = '' - fileExtension = '' - - filename = '' - - tainted = False - - def __init__(self, filename=None): - self.content = dict() - if filename: - self.parse(filename) - - def __cmp__(self, other): - return cmp(self.content, other.content) - - def parse(self, filename, headers=None): - # for performance reasons - content = self.content - - if not os.path.isfile(filename): - raise ParsingError("File not found", filename) - - try: - fd = file(filename, 'r') - except IOError, e: - if debug: - raise e - else: - return - - # parse file - for line in fd: - line = line.strip() - # empty line - if not line: - continue - # comment - elif line[0] == '#': - continue - # new group - elif line[0] == '[': - currentGroup = line.lstrip("[").rstrip("]") - if debug and self.hasGroup(currentGroup): - raise DuplicateGroupError(currentGroup, filename) - else: - content[currentGroup] = {} - # key - else: - index = line.find("=") - key = line[0:index].strip() - value = line[index+1:].strip() - try: - if debug and self.hasKey(key, currentGroup): - raise DuplicateKeyError(key, currentGroup, filename) - else: - content[currentGroup][key] = value - except (IndexError, UnboundLocalError): - raise ParsingError("Parsing error on key, group missing", filename) - - fd.close() - - self.filename = filename - self.tainted = False - - # check header - if headers: - for header in headers: - if content.has_key(header): - self.defaultGroup = header - break - else: - raise ParsingError("[%s]-Header missing" % headers[0], filename) - - # start stuff to access the keys - def get(self, key, group=None, locale=False, type="string", list=False): - # set default group - if not group: - group = self.defaultGroup - - # return key (with locale) - if self.content.has_key(group) and self.content[group].has_key(key): - if locale: - value = self.content[group][self.__addLocale(key, group)] - else: - value = self.content[group][key] - else: - if debug: - if not self.content.has_key(group): - raise NoGroupError(group, self.filename) - elif not self.content[group].has_key(key): - raise NoKeyError(key, group, self.filename) - else: - value = "" - - if list == True: - values = self.getList(value) - result = [] - else: - values = [value] - - for value in values: - if type == "string" and locale == True: - value = value.decode("utf-8", "ignore") - elif type == "boolean": - value = self.__getBoolean(value) - elif type == "integer": - try: - value = int(value) - except ValueError: - value = 0 - elif type == "numeric": - try: - value = float(value) - except ValueError: - value = 0.0 - elif type == "regex": - value = re.compile(value) - elif type == "point": - value = value.split(",") - - if list == True: - result.append(value) - else: - result = value - - return result - # end stuff to access the keys - - # start subget - def getList(self, string): - if re.search(r"(? 0: - key = key + "[" + xdg.Locale.langs[0] + "]" - - try: - if isinstance(value, unicode): - self.content[group][key] = value.encode("utf-8", "ignore") - else: - self.content[group][key] = value - except KeyError: - raise NoGroupError(group, self.filename) - - self.tainted = (value == self.get(key, group)) - - def addGroup(self, group): - if self.hasGroup(group): - if debug: - raise DuplicateGroupError(group, self.filename) - else: - pass - else: - self.content[group] = {} - self.tainted = True - - def removeGroup(self, group): - existed = group in self.content - if existed: - del self.content[group] - self.tainted = True - else: - if debug: - raise NoGroupError(group, self.filename) - return existed - - def removeKey(self, key, group=None, locales=True): - # set default group - if not group: - group = self.defaultGroup - - try: - if locales: - for (name, value) in self.content[group].items(): - if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key: - value = self.content[group][name] - del self.content[group][name] - value = self.content[group][key] - del self.content[group][key] - self.tainted = True - return value - except KeyError, e: - if debug: - if e == group: - raise NoGroupError(group, self.filename) - else: - raise NoKeyError(key, group, self.filename) - else: - return "" - - # misc - def groups(self): - return self.content.keys() - - def hasGroup(self, group): - if self.content.has_key(group): - return True - else: - return False - - def hasKey(self, key, group=None): - # set default group - if not group: - group = self.defaultGroup - - if self.content[group].has_key(key): - return True - else: - return False - - def getFileName(self): - return self.filename diff --git a/xdg/Locale.py b/xdg/Locale.py deleted file mode 100644 index d30d91a..0000000 --- a/xdg/Locale.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Helper Module for Locale settings - -This module is based on a ROX module (LGPL): - -http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log -""" - -import os -from locale import normalize - -regex = "(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z\-0-9]+)?(@[a-zA-Z]+)?\])?" - -def _expand_lang(locale): - locale = normalize(locale) - COMPONENT_CODESET = 1 << 0 - COMPONENT_MODIFIER = 1 << 1 - COMPONENT_TERRITORY = 1 << 2 - # split up the locale into its base components - mask = 0 - pos = locale.find('@') - if pos >= 0: - modifier = locale[pos:] - locale = locale[:pos] - mask |= COMPONENT_MODIFIER - else: - modifier = '' - pos = locale.find('.') - codeset = '' - if pos >= 0: - locale = locale[:pos] - pos = locale.find('_') - if pos >= 0: - territory = locale[pos:] - locale = locale[:pos] - mask |= COMPONENT_TERRITORY - else: - territory = '' - language = locale - ret = [] - for i in range(mask+1): - if not (i & ~mask): # if all components for this combo exist ... - val = language - if i & COMPONENT_TERRITORY: val += territory - if i & COMPONENT_CODESET: val += codeset - if i & COMPONENT_MODIFIER: val += modifier - ret.append(val) - ret.reverse() - return ret - -def expand_languages(languages=None): - # Get some reasonable defaults for arguments that were not supplied - if languages is None: - languages = [] - for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): - val = os.environ.get(envar) - if val: - languages = val.split(':') - break - #if 'C' not in languages: - # languages.append('C') - - # now normalize and expand the languages - nelangs = [] - for lang in languages: - for nelang in _expand_lang(lang): - if nelang not in nelangs: - nelangs.append(nelang) - return nelangs - -def update(language=None): - global langs - if language: - langs = expand_languages([language]) - else: - langs = expand_languages() - -langs = [] -update() diff --git a/xdg/Menu.py b/xdg/Menu.py deleted file mode 100644 index d437ee4..0000000 --- a/xdg/Menu.py +++ /dev/null @@ -1,1074 +0,0 @@ -""" -Implementation of the XDG Menu Specification Version 1.0.draft-1 -http://standards.freedesktop.org/menu-spec/ -""" - -from __future__ import generators -import locale, os, xml.dom.minidom - -from xdg.BaseDirectory import * -from xdg.DesktopEntry import * -from xdg.Exceptions import * - -import xdg.Locale -import xdg.Config - -ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE - -# for python <= 2.3 -try: - reversed = reversed -except NameError: - def reversed(x): - return x[::-1] - -class Menu: - def __init__(self): - # Public stuff - self.Name = "" - self.Directory = None - self.Entries = [] - self.Doc = "" - self.Filename = "" - self.Depth = 0 - self.Parent = None - self.NotInXml = False - - # Can be one of Deleted/NoDisplay/Hidden/Empty/NotShowIn or True - self.Show = True - self.Visible = 0 - - # Private stuff, only needed for parsing - self.AppDirs = [] - self.DefaultLayout = None - self.Deleted = "notset" - self.Directories = [] - self.DirectoryDirs = [] - self.Layout = None - self.MenuEntries = [] - self.Moves = [] - self.OnlyUnallocated = "notset" - self.Rules = [] - self.Submenus = [] - - def __str__(self): - return self.Name - - def __add__(self, other): - for dir in other.AppDirs: - self.AppDirs.append(dir) - - for dir in other.DirectoryDirs: - self.DirectoryDirs.append(dir) - - for directory in other.Directories: - self.Directories.append(directory) - - if other.Deleted != "notset": - self.Deleted = other.Deleted - - if other.OnlyUnallocated != "notset": - self.OnlyUnallocated = other.OnlyUnallocated - - if other.Layout: - self.Layout = other.Layout - - if other.DefaultLayout: - self.DefaultLayout = other.DefaultLayout - - for rule in other.Rules: - self.Rules.append(rule) - - for move in other.Moves: - self.Moves.append(move) - - for submenu in other.Submenus: - self.addSubmenu(submenu) - - return self - - # FIXME: Performance: cache getName() - def __cmp__(self, other): - return locale.strcoll(self.getName(), other.getName()) - - def __eq__(self, other): - if self.Name == str(other): - return True - else: - return False - - """ PUBLIC STUFF """ - def getEntries(self, hidden=False): - for entry in self.Entries: - if hidden == True: - yield entry - elif entry.Show == True: - yield entry - - # FIXME: Add searchEntry/seaqrchMenu function - # search for name/comment/genericname/desktopfileide - # return multiple items - - def getMenuEntry(self, desktopfileid, deep = False): - for menuentry in self.MenuEntries: - if menuentry.DesktopFileID == desktopfileid: - return menuentry - if deep == True: - for submenu in self.Submenus: - submenu.getMenuEntry(desktopfileid, deep) - - def getMenu(self, path): - array = path.split("/", 1) - for submenu in self.Submenus: - if submenu.Name == array[0]: - if len(array) > 1: - return submenu.getMenu(array[1]) - else: - return submenu - - def getPath(self, org=False, toplevel=False): - parent = self - names=[] - while 1: - if org: - names.append(parent.Name) - else: - names.append(parent.getName()) - if parent.Depth > 0: - parent = parent.Parent - else: - break - names.reverse() - path = "" - if toplevel == False: - names.pop(0) - for name in names: - path = os.path.join(path, name) - return path - - def getName(self): - try: - return self.Directory.DesktopEntry.getName() - except AttributeError: - return self.Name - - def getGenericName(self): - try: - return self.Directory.DesktopEntry.getGenericName() - except AttributeError: - return "" - - def getComment(self): - try: - return self.Directory.DesktopEntry.getComment() - except AttributeError: - return "" - - def getIcon(self): - try: - return self.Directory.DesktopEntry.getIcon() - except AttributeError: - return "" - - """ PRIVATE STUFF """ - def addSubmenu(self, newmenu): - for submenu in self.Submenus: - if submenu == newmenu: - submenu += newmenu - break - else: - self.Submenus.append(newmenu) - newmenu.Parent = self - newmenu.Depth = self.Depth + 1 - -class Move: - "A move operation" - def __init__(self, node=None): - if node: - self.parseNode(node) - else: - self.Old = "" - self.New = "" - - def __cmp__(self, other): - return cmp(self.Old, other.Old) - - def parseNode(self, node): - for child in node.childNodes: - if child.nodeType == ELEMENT_NODE: - if child.tagName == "Old": - try: - self.parseOld(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('Old cannot be empty', '??') - elif child.tagName == "New": - try: - self.parseNew(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('New cannot be empty', '??') - - def parseOld(self, value): - self.Old = value - def parseNew(self, value): - self.New = value - - -class Layout: - "Menu Layout class" - def __init__(self, node=None): - self.order = [] - if node: - self.show_empty = node.getAttribute("show_empty") or "false" - self.inline = node.getAttribute("inline") or "false" - self.inline_limit = node.getAttribute("inline_limit") or 4 - self.inline_header = node.getAttribute("inline_header") or "true" - self.inline_alias = node.getAttribute("inline_alias") or "false" - self.inline_limit = int(self.inline_limit) - self.parseNode(node) - else: - self.show_empty = "false" - self.inline = "false" - self.inline_limit = 4 - self.inline_header = "true" - self.inline_alias = "false" - self.order.append(["Merge", "menus"]) - self.order.append(["Merge", "files"]) - - def parseNode(self, node): - for child in node.childNodes: - if child.nodeType == ELEMENT_NODE: - if child.tagName == "Menuname": - try: - self.parseMenuname( - child.childNodes[0].nodeValue, - child.getAttribute("show_empty") or "false", - child.getAttribute("inline") or "false", - child.getAttribute("inline_limit") or 4, - child.getAttribute("inline_header") or "true", - child.getAttribute("inline_alias") or "false" ) - except IndexError: - raise ValidationError('Menuname cannot be empty', "") - elif child.tagName == "Separator": - self.parseSeparator() - elif child.tagName == "Filename": - try: - self.parseFilename(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('Filename cannot be empty', "") - elif child.tagName == "Merge": - self.parseMerge(child.getAttribute("type") or "all") - - def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"): - self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias]) - self.order[-1][4] = int(self.order[-1][4]) - - def parseSeparator(self): - self.order.append(["Separator"]) - - def parseFilename(self, value): - self.order.append(["Filename", value]) - - def parseMerge(self, type="all"): - self.order.append(["Merge", type]) - - -class Rule: - "Inlcude / Exclude Rules Class" - def __init__(self, type, node=None): - # Type is Include or Exclude - self.Type = type - # Rule is a python expression - self.Rule = "" - - # Private attributes, only needed for parsing - self.Depth = 0 - self.Expr = [ "or" ] - self.New = True - - # Begin parsing - if node: - self.parseNode(node) - self.compile() - - def __str__(self): - return self.Rule - - def compile(self): - exec(""" -def do(menuentries, type, run): - for menuentry in menuentries: - if run == 2 and ( menuentry.MatchedInclude == True \ - or menuentry.Allocated == True ): - continue - elif %s: - if type == "Include": - menuentry.Add = True - menuentry.MatchedInclude = True - else: - menuentry.Add = False - return menuentries -""" % self.Rule) in self.__dict__ - - def parseNode(self, node): - for child in node.childNodes: - if child.nodeType == ELEMENT_NODE: - if child.tagName == 'Filename': - try: - self.parseFilename(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('Filename cannot be empty', "???") - elif child.tagName == 'Category': - try: - self.parseCategory(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('Category cannot be empty', "???") - elif child.tagName == 'All': - self.parseAll() - elif child.tagName == 'And': - self.parseAnd(child) - elif child.tagName == 'Or': - self.parseOr(child) - elif child.tagName == 'Not': - self.parseNot(child) - - def parseNew(self, set=True): - if not self.New: - self.Rule += " " + self.Expr[self.Depth] + " " - if not set: - self.New = True - elif set: - self.New = False - - def parseFilename(self, value): - self.parseNew() - self.Rule += "menuentry.DesktopFileID == '%s'" % value.strip().replace("\\", r"\\").replace("'", r"\'") - - def parseCategory(self, value): - self.parseNew() - self.Rule += "'%s' in menuentry.Categories" % value.strip() - - def parseAll(self): - self.parseNew() - self.Rule += "True" - - def parseAnd(self, node): - self.parseNew(False) - self.Rule += "(" - self.Depth += 1 - self.Expr.append("and") - self.parseNode(node) - self.Depth -= 1 - self.Expr.pop() - self.Rule += ")" - - def parseOr(self, node): - self.parseNew(False) - self.Rule += "(" - self.Depth += 1 - self.Expr.append("or") - self.parseNode(node) - self.Depth -= 1 - self.Expr.pop() - self.Rule += ")" - - def parseNot(self, node): - self.parseNew(False) - self.Rule += "not (" - self.Depth += 1 - self.Expr.append("or") - self.parseNode(node) - self.Depth -= 1 - self.Expr.pop() - self.Rule += ")" - - -class MenuEntry: - "Wrapper for 'Menu Style' Desktop Entries" - def __init__(self, filename, dir="", prefix=""): - # Create entry - self.DesktopEntry = DesktopEntry(os.path.join(dir,filename)) - self.setAttributes(filename, dir, prefix) - - # Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True - self.Show = True - - # Semi-Private - self.Original = None - self.Parents = [] - - # Private Stuff - self.Allocated = False - self.Add = False - self.MatchedInclude = False - - # Caching - self.Categories = self.DesktopEntry.getCategories() - - def save(self): - if self.DesktopEntry.tainted == True: - self.DesktopEntry.write() - - def getDir(self): - return self.DesktopEntry.filename.replace(self.Filename, '') - - def getType(self): - # Can be one of System/User/Both - if xdg.Config.root_mode == False: - if self.Original: - return "Both" - elif xdg_data_dirs[0] in self.DesktopEntry.filename: - return "User" - else: - return "System" - else: - return "User" - - def setAttributes(self, filename, dir="", prefix=""): - self.Filename = filename - self.Prefix = prefix - self.DesktopFileID = os.path.join(prefix,filename).replace("/", "-") - - if not os.path.isabs(self.DesktopEntry.filename): - self.__setFilename() - - def updateAttributes(self): - if self.getType() == "System": - self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) - self.__setFilename() - - def __setFilename(self): - if xdg.Config.root_mode == False: - path = xdg_data_dirs[0] - else: - path= xdg_data_dirs[1] - - if self.DesktopEntry.getType() == "Application": - dir = os.path.join(path, "applications") - else: - dir = os.path.join(path, "desktop-directories") - - self.DesktopEntry.filename = os.path.join(dir, self.Filename) - - def __cmp__(self, other): - return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) - - def __eq__(self, other): - if self.DesktopFileID == str(other): - return True - else: - return False - - def __repr__(self): - return self.DesktopFileID - - -class Separator: - "Just a dummy class for Separators" - def __init__(self, parent): - self.Parent = parent - self.Show = True - - -class Header: - "Class for Inline Headers" - def __init__(self, name, generic_name, comment): - self.Name = name - self.GenericName = generic_name - self.Comment = comment - - def __str__(self): - return self.Name - - -tmp = {} - -def __getFileName(filename): - dirs = xdg_config_dirs[:] - if xdg.Config.root_mode == True: - dirs.pop(0) - - for dir in dirs: - menuname = os.path.join (dir, "menus" , filename) - if os.path.isdir(dir) and os.path.isfile(menuname): - return menuname - -def parse(filename=None): - # conver to absolute path - if filename and not os.path.isabs(filename): - filename = __getFileName(filename) - - # use default if no filename given - if not filename: - candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" - filename = __getFileName(candidate) - - if not filename: - raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) - - # check if it is a .menu file - if not os.path.splitext(filename)[1] == ".menu": - raise ParsingError('Not a .menu file', filename) - - # create xml parser - try: - doc = xml.dom.minidom.parse(filename) - except xml.parsers.expat.ExpatError: - raise ParsingError('Not a valid .menu file', filename) - - # parse menufile - tmp["Root"] = "" - tmp["mergeFiles"] = [] - tmp["DirectoryDirs"] = [] - tmp["cache"] = MenuEntryCache() - - __parse(doc, filename, tmp["Root"]) - __parsemove(tmp["Root"]) - __postparse(tmp["Root"]) - - tmp["Root"].Doc = doc - tmp["Root"].Filename = filename - - # generate the menu - __genmenuNotOnlyAllocated(tmp["Root"]) - __genmenuOnlyAllocated(tmp["Root"]) - - # and finally sort - sort(tmp["Root"]) - - return tmp["Root"] - - -def __parse(node, filename, parent=None): - for child in node.childNodes: - if child.nodeType == ELEMENT_NODE: - if child.tagName == 'Menu': - __parseMenu(child, filename, parent) - elif child.tagName == 'AppDir': - try: - __parseAppDir(child.childNodes[0].nodeValue, filename, parent) - except IndexError: - raise ValidationError('AppDir cannot be empty', filename) - elif child.tagName == 'DefaultAppDirs': - __parseDefaultAppDir(filename, parent) - elif child.tagName == 'DirectoryDir': - try: - __parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent) - except IndexError: - raise ValidationError('DirectoryDir cannot be empty', filename) - elif child.tagName == 'DefaultDirectoryDirs': - __parseDefaultDirectoryDir(filename, parent) - elif child.tagName == 'Name' : - try: - parent.Name = child.childNodes[0].nodeValue - except IndexError: - raise ValidationError('Name cannot be empty', filename) - elif child.tagName == 'Directory' : - try: - parent.Directories.append(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('Directory cannot be empty', filename) - elif child.tagName == 'OnlyUnallocated': - parent.OnlyUnallocated = True - elif child.tagName == 'NotOnlyUnallocated': - parent.OnlyUnallocated = False - elif child.tagName == 'Deleted': - parent.Deleted = True - elif child.tagName == 'NotDeleted': - parent.Deleted = False - elif child.tagName == 'Include' or child.tagName == 'Exclude': - parent.Rules.append(Rule(child.tagName, child)) - elif child.tagName == 'MergeFile': - try: - if child.getAttribute("type") == "parent": - __parseMergeFile("applications.menu", child, filename, parent) - else: - __parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent) - except IndexError: - raise ValidationError('MergeFile cannot be empty', filename) - elif child.tagName == 'MergeDir': - try: - __parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent) - except IndexError: - raise ValidationError('MergeDir cannot be empty', filename) - elif child.tagName == 'DefaultMergeDirs': - __parseDefaultMergeDirs(child, filename, parent) - elif child.tagName == 'Move': - parent.Moves.append(Move(child)) - elif child.tagName == 'Layout': - if len(child.childNodes) > 1: - parent.Layout = Layout(child) - elif child.tagName == 'DefaultLayout': - if len(child.childNodes) > 1: - parent.DefaultLayout = Layout(child) - elif child.tagName == 'LegacyDir': - try: - __parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent) - except IndexError: - raise ValidationError('LegacyDir cannot be empty', filename) - elif child.tagName == 'KDELegacyDirs': - __parseKDELegacyDirs(filename, parent) - -def __parsemove(menu): - for submenu in menu.Submenus: - __parsemove(submenu) - - # parse move operations - for move in menu.Moves: - move_from_menu = menu.getMenu(move.Old) - if move_from_menu: - move_to_menu = menu.getMenu(move.New) - - menus = move.New.split("/") - oldparent = None - while len(menus) > 0: - if not oldparent: - oldparent = menu - newmenu = oldparent.getMenu(menus[0]) - if not newmenu: - newmenu = Menu() - newmenu.Name = menus[0] - if len(menus) > 1: - newmenu.NotInXml = True - oldparent.addSubmenu(newmenu) - oldparent = newmenu - menus.pop(0) - - newmenu += move_from_menu - move_from_menu.Parent.Submenus.remove(move_from_menu) - -def __postparse(menu): - # unallocated / deleted - if menu.Deleted == "notset": - menu.Deleted = False - if menu.OnlyUnallocated == "notset": - menu.OnlyUnallocated = False - - # Layout Tags - if not menu.Layout or not menu.DefaultLayout: - if menu.DefaultLayout: - menu.Layout = menu.DefaultLayout - elif menu.Layout: - if menu.Depth > 0: - menu.DefaultLayout = menu.Parent.DefaultLayout - else: - menu.DefaultLayout = Layout() - else: - if menu.Depth > 0: - menu.Layout = menu.Parent.DefaultLayout - menu.DefaultLayout = menu.Parent.DefaultLayout - else: - menu.Layout = Layout() - menu.DefaultLayout = Layout() - - # add parent's app/directory dirs - if menu.Depth > 0: - menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs - menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs - - # remove duplicates - menu.Directories = __removeDuplicates(menu.Directories) - menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs) - menu.AppDirs = __removeDuplicates(menu.AppDirs) - - # go recursive through all menus - for submenu in menu.Submenus: - __postparse(submenu) - - # reverse so handling is easier - menu.Directories.reverse() - menu.DirectoryDirs.reverse() - menu.AppDirs.reverse() - - # get the valid .directory file out of the list - for directory in menu.Directories: - for dir in menu.DirectoryDirs: - if os.path.isfile(os.path.join(dir, directory)): - menuentry = MenuEntry(directory, dir) - if not menu.Directory: - menu.Directory = menuentry - elif menuentry.getType() == "System": - if menu.Directory.getType() == "User": - menu.Directory.Original = menuentry - if menu.Directory: - break - - -# Menu parsing stuff -def __parseMenu(child, filename, parent): - m = Menu() - __parse(child, filename, m) - if parent: - parent.addSubmenu(m) - else: - tmp["Root"] = m - -# helper function -def __check(value, filename, type): - path = os.path.dirname(filename) - - if not os.path.isabs(value): - value = os.path.join(path, value) - - value = os.path.abspath(value) - - if type == "dir" and os.path.exists(value) and os.path.isdir(value): - return value - elif type == "file" and os.path.exists(value) and os.path.isfile(value): - return value - else: - return False - -# App/Directory Dir Stuff -def __parseAppDir(value, filename, parent): - value = __check(value, filename, "dir") - if value: - parent.AppDirs.append(value) - -def __parseDefaultAppDir(filename, parent): - for dir in reversed(xdg_data_dirs): - __parseAppDir(os.path.join(dir, "applications"), filename, parent) - -def __parseDirectoryDir(value, filename, parent): - value = __check(value, filename, "dir") - if value: - parent.DirectoryDirs.append(value) - -def __parseDefaultDirectoryDir(filename, parent): - for dir in reversed(xdg_data_dirs): - __parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent) - -# Merge Stuff -def __parseMergeFile(value, child, filename, parent): - if child.getAttribute("type") == "parent": - for dir in xdg_config_dirs: - rel_file = filename.replace(dir, "").strip("/") - if rel_file != filename: - for p in xdg_config_dirs: - if dir == p: - continue - if os.path.isfile(os.path.join(p,rel_file)): - __mergeFile(os.path.join(p,rel_file),child,parent) - break - else: - value = __check(value, filename, "file") - if value: - __mergeFile(value, child, parent) - -def __parseMergeDir(value, child, filename, parent): - value = __check(value, filename, "dir") - if value: - for item in os.listdir(value): - try: - if os.path.splitext(item)[1] == ".menu": - __mergeFile(os.path.join(value, item), child, parent) - except UnicodeDecodeError: - continue - -def __parseDefaultMergeDirs(child, filename, parent): - basename = os.path.splitext(os.path.basename(filename))[0] - for dir in reversed(xdg_config_dirs): - __parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent) - -def __mergeFile(filename, child, parent): - # check for infinite loops - if filename in tmp["mergeFiles"]: - if debug: - raise ParsingError('Infinite MergeFile loop detected', filename) - else: - return - - tmp["mergeFiles"].append(filename) - - # load file - try: - doc = xml.dom.minidom.parse(filename) - except IOError: - if debug: - raise ParsingError('File not found', filename) - else: - return - except xml.parsers.expat.ExpatError: - if debug: - raise ParsingError('Not a valid .menu file', filename) - else: - return - - # append file - for child in doc.childNodes: - if child.nodeType == ELEMENT_NODE: - __parse(child,filename,parent) - break - -# Legacy Dir Stuff -def __parseLegacyDir(dir, prefix, filename, parent): - m = __mergeLegacyDir(dir,prefix,filename,parent) - if m: - parent += m - -def __mergeLegacyDir(dir, prefix, filename, parent): - dir = __check(dir,filename,"dir") - if dir and dir not in tmp["DirectoryDirs"]: - tmp["DirectoryDirs"].append(dir) - - m = Menu() - m.AppDirs.append(dir) - m.DirectoryDirs.append(dir) - m.Name = os.path.basename(dir) - m.NotInXml = True - - for item in os.listdir(dir): - try: - if item == ".directory": - m.Directories.append(item) - elif os.path.isdir(os.path.join(dir,item)): - m.addSubmenu(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent)) - except UnicodeDecodeError: - continue - - tmp["cache"].addMenuEntries([dir],prefix, True) - menuentries = tmp["cache"].getMenuEntries([dir], False) - - for menuentry in menuentries: - categories = menuentry.Categories - if len(categories) == 0: - r = Rule("Include") - r.parseFilename(menuentry.DesktopFileID) - r.compile() - m.Rules.append(r) - if not dir in parent.AppDirs: - categories.append("Legacy") - menuentry.Categories = categories - - return m - -def __parseKDELegacyDirs(filename, parent): - f=os.popen3("kde-config --path apps") - output = f[1].readlines() - try: - for dir in output[0].split(":"): - __parseLegacyDir(dir,"kde", filename, parent) - except IndexError: - pass - -# remove duplicate entries from a list -def __removeDuplicates(list): - set = {} - list.reverse() - list = [set.setdefault(e,e) for e in list if e not in set] - list.reverse() - return list - -# Finally generate the menu -def __genmenuNotOnlyAllocated(menu): - for submenu in menu.Submenus: - __genmenuNotOnlyAllocated(submenu) - - if menu.OnlyUnallocated == False: - tmp["cache"].addMenuEntries(menu.AppDirs) - menuentries = [] - for rule in menu.Rules: - menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 1) - for menuentry in menuentries: - if menuentry.Add == True: - menuentry.Parents.append(menu) - menuentry.Add = False - menuentry.Allocated = True - menu.MenuEntries.append(menuentry) - -def __genmenuOnlyAllocated(menu): - for submenu in menu.Submenus: - __genmenuOnlyAllocated(submenu) - - if menu.OnlyUnallocated == True: - tmp["cache"].addMenuEntries(menu.AppDirs) - menuentries = [] - for rule in menu.Rules: - menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 2) - for menuentry in menuentries: - if menuentry.Add == True: - menuentry.Parents.append(menu) - # menuentry.Add = False - # menuentry.Allocated = True - menu.MenuEntries.append(menuentry) - -# And sorting ... -def sort(menu): - menu.Entries = [] - menu.Visible = 0 - - for submenu in menu.Submenus: - sort(submenu) - - tmp_s = [] - tmp_e = [] - - for order in menu.Layout.order: - if order[0] == "Filename": - tmp_e.append(order[1]) - elif order[0] == "Menuname": - tmp_s.append(order[1]) - - for order in menu.Layout.order: - if order[0] == "Separator": - separator = Separator(menu) - if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator): - separator.Show = False - menu.Entries.append(separator) - elif order[0] == "Filename": - menuentry = menu.getMenuEntry(order[1]) - if menuentry: - menu.Entries.append(menuentry) - elif order[0] == "Menuname": - submenu = menu.getMenu(order[1]) - if submenu: - __parse_inline(submenu, menu) - elif order[0] == "Merge": - if order[1] == "files" or order[1] == "all": - menu.MenuEntries.sort() - for menuentry in menu.MenuEntries: - if menuentry not in tmp_e: - menu.Entries.append(menuentry) - elif order[1] == "menus" or order[1] == "all": - menu.Submenus.sort() - for submenu in menu.Submenus: - if submenu.Name not in tmp_s: - __parse_inline(submenu, menu) - - # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec - for entry in menu.Entries: - entry.Show = True - menu.Visible += 1 - if isinstance(entry, Menu): - if entry.Deleted == True: - entry.Show = "Deleted" - menu.Visible -= 1 - elif isinstance(entry.Directory, MenuEntry): - if entry.Directory.DesktopEntry.getNoDisplay() == True: - entry.Show = "NoDisplay" - menu.Visible -= 1 - elif entry.Directory.DesktopEntry.getHidden() == True: - entry.Show = "Hidden" - menu.Visible -= 1 - elif isinstance(entry, MenuEntry): - if entry.DesktopEntry.getNoDisplay() == True: - entry.Show = "NoDisplay" - menu.Visible -= 1 - elif entry.DesktopEntry.getHidden() == True: - entry.Show = "Hidden" - menu.Visible -= 1 - elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()): - entry.Show = "NoExec" - menu.Visible -= 1 - elif xdg.Config.windowmanager: - if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \ - or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn(): - entry.Show = "NotShowIn" - menu.Visible -= 1 - elif isinstance(entry,Separator): - menu.Visible -= 1 - - # remove separators at the beginning and at the end - if len(menu.Entries) > 0: - if isinstance(menu.Entries[0], Separator): - menu.Entries[0].Show = False - if len(menu.Entries) > 1: - if isinstance(menu.Entries[-1], Separator): - menu.Entries[-1].Show = False - - # show_empty tag - for entry in menu.Entries: - if isinstance(entry,Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0: - entry.Show = "Empty" - menu.Visible -= 1 - if entry.NotInXml == True: - menu.Entries.remove(entry) - -def __try_exec(executable): - paths = os.environ['PATH'].split(os.pathsep) - if not os.path.isfile(executable): - for p in paths: - f = os.path.join(p, executable) - if os.path.isfile(f): - if os.access(f, os.X_OK): - return True - else: - if os.access(executable, os.X_OK): - return True - return False - -# inline tags -def __parse_inline(submenu, menu): - if submenu.Layout.inline == "true": - if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true": - menuentry = submenu.Entries[0] - menuentry.DesktopEntry.set("Name", submenu.getName(), locale = True) - menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale = True) - menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale = True) - menu.Entries.append(menuentry) - elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: - if submenu.Layout.inline_header == "true": - header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) - menu.Entries.append(header) - for entry in submenu.Entries: - menu.Entries.append(entry) - else: - menu.Entries.append(submenu) - else: - menu.Entries.append(submenu) - -class MenuEntryCache: - "Class to cache Desktop Entries" - def __init__(self): - self.cacheEntries = {} - self.cacheEntries['legacy'] = [] - self.cache = {} - - def addMenuEntries(self, dirs, prefix="", legacy=False): - for dir in dirs: - if not self.cacheEntries.has_key(dir): - self.cacheEntries[dir] = [] - self.__addFiles(dir, "", prefix, legacy) - - def __addFiles(self, dir, subdir, prefix, legacy): - for item in os.listdir(os.path.join(dir,subdir)): - if os.path.splitext(item)[1] == ".desktop": - try: - menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix) - except ParsingError: - continue - - self.cacheEntries[dir].append(menuentry) - if legacy == True: - self.cacheEntries['legacy'].append(menuentry) - elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False: - self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy) - - def getMenuEntries(self, dirs, legacy=True): - list = [] - ids = [] - # handle legacy items - appdirs = dirs[:] - if legacy == True: - appdirs.append("legacy") - # cache the results again - key = "".join(appdirs) - try: - return self.cache[key] - except KeyError: - pass - for dir in appdirs: - for menuentry in self.cacheEntries[dir]: - try: - if menuentry.DesktopFileID not in ids: - ids.append(menuentry.DesktopFileID) - list.append(menuentry) - elif menuentry.getType() == "System": - # FIXME: This is only 99% correct, but still... - i = list.index(menuentry) - e = list[i] - if e.getType() == "User": - e.Original = menuentry - except UnicodeDecodeError: - continue - self.cache[key] = list - return list diff --git a/xdg/MenuEditor.py b/xdg/MenuEditor.py deleted file mode 100644 index cc5ce54..0000000 --- a/xdg/MenuEditor.py +++ /dev/null @@ -1,511 +0,0 @@ -""" CLass to edit XDG Menus """ - -from xdg.Menu import * -from xdg.BaseDirectory import * -from xdg.Exceptions import * -from xdg.DesktopEntry import * -from xdg.Config import * - -import xml.dom.minidom -import os -import re - -# XML-Cleanups: Move / Exclude -# FIXME: proper reverte/delete -# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions -# FIXME: catch Exceptions -# FIXME: copy functions -# FIXME: More Layout stuff -# FIXME: unod/redo function / remove menu... -# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile -# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs - -class MenuEditor: - def __init__(self, menu=None, filename=None, root=False): - self.menu = None - self.filename = None - self.doc = None - self.parse(menu, filename, root) - - # fix for creating two menus with the same name on the fly - self.filenames = [] - - def parse(self, menu=None, filename=None, root=False): - if root == True: - setRootMode(True) - - if isinstance(menu, Menu): - self.menu = menu - elif menu: - self.menu = parse(menu) - else: - self.menu = parse() - - if root == True: - self.filename = self.menu.Filename - elif filename: - self.filename = filename - else: - self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1]) - - try: - self.doc = xml.dom.minidom.parse(self.filename) - except IOError: - self.doc = xml.dom.minidom.parseString('Applications'+self.menu.Filename+'') - except xml.parsers.expat.ExpatError: - raise ParsingError('Not a valid .menu file', self.filename) - - self.__remove_whilespace_nodes(self.doc) - - def save(self): - self.__saveEntries(self.menu) - self.__saveMenu() - - def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None): - menuentry = MenuEntry(self.__getFileName(name, ".desktop")) - menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal) - - self.__addEntry(parent, menuentry, after, before) - - sort(self.menu) - - return menuentry - - def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None): - menu = Menu() - - menu.Parent = parent - menu.Depth = parent.Depth + 1 - menu.Layout = parent.DefaultLayout - menu.DefaultLayout = parent.DefaultLayout - - menu = self.editMenu(menu, name, genericname, comment, icon) - - self.__addEntry(parent, menu, after, before) - - sort(self.menu) - - return menu - - def createSeparator(self, parent, after=None, before=None): - separator = Separator(parent) - - self.__addEntry(parent, separator, after, before) - - sort(self.menu) - - return separator - - def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): - self.__deleteEntry(oldparent, menuentry, after, before) - self.__addEntry(newparent, menuentry, after, before) - - sort(self.menu) - - return menuentry - - def moveMenu(self, menu, oldparent, newparent, after=None, before=None): - self.__deleteEntry(oldparent, menu, after, before) - self.__addEntry(newparent, menu, after, before) - - root_menu = self.__getXmlMenu(self.menu.Name) - if oldparent.getPath(True) != newparent.getPath(True): - self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name)) - - sort(self.menu) - - return menu - - def moveSeparator(self, separator, parent, after=None, before=None): - self.__deleteEntry(parent, separator, after, before) - self.__addEntry(parent, separator, after, before) - - sort(self.menu) - - return separator - - def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): - self.__addEntry(newparent, menuentry, after, before) - - sort(self.menu) - - return menuentry - - def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None): - deskentry = menuentry.DesktopEntry - - if name: - if not deskentry.hasKey("Name"): - deskentry.set("Name", name) - deskentry.set("Name", name, locale = True) - if comment: - if not deskentry.hasKey("Comment"): - deskentry.set("Comment", comment) - deskentry.set("Comment", comment, locale = True) - if genericname: - if not deskentry.hasKey("GnericNe"): - deskentry.set("GenericName", genericname) - deskentry.set("GenericName", genericname, locale = True) - if command: - deskentry.set("Exec", command) - if icon: - deskentry.set("Icon", icon) - - if terminal == True: - deskentry.set("Terminal", "true") - elif terminal == False: - deskentry.set("Terminal", "false") - - if nodisplay == True: - deskentry.set("NoDisplay", "true") - elif nodisplay == False: - deskentry.set("NoDisplay", "false") - - if hidden == True: - deskentry.set("Hidden", "true") - elif hidden == False: - deskentry.set("Hidden", "false") - - menuentry.updateAttributes() - - if len(menuentry.Parents) > 0: - sort(self.menu) - - return menuentry - - def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None): - # Hack for legacy dirs - if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory": - xml_menu = self.__getXmlMenu(menu.getPath(True, True)) - self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory") - menu.Directory.setAttributes(menu.Name + ".directory") - # Hack for New Entries - elif not isinstance(menu.Directory, MenuEntry): - if not name: - name = menu.Name - filename = self.__getFileName(name, ".directory").replace("/", "") - if not menu.Name: - menu.Name = filename.replace(".directory", "") - xml_menu = self.__getXmlMenu(menu.getPath(True, True)) - self.__addXmlTextElement(xml_menu, 'Directory', filename) - menu.Directory = MenuEntry(filename) - - deskentry = menu.Directory.DesktopEntry - - if name: - if not deskentry.hasKey("Name"): - deskentry.set("Name", name) - deskentry.set("Name", name, locale = True) - if genericname: - if not deskentry.hasKey("GenericName"): - deskentry.set("GenericName", genericname) - deskentry.set("GenericName", genericname, locale = True) - if comment: - if not deskentry.hasKey("Comment"): - deskentry.set("Comment", comment) - deskentry.set("Comment", comment, locale = True) - if icon: - deskentry.set("Icon", icon) - - if nodisplay == True: - deskentry.set("NoDisplay", "true") - elif nodisplay == False: - deskentry.set("NoDisplay", "false") - - if hidden == True: - deskentry.set("Hidden", "true") - elif hidden == False: - deskentry.set("Hidden", "false") - - menu.Directory.updateAttributes() - - if isinstance(menu.Parent, Menu): - sort(self.menu) - - return menu - - def hideMenuEntry(self, menuentry): - self.editMenuEntry(menuentry, nodisplay = True) - - def unhideMenuEntry(self, menuentry): - self.editMenuEntry(menuentry, nodisplay = False, hidden = False) - - def hideMenu(self, menu): - self.editMenu(menu, nodisplay = True) - - def unhideMenu(self, menu): - self.editMenu(menu, nodisplay = False, hidden = False) - xml_menu = self.__getXmlMenu(menu.getPath(True,True), False) - for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu): - node.parentNode.removeChild(node) - - def deleteMenuEntry(self, menuentry): - if self.getAction(menuentry) == "delete": - self.__deleteFile(menuentry.DesktopEntry.filename) - for parent in menuentry.Parents: - self.__deleteEntry(parent, menuentry) - sort(self.menu) - return menuentry - - def revertMenuEntry(self, menuentry): - if self.getAction(menuentry) == "revert": - self.__deleteFile(menuentry.DesktopEntry.filename) - menuentry.Original.Parents = [] - for parent in menuentry.Parents: - index = parent.Entries.index(menuentry) - parent.Entries[index] = menuentry.Original - index = parent.MenuEntries.index(menuentry) - parent.MenuEntries[index] = menuentry.Original - menuentry.Original.Parents.append(parent) - sort(self.menu) - return menuentry - - def deleteMenu(self, menu): - if self.getAction(menu) == "delete": - self.__deleteFile(menu.Directory.DesktopEntry.filename) - self.__deleteEntry(menu.Parent, menu) - xml_menu = self.__getXmlMenu(menu.getPath(True, True)) - xml_menu.parentNode.removeChild(xml_menu) - sort(self.menu) - return menu - - def revertMenu(self, menu): - if self.getAction(menu) == "revert": - self.__deleteFile(menu.Directory.DesktopEntry.filename) - menu.Directory = menu.Directory.Original - sort(self.menu) - return menu - - def deleteSeparator(self, separator): - self.__deleteEntry(separator.Parent, separator, after=True) - - sort(self.menu) - - return separator - - """ Private Stuff """ - def getAction(self, entry): - if isinstance(entry, Menu): - if not isinstance(entry.Directory, MenuEntry): - return "none" - elif entry.Directory.getType() == "Both": - return "revert" - elif entry.Directory.getType() == "User" \ - and (len(entry.Submenus) + len(entry.MenuEntries)) == 0: - return "delete" - - elif isinstance(entry, MenuEntry): - if entry.getType() == "Both": - return "revert" - elif entry.getType() == "User": - return "delete" - else: - return "none" - - return "none" - - def __saveEntries(self, menu): - if not menu: - menu = self.menu - if isinstance(menu.Directory, MenuEntry): - menu.Directory.save() - for entry in menu.getEntries(hidden=True): - if isinstance(entry, MenuEntry): - entry.save() - elif isinstance(entry, Menu): - self.__saveEntries(entry) - - def __saveMenu(self): - if not os.path.isdir(os.path.dirname(self.filename)): - os.makedirs(os.path.dirname(self.filename)) - fd = open(self.filename, 'w') - fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*\n', ''))) - fd.close() - - def __getFileName(self, name, extension): - postfix = 0 - while 1: - if postfix == 0: - filename = name + extension - else: - filename = name + "-" + str(postfix) + extension - if extension == ".desktop": - dir = "applications" - elif extension == ".directory": - dir = "desktop-directories" - if not filename in self.filenames and not \ - os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)): - self.filenames.append(filename) - break - else: - postfix += 1 - - return filename - - def __getXmlMenu(self, path, create=True, element=None): - if not element: - element = self.doc - - if "/" in path: - (name, path) = path.split("/", 1) - else: - name = path - path = "" - - found = None - for node in self.__getXmlNodesByName("Menu", element): - for child in self.__getXmlNodesByName("Name", node): - if child.childNodes[0].nodeValue == name: - if path: - found = self.__getXmlMenu(path, create, node) - else: - found = node - break - if found: - break - if not found and create == True: - node = self.__addXmlMenuElement(element, name) - if path: - found = self.__getXmlMenu(path, create, node) - else: - found = node - - return found - - def __addXmlMenuElement(self, element, name): - node = self.doc.createElement('Menu') - self.__addXmlTextElement(node, 'Name', name) - return element.appendChild(node) - - def __addXmlTextElement(self, element, name, text): - node = self.doc.createElement(name) - text = self.doc.createTextNode(text) - node.appendChild(text) - return element.appendChild(node) - - def __addXmlFilename(self, element, filename, type = "Include"): - # remove old filenames - for node in self.__getXmlNodesByName(["Include", "Exclude"], element): - if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename: - element.removeChild(node) - - # add new filename - node = self.doc.createElement(type) - node.appendChild(self.__addXmlTextElement(node, 'Filename', filename)) - return element.appendChild(node) - - def __addXmlMove(self, element, old, new): - node = self.doc.createElement("Move") - node.appendChild(self.__addXmlTextElement(node, 'Old', old)) - node.appendChild(self.__addXmlTextElement(node, 'New', new)) - return element.appendChild(node) - - def __addXmlLayout(self, element, layout): - # remove old layout - for node in self.__getXmlNodesByName("Layout", element): - element.removeChild(node) - - # add new layout - node = self.doc.createElement("Layout") - for order in layout.order: - if order[0] == "Separator": - child = self.doc.createElement("Separator") - node.appendChild(child) - elif order[0] == "Filename": - child = self.__addXmlTextElement(node, "Filename", order[1]) - elif order[0] == "Menuname": - child = self.__addXmlTextElement(node, "Menuname", order[1]) - elif order[0] == "Merge": - child = self.doc.createElement("Merge") - child.setAttribute("type", order[1]) - node.appendChild(child) - return element.appendChild(node) - - def __getXmlNodesByName(self, name, element): - for child in element.childNodes: - if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name: - yield child - - def __addLayout(self, parent): - layout = Layout() - layout.order = [] - layout.show_empty = parent.Layout.show_empty - layout.inline = parent.Layout.inline - layout.inline_header = parent.Layout.inline_header - layout.inline_alias = parent.Layout.inline_alias - layout.inline_limit = parent.Layout.inline_limit - - layout.order.append(["Merge", "menus"]) - for entry in parent.Entries: - if isinstance(entry, Menu): - layout.parseMenuname(entry.Name) - elif isinstance(entry, MenuEntry): - layout.parseFilename(entry.DesktopFileID) - elif isinstance(entry, Separator): - layout.parseSeparator() - layout.order.append(["Merge", "files"]) - - parent.Layout = layout - - return layout - - def __addEntry(self, parent, entry, after=None, before=None): - if after or before: - if after: - index = parent.Entries.index(after) + 1 - elif before: - index = parent.Entries.index(before) - parent.Entries.insert(index, entry) - else: - parent.Entries.append(entry) - - xml_parent = self.__getXmlMenu(parent.getPath(True, True)) - - if isinstance(entry, MenuEntry): - parent.MenuEntries.append(entry) - entry.Parents.append(parent) - self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include") - elif isinstance(entry, Menu): - parent.addSubmenu(entry) - - if after or before: - self.__addLayout(parent) - self.__addXmlLayout(xml_parent, parent.Layout) - - def __deleteEntry(self, parent, entry, after=None, before=None): - parent.Entries.remove(entry) - - xml_parent = self.__getXmlMenu(parent.getPath(True, True)) - - if isinstance(entry, MenuEntry): - entry.Parents.remove(parent) - parent.MenuEntries.remove(entry) - self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude") - elif isinstance(entry, Menu): - parent.Submenus.remove(entry) - - if after or before: - self.__addLayout(parent) - self.__addXmlLayout(xml_parent, parent.Layout) - - def __deleteFile(self, filename): - try: - os.remove(filename) - except OSError: - pass - try: - self.filenames.remove(filename) - except ValueError: - pass - - def __remove_whilespace_nodes(self, node): - remove_list = [] - for child in node.childNodes: - if child.nodeType == xml.dom.minidom.Node.TEXT_NODE: - child.data = child.data.strip() - if not child.data.strip(): - remove_list.append(child) - elif child.hasChildNodes(): - self.__remove_whilespace_nodes(child) - for node in remove_list: - node.parentNode.removeChild(node) diff --git a/xdg/Mime.py b/xdg/Mime.py deleted file mode 100644 index 04fe0d2..0000000 --- a/xdg/Mime.py +++ /dev/null @@ -1,474 +0,0 @@ -""" -This module is based on a rox module (LGPL): - -http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log - -This module provides access to the shared MIME database. - -types is a dictionary of all known MIME types, indexed by the type name, e.g. -types['application/x-python'] - -Applications can install information about MIME types by storing an -XML file as /packages/.xml and running the -update-mime-database command, which is provided by the freedesktop.org -shared mime database package. - -See http://www.freedesktop.org/standards/shared-mime-info-spec/ for -information about the format of these files. - -(based on version 0.13) -""" - -import os -import stat -import fnmatch - -import xdg.BaseDirectory -import xdg.Locale - -from xml.dom import Node, minidom, XML_NAMESPACE - -FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info' - -types = {} # Maps MIME names to type objects - -exts = None # Maps extensions to types -globs = None # List of (glob, type) pairs -literals = None # Maps liternal names to types -magic = None - -def _get_node_data(node): - """Get text of XML node""" - return ''.join([n.nodeValue for n in node.childNodes]).strip() - -def lookup(media, subtype = None): - "Get the MIMEtype object for this type, creating a new one if needed." - if subtype is None and '/' in media: - media, subtype = media.split('/', 1) - if (media, subtype) not in types: - types[(media, subtype)] = MIMEtype(media, subtype) - return types[(media, subtype)] - -class MIMEtype: - """Type holding data about a MIME type""" - def __init__(self, media, subtype): - "Don't use this constructor directly; use mime.lookup() instead." - assert media and '/' not in media - assert subtype and '/' not in subtype - assert (media, subtype) not in types - - self.media = media - self.subtype = subtype - self._comment = None - - def _load(self): - "Loads comment for current language. Use get_comment() instead." - resource = os.path.join('mime', self.media, self.subtype + '.xml') - for path in xdg.BaseDirectory.load_data_paths(resource): - doc = minidom.parse(path) - if doc is None: - continue - for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'): - lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en' - goodness = 1 + (lang in xdg.Locale.langs) - if goodness > self._comment[0]: - self._comment = (goodness, _get_node_data(comment)) - if goodness == 2: return - - # FIXME: add get_icon method - def get_comment(self): - """Returns comment for current language, loading it if needed.""" - # Should we ever reload? - if self._comment is None: - self._comment = (0, str(self)) - self._load() - return self._comment[1] - - def __str__(self): - return self.media + '/' + self.subtype - - def __repr__(self): - return '[%s: %s]' % (self, self._comment or '(comment not loaded)') - -class MagicRule: - def __init__(self, f): - self.next=None - self.prev=None - - #print line - ind='' - while True: - c=f.read(1) - if c=='>': - break - ind+=c - if not ind: - self.nest=0 - else: - self.nest=int(ind) - - start='' - while True: - c=f.read(1) - if c=='=': - break - start+=c - self.start=int(start) - - hb=f.read(1) - lb=f.read(1) - self.lenvalue=ord(lb)+(ord(hb)<<8) - - self.value=f.read(self.lenvalue) - - c=f.read(1) - if c=='&': - self.mask=f.read(self.lenvalue) - c=f.read(1) - else: - self.mask=None - - if c=='~': - w='' - while c!='+' and c!='\n': - c=f.read(1) - if c=='+' or c=='\n': - break - w+=c - - self.word=int(w) - else: - self.word=1 - - if c=='+': - r='' - while c!='\n': - c=f.read(1) - if c=='\n': - break - r+=c - #print r - self.range=int(r) - else: - self.range=1 - - if c!='\n': - raise 'Malformed MIME magic line' - - def getLength(self): - return self.start+self.lenvalue+self.range - - def appendRule(self, rule): - if self.nest%d=[%d]%s&%s~%d+%d>' % (self.nest, - self.start, - self.lenvalue, - `self.value`, - `self.mask`, - self.word, - self.range) - -class MagicType: - def __init__(self, mtype): - self.mtype=mtype - self.top_rules=[] - self.last_rule=None - - def getLine(self, f): - nrule=MagicRule(f) - - if nrule.nest and self.last_rule: - self.last_rule.appendRule(nrule) - else: - self.top_rules.append(nrule) - - self.last_rule=nrule - - return nrule - - def match(self, buffer): - for rule in self.top_rules: - if rule.match(buffer): - return self.mtype - - def __repr__(self): - return '' % self.mtype - -class MagicDB: - def __init__(self): - self.types={} # Indexed by priority, each entry is a list of type rules - self.maxlen=0 - - def mergeFile(self, fname): - f=file(fname, 'r') - line=f.readline() - if line!='MIME-Magic\0\n': - raise 'Not a MIME magic file' - - while True: - shead=f.readline() - #print shead - if not shead: - break - if shead[0]!='[' or shead[-2:]!=']\n': - raise 'Malformed section heading' - pri, tname=shead[1:-2].split(':') - #print shead[1:-2] - pri=int(pri) - mtype=lookup(tname) - - try: - ents=self.types[pri] - except: - ents=[] - self.types[pri]=ents - - magictype=MagicType(mtype) - #print tname - - #rline=f.readline() - c=f.read(1) - f.seek(-1, 1) - while c and c!='[': - rule=magictype.getLine(f) - #print rule - if rule and rule.getLength()>self.maxlen: - self.maxlen=rule.getLength() - - c=f.read(1) - f.seek(-1, 1) - - ents.append(magictype) - #self.types[pri]=ents - if not c: - break - - def match_data(self, data, max_pri=100, min_pri=0): - pris=self.types.keys() - pris.sort(lambda a, b: -cmp(a, b)) - for pri in pris: - #print pri, max_pri, min_pri - if pri>max_pri: - continue - if pri' % self.types - - -# Some well-known types -text = lookup('text', 'plain') -inode_block = lookup('inode', 'blockdevice') -inode_char = lookup('inode', 'chardevice') -inode_dir = lookup('inode', 'directory') -inode_fifo = lookup('inode', 'fifo') -inode_socket = lookup('inode', 'socket') -inode_symlink = lookup('inode', 'symlink') -inode_door = lookup('inode', 'door') -app_exe = lookup('application', 'executable') - -_cache_uptodate = False - -def _cache_database(): - global exts, globs, literals, magic, _cache_uptodate - - _cache_uptodate = True - - exts = {} # Maps extensions to types - globs = [] # List of (glob, type) pairs - literals = {} # Maps liternal names to types - magic = MagicDB() - - def _import_glob_file(path): - """Loads name matching information from a MIME directory.""" - for line in file(path): - if line.startswith('#'): continue - line = line[:-1] - - type_name, pattern = line.split(':', 1) - mtype = lookup(type_name) - - if pattern.startswith('*.'): - rest = pattern[2:] - if not ('*' in rest or '[' in rest or '?' in rest): - exts[rest] = mtype - continue - if '*' in pattern or '[' in pattern or '?' in pattern: - globs.append((pattern, mtype)) - else: - literals[pattern] = mtype - - for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'globs')): - _import_glob_file(path) - for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'magic')): - magic.mergeFile(path) - - # Sort globs by length - globs.sort(lambda a, b: cmp(len(b[0]), len(a[0]))) - -def get_type_by_name(path): - """Returns type of file by its name, or None if not known""" - if not _cache_uptodate: - _cache_database() - - leaf = os.path.basename(path) - if leaf in literals: - return literals[leaf] - - lleaf = leaf.lower() - if lleaf in literals: - return literals[lleaf] - - ext = leaf - while 1: - p = ext.find('.') - if p < 0: break - ext = ext[p + 1:] - if ext in exts: - return exts[ext] - ext = lleaf - while 1: - p = ext.find('.') - if p < 0: break - ext = ext[p+1:] - if ext in exts: - return exts[ext] - for (glob, mime_type) in globs: - if fnmatch.fnmatch(leaf, glob): - return mime_type - if fnmatch.fnmatch(lleaf, glob): - return mime_type - return None - -def get_type_by_contents(path, max_pri=100, min_pri=0): - """Returns type of file by its contents, or None if not known""" - if not _cache_uptodate: - _cache_database() - - return magic.match(path, max_pri, min_pri) - -def get_type_by_data(data, max_pri=100, min_pri=0): - """Returns type of the data""" - if not _cache_uptodate: - _cache_database() - - return magic.match_data(data, max_pri, min_pri) - -def get_type(path, follow=1, name_pri=100): - """Returns type of file indicated by path. - path - pathname to check (need not exist) - follow - when reading file, follow symbolic links - name_pri - Priority to do name matches. 100=override magic""" - if not _cache_uptodate: - _cache_database() - - try: - if follow: - st = os.stat(path) - else: - st = os.lstat(path) - except: - t = get_type_by_name(path) - return t or text - - if stat.S_ISREG(st.st_mode): - t = get_type_by_contents(path, min_pri=name_pri) - if not t: t = get_type_by_name(path) - if not t: t = get_type_by_contents(path, max_pri=name_pri) - if t is None: - if stat.S_IMODE(st.st_mode) & 0111: - return app_exe - else: - return text - return t - elif stat.S_ISDIR(st.st_mode): return inode_dir - elif stat.S_ISCHR(st.st_mode): return inode_char - elif stat.S_ISBLK(st.st_mode): return inode_block - elif stat.S_ISFIFO(st.st_mode): return inode_fifo - elif stat.S_ISLNK(st.st_mode): return inode_symlink - elif stat.S_ISSOCK(st.st_mode): return inode_socket - return inode_door - -def install_mime_info(application, package_file): - """Copy 'package_file' as ~/.local/share/mime/packages/.xml. - If package_file is None, install /.xml. - If already installed, does nothing. May overwrite an existing - file with the same name (if the contents are different)""" - application += '.xml' - - new_data = file(package_file).read() - - # See if the file is already installed - package_dir = os.path.join('mime', 'packages') - resource = os.path.join(package_dir, application) - for x in xdg.BaseDirectory.load_data_paths(resource): - try: - old_data = file(x).read() - except: - continue - if old_data == new_data: - return # Already installed - - global _cache_uptodate - _cache_uptodate = False - - # Not already installed; add a new copy - # Create the directory structure... - new_file = os.path.join(xdg.BaseDirectory.save_data_path(package_dir), application) - - # Write the file... - file(new_file, 'w').write(new_data) - - # Update the database... - command = 'update-mime-database' - if os.spawnlp(os.P_WAIT, command, command, xdg.BaseDirectory.save_data_path('mime')): - os.unlink(new_file) - raise Exception("The '%s' command returned an error code!\n" \ - "Make sure you have the freedesktop.org shared MIME package:\n" \ - "http://standards.freedesktop.org/shared-mime-info/") % command diff --git a/xdg/RecentFiles.py b/xdg/RecentFiles.py deleted file mode 100644 index 6c2cd85..0000000 --- a/xdg/RecentFiles.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -Implementation of the XDG Recent File Storage Specification Version 0.2 -http://standards.freedesktop.org/recent-file-spec -""" - -import xml.dom.minidom, xml.sax.saxutils -import os, time, fcntl -from xdg.Exceptions import * - -class RecentFiles: - def __init__(self): - self.RecentFiles = [] - self.filename = "" - - def parse(self, filename=None): - if not filename: - filename = os.path.join(os.getenv("HOME"), ".recently-used") - - try: - doc = xml.dom.minidom.parse(filename) - except IOError: - raise ParsingError('File not found', filename) - except xml.parsers.expat.ExpatError: - raise ParsingError('Not a valid .menu file', filename) - - self.filename = filename - - for child in doc.childNodes: - if child.nodeType == xml.dom.Node.ELEMENT_NODE: - if child.tagName == "RecentFiles": - for recent in child.childNodes: - if recent.nodeType == xml.dom.Node.ELEMENT_NODE: - if recent.tagName == "RecentItem": - self.__parseRecentItem(recent) - - self.sort() - - def __parseRecentItem(self, item): - recent = RecentFile() - self.RecentFiles.append(recent) - - for attribute in item.childNodes: - if attribute.nodeType == xml.dom.Node.ELEMENT_NODE: - if attribute.tagName == "URI": - recent.URI = attribute.childNodes[0].nodeValue - elif attribute.tagName == "Mime-Type": - recent.MimeType = attribute.childNodes[0].nodeValue - elif attribute.tagName == "Timestamp": - recent.Timestamp = attribute.childNodes[0].nodeValue - elif attribute.tagName == "Private": - recent.Prviate = True - elif attribute.tagName == "Groups": - - for group in attribute.childNodes: - if group.nodeType == xml.dom.Node.ELEMENT_NODE: - if group.tagName == "Group": - recent.Groups.append(group.childNodes[0].nodeValue) - - def write(self, filename=None): - if not filename and not self.filename: - raise ParsingError('File not found', filename) - elif not filename: - filename = self.filename - - f = open(filename, "w") - fcntl.lockf(f, fcntl.LOCK_EX) - f.write('\n') - f.write("\n") - - for r in self.RecentFiles: - f.write(" \n") - f.write(" %s\n" % xml.sax.saxutils.escape(r.URI)) - f.write(" %s\n" % r.MimeType) - f.write(" %s\n" % r.Timestamp) - if r.Private == True: - f.write(" \n") - if len(r.Groups) > 0: - f.write(" \n") - for group in r.Groups: - f.write(" %s\n" % group) - f.write(" \n") - f.write(" \n") - - f.write("\n") - fcntl.lockf(f, fcntl.LOCK_UN) - f.close() - - def getFiles(self, mimetypes=None, groups=None, limit=0): - tmp = [] - i = 0 - for item in self.RecentFiles: - if groups: - for group in groups: - if group in item.Groups: - tmp.append(item) - i += 1 - elif mimetypes: - for mimetype in mimetypes: - if mimetype == item.MimeType: - tmp.append(item) - i += 1 - else: - if item.Private == False: - tmp.append(item) - i += 1 - if limit != 0 and i == limit: - break - - return tmp - - def addFile(self, item, mimetype, groups=None, private=False): - # check if entry already there - if item in self.RecentFiles: - index = self.RecentFiles.index(item) - recent = self.RecentFiles[index] - else: - # delete if more then 500 files - if len(self.RecentFiles) == 500: - self.RecentFiles.pop() - # add entry - recent = RecentFile() - self.RecentFiles.append(recent) - - recent.URI = item - recent.MimeType = mimetype - recent.Timestamp = int(time.time()) - recent.Private = private - recent.Groups = groups - - self.sort() - - def deleteFile(self, item): - if item in self.RecentFiles: - self.RecentFiles.remove(item) - - def sort(self): - self.RecentFiles.sort() - self.RecentFiles.reverse() - - -class RecentFile: - def __init__(self): - self.URI = "" - self.MimeType = "" - self.Timestamp = "" - self.Private = False - self.Groups = [] - - def __cmp__(self, other): - return cmp(self.Timestamp, other.Timestamp) - - def __eq__(self, other): - if self.URI == str(other): - return True - else: - return False - - def __str__(self): - return self.URI diff --git a/xdg/__init__.py b/xdg/__init__.py deleted file mode 100644 index 870cd00..0000000 --- a/xdg/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ]