Correctly set update time to never.
[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     """
600     A Widget for displaying an article.
601     """
602     def __init__(self, article_id, feed, feed_key, articles, config, listing):
603         """
604         article_id - The identifier of the article to load.
605
606         feed - The feed object containing the article (an
607         rss_sqlite:Feed object).
608
609         feed_key - The feed's identifier.
610
611         articles - A list of articles from the feed to display.
612         Needed for selecting the next/previous article (article_next).
613
614         config - A configuration object (config:Config).
615
616         listing - The listing object (rss_sqlite:Listing) that
617         contains the feed and article.
618         """
619         hildon.StackableWindow.__init__(self)
620
621         self.article_id = None
622         self.feed = feed
623         self.feed_key = feed_key
624         self.articles = articles
625         self.config = config
626         self.listing = listing
627
628         self.set_title(self.listing.getFeedTitle(feed_key))
629
630         # Init the article display
631         self.view = WebView()
632         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
633         self.view.connect("motion-notify-event", lambda w,ev: True)
634         self.view.connect('load-started', self.load_started)
635         self.view.connect('load-finished', self.load_finished)
636         self.view.connect('navigation-requested', self.navigation_requested)
637         self.view.connect("button_press_event", self.button_pressed)
638         self.gestureId = self.view.connect(
639             "button_release_event", self.button_released)
640
641         self.pannable_article = hildon.PannableArea()
642         self.pannable_article.add(self.view)
643
644         self.add(self.pannable_article)
645
646         self.pannable_article.show_all()
647
648         # Create the menu.
649         menu = hildon.AppMenu()
650
651         def menu_button(label, callback):
652             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
653             button.set_label(label)
654             button.connect("clicked", callback)
655             menu.append(button)
656
657         menu_button("Allow horizontal scrolling", self.horiz_scrolling_button)
658         menu_button("Open in browser", self.open_in_browser)
659         if feed_key == "ArchivedArticles":
660             menu_button(
661                 "Remove from archived articles", self.remove_archive_button)
662         else:
663             menu_button("Add to archived articles", self.archive_button)
664         
665         self.set_app_menu(menu)
666         menu.show_all()
667         
668         self.destroyId = self.connect("destroy", self.destroyWindow)
669
670         self.article_open(article_id)
671
672     def article_open(self, article_id):
673         """
674         Load the article with the specified id.
675         """
676         # If an article was open, close it.
677         if self.article_id is not None:
678             self.article_closed()
679
680         self.article_id = article_id
681         self.set_for_removal = False
682         self.loadedArticle = False
683         self.initial_article_load = True
684
685         contentLink = self.feed.getContentLink(self.article_id)
686         if contentLink.startswith("/home/user/"):
687             self.view.open("file://%s" % contentLink)
688             self.currentUrl = self.feed.getExternalLink(self.article_id)
689         else:
690             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
691             self.currentUrl = str(contentLink)
692
693         self.feed.setEntryRead(self.article_id)
694
695     def article_closed(self):
696         """
697         The user has navigated away from the article.  Execute any
698         pending actions.
699         """
700         if self.set_for_removal:
701             self.emit("article-deleted", self.article_id)
702         else:
703             self.emit("article-closed", self.article_id)
704
705
706     def navigation_requested(self, wv, fr, req):
707         """
708         http://webkitgtk.org/reference/webkitgtk-webkitwebview.html#WebKitWebView-navigation-requested
709
710         wv - a WebKitWebView
711         fr - a WebKitWebFrame
712         req - WebKitNetworkRequest
713         """
714         if self.initial_article_load:
715             # Always initially load an article in the internal
716             # browser.
717             self.initial_article_load = False
718             return False
719
720         # When following a link, only use the internal browser if so
721         # configured.  Otherwise, launch an external browser.
722         if self.config.getOpenInExternalBrowser():
723             self.open_in_browser(None, req.get_uri())
724             return True
725         else:
726             return False
727
728     def load_started(self, *widget):
729         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
730
731     def load_finished(self, *widget):
732         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
733         frame = self.view.get_main_frame()
734         if self.loadedArticle:
735             self.currentUrl = frame.get_uri()
736         else:
737             self.loadedArticle = True
738
739     def button_pressed(self, window, event):
740         """
741         The user pressed a "mouse button" (in our case, this means the
742         user likely started to drag with the finger).
743
744         We are only interested in whether the user performs a drag.
745         We record the starting position and when the user "releases
746         the button," we see how far the mouse moved.
747         """
748         self.coords = (event.x, event.y)
749         
750     def button_released(self, window, event):
751         x = self.coords[0] - event.x
752         y = self.coords[1] - event.y
753         
754         if (2*abs(y) < abs(x)):
755             if (x > 15):
756                 self.article_next(forward=False)
757             elif (x<-15):
758                 self.article_next(forward=True)
759
760             # We handled the event.  Don't propagate it further.
761             return True
762
763     def article_next(self, forward=True):
764         """
765         Advance to the next (or, if forward is false, the previous)
766         article.
767         """
768         first_id = None
769         id = self.article_id
770         i = 0
771         while True:
772             i += 1
773             id = self.feed.getNextId(id, forward)
774             if id == first_id:
775                 # We looped.
776                 break
777
778             if first_id is None:
779                 first_id = id
780
781             if id in self.articles:
782                 self.article_open(id)
783                 break
784
785     def destroyWindow(self, *args):
786         self.article_closed()
787         self.disconnect(self.destroyId)
788         self.destroy()
789         
790     def horiz_scrolling_button(self, *widget):
791         self.pannable_article.disconnect(self.gestureId)
792         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
793         
794     def archive_button(self, *widget):
795         # Call the listing.addArchivedArticle
796         self.listing.addArchivedArticle(self.feed_key, self.article_id)
797         
798     def remove_archive_button(self, *widget):
799         self.set_for_removal = True
800
801     def open_in_browser(self, object, link=None):
802         """
803         Open the specified link using the system's browser.  If not
804         link is specified, reopen the current page using the system's
805         browser.
806         """
807         if link == None:
808             link = self.currentUrl
809
810         bus = dbus.SessionBus()
811         b_proxy = bus.get_object("com.nokia.osso_browser",
812                                  "/com/nokia/osso_browser/request")
813         b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
814
815         notify("Opening %s" % link)
816
817         # We open the link asynchronously: if the web browser is not
818         # already running, this can take a while.
819         def error_handler():
820             """
821             Something went wrong opening the URL.
822             """
823             def e(exception):
824                 notify("Error opening %s: %s" % (link, str(exception)))
825             return e
826
827         b_iface.open_new_window(link,
828                                 reply_handler=lambda *args: None,
829                                 error_handler=error_handler())
830
831 class DisplayFeed(hildon.StackableWindow):
832     def __init__(self, listing, feed, title, key, config):
833         hildon.StackableWindow.__init__(self)
834         self.listing = listing
835         self.feed = feed
836         self.feedTitle = title
837         self.set_title(title)
838         self.key=key
839         # Articles to show.
840         #
841         # If hide read articles is set, this is set to the set of
842         # unread articles at the time that feed is loaded.  The last
843         # bit is important: when the user selects the next article,
844         # but then decides to move back, previous should select the
845         # just read article.
846         self.articles = list()
847         self.config = config
848         
849         self.downloadDialog = False
850         
851         #self.listing.setCurrentlyDisplayedFeed(self.key)
852         
853         self.disp = False
854         
855         menu = hildon.AppMenu()
856         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
857         button.set_label("Update feed")
858         button.connect("clicked", self.button_update_clicked)
859         menu.append(button)
860         
861         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
862         button.set_label("Mark all as read")
863         button.connect("clicked", self.buttonReadAllClicked)
864         menu.append(button)
865         
866         if key=="ArchivedArticles":
867             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
868             button.set_label("Delete read articles")
869             button.connect("clicked", self.buttonPurgeArticles)
870             menu.append(button)
871         
872         self.set_app_menu(menu)
873         menu.show_all()
874         
875         self.main_vbox = gtk.VBox(False, 0)
876         self.add(self.main_vbox)
877
878         self.pannableFeed = None
879         self.displayFeed()
880
881         if DownloadBar.downloading ():
882             self.show_download_bar ()
883         
884         self.connect('configure-event', self.on_configure_event)
885         self.connect("destroy", self.destroyWindow)
886
887     def on_configure_event(self, window, event):
888         if getattr(self, 'markup_renderer', None) is None:
889             return
890
891         # Fix up the column width for wrapping the text when the window is
892         # resized (i.e. orientation changed)
893         self.markup_renderer.set_property('wrap-width', event.width-20)  
894         it = self.feedItems.get_iter_first()
895         while it is not None:
896             markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
897             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
898             it = self.feedItems.iter_next(it)
899
900     def destroyWindow(self, *args):
901         #self.feed.saveUnread(CONFIGDIR)
902         self.listing.updateUnread(self.key)
903         self.emit("feed-closed", self.key)
904         self.destroy()
905         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
906         #self.listing.closeCurrentlyDisplayedFeed()
907
908     def fix_title(self, title):
909         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
910
911     def displayFeed(self):
912         if self.pannableFeed:
913             self.pannableFeed.destroy()
914
915         self.pannableFeed = hildon.PannableArea()
916
917         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
918
919         self.feedItems = gtk.ListStore(str, str)
920         #self.feedList = gtk.TreeView(self.feedItems)
921         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
922         self.feedList.set_rules_hint(True)
923
924         selection = self.feedList.get_selection()
925         selection.set_mode(gtk.SELECTION_NONE)
926         #selection.connect("changed", lambda w: True)
927         
928         self.feedList.set_model(self.feedItems)
929         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
930
931         
932         self.feedList.set_hover_selection(False)
933         #self.feedList.set_property('enable-grid-lines', True)
934         #self.feedList.set_property('hildon-mode', 1)
935         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
936         
937         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
938
939         vbox= gtk.VBox(False, 10)
940         vbox.pack_start(self.feedList)
941         
942         self.pannableFeed.add_with_viewport(vbox)
943
944         self.markup_renderer = gtk.CellRendererText()
945         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
946         self.markup_renderer.set_property('background', bg_color) #"#333333")
947         (width, height) = self.get_size()
948         self.markup_renderer.set_property('wrap-width', width-20)
949         self.markup_renderer.set_property('ypad', 8)
950         self.markup_renderer.set_property('xpad', 5)
951         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
952                 markup=FEED_COLUMN_MARKUP)
953         self.feedList.append_column(markup_column)
954
955         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
956         hideReadArticles = self.config.getHideReadArticles()
957         if hideReadArticles:
958             articles = self.feed.getIds(onlyUnread=True)
959         else:
960             articles = self.feed.getIds()
961         
962         hasArticle = False
963         self.articles[:] = []
964         for id in articles:
965             isRead = False
966             try:
967                 isRead = self.feed.isEntryRead(id)
968             except:
969                 pass
970             if not ( isRead and hideReadArticles ):
971                 title = self.fix_title(self.feed.getTitle(id))
972                 self.articles.append(id)
973                 if isRead:
974                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
975                 else:
976                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
977     
978                 self.feedItems.append((markup, id))
979                 hasArticle = True
980         if hasArticle:
981             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
982         else:
983             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
984             self.feedItems.append((markup, ""))
985
986         self.main_vbox.pack_start(self.pannableFeed)
987         self.show_all()
988
989     def clear(self):
990         self.pannableFeed.destroy()
991         #self.remove(self.pannableFeed)
992
993     def on_feedList_row_activated(self, treeview, path): #, column):
994         selection = self.feedList.get_selection()
995         selection.set_mode(gtk.SELECTION_SINGLE)
996         self.feedList.get_selection().select_path(path)
997         model = treeview.get_model()
998         iter = model.get_iter(path)
999         key = model.get_value(iter, FEED_COLUMN_KEY)
1000         # Emulate legacy "button_clicked" call via treeview
1001         gobject.idle_add(self.button_clicked, treeview, key)
1002         #return True
1003
1004     def button_clicked(self, button, index, previous=False, next=False):
1005         newDisp = DisplayArticle(index, self.feed, self.key, self.articles, self.config, self.listing)
1006         stack = hildon.WindowStack.get_default()
1007         if previous:
1008             tmp = stack.peek()
1009             stack.pop_and_push(1, newDisp, tmp)
1010             newDisp.show()
1011             gobject.timeout_add(200, self.destroyArticle, tmp)
1012             #print "previous"
1013             self.disp = newDisp
1014         elif next:
1015             newDisp.show_all()
1016             if type(self.disp).__name__ == "DisplayArticle":
1017                 gobject.timeout_add(200, self.destroyArticle, self.disp)
1018             self.disp = newDisp
1019         else:
1020             self.disp = newDisp
1021             self.disp.show_all()
1022         
1023         self.ids = []
1024         if self.key == "ArchivedArticles":
1025             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
1026         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
1027
1028     def buttonPurgeArticles(self, *widget):
1029         self.clear()
1030         self.feed.purgeReadArticles()
1031         #self.feed.saveFeed(CONFIGDIR)
1032         self.displayFeed()
1033
1034     def destroyArticle(self, handle):
1035         handle.destroyWindow()
1036
1037     def mark_item_read(self, key):
1038         it = self.feedItems.get_iter_first()
1039         while it is not None:
1040             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1041             if k == key:
1042                 title = self.fix_title(self.feed.getTitle(key))
1043                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1044                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1045                 break
1046             it = self.feedItems.iter_next(it)
1047
1048     def onArticleClosed(self, object, index):
1049         selection = self.feedList.get_selection()
1050         selection.set_mode(gtk.SELECTION_NONE)
1051         self.mark_item_read(index)
1052
1053     def onArticleDeleted(self, object, index):
1054         self.clear()
1055         self.feed.removeArticle(index)
1056         #self.feed.saveFeed(CONFIGDIR)
1057         self.displayFeed()
1058
1059
1060     def do_update_feed(self):
1061         self.listing.updateFeed (self.key, priority=-1)
1062
1063     def button_update_clicked(self, button):
1064         gobject.idle_add(self.do_update_feed)
1065             
1066     def show_download_bar(self):
1067         if not type(self.downloadDialog).__name__=="DownloadBar":
1068             self.downloadDialog = DownloadBar(self.window)
1069             self.downloadDialog.connect("download-done", self.onDownloadDone)
1070             self.main_vbox.pack_end(self.downloadDialog,
1071                                     expand=False, fill=False)
1072             self.show_all()
1073         
1074     def onDownloadDone(self, widget, feed):
1075         if feed == self.feed:
1076             self.feed = self.listing.getFeed(self.key)
1077             self.displayFeed()
1078
1079         if feed is None:
1080             self.downloadDialog.destroy()
1081             self.downloadDialog = False
1082
1083     def buttonReadAllClicked(self, button):
1084         #self.clear()
1085         self.feed.markAllAsRead()
1086         it = self.feedItems.get_iter_first()
1087         while it is not None:
1088             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1089             title = self.fix_title(self.feed.getTitle(k))
1090             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1091             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1092             it = self.feedItems.iter_next(it)
1093         #self.displayFeed()
1094         #for index in self.feed.getIds():
1095         #    self.feed.setEntryRead(index)
1096         #    self.mark_item_read(index)
1097
1098
1099 class FeedingIt:
1100     def __init__(self):
1101         # Init the windows
1102         self.window = hildon.StackableWindow()
1103         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1104
1105         self.config = Config(self.window, CONFIGDIR+"config.ini")
1106
1107         try:
1108             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1109             self.orientation.set_mode(self.config.getOrientation())
1110         except Exception, e:
1111             logger.warn("Could not start rotation manager: %s" % str(e))
1112         
1113         self.window.set_title(__appname__)
1114         self.mainVbox = gtk.VBox(False,10)
1115
1116         if isfile(CONFIGDIR+"/feeds.db"):           
1117             self.introLabel = gtk.Label("Loading...")
1118         else:
1119             self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1120         
1121         self.mainVbox.pack_start(self.introLabel)
1122
1123         self.window.add(self.mainVbox)
1124         self.window.show_all()
1125         gobject.idle_add(self.createWindow)
1126
1127     def createWindow(self):
1128         self.category = 0
1129         self.listing = Listing(self.config, CONFIGDIR)
1130
1131         self.downloadDialog = False
1132         
1133         menu = hildon.AppMenu()
1134         # Create a button and add it to the menu
1135         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1136         button.set_label("Update feeds")
1137         button.connect("clicked", self.button_update_clicked, "All")
1138         menu.append(button)
1139         
1140         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1141         button.set_label("Mark all as read")
1142         button.connect("clicked", self.button_markAll)
1143         menu.append(button)
1144
1145         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1146         button.set_label("Add new feed")
1147         button.connect("clicked", lambda b: self.addFeed())
1148         menu.append(button)
1149
1150         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1151         button.set_label("Manage subscriptions")
1152         button.connect("clicked", self.button_organize_clicked)
1153         menu.append(button)
1154
1155         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1156         button.set_label("Settings")
1157         button.connect("clicked", self.button_preferences_clicked)
1158         menu.append(button)
1159        
1160         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1161         button.set_label("About")
1162         button.connect("clicked", self.button_about_clicked)
1163         menu.append(button)
1164         
1165         self.window.set_app_menu(menu)
1166         menu.show_all()
1167         
1168         #self.feedWindow = hildon.StackableWindow()
1169         #self.articleWindow = hildon.StackableWindow()
1170         self.introLabel.destroy()
1171         self.pannableListing = hildon.PannableArea()
1172         self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1173         self.feedList = gtk.TreeView(self.feedItems)
1174         self.feedList.connect('row-activated', self.on_feedList_row_activated)
1175         #self.feedList.set_enable_tree_lines(True)                                                                                           
1176         #self.feedList.set_show_expanders(True)
1177         self.pannableListing.add(self.feedList)
1178
1179         icon_renderer = gtk.CellRendererPixbuf()
1180         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1181         icon_column = gtk.TreeViewColumn('', icon_renderer, \
1182                 pixbuf=COLUMN_ICON)
1183         self.feedList.append_column(icon_column)
1184
1185         markup_renderer = gtk.CellRendererText()
1186         markup_column = gtk.TreeViewColumn('', markup_renderer, \
1187                 markup=COLUMN_MARKUP)
1188         self.feedList.append_column(markup_column)
1189         self.mainVbox.pack_start(self.pannableListing)
1190         self.mainVbox.show_all()
1191
1192         self.displayListing()
1193         self.autoupdate = False
1194         self.checkAutoUpdate()
1195         
1196         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1197         gobject.idle_add(self.late_init)
1198         
1199     def update_progress(self, percent_complete,
1200                         completed, in_progress, queued,
1201                         bytes_downloaded, bytes_updated, bytes_per_second,
1202                         updated_feed):
1203         if (in_progress or queued) and not self.downloadDialog:
1204             self.downloadDialog = DownloadBar(self.window)
1205             self.downloadDialog.connect("download-done", self.onDownloadDone)
1206             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1207             self.mainVbox.show_all()
1208
1209             if self.__dict__.get ('disp', None):
1210                 self.disp.show_download_bar ()
1211
1212     def onDownloadDone(self, widget, feed):
1213         if feed is None:
1214             self.downloadDialog.destroy()
1215             self.downloadDialog = False
1216             self.displayListing()
1217
1218     def late_init(self):
1219         self.dbusHandler = ServerObject(self)
1220         bus = dbus.SessionBus()
1221         bus.add_signal_receiver(handler_function=self.update_progress,
1222                                 bus_name=None,
1223                                 signal_name='UpdateProgress',
1224                                 dbus_interface='org.marcoz.feedingit',
1225                                 path='/org/marcoz/feedingit/update')
1226
1227     def button_markAll(self, button):
1228         for key in self.listing.getListOfFeeds():
1229             feed = self.listing.getFeed(key)
1230             feed.markAllAsRead()
1231             #for id in feed.getIds():
1232             #    feed.setEntryRead(id)
1233             self.listing.updateUnread(key)
1234         self.displayListing()
1235
1236     def button_about_clicked(self, button):
1237         HeAboutDialog.present(self.window, \
1238                 __appname__, \
1239                 ABOUT_ICON, \
1240                 __version__, \
1241                 __description__, \
1242                 ABOUT_COPYRIGHT, \
1243                 ABOUT_WEBSITE, \
1244                 ABOUT_BUGTRACKER, \
1245                 ABOUT_DONATE)
1246
1247     def button_export_clicked(self, button):
1248         opml = ExportOpmlData(self.window, self.listing)
1249         
1250     def button_import_clicked(self, button):
1251         opml = GetOpmlData(self.window)
1252         feeds = opml.getData()
1253         for (title, url) in feeds:
1254             self.listing.addFeed(title, url)
1255         self.displayListing()
1256
1257     def addFeed(self, urlIn="http://"):
1258         wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1259         ret = wizard.run()
1260         if ret == 2:
1261             (title, url, category) = wizard.getData()
1262             if url:
1263                self.listing.addFeed(title, url, category=category)
1264         wizard.destroy()
1265         self.displayListing()
1266
1267     def button_organize_clicked(self, button):
1268         def after_closing():
1269             self.displayListing()
1270         SortList(self.window, self.listing, self, after_closing)
1271
1272     def do_update_feeds(self):
1273         for k in self.listing.getListOfFeeds():
1274             self.listing.updateFeed (k)
1275
1276     def button_update_clicked(self, button, key):
1277         gobject.idle_add(self.do_update_feeds)
1278
1279     def onDownloadsDone(self, *widget):
1280         self.downloadDialog.destroy()
1281         self.downloadDialog = False
1282         self.displayListing()
1283
1284     def button_preferences_clicked(self, button):
1285         dialog = self.config.createDialog()
1286         dialog.connect("destroy", self.prefsClosed)
1287
1288     def show_confirmation_note(self, parent, title):
1289         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1290
1291         retcode = gtk.Dialog.run(note)
1292         note.destroy()
1293         
1294         if retcode == gtk.RESPONSE_OK:
1295             return True
1296         else:
1297             return False
1298         
1299     def saveExpandedLines(self):
1300        self.expandedLines = []
1301        model = self.feedList.get_model()
1302        model.foreach(self.checkLine)
1303
1304     def checkLine(self, model, path, iter, data = None):
1305        if self.feedList.row_expanded(path):
1306            self.expandedLines.append(path)
1307
1308     def restoreExpandedLines(self):
1309        model = self.feedList.get_model()
1310        model.foreach(self.restoreLine)
1311
1312     def restoreLine(self, model, path, iter, data = None):
1313        if path in self.expandedLines:
1314            self.feedList.expand_row(path, False)
1315         
1316     def displayListing(self):
1317         icon_theme = gtk.icon_theme_get_default()
1318         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1319                 gtk.ICON_LOOKUP_USE_BUILTIN)
1320
1321         self.saveExpandedLines()
1322
1323         self.feedItems.clear()
1324         hideReadFeed = self.config.getHideReadFeeds()
1325         order = self.config.getFeedSortOrder()
1326         
1327         categories = self.listing.getListOfCategories()
1328         if len(categories) > 1:
1329             showCategories = True
1330         else:
1331             showCategories = False
1332         
1333         for categoryId in categories:
1334         
1335             title = self.listing.getCategoryTitle(categoryId)
1336             keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1337             
1338             if showCategories and len(keys)>0:
1339                 category = self.feedItems.append(None, (None, title, categoryId))
1340                 #print "catID" + str(categoryId) + " " + str(self.category)
1341                 if categoryId == self.category:
1342                     #print categoryId
1343                     expandedRow = category
1344     
1345             for key in keys:
1346                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1347                 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1348                 updateTime = self.listing.getFeedUpdateTime(key)
1349                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1350                 if unreadItems:
1351                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1352                 else:
1353                     markup = FEED_TEMPLATE % (title, subtitle)
1354         
1355                 try:
1356                     icon_filename = self.listing.getFavicon(key)
1357                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1358                                                    LIST_ICON_SIZE, LIST_ICON_SIZE)
1359                 except:
1360                     pixbuf = default_pixbuf
1361                 
1362                 if showCategories:
1363                     self.feedItems.append(category, (pixbuf, markup, key))
1364                 else:
1365                     self.feedItems.append(None, (pixbuf, markup, key))
1366                     
1367                 
1368         self.restoreExpandedLines()
1369         #try:
1370             
1371         #    self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1372         #except:
1373         #    pass
1374
1375     def on_feedList_row_activated(self, treeview, path, column):
1376         model = treeview.get_model()
1377         iter = model.get_iter(path)
1378         key = model.get_value(iter, COLUMN_KEY)
1379         
1380         try:
1381             #print "Key: " + str(key)
1382             catId = int(key)
1383             self.category = catId
1384             if treeview.row_expanded(path):
1385                 treeview.collapse_row(path)
1386         #else:
1387         #    treeview.expand_row(path, True)
1388             #treeview.collapse_all()
1389             #treeview.expand_row(path, False)
1390             #for i in range(len(path)):
1391             #    self.feedList.expand_row(path[:i+1], False)
1392             #self.show_confirmation_note(self.window, "Working")
1393             #return True
1394         except:
1395             if key:
1396                 self.openFeed(key)
1397             
1398     def openFeed(self, key):
1399         if key != None:
1400             self.disp = DisplayFeed(
1401                 self.listing, self.listing.getFeed(key),
1402                 self.listing.getFeedTitle(key), key,
1403                 self.config)
1404             self.disp.connect("feed-closed", self.onFeedClosed)
1405                 
1406     def openArticle(self, key, id):
1407         if key != None:
1408             self.openFeed(key)
1409             self.disp.button_clicked(None, id)
1410
1411     def onFeedClosed(self, object, key):
1412         self.displayListing()
1413         
1414     def quit(self, *args):
1415         self.window.hide()
1416         gtk.main_quit ()
1417
1418     def run(self):
1419         self.window.connect("destroy", self.quit)
1420         gtk.main()
1421
1422     def prefsClosed(self, *widget):
1423         try:
1424             self.orientation.set_mode(self.config.getOrientation())
1425         except:
1426             pass
1427         self.displayListing()
1428         self.checkAutoUpdate()
1429
1430     def checkAutoUpdate(self, *widget):
1431         interval = int(self.config.getUpdateInterval()*3600000)
1432         if self.config.isAutoUpdateEnabled():
1433             if self.autoupdate == False:
1434                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1435                 self.autoupdate = interval
1436             elif not self.autoupdate == interval:
1437                 # If auto-update is enabled, but not at the right frequency
1438                 gobject.source_remove(self.autoupdateId)
1439                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1440                 self.autoupdate = interval
1441         else:
1442             if not self.autoupdate == False:
1443                 gobject.source_remove(self.autoupdateId)
1444                 self.autoupdate = False
1445
1446     def automaticUpdate(self, *widget):
1447         # Need to check for internet connection
1448         # If no internet connection, try again in 10 minutes:
1449         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1450         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1451         #from time import localtime, strftime
1452         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1453         #file.close()
1454         self.button_update_clicked(None, None)
1455         return True
1456     
1457     def getStatus(self):
1458         status = ""
1459         for key in self.listing.getListOfFeeds():
1460             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1461                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1462         if status == "":
1463             status = "No unread items"
1464         return status
1465
1466     def grabFocus(self):
1467         self.window.present()
1468
1469 if __name__ == "__main__":
1470     mainthread.init ()
1471     debugging.init(dot_directory=".feedingit", program_name="feedingit")
1472
1473     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1474     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1475     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1476     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1477     gobject.threads_init()
1478     if not isdir(CONFIGDIR):
1479         try:
1480             mkdir(CONFIGDIR)
1481         except:
1482             logger.error("Error: Can't create configuration directory")
1483             from sys import exit
1484             exit(1)
1485     app = FeedingIt()
1486     app.run()