From 58ee04611bdfe57541655b280407baa4ac69db5b Mon Sep 17 00:00:00 2001 From: Stefanos Harhalakis Date: Sun, 20 Jun 2010 21:41:29 +0000 Subject: [PATCH] --- apps.py | 87 ++++ config.py | 30 ++ launcher.py | 49 +++ widget.py | 422 ++++++++++++++++++++ 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 + 16 files changed, 4267 insertions(+) create mode 100755 apps.py create mode 100755 config.py create mode 100755 launcher.py create mode 100755 widget.py create mode 100644 xdg/BaseDirectory.py create mode 100644 xdg/Config.py create mode 100644 xdg/DesktopEntry.py create mode 100644 xdg/Exceptions.py create mode 100644 xdg/IconTheme.py create mode 100644 xdg/IniFile.py create mode 100644 xdg/Locale.py create mode 100644 xdg/Menu.py create mode 100644 xdg/MenuEditor.py create mode 100644 xdg/Mime.py create mode 100644 xdg/RecentFiles.py create mode 100644 xdg/__init__.py diff --git a/apps.py b/apps.py new file mode 100755 index 0000000..8b0f9a8 --- /dev/null +++ b/apps.py @@ -0,0 +1,87 @@ +#!/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 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, + } + for line in f: + line=line.strip() + if 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) + + 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 + dt=readOneFn(f) + t=f[:-8] + ret[t]=dt + + return(ret) + +if __name__=="__main__": + print scan() + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/config.py b/config.py new file mode 100755 index 0000000..005de06 --- /dev/null +++ b/config.py @@ -0,0 +1,30 @@ +#!/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 $" + +size = 4 +iconsize = 64 +iconspace = 42 + +# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent: + diff --git a/launcher.py b/launcher.py new file mode 100755 index 0000000..dacde19 --- /dev/null +++ b/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/widget.py b/widget.py new file mode 100755 index 0000000..0a7573d --- /dev/null +++ b/widget.py @@ -0,0 +1,422 @@ +#!/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 + + +class Icons: + def __init__(self): + self.mode='l' + + + def load(self): + x=0 + y=0 + fn=["maegirls", "wifieye"]*3 + for f in fn: + dt=apps.readOne(f) + ico=getIconPath(dt['icon'], config.iconsize) + print "ico:", ico + p=gtk.gdk.pixbuf_new_from_file_at_size(ico, config.iconsize, + config.iconsize) + print x, y + #dt={'icon2': p} + dt['icon2']=p + print x, y, dt + self.icons[x][y].setApp(dt) + x+=1 + if x>=config.size: + x=0 + y+=1 +# self.icons.append(p) + + print "end of Icons init" + + def setMode(self, mode): + self.mode=mode + +class Icon: + def __init__(self): + 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 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'] + + def draw(self, cr, x, y, mode): + self.x=x + self.y=y + + if self.icon==None: + 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.paint() + cr.restore() + + 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() + + 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): + # 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) + + def doLaunch(self): + print "launch:", self.name + launcher.launch(self.name) + + def doTrippleClick(self): + print "tripple" + aps=apps.scan() + + lst=[aps[x]['name'] for x in aps] + lst.sort() + + # TODO: EDO EDO EDO + # Na doylevei o selector, na dialegei efarmogi kai na + # efarmozetai sto sygkekrimeno eikonidio + # + # Na brethei lysh kai gia ta kena + 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) + + for app in lst: + if app==None: + continue + selector.append_text(app) + + dialog.show_all() + + r=dialog.run() + print "r:", r +# prog=hildon.Program.get_instance() + + print lst + + def doRelease(self): + dt=time.time() - self.lastpress + self.ispressed=False + if dt>self.presstime and dt<2: + self.doLaunch() + elif dt=config.size: + x=0 + y+=1 + + def do_expose_event(self, event): + #print "do_expose" + + cr=self.window.cairo_create() + + cr.rectangle(event.area.x, event.area.y, + event.area.width, event.area.height) + + cr.clip() + + self._draw(cr) + +# HomePluginItem.do_expose_event(self, 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_realize(self): + screen=self.get_screen() + self.set_colormap(screen.get_rgba_colormap()) + self.set_app_paintable(True) + + HomePluginItem.do_realize(self) + + def do_button_press_event(self, event): + #print "press", event.type + icon=self.iconAt(event.x, event.y) +# 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" + icon=self.iconAt(event.x, event.y) + icon.doRelease() + icon.invalidate(self.window) + self.setLastIcon(None) + return(True) + + def do_leave_notify_event(self, event): + #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) + icon.doCancel() + icon.invalidate(self.window) + return(True) + + def do_motion_notify_event(self, event): + #print "motion" + self.setLastIcon(None) +# icon=self.iconAt(event.x, event.y) +# 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 on_orientation_changed(self, orientation): + print "orch:", orientation + o=orientation[0] + self.setMode(o) + self.queue_draw() + +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/xdg/BaseDirectory.py b/xdg/BaseDirectory.py new file mode 100644 index 0000000..6f532c9 --- /dev/null +++ b/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/xdg/Config.py b/xdg/Config.py new file mode 100644 index 0000000..e2fbe64 --- /dev/null +++ b/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/xdg/DesktopEntry.py b/xdg/DesktopEntry.py new file mode 100644 index 0000000..8626d7f --- /dev/null +++ b/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/xdg/Exceptions.py b/xdg/Exceptions.py new file mode 100644 index 0000000..f7d08be --- /dev/null +++ b/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/xdg/IconTheme.py b/xdg/IconTheme.py new file mode 100644 index 0000000..1f1fa18 --- /dev/null +++ b/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/xdg/IniFile.py b/xdg/IniFile.py new file mode 100644 index 0000000..f3f08c7 --- /dev/null +++ b/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/xdg/Locale.py b/xdg/Locale.py new file mode 100644 index 0000000..d30d91a --- /dev/null +++ b/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/xdg/Menu.py b/xdg/Menu.py new file mode 100644 index 0000000..d437ee4 --- /dev/null +++ b/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/xdg/MenuEditor.py b/xdg/MenuEditor.py new file mode 100644 index 0000000..cc5ce54 --- /dev/null +++ b/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/xdg/Mime.py b/xdg/Mime.py new file mode 100644 index 0000000..04fe0d2 --- /dev/null +++ b/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/xdg/RecentFiles.py b/xdg/RecentFiles.py new file mode 100644 index 0000000..6c2cd85 --- /dev/null +++ b/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/xdg/__init__.py b/xdg/__init__.py new file mode 100644 index 0000000..870cd00 --- /dev/null +++ b/xdg/__init__.py @@ -0,0 +1 @@ +__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] -- 1.7.9.5