From 15a43ab3378c0aa46af8ce75b3303d15947ac4a6 Mon Sep 17 00:00:00 2001 From: Stefanos Harhalakis Date: Thu, 24 Jun 2010 15:33:42 +0000 Subject: [PATCH 1/1] Moved into src. --- 0.py | 26 -- apps.py | 119 ----- config.py | 125 ------ icon.py | 215 ---------- icongrid.py | 296 ------------- icons.py | 175 -------- launcher.py | 49 --- portrait.py | 189 -------- src/0.py | 26 ++ src/apps.py | 119 +++++ src/config.py | 125 ++++++ src/icon.py | 215 ++++++++++ src/icongrid.py | 296 +++++++++++++ src/icons.py | 175 ++++++++ src/launcher.py | 49 +++ src/portrait.py | 189 ++++++++ src/widget.py | 141 ++++++ src/win_config.py | 209 +++++++++ src/xdg/BaseDirectory.py | 97 +++++ src/xdg/Config.py | 39 ++ src/xdg/DesktopEntry.py | 397 +++++++++++++++++ src/xdg/Exceptions.py | 51 +++ src/xdg/IconTheme.py | 391 +++++++++++++++++ src/xdg/IniFile.py | 406 ++++++++++++++++++ src/xdg/Locale.py | 79 ++++ src/xdg/Menu.py | 1074 ++++++++++++++++++++++++++++++++++++++++++++++ src/xdg/MenuEditor.py | 511 ++++++++++++++++++++++ src/xdg/Mime.py | 474 ++++++++++++++++++++ src/xdg/RecentFiles.py | 159 +++++++ src/xdg/__init__.py | 1 + widget.py | 141 ------ win_config.py | 209 --------- xdg/BaseDirectory.py | 97 ----- xdg/Config.py | 39 -- xdg/DesktopEntry.py | 397 ----------------- xdg/Exceptions.py | 51 --- xdg/IconTheme.py | 391 ----------------- xdg/IniFile.py | 406 ------------------ xdg/Locale.py | 79 ---- xdg/Menu.py | 1074 ---------------------------------------------- xdg/MenuEditor.py | 511 ---------------------- xdg/Mime.py | 474 -------------------- xdg/RecentFiles.py | 159 ------- xdg/__init__.py | 1 - 44 files changed, 5223 insertions(+), 5223 deletions(-) delete mode 100755 0.py delete mode 100755 apps.py delete mode 100755 config.py delete mode 100755 icon.py delete mode 100755 icongrid.py delete mode 100755 icons.py delete mode 100755 launcher.py delete mode 100644 portrait.py create mode 100755 src/0.py create mode 100755 src/apps.py create mode 100755 src/config.py create mode 100755 src/icon.py create mode 100755 src/icongrid.py create mode 100755 src/icons.py create mode 100755 src/launcher.py create mode 100644 src/portrait.py create mode 100755 src/widget.py create mode 100755 src/win_config.py create mode 100644 src/xdg/BaseDirectory.py create mode 100644 src/xdg/Config.py create mode 100644 src/xdg/DesktopEntry.py create mode 100644 src/xdg/Exceptions.py create mode 100644 src/xdg/IconTheme.py create mode 100644 src/xdg/IniFile.py create mode 100644 src/xdg/Locale.py create mode 100644 src/xdg/Menu.py create mode 100644 src/xdg/MenuEditor.py create mode 100644 src/xdg/Mime.py create mode 100644 src/xdg/RecentFiles.py create mode 100644 src/xdg/__init__.py delete mode 100755 widget.py delete mode 100755 win_config.py delete mode 100644 xdg/BaseDirectory.py delete mode 100644 xdg/Config.py delete mode 100644 xdg/DesktopEntry.py delete mode 100644 xdg/Exceptions.py delete mode 100644 xdg/IconTheme.py delete mode 100644 xdg/IniFile.py delete mode 100644 xdg/Locale.py delete mode 100644 xdg/Menu.py delete mode 100644 xdg/MenuEditor.py delete mode 100644 xdg/Mime.py delete mode 100644 xdg/RecentFiles.py delete mode 100644 xdg/__init__.py 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" ] -- 1.7.9.5