EcasaGui.py: fix displaying error messages in errbacks
[enigma2-plugins.git] / ecasa / src / EcasaGui.py
1 from __future__ import print_function
2
3 #pragma mark - GUI
4
5 #pragma mark Screens
6 from Screens.ChoiceBox import ChoiceBox
7 from Screens.Screen import Screen
8 from Screens.HelpMenu import HelpableScreen
9 from Screens.InfoBarGenerics import InfoBarNotifications
10 from Screens.LocationBox import LocationBox
11 from Screens.MessageBox import MessageBox
12 from NTIVirtualKeyBoard import NTIVirtualKeyBoard
13 from EcasaSetup import EcasaSetup
14
15 #pragma mark Components
16 from Components.ActionMap import HelpableActionMap
17 from Components.AVSwitch import AVSwitch
18 from Components.Label import Label
19 from Components.Pixmap import Pixmap, MovingPixmap
20 from Components.Sources.StaticText import StaticText
21 from Components.Sources.List import List
22
23 #pragma mark Configuration
24 from Components.config import config
25
26 #pragma mark Picasa
27 from .PicasaApi import PicasaApi
28 from TagStrip import strip_readable
29
30 from enigma import ePicLoad, ePythonMessagePump, eTimer, getDesktop
31 from Tools.Directories import resolveFilename, SCOPE_PLUGINS
32 from Tools.Notifications import AddPopup
33 from collections import deque
34
35 try:
36         xrange = xrange
37 except NameError:
38         xrange = range
39
40 our_print = lambda *args, **kwargs: print("[EcasaGui]", *args, **kwargs)
41
42 AUTHENTICATION_ERROR_ID = "EcasaAuthenticationError"
43
44 class EcasaPictureWall(Screen, HelpableScreen, InfoBarNotifications):
45         """Base class for so-called "picture walls"."""
46         PICS_PER_PAGE = 15
47         PICS_PER_ROW = 5
48         skin = """<screen position="center,center" size="600,380">
49                 <ePixmap position="0,0" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on"/>
50                 <ePixmap position="140,0" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on"/>
51                 <ePixmap position="280,0" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on"/>
52                 <ePixmap position="420,0" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on"/>
53                 <ePixmap position="565,10" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on"/>
54                 <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
55                 <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
56                 <widget source="key_yellow" render="Label" position="280,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
57                 <widget source="key_blue" render="Label" position="420,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
58                 <widget name="waitingtext" position="100,179" size="400,22" valign="center" halign="center" font="Regular;22"/>
59                 <widget name="image0"  position="30,50"   size="90,90"/>
60                 <widget name="image1"  position="140,50"  size="90,90"/>
61                 <widget name="image2"  position="250,50"  size="90,90"/>
62                 <widget name="image3"  position="360,50"  size="90,90"/>
63                 <widget name="image4"  position="470,50"  size="90,90"/>
64                 <widget name="image5"  position="30,160"  size="90,90"/>
65                 <widget name="image6"  position="140,160" size="90,90"/>
66                 <widget name="image7"  position="250,160" size="90,90"/>
67                 <widget name="image8"  position="360,160" size="90,90"/>
68                 <widget name="image9"  position="470,160" size="90,90"/>
69                 <widget name="image10" position="30,270"  size="90,90"/>
70                 <widget name="image11" position="140,270" size="90,90"/>
71                 <widget name="image12" position="250,270" size="90,90"/>
72                 <widget name="image13" position="360,270" size="90,90"/>
73                 <widget name="image14" position="470,270" size="90,90"/>
74                 <!-- TODO: find some better picture -->
75                 <widget name="highlight" position="30,142" size="90,5"/>
76                 </screen>"""
77         def __init__(self, session, api=None):
78                 Screen.__init__(self, session)
79                 HelpableScreen.__init__(self)
80                 InfoBarNotifications.__init__(self)
81
82                 if api is None:
83                         self.api = PicasaApi(cache=config.plugins.ecasa.cache.value)
84                         try:
85                                 self.api.setCredentials(
86                                         config.plugins.ecasa.google_username.value,
87                                         config.plugins.ecasa.google_password.value
88                                 )
89                         except Exception as e:
90                                 AddPopup(
91                                         _("Unable to authenticate with Google: %s.") % (e.message),
92                                         MessageBox.TYPE_ERROR,
93                                         5,
94                                         id=AUTHENTICATION_ERROR_ID,
95                                 )
96                 else:
97                         self.api = api
98
99                 self["key_red"] = StaticText(_("Close"))
100                 self["key_green"] = StaticText(_("Albums"))
101                 self["key_yellow"] = StaticText()
102                 self["key_blue"] = StaticText(_("Search"))
103                 for i in xrange(self.PICS_PER_PAGE):
104                         self['image%d' % i] = Pixmap()
105                         self['title%d' % i] = StaticText()
106                 self["highlight"] = MovingPixmap()
107                 self["waitingtext"] = Label(_("Please wait... Loading list..."))
108
109                 self["overviewActions"] = HelpableActionMap(self, "EcasaOverviewActions", {
110                         "up": self.up,
111                         "down": self.down,
112                         "left": self.left,
113                         "right": self.right,
114                         "nextPage": (self.nextPage, _("show next page")),
115                         "prevPage": (self.prevPage, _("show previous page")),
116                         "select": self.select,
117                         "exit":self.close,
118                         "albums":(self.albums, _("show your albums (if logged in)")),
119                         "search":(self.search, _("start a new search")),
120                         "contextMenu":(self.contextMenu, _("open context menu")),
121                         }, -1)
122
123                 self.offset = 0
124                 self.__highlighted = 0
125                 self.pictures = ()
126
127                 # thumbnail loader
128                 self.picload = ePicLoad()
129                 self.picload.PictureData.get().append(self.gotPicture)
130                 self.currentphoto = None
131                 self.queue = deque()
132
133                 self.onLayoutFinish.append(self.layoutFinished)
134
135         def layoutFinished(self):
136                 self["highlight"].instance.setPixmapFromFile(resolveFilename(SCOPE_PLUGINS, "Extensions/Ecasa/highlighted.png"))
137                 self["highlight"].hide()
138
139                 size = self['image0'].instance.size()
140                 sc = AVSwitch().getFramebufferScale()
141                 self.picload.setPara((size.width(), size.height(), sc[0], sc[1], False, 1, '#ff000000'))
142
143         @property
144         def highlighted(self):
145                 return self.__highlighted
146
147         @highlighted.setter
148         def highlighted(self, highlighted):
149                 our_print("setHighlighted", highlighted)
150                 # only allow to select valid pictures
151                 if highlighted + self.offset >= len(self.pictures): return
152
153                 self.__highlighted = highlighted
154                 pixmap = self['image%d' % highlighted]
155                 origpos = pixmap.getPosition()
156                 origsize = pixmap.instance.size()
157                 # TODO: hardcoded highlight offset is evil :P
158                 self["highlight"].moveTo(origpos[0], origpos[1]+origsize.height()+2, 1)
159                 self["highlight"].startMoving()
160
161         def gotPicture(self, picInfo=None):
162                 ptr = self.picload.getData()
163                 idx = self.pictures.index(self.currentphoto)
164                 realIdx = idx - self.offset
165                 if ptr is not None:
166                         self['image%d' % realIdx].instance.setPixmap(ptr.__deref__())
167                 else:
168                         our_print("gotPicture got invalid results for idx", idx, "("+str(realIdx)+")")
169                         # NOTE: we could use a different picture here that indicates a failure
170                         self['image%d' % realIdx].instance.setPixmap(None)
171                         # NOTE: the thread WILL most likely be hung and NOT recover from it, so we should remove the old picload and create a new one :/
172                 self.currentphoto = None
173                 self.maybeDecode()
174
175         def maybeDecode(self):
176                 if self.currentphoto is not None: return
177                 try:
178                         filename, self.currentphoto = self.queue.pop()
179                 except IndexError:
180                         our_print("no queued photos")
181                         # no more pictures
182                         pass
183                 else:
184                         self.picload.startDecode(filename)
185
186         def pictureDownloaded(self, tup):
187                 filename, photo = tup
188                 self.queue.append((filename, photo))
189                 self.maybeDecode()
190
191         def pictureDownloadFailed(self, tup):
192                 error, photo = tup
193                 our_print("pictureDownloadFailed", error, photo)
194                 # TODO: indicate in gui
195
196         def setup(self):
197                 our_print("setup")
198                 self["waitingtext"].hide()
199                 self["highlight"].show()
200                 self.queue.clear()
201                 pictures = self.pictures
202                 for i in xrange(self.PICS_PER_PAGE):
203                         try:
204                                 our_print("trying to initiate download of idx", i+self.offset)
205                                 picture = pictures[i+self.offset]
206                                 self.api.downloadThumbnail(picture).addCallbacks(self.pictureDownloaded, self.pictureDownloadFailed)
207                         except IndexError:
208                                 # no more pictures
209                                 self['image%d' % i].instance.setPixmap(None)
210                         except Exception as e:
211                                 our_print("unexpected exception in setup:", e)
212
213         def up(self):
214                 # TODO: implement for incomplete pages
215                 highlighted = (self.highlighted - self.PICS_PER_ROW) % self.PICS_PER_PAGE
216                 our_print("up. before:", self.highlighted, ", after:", highlighted)
217                 self.highlighted = highlighted
218
219                 # we requested an invalid idx
220                 if self.highlighted != highlighted:
221                         # so skip another row
222                         highlighted = (highlighted - self.PICS_PER_ROW) % self.PICS_PER_PAGE
223                         our_print("up2. before:", self.highlighted, ", after:", highlighted)
224                         self.highlighted = highlighted
225
226         def down(self):
227                 # TODO: implement for incomplete pages
228                 highlighted = (self.highlighted + self.PICS_PER_ROW) % self.PICS_PER_PAGE
229                 our_print("down. before:", self.highlighted, ", after:", highlighted)
230                 self.highlighted = highlighted
231
232                 # we requested an invalid idx
233                 if self.highlighted != highlighted:
234                         # so try to skip another row
235                         highlighted = (highlighted + self.PICS_PER_ROW) % self.PICS_PER_PAGE
236                         our_print("down2. before:", self.highlighted, ", after:", highlighted)
237                         self.highlighted = highlighted
238
239         def left(self):
240                 highlighted = (self.highlighted - 1) % self.PICS_PER_PAGE
241                 our_print("left. before:", self.highlighted, ", after:", highlighted)
242                 self.highlighted = highlighted
243
244                 # we requested an invalid idx
245                 if self.highlighted != highlighted:
246                         # go to last possible item
247                         highlighted = (len(self.pictures) - 1) % self.PICS_PER_PAGE
248                         our_print("left2. before:", self.highlighted, ", after:", highlighted)
249                         self.highlighted = highlighted
250
251         def right(self):
252                 highlighted = (self.highlighted + 1) % self.PICS_PER_PAGE
253                 if highlighted + self.offset >= len(self.pictures):
254                         highlighted = 0
255                 our_print("right. before:", self.highlighted, ", after:", highlighted)
256                 self.highlighted = highlighted
257         def nextPage(self):
258                 our_print("nextPage")
259                 if not self.pictures: return
260                 offset = self.offset + self.PICS_PER_PAGE
261                 Len = len(self.pictures)
262                 if offset >= Len:
263                         self.offset = 0
264                 else:
265                         self.offset = offset
266                         if offset + self.highlighted > Len:
267                                 self.highlighted = Len - offset - 1
268                 self.setup()
269         def prevPage(self):
270                 our_print("prevPage")
271                 if not self.pictures: return
272                 offset = self.offset - self.PICS_PER_PAGE
273                 if offset < 0:
274                         Len = len(self.pictures) - 1
275                         offset = Len - (Len % self.PICS_PER_PAGE)
276                         self.offset = offset
277                         if offset + self.highlighted >= Len:
278                                 self.highlighted = Len - offset
279                 else:
280                         self.offset = offset
281                 self.setup()
282
283         def prevFunc(self):
284                 old = self.highlighted
285                 self.left()
286                 highlighted = self.highlighted
287                 if highlighted > old:
288                         self.prevPage()
289
290                 photo = None
291                 try:
292                         # NOTE: using self.highlighted as prevPage might have moved this if the page is not full
293                         photo = self.pictures[self.highlighted+self.offset]
294                 except IndexError:
295                         pass
296                 return photo
297
298         def nextFunc(self):
299                 old = self.highlighted
300                 self.right()
301                 highlighted = self.highlighted
302                 if highlighted < old:
303                         self.nextPage()
304
305                 photo = None
306                 try:
307                         # NOTE: using self.highlighted as nextPage might have moved this if the page is not full
308                         photo = self.pictures[self.highlighted+self.offset]
309                 except IndexError:
310                         pass
311                 return photo
312
313         def select(self):
314                 try:
315                         photo = self.pictures[self.highlighted+self.offset]
316                 except IndexError:
317                         our_print("no such picture")
318                         # TODO: indicate in gui
319                 else:
320                         self.session.open(EcasaPicture, photo, api=self.api, prevFunc=self.prevFunc, nextFunc=self.nextFunc)
321         def albums(self):
322                 self.session.open(EcasaAlbumview, self.api, user=config.plugins.ecasa.user.value)
323         def search(self):
324                 self.session.openWithCallback(
325                         self.searchCallback,
326                         NTIVirtualKeyBoard,
327                         title=_("Enter text to search for")
328                 )
329         def searchCallback(self, text=None):
330                 if text:
331                         # Maintain history
332                         history = config.plugins.ecasa.searchhistory.value
333                         if text not in history:
334                                 history.insert(0, text)
335                                 del history[10:]
336                         else:
337                                 history.remove(text)
338                                 history.insert(0, text)
339                         config.plugins.ecasa.searchhistory.save()
340
341                         thread = EcasaThread(lambda:self.api.getSearch(text, limit=str(config.plugins.ecasa.searchlimit.value)))
342                         self.session.open(EcasaFeedview, thread, api=self.api, title=_("Search for %s") % (text))
343
344         def contextMenu(self):
345                 options = [
346                         (_("Setup"), lambda: self.session.openWithCallback(self.setupClosed, EcasaSetup)),
347                         (_("Search History"), self.openHistory),
348                 ]
349                 self.session.openWithCallback(
350                         self.menuCallback,
351                         ChoiceBox,
352                         list=options
353                 )
354
355         def menuCallback(self, ret=None):
356                 if ret:
357                         ret[1]()
358
359         def openHistory(self):
360                 options = [(x, x) for x in config.plugins.ecasa.searchhistory.value]
361
362                 if options:
363                         self.session.openWithCallback(
364                                 self.historyWrapper,
365                                 ChoiceBox,
366                                 title=_("Select text to search for"),
367                                 list=options
368                         )
369                 else:
370                         self.session.open(
371                                 MessageBox,
372                                 _("No history"),
373                                 type=MessageBox.TYPE_INFO
374                         )
375
376         def historyWrapper(self, ret):
377                 if ret:
378                         self.searchCallback(ret[1])
379
380         def setupClosed(self):
381                 try:
382                         self.api.setCredentials(
383                                 config.plugins.ecasa.google_username.value,
384                                 config.plugins.ecasa.google_password.value
385                         )
386                 except Exception as e:
387                         AddPopup(
388                                 _("Unable to authenticate with Google: %s.") % (e.message),
389                                 MessageBox.TYPE_ERROR,
390                                 5,
391                                 id=AUTHENTICATION_ERROR_ID,
392                         )
393                 self.api.cache = config.plugins.ecasa.cache.value
394
395         def gotPictures(self, pictures):
396                 if not self.instance: return
397                 self.pictures = pictures
398                 self.setup()
399
400         def errorPictures(self, error):
401                 if not self.instance: return
402                 our_print("errorPictures", error)
403                 self.session.open(
404                         MessageBox,
405                         _("Error downloading") + ': ' + error.value.message,
406                         type=MessageBox.TYPE_ERROR,
407                         timeout=3
408                 )
409                 self["waitingtext"].hide()
410
411 class EcasaOverview(EcasaPictureWall):
412         """Overview and supposed entry point of ecasa. Shows featured pictures on the "EcasaPictureWall"."""
413         def __init__(self, session):
414                 EcasaPictureWall.__init__(self, session)
415                 self.skinName = ["EcasaOverview", "EcasaPictureWall"]
416                 thread = EcasaThread(self.api.getFeatured)
417                 thread.deferred.addCallbacks(self.gotPictures, self.errorPictures)
418                 thread.start()
419
420                 self.onClose.append(self.__onClose)
421
422         def __onClose(self):
423                 thread = EcasaThread(lambda: self.api.cleanupCache(config.plugins.ecasa.cachesize.value))
424                 thread.start()
425
426         def layoutFinished(self):
427                 EcasaPictureWall.layoutFinished(self)
428                 self.setTitle(_("eCasa: %s") % (_("Featured Photos")))
429
430 class EcasaFeedview(EcasaPictureWall):
431         """Display a nonspecific feed."""
432         def __init__(self, session, thread, api=None, title=None):
433                 EcasaPictureWall.__init__(self, session, api=api)
434                 self.skinName = ["EcasaFeedview", "EcasaPictureWall"]
435                 self.feedTitle = title
436                 self['key_green'].text = ''
437                 thread.deferred.addCallbacks(self.gotPictures, self.errorPictures)
438                 thread.start()
439
440         def layoutFinished(self):
441                 EcasaPictureWall.layoutFinished(self)
442                 self.setTitle(_("eCasa: %s") % (self.feedTitle or _("Album")))
443
444         def albums(self):
445                 pass
446
447 class EcasaAlbumview(Screen, HelpableScreen, InfoBarNotifications):
448         """Displays albums."""
449         skin = """<screen position="center,center" size="560,420">
450                 <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" transparent="1" alphatest="on" />
451                 <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" transparent="1" alphatest="on" />
452                 <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" size="140,40" transparent="1" alphatest="on" />
453                 <ePixmap pixmap="skin_default/buttons/blue.png" position="420,0" size="140,40" transparent="1" alphatest="on" />
454                 <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
455                 <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
456                 <widget source="key_yellow" render="Label" position="280,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
457                 <widget source="key_blue" render="Label" position="420,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
458                 <widget source="list" render="Listbox" position="0,50" size="560,360" scrollbarMode="showAlways">
459                         <convert type="TemplatedMultiContent">
460                                 {"template": [
461                                                 MultiContentEntryText(pos=(1,1), size=(540,22), text = 0, font = 0, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER),
462                                         ],
463                                   "fonts": [gFont("Regular", 20)],
464                                   "itemHeight": 24
465                                  }
466                         </convert>
467                 </widget>
468         </screen>"""
469         def __init__(self, session, api, user='default'):
470                 Screen.__init__(self, session)
471                 HelpableScreen.__init__(self)
472                 InfoBarNotifications.__init__(self)
473                 self.api = api
474                 self.user = user
475
476                 self['list'] = List()
477                 self['key_red'] = StaticText(_("Close"))
478                 self['key_green'] = StaticText()
479                 self['key_yellow'] = StaticText(_("Change user"))
480                 self['key_blue'] = StaticText(_("User history"))
481
482                 self["albumviewActions"] = HelpableActionMap(self, "EcasaAlbumviewActions", {
483                         "select":(self.select, _("show album")),
484                         "exit":(self.close, _("Close")),
485                         "users":(self.users, _("Change user")),
486                         "history":(self.history, _("User history")),
487                 }, -1)
488
489                 self.acquireAlbumsForUser(user)
490                 self.onLayoutFinish.append(self.layoutFinished)
491
492         def layoutFinished(self):
493                 self.setTitle(_("eCasa: Albums for user %s") % (self.user,))
494
495         def acquireAlbumsForUser(self, user):
496                 thread = EcasaThread(lambda:self.api.getAlbums(user=user))
497                 thread.deferred.addCallbacks(self.gotAlbums, self.errorAlbums)
498                 thread.start()
499
500         def gotAlbums(self, albums):
501                 if not self.instance: return
502                 self['list'].list = albums
503
504         def errorAlbums(self, error):
505                 if not self.instance: return
506                 our_print("errorAlbums", error)
507                 self['list'].setList([(_("Error downloading"), "0", None)])
508                 self.session.open(
509                         MessageBox,
510                         _("Error downloading") + ': ' + error.value.message,
511                         type=MessageBox.TYPE_ERROR,
512                         timeout=30,
513                 )
514
515         def select(self):
516                 cur = self['list'].getCurrent()
517                 if cur:
518                         album = cur[-1]
519                         thread = EcasaThread(lambda:self.api.getAlbum(album))
520                         self.session.open(EcasaFeedview, thread, api=self.api, title=album.title and album.title.text)
521
522         def users(self):
523                 self.session.openWithCallback(
524                         self.searchCallback,
525                         NTIVirtualKeyBoard,
526                         title = _("Enter username")
527                 )
528         def searchCallback(self, text=None):
529                 if text:
530                         # Maintain history
531                         history = config.plugins.ecasa.userhistory.value
532                         if text not in history:
533                                 history.insert(0, text)
534                                 del history[10:]
535                         else:
536                                 history.remove(text)
537                                 history.insert(0, text)
538                         config.plugins.ecasa.userhistory.save()
539
540                         self.session.openWithCallback(self.close, EcasaAlbumview, self.api, text)
541
542         def history(self):
543                 options = [(x, x) for x in config.plugins.ecasa.userhistory.value]
544
545                 if options:
546                         self.session.openWithCallback(
547                                 self.historyWrapper,
548                                 ChoiceBox,
549                                 title=_("Select user"),
550                                 list=options
551                         )
552                 else:
553                         self.session.open(
554                                 MessageBox,
555                                 _("No history"),
556                                 type=MessageBox.TYPE_INFO
557                         )
558
559         def historyWrapper(self, ret):
560                 if ret:
561                         self.searchCallback(ret[1])
562
563 class EcasaPicture(Screen, HelpableScreen, InfoBarNotifications):
564         """Display a single picture and its metadata."""
565         PAGE_PICTURE = 0
566         PAGE_INFO = 1
567         def __init__(self, session, photo, api=None, prevFunc=None, nextFunc=None):
568                 size_w = getDesktop(0).size().width()
569                 size_h = getDesktop(0).size().height()
570                 self.skin = """<screen position="0,0" size="{size_w},{size_h}" flags="wfNoBorder">
571                         <widget name="pixmap" position="0,0" size="{size_w},{size_h}" backgroundColor="black" zPosition="2"/>
572                         <widget source="title" render="Label" position="25,20" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
573                         <widget source="summary" render="Label" position="25,60" zPosition="1" size="{labelwidth},100" valign="top" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
574                         <widget source="keywords" render="Label" position="25,160" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
575                         <widget source="camera" render="Label" position="25,180" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
576                 </screen>""".format(size_w=size_w,size_h=size_h,labelwidth=size_w-50)
577                 Screen.__init__(self, session)
578                 HelpableScreen.__init__(self)
579                 InfoBarNotifications.__init__(self)
580
581                 self.api = api
582                 self.page = self.PAGE_PICTURE
583                 self.prevFunc = prevFunc
584                 self.nextFunc = nextFunc
585                 self.nextPhoto = None
586
587                 self['pixmap'] = Pixmap()
588                 self['camera'] = StaticText()
589                 self['title'] = StaticText()
590                 self['summary'] = StaticText()
591                 self['keywords'] = StaticText()
592
593                 self["pictureActions"] = HelpableActionMap(self, "EcasaPictureActions", {
594                         "info": (self.info, _("show metadata")),
595                         "exit": (self.close, _("Close")),
596                         "contextMenu":(self.contextMenu, _("open context menu")),
597                         }, -1)
598                 if prevFunc and nextFunc:
599                         self["directionActions"] = HelpableActionMap(self, "DirectionActions", {
600                                 "left": self.previous,
601                                 "right": self.next,
602                                 }, -2)
603
604                 self.picload = ePicLoad()
605                 self.picload.PictureData.get().append(self.gotPicture)
606                 self.timer = eTimer()
607                 self.timer.callback.append(self.timerFired)
608
609                 # populate with data, initiate download
610                 self.reloadData(photo)
611
612                 self.onClose.append(self.__onClose)
613
614         def __onClose(self):
615                 if self.nextPhoto is not None:
616                         self.toggleSlideshow()
617
618         def gotPicture(self, picInfo=None):
619                 our_print("picture decoded")
620                 ptr = self.picload.getData()
621                 if ptr is not None:
622                         self['pixmap'].instance.setPixmap(ptr.__deref__())
623                         if self.nextPhoto is not None:
624                                 self.timer.start(config.plugins.ecasa.slideshow_interval.value*1000, True)
625
626         def cbDownload(self, tup):
627                 if not self.instance: return
628                 filename, photo = tup
629                 self.picload.startDecode(filename)
630
631         def ebDownload(self, tup):
632                 if not self.instance: return
633                 error, photo = tup
634                 print("ebDownload", error)
635                 self.session.open(
636                         MessageBox,
637                         _("Error downloading") + ': ' + error.value.message,
638                         type=MessageBox.TYPE_ERROR,
639                         timeout=3
640                 )
641
642         def info(self):
643                 our_print("info")
644                 if self.page == self.PAGE_PICTURE:
645                         self.page = self.PAGE_INFO
646                         self['pixmap'].hide()
647                 else:
648                         self.page = self.PAGE_PICTURE
649                         self['pixmap'].show()
650
651         def contextMenu(self):
652                 options = [
653                         (_("Download Picture"), self.doDownload),
654                 ]
655                 photo = self.photo
656                 if photo.author:
657                         author = photo.author[0]
658                         options.append(
659                                 (_("%s's Gallery") % (author.name.text), self.showAlbums)
660                         )
661                 if self.prevFunc and self.nextFunc:
662                         options.append(
663                                 (_("Start Slideshow") if self.nextPhoto is None else _("Stop Slideshow"), self.toggleSlideshow)
664                         )
665                 self.session.openWithCallback(
666                         self.menuCallback,
667                         ChoiceBox,
668                         list=options
669                 )
670
671         def menuCallback(self, ret=None):
672                 if ret:
673                         ret[1]()
674
675         def doDownload(self):
676                 self.session.openWithCallback(
677                         self.gotFilename,
678                         LocationBox,
679                         _("Where to save?"),
680                         self.photo.media.content[0].url.split('/')[-1],
681                 )
682
683         def gotFilename(self, res):
684                 if res:
685                         try:
686                                 self.api.copyPhoto(self.photo, res)
687                         except Exception as e:
688                                 self.session.open(
689                                         MessageBox,
690                                         _("Unable to download picture: %s") % (e),
691                                         type=MessageBox.TYPE_INFO
692                                 )
693
694         def showAlbums(self):
695                 self.session.open(EcasaAlbumview, self.api, user=self.photo.author[0].email.text)
696
697         def toggleSlideshow(self):
698                 # is slideshow currently running?
699                 if self.nextPhoto is not None:
700                         self.timer.stop()
701                         self.previous() # we already moved forward in our parent view, so move back
702                         self.nextPhoto = None
703                 else:
704                         self.timer.start(config.plugins.ecasa.slideshow_interval.value*1000, True)
705                         self.timerFired()
706
707         def timerFired(self):
708                 if self.nextPhoto:
709                         self.reloadData(self.nextPhoto)
710                         self.timer.stop()
711                 self.nextPhoto = self.nextFunc()
712                 # XXX: for now, only start download. later on we might want to pre-parse the picture
713                 self.api.downloadPhoto(self.nextPhoto)
714
715         def reloadData(self, photo):
716                 if photo is None: return
717                 self.photo = photo
718                 unk = _("unknown")
719
720                 # camera
721                 if photo.exif.make and photo.exif.model:
722                         camera = '%s %s' % (photo.exif.make.text, photo.exif.model.text)
723                 elif photo.exif.make:
724                         camera = photo.exif.make.text
725                 elif photo.exif.model:
726                         camera = photo.exif.model.text
727                 else:
728                         camera = unk
729                 self['camera'].text = _("Camera: %s") % (camera,)
730
731                 title = photo.title.text if photo.title.text else unk
732                 self.setTitle(_("eCasa: %s") % (title))
733                 self['title'].text = _("Title: %s") % (title,)
734                 summary = strip_readable(photo.summary.text).replace('\n\nView Photo', '') if photo.summary.text else ''
735                 self['summary'].text = summary
736                 if photo.media and photo.media.keywords and photo.media.keywords.text:
737                         keywords = photo.media.keywords.text
738                         # TODO: find a better way to handle this
739                         if len(keywords) > 50:
740                                 keywords = keywords[:47] + "..."
741                 else:
742                         keywords = unk
743                 self['keywords'].text = _("Keywords: %s") % (keywords,)
744
745                 try:
746                         real_w = int(photo.media.content[0].width)
747                         real_h = int(photo.media.content[0].heigth)
748                 except Exception as e:
749                         our_print("EcasaPicture.__init__: illegal w/h values, using max size!")
750                         size = getDesktop(0).size()
751                         real_w = size.width()
752                         real_h = size.height()
753
754                 sc = AVSwitch().getFramebufferScale()
755                 self.picload.setPara((real_w, real_h, sc[0], sc[1], False, 1, '#ff000000'))
756
757                 # NOTE: no need to start an extra thread for this, twisted is "parallel" enough in this case
758                 self.api.downloadPhoto(photo).addCallbacks(self.cbDownload, self.ebDownload)
759
760         def previous(self):
761                 if self.prevFunc: self.reloadData(self.prevFunc())
762                 self['pixmap'].instance.setPixmap(None)
763         def next(self):
764                 if self.nextFunc: self.reloadData(self.nextFunc())
765                 self['pixmap'].instance.setPixmap(None)
766
767 #pragma mark - Thread
768
769 import threading
770 from twisted.internet import defer
771
772 class EcasaThread(threading.Thread):
773         def __init__(self, fnc):
774                 threading.Thread.__init__(self)
775                 self.deferred = defer.Deferred()
776                 self.__pump = ePythonMessagePump()
777                 self.__pump.recv_msg.get().append(self.gotThreadMsg)
778                 self.__asyncFunc = fnc
779                 self.__result = None
780                 self.__err = None
781
782         def gotThreadMsg(self, msg):
783                 if self.__err:
784                         self.deferred.errback(self.__err)
785                 else:
786                         try:
787                                 self.deferred.callback(self.__result)
788                         except Exception as e:
789                                 self.deferred.errback(e)
790
791         def run(self):
792                 try:
793                         self.__result = self.__asyncFunc()
794                 except Exception as e:
795                         self.__err = e
796                 finally:
797                         self.__pump.send(0)