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