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