Changed default feed to Maemo News
[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, 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         try:
822             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
823             self.orientation.set_mode(self.config.getOrientation())
824         except:
825             print "Could not start rotation manager"
826         
827         menu = hildon.AppMenu()
828         # Create a button and add it to the menu
829         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
830         button.set_label("Update All Feeds")
831         button.connect("clicked", self.button_update_clicked, "All")
832         menu.append(button)
833         
834         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
835         button.set_label("Mark All As Read")
836         button.connect("clicked", self.button_markAll)
837         menu.append(button)
838         
839         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
840         button.set_label("Organize Feeds")
841         button.connect("clicked", self.button_organize_clicked)
842         menu.append(button)
843
844         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
845         button.set_label("Preferences")
846         button.connect("clicked", self.button_preferences_clicked)
847         menu.append(button)
848        
849         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
850         button.set_label("Import Feeds")
851         button.connect("clicked", self.button_import_clicked)
852         menu.append(button)
853         
854         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
855         button.set_label("Export Feeds")
856         button.connect("clicked", self.button_export_clicked)
857         menu.append(button)
858
859         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
860         button.set_label("About")
861         button.connect("clicked", self.button_about_clicked)
862         menu.append(button)
863         
864         self.window.set_app_menu(menu)
865         menu.show_all()
866         
867         self.feedWindow = hildon.StackableWindow()
868         self.articleWindow = hildon.StackableWindow()
869
870         self.displayListing()
871         self.autoupdate = False
872         self.checkAutoUpdate()
873         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
874         gobject.idle_add(self.enableDbus)
875         
876     def enableDbus(self):
877         self.dbusHandler = ServerObject(self)
878         self.updateDbusHandler = UpdateServerObject(self)
879
880     def button_markAll(self, button):
881         for key in self.listing.getListOfFeeds():
882             feed = self.listing.getFeed(key)
883             for id in feed.getIds():
884                 feed.setEntryRead(id)
885             feed.saveUnread(CONFIGDIR)
886             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
887         self.displayListing()
888
889     def button_about_clicked(self, button):
890         HeAboutDialog.present(self.window, \
891                 __appname__, \
892                 ABOUT_ICON, \
893                 __version__, \
894                 __description__, \
895                 ABOUT_COPYRIGHT, \
896                 ABOUT_WEBSITE, \
897                 ABOUT_BUGTRACKER, \
898                 ABOUT_DONATE)
899
900     def button_export_clicked(self, button):
901         opml = ExportOpmlData(self.window, self.listing)
902         
903     def button_import_clicked(self, button):
904         opml = GetOpmlData(self.window)
905         feeds = opml.getData()
906         for (title, url) in feeds:
907             self.listing.addFeed(title, url)
908         self.displayListing()
909
910     def addFeed(self, urlIn="http://"):
911         wizard = AddWidgetWizard(self.window, urlIn)
912         ret = wizard.run()
913         if ret == 2:
914             (title, url) = wizard.getData()
915             if (not title == '') and (not url == ''): 
916                self.listing.addFeed(title, url)
917         wizard.destroy()
918         self.displayListing()
919
920     def button_organize_clicked(self, button):
921         org = SortList(self.window, self.listing)
922         org.run()
923         org.destroy()
924         self.listing.saveConfig()
925         self.displayListing()
926         
927     def button_update_clicked(self, button, key):
928         if not type(self.downloadDialog).__name__=="DownloadBar":
929             self.updateDbusHandler.UpdateStarted()
930             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
931             self.downloadDialog.connect("download-done", self.onDownloadsDone)
932             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
933             self.mainVbox.show_all()
934         #self.displayListing()
935
936     def onDownloadsDone(self, *widget):
937         self.downloadDialog.destroy()
938         self.downloadDialog = False
939         self.displayListing()
940         self.updateDbusHandler.UpdateFinished()
941         self.updateDbusHandler.ArticleCountUpdated()
942
943     def button_preferences_clicked(self, button):
944         dialog = self.config.createDialog()
945         dialog.connect("destroy", self.prefsClosed)
946
947     def show_confirmation_note(self, parent, title):
948         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
949
950         retcode = gtk.Dialog.run(note)
951         note.destroy()
952         
953         if retcode == gtk.RESPONSE_OK:
954             return True
955         else:
956             return False
957         
958     def displayListing(self):
959         icon_theme = gtk.icon_theme_get_default()
960         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
961                 gtk.ICON_LOOKUP_USE_BUILTIN)
962
963         self.feedItems.clear()
964         for key in self.listing.getListOfFeeds():
965             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
966             if unreadItems > 0 or not self.config.getHideReadFeeds():
967                 title = self.listing.getFeedTitle(key)
968                 updateTime = self.listing.getFeedUpdateTime(key)
969                 
970                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
971     
972                 if unreadItems:
973                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
974                 else:
975                     markup = FEED_TEMPLATE % (title, subtitle)
976     
977                 try:
978                     icon_filename = self.listing.getFavicon(key)
979                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
980                             LIST_ICON_SIZE, LIST_ICON_SIZE)
981                 except:
982                     pixbuf = default_pixbuf
983     
984                 self.feedItems.append((pixbuf, markup, key))
985
986     def on_feedList_row_activated(self, treeview, path, column):
987         model = treeview.get_model()
988         iter = model.get_iter(path)
989         key = model.get_value(iter, COLUMN_KEY)
990
991         try:
992             self.feed_lock
993         except:
994             # If feed_lock doesn't exist, we can open the feed, else we do nothing
995             self.feed_lock = get_lock(key)
996             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
997                     self.listing.getFeedTitle(key), key, \
998                     self.config, self.updateDbusHandler)
999             self.disp.connect("feed-closed", self.onFeedClosed)
1000
1001     def onFeedClosed(self, object, key):
1002         #self.listing.saveConfig()
1003         #del self.feed_lock
1004         gobject.idle_add(self.onFeedClosedTimeout)
1005         self.displayListing()
1006         #self.updateDbusHandler.ArticleCountUpdated()
1007         
1008     def onFeedClosedTimeout(self):
1009         self.listing.saveConfig()
1010         del self.feed_lock
1011         self.updateDbusHandler.ArticleCountUpdated()
1012      
1013     def run(self):
1014         self.window.connect("destroy", gtk.main_quit)
1015         gtk.main()
1016         self.listing.saveConfig()
1017         del self.app_lock
1018
1019     def prefsClosed(self, *widget):
1020         self.orientation.set_mode(self.config.getOrientation())
1021         self.displayListing()
1022         self.checkAutoUpdate()
1023
1024     def checkAutoUpdate(self, *widget):
1025         interval = int(self.config.getUpdateInterval()*3600000)
1026         if self.config.isAutoUpdateEnabled():
1027             if self.autoupdate == False:
1028                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1029                 self.autoupdate = interval
1030             elif not self.autoupdate == interval:
1031                 # If auto-update is enabled, but not at the right frequency
1032                 gobject.source_remove(self.autoupdateId)
1033                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1034                 self.autoupdate = interval
1035         else:
1036             if not self.autoupdate == False:
1037                 gobject.source_remove(self.autoupdateId)
1038                 self.autoupdate = False
1039
1040     def automaticUpdate(self, *widget):
1041         # Need to check for internet connection
1042         # If no internet connection, try again in 10 minutes:
1043         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1044         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1045         #from time import localtime, strftime
1046         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1047         #file.close()
1048         self.button_update_clicked(None, None)
1049         return True
1050     
1051     def stopUpdate(self):
1052         # Not implemented in the app (see update_feeds.py)
1053         try:
1054             self.downloadDialog.listOfKeys = []
1055         except:
1056             pass
1057     
1058     def getStatus(self):
1059         status = ""
1060         for key in self.listing.getListOfFeeds():
1061             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1062                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1063         if status == "":
1064             status = "No unread items"
1065         return status
1066
1067 if __name__ == "__main__":
1068     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1069     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1070     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1071     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1072     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1073     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1074     gobject.threads_init()
1075     if not isdir(CONFIGDIR):
1076         try:
1077             mkdir(CONFIGDIR)
1078         except:
1079             print "Error: Can't create configuration directory"
1080             from sys import exit
1081             exit(1)
1082     app = FeedingIt()
1083     app.run()