pluginsort: probably fix disappearing icons on some plugins
[enigma2-plugins.git] / pluginsort / src / plugin.py
1 from . import _
2
3 # Plugin definition
4 from Plugins.Plugin import PluginDescriptor
5
6 from Screens import PluginBrowser
7 from Screens.MessageBox import MessageBox
8 from Screens.ChoiceBox import ChoiceBox
9 from Components.PluginComponent import PluginComponent, plugins
10 from Components.PluginList import PluginEntryComponent
11 from Tools.Directories import resolveFilename, fileExists, SCOPE_SKIN_IMAGE, SCOPE_PLUGINS
12 from Tools.BoundFunction import boundFunction
13 from Screens.InfoBarGenerics import InfoBarPlugins
14
15 from Components.ActionMap import ActionMap, NumberActionMap
16 from operator import attrgetter # python 2.5+
17
18 from Components.MultiContent import MultiContentEntryText, MultiContentEntryPixmapAlphaTest
19
20 from enigma import eListboxPythonMultiContent, gFont
21 from Tools.LoadPixmap import LoadPixmap
22
23 from xml.etree.cElementTree import parse as cet_parse
24 try:
25         from xml.etree.cElementTree import ParseError
26 except ImportError, ie:
27         ParseError = SyntaxError
28
29 from shutil import copyfile, Error
30
31 XML_CONFIG = "/etc/enigma2/pluginsort.xml"
32 DEBUG = False
33
34 def SelectedPluginEntryComponent(plugin):
35         if plugin.icon is None:
36                 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "skin_default/icons/plugin.png"))
37         else:
38                 png = plugin.icon
39
40         return [
41                 plugin,
42                 MultiContentEntryText(pos=(0,0), size=(440, 50), backcolor_sel=8388608),
43                 MultiContentEntryText(pos=(120, 5), size=(320, 25), font=0, text=plugin.name),
44                 MultiContentEntryText(pos=(120, 26), size=(320, 17), font=1, text=plugin.description),
45                 MultiContentEntryPixmapAlphaTest(pos=(10, 5), size=(100, 40), png = png),
46         ]
47
48 WHEREMAP = {}
49 pdict = PluginDescriptor.__dict__
50 for where in pdict:
51         if where.startswith('WHERE_'):
52                 WHEREMAP[where] = pdict[where]
53 del pdict
54 reverse = lambda map: dict(zip(map.values(), map.keys()))
55
56 class PluginWeights:
57         def __init__(self):
58                 self.plugins = {}
59                 self.load()
60
61         def load(self):
62                 if not fileExists(XML_CONFIG):
63                         return
64
65                 try:
66                         config = cet_parse(XML_CONFIG).getroot()
67                 except ParseError, pe:
68                         from time import time
69                         print "[PluginSort] Parse Error occured in configuration, backing it up and starting from scratch!"
70                         try:
71                                 copyfile(XML_CONFIG, "/etc/enigma2/pluginsort.xml.%d" % (int(time()),))
72                         except Error, she:
73                                 print "[PluginSort] Uh oh, failed to create the backup... I hope you have one anyway :D"
74                         return
75
76                 for wheresection in config.findall('where'):
77                         where = wheresection.get('type')
78                         whereid = WHEREMAP.get(where, None)
79                         whereplugins = wheresection.findall('plugin')
80                         if whereid is None or not whereplugins:
81                                 print "[PluginSort] Ignoring section %s because of invalid id (%s) or no plugins (%s)" % (where, repr(whereid), repr(whereplugins))
82                                 continue
83
84                         for plugin in whereplugins:
85                                 name = plugin.get('name')
86                                 try:
87                                         weight = int(plugin.get('weight'))
88                                 except ValueError, ve:
89                                         print "[PluginSort] Invalid weight of %s received for plugin %s, ignoring" % (repr(plugin.get('weight')), repr(name))
90                                 else:
91                                         self.plugins.setdefault(whereid, {})[name] = weight
92
93         def save(self):
94                 list = ['<?xml version="1.0" ?>\n<pluginsort>\n\n']
95                 append = list.append
96                 extend = list.extend
97
98                 idmap = reverse(WHEREMAP)
99                 for key in self.plugins.keys():
100                         whereplugins = self.plugins.get(key, None)
101                         if not whereplugins:
102                                 continue
103
104                         where = idmap[key]
105                         extend((' <where type="', str(where), '">\n'))
106                         for key, value in whereplugins.iteritems():
107                                 extend(('  <plugin name="', str(key), '" weight="', str(value), '" />\n'))
108                         append((' </where>\n'))
109                 append('\n</pluginsort>\n')
110                 
111                 file = open(XML_CONFIG, 'w')
112                 file.writelines(list)
113                 file.close()
114
115         def get(self, plugin):
116                 for x in plugin.where:
117                         whereplugins = self.plugins.get(x, None)
118                         weight = whereplugins and whereplugins.get(plugin.name, None)
119                         if weight is not None:
120                                 return weight
121                 return plugin.weight
122
123         def set(self, plugin):
124                 for x in plugin.where:
125                         whereplugins = self.plugins.get(x, None)
126                         if whereplugins:
127                                 whereplugins[plugin.name] = plugin.weight
128                         else:
129                                 self.plugins[x] = {plugin.name: plugin.weight}
130
131 pluginWeights = PluginWeights()
132
133 def PluginComponent_addPlugin(self, plugin, *args, **kwargs):
134         if len(plugin.where) > 1:
135                 print "[PluginSort] Splitting %s up in individual entries (%s)" % (plugin.name, repr(plugin.where))
136                 for x in plugin.where:
137                         pd = PluginDescriptor(name=plugin.name, where=[x], description=plugin.description, icon=plugin.icon, fnc=plugin.__call__, wakeupfnc=plugin.wakeupfnc, needsRestart=plugin.needsRestart, internal=plugin.internal, weight=plugin.weight)
138
139                         newWeight = pluginWeights.get(pd)
140                         if DEBUG: print "[PluginSort] Setting weight of %s from %d to %d" % (pd.name, pd.weight, newWeight)
141                         pd.weight = newWeight
142                         PluginComponent.pluginSort_baseAddPlugin(self, pd, *args, **kwargs)
143
144                 # installedPluginList is a list of original descriptors, but we changed it to be a copy, not a reference. so keep it up to date
145                 if self.firstRun:
146                         self.installedPluginList.append(plugin)
147                         if DEBUG: print "[PluginSort] Adding %s to list of installed plugins (%s, %s)." % (plugin.name, plugin.path, repr(plugin.where))
148                 return
149
150         newWeight = pluginWeights.get(plugin)
151         if DEBUG: print "[PluginSort] Setting weight of %s from %d to %d" % (plugin.name, plugin.weight, newWeight)
152         plugin.weight = newWeight
153         PluginComponent.pluginSort_baseAddPlugin(self, plugin, *args, **kwargs)
154
155         if self.firstRun:
156                 if DEBUG: print "[PluginSort] Adding %s to list of installed plugins (%s, %s)." % (plugin.name, plugin.path, repr(plugin.where))
157                 self.installedPluginList.append(plugin)
158
159 if DEBUG:
160         def PluginComponent_removePlugin(self, plugin, *args, **kwargs):
161                 print "[PluginSort] Supposed to remove plugin: %s (%s, %s)." % (plugin.name, plugin.path, repr(plugin.where))
162                 try:
163                         PluginComponent.pluginSort_baseRemovePlugin(self, plugin, *args, **kwargs)
164                 except ValueError, ve:
165                         revMap = reverse(WHEREMAP)
166                         print "-"*40
167                         print "-"*40
168                         print "-"*40
169                         print "[PluginSort] pluginList: %s" % (repr([(x.name, x.path, repr([revMap[y] for y in x.where])) for x in self.pluginList]),)
170                         for w in plugin.where:
171                                 print "[PluginSort] plugins[%s]: %s" % (revMap[w], repr([(x.name, x.path, repr([revMap[y] for y in x.where])) for x in self.plugins[w]]))
172         PluginComponent.pluginSort_baseRemovePlugin = PluginComponent.removePlugin
173         PluginComponent.removePlugin = PluginComponent_removePlugin
174
175 OriginalPluginBrowser = PluginBrowser.PluginBrowser
176 class SortingPluginBrowser(OriginalPluginBrowser):
177         def __init__(self, *args, **kwargs):
178                 self.movemode = False
179                 self.selected = -1
180                 if 'where' in kwargs:
181                         self.where = kwargs['where']
182                         del kwargs['where']
183                 else:
184                         self.where = PluginDescriptor.WHERE_PLUGINMENU
185
186                 OriginalPluginBrowser.__init__(self, *args, **kwargs)
187                 self.skinName = ["SortingPluginBrowser", "PluginBrowser"]
188
189                 self["ColorActions"] = ActionMap(["ColorActions"],
190                         {
191                                 "green": self.toggleMoveMode,
192                         }, -2
193                 )
194                 self["ColorActions"].setEnabled(False)
195
196                 self["WizardActions"] = ActionMap(["WizardActions"],
197                         {
198                                 "left": boundFunction(self.doMove, self["list"].pageUp),
199                                 "right": boundFunction(self.doMove, self["list"].pageDown),
200                                 "up": boundFunction(self.doMove, self["list"].up),
201                                 "down": boundFunction(self.doMove, self["list"].down),
202                         }, -2
203                 )
204
205                 if self.where != PluginDescriptor.WHERE_PLUGINMENU:
206                         self.toggleMoveMode()
207                         self.onShow.append(self.setCustomTitle)
208                 else:
209                         self["NumberActions"] = NumberActionMap(["NumberActions"],
210                                 {
211                                         "1": self.keyNumberGlobal,
212                                         "2": self.keyNumberGlobal,
213                                         "3": self.keyNumberGlobal,
214                                         "4": self.keyNumberGlobal,
215                                         "5": self.keyNumberGlobal,
216                                         "6": self.keyNumberGlobal,
217                                         "7": self.keyNumberGlobal,
218                                         "8": self.keyNumberGlobal,
219                                         "9": self.keyNumberGlobal,
220                                         "0": self.keyNumberGlobal,
221                                 }, -2
222                         )
223
224                         self["MenuActions"] = ActionMap(["MenuActions"],
225                                 {
226                                         "menu": self.openMenu,
227                                 }, -1
228                         )
229
230         def setCustomTitle(self):
231                 titleMap = {
232                         PluginDescriptor.WHERE_EXTENSIONSMENU: _("Sort Extensions"),
233                         PluginDescriptor.WHERE_MOVIELIST: _("Sort MovieList Extensions"),
234                         PluginDescriptor.WHERE_EVENTINFO: _("Sort EventInfo Extensions"),
235                 }
236                 title = titleMap.get(self.where, None)
237                 if title:
238                         self.setTitle(title)
239
240         def keyNumberGlobal(self, number):
241                 if not self.movemode:
242                         realnumber = (number - 1) % 10
243                         if realnumber < len(self.list):
244                                 self["list"].moveToIndex(realnumber)
245                                 self.save()
246
247         def close(self, *args, **kwargs):
248                 if self.movemode:
249                         self.toggleMoveMode()
250                 OriginalPluginBrowser.close(self, *args, **kwargs)
251
252         # copied from PluginBrowser because we redo pretty much anything :-)
253         def updateList(self):
254                 self.pluginlist = plugins.getPlugins(self.where)
255                 if self.where in (PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU):
256                         self.pluginlist.sort(key=attrgetter('weight', 'name')) # sort first by weight, then by name; we get pretty much a weight sorted but otherwise random list
257                 else: #if self.where in (PluginDescriptor.WHERE_EVENTINFO, PluginDescriptor.WHERE_MOVIELIST):
258                         self.pluginlist.sort(key=attrgetter('weight'))
259                 self.list = [PluginEntryComponent(plugin) for plugin in self.pluginlist]
260                 self["list"].l.setList(self.list)
261                 if self.where == PluginDescriptor.WHERE_PLUGINMENU:
262                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
263                                 # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
264                                 self["red"].setText(_("Manage extensions"))
265                                 self["green"].setText(_("Sort") if not self.movemode else _("End Sort"))
266                                 self["SoftwareActions"].setEnabled(True)
267                                 self["PluginDownloadActions"].setEnabled(False)
268                                 self["ColorActions"].setEnabled(True)
269                         else:
270                                 # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
271                                 self["red"].setText(_("Remove Plugins"))
272                                 # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
273                                 self["green"].setText(_("Download Plugins"))
274                                 self["SoftwareActions"].setEnabled(False)
275                                 self["PluginDownloadActions"].setEnabled(True)
276                                 self["ColorActions"].setEnabled(False)
277                 else:
278                         self["red"].setText("")
279                         self["green"].setText(_("Sort") if not self.movemode else _("End Sort"))
280                         self["SoftwareActions"].setEnabled(False)
281                         self["PluginDownloadActions"].setEnabled(False)
282                         self["ColorActions"].setEnabled(True)
283
284         def doMove(self, func):
285                 if self.selected != -1:
286                         oldpos = self["list"].getSelectedIndex()
287                         func()
288                         entry = self.list.pop(oldpos)
289                         newpos = self["list"].getSelectedIndex()
290                         self.list.insert(newpos, entry)
291                         self["list"].l.setList(self.list)
292                 else:
293                         func()
294
295         def save(self):
296                 selected = self.selected
297                 if not self.movemode:
298                         OriginalPluginBrowser.save(self)
299                 elif selected != -1:
300                         Len = len(self.pluginlist)
301                         newpos = self["list"].getSelectedIndex()
302                         entry = self.pluginlist[selected]
303                         self.pluginlist.remove(entry)
304                         self.pluginlist.insert(newpos, entry)
305
306                         # we moved up, increase weight of plugins after us
307                         if newpos < selected:
308                                 print "[PluginSort]", entry.name, "moved up"
309                                 i = newpos + 1
310                                 # since we moved up, there has to be an entry after this one
311                                 diff = abs(self.pluginlist[i].weight - self.pluginlist[newpos].weight) + 1
312                                 print "[PluginSort] Using weight from %d (%d) and %d (%d) to calculate diff (%d)" % (i, self.pluginlist[i].weight, newpos, self.pluginlist[newpos].weight, diff)
313                                 while i < Len:
314                                         if DEBUG: print "[PluginSort] INCREASE WEIGHT OF", self.pluginlist[i].name, "BY", diff
315                                         self.pluginlist[i].weight += diff
316                                         i += 1
317                         # we moved down, decrease weight of plugins before us
318                         elif newpos > selected:
319                                 print "[PluginSort]", entry.name, "moved down"
320                                 i = newpos - 1
321                                 # since we moved up, there has to be an entry before this one
322                                 diff = abs(self.pluginlist[newpos].weight - self.pluginlist[i].weight) + 1
323                                 print "[PluginSort] Using weight from %d (%d) and %d (%d) to calculate diff (%d)" % (newpos, self.pluginlist[newpos].weight, i, self.pluginlist[i].weight, diff)
324                                 while i > -1:
325                                         if DEBUG: print "[PluginSort] DECREASE WEIGHT OF", self.pluginlist[i].name, "BY", diff
326                                         self.pluginlist[i].weight -= diff
327                                         i -= 1
328                         else:
329                                 if DEBUG: print "[PluginSort]", entry.name, "did not move (%d to %d)?" % (selected, newpos)
330
331                         self.list = [PluginEntryComponent(plugin) for plugin in self.pluginlist]
332                         if DEBUG: print "[PluginSort] NEW LIST:", [(plugin.name, plugin.weight) for plugin in self.pluginlist]
333                         self["list"].l.setList(self.list)
334                         self.selected = -1
335                 else:
336                         self.selected = self["list"].getSelectedIndex()
337                         self.list[self.selected] = SelectedPluginEntryComponent(self.pluginlist[self.selected])
338                         self["list"].l.setList(self.list)
339         
340         def openMenu(self):
341                 if self.movemode:
342                         # TRANSLATORS: there is no need to translate this string, as it was reused from e2 core
343                         moveString = _("disable move mode")
344                 else:
345                         # TRANSLATORS: there is no need to translate this string, as it was reused from e2 core
346                         moveString = _("enable move mode")
347
348                 list = [
349                         (moveString, self.toggleMoveMode),
350                         (_("move extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_EXTENSIONSMENU)),
351                         (_("move movie extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_MOVIELIST)),
352                         (_("move event extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_EVENTINFO)),
353                 ]
354
355                 if fileExists(resolveFilename(SCOPE_PLUGINS, "Extensions/PluginHider/plugin.py")):
356                         list.insert(0, (_("hide selected plugin"), self.hidePlugin))
357
358                 self.session.openWithCallback(
359                         self.menuCallback,
360                         ChoiceBox,
361                         list = list,
362                 )
363
364         def menuCallback(self, ret):
365                 ret and ret[1]()
366
367         def openMover(self, where):
368                 self.session.open(SortingPluginBrowser, where=where)
369
370         def hidePlugin(self):
371                 try:
372                         from Plugins.Extensions.PluginHider.plugin import hidePlugin
373                 except Exception, e:
374                         self.session.open(MessageBox, _("Unable to load PluginHider"), MessageBox.TYPE_ERROR)
375                 else:
376                         hidePlugin(self["list"].l.getCurrentSelection()[0])
377
378                         # we were actually in move mode, so save the current position
379                         if self.selected != -1:
380                                 self.save()
381                         self.updateList()
382
383         def toggleMoveMode(self):
384                 if self.movemode:
385                         if self.selected != -1:
386                                 self.save()
387                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
388                                 self["green"].setText(_("Sort"))
389
390                         for plugin in self.pluginlist:
391                                 pluginWeights.set(plugin)
392                         pluginWeights.save()
393
394                         # auto-close if not "PluginBrowser"
395                         if self.where != PluginDescriptor.WHERE_PLUGINMENU:
396                                 self.movemode = False
397                                 return self.close()
398                 else:
399                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
400                                 self["green"].setText(_("End Sort"))
401                 self.movemode = not self.movemode
402
403 def autostart(reason, *args, **kwargs):
404         if reason == 0:
405                 if hasattr(PluginComponent, 'pluginSort_baseAddPlugin'):
406                         print "[PluginSort] Something went wrong as our autostart handler was called multiple times for startup, printing traceback and ignoring."
407                         import traceback, sys
408                         traceback.print_stack(limit=5, file=sys.stdout)
409                 else:
410                         PluginComponent.pluginSort_baseAddPlugin = PluginComponent.addPlugin
411                         PluginComponent.addPlugin = PluginComponent_addPlugin
412
413                         # we use a copy for installed plugins because we might change the 'where'-lists
414                         plugins.installedPluginList = plugins.pluginList[:]
415                         def PluginComponent__setattr__(self, key, value):
416                                 if key == 'installedPluginList': return
417                                 else: self.__dict__[key] = value
418                         PluginComponent.__setattr__ = PluginComponent__setattr__
419
420                         if hasattr(plugins, 'PluginComponent.pluginHider_baseGetPlugins'):
421                                 pluginlist = plugins.pluginHider_baseGetPlugins([PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU, PluginDescriptor.WHERE_MOVIELIST, PluginDescriptor.WHERE_EVENTINFO])
422                         else:
423                                 pluginlist = plugins.getPlugins([PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU, PluginDescriptor.WHERE_MOVIELIST, PluginDescriptor.WHERE_EVENTINFO])
424
425                         # "fix" weight of plugins already added to list, future ones will be fixed automatically
426                         fixed = []
427                         for plugin in pluginlist:
428                                 if plugin in fixed: continue # skip double entries
429
430                                 # create individual entries for multiple wheres, this is potentially harmful!
431                                 if len(plugin.where) > 1:
432                                         # remove all entries except for a potential autostart one (highly unlikely to mix autostart with one of the above, but you never know :D)
433                                         if PluginDescriptor.WHERE_AUTOSTART in plugin.where:
434                                                 plugin.where.remove(PluginDescriptor.WHERE_AUTOSTART)
435                                                 hadAutostart = True
436                                         else:
437                                                 hadAutostart = False
438                                         plugins.removePlugin(plugin)
439                                         plugins.addPlugin(plugin) # this is our own addPlugin now, which automatically creates copies
440
441                                         # HACK: re-add autostart entry to internal list inside PluginComponent
442                                         if hadAutostart:
443                                                 plugin.where = [ PluginDescriptor.WHERE_AUTOSTART ]
444                                                 plugins.pluginList.append(plugin)
445
446                                 # we're keeping the entry, just fix the weight
447                                 else:
448                                         newWeight = pluginWeights.get(plugin)
449                                         print "[PluginSort] Fixing weight for %s (was %d, now %d)" % (plugin.name, plugin.weight, newWeight)
450                                         plugin.weight = newWeight
451
452                                 fixed.append(plugin)
453
454                         # let movieepg fix extensions list sorting if installed, else do this ourselves
455                         if not fileExists(resolveFilename(SCOPE_PLUGINS, "Extensions/MovieEPG/plugin.py")):
456                                 def InfoBarPlugins_getPluginList(self, *args, **kwargs):
457                                         l = InfoBarPlugins.pluginSort_baseGetPluginList(self, *args, **kwargs)
458                                         try:
459                                                 l.sort(key=lambda e: (e[0][1].args[0].weight, e[2]))
460                                         except Exception, e:
461                                                 print "[PluginSort] Failed to sort extensions", e
462                                         return l
463
464                                 InfoBarPlugins.pluginSort_baseGetPluginList = InfoBarPlugins.getPluginList
465                                 InfoBarPlugins.getPluginList = InfoBarPlugins_getPluginList
466
467
468                         PluginBrowser.PluginBrowser = SortingPluginBrowser
469         else:
470                 if hasattr(PluginComponent, 'pluginSort_baseAddPlugin'):
471                         PluginComponent.addPlugin = PluginComponent.pluginSort_baseAddPlugin
472                         del PluginComponent.pluginSort_baseAddPlugin
473                 if hasattr(InfoBarPlugins, 'pluginSort_baseGetPluginList'):
474                         InfoBarPlugins.getPluginList = InfoBarPlugins.pluginSort_baseGetPluginList
475                         del InfoBarPlugins.pluginSort_baseGetPluginList
476                 PluginBrowser.PluginBrowser = OriginalPluginBrowser
477
478 def Plugins(**kwargs):
479         return [
480                 PluginDescriptor(
481                         where=PluginDescriptor.WHERE_AUTOSTART,
482                         fnc=autostart,
483                         needsRestart=True,
484                 ),
485         ]