|
1 # -*- coding: UTF-8 -*- |
|
2 # Mosaic by AliAbdul |
|
3 from Components.ActionMap import NumberActionMap |
|
4 from Components.config import config, ConfigSubsection, ConfigInteger |
|
5 from Components.Console import Console |
|
6 from Components.Label import Label |
|
7 from Components.Language import language |
|
8 from Components.Pixmap import Pixmap |
|
9 from Components.VideoWindow import VideoWindow |
|
10 from enigma import eServiceCenter, eServiceReference, eTimer, getDesktop, loadPNG |
|
11 from os import environ |
|
12 from Plugins.Plugin import PluginDescriptor |
|
13 from Screens.ChannelSelection import BouquetSelector |
|
14 from Screens.MessageBox import MessageBox |
|
15 from Screens.Screen import Screen |
|
16 from Tools.Directories import resolveFilename, SCOPE_SKIN_IMAGE, SCOPE_LANGUAGE, SCOPE_PLUGINS |
|
17 from Tools.LoadPixmap import LoadPixmap |
|
18 import gettext |
|
19 |
|
20 ################################################ |
|
21 |
|
22 grab_binary = "/usr/bin/grab" |
|
23 grab_picture = "/tmp/mosaic.jpg" |
|
24 grab_errorlog = "/tmp/mosaic.log" |
|
25 |
|
26 config_limits = (3, 30) |
|
27 config.plugins.Mosaic = ConfigSubsection() |
|
28 config.plugins.Mosaic.countdown = ConfigInteger(default=5, limits=config_limits) |
|
29 |
|
30 playingIcon = loadPNG(resolveFilename(SCOPE_SKIN_IMAGE, 'skin_default/icons/ico_mp_play.png')) |
|
31 pausedIcon = loadPNG(resolveFilename(SCOPE_SKIN_IMAGE, 'skin_default/icons/ico_mp_pause.png')) |
|
32 |
|
33 ################################################ |
|
34 |
|
35 lang = language.getLanguage() |
|
36 environ["LANGUAGE"] = lang[:2] |
|
37 gettext.bindtextdomain("enigma2", resolveFilename(SCOPE_LANGUAGE)) |
|
38 gettext.textdomain("enigma2") |
|
39 gettext.bindtextdomain("Mosaic", "%s%s" % (resolveFilename(SCOPE_PLUGINS), "Extensions/Mosaic/locale/")) |
|
40 |
|
41 def _(txt): |
|
42 t = gettext.dgettext("Mosaic", txt) |
|
43 if t == txt: |
|
44 t = gettext.gettext(txt) |
|
45 return t |
|
46 |
|
47 ################################################ |
|
48 |
|
49 class Mosaic(Screen): |
|
50 PLAY = 0 |
|
51 PAUSE = 1 |
|
52 |
|
53 desktop = getDesktop(0) |
|
54 size = desktop.size() |
|
55 width = size.width() |
|
56 height = size.height() |
|
57 windowWidth = width / 4 |
|
58 windowHeight = height / 4 |
|
59 |
|
60 positions = [] |
|
61 x = 80 |
|
62 y = 50 |
|
63 for i in range(1, 10): |
|
64 positions.append([x, y]) |
|
65 x += windowWidth |
|
66 x += ((width - 160) - (windowWidth * 3)) / 2 |
|
67 if (i == 3) or (i == 6): |
|
68 y = y + windowHeight + 20 |
|
69 x = 80 |
|
70 |
|
71 skin = "" |
|
72 skin += """<screen position="0,0" size="%d,%d" title="Mosaic" flags="wfNoBorder" backgroundColor="#ffffff" >""" % (width, height) |
|
73 skin += """<widget name="playState" position="55,55" size="16,16" alphatest="on" />""" |
|
74 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[0][0]-2, positions[0][1]-1, windowWidth, windowHeight) |
|
75 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[1][0]-2, positions[1][1]-1, windowWidth, windowHeight) |
|
76 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[2][0]-2, positions[2][1]-1, windowWidth, windowHeight) |
|
77 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[3][0]-2, positions[3][1]-1, windowWidth, windowHeight) |
|
78 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[4][0]-2, positions[4][1]-1, windowWidth, windowHeight) |
|
79 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[5][0]-2, positions[5][1]-1, windowWidth, windowHeight) |
|
80 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[6][0]-2, positions[6][1]-1, windowWidth, windowHeight) |
|
81 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[7][0]-2, positions[7][1]-1, windowWidth, windowHeight) |
|
82 skin += """<eLabel position="%d,%d" size="%d,%d" />""" % (positions[8][0]-2, positions[8][1]-1, windowWidth, windowHeight) |
|
83 skin += """<widget name="channel1" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[0][0], positions[0][1]-18, windowWidth-4) |
|
84 skin += """<widget name="channel2" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[1][0], positions[1][1]-18, windowWidth-4) |
|
85 skin += """<widget name="channel3" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[2][0], positions[2][1]-18, windowWidth-4) |
|
86 skin += """<widget name="channel4" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[3][0], positions[3][1]-18, windowWidth-4) |
|
87 skin += """<widget name="channel5" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[4][0], positions[4][1]-18, windowWidth-4) |
|
88 skin += """<widget name="channel6" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[5][0], positions[5][1]-18, windowWidth-4) |
|
89 skin += """<widget name="channel7" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[6][0], positions[6][1]-18, windowWidth-4) |
|
90 skin += """<widget name="channel8" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[7][0], positions[7][1]-18, windowWidth-4) |
|
91 skin += """<widget name="channel9" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (positions[8][0], positions[8][1]-18, windowWidth-4) |
|
92 skin += """<widget name="window1" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[0][0]-2, positions[0][1]-1, windowWidth, windowHeight) |
|
93 skin += """<widget name="window2" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[1][0]-2, positions[1][1]-1, windowWidth, windowHeight) |
|
94 skin += """<widget name="window3" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[2][0]-2, positions[2][1]-1, windowWidth, windowHeight) |
|
95 skin += """<widget name="window4" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[3][0]-2, positions[3][1]-1, windowWidth, windowHeight) |
|
96 skin += """<widget name="window5" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[4][0]-2, positions[4][1]-1, windowWidth, windowHeight) |
|
97 skin += """<widget name="window6" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[5][0]-2, positions[5][1]-1, windowWidth, windowHeight) |
|
98 skin += """<widget name="window7" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[6][0]-2, positions[6][1]-1, windowWidth, windowHeight) |
|
99 skin += """<widget name="window8" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[7][0]-2, positions[7][1]-1, windowWidth, windowHeight) |
|
100 skin += """<widget name="window9" position="%d,%d" zPosition="1" size="%d,%d" />""" % (positions[8][0]-2, positions[8][1]-1, windowWidth, windowHeight) |
|
101 skin += """<widget name="video1" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[0][0]-2, positions[0][1]-1, windowWidth, windowHeight) |
|
102 skin += """<widget name="video2" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[1][0]-2, positions[1][1]-1, windowWidth, windowHeight) |
|
103 skin += """<widget name="video3" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[2][0]-2, positions[2][1]-1, windowWidth, windowHeight) |
|
104 skin += """<widget name="video4" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[3][0]-2, positions[3][1]-1, windowWidth, windowHeight) |
|
105 skin += """<widget name="video5" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[4][0]-2, positions[4][1]-1, windowWidth, windowHeight) |
|
106 skin += """<widget name="video6" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[5][0]-2, positions[5][1]-1, windowWidth, windowHeight) |
|
107 skin += """<widget name="video7" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[6][0]-2, positions[6][1]-1, windowWidth, windowHeight) |
|
108 skin += """<widget name="video8" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[7][0]-2, positions[7][1]-1, windowWidth, windowHeight) |
|
109 skin += """<widget name="video9" position="%d,%d" zPosition="2" size="%d,%d" backgroundColor="#ffffffff" />""" % (positions[8][0]-2, positions[8][1]-1, windowWidth, windowHeight) |
|
110 skin += """<widget name="event1" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[0][0]-2, positions[0][1]-1, windowWidth) |
|
111 skin += """<widget name="event2" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[1][0]-2, positions[1][1]-1, windowWidth) |
|
112 skin += """<widget name="event3" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[2][0]-2, positions[2][1]-1, windowWidth) |
|
113 skin += """<widget name="event4" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[3][0]-2, positions[3][1]-1, windowWidth) |
|
114 skin += """<widget name="event5" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[4][0]-2, positions[4][1]-1, windowWidth) |
|
115 skin += """<widget name="event6" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[5][0]-2, positions[5][1]-1, windowWidth) |
|
116 skin += """<widget name="event7" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[6][0]-2, positions[6][1]-1, windowWidth) |
|
117 skin += """<widget name="event8" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[7][0]-2, positions[7][1]-1, windowWidth) |
|
118 skin += """<widget name="event9" position="%d,%d" size="%d,20" zPosition="3" font="Regular;18" backgroundColor="#000000" foregroundColor="#ffffff" />""" % (positions[8][0]-2, positions[8][1]-1, windowWidth) |
|
119 |
|
120 skin += """<widget name="countdown" position="80,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" />""" % (height-50, windowWidth) |
|
121 skin += """<widget name="count" position="%d,%d" size="%d,20" font="Regular;18" backgroundColor="#ffffff" foregroundColor="#000000" halign="right" /> |
|
122 </screen>""" % (positions[2][0] ,height-50, windowWidth) |
|
123 |
|
124 def __init__(self, session, services): |
|
125 Screen.__init__(self, session) |
|
126 |
|
127 self.session = session |
|
128 self.oldService = self.session.nav.getCurrentlyPlayingServiceReference() |
|
129 self.consoleCmd = "" |
|
130 self.Console = Console() |
|
131 self.serviceHandler = eServiceCenter.getInstance() |
|
132 self.ref_list = services |
|
133 self.window_refs = [None, None, None, None, None, None, None, None, None] |
|
134 self.current_refidx = 0 |
|
135 self.current_window = 1 |
|
136 self.countdown = config.plugins.Mosaic.countdown.value |
|
137 self.working = False |
|
138 self.state = self.PLAY |
|
139 |
|
140 self["playState"] = Pixmap() |
|
141 for i in range(1, 10): |
|
142 self["window" + str(i)] = Pixmap() |
|
143 self["video" + str(i)] = VideoWindow(decoder=0, fb_width=self.width, fb_height=self.height) |
|
144 self["video" + str(i)].hide() |
|
145 self["channel" + str(i)] = Label("") |
|
146 self["event" + str(i)] = Label("") |
|
147 self["event" + str(i)].hide() |
|
148 self["video1"].decoder = 0 |
|
149 self["video1"].show() |
|
150 self["countdown"] = Label() |
|
151 self.updateCountdownLabel() |
|
152 self["count"] = Label() |
|
153 |
|
154 self["actions"] = NumberActionMap(["MosaicActions"], |
|
155 { |
|
156 "ok": self.exit, |
|
157 "cancel": self.closeWithOldService, |
|
158 "green": self.play, |
|
159 "yellow": self.pause, |
|
160 "channelup": self.countdownPlus, |
|
161 "channeldown": self.countdownMinus, |
|
162 "1": self.numberPressed, |
|
163 "2": self.numberPressed, |
|
164 "3": self.numberPressed, |
|
165 "4": self.numberPressed, |
|
166 "5": self.numberPressed, |
|
167 "6": self.numberPressed, |
|
168 "7": self.numberPressed, |
|
169 "8": self.numberPressed, |
|
170 "9": self.numberPressed |
|
171 }, prio=-1) |
|
172 |
|
173 self.updateTimer = eTimer() |
|
174 self.updateTimer.callback.append(self.updateCountdown) |
|
175 self.checkTimer = eTimer() |
|
176 self.checkTimer.callback.append(self.checkGrab) |
|
177 self.checkTimer.start(500, 1) |
|
178 |
|
179 def checkGrab(self): |
|
180 # Start the first service in the bouquet and show the service-name |
|
181 ref = self.ref_list[0] |
|
182 self.window_refs[0] = ref |
|
183 info = self.serviceHandler.info(ref) |
|
184 name = info.getName(ref).replace('\xc2\x86', '').replace('\xc2\x87', '') |
|
185 event_name = self.getEventName(info, ref) |
|
186 self["channel1"].setText(name) |
|
187 self["event1"].setText(event_name) |
|
188 self.session.nav.playService(ref) |
|
189 self["count"].setText(_("Channel: ") + "1 / " + str(len(self.ref_list))) |
|
190 self["playState"].instance.setPixmap(playingIcon) |
|
191 # Start updating the video-screenshots |
|
192 self.updateTimer.start(1, 1) |
|
193 |
|
194 def exit(self, callback=None): |
|
195 self.deleteConsoleCallbacks() |
|
196 self.close() |
|
197 |
|
198 def deleteConsoleCallbacks(self): |
|
199 if self.Console.appContainers.has_key(self.consoleCmd): |
|
200 del self.Console.appContainers[self.consoleCmd].dataAvail[:] |
|
201 del self.Console.appContainers[self.consoleCmd].appClosed[:] |
|
202 del self.Console.appContainers[self.consoleCmd] |
|
203 del self.Console.extra_args[self.consoleCmd] |
|
204 del self.Console.callbacks[self.consoleCmd] |
|
205 |
|
206 def closeWithOldService(self): |
|
207 self.session.nav.playService(self.oldService) |
|
208 self.deleteConsoleCallbacks() |
|
209 self.close() |
|
210 |
|
211 def numberPressed(self, number): |
|
212 ref = self.window_refs[number-1] |
|
213 if ref is not None: |
|
214 self.session.nav.playService(ref) |
|
215 self.deleteConsoleCallbacks() |
|
216 self.close() |
|
217 |
|
218 def play(self): |
|
219 if self.working == False and self.state == self.PAUSE: |
|
220 self.state = self.PLAY |
|
221 self.updateTimer.start(1000, 1) |
|
222 self["playState"].instance.setPixmap(playingIcon) |
|
223 |
|
224 def pause(self): |
|
225 if self.working == False and self.state == self.PLAY: |
|
226 self.state = self.PAUSE |
|
227 self.updateTimer.stop() |
|
228 self["playState"].instance.setPixmap(pausedIcon) |
|
229 |
|
230 def countdownPlus(self): |
|
231 self.changeCountdown(1) |
|
232 |
|
233 def countdownMinus(self): |
|
234 self.changeCountdown(-1) |
|
235 |
|
236 def changeCountdown(self, direction): |
|
237 if self.working == False: |
|
238 configNow = config.plugins.Mosaic.countdown.value |
|
239 configNow += direction |
|
240 |
|
241 if configNow < config_limits[0]: |
|
242 configNow = config_limits[0] |
|
243 elif configNow > config_limits[1]: |
|
244 configNow = config_limits[1] |
|
245 |
|
246 config.plugins.Mosaic.countdown.value = configNow |
|
247 config.plugins.Mosaic.countdown.save() |
|
248 |
|
249 self.updateCountdownLabel() |
|
250 |
|
251 def makeNextScreenshot(self): |
|
252 # Grab video |
|
253 if not self.Console: |
|
254 self.Console = Console() |
|
255 self.consoleCmd = "%s -v -r %d -l -j 100 %s" % (grab_binary, self.windowWidth, grab_picture) |
|
256 self.Console.ePopen(self.consoleCmd, self.showNextScreenshot) |
|
257 |
|
258 def showNextScreenshot(self, result, retval, extra_args): |
|
259 if retval == 0: |
|
260 # Show screenshot in the current window |
|
261 pic = LoadPixmap(grab_picture) |
|
262 self["window" + str(self.current_window)].instance.setPixmap(pic) |
|
263 |
|
264 # Hide current video-window and show the running event-name |
|
265 self["video" + str(self.current_window)].hide() |
|
266 self["event" + str(self.current_window)].show() |
|
267 |
|
268 # Get next ref |
|
269 self.current_refidx += 1 |
|
270 if self.current_refidx > (len(self.ref_list) -1): |
|
271 self.current_refidx = 0 |
|
272 |
|
273 # Play next ref |
|
274 ref = self.ref_list[self.current_refidx] |
|
275 info = self.serviceHandler.info(ref) |
|
276 name = info.getName(ref).replace('\xc2\x86', '').replace('\xc2\x87', '') |
|
277 event_name = self.getEventName(info, ref) |
|
278 self.session.nav.playService(ref) |
|
279 |
|
280 # Get next window index |
|
281 self.current_window += 1 |
|
282 if self.current_window > 9: |
|
283 self.current_window = 1 |
|
284 |
|
285 # Save the ref |
|
286 self.window_refs[self.current_window-1] = ref |
|
287 |
|
288 # Save the event-name and hide the label |
|
289 self["event" + str(self.current_window)].hide() |
|
290 self["event" + str(self.current_window)].setText(event_name) |
|
291 |
|
292 # Show the new video-window |
|
293 self["video" + str(self.current_window)].show() |
|
294 self["video" + str(self.current_window)].decoder = 0 |
|
295 |
|
296 # Show the servicename |
|
297 self["channel" + str(self.current_window)].setText(name) |
|
298 self["count"].setText(_("Channel: ") + str(self.current_refidx + 1) + " / " + str(len(self.ref_list))) |
|
299 |
|
300 # Restart timer |
|
301 self.working = False |
|
302 self.updateTimer.start(1, 1) |
|
303 else: |
|
304 print "[Mosaic] retval: %d result: %s" % (retval, result) |
|
305 |
|
306 try: |
|
307 f = open(grab_errorlog, "w") |
|
308 f.write("retval: %d\nresult: %s" % (retval, result)) |
|
309 f.close() |
|
310 except: |
|
311 pass |
|
312 |
|
313 self.session.openWithCallback(self.exit, MessageBox, _("Error while creating screenshot. You need grab version 0.8 or higher!"), MessageBox.TYPE_ERROR, timeout=5) |
|
314 |
|
315 def updateCountdown(self, callback=None): |
|
316 self.countdown -= 1 |
|
317 self.updateCountdownLabel() |
|
318 if self.countdown == 0: |
|
319 self.countdown = config.plugins.Mosaic.countdown.value |
|
320 self.working = True |
|
321 self.makeNextScreenshot() |
|
322 else: |
|
323 self.updateTimer.start(1000, 1) |
|
324 |
|
325 def updateCountdownLabel(self): |
|
326 self["countdown"].setText("%s %s / %s" % (_("Countdown:"), str(self.countdown), str(config.plugins.Mosaic.countdown.value))) |
|
327 |
|
328 def getEventName(self, info, ref): |
|
329 event = info.getEvent(ref) |
|
330 if event is not None: |
|
331 eventName = event.getEventName() |
|
332 if eventName is None: |
|
333 eventName = "" |
|
334 else: |
|
335 eventName = "" |
|
336 return eventName |
|
337 |
|
338 ################################################ |
|
339 # Most stuff stolen from the GraphMultiEPG |
|
340 |
|
341 Session = None |
|
342 Servicelist = None |
|
343 BouquetSelectorScreen = None |
|
344 |
|
345 def getBouquetServices(bouquet): |
|
346 services = [] |
|
347 Servicelist = eServiceCenter.getInstance().list(bouquet) |
|
348 if Servicelist is not None: |
|
349 while True: |
|
350 service = Servicelist.getNext() |
|
351 if not service.valid(): |
|
352 break |
|
353 if service.flags & (eServiceReference.isDirectory | eServiceReference.isMarker): |
|
354 continue |
|
355 services.append(service) |
|
356 return services |
|
357 |
|
358 def closeBouquetSelectorScreen(ret=None): |
|
359 if BouquetSelectorScreen is not None: |
|
360 BouquetSelectorScreen.close() |
|
361 |
|
362 def openMosaic(bouquet): |
|
363 if bouquet is not None: |
|
364 services = getBouquetServices(bouquet) |
|
365 if len(services): |
|
366 Session.openWithCallback(closeBouquetSelectorScreen, Mosaic, services) |
|
367 |
|
368 def main(session, servicelist, **kwargs): |
|
369 global Session |
|
370 Session = session |
|
371 global Servicelist |
|
372 Servicelist = servicelist |
|
373 global BouquetSelectorScreen |
|
374 |
|
375 bouquets = Servicelist.getBouquetList() |
|
376 if bouquets is not None: |
|
377 if len(bouquets) == 1: |
|
378 openMosaic(bouquets[0][1]) |
|
379 elif len(bouquets) > 1: |
|
380 BouquetSelectorScreen = Session.open(BouquetSelector, bouquets, openMosaic, enableWrapAround=True) |
|
381 |
|
382 def Plugins(**kwargs): |
|
383 return PluginDescriptor(name=_("Mosaic"), where=PluginDescriptor.WHERE_EXTENSIONSMENU, fnc=main) |