Open external links asynchronously.
[feedingit] / src / FeedingIt.py
1 #!/usr/bin/env python2.5
2
3
4 # Copyright (c) 2007-2008 INdT.
5 # Copyright (c) 2011 Neal H. Walfield
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 #  This program is distributed in the hope that it will be useful,
12 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #  GNU Lesser General Public License for more details.
15 #
16 #  You should have received a copy of the GNU Lesser General Public License
17 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 # ============================================================================
21 __appname__ = 'FeedingIt'
22 __author__  = 'Yves Marcoz'
23 __version__ = '0.9.1~woodchuck'
24 __description__ = 'A simple RSS Reader for Maemo 5'
25 # ============================================================================
26
27 import gtk
28 from pango import FontDescription
29 import pango
30 import hildon
31 #import gtkhtml2
32 #try:
33 from webkit import WebView
34 #    has_webkit=True
35 #except:
36 #    import gtkhtml2
37 #    has_webkit=False
38 from os.path import isfile, isdir, exists
39 from os import mkdir, remove, stat, environ
40 import gobject
41 from aboutdialog import HeAboutDialog
42 from portrait import FremantleRotation
43 from threading import Thread, activeCount
44 from feedingitdbus import ServerObject
45 from config import Config
46 from cgi import escape
47 import weakref
48 import dbus
49 import debugging
50 import logging
51 logger = logging.getLogger(__name__)
52
53 from rss_sqlite import Listing
54 from opml import GetOpmlData, ExportOpmlData
55
56 import mainthread
57
58 from socket import setdefaulttimeout
59 timeout = 5
60 setdefaulttimeout(timeout)
61 del timeout
62
63 import xml.sax
64
65 LIST_ICON_SIZE = 32
66 LIST_ICON_BORDER = 10
67
68 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
69 ABOUT_ICON = 'feedingit'
70 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
71 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
72 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
73 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
74
75 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
76 unread_color = color_style.lookup_color('ActiveTextColor')
77 read_color = color_style.lookup_color('DefaultTextColor')
78 del color_style
79
80 CONFIGDIR="/home/user/.feedingit/"
81 LOCK = CONFIGDIR + "update.lock"
82
83 from re import sub
84 from htmlentitydefs import name2codepoint
85
86 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
87
88 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
89
90 import style
91
92 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
93 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
94 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
95
96 # Build the markup template for the Maemo 5 text style
97 head_font = style.get_font_desc('SystemFont')
98 sub_font = style.get_font_desc('SmallSystemFont')
99
100 #head_color = style.get_color('ButtonTextColor')
101 head_color = style.get_color('DefaultTextColor')
102 sub_color = style.get_color('DefaultTextColor')
103 active_color = style.get_color('ActiveTextColor')
104
105 bg_color = style.get_color('DefaultBackgroundColor').to_string()
106 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
107 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
108 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
109 bg_color = "#" + c1 + c2 + c3
110
111
112 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
113 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
114
115 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
116 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
117
118 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
119 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
120
121 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
122 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
123
124 FEED_TEMPLATE = '\n'.join((head, normal_sub))
125 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
126
127 ENTRY_TEMPLATE = entry_head
128 ENTRY_TEMPLATE_UNREAD = entry_active_head
129
130 notification_iface = None
131 def notify(message):
132     def get_iface():
133         global notification_iface
134
135         bus = dbus.SessionBus()
136         proxy = bus.get_object('org.freedesktop.Notifications',
137                                '/org/freedesktop/Notifications')
138         notification_iface \
139             = dbus.Interface(proxy, 'org.freedesktop.Notifications')
140
141     def doit():
142         notification_iface.SystemNoteInfoprint("FeedingIt: " + message)
143
144     if notification_iface is None:
145         get_iface()
146
147     try:
148         doit()
149     except dbus.DBusException:
150         # Rebind the name and try again.
151         get_iface()
152         doit()
153
154 ##
155 # Removes HTML or XML character references and entities from a text string.
156 #
157 # @param text The HTML (or XML) source text.
158 # @return The plain text, as a Unicode string, if necessary.
159 # http://effbot.org/zone/re-sub.htm#unescape-html
160 def unescape(text):
161     def fixup(m):
162         text = m.group(0)
163         if text[:2] == "&#":
164             # character reference
165             try:
166                 if text[:3] == "&#x":
167                     return unichr(int(text[3:-1], 16))
168                 else:
169                     return unichr(int(text[2:-1]))
170             except ValueError:
171                 pass
172         else:
173             # named entity
174             try:
175                 text = unichr(name2codepoint[text[1:-1]])
176             except KeyError:
177                 pass
178         return text # leave as is
179     return sub("&#?\w+;", fixup, text)
180
181
182 class AddWidgetWizard(gtk.Dialog):
183     def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
184         gtk.Dialog.__init__(self)
185         self.set_transient_for(parent)
186         
187         #self.category = categories[0]
188         self.category = currentCat
189
190         if isEdit:
191             self.set_title('Edit RSS feed')
192         else:
193             self.set_title('Add new RSS feed')
194
195         if isEdit:
196             self.btn_add = self.add_button('Save', 2)
197         else:
198             self.btn_add = self.add_button('Add', 2)
199
200         self.set_default_response(2)
201
202         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
203         self.nameEntry.set_placeholder('Feed name')
204         # If titleIn matches urlIn, there is no title.
205         if not titleIn == None and titleIn != urlIn:
206             self.nameEntry.set_text(titleIn)
207             self.nameEntry.select_region(-1, -1)
208
209         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
210         self.urlEntry.set_placeholder('Feed URL')
211         self.urlEntry.set_text(urlIn)
212         self.urlEntry.select_region(-1, -1)
213         self.urlEntry.set_activates_default(True)
214
215         self.table = gtk.Table(3, 2, False)
216         self.table.set_col_spacings(5)
217         label = gtk.Label('Name:')
218         label.set_alignment(1., .5)
219         self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
220         self.table.attach(self.nameEntry, 1, 2, 0, 1)
221         label = gtk.Label('URL:')
222         label.set_alignment(1., .5)
223         self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
224         self.table.attach(self.urlEntry, 1, 2, 1, 2)
225         selector = self.create_selector(categories, listing)
226         picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
227         picker.set_selector(selector)
228         picker.set_title("Select category")
229         #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
230         picker.set_name('HildonButton-finger')
231         picker.set_alignment(0,0,1,1)
232         
233         self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
234         
235         self.vbox.pack_start(self.table)
236
237         self.show_all()
238
239     def getData(self):
240         return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
241     
242     def create_selector(self, choices, listing):
243         #self.pickerDialog = hildon.PickerDialog(self.parent)
244         selector = hildon.TouchSelector(text=True)
245         index = 0
246         self.map = {}
247         for item in choices:
248             title = listing.getCategoryTitle(item)
249             iter = selector.append_text(str(title))
250             if self.category == item: 
251                 selector.set_active(0, index)
252             self.map[title] = item
253             index += 1
254         selector.connect("changed", self.selection_changed)
255         #self.pickerDialog.set_selector(selector)
256         return selector
257
258     def selection_changed(self, selector, button):
259         current_selection = selector.get_current_text()
260         if current_selection:
261             self.category = self.map[current_selection]
262
263 class AddCategoryWizard(gtk.Dialog):
264     def __init__(self, parent, titleIn=None, isEdit=False):
265         gtk.Dialog.__init__(self)
266         self.set_transient_for(parent)
267
268         if isEdit:
269             self.set_title('Edit Category')
270         else:
271             self.set_title('Add Category')
272
273         if isEdit:
274             self.btn_add = self.add_button('Save', 2)
275         else:
276             self.btn_add = self.add_button('Add', 2)
277
278         self.set_default_response(2)
279
280         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
281         self.nameEntry.set_placeholder('Category name')
282         if not titleIn == None:
283             self.nameEntry.set_text(titleIn)
284             self.nameEntry.select_region(-1, -1)
285
286         self.table = gtk.Table(1, 2, False)
287         self.table.set_col_spacings(5)
288         label = gtk.Label('Name:')
289         label.set_alignment(1., .5)
290         self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
291         self.table.attach(self.nameEntry, 1, 2, 0, 1)
292         #label = gtk.Label('URL:')
293         #label.set_alignment(1., .5)
294         #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
295         #self.table.attach(self.urlEntry, 1, 2, 1, 2)
296         self.vbox.pack_start(self.table)
297
298         self.show_all()
299
300     def getData(self):
301         return self.nameEntry.get_text()
302         
303 class DownloadBar(gtk.ProgressBar):
304     @classmethod
305     def class_init(cls):
306         if hasattr (cls, 'class_init_done'):
307             return
308
309         cls.downloadbars = []
310         # Total number of jobs we are monitoring.
311         cls.total = 0
312         # Number of jobs complete (of those that we are monitoring).
313         cls.done = 0
314         # Percent complete.
315         cls.progress = 0
316
317         cls.class_init_done = True
318
319         bus = dbus.SessionBus()
320         bus.add_signal_receiver(handler_function=cls.update_progress,
321                                 bus_name=None,
322                                 signal_name='UpdateProgress',
323                                 dbus_interface='org.marcoz.feedingit',
324                                 path='/org/marcoz/feedingit/update')
325
326     def __init__(self, parent):
327         self.class_init ()
328
329         gtk.ProgressBar.__init__(self)
330
331         self.downloadbars.append(weakref.ref (self))
332         self.set_fraction(0)
333         self.__class__.update_bars()
334         self.show_all()
335
336     @classmethod
337     def downloading(cls):
338         cls.class_init ()
339         return cls.done != cls.total
340
341     @classmethod
342     def update_progress(cls, percent_complete,
343                         completed, in_progress, queued,
344                         bytes_downloaded, bytes_updated, bytes_per_second,
345                         feed_updated):
346         if not cls.downloadbars:
347             return
348
349         cls.total = completed + in_progress + queued
350         cls.done = completed
351         cls.progress = percent_complete / 100.
352         if cls.progress < 0: cls.progress = 0
353         if cls.progress > 1: cls.progress = 1
354
355         if feed_updated:
356             for ref in cls.downloadbars:
357                 bar = ref ()
358                 if bar is None:
359                     # The download bar disappeared.
360                     cls.downloadbars.remove (ref)
361                 else:
362                     bar.emit("download-done", feed_updated)
363
364         if in_progress == 0 and queued == 0:
365             for ref in cls.downloadbars:
366                 bar = ref ()
367                 if bar is None:
368                     # The download bar disappeared.
369                     cls.downloadbars.remove (ref)
370                 else:
371                     bar.emit("download-done", None)
372             return
373
374         cls.update_bars()
375
376     @classmethod
377     def update_bars(cls):
378         # In preparation for i18n/l10n
379         def N_(a, b, n):
380             return (a if n == 1 else b)
381
382         text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
383                    cls.total)
384                 % (cls.done, cls.total))
385
386         for ref in cls.downloadbars:
387             bar = ref ()
388             if bar is None:
389                 # The download bar disappeared.
390                 cls.downloadbars.remove (ref)
391             else:
392                 bar.set_text(text)
393                 bar.set_fraction(cls.progress)
394
395 class SortList(hildon.StackableWindow):
396     def __init__(self, parent, listing, feedingit, after_closing, category=None):
397         hildon.StackableWindow.__init__(self)
398         self.set_transient_for(parent)
399         if category:
400             self.isEditingCategories = False
401             self.category = category
402             self.set_title(listing.getCategoryTitle(category))
403         else:
404             self.isEditingCategories = True
405             self.set_title('Categories')
406         self.listing = listing
407         self.feedingit = feedingit
408         self.after_closing = after_closing
409         if after_closing:
410             self.connect('destroy', lambda w: self.after_closing())
411         self.vbox2 = gtk.VBox(False, 2)
412
413         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
414         button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
415         button.connect("clicked", self.buttonUp)
416         self.vbox2.pack_start(button, expand=False, fill=False)
417
418         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
419         button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
420         button.connect("clicked", self.buttonDown)
421         self.vbox2.pack_start(button, expand=False, fill=False)
422
423         self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
424
425         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
426         button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
427         button.connect("clicked", self.buttonAdd)
428         self.vbox2.pack_start(button, expand=False, fill=False)
429
430         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
431         button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
432         button.connect("clicked", self.buttonEdit)
433         self.vbox2.pack_start(button, expand=False, fill=False)
434
435         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
436         button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
437         button.connect("clicked", self.buttonDelete)
438         self.vbox2.pack_start(button, expand=False, fill=False)
439
440         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
441         #button.set_label("Done")
442         #button.connect("clicked", self.buttonDone)
443         #self.vbox.pack_start(button)
444         self.hbox2= gtk.HBox(False, 10)
445         self.pannableArea = hildon.PannableArea()
446         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
447         self.treeview = gtk.TreeView(self.treestore)
448         self.hbox2.pack_start(self.pannableArea, expand=True)
449         self.displayFeeds()
450         self.hbox2.pack_end(self.vbox2, expand=False)
451         self.set_default_size(-1, 600)
452         self.add(self.hbox2)
453
454         menu = hildon.AppMenu()
455         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
456         button.set_label("Import from OPML")
457         button.connect("clicked", self.feedingit.button_import_clicked)
458         menu.append(button)
459
460         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
461         button.set_label("Export to OPML")
462         button.connect("clicked", self.feedingit.button_export_clicked)
463         menu.append(button)
464         self.set_app_menu(menu)
465         menu.show_all()
466         
467         self.show_all()
468         #self.connect("destroy", self.buttonDone)
469         
470     def displayFeeds(self):
471         self.treeview.destroy()
472         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
473         self.treeview = gtk.TreeView()
474         
475         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
476         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
477         self.refreshList()
478         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
479
480         self.pannableArea.add(self.treeview)
481
482         #self.show_all()
483
484     def refreshList(self, selected=None, offset=0):
485         #rect = self.treeview.get_visible_rect()
486         #y = rect.y+rect.height
487         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
488         if self.isEditingCategories:
489             for key in self.listing.getListOfCategories():
490                 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
491                 if key == selected:
492                     selectedItem = item
493         else:
494             for key in self.listing.getListOfFeeds(category=self.category):
495                 item = self.treestore.append([self.listing.getFeedTitle(key), key])
496                 if key == selected:
497                     selectedItem = item
498         self.treeview.set_model(self.treestore)
499         if not selected == None:
500             self.treeview.get_selection().select_iter(selectedItem)
501             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
502         self.pannableArea.show_all()
503
504     def getSelectedItem(self):
505         (model, iter) = self.treeview.get_selection().get_selected()
506         if not iter:
507             return None
508         return model.get_value(iter, 1)
509
510     def findIndex(self, key):
511         after = None
512         before = None
513         found = False
514         for row in self.treestore:
515             if found:
516                 return (before, row.iter)
517             if key == list(row)[0]:
518                 found = True
519             else:
520                 before = row.iter
521         return (before, None)
522
523     def buttonUp(self, button):
524         key  = self.getSelectedItem()
525         if not key == None:
526             if self.isEditingCategories:
527                 self.listing.moveCategoryUp(key)
528             else:
529                 self.listing.moveUp(key)
530             self.refreshList(key, -10)
531
532     def buttonDown(self, button):
533         key = self.getSelectedItem()
534         if not key == None:
535             if self.isEditingCategories:
536                 self.listing.moveCategoryDown(key)
537             else:
538                 self.listing.moveDown(key)
539             self.refreshList(key, 10)
540
541     def buttonDelete(self, button):
542         key = self.getSelectedItem()
543
544         message = 'Really remove this feed and its entries?'
545         dlg = hildon.hildon_note_new_confirmation(self, message)
546         response = dlg.run()
547         dlg.destroy()
548         if response == gtk.RESPONSE_OK:
549             if self.isEditingCategories:
550                 self.listing.removeCategory(key)
551             else:
552                 self.listing.removeFeed(key)
553             self.refreshList()
554
555     def buttonEdit(self, button):
556         key = self.getSelectedItem()
557
558         if key == 'ArchivedArticles':
559             message = 'Cannot edit the archived articles feed.'
560             hildon.hildon_banner_show_information(self, '', message)
561             return
562         if self.isEditingCategories:
563             if key is not None:
564                 SortList(self.parent, self.listing, self.feedingit, None, category=key)
565         else:
566             if key is not None:
567                 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
568                 ret = wizard.run()
569                 if ret == 2:
570                     (title, url, category) = wizard.getData()
571                     if url != '':
572                         self.listing.editFeed(key, title, url, category=category)
573                         self.refreshList()
574                 wizard.destroy()
575
576     def buttonDone(self, *args):
577         self.destroy()
578         
579     def buttonAdd(self, button, urlIn="http://"):
580         if self.isEditingCategories:
581             wizard = AddCategoryWizard(self)
582             ret = wizard.run()
583             if ret == 2:
584                 title = wizard.getData()
585                 if (not title == ''): 
586                    self.listing.addCategory(title)
587         else:
588             wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
589             ret = wizard.run()
590             if ret == 2:
591                 (title, url, category) = wizard.getData()
592                 if url:
593                    self.listing.addFeed(title, url, category=category)
594         wizard.destroy()
595         self.refreshList()
596                
597
598 class DisplayArticle(hildon.StackableWindow):
599     def __init__(self, feed, id, key, config, listing):
600         hildon.StackableWindow.__init__(self)
601         #self.imageDownloader = ImageDownloader()
602         self.feed = feed
603         self.listing=listing
604         self.key = key
605         self.id = id
606         #self.set_title(feed.getTitle(id))
607         self.set_title(self.listing.getFeedTitle(key))
608         self.config = config
609         self.set_for_removal = False
610         
611         # Init the article display
612         #if self.config.getWebkitSupport():
613         self.view = WebView()
614             #self.view.set_editable(False)
615         #else:
616         #    import gtkhtml2
617         #    self.view = gtkhtml2.View()
618         #    self.document = gtkhtml2.Document()
619         #    self.view.set_document(self.document)
620         #    self.document.connect("link_clicked", self._signal_link_clicked)
621         self.pannable_article = hildon.PannableArea()
622         self.pannable_article.add(self.view)
623         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
624         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
625
626         #if self.config.getWebkitSupport():
627         contentLink = self.feed.getContentLink(self.id)
628         self.feed.setEntryRead(self.id)
629         #if key=="ArchivedArticles":
630         self.loadedArticle = False
631         if contentLink.startswith("/home/user/"):
632             self.view.open("file://%s" % contentLink)
633             self.currentUrl = self.feed.getExternalLink(self.id)
634         else:
635             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
636             self.currentUrl = "%s" % contentLink
637         self.view.connect("motion-notify-event", lambda w,ev: True)
638         self.view.connect('load-started', self.load_started)
639         self.view.connect('load-finished', self.load_finished)
640
641         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
642         
643         menu = hildon.AppMenu()
644         # Create a button and add it to the menu
645         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
646         button.set_label("Allow horizontal scrolling")
647         button.connect("clicked", self.horiz_scrolling_button)
648         menu.append(button)
649         
650         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
651         button.set_label("Open in browser")
652         button.connect("clicked", self.open_in_browser)
653         menu.append(button)
654         
655         if key == "ArchivedArticles":
656             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
657             button.set_label("Remove from archived articles")
658             button.connect("clicked", self.remove_archive_button)
659         else:
660             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
661             button.set_label("Add to archived articles")
662             button.connect("clicked", self.archive_button)
663         menu.append(button)
664         
665         self.set_app_menu(menu)
666         menu.show_all()
667         
668         self.add(self.pannable_article)
669         
670         self.pannable_article.show_all()
671
672         self.destroyId = self.connect("destroy", self.destroyWindow)
673         
674         #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
675         ## Still using an old version of WebKit, so using navigation-requested signal
676         self.view.connect('navigation-requested', self.navigation_requested)
677         
678         self.view.connect("button_press_event", self.button_pressed)
679         self.gestureId = self.view.connect("button_release_event", self.button_released)
680
681     #def navigation_policy_decision(self, wv, fr, req, action, decision):
682     def navigation_requested(self, wv, fr, req):
683         if self.config.getOpenInExternalBrowser():
684             self.open_in_browser(None, req.get_uri())
685             return True
686         else:
687             return False
688
689     def load_started(self, *widget):
690         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
691         
692     def load_finished(self, *widget):
693         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
694         frame = self.view.get_main_frame()
695         if self.loadedArticle:
696             self.currentUrl = frame.get_uri()
697         else:
698             self.loadedArticle = True
699
700     def button_pressed(self, window, event):
701         #print event.x, event.y
702         self.coords = (event.x, event.y)
703         
704     def button_released(self, window, event):
705         x = self.coords[0] - event.x
706         y = self.coords[1] - event.y
707         
708         if (2*abs(y) < abs(x)):
709             if (x > 15):
710                 self.emit("article-previous", self.id)
711             elif (x<-15):
712                 self.emit("article-next", self.id)   
713
714     def destroyWindow(self, *args):
715         self.disconnect(self.destroyId)
716         if self.set_for_removal:
717             self.emit("article-deleted", self.id)
718         else:
719             self.emit("article-closed", self.id)
720         #self.imageDownloader.stopAll()
721         self.destroy()
722         
723     def horiz_scrolling_button(self, *widget):
724         self.pannable_article.disconnect(self.gestureId)
725         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
726         
727     def archive_button(self, *widget):
728         # Call the listing.addArchivedArticle
729         self.listing.addArchivedArticle(self.key, self.id)
730         
731     def remove_archive_button(self, *widget):
732         self.set_for_removal = True
733
734     def open_in_browser(self, object, link=None):
735         if link == None:
736             link = self.currentUrl
737
738         bus = dbus.SessionBus()
739         b_proxy = bus.get_object("com.nokia.osso_browser",
740                                  "/com/nokia/osso_browser/request")
741         b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
742
743         notify("Opening %s" % link)
744
745         # We open the link asynchronously: if the web browser is not
746         # already running, this can take a while.
747         def error_handler():
748             """
749             Something went wrong opening the URL.
750             """
751             def e(exception):
752                 notify("Error opening %s: %s" % (link, str(exception)))
753             return e
754
755         b_iface.open_new_window(link,
756                                 reply_handler=lambda *args: None,
757                                 error_handler=error_handler())
758
759 class DisplayFeed(hildon.StackableWindow):
760     def __init__(self, listing, feed, title, key, config):
761         hildon.StackableWindow.__init__(self)
762         self.listing = listing
763         self.feed = feed
764         self.feedTitle = title
765         self.set_title(title)
766         self.key=key
767         self.current = list()
768         self.config = config
769         
770         self.downloadDialog = False
771         
772         #self.listing.setCurrentlyDisplayedFeed(self.key)
773         
774         self.disp = False
775         
776         menu = hildon.AppMenu()
777         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
778         button.set_label("Update feed")
779         button.connect("clicked", self.button_update_clicked)
780         menu.append(button)
781         
782         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
783         button.set_label("Mark all as read")
784         button.connect("clicked", self.buttonReadAllClicked)
785         menu.append(button)
786         
787         if key=="ArchivedArticles":
788             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
789             button.set_label("Delete read articles")
790             button.connect("clicked", self.buttonPurgeArticles)
791             menu.append(button)
792         
793         self.set_app_menu(menu)
794         menu.show_all()
795         
796         self.main_vbox = gtk.VBox(False, 0)
797         self.add(self.main_vbox)
798
799         self.pannableFeed = None
800         self.displayFeed()
801
802         if DownloadBar.downloading ():
803             self.show_download_bar ()
804         
805         self.connect('configure-event', self.on_configure_event)
806         self.connect("destroy", self.destroyWindow)
807
808     def on_configure_event(self, window, event):
809         if getattr(self, 'markup_renderer', None) is None:
810             return
811
812         # Fix up the column width for wrapping the text when the window is
813         # resized (i.e. orientation changed)
814         self.markup_renderer.set_property('wrap-width', event.width-20)  
815         it = self.feedItems.get_iter_first()
816         while it is not None:
817             markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
818             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
819             it = self.feedItems.iter_next(it)
820
821     def destroyWindow(self, *args):
822         #self.feed.saveUnread(CONFIGDIR)
823         self.listing.updateUnread(self.key)
824         self.emit("feed-closed", self.key)
825         self.destroy()
826         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
827         #self.listing.closeCurrentlyDisplayedFeed()
828
829     def fix_title(self, title):
830         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
831
832     def displayFeed(self):
833         if self.pannableFeed:
834             self.pannableFeed.destroy()
835
836         self.pannableFeed = hildon.PannableArea()
837
838         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
839
840         self.feedItems = gtk.ListStore(str, str)
841         #self.feedList = gtk.TreeView(self.feedItems)
842         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
843         self.feedList.set_rules_hint(True)
844
845         selection = self.feedList.get_selection()
846         selection.set_mode(gtk.SELECTION_NONE)
847         #selection.connect("changed", lambda w: True)
848         
849         self.feedList.set_model(self.feedItems)
850         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
851
852         
853         self.feedList.set_hover_selection(False)
854         #self.feedList.set_property('enable-grid-lines', True)
855         #self.feedList.set_property('hildon-mode', 1)
856         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
857         
858         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
859
860         vbox= gtk.VBox(False, 10)
861         vbox.pack_start(self.feedList)
862         
863         self.pannableFeed.add_with_viewport(vbox)
864
865         self.markup_renderer = gtk.CellRendererText()
866         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
867         self.markup_renderer.set_property('background', bg_color) #"#333333")
868         (width, height) = self.get_size()
869         self.markup_renderer.set_property('wrap-width', width-20)
870         self.markup_renderer.set_property('ypad', 8)
871         self.markup_renderer.set_property('xpad', 5)
872         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
873                 markup=FEED_COLUMN_MARKUP)
874         self.feedList.append_column(markup_column)
875
876         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
877         hideReadArticles = self.config.getHideReadArticles()
878         if hideReadArticles:
879             articles = self.feed.getIds(onlyUnread=True)
880         else:
881             articles = self.feed.getIds()
882         
883         hasArticle = False
884         self.current = list()
885         for id in articles:
886             isRead = False
887             try:
888                 isRead = self.feed.isEntryRead(id)
889             except:
890                 pass
891             if not ( isRead and hideReadArticles ):
892                 title = self.fix_title(self.feed.getTitle(id))
893                 self.current.append(id)
894                 if isRead:
895                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
896                 else:
897                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
898     
899                 self.feedItems.append((markup, id))
900                 hasArticle = True
901         if hasArticle:
902             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
903         else:
904             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
905             self.feedItems.append((markup, ""))
906
907         self.main_vbox.pack_start(self.pannableFeed)
908         self.show_all()
909
910     def clear(self):
911         self.pannableFeed.destroy()
912         #self.remove(self.pannableFeed)
913
914     def on_feedList_row_activated(self, treeview, path): #, column):
915         selection = self.feedList.get_selection()
916         selection.set_mode(gtk.SELECTION_SINGLE)
917         self.feedList.get_selection().select_path(path)
918         model = treeview.get_model()
919         iter = model.get_iter(path)
920         key = model.get_value(iter, FEED_COLUMN_KEY)
921         # Emulate legacy "button_clicked" call via treeview
922         gobject.idle_add(self.button_clicked, treeview, key)
923         #return True
924
925     def button_clicked(self, button, index, previous=False, next=False):
926         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
927         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
928         stack = hildon.WindowStack.get_default()
929         if previous:
930             tmp = stack.peek()
931             stack.pop_and_push(1, newDisp, tmp)
932             newDisp.show()
933             gobject.timeout_add(200, self.destroyArticle, tmp)
934             #print "previous"
935             self.disp = newDisp
936         elif next:
937             newDisp.show_all()
938             if type(self.disp).__name__ == "DisplayArticle":
939                 gobject.timeout_add(200, self.destroyArticle, self.disp)
940             self.disp = newDisp
941         else:
942             self.disp = newDisp
943             self.disp.show_all()
944         
945         self.ids = []
946         if self.key == "ArchivedArticles":
947             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
948         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
949         self.ids.append(self.disp.connect("article-next", self.nextArticle))
950         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
951
952     def buttonPurgeArticles(self, *widget):
953         self.clear()
954         self.feed.purgeReadArticles()
955         #self.feed.saveFeed(CONFIGDIR)
956         self.displayFeed()
957
958     def destroyArticle(self, handle):
959         handle.destroyWindow()
960
961     def mark_item_read(self, key):
962         it = self.feedItems.get_iter_first()
963         while it is not None:
964             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
965             if k == key:
966                 title = self.fix_title(self.feed.getTitle(key))
967                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
968                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
969                 break
970             it = self.feedItems.iter_next(it)
971
972     def nextArticle(self, object, index):
973         self.mark_item_read(index)
974         id = self.feed.getNextId(index)
975         while id not in self.current and id != index:
976             id = self.feed.getNextId(id)
977         if id != index:
978             self.button_clicked(object, id, next=True)
979
980     def previousArticle(self, object, index):
981         self.mark_item_read(index)
982         id = self.feed.getPreviousId(index)
983         while id not in self.current and id != index:
984             id = self.feed.getPreviousId(id)
985         if id != index:
986             self.button_clicked(object, id, previous=True)
987
988     def onArticleClosed(self, object, index):
989         selection = self.feedList.get_selection()
990         selection.set_mode(gtk.SELECTION_NONE)
991         self.mark_item_read(index)
992
993     def onArticleDeleted(self, object, index):
994         self.clear()
995         self.feed.removeArticle(index)
996         #self.feed.saveFeed(CONFIGDIR)
997         self.displayFeed()
998
999
1000     def do_update_feed(self):
1001         self.listing.updateFeed (self.key, priority=-1)
1002
1003     def button_update_clicked(self, button):
1004         gobject.idle_add(self.do_update_feed)
1005             
1006     def show_download_bar(self):
1007         if not type(self.downloadDialog).__name__=="DownloadBar":
1008             self.downloadDialog = DownloadBar(self.window)
1009             self.downloadDialog.connect("download-done", self.onDownloadDone)
1010             self.main_vbox.pack_end(self.downloadDialog,
1011                                     expand=False, fill=False)
1012             self.show_all()
1013         
1014     def onDownloadDone(self, widget, feed):
1015         if feed == self.feed:
1016             self.feed = self.listing.getFeed(self.key)
1017             self.displayFeed()
1018
1019         if feed is None:
1020             self.downloadDialog.destroy()
1021             self.downloadDialog = False
1022
1023     def buttonReadAllClicked(self, button):
1024         #self.clear()
1025         self.feed.markAllAsRead()
1026         it = self.feedItems.get_iter_first()
1027         while it is not None:
1028             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1029             title = self.fix_title(self.feed.getTitle(k))
1030             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1031             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1032             it = self.feedItems.iter_next(it)
1033         #self.displayFeed()
1034         #for index in self.feed.getIds():
1035         #    self.feed.setEntryRead(index)
1036         #    self.mark_item_read(index)
1037
1038
1039 class FeedingIt:
1040     def __init__(self):
1041         # Init the windows
1042         self.window = hildon.StackableWindow()
1043         self.window.set_title(__appname__)
1044         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1045         self.mainVbox = gtk.VBox(False,10)
1046         
1047         if isfile(CONFIGDIR+"/feeds.db"):           
1048             self.introLabel = gtk.Label("Loading...")
1049         else:
1050             self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1051         
1052         self.mainVbox.pack_start(self.introLabel)
1053
1054         self.window.add(self.mainVbox)
1055         self.window.show_all()
1056         self.config = Config(self.window, CONFIGDIR+"config.ini")
1057         gobject.idle_add(self.createWindow)
1058
1059     def createWindow(self):
1060         self.category = 0
1061         self.listing = Listing(self.config, CONFIGDIR)
1062
1063         self.downloadDialog = False
1064         try:
1065             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1066             self.orientation.set_mode(self.config.getOrientation())
1067         except Exception, e:
1068             logger.warn("Could not start rotation manager: %s" % str(e))
1069         
1070         menu = hildon.AppMenu()
1071         # Create a button and add it to the menu
1072         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1073         button.set_label("Update feeds")
1074         button.connect("clicked", self.button_update_clicked, "All")
1075         menu.append(button)
1076         
1077         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1078         button.set_label("Mark all as read")
1079         button.connect("clicked", self.button_markAll)
1080         menu.append(button)
1081
1082         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1083         button.set_label("Add new feed")
1084         button.connect("clicked", lambda b: self.addFeed())
1085         menu.append(button)
1086
1087         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1088         button.set_label("Manage subscriptions")
1089         button.connect("clicked", self.button_organize_clicked)
1090         menu.append(button)
1091
1092         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1093         button.set_label("Settings")
1094         button.connect("clicked", self.button_preferences_clicked)
1095         menu.append(button)
1096        
1097         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1098         button.set_label("About")
1099         button.connect("clicked", self.button_about_clicked)
1100         menu.append(button)
1101         
1102         self.window.set_app_menu(menu)
1103         menu.show_all()
1104         
1105         #self.feedWindow = hildon.StackableWindow()
1106         #self.articleWindow = hildon.StackableWindow()
1107         self.introLabel.destroy()
1108         self.pannableListing = hildon.PannableArea()
1109         self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1110         self.feedList = gtk.TreeView(self.feedItems)
1111         self.feedList.connect('row-activated', self.on_feedList_row_activated)
1112         #self.feedList.set_enable_tree_lines(True)                                                                                           
1113         #self.feedList.set_show_expanders(True)
1114         self.pannableListing.add(self.feedList)
1115
1116         icon_renderer = gtk.CellRendererPixbuf()
1117         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1118         icon_column = gtk.TreeViewColumn('', icon_renderer, \
1119                 pixbuf=COLUMN_ICON)
1120         self.feedList.append_column(icon_column)
1121
1122         markup_renderer = gtk.CellRendererText()
1123         markup_column = gtk.TreeViewColumn('', markup_renderer, \
1124                 markup=COLUMN_MARKUP)
1125         self.feedList.append_column(markup_column)
1126         self.mainVbox.pack_start(self.pannableListing)
1127         self.mainVbox.show_all()
1128
1129         self.displayListing()
1130         self.autoupdate = False
1131         self.checkAutoUpdate()
1132         
1133         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1134         gobject.idle_add(self.late_init)
1135         
1136     def update_progress(self, percent_complete,
1137                         completed, in_progress, queued,
1138                         bytes_downloaded, bytes_updated, bytes_per_second,
1139                         updated_feed):
1140         if (in_progress or queued) and not self.downloadDialog:
1141             self.downloadDialog = DownloadBar(self.window)
1142             self.downloadDialog.connect("download-done", self.onDownloadDone)
1143             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1144             self.mainVbox.show_all()
1145
1146             if self.__dict__.get ('disp', None):
1147                 self.disp.show_download_bar ()
1148
1149     def onDownloadDone(self, widget, feed):
1150         if feed is None:
1151             self.downloadDialog.destroy()
1152             self.downloadDialog = False
1153             self.displayListing()
1154
1155     def late_init(self):
1156         self.dbusHandler = ServerObject(self)
1157         bus = dbus.SessionBus()
1158         bus.add_signal_receiver(handler_function=self.update_progress,
1159                                 bus_name=None,
1160                                 signal_name='UpdateProgress',
1161                                 dbus_interface='org.marcoz.feedingit',
1162                                 path='/org/marcoz/feedingit/update')
1163
1164     def button_markAll(self, button):
1165         for key in self.listing.getListOfFeeds():
1166             feed = self.listing.getFeed(key)
1167             feed.markAllAsRead()
1168             #for id in feed.getIds():
1169             #    feed.setEntryRead(id)
1170             self.listing.updateUnread(key)
1171         self.displayListing()
1172
1173     def button_about_clicked(self, button):
1174         HeAboutDialog.present(self.window, \
1175                 __appname__, \
1176                 ABOUT_ICON, \
1177                 __version__, \
1178                 __description__, \
1179                 ABOUT_COPYRIGHT, \
1180                 ABOUT_WEBSITE, \
1181                 ABOUT_BUGTRACKER, \
1182                 ABOUT_DONATE)
1183
1184     def button_export_clicked(self, button):
1185         opml = ExportOpmlData(self.window, self.listing)
1186         
1187     def button_import_clicked(self, button):
1188         opml = GetOpmlData(self.window)
1189         feeds = opml.getData()
1190         for (title, url) in feeds:
1191             self.listing.addFeed(title, url)
1192         self.displayListing()
1193
1194     def addFeed(self, urlIn="http://"):
1195         wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1196         ret = wizard.run()
1197         if ret == 2:
1198             (title, url, category) = wizard.getData()
1199             if url:
1200                self.listing.addFeed(title, url, category=category)
1201         wizard.destroy()
1202         self.displayListing()
1203
1204     def button_organize_clicked(self, button):
1205         def after_closing():
1206             self.displayListing()
1207         SortList(self.window, self.listing, self, after_closing)
1208
1209     def do_update_feeds(self):
1210         for k in self.listing.getListOfFeeds():
1211             self.listing.updateFeed (k)
1212
1213     def button_update_clicked(self, button, key):
1214         gobject.idle_add(self.do_update_feeds)
1215
1216     def onDownloadsDone(self, *widget):
1217         self.downloadDialog.destroy()
1218         self.downloadDialog = False
1219         self.displayListing()
1220
1221     def button_preferences_clicked(self, button):
1222         dialog = self.config.createDialog()
1223         dialog.connect("destroy", self.prefsClosed)
1224
1225     def show_confirmation_note(self, parent, title):
1226         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1227
1228         retcode = gtk.Dialog.run(note)
1229         note.destroy()
1230         
1231         if retcode == gtk.RESPONSE_OK:
1232             return True
1233         else:
1234             return False
1235         
1236     def saveExpandedLines(self):
1237        self.expandedLines = []
1238        model = self.feedList.get_model()
1239        model.foreach(self.checkLine)
1240
1241     def checkLine(self, model, path, iter, data = None):
1242        if self.feedList.row_expanded(path):
1243            self.expandedLines.append(path)
1244
1245     def restoreExpandedLines(self):
1246        model = self.feedList.get_model()
1247        model.foreach(self.restoreLine)
1248
1249     def restoreLine(self, model, path, iter, data = None):
1250        if path in self.expandedLines:
1251            self.feedList.expand_row(path, False)
1252         
1253     def displayListing(self):
1254         icon_theme = gtk.icon_theme_get_default()
1255         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1256                 gtk.ICON_LOOKUP_USE_BUILTIN)
1257
1258         self.saveExpandedLines()
1259
1260         self.feedItems.clear()
1261         hideReadFeed = self.config.getHideReadFeeds()
1262         order = self.config.getFeedSortOrder()
1263         
1264         categories = self.listing.getListOfCategories()
1265         if len(categories) > 1:
1266             showCategories = True
1267         else:
1268             showCategories = False
1269         
1270         for categoryId in categories:
1271         
1272             title = self.listing.getCategoryTitle(categoryId)
1273             keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1274             
1275             if showCategories and len(keys)>0:
1276                 category = self.feedItems.append(None, (None, title, categoryId))
1277                 #print "catID" + str(categoryId) + " " + str(self.category)
1278                 if categoryId == self.category:
1279                     #print categoryId
1280                     expandedRow = category
1281     
1282             for key in keys:
1283                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1284                 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1285                 updateTime = self.listing.getFeedUpdateTime(key)
1286                 if updateTime == 0:
1287                     updateTime = "Never"
1288                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1289                 if unreadItems:
1290                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1291                 else:
1292                     markup = FEED_TEMPLATE % (title, subtitle)
1293         
1294                 try:
1295                     icon_filename = self.listing.getFavicon(key)
1296                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1297                                                    LIST_ICON_SIZE, LIST_ICON_SIZE)
1298                 except:
1299                     pixbuf = default_pixbuf
1300                 
1301                 if showCategories:
1302                     self.feedItems.append(category, (pixbuf, markup, key))
1303                 else:
1304                     self.feedItems.append(None, (pixbuf, markup, key))
1305                     
1306                 
1307         self.restoreExpandedLines()
1308         #try:
1309             
1310         #    self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1311         #except:
1312         #    pass
1313
1314     def on_feedList_row_activated(self, treeview, path, column):
1315         model = treeview.get_model()
1316         iter = model.get_iter(path)
1317         key = model.get_value(iter, COLUMN_KEY)
1318         
1319         try:
1320             #print "Key: " + str(key)
1321             catId = int(key)
1322             self.category = catId
1323             if treeview.row_expanded(path):
1324                 treeview.collapse_row(path)
1325         #else:
1326         #    treeview.expand_row(path, True)
1327             #treeview.collapse_all()
1328             #treeview.expand_row(path, False)
1329             #for i in range(len(path)):
1330             #    self.feedList.expand_row(path[:i+1], False)
1331             #self.show_confirmation_note(self.window, "Working")
1332             #return True
1333         except:
1334             if key:
1335                 self.openFeed(key)
1336             
1337     def openFeed(self, key):
1338         if key != None:
1339             self.disp = DisplayFeed(
1340                 self.listing, self.listing.getFeed(key),
1341                 self.listing.getFeedTitle(key), key,
1342                 self.config)
1343             self.disp.connect("feed-closed", self.onFeedClosed)
1344                 
1345     def openArticle(self, key, id):
1346         if key != None:
1347             self.openFeed(key)
1348             self.disp.button_clicked(None, id)
1349
1350     def onFeedClosed(self, object, key):
1351         self.displayListing()
1352         
1353     def quit(self, *args):
1354         self.window.hide()
1355         gtk.main_quit ()
1356
1357     def run(self):
1358         self.window.connect("destroy", self.quit)
1359         gtk.main()
1360
1361     def prefsClosed(self, *widget):
1362         try:
1363             self.orientation.set_mode(self.config.getOrientation())
1364         except:
1365             pass
1366         self.displayListing()
1367         self.checkAutoUpdate()
1368
1369     def checkAutoUpdate(self, *widget):
1370         interval = int(self.config.getUpdateInterval()*3600000)
1371         if self.config.isAutoUpdateEnabled():
1372             if self.autoupdate == False:
1373                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1374                 self.autoupdate = interval
1375             elif not self.autoupdate == interval:
1376                 # If auto-update is enabled, but not at the right frequency
1377                 gobject.source_remove(self.autoupdateId)
1378                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1379                 self.autoupdate = interval
1380         else:
1381             if not self.autoupdate == False:
1382                 gobject.source_remove(self.autoupdateId)
1383                 self.autoupdate = False
1384
1385     def automaticUpdate(self, *widget):
1386         # Need to check for internet connection
1387         # If no internet connection, try again in 10 minutes:
1388         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1389         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1390         #from time import localtime, strftime
1391         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1392         #file.close()
1393         self.button_update_clicked(None, None)
1394         return True
1395     
1396     def getStatus(self):
1397         status = ""
1398         for key in self.listing.getListOfFeeds():
1399             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1400                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1401         if status == "":
1402             status = "No unread items"
1403         return status
1404
1405 if __name__ == "__main__":
1406     mainthread.init ()
1407     debugging.init(dot_directory=".feedingit", program_name="feedingit")
1408
1409     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1410     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1411     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1412     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1413     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1414     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1415     gobject.threads_init()
1416     if not isdir(CONFIGDIR):
1417         try:
1418             mkdir(CONFIGDIR)
1419         except:
1420             logger.error("Error: Can't create configuration directory")
1421             from sys import exit
1422             exit(1)
1423     app = FeedingIt()
1424     app.run()