home *** CD-ROM | disk | FTP | other *** search
/ PC Welt 2006 November (DVD) / PCWELT_11_2006.ISO / casper / filesystem.squashfs / usr / lib / python2.4 / site-packages / AppInstall / Menu.py < prev    next >
Encoding:
Python Source  |  2006-08-28  |  21.2 KB  |  542 lines

  1. # (c) 2005 Canonical, GPL
  2.  
  3. import pygtk
  4. pygtk.require("2.0")
  5. import gtk
  6. import gtk.gdk
  7. import gobject
  8. import xdg.Menu
  9. import sys
  10. import os
  11. import gettext
  12. import common
  13.  
  14. from warnings import warn
  15. from gettext import gettext as _
  16.  
  17. from Util import *
  18.  
  19. # possible filter states for the application list
  20. ( SHOW_ALL,
  21.   SHOW_ALL_SUPPORTED,
  22.   SHOW_ALL_FREE,
  23.   SHOW_ONLY_MAIN,
  24.   SHOW_ONLY_PROPRIETARY,
  25.   SHOW_ONLY_THIRD_PARTY,
  26.  ) = range(6)
  27.  
  28. class MenuItem(object):
  29.     " base class for a object in the menu "
  30.     def __init__(self, name, iconname=None):
  31.         # the name that is displayed
  32.         self.name = name
  33.         # the icon that is displayed
  34.         self.iconname = iconname
  35.         self.icontheme = None
  36.     def __repr__(self):
  37.         return "MenuItem: %s" % self.name
  38.  
  39. class Category(MenuItem):
  40.     """ represents a category """
  41.     def __init__(self, parent, name, iconname=None):
  42.         MenuItem.__init__(self, name, iconname)
  43.     def initListStores(self, parent):
  44.         self.icontheme = parent.icons
  45.         # if that category has applications, add them to the
  46.         # store here
  47.         self.all_applications = gtk.ListStore(gobject.TYPE_STRING,
  48.                                                gobject.TYPE_PYOBJECT,
  49.                                                gobject.TYPE_INT)
  50.         # set the visible filter
  51.         self.filtered_applications = self.all_applications.filter_new()
  52.         self.filtered_applications.set_visible_func(parent._visible_filter)
  53.         self.applications = gtk.TreeModelSort(self.filtered_applications)
  54.         #self.applications.set_sort_func(SORT_RANK, parent._ranking_sort_func)
  55.  
  56. class Application(MenuItem):
  57.     """ this class represents a application """
  58.     def __init__(self, name, iconname=None):
  59.         MenuItem.__init__(self, name, iconname)
  60.         # the apt-pkg name that belongs to the app
  61.         self.pkgname = None
  62.         # the description
  63.         self.description = ""
  64.         # a html page
  65.         self.html = None
  66.         # mime type
  67.         self.mime = None
  68.         # exec
  69.         self.execCmd = None
  70.         # needsTerminal
  71.         self.needsTerminal = False
  72.         # we have it right now
  73.         self.available = False
  74.         # component the package is in (main, universe, multiverse, restricted)
  75.         self.component = None
  76.         # channel the pacakge is in (e.g. skype)
  77.         self.channel = None
  78.         # states
  79.         self.isInstalled = False
  80.         self.toInstall = False
  81.         self.popcon = 1            # the raw popcon data
  82.         self.rank = 1              # used by the ranking algo
  83.         # licence and support
  84.         self.free = False
  85.         self.licenseUri = None
  86.         self.supported = False
  87.         self.thirdparty = False
  88.         self.architectures = []
  89.         # textual menu path
  90.         self.menupath = ""
  91.  
  92. class ApplicationMenu(object):
  93.     """ this represents the application menu, the interessting bits are:
  94.         - store that can be attached to a TreeView
  95.         - pkg_to_app a dictionary that maps the apt pkgname to the application
  96.                      items
  97.     """
  98.  
  99.     debug = 0
  100.  
  101.     def __init__(self, datadir, cachedir, cache, treeview_categories,
  102.                  treeview_packages, progress, filter=SHOW_ONLY_MAIN,
  103.                  dontPopulate=False):
  104.         self.menudir = datadir+"/desktop"
  105.         self.cache = cache
  106.         self.treeview_categories = treeview_categories
  107.         self.treeview_packages = treeview_packages
  108.         self.popcon_max = 1
  109.         
  110.         # cache
  111.         self.pickle = {}
  112.         
  113.         # icon theme
  114.         self.icons = common.ToughIconTheme()
  115.         self.icons.prepend_search_path(os.path.join(datadir, "icons"))
  116.         # some icon-themes (kde) don't support this icon
  117.         try:
  118.             gtk.window_set_default_icon(self.icons.load_icon("gnome-settings-default-applications", 32, 0))
  119.         except gobject.GError:
  120.             pass
  121.  
  122.         # search
  123.         self.searchTerms = []
  124.         self.mimeSearch = None
  125.         
  126.         # properties for the view
  127.         self.filter = filter
  128.  
  129.         # a dictonary that provides a mapping from a pkg to the
  130.         # application names it provides
  131.         self.pkg_to_app = {}
  132.  
  133.         # a set of seen desktop entries
  134.         self.desktopEntriesSeen = set()
  135.  
  136.         # the categories 
  137.         self.real_categories_store = gtk.ListStore(gobject.TYPE_STRING,
  138.                                                    gobject.TYPE_PYOBJECT)
  139.  
  140.         if dontPopulate:
  141.             return
  142.  
  143.         # populate the tree
  144.         # use cached self.pickle (should be renamed to self.categories)
  145.         # and cache self.pkgs_to_app
  146.         if os.path.exists("%s/menu.p" % cachedir):
  147.             print "using existing cache"
  148.             import cPickle
  149.             self.pickle = cPickle.load(open("%s/menu.p" % cachedir))
  150.         else:
  151.             print "no cache found"
  152.             self.desktopEntriesSeen.clear()
  153.             menu = xdg.Menu.parse(os.path.join(self.menudir, "applications.menu"))
  154.             self._populateFromEntry(menu)
  155.             
  156.         # refresh based on the pickled information
  157.         self.refresh(progress)
  158.         self.store = self.real_categories_store
  159.         
  160.         # default is the tree
  161.         self.treeview_categories.set_model(self.real_categories_store)
  162.         self.treeview_packages.set_model(None)
  163.         
  164.  
  165.     # helpers
  166.     def _refilter(self):
  167.         # we need to disconnect the model from the view when we
  168.         # do a refilter, otherwise we get random crashes in the search
  169.         # (to reproduce:
  170.         #  1. open "accessability" 2. unselect "show unsupported"
  171.         #  3. search for "apt" 4. turn "show unsupported" on/off -> BOOM
  172.         model = self.treeview_packages.get_model()
  173.  
  174.         # save the cursor position (or rather, the name of the app selected)
  175.         name = None
  176.         (path, colum) = self.treeview_packages.get_cursor()
  177.         if path:
  178.             name = model.get_value(model.get_iter(path), COL_NAME)
  179.             #print "found: %s (%s) " % (name, path)
  180.  
  181.         # this is the actual refiltering
  182.         self.treeview_packages.set_model(None)
  183.         if model != None:
  184.             model.get_model().refilter()
  185.         self.treeview_packages.set_model(model)
  186.  
  187.         # recalculate the width of the treeview to avoid useless 
  188.         # scrollbars
  189.         self.treeview_packages.columns_autosize()
  190.  
  191.         # redo the cursor
  192.         if name != None:
  193.             for it in iterate_list_store(model, model.get_iter_first()):
  194.                 aname = model.get_value(it, COL_NAME)
  195.                 if name == aname:
  196.                     #print "selecting: %s (%s)" % (name, model.get_path(it))
  197.                     #self.treeview_packages.expand_to_path(model.get_path(it))
  198.                     self.treeview_packages.set_cursor(model.get_path(it))
  199.                     return
  200.         elif len(model) > 0:
  201.             self.treeview_packages.set_cursor(0)
  202.  
  203.     def _ranking_sort_func(self, model, iter1, iter2):
  204.         """
  205.         Sort by the search result rank
  206.         """
  207.         #print "_sort_func()"
  208.         item1 = model.get_value(iter1, COL_ITEM)
  209.         item2 = model.get_value(iter2, COL_ITEM)
  210.         if item1 == None or item2 == None:
  211.             return 0
  212.         if item1.rank < item2.rank: return 1
  213.         elif item1.rank > item2.rank: return -1
  214.         else: return 0
  215.  
  216.     def _visible_filter(self, model, iter):
  217.         item = model.get_value(iter, COL_ITEM)
  218.         if item:
  219.             # check for the various view settings
  220.             if self.mimeSearch and not self.mimeSearch.approved(
  221.                 item.component, item.pkgname):
  222.         return False
  223.             if self.filter == SHOW_ONLY_MAIN and item.component != "main":
  224.                 return False
  225.             if self.filter == SHOW_ALL_SUPPORTED and item.supported != True:
  226.                 return False
  227.             if self.filter == SHOW_ALL_FREE and item.free == False:
  228.                 return False
  229.             if self.filter == SHOW_ONLY_PROPRIETARY and item.free == True:
  230.                 return False
  231.             if self.filter == SHOW_ONLY_THIRD_PARTY and item.thirdparty != True:
  232.                 return False
  233.             # if we search, do the ranking updates 
  234.             if len(self.searchTerms) > 0:
  235.                  rank = self._filterAndRank(item)
  236.                  if rank == None:
  237.                      return False
  238.                  else:
  239.                      item.rank = rank
  240.         return True
  241.  
  242.     def _filterAndRank(self, item):
  243.         """
  244.         Watch out, Google!
  245.         """
  246.         trigger = ""
  247.         rank = item.popcon
  248.  
  249.         # special case the mime-search
  250.         if self.mimeSearch:
  251.             if self._mimeFilter(item):
  252.                 return rank
  253.             else:
  254.                 return None
  255.  
  256.         # the normal case
  257.         for term in self.searchTerms:
  258.             hit = False
  259.             if term == item.name.lower() or \
  260.                term == item.pkgname.lower():
  261.                 # FIXME: we should probably not use a hardcoded number here
  262.                 #        because the popcon numbers will change over time
  263.                 #        so rather calculate it based on the available numbers
  264.                 rank += 1500
  265.                 hit = True
  266.             if term in item.name.lower():
  267.                 rank += item.popcon * 3
  268.                 trigger += " name"
  269.                 hit = True
  270.             if term in item.pkgname.lower():
  271.                 rank += item.popcon * 3
  272.                 trigger += " pkg_name"
  273.                 hit = True
  274.             if self._mimeMatch(item, term, fuzzy=True):
  275.                 rank += item.popcon * 2
  276.                 trigger += " mime"
  277.                 hit = True
  278.             if term in item.description.lower():
  279.                 rank += item.popcon
  280.                 trigger += " desc"
  281.                 hit = True
  282.             if self.cache.has_key(item.pkgname) and \
  283.                  term in self.cache[item.pkgname].description.lower():
  284.                 rank += item.popcon
  285.                 trigger += " pkg_desc"
  286.                 hit = True
  287.             if hit == False:
  288.                 return None
  289.         #print "found %s (%s/%s): %s" % (item.name, item.popcon, rank, trigger)
  290.         return rank
  291.  
  292.     def _mimeMatch(self, item, term, fuzzy=False):
  293.         for re_pattern in item.mime:
  294.             # mvo: we get a list of regexp from
  295.             # pyxdg.DesktopEntry.getMimeType, but it does not
  296.             # use any special pattern at all, so we use the plain
  297.             # pattern (e.g. text/html, audio/mp3 here)
  298.             pattern = re_pattern.pattern
  299.             if fuzzy and term in pattern:
  300.                 return True
  301.             elif not fuzzy and re_pattern.match(term):
  302.                 return True
  303.         return False
  304.  
  305.     def _mimeFilter(self, item):
  306.         for mime_type in self.searchTerms:
  307.             if self._mimeMatch(item, mime_type):
  308.                 return True
  309.         return False
  310.  
  311.     def doMimeSearch(self, mime_type, fuzzy=False):
  312.         res = set()
  313.         model = self.real_categories_store.get_value(self.all_category_iter, COL_ITEM).all_applications
  314.         for it in iterate_list_store(model, model.get_iter_first()):
  315.             item = model.get_value(it, COL_ITEM)
  316.             for re_pattern in item.mime:
  317.                 # mvo: we get a list of regexp from
  318.                 # pyxdg.DesktopEntry.getMimeType, but it does not
  319.                 # use any special pattern at all, so we use the plain
  320.                 # pattern (e.g. text/html, audio/mp3 here)
  321.                 pattern = re_pattern.pattern
  322.                 if fuzzy and mime_type in pattern:
  323.                     res.add(item)
  324.                 elif not fuzzy and re_pattern.match(mime_type):
  325.                     res.add(item)
  326.         return res
  327.  
  328.     def createMenuCache(self, targetdir):
  329.         self.desktopEntriesSeen.clear()
  330.         self.pkg_to_app.clear()
  331.         menu = xdg.Menu.parse(os.path.abspath(os.path.join(self.menudir, "applications.menu")))
  332.         self._populateFromEntry(menu)
  333.         import pickle
  334.         pickle.dump(self.pickle, open('%s/menu.p' % targetdir,'w'))
  335.  
  336.     def refresh(self, progress):        
  337.         self.real_categories_store.clear()
  338.  
  339.         # add "All" category
  340.         self.all_category_iter = self.real_categories_store.append()
  341.         item = Category(self, "<b>%s</b>" % _("All"), "distributor-logo")
  342.         item.initListStores(self)
  343.         self.real_categories_store.set(self.all_category_iter,
  344.                                        COL_NAME, "<b>%s</b>" % _("All"),
  345.                                        COL_ITEM, item)
  346.  
  347.         # now go for the categories
  348.         i=0
  349.         lenx=len(self.pickle.keys())
  350.         keys = self.pickle.keys()
  351.         keys.sort(cmp=lambda x,y: cmp(x.name.lower(), y.name.lower()))
  352.         for category in keys:
  353.             category.initListStores(self)
  354.             self.real_categories_store.set(self.real_categories_store.append(),
  355.                                            COL_NAME, category.name,
  356.                                            COL_ITEM, category,)
  357.             i += 1
  358.             progress.update(i/float(lenx)*100.0)
  359.             for item in self.pickle[category]:
  360.                 # get name/description again to make sure that i18n is honored
  361.                 item.name = xmlescape(item.desktop_entry.getName())
  362.                 item.description = xmlescape(item.desktop_entry.getComment())
  363.                 # kde uses generic name *grumpf*
  364.                 if not item.description:
  365.                     item.description = xmlescape(item.desktop_entry.get('GenericName'))
  366.                 item.icontheme = self.icons
  367.  
  368.                 # add to category
  369.                 category.all_applications.set(category.all_applications.append(),
  370.                                               COL_NAME, item.name,
  371.                                               COL_ITEM, item,
  372.                                               COL_POPCON, item.popcon)
  373.                 # add to all
  374.                 store = self.real_categories_store.get_value(self.all_category_iter, COL_ITEM).all_applications
  375.                 store.set(store.append(),
  376.                           COL_NAME, item.name,
  377.                           COL_ITEM, item,
  378.                           COL_POPCON, item.popcon)
  379.  
  380.                 # do the popcon_max calculation
  381.                 if item.popcon > self.popcon_max:
  382.                     self.popcon_max = item.popcon
  383.  
  384.                 # populate the pkg_to_app data structure
  385.                 pkgname = item.pkgname
  386.                 if self.pkg_to_app.has_key(pkgname):
  387.                     if not item.name in [pkg.name for pkg in self.pkg_to_app[pkgname]]:
  388.                         self.pkg_to_app[pkgname].append(item)
  389.                 else:
  390.                     self.pkg_to_app[pkgname] = [item]
  391.  
  392.                 # update the installed information
  393.                 if self.cache.has_key(pkgname):
  394.                     item.isInstalled = self.cache[pkgname].isInstalled
  395.                 else:
  396.                     item.isInstalled = False
  397.                 item.toInstall = item.isInstalled
  398.  
  399.  
  400.     def getChanges(self, get_paths=False):
  401.         """ return the selected changes in the tree
  402.             TODO: what is get_paths?
  403.         """
  404.         to_inst = set()
  405.         to_rm = set()
  406.         for (name, item) in self.store:
  407.             for (name,item,popcon) in item.all_applications:
  408.                 if item.isInstalled and not item.toInstall:
  409.                     to_rm.add(item)
  410.                 if not item.isInstalled and item.toInstall:
  411.                     to_inst.add(item)
  412.         #print "to_add: %s" % to_inst
  413.         #print "to_rm: %s" % to_rm
  414.         return (to_inst, to_rm)
  415.         
  416.     def isChanged(self):
  417.         """ check if there are changes at all """
  418.         for (cat_name, cat)  in self.store:
  419.             for (name,item,popcon) in cat.all_applications:
  420.                 if item.toInstall != item.isInstalled:
  421.                     return True
  422.         return False
  423.  
  424.     def _populateFromEntry(self, node, parent = None, progress=None):
  425.         #progress.update((len(self.real_categories_store)/float(progress.allItems))*100)
  426.         # for some reason xdg hiddes some entries, but we don't like that
  427.         for entry in node.getEntries(hidden=True):
  428.             self._dbg(2, "entry: %s" % (entry))
  429.             if isinstance(entry, xdg.Menu.Menu):
  430.                 # we found a toplevel menu
  431.                 name = xmlescape(entry.getName())
  432.                 self._dbg(1, "we have a sub-menu %s " % name)
  433.                 item = Category(self, name, entry.getIcon())
  434.                 #print "adding: %s" % name
  435.                 self.pickle[item] = []
  436.                 self._populateFromEntry(entry, item,  progress=progress)
  437.             elif isinstance(entry, xdg.Menu.MenuEntry):
  438.                 # more debug output
  439.                 self._dbg(3, node.getPath() + "/\t" + entry.DesktopFileID + "\t" + entry.DesktopEntry.getFileName())
  440.  
  441.                 # we found a application
  442.                 name = xmlescape(entry.DesktopEntry.getName())
  443.                 self._dbg(1, "we have a application %s (%s) " % (name,entry.DesktopFileID))
  444.                 if name and entry.DesktopEntry.hasKey("X-AppInstall-Package"):
  445.                     self._dbg(2,"parent is %s" % parent.name)
  446.  
  447.                     # check for duplicates, caused by e.g. scribus that has:
  448.                     #   Categories=Application;Graphics;Qt;Office;
  449.                     # so it appears in Graphics and Office
  450.                     if name in self.desktopEntriesSeen:
  451.                         #print "already seen %s (%s)" % (name, entry)
  452.                         continue
  453.                     self.desktopEntriesSeen.add(name)
  454.  
  455.                     item = Application(name)
  456.                     # save the desktop entry to get the translations back later
  457.                     item.desktop_entry = entry.DesktopEntry
  458.                     pkgname = entry.DesktopEntry.get("X-AppInstall-Package")
  459.                     item.pkgname = pkgname
  460.                     # figure component and support status
  461.                     item.component = entry.DesktopEntry.get("X-AppInstall-Section")
  462.                     supported =  entry.DesktopEntry.get("X-AppInstall-Supported")
  463.                     if supported != "":
  464.                         item.supported = bool(supported)
  465.                     else:
  466.                         if item.component == "main" or \
  467.                                item.component == "restricted":
  468.                             item.supported = True
  469.                     # check for free software
  470.                     if item.component == "main" or item.component == "universe":
  471.                         item.free = True
  472.                     else:
  473.                         item.free = False
  474.                     # check for third party apps
  475.                     item.channel = entry.DesktopEntry.get("X-AppInstall-Channel")
  476.                     thirdparty =  entry.DesktopEntry.get("X-AppInstall-Proprietary")
  477.                     if thirdparty != "":
  478.                         item.thirdparty = bool(thirdparty)
  479.                         item.licenseUri = entry.DesktopEntry.get("X-AppInstall-LicenseUri")
  480.                     if self.cache.has_key(item.pkgname):
  481.                         item.available = True
  482.                     else:
  483.                         item.available = False
  484.                     # Supported architectures
  485.                     archs = entry.DesktopEntry.get("X-AppInstall-Architectures", list=True)
  486.                     if archs:
  487.                         item.architectures.extend(archs)
  488.                     # Icon
  489.                     item.iconname = entry.DesktopEntry.get("X-AppInstall-Icon", "") or entry.DesktopEntry.getIcon()
  490.                     if self.cache.has_key(pkgname):
  491.                         item.isInstalled = self.cache[pkgname].isInstalled
  492.                     else:
  493.                         item.isInstalled = False
  494.                     item.toInstall = item.isInstalled
  495.                     item.mime = entry.DesktopEntry.getMimeType()
  496.                     # popcon data
  497.                     popcon_str = entry.DesktopEntry.get("X-AppInstall-Popcon")
  498.                     if popcon_str != "":
  499.                         popcon = int(popcon_str)
  500.                         item.popcon = popcon
  501.                         if popcon > self.popcon_max:
  502.                             self.popcon_max = popcon
  503.  
  504.                     item.execCmd = entry.DesktopEntry.getExec()
  505.                     item.needsTerminal = entry.DesktopEntry.getTerminal()
  506.                     item.menupath = [_("Applications"),parent.name]
  507.                     #print item.menupath
  508.                     #store.set(store.append(),
  509.                     #          COL_NAME, name,
  510.                     #          COL_ITEM, item)
  511.                     self.pickle[parent].append(item)
  512.                     
  513.                 else:
  514.                     try:
  515.                         print "Got non-package menu entry %s" % entry
  516.                     except UnicodeEncodeError:
  517.                         pass
  518.             elif isinstance(entry, xdg.Menu.Header):
  519.                 print "got header"
  520.  
  521.     def _dbg(self, level, msg):
  522.         """Write debugging output to sys.stderr.
  523.         """
  524.         if level <= self.debug:
  525.             print >> sys.stderr, msg
  526.  
  527.  
  528. if __name__ == "__main__":
  529.     print "testing the menu"
  530.  
  531.     desktopdir = "/usr/share/app-install"
  532.     from Util import MyCache
  533.     cache = MyCache()
  534.     treeview = gtk.TreeView()
  535.     menu = ApplicationMenu(desktopdir, cache, treeview, treeview, apt.progress.OpProgress())
  536.     #matches = menu.doMimeSearch("mp3",fuzzy=True)
  537.     #print matches
  538.     #matches = menu.doMimeSearch("audio/mp3")
  539.     #print matches
  540.  
  541.     
  542.