update some syntax and make the code more future-proof ;)
[enigma2-plugins.git] / menusort / src / plugin.py
1 from __future__ import print_function
2
3 # Plugin definition
4 from Plugins.Plugin import PluginDescriptor
5
6 from Screens.Menu import Menu, mdom
7 from Screens.HelpMenu import HelpableScreen
8
9 from Tools.BoundFunction import boundFunction
10 from Tools.Directories import fileExists
11
12 from enigma import eListboxPythonMultiContent, gFont, RT_HALIGN_LEFT, \
13                 RT_WRAP
14 from Components.MenuList import MenuList
15 from skin import parseColor, parseFont
16
17 from Components.ActionMap import HelpableActionMap, ActionMap
18 from Components.SystemInfo import SystemInfo
19
20 from xml.etree.cElementTree import parse as cet_parse
21 try:
22         from xml.etree.cElementTree import ParseError
23 except ImportError as ie:
24         ParseError = SyntaxError
25
26 from shutil import copyfile, Error
27
28 XML_CONFIG = "/etc/enigma2/menusort.xml"
29 DEBUG = False
30
31 class baseMethods:
32         pass
33
34 class MenuWeights:
35         def __init__(self):
36                 # map text -> weight
37                 self.weights = {}
38                 self.load()
39
40         def load(self):
41                 if not fileExists(XML_CONFIG):
42                         return
43
44                 try:
45                         config = cet_parse(XML_CONFIG).getroot()
46                 except ParseError as pe:
47                         from time import time
48                         print("[MenuSort] Parse Error occured in configuration, backing it up and starting from scratch!")
49                         try:
50                                 copyfile(XML_CONFIG, "/etc/enigma2/menusort.xml.%d" % (int(time()),))
51                         except Error as she:
52                                 print("[MenuSort] Uh oh, failed to create the backup... I hope you have one anyway :D")
53                         return
54
55                 for node in config.findall('entry'):
56                         text = node.get('text', '').encode("UTF-8")
57                         weight = node.get("weight", None)
58                         try:
59                                 weight = int(weight)
60                         except ValueError as ve:
61                                 print("[MenuSort] Invalid value for weight on entry %s: %s" % (repr(text), repr(weight)))
62                                 continue
63                         if not text or weight is None:
64                                 print("[MenuSort] Invalid entry in xml (%s, %s), ignoring" % (repr(text), repr(weight)))
65                                 continue
66                         self.weights[text] = weight
67
68         def save(self):
69                 list = ['<?xml version="1.0" ?>\n<menusort>\n\n']
70                 append = list.append
71                 extend = list.extend
72
73                 for text, weight in self.weights.items():
74                         extend((' <entry text="', str(text), '" weight="', str(weight), '" />\n'))
75                 append('\n</menusort>\n')
76
77                 file = open(XML_CONFIG, 'w')
78                 file.writelines(list)
79                 file.close()
80
81         def get(self, tuple):
82                 return int(self.weights.get(tuple[0], tuple[3]))
83
84         def cmp(self, first, second):
85                 return self.get(first) - self.get(second)
86
87         def set(self, tuple):
88                 self.weights[tuple[0]] = tuple[3]
89 menuWeights = MenuWeights()
90
91 def Menu__init__(self, session, parent, *args, **kwargs):
92         baseMethods.Menu__init__(self, session, parent, *args, **kwargs)
93         list = self["menu"].list
94         list.sort(cmp=menuWeights.cmp)
95         self["menu"].list = list
96
97 class SortableMenuList(MenuList):
98         def __init__(self, list):
99                 MenuList.__init__(self, list, False, content=eListboxPythonMultiContent)
100
101                 l = self.l
102                 l.setFont(0, gFont("Regular", 22))
103                 l.setBuildFunc(self.buildListboxEntry)
104                 self.selected = None
105                 self.selectedColor = 8388608
106
107         def invalidate(self):
108                 self.l.invalidate()
109
110         def applySkin(self, desktop, parent):
111                 attribs = [ ] 
112                 if self.skinAttributes is not None:
113                         for (attrib, value) in self.skinAttributes:
114                                 if attrib == "font":
115                                         self.l.setFont(0, parseFont(value, ((1,1),(1,1))))
116                                 elif attrib == "itemHeight":
117                                         self.l.setItemHeight(int(value))
118                                 elif attrib == "selectedColor":
119                                         self.selectedColor = int(parseColor(value))
120                                 else:
121                                         attribs.append((attrib, value))
122                 self.skinAttributes = attribs
123                 return MenuList.applySkin(self, desktop, parent)
124
125         def buildListboxEntry(self, *menu):
126                 size = self.l.getItemSize()
127                 height = size.height()
128                 width = size.width()
129
130                 l = [
131                         None,
132                         (eListboxPythonMultiContent.TYPE_TEXT, 0, 0, width, height, 0, RT_HALIGN_LEFT|RT_WRAP, menu[0]),
133                 ]
134                 if menu[0] == self.selected:
135                         l.insert(1, (eListboxPythonMultiContent.TYPE_TEXT, 0, 0, width, height, 0, RT_HALIGN_LEFT|RT_WRAP, '',  None, None, None, self.selectedColor, None, None))
136                 return l
137
138 class SortableMenu(Menu, HelpableScreen):
139         skin = """<screen name="SortableMenu" position="center,center" size="210,285">
140                 <widget source="title" render="Label" position="5,10" size="200,35" font="Regular;23" />
141                 <widget name="menu" position="5,55" size="200,225" scrollbarMode="showOnDemand" font="Regular;23" />
142                 </screen>"""
143         def __init__(self, *args, **kwargs):
144                 baseMethods.Menu__init__(self, *args, **kwargs) # using the base initializer saves us a few cycles
145                 HelpableScreen.__init__(self)
146                 self.skinName = "SortableMenu"
147
148                 # XXX: not nice, but makes our life a little easier
149                 l = [(x[0], x[1], x[2], menuWeights.get(x)) for x in self["menu"].list]
150                 l.sort(key=lambda x:x[3])
151                 self["menu"] = SortableMenuList(l)
152
153                 self["WizardActions"] = ActionMap(["WizardActions"],
154                         {
155                                 "left": boundFunction(self.doMove, self["menu"].pageUp),
156                                 "right": boundFunction(self.doMove, self["menu"].pageDown),
157                                 "up": boundFunction(self.doMove, self["menu"].up),
158                                 "down": boundFunction(self.doMove, self["menu"].down),
159                         }, -1
160                 )
161
162                 self["MenuSortActions"] = HelpableActionMap(self, "MenuSortActions",
163                         {
164                                 "ignore": lambda: None, # we need to overwrite some regular actions :-)
165                                 "toggleSelection": (self.toggleSelection, _("toggle selection")),
166                                 "selectEntry": (self.okbuttonClick, _("enter menu")),
167                         }, -1
168                 )
169                 self.selected = -1
170
171         def createSummary(self):
172                 return None
173
174         # copied from original Menu for simplicity
175         def addMenu(self, destList, node):
176                 requires = node.get("requires")
177                 if requires:
178                         if requires[0] == '!':
179                                 if SystemInfo.get(requires[1:], False):
180                                         return
181                         elif not SystemInfo.get(requires, False):
182                                 return
183                 MenuTitle = _(node.get("text", "??").encode("UTF-8"))
184                 entryID = node.get("entryID", "undefined")
185                 weight = node.get("weight", 50)
186                 x = node.get("flushConfigOnClose")
187                 if x:
188                         a = boundFunction(self.session.openWithCallback, self.menuClosedWithConfigFlush, SortableMenu, node)
189                 else:
190                         a = boundFunction(self.session.openWithCallback, self.menuClosed, SortableMenu, node)
191                 #TODO add check if !empty(node.childNodes)
192                 destList.append((MenuTitle, a, entryID, weight))
193
194         def close(self, *args, **kwargs):
195                 for entry in self["menu"].list:
196                         menuWeights.set(entry)
197                 menuWeights.save()
198                 Menu.close(self, *args, **kwargs)
199
200         def doMove(self, func):
201                 if self.selected != -1:
202                         l = self["menu"].list
203                         oldpos = self["menu"].getSelectedIndex()
204                         func()
205                         entry = l.pop(oldpos)
206                         newpos = self["menu"].getSelectedIndex()
207                         l.insert(newpos, entry)
208                         self["menu"].setList(l)
209                 else:
210                         func()
211
212         def toggleSelection(self):
213                 selected = self.selected
214                 if selected != -1:
215                         l = self["menu"].list
216                         Len = len(l)
217                         newpos = self["menu"].getSelectedIndex()
218                         entry = l[newpos]
219
220                         # we moved up, increase weight of plugins after us
221                         if newpos < selected:
222                                 print("[MenuSort]", entry[0], "moved up")
223                                 i = newpos + 1
224                                 # since we moved up, there has to be an entry after this one
225                                 diff = abs(int(l[i][3]) - int(l[newpos][3])) + 1
226                                 print("[MenuSort] Using weight from %d (%d) and %d (%d) to calculate diff (%d)" % (i, int(l[i][3]), newpos, int(l[newpos][3]), diff))
227                                 while i < Len:
228                                         if DEBUG: print("[MenuSort] INCREASE WEIGHT OF", l[i][0], "BY", diff)
229                                         l[i] = (l[i][0], l[i][1], l[i][2], int(l[i][3]) + diff)
230                                         i += 1
231                         # we moved down, decrease weight of plugins before us
232                         elif newpos > selected:
233                                 print("[MenuSort]", entry[0], "moved down")
234                                 i = newpos - 1
235                                 # since we moved up, there has to be an entry before this one
236                                 diff = abs(int(l[i][3]) - int(l[newpos][3])) + 1
237                                 print("[MenuSort] Using weight from %d (%d) and %d (%d) to calculate diff (%d)" % (newpos, int(l[newpos][3]), i, int(l[i][3]), diff))
238                                 while i > -1:
239                                         if DEBUG: print("[MenuSort] DECREASE WEIGHT OF", l[i][0], "BY", diff)
240                                         l[i] = (l[i][0], l[i][1], l[i][2], int(l[i][3]) - diff)
241                                         i -= 1
242                         else:
243                                 if DEBUG: print("[MenuSort]", entry[0], "did not move (%d to %d)?" % (selected, newpos))
244
245                         if DEBUG: print("[MenuSort] NEW LIST:", l)
246                         self["menu"].setList(l)
247                         self.selected = -1
248                         self["menu"].selected = None
249                 else:
250                         sel = self["menu"].getCurrent()
251                         if sel:
252                                 self["menu"].selected = sel[0]
253                                 self.selected = self["menu"].getSelectedIndex()
254                                 self["menu"].invalidate()
255                         else:
256                                 self.selected = -1
257
258         def keyNumberGlobal(self, number):
259                 pass
260
261 def autostart(reason, *args, **kwargs):
262         if reason == 0:
263                 try:
264                         baseMethods.Menu__init__
265                 except AttributeError as ae:
266                         pass
267                 else:
268                         print("[MenuSort] Initialized more than once, ignoring request.")
269                         return
270
271                 baseMethods.Menu__init__ = Menu.__init__
272                 Menu.__init__ = Menu__init__
273         else:
274                 Menu.__init__ = baseMethods.Menu__init__
275
276 def main(session, *args, **kwargs):
277         session.open(SortableMenu, mdom.getroot())
278
279 def Plugins(**kwargs):
280         return [
281                 PluginDescriptor(
282                         where=PluginDescriptor.WHERE_AUTOSTART,
283                         fnc=autostart,
284                         needsRestart=False,
285                 ),
286                 PluginDescriptor(
287                         where=PluginDescriptor.WHERE_PLUGINMENU,
288                         name="MenuSort",
289                         description=_("Sort main menu"),
290                         fnc=main,
291                         needsRestart=False,
292                 ),
293         ]