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