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