ecasa: some code adjustments for future extensions
[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.encode('utf-8') 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.encode('utf-8'),))
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.encode('utf-8'),
511                         type=MessageBox.TYPE_ERROR,
512                         timeout=30,
513                 )
514
515         def select(self):
516                 cur = self['list'].getCurrent()
517                 if cur and cur[-1]:
518                         album = cur[-1]
519                         title = cur[0] # NOTE: retrieve from array to be independent of underlaying API as the flickr and picasa albums are not compatible here
520                         thread = EcasaThread(lambda:self.api.getAlbum(album))
521                         self.session.open(EcasaFeedview, thread, api=self.api, title=title)
522
523         def users(self):
524                 self.session.openWithCallback(
525                         self.searchCallback,
526                         NTIVirtualKeyBoard,
527                         title = _("Enter username")
528                 )
529         def searchCallback(self, text=None):
530                 if text:
531                         # Maintain history
532                         history = config.plugins.ecasa.userhistory.value
533                         if text not in history:
534                                 history.insert(0, text)
535                                 del history[10:]
536                         else:
537                                 history.remove(text)
538                                 history.insert(0, text)
539                         config.plugins.ecasa.userhistory.save()
540
541                         self.session.openWithCallback(self.close, EcasaAlbumview, self.api, text)
542
543         def history(self):
544                 options = [(x, x) for x in config.plugins.ecasa.userhistory.value]
545
546                 if options:
547                         self.session.openWithCallback(
548                                 self.historyWrapper,
549                                 ChoiceBox,
550                                 title=_("Select user"),
551                                 list=options
552                         )
553                 else:
554                         self.session.open(
555                                 MessageBox,
556                                 _("No history"),
557                                 type=MessageBox.TYPE_INFO
558                         )
559
560         def historyWrapper(self, ret):
561                 if ret:
562                         self.searchCallback(ret[1])
563
564 class EcasaPicture(Screen, HelpableScreen, InfoBarNotifications):
565         """Display a single picture and its metadata."""
566         PAGE_PICTURE = 0
567         PAGE_INFO = 1
568         def __init__(self, session, photo, api=None, prevFunc=None, nextFunc=None):
569                 size_w = getDesktop(0).size().width()
570                 size_h = getDesktop(0).size().height()
571                 self.skin = """<screen position="0,0" size="{size_w},{size_h}" flags="wfNoBorder">
572                         <widget name="pixmap" position="0,0" size="{size_w},{size_h}" backgroundColor="black" zPosition="2"/>
573                         <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"/>
574                         <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"/>
575                         <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"/>
576                         <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"/>
577                 </screen>""".format(size_w=size_w,size_h=size_h,labelwidth=size_w-50)
578                 Screen.__init__(self, session)
579                 HelpableScreen.__init__(self)
580                 InfoBarNotifications.__init__(self)
581
582                 self.api = api
583                 self.page = self.PAGE_PICTURE
584                 self.prevFunc = prevFunc
585                 self.nextFunc = nextFunc
586                 self.nextPhoto = None
587
588                 self['pixmap'] = Pixmap()
589                 self['camera'] = StaticText()
590                 self['title'] = StaticText()
591                 self['summary'] = StaticText()
592                 self['keywords'] = StaticText()
593
594                 self["pictureActions"] = HelpableActionMap(self, "EcasaPictureActions", {
595                         "info": (self.info, _("show metadata")),
596                         "exit": (self.close, _("Close")),
597                         "contextMenu":(self.contextMenu, _("open context menu")),
598                         }, -1)
599                 if prevFunc and nextFunc:
600                         self["directionActions"] = HelpableActionMap(self, "DirectionActions", {
601                                 "left": self.previous,
602                                 "right": self.next,
603                                 }, -2)
604
605                 self.picload = ePicLoad()
606                 self.picload.PictureData.get().append(self.gotPicture)
607                 self.timer = eTimer()
608                 self.timer.callback.append(self.timerFired)
609
610                 # populate with data, initiate download
611                 self.reloadData(photo)
612
613                 self.onClose.append(self.__onClose)
614
615         def __onClose(self):
616                 if self.nextPhoto is not None:
617                         self.toggleSlideshow()
618
619         def gotPicture(self, picInfo=None):
620                 our_print("picture decoded")
621                 ptr = self.picload.getData()
622                 if ptr is not None:
623                         self['pixmap'].instance.setPixmap(ptr.__deref__())
624                         if self.nextPhoto is not None:
625                                 self.timer.start(config.plugins.ecasa.slideshow_interval.value*1000, True)
626
627         def cbDownload(self, tup):
628                 if not self.instance: return
629                 filename, photo = tup
630                 self.picload.startDecode(filename)
631
632         def ebDownload(self, tup):
633                 if not self.instance: return
634                 error, photo = tup
635                 print("ebDownload", error)
636                 self.session.open(
637                         MessageBox,
638                         _("Error downloading") + ': ' + error.value.message,
639                         type=MessageBox.TYPE_ERROR,
640                         timeout=3
641                 )
642
643         def info(self):
644                 our_print("info")
645                 if self.page == self.PAGE_PICTURE:
646                         self.page = self.PAGE_INFO
647                         self['pixmap'].hide()
648                 else:
649                         self.page = self.PAGE_PICTURE
650                         self['pixmap'].show()
651
652         def contextMenu(self):
653                 options = [
654                         (_("Download Picture"), self.doDownload),
655                 ]
656                 photo = self.photo
657                 if photo.author:
658                         author = photo.author[0]
659                         options.append(
660                                 (_("%s's Gallery") % (author.name.text), self.showAlbums)
661                         )
662                 if self.prevFunc and self.nextFunc:
663                         options.append(
664                                 (_("Start Slideshow") if self.nextPhoto is None else _("Stop Slideshow"), self.toggleSlideshow)
665                         )
666                 self.session.openWithCallback(
667                         self.menuCallback,
668                         ChoiceBox,
669                         list=options
670                 )
671
672         def menuCallback(self, ret=None):
673                 if ret:
674                         ret[1]()
675
676         def doDownload(self):
677                 self.session.openWithCallback(
678                         self.gotFilename,
679                         LocationBox,
680                         _("Where to save?"),
681                         self.photo.media.content[0].url.split('/')[-1],
682                 )
683
684         def gotFilename(self, res):
685                 if res:
686                         try:
687                                 self.api.copyPhoto(self.photo, res)
688                         except Exception as e:
689                                 self.session.open(
690                                         MessageBox,
691                                         _("Unable to download picture: %s") % (e),
692                                         type=MessageBox.TYPE_INFO
693                                 )
694
695         def showAlbums(self):
696                 self.session.open(EcasaAlbumview, self.api, user=self.photo.author[0].email.text)
697
698         def toggleSlideshow(self):
699                 # is slideshow currently running?
700                 if self.nextPhoto is not None:
701                         self.timer.stop()
702                         self.previous() # we already moved forward in our parent view, so move back
703                         self.nextPhoto = None
704                 else:
705                         self.timer.start(config.plugins.ecasa.slideshow_interval.value*1000, True)
706                         self.timerFired()
707
708         def timerFired(self):
709                 if self.nextPhoto:
710                         self.reloadData(self.nextPhoto)
711                         self.timer.stop()
712                 self.nextPhoto = self.nextFunc()
713                 # XXX: for now, only start download. later on we might want to pre-parse the picture
714                 self.api.downloadPhoto(self.nextPhoto)
715
716         def reloadData(self, photo):
717                 if photo is None: return
718                 self.photo = photo
719                 unk = _("unknown")
720
721                 # camera
722                 if photo.exif.make and photo.exif.model:
723                         camera = '%s %s' % (photo.exif.make.text, photo.exif.model.text)
724                 elif photo.exif.make:
725                         camera = photo.exif.make.text
726                 elif photo.exif.model:
727                         camera = photo.exif.model.text
728                 else:
729                         camera = unk
730                 self['camera'].text = _("Camera: %s") % (camera,)
731
732                 title = photo.title.text.encode('utf-8') if photo.title.text else unk
733                 self.setTitle(_("eCasa: %s") % (title))
734                 self['title'].text = _("Title: %s") % (title,)
735                 summary = strip_readable(photo.summary.text).replace('\n\nView Photo', '').encode('utf-8') if photo.summary.text else ''
736                 self['summary'].text = summary
737                 if photo.media and photo.media.keywords and photo.media.keywords.text:
738                         keywords = photo.media.keywords.text
739                         # TODO: find a better way to handle this
740                         if len(keywords) > 50:
741                                 keywords = keywords[:47] + "..."
742                 else:
743                         keywords = unk
744                 self['keywords'].text = _("Keywords: %s") % (keywords,)
745
746                 try:
747                         real_w = int(photo.media.content[0].width)
748                         real_h = int(photo.media.content[0].heigth)
749                 except Exception as e:
750                         our_print("EcasaPicture.__init__: illegal w/h values, using max size!")
751                         size = getDesktop(0).size()
752                         real_w = size.width()
753                         real_h = size.height()
754
755                 sc = AVSwitch().getFramebufferScale()
756                 self.picload.setPara((real_w, real_h, sc[0], sc[1], False, 1, '#ff000000'))
757
758                 # NOTE: no need to start an extra thread for this, twisted is "parallel" enough in this case
759                 self.api.downloadPhoto(photo).addCallbacks(self.cbDownload, self.ebDownload)
760
761         def previous(self):
762                 if self.prevFunc: self.reloadData(self.prevFunc())
763                 self['pixmap'].instance.setPixmap(None)
764         def next(self):
765                 if self.nextFunc: self.reloadData(self.nextFunc())
766                 self['pixmap'].instance.setPixmap(None)
767
768 #pragma mark - Thread
769
770 import threading
771 from twisted.internet import defer
772
773 class EcasaThread(threading.Thread):
774         def __init__(self, fnc):
775                 threading.Thread.__init__(self)
776                 self.deferred = defer.Deferred()
777                 self.__pump = ePythonMessagePump()
778                 self.__pump.recv_msg.get().append(self.gotThreadMsg)
779                 self.__asyncFunc = fnc
780                 self.__result = None
781                 self.__err = None
782
783         def gotThreadMsg(self, msg):
784                 if self.__err:
785                         self.deferred.errback(self.__err)
786                 else:
787                         try:
788                                 self.deferred.callback(self.__result)
789                         except Exception as e:
790                                 self.deferred.errback(e)
791
792         def run(self):
793                 try:
794                         self.__result = self.__asyncFunc()
795                 except Exception as e:
796                         self.__err = e
797                 finally:
798                         self.__pump.send(0)