Workaround for article listing word-wrap when switching portrait/landscare
[feedingit] / src / FeedingIt.py
1 #!/usr/bin/env python2.5
2
3
4 # Copyright (c) 2007-2008 INdT.
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public License
16 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18
19 # ============================================================================
20 __appname__ = 'FeedingIt'
21 __author__  = 'Yves Marcoz'
22 __version__ = '0.7.0'
23 __description__ = 'A simple RSS Reader for Maemo 5'
24 # ============================================================================
25
26 import gtk
27 from pango import FontDescription
28 import pango
29 import hildon
30 #import gtkhtml2
31 #try:
32 from webkit import WebView
33 #    has_webkit=True
34 #except:
35 #    import gtkhtml2
36 #    has_webkit=False
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat
39 import gobject
40 from aboutdialog import HeAboutDialog
41 from portrait import FremantleRotation
42 from threading import Thread, activeCount
43 from feedingitdbus import ServerObject
44 from updatedbus import UpdateServerObject, get_lock
45 from config import Config
46 from cgi import escape
47
48 from rss import Listing
49 from opml import GetOpmlData, ExportOpmlData
50
51 from urllib2 import install_opener, build_opener
52
53 from socket import setdefaulttimeout
54 timeout = 5
55 setdefaulttimeout(timeout)
56 del timeout
57
58 LIST_ICON_SIZE = 32
59 LIST_ICON_BORDER = 10
60
61 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
62 ABOUT_ICON = 'feedingit'
63 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
64 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
65 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
66 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
67
68 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
69 unread_color = color_style.lookup_color('ActiveTextColor')
70 read_color = color_style.lookup_color('DefaultTextColor')
71 del color_style
72
73 CONFIGDIR="/home/user/.feedingit/"
74 LOCK = CONFIGDIR + "update.lock"
75
76 from re import sub
77 from htmlentitydefs import name2codepoint
78
79 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
80
81 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
82
83 import style
84
85 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
86 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
87
88 # Build the markup template for the Maemo 5 text style
89 head_font = style.get_font_desc('SystemFont')
90 sub_font = style.get_font_desc('SmallSystemFont')
91
92 head_color = style.get_color('ButtonTextColor')
93 sub_color = style.get_color('DefaultTextColor')
94 active_color = style.get_color('ActiveTextColor')
95
96 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
97 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
98
99 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
100 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
101
102 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
103 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
104
105 entry_active_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), active_color.to_string())
106 entry_active_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), active_color.to_string())
107
108 FEED_TEMPLATE = '\n'.join((head, normal_sub))
109 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
110
111 ENTRY_TEMPLATE = entry_head
112 ENTRY_TEMPLATE_UNREAD = entry_active_head
113
114 ##
115 # Removes HTML or XML character references and entities from a text string.
116 #
117 # @param text The HTML (or XML) source text.
118 # @return The plain text, as a Unicode string, if necessary.
119 # http://effbot.org/zone/re-sub.htm#unescape-html
120 def unescape(text):
121     def fixup(m):
122         text = m.group(0)
123         if text[:2] == "&#":
124             # character reference
125             try:
126                 if text[:3] == "&#x":
127                     return unichr(int(text[3:-1], 16))
128                 else:
129                     return unichr(int(text[2:-1]))
130             except ValueError:
131                 pass
132         else:
133             # named entity
134             try:
135                 text = unichr(name2codepoint[text[1:-1]])
136             except KeyError:
137                 pass
138         return text # leave as is
139     return sub("&#?\w+;", fixup, text)
140
141
142 class AddWidgetWizard(hildon.WizardDialog):
143     
144     def __init__(self, parent, urlIn, titleIn=None):
145         # Create a Notebook
146         self.notebook = gtk.Notebook()
147
148         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
149         self.nameEntry.set_placeholder("Enter Feed Name")
150         vbox = gtk.VBox(False,10)
151         label = gtk.Label("Enter Feed Name:")
152         vbox.pack_start(label)
153         vbox.pack_start(self.nameEntry)
154         if not titleIn == None:
155             self.nameEntry.set_text(titleIn)
156         self.notebook.append_page(vbox, None)
157         
158         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
159         self.urlEntry.set_placeholder("Enter a URL")
160         self.urlEntry.set_text(urlIn)
161         self.urlEntry.select_region(0,-1)
162         
163         vbox = gtk.VBox(False,10)
164         label = gtk.Label("Enter Feed URL:")
165         vbox.pack_start(label)
166         vbox.pack_start(self.urlEntry)
167         self.notebook.append_page(vbox, None)
168
169         labelEnd = gtk.Label("Success")
170         
171         self.notebook.append_page(labelEnd, None)      
172
173         hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
174    
175         # Set a handler for "switch-page" signal
176         #self.notebook.connect("switch_page", self.on_page_switch, self)
177    
178         # Set a function to decide if user can go to next page
179         self.set_forward_page_func(self.some_page_func)
180    
181         self.show_all()
182         
183     def getData(self):
184         return (self.nameEntry.get_text(), self.urlEntry.get_text())
185         
186     def on_page_switch(self, notebook, page, num, dialog):
187         return True
188    
189     def some_page_func(self, nb, current, userdata):
190         # Validate data for 1st page
191         if current == 0:
192             return len(self.nameEntry.get_text()) != 0
193         elif current == 1:
194             # Check the url is not null, and starts with http
195             return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
196         elif current != 2:
197             return False
198         else:
199             return True
200         
201 class Download(Thread):
202     def __init__(self, listing, key, config):
203         Thread.__init__(self)
204         self.listing = listing
205         self.key = key
206         self.config = config
207         
208     def run (self):
209         (use_proxy, proxy) = self.config.getProxy()
210         key_lock = get_lock(self.key)
211         if key_lock != None:
212             if use_proxy:
213                 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
214             else:
215                 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
216         del key_lock
217
218         
219 class DownloadBar(gtk.ProgressBar):
220     def __init__(self, parent, listing, listOfKeys, config, single=False):
221         
222         update_lock = get_lock("update_lock")
223         if update_lock != None:
224             gtk.ProgressBar.__init__(self)
225             self.listOfKeys = listOfKeys[:]
226             self.listing = listing
227             self.total = len(self.listOfKeys)
228             self.config = config
229             self.current = 0
230             self.single = single
231             (use_proxy, proxy) = self.config.getProxy()
232             if use_proxy:
233                 opener = build_opener(proxy)
234             else:
235                 opener = build_opener()
236
237             opener.addheaders = [('User-agent', USER_AGENT)]
238             install_opener(opener)
239
240             if self.total>0:
241                 # In preparation for i18n/l10n
242                 def N_(a, b, n):
243                     return (a if n == 1 else b)
244
245                 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
246
247                 self.fraction = 0
248                 self.set_fraction(self.fraction)
249                 self.show_all()
250                 # Create a timeout
251                 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
252
253     def update_progress_bar(self):
254         #self.progress_bar.pulse()
255         if activeCount() < 4:
256             x = activeCount() - 1
257             k = len(self.listOfKeys)
258             fin = self.total - k - x
259             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
260             #print x, k, fin, fraction
261             self.set_fraction(fraction)
262
263             if len(self.listOfKeys)>0:
264                 self.current = self.current+1
265                 key = self.listOfKeys.pop()
266                 #if self.single == True:
267                     # Check if the feed is being displayed
268                 download = Download(self.listing, key, self.config)
269                 download.start()
270                 return True
271             elif activeCount() > 1:
272                 return True
273             else:
274                 #self.waitingWindow.destroy()
275                 #self.destroy()
276                 try:
277                     del self.update_lock
278                 except:
279                     pass
280                 self.emit("download-done", "success")
281                 return False 
282         return True
283     
284     
285 class SortList(hildon.StackableWindow):
286     def __init__(self, parent, listing, feedingit, after_closing):
287         hildon.StackableWindow.__init__(self)
288         self.set_transient_for(parent)
289         self.set_title('Subscriptions')
290         self.listing = listing
291         self.feedingit = feedingit
292         self.after_closing = after_closing
293         self.connect('destroy', lambda w: self.after_closing())
294         self.vbox2 = gtk.VBox(False, 2)
295
296         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
297         button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
298         button.connect("clicked", self.buttonUp)
299         self.vbox2.pack_start(button, expand=False, fill=False)
300
301         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
302         button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
303         button.connect("clicked", self.buttonDown)
304         self.vbox2.pack_start(button, expand=False, fill=False)
305
306         self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
307
308         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
309         button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
310         button.connect("clicked", self.buttonAdd)
311         self.vbox2.pack_start(button, expand=False, fill=False)
312
313         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
314         button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
315         button.connect("clicked", self.buttonEdit)
316         self.vbox2.pack_start(button, expand=False, fill=False)
317
318         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
319         button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
320         button.connect("clicked", self.buttonDelete)
321         self.vbox2.pack_start(button, expand=False, fill=False)
322
323         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
324         #button.set_label("Done")
325         #button.connect("clicked", self.buttonDone)
326         #self.vbox.pack_start(button)
327         self.hbox2= gtk.HBox(False, 10)
328         self.pannableArea = hildon.PannableArea()
329         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
330         self.treeview = gtk.TreeView(self.treestore)
331         self.hbox2.pack_start(self.pannableArea, expand=True)
332         self.displayFeeds()
333         self.hbox2.pack_end(self.vbox2, expand=False)
334         self.set_default_size(-1, 600)
335         self.add(self.hbox2)
336
337         menu = hildon.AppMenu()
338         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
339         button.set_label("Import from OPML")
340         button.connect("clicked", self.feedingit.button_import_clicked)
341         menu.append(button)
342
343         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
344         button.set_label("Export to OPML")
345         button.connect("clicked", self.feedingit.button_export_clicked)
346         menu.append(button)
347         self.set_app_menu(menu)
348         menu.show_all()
349         
350         self.show_all()
351         #self.connect("destroy", self.buttonDone)
352         
353     def displayFeeds(self):
354         self.treeview.destroy()
355         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
356         self.treeview = gtk.TreeView()
357         
358         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
359         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
360         self.refreshList()
361         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
362
363         self.pannableArea.add(self.treeview)
364
365         #self.show_all()
366
367     def refreshList(self, selected=None, offset=0):
368         #rect = self.treeview.get_visible_rect()
369         #y = rect.y+rect.height
370         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
371         for key in self.listing.getListOfFeeds():
372             item = self.treestore.append([self.listing.getFeedTitle(key), key])
373             if key == selected:
374                 selectedItem = item
375         self.treeview.set_model(self.treestore)
376         if not selected == None:
377             self.treeview.get_selection().select_iter(selectedItem)
378             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
379         self.pannableArea.show_all()
380
381     def getSelectedItem(self):
382         (model, iter) = self.treeview.get_selection().get_selected()
383         if not iter:
384             return None
385         return model.get_value(iter, 1)
386
387     def findIndex(self, key):
388         after = None
389         before = None
390         found = False
391         for row in self.treestore:
392             if found:
393                 return (before, row.iter)
394             if key == list(row)[0]:
395                 found = True
396             else:
397                 before = row.iter
398         return (before, None)
399
400     def buttonUp(self, button):
401         key  = self.getSelectedItem()
402         if not key == None:
403             self.listing.moveUp(key)
404             self.refreshList(key, -10)
405
406     def buttonDown(self, button):
407         key = self.getSelectedItem()
408         if not key == None:
409             self.listing.moveDown(key)
410             self.refreshList(key, 10)
411
412     def buttonDelete(self, button):
413         key = self.getSelectedItem()
414         if not key == None:
415             self.listing.removeFeed(key)
416         self.refreshList()
417
418     def buttonEdit(self, button):
419         key = self.getSelectedItem()
420         if not key == None:
421             wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
422             ret = wizard.run()
423             if ret == 2:
424                 (title, url) = wizard.getData()
425                 if (not title == '') and (not url == ''):
426                     self.listing.editFeed(key, title, url)
427             wizard.destroy()
428         self.refreshList()
429
430     def buttonDone(self, *args):
431         self.destroy()
432         
433     def buttonAdd(self, button, urlIn="http://"):
434         wizard = AddWidgetWizard(self, urlIn)
435         ret = wizard.run()
436         if ret == 2:
437             (title, url) = wizard.getData()
438             if (not title == '') and (not url == ''): 
439                self.listing.addFeed(title, url)
440         wizard.destroy()
441         self.refreshList()
442                
443
444 class DisplayArticle(hildon.StackableWindow):
445     def __init__(self, feed, id, key, config, listing):
446         hildon.StackableWindow.__init__(self)
447         #self.imageDownloader = ImageDownloader()
448         self.feed = feed
449         self.listing=listing
450         self.key = key
451         self.id = id
452         #self.set_title(feed.getTitle(id))
453         self.set_title(self.listing.getFeedTitle(key))
454         self.config = config
455         self.set_for_removal = False
456         
457         # Init the article display
458         #if self.config.getWebkitSupport():
459         self.view = WebView()
460             #self.view.set_editable(False)
461         #else:
462         #    import gtkhtml2
463         #    self.view = gtkhtml2.View()
464         #    self.document = gtkhtml2.Document()
465         #    self.view.set_document(self.document)
466         #    self.document.connect("link_clicked", self._signal_link_clicked)
467         self.pannable_article = hildon.PannableArea()
468         self.pannable_article.add(self.view)
469         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
470         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
471
472         #if self.config.getWebkitSupport():
473         contentLink = self.feed.getContentLink(self.id)
474         self.feed.setEntryRead(self.id)
475         #if key=="ArchivedArticles":
476         if contentLink.startswith("/home/user/"):
477             self.view.open("file://" + contentLink)
478         else:
479             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
480         self.view.connect("motion-notify-event", lambda w,ev: True)
481         self.view.connect('load-started', self.load_started)
482         self.view.connect('load-finished', self.load_finished)
483
484         #else:
485         #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
486         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
487         #else:
488         #    if not key == "ArchivedArticles":
489                 # Do not download images if the feed is "Archived Articles"
490         #        self.document.connect("request-url", self._signal_request_url)
491             
492         #    self.document.clear()
493         #    self.document.open_stream("text/html")
494         #    self.document.write_stream(self.text)
495         #    self.document.close_stream()
496         
497         menu = hildon.AppMenu()
498         # Create a button and add it to the menu
499         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
500         button.set_label("Allow horizontal scrolling")
501         button.connect("clicked", self.horiz_scrolling_button)
502         menu.append(button)
503         
504         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
505         button.set_label("Open in browser")
506         button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
507         menu.append(button)
508         
509         if key == "ArchivedArticles":
510             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
511             button.set_label("Remove from archived articles")
512             button.connect("clicked", self.remove_archive_button)
513         else:
514             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
515             button.set_label("Add to archived articles")
516             button.connect("clicked", self.archive_button)
517         menu.append(button)
518         
519         self.set_app_menu(menu)
520         menu.show_all()
521         
522         #self.event_box = gtk.EventBox()
523         #self.event_box.add(self.pannable_article)
524         self.add(self.pannable_article)
525         
526         
527         self.pannable_article.show_all()
528
529         self.destroyId = self.connect("destroy", self.destroyWindow)
530         
531         self.view.connect("button_press_event", self.button_pressed)
532         self.gestureId = self.view.connect("button_release_event", self.button_released)
533         #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
534
535     def load_started(self, *widget):
536         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
537         
538     def load_finished(self, *widget):
539         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
540
541     def button_pressed(self, window, event):
542         #print event.x, event.y
543         self.coords = (event.x, event.y)
544         
545     def button_released(self, window, event):
546         x = self.coords[0] - event.x
547         y = self.coords[1] - event.y
548         
549         if (2*abs(y) < abs(x)):
550             if (x > 15):
551                 self.emit("article-previous", self.id)
552             elif (x<-15):
553                 self.emit("article-next", self.id)   
554         #print x, y
555         #print "Released"
556
557     #def gesture(self, widget, direction, startx, starty):
558     #    if (direction == 3):
559     #        self.emit("article-next", self.index)
560     #    if (direction == 2):
561     #        self.emit("article-previous", self.index)
562         #print startx, starty
563         #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
564
565     def destroyWindow(self, *args):
566         self.disconnect(self.destroyId)
567         if self.set_for_removal:
568             self.emit("article-deleted", self.id)
569         else:
570             self.emit("article-closed", self.id)
571         #self.imageDownloader.stopAll()
572         self.destroy()
573         
574     def horiz_scrolling_button(self, *widget):
575         self.pannable_article.disconnect(self.gestureId)
576         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
577         
578     def archive_button(self, *widget):
579         # Call the listing.addArchivedArticle
580         self.listing.addArchivedArticle(self.key, self.id)
581         
582     def remove_archive_button(self, *widget):
583         self.set_for_removal = True
584         
585     #def reloadArticle(self, *widget):
586     #    if threading.activeCount() > 1:
587             # Image thread are still running, come back in a bit
588     #        return True
589     #    else:
590     #        for (stream, imageThread) in self.images:
591     #            imageThread.join()
592     #            stream.write(imageThread.data)
593     #            stream.close()
594     #        return False
595     #    self.show_all()
596
597     def _signal_link_clicked(self, object, link):
598         import dbus
599         bus = dbus.SessionBus()
600         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
601         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
602         iface.open_new_window(link)
603
604     #def _signal_request_url(self, object, url, stream):
605         #print url
606     #    self.imageDownloader.queueImage(url, stream)
607         #imageThread = GetImage(url)
608         #imageThread.start()
609         #self.images.append((stream, imageThread))
610
611
612 class DisplayFeed(hildon.StackableWindow):
613     def __init__(self, listing, feed, title, key, config, updateDbusHandler):
614         hildon.StackableWindow.__init__(self)
615         self.listing = listing
616         self.feed = feed
617         self.feedTitle = title
618         self.set_title(title)
619         self.key=key
620         self.config = config
621         self.updateDbusHandler = updateDbusHandler
622         
623         self.downloadDialog = False
624         
625         #self.listing.setCurrentlyDisplayedFeed(self.key)
626         
627         self.disp = False
628         
629         menu = hildon.AppMenu()
630         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631         button.set_label("Update feed")
632         button.connect("clicked", self.button_update_clicked)
633         menu.append(button)
634         
635         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
636         button.set_label("Mark all as read")
637         button.connect("clicked", self.buttonReadAllClicked)
638         menu.append(button)
639         
640         if key=="ArchivedArticles":
641             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
642             button.set_label("Delete read articles")
643             button.connect("clicked", self.buttonPurgeArticles)
644             menu.append(button)
645         
646         self.set_app_menu(menu)
647         menu.show_all()
648         
649         self.displayFeed()
650         
651         self.connect('configure-event', self.on_configure_event)
652         self.connect("destroy", self.destroyWindow)
653
654     def on_configure_event(self, window, event):
655         if getattr(self, 'markup_renderer', None) is None:
656             return
657
658         # Fix up the column width for wrapping the text when the window is
659         # resized (i.e. orientation changed)
660         self.markup_renderer.set_property('wrap-width', event.width-20)  
661         it = self.feedItems.get_iter_first()
662         while it is not None:
663             markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
664             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
665             it = self.feedItems.iter_next(it)
666
667     def destroyWindow(self, *args):
668         #self.feed.saveUnread(CONFIGDIR)
669         gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
670         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
671         self.emit("feed-closed", self.key)
672         self.destroy()
673         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
674         #self.listing.closeCurrentlyDisplayedFeed()
675
676     def fix_title(self, title):
677         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
678
679     def displayFeed(self):
680         self.pannableFeed = hildon.PannableArea()
681
682         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
683
684         self.feedItems = gtk.ListStore(str, str)
685         #self.feedList = gtk.TreeView(self.feedItems)
686         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
687         selection = self.feedList.get_selection()
688         selection.set_mode(gtk.SELECTION_NONE)
689         #selection.connect("changed", lambda w: True)
690         
691         self.feedList.set_model(self.feedItems)
692         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
693
694         
695         self.feedList.set_hover_selection(False)
696         #self.feedList.set_property('enable-grid-lines', True)
697         #self.feedList.set_property('hildon-mode', 1)
698         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
699         
700         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
701
702         vbox= gtk.VBox(False, 10)
703         vbox.pack_start(self.feedList)
704         
705         self.pannableFeed.add_with_viewport(vbox)
706
707         self.markup_renderer = gtk.CellRendererText()
708         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
709         (width, height) = self.get_size()
710         self.markup_renderer.set_property('wrap-width', width-20)
711         self.markup_renderer.set_property('ypad', 5)
712         self.markup_renderer.set_property('xpad', 5)
713         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
714                 markup=FEED_COLUMN_MARKUP)
715         self.feedList.append_column(markup_column)
716
717         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
718         hideReadArticles = self.config.getHideReadArticles()
719         hasArticle = False
720         for id in self.feed.getIds():
721             isRead = False
722             try:
723                 isRead = self.feed.isEntryRead(id)
724             except:
725                 pass
726             if not ( isRead and hideReadArticles ):
727             #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
728                 #title = self.feed.getTitle(id)
729                 title = self.fix_title(self.feed.getTitle(id))
730     
731                 #if self.feed.isEntryRead(id):
732                 if isRead:
733                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
734                 else:
735                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
736     
737                 self.feedItems.append((markup, id))
738                 hasArticle = True
739         if hasArticle:
740             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
741         else:
742             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
743             self.feedItems.append((markup, ""))
744
745         self.add(self.pannableFeed)
746         self.show_all()
747
748     def clear(self):
749         self.pannableFeed.destroy()
750         #self.remove(self.pannableFeed)
751
752     def on_feedList_row_activated(self, treeview, path): #, column):
753         selection = self.feedList.get_selection()
754         selection.set_mode(gtk.SELECTION_SINGLE)
755         self.feedList.get_selection().select_path(path)
756         model = treeview.get_model()
757         iter = model.get_iter(path)
758         key = model.get_value(iter, FEED_COLUMN_KEY)
759         # Emulate legacy "button_clicked" call via treeview
760         gobject.idle_add(self.button_clicked, treeview, key)
761         #return True
762
763     def button_clicked(self, button, index, previous=False, next=False):
764         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
765         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
766         stack = hildon.WindowStack.get_default()
767         if previous:
768             tmp = stack.peek()
769             stack.pop_and_push(1, newDisp, tmp)
770             newDisp.show()
771             gobject.timeout_add(200, self.destroyArticle, tmp)
772             #print "previous"
773             self.disp = newDisp
774         elif next:
775             newDisp.show_all()
776             if type(self.disp).__name__ == "DisplayArticle":
777                 gobject.timeout_add(200, self.destroyArticle, self.disp)
778             self.disp = newDisp
779         else:
780             self.disp = newDisp
781             self.disp.show_all()
782         
783         self.ids = []
784         if self.key == "ArchivedArticles":
785             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
786         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
787         self.ids.append(self.disp.connect("article-next", self.nextArticle))
788         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
789
790     def buttonPurgeArticles(self, *widget):
791         self.clear()
792         self.feed.purgeReadArticles()
793         self.feed.saveUnread(CONFIGDIR)
794         self.feed.saveFeed(CONFIGDIR)
795         self.displayFeed()
796
797     def destroyArticle(self, handle):
798         handle.destroyWindow()
799
800     def mark_item_read(self, key):
801         it = self.feedItems.get_iter_first()
802         while it is not None:
803             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
804             if k == key:
805                 title = self.fix_title(self.feed.getTitle(key))
806                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
807                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
808                 break
809             it = self.feedItems.iter_next(it)
810
811     def nextArticle(self, object, index):
812         self.mark_item_read(index)
813         id = self.feed.getNextId(index)
814         if self.config.getHideReadArticles():
815             isRead = False
816             try:
817                 isRead = self.feed.isEntryRead(id)
818             except:
819                 pass
820             while isRead and id != index:
821                 id = self.feed.getNextId(id)
822                 isRead = False
823                 try:
824                        isRead = self.feed.isEntryRead(id)
825                 except:
826                        pass
827         if id != index:
828             self.button_clicked(object, id, next=True)
829
830     def previousArticle(self, object, index):
831         self.mark_item_read(index)
832         id = self.feed.getPreviousId(index)
833         if self.config.getHideReadArticles():
834             isRead = False
835             try:
836                 isRead = self.feed.isEntryRead(id)
837             except:
838                 pass
839             while isRead and id != index:
840                 id = self.feed.getPreviousId(id)
841                 isRead = False
842                 try:
843                        isRead = self.feed.isEntryRead(id)
844                 except:
845                        pass
846         if id != index:
847             self.button_clicked(object, id, previous=True)
848
849     def onArticleClosed(self, object, index):
850         selection = self.feedList.get_selection()
851         selection.set_mode(gtk.SELECTION_NONE)
852         self.mark_item_read(index)
853
854     def onArticleDeleted(self, object, index):
855         self.clear()
856         self.feed.removeArticle(index)
857         self.feed.saveUnread(CONFIGDIR)
858         self.feed.saveFeed(CONFIGDIR)
859         self.displayFeed()
860
861     def button_update_clicked(self, button):
862         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
863         if not type(self.downloadDialog).__name__=="DownloadBar":
864             self.pannableFeed.destroy()
865             self.vbox = gtk.VBox(False, 10)
866             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
867             self.downloadDialog.connect("download-done", self.onDownloadsDone)
868             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
869             self.add(self.vbox)
870             self.show_all()
871             
872     def onDownloadsDone(self, *widget):
873         self.vbox.destroy()
874         self.feed = self.listing.getFeed(self.key)
875         self.displayFeed()
876         self.updateDbusHandler.ArticleCountUpdated()
877         
878     def buttonReadAllClicked(self, button):
879         for index in self.feed.getIds():
880             self.feed.setEntryRead(index)
881             self.mark_item_read(index)
882
883
884 class FeedingIt:
885     def __init__(self):
886         # Init the windows
887         self.window = hildon.StackableWindow()
888         self.window.set_title(__appname__)
889         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
890         self.mainVbox = gtk.VBox(False,10)
891         
892         self.introLabel = gtk.Label("Loading...")
893
894         
895         self.mainVbox.pack_start(self.introLabel)
896
897         self.window.add(self.mainVbox)
898         self.window.show_all()
899         self.config = Config(self.window, CONFIGDIR+"config.ini")
900         gobject.idle_add(self.createWindow)
901         
902     def createWindow(self):
903         self.app_lock = get_lock("app_lock")
904         if self.app_lock == None:
905             self.introLabel.set_label("Update in progress, please wait.")
906             gobject.timeout_add_seconds(3, self.createWindow)
907             return False
908         self.listing = Listing(CONFIGDIR)
909         
910         self.downloadDialog = False
911         try:
912             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
913             self.orientation.set_mode(self.config.getOrientation())
914         except:
915             print "Could not start rotation manager"
916         
917         menu = hildon.AppMenu()
918         # Create a button and add it to the menu
919         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
920         button.set_label("Update feeds")
921         button.connect("clicked", self.button_update_clicked, "All")
922         menu.append(button)
923         
924         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
925         button.set_label("Mark all as read")
926         button.connect("clicked", self.button_markAll)
927         menu.append(button)
928         
929         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
930         button.set_label("Manage subscriptions")
931         button.connect("clicked", self.button_organize_clicked)
932         menu.append(button)
933
934         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
935         button.set_label("Settings")
936         button.connect("clicked", self.button_preferences_clicked)
937         menu.append(button)
938        
939         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
940         button.set_label("About")
941         button.connect("clicked", self.button_about_clicked)
942         menu.append(button)
943         
944         self.window.set_app_menu(menu)
945         menu.show_all()
946         
947         #self.feedWindow = hildon.StackableWindow()
948         #self.articleWindow = hildon.StackableWindow()
949         self.introLabel.destroy()
950         self.pannableListing = hildon.PannableArea()
951         self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
952         self.feedList = gtk.TreeView(self.feedItems)
953         self.feedList.connect('row-activated', self.on_feedList_row_activated)
954         self.pannableListing.add(self.feedList)
955
956         icon_renderer = gtk.CellRendererPixbuf()
957         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
958         icon_column = gtk.TreeViewColumn('', icon_renderer, \
959                 pixbuf=COLUMN_ICON)
960         self.feedList.append_column(icon_column)
961
962         markup_renderer = gtk.CellRendererText()
963         markup_column = gtk.TreeViewColumn('', markup_renderer, \
964                 markup=COLUMN_MARKUP)
965         self.feedList.append_column(markup_column)
966         self.mainVbox.pack_start(self.pannableListing)
967         self.mainVbox.show_all()
968
969         self.displayListing()
970         self.autoupdate = False
971         self.checkAutoUpdate()
972         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
973         gobject.idle_add(self.enableDbus)
974         
975     def enableDbus(self):
976         self.dbusHandler = ServerObject(self)
977         self.updateDbusHandler = UpdateServerObject(self)
978
979     def button_markAll(self, button):
980         for key in self.listing.getListOfFeeds():
981             feed = self.listing.getFeed(key)
982             for id in feed.getIds():
983                 feed.setEntryRead(id)
984             feed.saveUnread(CONFIGDIR)
985             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
986         self.displayListing()
987
988     def button_about_clicked(self, button):
989         HeAboutDialog.present(self.window, \
990                 __appname__, \
991                 ABOUT_ICON, \
992                 __version__, \
993                 __description__, \
994                 ABOUT_COPYRIGHT, \
995                 ABOUT_WEBSITE, \
996                 ABOUT_BUGTRACKER, \
997                 ABOUT_DONATE)
998
999     def button_export_clicked(self, button):
1000         opml = ExportOpmlData(self.window, self.listing)
1001         
1002     def button_import_clicked(self, button):
1003         opml = GetOpmlData(self.window)
1004         feeds = opml.getData()
1005         for (title, url) in feeds:
1006             self.listing.addFeed(title, url)
1007         self.displayListing()
1008
1009     def addFeed(self, urlIn="http://"):
1010         wizard = AddWidgetWizard(self.window, urlIn)
1011         ret = wizard.run()
1012         if ret == 2:
1013             (title, url) = wizard.getData()
1014             if (not title == '') and (not url == ''): 
1015                self.listing.addFeed(title, url)
1016         wizard.destroy()
1017         self.displayListing()
1018
1019     def button_organize_clicked(self, button):
1020         def after_closing():
1021             self.listing.saveConfig()
1022             self.displayListing()
1023         SortList(self.window, self.listing, self, after_closing)
1024
1025     def button_update_clicked(self, button, key):
1026         if not type(self.downloadDialog).__name__=="DownloadBar":
1027             self.updateDbusHandler.UpdateStarted()
1028             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1029             self.downloadDialog.connect("download-done", self.onDownloadsDone)
1030             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1031             self.mainVbox.show_all()
1032         #self.displayListing()
1033
1034     def onDownloadsDone(self, *widget):
1035         self.downloadDialog.destroy()
1036         self.downloadDialog = False
1037         self.displayListing()
1038         self.updateDbusHandler.UpdateFinished()
1039         self.updateDbusHandler.ArticleCountUpdated()
1040
1041     def button_preferences_clicked(self, button):
1042         dialog = self.config.createDialog()
1043         dialog.connect("destroy", self.prefsClosed)
1044
1045     def show_confirmation_note(self, parent, title):
1046         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1047
1048         retcode = gtk.Dialog.run(note)
1049         note.destroy()
1050         
1051         if retcode == gtk.RESPONSE_OK:
1052             return True
1053         else:
1054             return False
1055         
1056     def displayListing(self):
1057         icon_theme = gtk.icon_theme_get_default()
1058         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1059                 gtk.ICON_LOOKUP_USE_BUILTIN)
1060
1061         self.feedItems.clear()
1062         for key in self.listing.getListOfFeeds():
1063             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1064             if unreadItems > 0 or not self.config.getHideReadFeeds():
1065                 title = self.listing.getFeedTitle(key)
1066                 updateTime = self.listing.getFeedUpdateTime(key)
1067                 
1068                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1069     
1070                 if unreadItems:
1071                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1072                 else:
1073                     markup = FEED_TEMPLATE % (title, subtitle)
1074     
1075                 try:
1076                     icon_filename = self.listing.getFavicon(key)
1077                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1078                             LIST_ICON_SIZE, LIST_ICON_SIZE)
1079                 except:
1080                     pixbuf = default_pixbuf
1081     
1082                 self.feedItems.append((pixbuf, markup, key))
1083
1084     def on_feedList_row_activated(self, treeview, path, column):
1085         model = treeview.get_model()
1086         iter = model.get_iter(path)
1087         key = model.get_value(iter, COLUMN_KEY)
1088         self.openFeed(key)
1089             
1090     def openFeed(self, key):
1091         try:
1092             self.feed_lock
1093         except:
1094             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1095             self.feed_lock = get_lock(key)
1096             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1097                     self.listing.getFeedTitle(key), key, \
1098                     self.config, self.updateDbusHandler)
1099             self.disp.connect("feed-closed", self.onFeedClosed)
1100         
1101
1102     def onFeedClosed(self, object, key):
1103         #self.listing.saveConfig()
1104         #del self.feed_lock
1105         gobject.idle_add(self.onFeedClosedTimeout)
1106         self.displayListing()
1107         #self.updateDbusHandler.ArticleCountUpdated()
1108         
1109     def onFeedClosedTimeout(self):
1110         self.listing.saveConfig()
1111         del self.feed_lock
1112         self.updateDbusHandler.ArticleCountUpdated()
1113      
1114     def run(self):
1115         self.window.connect("destroy", gtk.main_quit)
1116         gtk.main()
1117         self.listing.saveConfig()
1118         del self.app_lock
1119
1120     def prefsClosed(self, *widget):
1121         try:
1122             self.orientation.set_mode(self.config.getOrientation())
1123         except:
1124             pass
1125         self.displayListing()
1126         self.checkAutoUpdate()
1127
1128     def checkAutoUpdate(self, *widget):
1129         interval = int(self.config.getUpdateInterval()*3600000)
1130         if self.config.isAutoUpdateEnabled():
1131             if self.autoupdate == False:
1132                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1133                 self.autoupdate = interval
1134             elif not self.autoupdate == interval:
1135                 # If auto-update is enabled, but not at the right frequency
1136                 gobject.source_remove(self.autoupdateId)
1137                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1138                 self.autoupdate = interval
1139         else:
1140             if not self.autoupdate == False:
1141                 gobject.source_remove(self.autoupdateId)
1142                 self.autoupdate = False
1143
1144     def automaticUpdate(self, *widget):
1145         # Need to check for internet connection
1146         # If no internet connection, try again in 10 minutes:
1147         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1148         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1149         #from time import localtime, strftime
1150         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1151         #file.close()
1152         self.button_update_clicked(None, None)
1153         return True
1154     
1155     def stopUpdate(self):
1156         # Not implemented in the app (see update_feeds.py)
1157         try:
1158             self.downloadDialog.listOfKeys = []
1159         except:
1160             pass
1161     
1162     def getStatus(self):
1163         status = ""
1164         for key in self.listing.getListOfFeeds():
1165             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1166                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1167         if status == "":
1168             status = "No unread items"
1169         return status
1170
1171 if __name__ == "__main__":
1172     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1173     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1174     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1175     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1176     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1177     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1178     gobject.threads_init()
1179     if not isdir(CONFIGDIR):
1180         try:
1181             mkdir(CONFIGDIR)
1182         except:
1183             print "Error: Can't create configuration directory"
1184             from sys import exit
1185             exit(1)
1186     app = FeedingIt()
1187     app.run()