Merge remote-tracking branch 'origin/master'
[enigma2-plugins.git] / pluginsort / src / plugin.py
1 from . import _
2
3 # Plugin definition
4 from Plugins.Plugin import PluginDescriptor
5
6 from Components.config import config, ConfigSubsection, ConfigSet
7 from Screens import PluginBrowser
8 from Screens.MessageBox import MessageBox
9 from Screens.ChoiceBox import ChoiceBox
10 from Components.PluginComponent import PluginComponent, plugins
11 from Components.PluginList import PluginEntryComponent
12 from Tools.Directories import resolveFilename, fileExists, SCOPE_SKIN_IMAGE, SCOPE_PLUGINS
13
14 from Components.ActionMap import ActionMap, NumberActionMap
15 from operator import attrgetter # python 2.5+
16
17 from Components.MultiContent import MultiContentEntryText, MultiContentEntryPixmapAlphaTest
18
19 from enigma import eListboxPythonMultiContent, gFont
20 from Tools.LoadPixmap import LoadPixmap
21
22 from xml.etree.cElementTree import parse as cet_parse
23 try:
24         from xml.etree.cElementTree import ParseError
25 except ImportError, ie:
26         ParseError = SyntaxError
27
28 from shutil import copyfile, Error
29
30 XML_CONFIG = "/etc/enigma2/pluginsort.xml"
31
32 def SelectedPluginEntryComponent(plugin):
33         if plugin.icon is None:
34                 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "skin_default/icons/plugin.png"))
35         else:
36                 png = plugin.icon
37
38         return [
39                 plugin,
40                 MultiContentEntryText(pos=(0,0), size=(440, 50), backcolor_sel=8388608),
41                 MultiContentEntryText(pos=(120, 5), size=(320, 25), font=0, text=plugin.name),
42                 MultiContentEntryText(pos=(120, 26), size=(320, 17), font=1, text=plugin.description),
43                 MultiContentEntryPixmapAlphaTest(pos=(10, 5), size=(100, 40), png = png),
44         ]
45
46 WHEREMAP = {}
47 pdict = PluginDescriptor.__dict__
48 for where in pdict:
49         if where.startswith('WHERE_'):
50                 WHEREMAP[where] = pdict[where]
51 del pdict
52 reverse = lambda map: dict(zip(map.values(), map.keys()))
53
54 class PluginWeights:
55         def __init__(self):
56                 self.plugins = {}
57                 self.load()
58
59         def load(self):
60                 if not fileExists(XML_CONFIG):
61                         return
62
63                 try:
64                         config = cet_parse(XML_CONFIG).getroot()
65                 except ParseError, pe:
66                         from time import time
67                         print "[PluginSort] Parse Error occured in configuration, backing it up and starting from scratch!"
68                         try:
69                                 copyfile(XML_CONFIG, "/etc/enigma2/pluginsort.xml.%d" % (int(time()),))
70                         except Error, she:
71                                 print "[PluginSort] Uh oh, failed to create the backup... I hope you have one anyway :D"
72                         return
73
74                 for wheresection in config.findall('where'):
75                         where = wheresection.get('type')
76                         whereid = WHEREMAP.get(where, None)
77                         whereplugins = wheresection.findall('plugin')
78                         if not whereid or not whereplugins:
79                                 print "[PluginSort] Ignoring section %s because of invalid id (%s) or no plugins" % (where, repr(whereid))
80                                 continue
81
82                         for plugin in whereplugins:
83                                 name = plugin.get('name')
84                                 try:
85                                         weight = int(plugin.get('weight'))
86                                 except ValueError, ve:
87                                         print "[PluginSort] Invalid weight of %s received for plugin %s, ignoring" % (repr(plugin.get('weight')), repr(name))
88                                 else:
89                                         self.plugins.setdefault(whereid, {})[name] = weight
90
91         def save(self):
92                 list = ['<?xml version="1.0" ?>\n<pluginsort>\n\n']
93                 append = list.append
94                 extend = list.extend
95
96                 idmap = reverse(WHEREMAP)
97                 for key in self.plugins.keys():
98                         whereplugins = self.plugins.get(key, None)
99                         if not whereplugins:
100                                 continue
101
102                         where = idmap[key]
103                         extend((' <where type="', str(where), '">\n'))
104                         for key, value in whereplugins.iteritems():
105                                 extend(('  <plugin name="', str(key), '" weight="', str(value), '" />\n'))
106                         append((' </where>\n'))
107                 append('\n</pluginsort>\n')
108                 
109                 file = open(XML_CONFIG, 'w')
110                 file.writelines(list)
111                 file.close()
112
113         def get(self, plugin):
114                 for x in plugin.where:
115                         whereplugins = self.plugins.get(x, None)
116                         weight = whereplugins and whereplugins.get(plugin.name, None)
117                         if weight is not None:
118                                 return weight
119                 return plugin.weight
120
121         def set(self, plugin):
122                 for x in plugin.where:
123                         whereplugins = self.plugins.get(x, None)
124                         if whereplugins:
125                                 whereplugins[plugin.name] = plugin.weight
126                         else:
127                                 self.plugins[x] = {plugin.name: plugin.weight}
128
129 pluginWeights = PluginWeights()
130
131 def PluginComponent_addPlugin(self, plugin, *args, **kwargs):
132         newWeight = pluginWeights.get(plugin)
133         print "[PluginSort] Setting weight of %s from %d to %d" % (plugin.name, plugin.weight, newWeight)
134         plugin.weight = newWeight
135         PluginComponent.pluginSorter_baseAddPlugin(self, plugin, *args, **kwargs)
136
137 OriginalPluginBrowser = PluginBrowser.PluginBrowser
138 class SortingPluginBrowser(OriginalPluginBrowser):
139         def __init__(self, *args, **kwargs):
140                 self.movemode = False
141                 self.selected = -1
142
143                 OriginalPluginBrowser.__init__(self, *args, **kwargs)
144                 self.skinName = ["SortingPluginBrowser", "PluginBrowser"]
145
146                 self["MenuActions"] = ActionMap(["MenuActions"],
147                         {
148                                 "menu": self.openMenu,
149                         }, -1
150                 )
151                 self["ColorActions"] = ActionMap(["ColorActions"],
152                         {
153                                 "green": self.toggleMoveMode,
154                         }, -1
155                 )
156                 self["ColorActions"].setEnabled(False)
157
158                 self["WizardActions"] = ActionMap(["WizardActions"],
159                         {
160                                 "left": self.left,
161                                 "right": self.right,
162                                 "up": self.up,
163                                 "down": self.down,
164                         }, -2
165                 )
166                 # TODO: allow to select first 10 plugins by number (1-9, 0)
167                 self["NumberActions"] = NumberActionMap(["NumberActions"],
168                         {
169                                 "1": self.keyNumberGlobal,
170                                 "2": self.keyNumberGlobal,
171                                 "3": self.keyNumberGlobal,
172                                 "4": self.keyNumberGlobal,
173                                 "5": self.keyNumberGlobal,
174                                 "6": self.keyNumberGlobal,
175                                 "7": self.keyNumberGlobal,
176                                 "8": self.keyNumberGlobal,
177                                 "9": self.keyNumberGlobal,
178                                 "0": self.keyNumberGlobal,
179                         }, -1
180                 )
181
182         def keyNumberGlobal(self, number):
183                 if not self.movemode:
184                         realnumber = (number - 1) % 10
185                         if realnumber < len(self.list):
186                                 self["list"].moveToIndex(realnumber)
187                                 self.save()
188
189         def close(self, *args, **kwargs):
190                 if self.movemode:
191                         self.toggleMoveMode()
192                 OriginalPluginBrowser.close(self, *args, **kwargs)
193
194         # copied from PluginBrowser because we redo pretty much anything :-)
195         def updateList(self):
196                 self.pluginlist = plugins.getPlugins(PluginDescriptor.WHERE_PLUGINMENU)
197                 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
198                 self.list = [PluginEntryComponent(plugin) for plugin in self.pluginlist]
199                 self["list"].l.setList(self.list)
200                 if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
201                         # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
202                         self["red"].setText(_("Manage extensions"))
203                         self["green"].setText(_("Sort") if not self.movemode else _("End Sort"))
204                         self["SoftwareActions"].setEnabled(True)
205                         self["PluginDownloadActions"].setEnabled(False)
206                         self["ColorActions"].setEnabled(True)
207                 else:
208                         # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
209                         self["red"].setText(_("Remove Plugins"))
210                         # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
211                         self["green"].setText(_("Download Plugins"))
212                         self["SoftwareActions"].setEnabled(False)
213                         self["PluginDownloadActions"].setEnabled(True)
214                         self["ColorActions"].setEnabled(False)
215
216         def doMove(self, func):
217                 if self.selected != -1:
218                         oldpos = self["list"].getSelectedIndex()
219                         func()
220                         entry = self.list.pop(oldpos)
221                         newpos = self["list"].getSelectedIndex()
222                         self.list.insert(newpos, entry)
223                         self["list"].l.setList(self.list)
224                 else:
225                         func()
226
227         def left(self):
228                 self.doMove(self["list"].pageUp)
229
230         def right(self):
231                 self.doMove(self["list"].pageDown)
232
233         def up(self):
234                 self.doMove(self["list"].up)
235
236         def down(self):
237                 self.doMove(self["list"].down)
238
239         def save(self):
240                 selected = self.selected
241                 if not self.movemode:
242                         OriginalPluginBrowser.save(self)
243                 elif selected != -1:
244                         Len = len(self.pluginlist)
245                         newpos = self["list"].getSelectedIndex()
246                         entry = self.pluginlist[selected]
247                         self.pluginlist.remove(entry)
248                         self.pluginlist.insert(newpos, entry)
249
250                         # we moved up, increase weight of plugins after us
251                         if newpos < selected:
252                                 print "[PluginSort]", entry.name, "moved up"
253                                 i = newpos + 1
254                                 # since we moved up, there has to be an entry after this one
255                                 diff = abs(self.pluginlist[i].weight - self.pluginlist[newpos].weight) + 1
256                                 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)
257                                 while i < Len:
258                                         print "[PluginSort] INCREASE WEIGHT OF", self.pluginlist[i].name, "BY", diff
259                                         self.pluginlist[i].weight += diff
260                                         i += 1
261                         # we moved down, decrease weight of plugins before us
262                         elif newpos > selected:
263                                 print "[PluginSort]", entry.name, "moved down"
264                                 i = newpos - 1
265                                 # since we moved up, there has to be an entry before this one
266                                 diff = abs(self.pluginlist[newpos].weight - self.pluginlist[i].weight) + 1
267                                 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)
268                                 while i > -1:
269                                         print "[PluginSort] DECREASE WEIGHT OF", self.pluginlist[i].name, "BY", diff
270                                         self.pluginlist[i].weight -= diff
271                                         i -= 1
272                         else:
273                                 print "[PluginSort]", entry.name, "did not move (%d to %d)?" % (selected, newpos)
274
275                         self.list = [PluginEntryComponent(plugin) for plugin in self.pluginlist]
276                         print "[PluginSort] NEW LIST:", [(plugin.name, plugin.weight) for plugin in self.pluginlist]
277                         self["list"].l.setList(self.list)
278                         self.selected = -1
279                 else:
280                         self.selected = self["list"].getSelectedIndex()
281                         self.list[self.selected] = SelectedPluginEntryComponent(self.pluginlist[self.selected])
282                         self["list"].l.setList(self.list)
283         
284         def openMenu(self):
285                 if fileExists(resolveFilename(SCOPE_PLUGINS, "Extensions/PluginHider/plugin.py")):
286                         if self.movemode:
287                                 # TRANSLATORS: there is no need to translate this string, as it was reused from e2 core
288                                 moveString = _("disable move mode")
289                         else:
290                                 # TRANSLATORS: there is no need to translate this string, as it was reused from e2 core
291                                 moveString = _("enable move mode")
292                         list = [
293                                 (_("hide selected plugin"), self.hidePlugin),
294                                 (moveString, self.toggleMoveMode),
295                         ]
296                         self.session.openWithCallback(
297                                 self.menuCallback,
298                                 ChoiceBox,
299                                 list = list,
300                         )
301                 else:
302                         self.toggleMoveMode()
303
304         def menuCallback(self, ret):
305                 ret and ret[1]()
306
307         def hidePlugin(self):
308                 try:
309                         from Plugins.Extensions.PluginHider.plugin import hidePlugin
310                 except Exception, e:
311                         self.session.open(MessageBox, _("Unable to load PluginHider"), MessageBox.TYPE_ERROR)
312                 else:
313                         hidePlugin(self["list"].l.getCurrentSelection()[0])
314
315                         # we were actually in move mode, so save the current position
316                         if self.selected != -1:
317                                 self.save()
318                         self.updateList()
319
320         def toggleMoveMode(self):
321                 if self.movemode:
322                         if self.selected != -1:
323                                 self.save()
324                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
325                                 self["green"].setText(_("Sort"))
326
327                         for plugin in self.pluginlist:
328                                 pluginWeights.set(plugin)
329                         pluginWeights.save()
330                 else:
331                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
332                                 self["green"].setText(_("End Sort"))
333                 self.movemode = not self.movemode
334
335 def autostart(reason, *args, **kwargs):
336         if reason == 0:
337                 PluginComponent.pluginSorter_baseAddPlugin = PluginComponent.addPlugin
338                 PluginComponent.addPlugin = PluginComponent_addPlugin
339
340                 # "fix" weight of plugins already added to list, future ones will be fixed automatically
341                 for plugin in plugins.getPlugins(PluginDescriptor.WHERE_PLUGINMENU):
342                         # enigma2 older than 3.3.2011 does not know plugin weights, so default them to 0 manually
343                         try:
344                                 newWeight = pluginWeights.get(plugin)
345                         except AttributeError, ae:
346                                 plugin.weight = 0
347                                 newWeight = 0
348                                 PluginDescriptor.weight = 0
349                                 print "[PluginSort] Introduced weight attribute to PluginDescriptor for old enigma2 (this message may show multiple times)"
350
351                         print "[PluginSort] Fixing weight for %s (was %d, now %d)" % (plugin.name, plugin.weight, newWeight)
352                         plugin.weight = newWeight
353
354                 PluginBrowser.PluginBrowser = SortingPluginBrowser
355         else:
356                 PluginComponent.addPlugin = PluginComponent.pluginSorter_baseAddPlugin
357                 PluginBrowser.PluginBrowser = OriginalPluginBrowser
358
359 def Plugins(**kwargs):
360         return [
361                 PluginDescriptor(
362                         where=PluginDescriptor.WHERE_AUTOSTART,
363                         fnc=autostart,
364                         needsRestart=False, # TODO: check this!
365                 ),
366         ]