Modified TreeView for article listing, and for the widget
[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.6.2'
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                 opener.addheaders = [('User-agent', USER_AGENT)]
235                 install_opener(opener)
236             else:
237                 opener = build_opener()
238                 opener.addheaders = [('User-agent', USER_AGENT)]
239                 install_opener(opener)
240
241             if self.total>0:
242                 self.set_text("Updating...")
243                 self.fraction = 0
244                 self.set_fraction(self.fraction)
245                 self.show_all()
246                 # Create a timeout
247                 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
248
249     def update_progress_bar(self):
250         #self.progress_bar.pulse()
251         if activeCount() < 4:
252             x = activeCount() - 1
253             k = len(self.listOfKeys)
254             fin = self.total - k - x
255             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
256             #print x, k, fin, fraction
257             self.set_fraction(fraction)
258
259             if len(self.listOfKeys)>0:
260                 self.current = self.current+1
261                 key = self.listOfKeys.pop()
262                 #if self.single == True:
263                     # Check if the feed is being displayed
264                 download = Download(self.listing, key, self.config)
265                 download.start()
266                 return True
267             elif activeCount() > 1:
268                 return True
269             else:
270                 #self.waitingWindow.destroy()
271                 #self.destroy()
272                 try:
273                     del self.update_lock
274                 except:
275                     pass
276                 self.emit("download-done", "success")
277                 return False 
278         return True
279     
280     
281 class SortList(gtk.Dialog):
282     def __init__(self, parent, listing):
283         gtk.Dialog.__init__(self, "Organizer",  parent)
284         self.listing = listing
285         
286         self.vbox2 = gtk.VBox(False, 10)
287         
288         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
289         button.set_label("Move Up")
290         button.connect("clicked", self.buttonUp)
291         self.vbox2.pack_start(button, expand=False, fill=False)
292         
293         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
294         button.set_label("Move Down")
295         button.connect("clicked", self.buttonDown)
296         self.vbox2.pack_start(button, expand=False, fill=False)
297
298         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
299         button.set_label("Add Feed")
300         button.connect("clicked", self.buttonAdd)
301         self.vbox2.pack_start(button, expand=False, fill=False)
302
303         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
304         button.set_label("Edit Feed")
305         button.connect("clicked", self.buttonEdit)
306         self.vbox2.pack_start(button, expand=False, fill=False)
307         
308         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
309         button.set_label("Delete")
310         button.connect("clicked", self.buttonDelete)
311         self.vbox2.pack_start(button, expand=False, fill=False)
312         
313         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
314         #button.set_label("Done")
315         #button.connect("clicked", self.buttonDone)
316         #self.vbox.pack_start(button)
317         self.hbox2= gtk.HBox(False, 10)
318         self.pannableArea = hildon.PannableArea()
319         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
320         self.treeview = gtk.TreeView(self.treestore)
321         self.hbox2.pack_start(self.pannableArea, expand=True)
322         self.displayFeeds()
323         self.hbox2.pack_end(self.vbox2, expand=False)
324         self.set_default_size(-1, 600)
325         self.vbox.pack_start(self.hbox2)
326         
327         self.show_all()
328         #self.connect("destroy", self.buttonDone)
329         
330     def displayFeeds(self):
331         self.treeview.destroy()
332         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
333         self.treeview = gtk.TreeView()
334         
335         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
336         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
337         self.refreshList()
338         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
339
340         self.pannableArea.add(self.treeview)
341
342         #self.show_all()
343
344     def refreshList(self, selected=None, offset=0):
345         #rect = self.treeview.get_visible_rect()
346         #y = rect.y+rect.height
347         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
348         for key in self.listing.getListOfFeeds():
349             item = self.treestore.append([self.listing.getFeedTitle(key), key])
350             if key == selected:
351                 selectedItem = item
352         self.treeview.set_model(self.treestore)
353         if not selected == None:
354             self.treeview.get_selection().select_iter(selectedItem)
355             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
356         self.pannableArea.show_all()
357
358     def getSelectedItem(self):
359         (model, iter) = self.treeview.get_selection().get_selected()
360         if not iter:
361             return None
362         return model.get_value(iter, 1)
363
364     def findIndex(self, key):
365         after = None
366         before = None
367         found = False
368         for row in self.treestore:
369             if found:
370                 return (before, row.iter)
371             if key == list(row)[0]:
372                 found = True
373             else:
374                 before = row.iter
375         return (before, None)
376
377     def buttonUp(self, button):
378         key  = self.getSelectedItem()
379         if not key == None:
380             self.listing.moveUp(key)
381             self.refreshList(key, -10)
382
383     def buttonDown(self, button):
384         key = self.getSelectedItem()
385         if not key == None:
386             self.listing.moveDown(key)
387             self.refreshList(key, 10)
388
389     def buttonDelete(self, button):
390         key = self.getSelectedItem()
391         if not key == None:
392             self.listing.removeFeed(key)
393         self.refreshList()
394
395     def buttonEdit(self, button):
396         key = self.getSelectedItem()
397         if not key == None:
398             wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
399             ret = wizard.run()
400             if ret == 2:
401                 (title, url) = wizard.getData()
402                 if (not title == '') and (not url == ''):
403                     self.listing.editFeed(key, title, url)
404             wizard.destroy()
405         self.refreshList()
406
407     def buttonDone(self, *args):
408         self.destroy()
409         
410     def buttonAdd(self, button, urlIn="http://"):
411         wizard = AddWidgetWizard(self, urlIn)
412         ret = wizard.run()
413         if ret == 2:
414             (title, url) = wizard.getData()
415             if (not title == '') and (not url == ''): 
416                self.listing.addFeed(title, url)
417         wizard.destroy()
418         self.refreshList()
419                
420
421 class DisplayArticle(hildon.StackableWindow):
422     def __init__(self, feed, id, key, config, listing):
423         hildon.StackableWindow.__init__(self)
424         #self.imageDownloader = ImageDownloader()
425         self.feed = feed
426         self.listing=listing
427         self.key = key
428         self.id = id
429         #self.set_title(feed.getTitle(id))
430         self.set_title(self.listing.getFeedTitle(key))
431         self.config = config
432         self.set_for_removal = False
433         
434         # Init the article display
435         #if self.config.getWebkitSupport():
436         self.view = WebView()
437             #self.view.set_editable(False)
438         #else:
439         #    import gtkhtml2
440         #    self.view = gtkhtml2.View()
441         #    self.document = gtkhtml2.Document()
442         #    self.view.set_document(self.document)
443         #    self.document.connect("link_clicked", self._signal_link_clicked)
444         self.pannable_article = hildon.PannableArea()
445         self.pannable_article.add(self.view)
446         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
447         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
448
449         #if self.config.getWebkitSupport():
450         contentLink = self.feed.getContentLink(self.id)
451         self.feed.setEntryRead(self.id)
452         #if key=="ArchivedArticles":
453         if contentLink.startswith("/home/user/"):
454             self.view.open("file://" + contentLink)
455         else:
456             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
457         self.view.connect("motion-notify-event", lambda w,ev: True)
458         self.view.connect('load-started', self.load_started)
459         self.view.connect('load-finished', self.load_finished)
460
461         #else:
462         #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
463         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
464         #else:
465         #    if not key == "ArchivedArticles":
466                 # Do not download images if the feed is "Archived Articles"
467         #        self.document.connect("request-url", self._signal_request_url)
468             
469         #    self.document.clear()
470         #    self.document.open_stream("text/html")
471         #    self.document.write_stream(self.text)
472         #    self.document.close_stream()
473         
474         menu = hildon.AppMenu()
475         # Create a button and add it to the menu
476         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
477         button.set_label("Allow Horizontal Scrolling")
478         button.connect("clicked", self.horiz_scrolling_button)
479         menu.append(button)
480         
481         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
482         button.set_label("Open in Browser")
483         button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
484         menu.append(button)
485         
486         if key == "ArchivedArticles":
487             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
488             button.set_label("Remove from Archived Articles")
489             button.connect("clicked", self.remove_archive_button)
490         else:
491             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
492             button.set_label("Add to Archived Articles")
493             button.connect("clicked", self.archive_button)
494         menu.append(button)
495         
496         self.set_app_menu(menu)
497         menu.show_all()
498         
499         #self.event_box = gtk.EventBox()
500         #self.event_box.add(self.pannable_article)
501         self.add(self.pannable_article)
502         
503         
504         self.pannable_article.show_all()
505
506         self.destroyId = self.connect("destroy", self.destroyWindow)
507         
508         self.view.connect("button_press_event", self.button_pressed)
509         self.gestureId = self.view.connect("button_release_event", self.button_released)
510         #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
511
512     def load_started(self, *widget):
513         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
514         
515     def load_finished(self, *widget):
516         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
517
518     def button_pressed(self, window, event):
519         #print event.x, event.y
520         self.coords = (event.x, event.y)
521         
522     def button_released(self, window, event):
523         x = self.coords[0] - event.x
524         y = self.coords[1] - event.y
525         
526         if (2*abs(y) < abs(x)):
527             if (x > 15):
528                 self.emit("article-previous", self.id)
529             elif (x<-15):
530                 self.emit("article-next", self.id)   
531         #print x, y
532         #print "Released"
533
534     #def gesture(self, widget, direction, startx, starty):
535     #    if (direction == 3):
536     #        self.emit("article-next", self.index)
537     #    if (direction == 2):
538     #        self.emit("article-previous", self.index)
539         #print startx, starty
540         #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
541
542     def destroyWindow(self, *args):
543         self.disconnect(self.destroyId)
544         if self.set_for_removal:
545             self.emit("article-deleted", self.id)
546         else:
547             self.emit("article-closed", self.id)
548         #self.imageDownloader.stopAll()
549         self.destroy()
550         
551     def horiz_scrolling_button(self, *widget):
552         self.pannable_article.disconnect(self.gestureId)
553         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
554         
555     def archive_button(self, *widget):
556         # Call the listing.addArchivedArticle
557         self.listing.addArchivedArticle(self.key, self.id)
558         
559     def remove_archive_button(self, *widget):
560         self.set_for_removal = True
561         
562     #def reloadArticle(self, *widget):
563     #    if threading.activeCount() > 1:
564             # Image thread are still running, come back in a bit
565     #        return True
566     #    else:
567     #        for (stream, imageThread) in self.images:
568     #            imageThread.join()
569     #            stream.write(imageThread.data)
570     #            stream.close()
571     #        return False
572     #    self.show_all()
573
574     def _signal_link_clicked(self, object, link):
575         import dbus
576         bus = dbus.SessionBus()
577         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
578         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
579         iface.open_new_window(link)
580
581     #def _signal_request_url(self, object, url, stream):
582         #print url
583     #    self.imageDownloader.queueImage(url, stream)
584         #imageThread = GetImage(url)
585         #imageThread.start()
586         #self.images.append((stream, imageThread))
587
588
589 class DisplayFeed(hildon.StackableWindow):
590     def __init__(self, listing, feed, title, key, config, updateDbusHandler):
591         hildon.StackableWindow.__init__(self)
592         self.listing = listing
593         self.feed = feed
594         self.feedTitle = title
595         self.set_title(title)
596         self.key=key
597         self.config = config
598         self.updateDbusHandler = updateDbusHandler
599         
600         self.downloadDialog = False
601         
602         #self.listing.setCurrentlyDisplayedFeed(self.key)
603         
604         self.disp = False
605         
606         menu = hildon.AppMenu()
607         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
608         button.set_label("Update Feed")
609         button.connect("clicked", self.button_update_clicked)
610         menu.append(button)
611         
612         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
613         button.set_label("Mark All As Read")
614         button.connect("clicked", self.buttonReadAllClicked)
615         menu.append(button)
616         
617         if key=="ArchivedArticles":
618             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
619             button.set_label("Purge Read Articles")
620             button.connect("clicked", self.buttonPurgeArticles)
621             menu.append(button)
622         
623         self.set_app_menu(menu)
624         menu.show_all()
625         
626         self.displayFeed()
627         
628         self.connect('configure-event', self.on_configure_event)
629         self.connect("destroy", self.destroyWindow)
630
631     def on_configure_event(self, window, event):
632         if getattr(self, 'markup_renderer', None) is None:
633             return
634
635         # Fix up the column width for wrapping the text when the window is
636         # resized (i.e. orientation changed)
637         self.markup_renderer.set_property('wrap-width', event.width-10)
638
639     def destroyWindow(self, *args):
640         #self.feed.saveUnread(CONFIGDIR)
641         gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
642         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
643         self.emit("feed-closed", self.key)
644         self.destroy()
645         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
646         #self.listing.closeCurrentlyDisplayedFeed()
647
648     def fix_title(self, title):
649         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
650
651     def displayFeed(self):
652         self.pannableFeed = hildon.PannableArea()
653
654         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
655
656         self.feedItems = gtk.ListStore(str, str)
657         #self.feedList = gtk.TreeView(self.feedItems)
658         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
659         selection = self.feedList.get_selection()
660         selection.set_mode(gtk.SELECTION_NONE)
661         #selection.connect("changed", lambda w: True)
662         
663         self.feedList.set_model(self.feedItems)
664         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
665
666         
667         self.feedList.set_hover_selection(False)
668         #self.feedList.set_property('enable-grid-lines', True)
669         #self.feedList.set_property('hildon-mode', 1)
670         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
671         
672         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
673
674         vbox= gtk.VBox(False, 10)
675         vbox.pack_start(self.feedList)
676         
677         self.pannableFeed.add_with_viewport(vbox)
678
679         self.markup_renderer = gtk.CellRendererText()
680         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
681         self.markup_renderer.set_property('wrap-width', 780)
682         self.markup_renderer.set_property('ypad', 5)
683         self.markup_renderer.set_property('xpad', 5)
684         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
685                 markup=FEED_COLUMN_MARKUP)
686         self.feedList.append_column(markup_column)
687
688         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
689         hideReadArticles = self.config.getHideReadArticles()
690         hasArticle = False
691         for id in self.feed.getIds():
692             isRead = False
693             try:
694                 isRead = self.feed.isEntryRead(id)
695             except:
696                 pass
697             if not ( isRead and hideReadArticles ):
698             #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
699                 #title = self.feed.getTitle(id)
700                 title = self.fix_title(self.feed.getTitle(id))
701     
702                 #if self.feed.isEntryRead(id):
703                 if isRead:
704                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
705                 else:
706                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
707     
708                 self.feedItems.append((markup, id))
709                 hasArticle = True
710         if hasArticle:
711             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
712         else:
713             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
714             self.feedItems.append((markup, ""))
715
716         self.add(self.pannableFeed)
717         self.show_all()
718
719     def clear(self):
720         self.pannableFeed.destroy()
721         #self.remove(self.pannableFeed)
722
723     def on_feedList_row_activated(self, treeview, path): #, column):
724         selection = self.feedList.get_selection()
725         selection.set_mode(gtk.SELECTION_SINGLE)
726         self.feedList.get_selection().select_path(path)
727         model = treeview.get_model()
728         iter = model.get_iter(path)
729         key = model.get_value(iter, FEED_COLUMN_KEY)
730         # Emulate legacy "button_clicked" call via treeview
731         gobject.idle_add(self.button_clicked, treeview, key)
732         #return True
733
734     def button_clicked(self, button, index, previous=False, next=False):
735         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
736         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
737         stack = hildon.WindowStack.get_default()
738         if previous:
739             tmp = stack.peek()
740             stack.pop_and_push(1, newDisp, tmp)
741             newDisp.show()
742             gobject.timeout_add(200, self.destroyArticle, tmp)
743             #print "previous"
744             self.disp = newDisp
745         elif next:
746             newDisp.show_all()
747             if type(self.disp).__name__ == "DisplayArticle":
748                 gobject.timeout_add(200, self.destroyArticle, self.disp)
749             self.disp = newDisp
750         else:
751             self.disp = newDisp
752             self.disp.show_all()
753         
754         self.ids = []
755         if self.key == "ArchivedArticles":
756             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
757         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
758         self.ids.append(self.disp.connect("article-next", self.nextArticle))
759         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
760
761     def buttonPurgeArticles(self, *widget):
762         self.clear()
763         self.feed.purgeReadArticles()
764         self.feed.saveUnread(CONFIGDIR)
765         self.feed.saveFeed(CONFIGDIR)
766         self.displayFeed()
767
768     def destroyArticle(self, handle):
769         handle.destroyWindow()
770
771     def mark_item_read(self, key):
772         it = self.feedItems.get_iter_first()
773         while it is not None:
774             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
775             if k == key:
776                 title = self.fix_title(self.feed.getTitle(key))
777                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
778                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
779                 break
780             it = self.feedItems.iter_next(it)
781
782     def nextArticle(self, object, index):
783         self.mark_item_read(index)
784         id = self.feed.getNextId(index)
785         if self.config.getHideReadArticles():
786             isRead = False
787             try:
788                 isRead = self.feed.isEntryRead(id)
789             except:
790                 pass
791             while isRead and id != index:
792                 id = self.feed.getNextId(id)
793                 isRead = False
794                 try:
795                        isRead = self.feed.isEntryRead(id)
796                 except:
797                        pass
798         if id != index:
799             self.button_clicked(object, id, next=True)
800
801     def previousArticle(self, object, index):
802         self.mark_item_read(index)
803         id = self.feed.getPreviousId(index)
804         if self.config.getHideReadArticles():
805             isRead = False
806             try:
807                 isRead = self.feed.isEntryRead(id)
808             except:
809                 pass
810             while isRead and id != index:
811                 id = self.feed.getPreviousId(id)
812                 isRead = False
813                 try:
814                        isRead = self.feed.isEntryRead(id)
815                 except:
816                        pass
817         if id != index:
818             self.button_clicked(object, id, previous=True)
819
820     def onArticleClosed(self, object, index):
821         selection = self.feedList.get_selection()
822         selection.set_mode(gtk.SELECTION_NONE)
823         self.mark_item_read(index)
824
825     def onArticleDeleted(self, object, index):
826         self.clear()
827         self.feed.removeArticle(index)
828         self.feed.saveUnread(CONFIGDIR)
829         self.feed.saveFeed(CONFIGDIR)
830         self.displayFeed()
831
832     def button_update_clicked(self, button):
833         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
834         if not type(self.downloadDialog).__name__=="DownloadBar":
835             self.pannableFeed.destroy()
836             self.vbox = gtk.VBox(False, 10)
837             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
838             self.downloadDialog.connect("download-done", self.onDownloadsDone)
839             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
840             self.add(self.vbox)
841             self.show_all()
842             
843     def onDownloadsDone(self, *widget):
844         self.vbox.destroy()
845         self.feed = self.listing.getFeed(self.key)
846         self.displayFeed()
847         self.updateDbusHandler.ArticleCountUpdated()
848         
849     def buttonReadAllClicked(self, button):
850         for index in self.feed.getIds():
851             self.feed.setEntryRead(index)
852             self.mark_item_read(index)
853
854
855 class FeedingIt:
856     def __init__(self):
857         # Init the windows
858         self.window = hildon.StackableWindow()
859         self.window.set_title(__appname__)
860         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
861         self.mainVbox = gtk.VBox(False,10)
862         
863         self.introLabel = gtk.Label("Loading...")
864
865         
866         self.mainVbox.pack_start(self.introLabel)
867
868         self.window.add(self.mainVbox)
869         self.window.show_all()
870         self.config = Config(self.window, CONFIGDIR+"config.ini")
871         gobject.idle_add(self.createWindow)
872         
873     def createWindow(self):
874         self.app_lock = get_lock("app_lock")
875         if self.app_lock == None:
876             self.introLabel.set_label("Update in progress, please wait.")
877             gobject.timeout_add_seconds(3, self.createWindow)
878             return False
879         self.listing = Listing(CONFIGDIR)
880         
881         self.downloadDialog = False
882         try:
883             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
884             self.orientation.set_mode(self.config.getOrientation())
885         except:
886             print "Could not start rotation manager"
887         
888         menu = hildon.AppMenu()
889         # Create a button and add it to the menu
890         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
891         button.set_label("Update All Feeds")
892         button.connect("clicked", self.button_update_clicked, "All")
893         menu.append(button)
894         
895         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
896         button.set_label("Mark All As Read")
897         button.connect("clicked", self.button_markAll)
898         menu.append(button)
899         
900         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
901         button.set_label("Organize Feeds")
902         button.connect("clicked", self.button_organize_clicked)
903         menu.append(button)
904
905         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
906         button.set_label("Preferences")
907         button.connect("clicked", self.button_preferences_clicked)
908         menu.append(button)
909        
910         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
911         button.set_label("Import Feeds")
912         button.connect("clicked", self.button_import_clicked)
913         menu.append(button)
914         
915         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
916         button.set_label("Export Feeds")
917         button.connect("clicked", self.button_export_clicked)
918         menu.append(button)
919
920         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
921         button.set_label("About")
922         button.connect("clicked", self.button_about_clicked)
923         menu.append(button)
924         
925         self.window.set_app_menu(menu)
926         menu.show_all()
927         
928         #self.feedWindow = hildon.StackableWindow()
929         #self.articleWindow = hildon.StackableWindow()
930         self.introLabel.destroy()
931         self.pannableListing = hildon.PannableArea()
932         self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
933         self.feedList = gtk.TreeView(self.feedItems)
934         self.feedList.connect('row-activated', self.on_feedList_row_activated)
935         self.pannableListing.add(self.feedList)
936
937         icon_renderer = gtk.CellRendererPixbuf()
938         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
939         icon_column = gtk.TreeViewColumn('', icon_renderer, \
940                 pixbuf=COLUMN_ICON)
941         self.feedList.append_column(icon_column)
942
943         markup_renderer = gtk.CellRendererText()
944         markup_column = gtk.TreeViewColumn('', markup_renderer, \
945                 markup=COLUMN_MARKUP)
946         self.feedList.append_column(markup_column)
947         self.mainVbox.pack_start(self.pannableListing)
948         self.mainVbox.show_all()
949
950         self.displayListing()
951         self.autoupdate = False
952         self.checkAutoUpdate()
953         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
954         gobject.idle_add(self.enableDbus)
955         
956     def enableDbus(self):
957         self.dbusHandler = ServerObject(self)
958         self.updateDbusHandler = UpdateServerObject(self)
959
960     def button_markAll(self, button):
961         for key in self.listing.getListOfFeeds():
962             feed = self.listing.getFeed(key)
963             for id in feed.getIds():
964                 feed.setEntryRead(id)
965             feed.saveUnread(CONFIGDIR)
966             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
967         self.displayListing()
968
969     def button_about_clicked(self, button):
970         HeAboutDialog.present(self.window, \
971                 __appname__, \
972                 ABOUT_ICON, \
973                 __version__, \
974                 __description__, \
975                 ABOUT_COPYRIGHT, \
976                 ABOUT_WEBSITE, \
977                 ABOUT_BUGTRACKER, \
978                 ABOUT_DONATE)
979
980     def button_export_clicked(self, button):
981         opml = ExportOpmlData(self.window, self.listing)
982         
983     def button_import_clicked(self, button):
984         opml = GetOpmlData(self.window)
985         feeds = opml.getData()
986         for (title, url) in feeds:
987             self.listing.addFeed(title, url)
988         self.displayListing()
989
990     def addFeed(self, urlIn="http://"):
991         wizard = AddWidgetWizard(self.window, urlIn)
992         ret = wizard.run()
993         if ret == 2:
994             (title, url) = wizard.getData()
995             if (not title == '') and (not url == ''): 
996                self.listing.addFeed(title, url)
997         wizard.destroy()
998         self.displayListing()
999
1000     def button_organize_clicked(self, button):
1001         org = SortList(self.window, self.listing)
1002         org.run()
1003         org.destroy()
1004         self.listing.saveConfig()
1005         self.displayListing()
1006         
1007     def button_update_clicked(self, button, key):
1008         if not type(self.downloadDialog).__name__=="DownloadBar":
1009             self.updateDbusHandler.UpdateStarted()
1010             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1011             self.downloadDialog.connect("download-done", self.onDownloadsDone)
1012             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1013             self.mainVbox.show_all()
1014         #self.displayListing()
1015
1016     def onDownloadsDone(self, *widget):
1017         self.downloadDialog.destroy()
1018         self.downloadDialog = False
1019         self.displayListing()
1020         self.updateDbusHandler.UpdateFinished()
1021         self.updateDbusHandler.ArticleCountUpdated()
1022
1023     def button_preferences_clicked(self, button):
1024         dialog = self.config.createDialog()
1025         dialog.connect("destroy", self.prefsClosed)
1026
1027     def show_confirmation_note(self, parent, title):
1028         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1029
1030         retcode = gtk.Dialog.run(note)
1031         note.destroy()
1032         
1033         if retcode == gtk.RESPONSE_OK:
1034             return True
1035         else:
1036             return False
1037         
1038     def displayListing(self):
1039         icon_theme = gtk.icon_theme_get_default()
1040         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1041                 gtk.ICON_LOOKUP_USE_BUILTIN)
1042
1043         self.feedItems.clear()
1044         for key in self.listing.getListOfFeeds():
1045             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1046             if unreadItems > 0 or not self.config.getHideReadFeeds():
1047                 title = self.listing.getFeedTitle(key)
1048                 updateTime = self.listing.getFeedUpdateTime(key)
1049                 
1050                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1051     
1052                 if unreadItems:
1053                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1054                 else:
1055                     markup = FEED_TEMPLATE % (title, subtitle)
1056     
1057                 try:
1058                     icon_filename = self.listing.getFavicon(key)
1059                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1060                             LIST_ICON_SIZE, LIST_ICON_SIZE)
1061                 except:
1062                     pixbuf = default_pixbuf
1063     
1064                 self.feedItems.append((pixbuf, markup, key))
1065
1066     def on_feedList_row_activated(self, treeview, path, column):
1067         model = treeview.get_model()
1068         iter = model.get_iter(path)
1069         key = model.get_value(iter, COLUMN_KEY)
1070         self.openFeed(key)
1071             
1072     def openFeed(self, key):
1073         try:
1074             self.feed_lock
1075         except:
1076             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1077             self.feed_lock = get_lock(key)
1078             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1079                     self.listing.getFeedTitle(key), key, \
1080                     self.config, self.updateDbusHandler)
1081             self.disp.connect("feed-closed", self.onFeedClosed)
1082         
1083
1084     def onFeedClosed(self, object, key):
1085         #self.listing.saveConfig()
1086         #del self.feed_lock
1087         gobject.idle_add(self.onFeedClosedTimeout)
1088         self.displayListing()
1089         #self.updateDbusHandler.ArticleCountUpdated()
1090         
1091     def onFeedClosedTimeout(self):
1092         self.listing.saveConfig()
1093         del self.feed_lock
1094         self.updateDbusHandler.ArticleCountUpdated()
1095      
1096     def run(self):
1097         self.window.connect("destroy", gtk.main_quit)
1098         gtk.main()
1099         self.listing.saveConfig()
1100         del self.app_lock
1101
1102     def prefsClosed(self, *widget):
1103         try:
1104             self.orientation.set_mode(self.config.getOrientation())
1105         except:
1106             pass
1107         self.displayListing()
1108         self.checkAutoUpdate()
1109
1110     def checkAutoUpdate(self, *widget):
1111         interval = int(self.config.getUpdateInterval()*3600000)
1112         if self.config.isAutoUpdateEnabled():
1113             if self.autoupdate == False:
1114                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1115                 self.autoupdate = interval
1116             elif not self.autoupdate == interval:
1117                 # If auto-update is enabled, but not at the right frequency
1118                 gobject.source_remove(self.autoupdateId)
1119                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1120                 self.autoupdate = interval
1121         else:
1122             if not self.autoupdate == False:
1123                 gobject.source_remove(self.autoupdateId)
1124                 self.autoupdate = False
1125
1126     def automaticUpdate(self, *widget):
1127         # Need to check for internet connection
1128         # If no internet connection, try again in 10 minutes:
1129         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1130         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1131         #from time import localtime, strftime
1132         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1133         #file.close()
1134         self.button_update_clicked(None, None)
1135         return True
1136     
1137     def stopUpdate(self):
1138         # Not implemented in the app (see update_feeds.py)
1139         try:
1140             self.downloadDialog.listOfKeys = []
1141         except:
1142             pass
1143     
1144     def getStatus(self):
1145         status = ""
1146         for key in self.listing.getListOfFeeds():
1147             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1148                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1149         if status == "":
1150             status = "No unread items"
1151         return status
1152
1153 if __name__ == "__main__":
1154     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1155     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1156     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1157     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1158     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1159     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1160     gobject.threads_init()
1161     if not isdir(CONFIGDIR):
1162         try:
1163             mkdir(CONFIGDIR)
1164         except:
1165             print "Error: Can't create configuration directory"
1166             from sys import exit
1167             exit(1)
1168     app = FeedingIt()
1169     app.run()