48faf043d65c9e53bd51eef81070539287b46c40
[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
335     @classmethod
336     def downloading(cls):
337         cls.class_init ()
338         return cls.done != cls.total
339
340     @classmethod
341     def update_progress(cls, percent_complete,
342                         completed, in_progress, queued,
343                         bytes_downloaded, bytes_updated, bytes_per_second,
344                         feed_updated):
345         if not cls.downloadbars:
346             return
347
348         cls.total = completed + in_progress + queued
349         cls.done = completed
350         cls.progress = percent_complete / 100.
351         if cls.progress < 0: cls.progress = 0
352         if cls.progress > 1: cls.progress = 1
353
354         if feed_updated:
355             for ref in cls.downloadbars:
356                 bar = ref ()
357                 if bar is None:
358                     # The download bar disappeared.
359                     cls.downloadbars.remove (ref)
360                 else:
361                     bar.emit("download-done", feed_updated)
362
363         if in_progress == 0 and queued == 0:
364             for ref in cls.downloadbars:
365                 bar = ref ()
366                 if bar is None:
367                     # The download bar disappeared.
368                     cls.downloadbars.remove (ref)
369                 else:
370                     bar.emit("download-done", None)
371             return
372
373         cls.update_bars()
374
375     @classmethod
376     def update_bars(cls):
377         # In preparation for i18n/l10n
378         def N_(a, b, n):
379             return (a if n == 1 else b)
380
381         text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
382                    cls.total)
383                 % (cls.done, cls.total))
384
385         for ref in cls.downloadbars:
386             bar = ref ()
387             if bar is None:
388                 # The download bar disappeared.
389                 cls.downloadbars.remove (ref)
390             else:
391                 bar.set_text(text)
392                 bar.set_fraction(cls.progress)
393
394 class SortList(hildon.StackableWindow):
395     def __init__(self, parent, listing, feedingit, after_closing, category=None):
396         hildon.StackableWindow.__init__(self)
397         self.set_transient_for(parent)
398         if category:
399             self.isEditingCategories = False
400             self.category = category
401             self.set_title(listing.getCategoryTitle(category))
402         else:
403             self.isEditingCategories = True
404             self.set_title('Categories')
405         self.listing = listing
406         self.feedingit = feedingit
407         self.after_closing = after_closing
408         if after_closing:
409             self.connect('destroy', lambda w: self.after_closing())
410         self.vbox2 = gtk.VBox(False, 2)
411
412         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
413         button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
414         button.connect("clicked", self.buttonUp)
415         self.vbox2.pack_start(button, expand=False, fill=False)
416
417         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
418         button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
419         button.connect("clicked", self.buttonDown)
420         self.vbox2.pack_start(button, expand=False, fill=False)
421
422         self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
423
424         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
425         button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
426         button.connect("clicked", self.buttonAdd)
427         self.vbox2.pack_start(button, expand=False, fill=False)
428
429         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
430         button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
431         button.connect("clicked", self.buttonEdit)
432         self.vbox2.pack_start(button, expand=False, fill=False)
433
434         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
435         button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
436         button.connect("clicked", self.buttonDelete)
437         self.vbox2.pack_start(button, expand=False, fill=False)
438
439         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
440         #button.set_label("Done")
441         #button.connect("clicked", self.buttonDone)
442         #self.vbox.pack_start(button)
443         self.hbox2= gtk.HBox(False, 10)
444         self.pannableArea = hildon.PannableArea()
445         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
446         self.treeview = gtk.TreeView(self.treestore)
447         self.hbox2.pack_start(self.pannableArea, expand=True)
448         self.displayFeeds()
449         self.hbox2.pack_end(self.vbox2, expand=False)
450         self.set_default_size(-1, 600)
451         self.add(self.hbox2)
452
453         menu = hildon.AppMenu()
454         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
455         button.set_label("Import from OPML")
456         button.connect("clicked", self.feedingit.button_import_clicked)
457         menu.append(button)
458
459         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
460         button.set_label("Export to OPML")
461         button.connect("clicked", self.feedingit.button_export_clicked)
462         menu.append(button)
463         self.set_app_menu(menu)
464         menu.show_all()
465         
466         self.show_all()
467         #self.connect("destroy", self.buttonDone)
468         
469     def displayFeeds(self):
470         self.treeview.destroy()
471         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
472         self.treeview = gtk.TreeView()
473         
474         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
475         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
476         self.refreshList()
477         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
478
479         self.pannableArea.add(self.treeview)
480
481         #self.show_all()
482
483     def refreshList(self, selected=None, offset=0):
484         #rect = self.treeview.get_visible_rect()
485         #y = rect.y+rect.height
486         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
487         if self.isEditingCategories:
488             for key in self.listing.getListOfCategories():
489                 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
490                 if key == selected:
491                     selectedItem = item
492         else:
493             for key in self.listing.getListOfFeeds(category=self.category):
494                 item = self.treestore.append([self.listing.getFeedTitle(key), key])
495                 if key == selected:
496                     selectedItem = item
497         self.treeview.set_model(self.treestore)
498         if not selected == None:
499             self.treeview.get_selection().select_iter(selectedItem)
500             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
501         self.pannableArea.show_all()
502
503     def getSelectedItem(self):
504         (model, iter) = self.treeview.get_selection().get_selected()
505         if not iter:
506             return None
507         return model.get_value(iter, 1)
508
509     def findIndex(self, key):
510         after = None
511         before = None
512         found = False
513         for row in self.treestore:
514             if found:
515                 return (before, row.iter)
516             if key == list(row)[0]:
517                 found = True
518             else:
519                 before = row.iter
520         return (before, None)
521
522     def buttonUp(self, button):
523         key  = self.getSelectedItem()
524         if not key == None:
525             if self.isEditingCategories:
526                 self.listing.moveCategoryUp(key)
527             else:
528                 self.listing.moveUp(key)
529             self.refreshList(key, -10)
530
531     def buttonDown(self, button):
532         key = self.getSelectedItem()
533         if not key == None:
534             if self.isEditingCategories:
535                 self.listing.moveCategoryDown(key)
536             else:
537                 self.listing.moveDown(key)
538             self.refreshList(key, 10)
539
540     def buttonDelete(self, button):
541         key = self.getSelectedItem()
542
543         message = 'Really remove this feed and its entries?'
544         dlg = hildon.hildon_note_new_confirmation(self, message)
545         response = dlg.run()
546         dlg.destroy()
547         if response == gtk.RESPONSE_OK:
548             if self.isEditingCategories:
549                 self.listing.removeCategory(key)
550             else:
551                 self.listing.removeFeed(key)
552             self.refreshList()
553
554     def buttonEdit(self, button):
555         key = self.getSelectedItem()
556
557         if key == 'ArchivedArticles':
558             message = 'Cannot edit the archived articles feed.'
559             hildon.hildon_banner_show_information(self, '', message)
560             return
561         if self.isEditingCategories:
562             if key is not None:
563                 SortList(self.parent, self.listing, self.feedingit, None, category=key)
564         else:
565             if key is not None:
566                 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
567                 ret = wizard.run()
568                 if ret == 2:
569                     (title, url, category) = wizard.getData()
570                     if url != '':
571                         self.listing.editFeed(key, title, url, category=category)
572                         self.refreshList()
573                 wizard.destroy()
574
575     def buttonDone(self, *args):
576         self.destroy()
577         
578     def buttonAdd(self, button, urlIn="http://"):
579         if self.isEditingCategories:
580             wizard = AddCategoryWizard(self)
581             ret = wizard.run()
582             if ret == 2:
583                 title = wizard.getData()
584                 if (not title == ''): 
585                    self.listing.addCategory(title)
586         else:
587             wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
588             ret = wizard.run()
589             if ret == 2:
590                 (title, url, category) = wizard.getData()
591                 if url:
592                    self.listing.addFeed(title, url, category=category)
593         wizard.destroy()
594         self.refreshList()
595                
596
597 class DisplayArticle(hildon.StackableWindow):
598     """
599     A Widget for displaying an article.
600     """
601     def __init__(self, article_id, feed, feed_key, articles, config, listing):
602         """
603         article_id - The identifier of the article to load.
604
605         feed - The feed object containing the article (an
606         rss_sqlite:Feed object).
607
608         feed_key - The feed's identifier.
609
610         articles - A list of articles from the feed to display.
611         Needed for selecting the next/previous article (article_next).
612
613         config - A configuration object (config:Config).
614
615         listing - The listing object (rss_sqlite:Listing) that
616         contains the feed and article.
617         """
618         hildon.StackableWindow.__init__(self)
619
620         self.article_id = None
621         self.feed = feed
622         self.feed_key = feed_key
623         self.articles = articles
624         self.config = config
625         self.listing = listing
626
627         self.set_title(self.listing.getFeedTitle(feed_key))
628
629         # Init the article display
630         self.view = WebView()
631         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
632         self.view.connect("motion-notify-event", lambda w,ev: True)
633         self.view.connect('load-started', self.load_started)
634         self.view.connect('load-finished', self.load_finished)
635         self.view.connect('navigation-requested', self.navigation_requested)
636         self.view.connect("button_press_event", self.button_pressed)
637         self.gestureId = self.view.connect(
638             "button_release_event", self.button_released)
639
640         self.pannable_article = hildon.PannableArea()
641         self.pannable_article.add(self.view)
642
643         self.add(self.pannable_article)
644
645         self.pannable_article.show_all()
646
647         # Create the menu.
648         menu = hildon.AppMenu()
649
650         def menu_button(label, callback):
651             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
652             button.set_label(label)
653             button.connect("clicked", callback)
654             menu.append(button)
655
656         menu_button("Allow horizontal scrolling", self.horiz_scrolling_button)
657         menu_button("Open in browser", self.open_in_browser)
658         if feed_key == "ArchivedArticles":
659             menu_button(
660                 "Remove from archived articles", self.remove_archive_button)
661         else:
662             menu_button("Add to archived articles", self.archive_button)
663         
664         self.set_app_menu(menu)
665         menu.show_all()
666         
667         self.destroyId = self.connect("destroy", self.destroyWindow)
668
669         self.article_open(article_id)
670
671     def article_open(self, article_id):
672         """
673         Load the article with the specified id.
674         """
675         # If an article was open, close it.
676         if self.article_id is not None:
677             self.article_closed()
678
679         self.article_id = article_id
680         self.set_for_removal = False
681         self.loadedArticle = False
682         self.initial_article_load = True
683
684         contentLink = self.feed.getContentLink(self.article_id)
685         if contentLink.startswith("/home/user/"):
686             self.view.open("file://%s" % contentLink)
687             self.currentUrl = self.feed.getExternalLink(self.article_id)
688         else:
689             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
690             self.currentUrl = str(contentLink)
691
692         self.feed.setEntryRead(self.article_id)
693
694     def article_closed(self):
695         """
696         The user has navigated away from the article.  Execute any
697         pending actions.
698         """
699         if self.set_for_removal:
700             self.emit("article-deleted", self.article_id)
701         else:
702             self.emit("article-closed", self.article_id)
703
704
705     def navigation_requested(self, wv, fr, req):
706         """
707         http://webkitgtk.org/reference/webkitgtk-webkitwebview.html#WebKitWebView-navigation-requested
708
709         wv - a WebKitWebView
710         fr - a WebKitWebFrame
711         req - WebKitNetworkRequest
712         """
713         if self.initial_article_load:
714             # Always initially load an article in the internal
715             # browser.
716             self.initial_article_load = False
717             return False
718
719         # When following a link, only use the internal browser if so
720         # configured.  Otherwise, launch an external browser.
721         if self.config.getOpenInExternalBrowser():
722             self.open_in_browser(None, req.get_uri())
723             return True
724         else:
725             return False
726
727     def load_started(self, *widget):
728         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
729
730     def load_finished(self, *widget):
731         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
732         frame = self.view.get_main_frame()
733         if self.loadedArticle:
734             self.currentUrl = frame.get_uri()
735         else:
736             self.loadedArticle = True
737
738     def button_pressed(self, window, event):
739         """
740         The user pressed a "mouse button" (in our case, this means the
741         user likely started to drag with the finger).
742
743         We are only interested in whether the user performs a drag.
744         We record the starting position and when the user "releases
745         the button," we see how far the mouse moved.
746         """
747         self.coords = (event.x, event.y)
748         
749     def button_released(self, window, event):
750         x = self.coords[0] - event.x
751         y = self.coords[1] - event.y
752         
753         if (2*abs(y) < abs(x)):
754             if (x > 15):
755                 self.article_next(forward=False)
756             elif (x<-15):
757                 self.article_next(forward=True)
758
759             # We handled the event.  Don't propagate it further.
760             return True
761
762     def article_next(self, forward=True):
763         """
764         Advance to the next (or, if forward is false, the previous)
765         article.
766         """
767         first_id = None
768         id = self.article_id
769         i = 0
770         while True:
771             i += 1
772             id = self.feed.getNextId(id, forward)
773             if id == first_id:
774                 # We looped.
775                 break
776
777             if first_id is None:
778                 first_id = id
779
780             if id in self.articles:
781                 self.article_open(id)
782                 break
783
784     def destroyWindow(self, *args):
785         self.article_closed()
786         self.disconnect(self.destroyId)
787         self.destroy()
788         
789     def horiz_scrolling_button(self, *widget):
790         self.pannable_article.disconnect(self.gestureId)
791         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
792         
793     def archive_button(self, *widget):
794         # Call the listing.addArchivedArticle
795         self.listing.addArchivedArticle(self.feed_key, self.article_id)
796         
797     def remove_archive_button(self, *widget):
798         self.set_for_removal = True
799
800     def open_in_browser(self, object, link=None):
801         """
802         Open the specified link using the system's browser.  If not
803         link is specified, reopen the current page using the system's
804         browser.
805         """
806         if link == None:
807             link = self.currentUrl
808
809         bus = dbus.SessionBus()
810         b_proxy = bus.get_object("com.nokia.osso_browser",
811                                  "/com/nokia/osso_browser/request")
812         b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
813
814         notify("Opening %s" % link)
815
816         # We open the link asynchronously: if the web browser is not
817         # already running, this can take a while.
818         def error_handler():
819             """
820             Something went wrong opening the URL.
821             """
822             def e(exception):
823                 notify("Error opening %s: %s" % (link, str(exception)))
824             return e
825
826         b_iface.open_new_window(link,
827                                 reply_handler=lambda *args: None,
828                                 error_handler=error_handler())
829
830 class DisplayFeed(hildon.StackableWindow):
831     def __init__(self, listing, config):
832         hildon.StackableWindow.__init__(self)
833         self.connect('configure-event', self.on_configure_event)
834         self.connect("delete_event", self.delete_window)
835         self.connect("destroy", self.destroyWindow)
836
837         self.listing = listing
838         self.config = config
839
840         # Articles to show.
841         #
842         # If hide read articles is set, this is set to the set of
843         # unread articles at the time that feed is loaded.  The last
844         # bit is important: when the user selects the next article,
845         # but then decides to move back, previous should select the
846         # just read article.
847         self.articles = list()
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         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
867         button.set_label("Delete read articles")
868         button.connect("clicked", self.buttonPurgeArticles)
869         menu.append(button)
870         self.archived_article_buttons = [button]
871         
872         self.set_app_menu(menu)
873         menu.show_all()
874         
875         self.feedItems = gtk.ListStore(str, str)
876
877         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
878         self.feedList.set_model(self.feedItems)
879         self.feedList.set_rules_hint(True)
880         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
881         self.feedList.set_hover_selection(False)
882         self.feedList.connect('hildon-row-tapped',
883                               self.on_feedList_row_activated)
884
885         self.markup_renderer = gtk.CellRendererText()
886         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
887         self.markup_renderer.set_property('background', bg_color) #"#333333")
888         (width, height) = self.get_size()
889         self.markup_renderer.set_property('wrap-width', width-20)
890         self.markup_renderer.set_property('ypad', 8)
891         self.markup_renderer.set_property('xpad', 5)
892         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
893                 markup=FEED_COLUMN_MARKUP)
894         self.feedList.append_column(markup_column)
895
896         vbox = gtk.VBox(False, 10)
897         vbox.pack_start(self.feedList)
898         
899         self.pannableFeed = hildon.PannableArea()
900         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
901         self.pannableFeed.add_with_viewport(vbox)
902
903         self.main_vbox = gtk.VBox(False, 0)
904         self.main_vbox.pack_start(self.pannableFeed)
905
906         self.add(self.main_vbox)
907
908         self.main_vbox.show_all()
909
910     def on_configure_event(self, window, event):
911         if getattr(self, 'markup_renderer', None) is None:
912             return
913
914         # Fix up the column width for wrapping the text when the window is
915         # resized (i.e. orientation changed)
916         self.markup_renderer.set_property('wrap-width', event.width-20)  
917         it = self.feedItems.get_iter_first()
918         while it is not None:
919             markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
920             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
921             it = self.feedItems.iter_next(it)
922
923     def delete_window(self, *args):
924         """
925         Prevent the window from being deleted.
926         """
927         self.hide()
928
929         try:
930             key = self.key
931         except AttributeError:
932             key = None
933
934         if key is not None:
935             self.listing.updateUnread(key)
936             self.emit("feed-closed", key)
937             self.key = None
938             self.feedItems.clear()
939
940         return True
941
942     def destroyWindow(self, *args):
943         try:
944             key = self.key
945         except AttributeError:
946             key = None
947
948         self.destroy()
949
950     def fix_title(self, title):
951         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
952
953     def displayFeed(self, key=None):
954         """
955         Select and display a feed.  If feed, title and key are None,
956         reloads the current feed.
957         """
958         if key:
959             try:
960                 old_key = self.key
961             except AttributeError:
962                 old_key = None
963
964             if old_key:
965                 self.listing.updateUnread(self.key)
966                 self.emit("feed-closed", self.key)
967
968             self.feed = self.listing.getFeed(key)
969             self.feedTitle = self.listing.getFeedTitle(key)
970             self.key = key
971
972             self.set_title(self.feedTitle)
973
974             selection = self.feedList.get_selection()
975             if selection is not None:
976                 selection.set_mode(gtk.SELECTION_NONE)
977
978             for b in self.archived_article_buttons:
979                 if key == "ArchivedArticles":
980                     b.show()
981                 else:
982                     b.hide()
983         
984         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
985         hideReadArticles = self.config.getHideReadArticles()
986         if hideReadArticles:
987             articles = self.feed.getIds(onlyUnread=True)
988         else:
989             articles = self.feed.getIds()
990         
991         self.articles[:] = []
992
993         self.feedItems.clear()
994         for id in articles:
995             try:
996                 isRead = self.feed.isEntryRead(id)
997             except Exception:
998                 isRead = False
999             if not ( isRead and hideReadArticles ):
1000                 title = self.fix_title(self.feed.getTitle(id))
1001                 self.articles.append(id)
1002                 if isRead:
1003                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1004                 else:
1005                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
1006     
1007                 self.feedItems.append((markup, id))
1008         if not articles:
1009             # No articles.
1010             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
1011             self.feedItems.append((markup, ""))
1012
1013         self.show()
1014
1015     def clear(self):
1016         self.pannableFeed.destroy()
1017         #self.remove(self.pannableFeed)
1018
1019     def on_feedList_row_activated(self, treeview, path): #, column):
1020         if not self.articles:
1021             # There are not actually any articles.  Ignore.
1022             return False
1023
1024         selection = self.feedList.get_selection()
1025         selection.set_mode(gtk.SELECTION_SINGLE)
1026         self.feedList.get_selection().select_path(path)
1027         model = treeview.get_model()
1028         iter = model.get_iter(path)
1029         key = model.get_value(iter, FEED_COLUMN_KEY)
1030         # Emulate legacy "button_clicked" call via treeview
1031         gobject.idle_add(self.button_clicked, treeview, key)
1032         #return True
1033
1034     def button_clicked(self, button, index, previous=False, next=False):
1035         newDisp = DisplayArticle(index, self.feed, self.key, self.articles, self.config, self.listing)
1036         stack = hildon.WindowStack.get_default()
1037         if previous:
1038             tmp = stack.peek()
1039             stack.pop_and_push(1, newDisp, tmp)
1040             newDisp.show()
1041             gobject.timeout_add(200, self.destroyArticle, tmp)
1042             #print "previous"
1043             self.disp = newDisp
1044         elif next:
1045             newDisp.show_all()
1046             if type(self.disp).__name__ == "DisplayArticle":
1047                 gobject.timeout_add(200, self.destroyArticle, self.disp)
1048             self.disp = newDisp
1049         else:
1050             self.disp = newDisp
1051             self.disp.show_all()
1052         
1053         self.ids = []
1054         if self.key == "ArchivedArticles":
1055             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
1056         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
1057
1058     def buttonPurgeArticles(self, *widget):
1059         self.clear()
1060         self.feed.purgeReadArticles()
1061         #self.feed.saveFeed(CONFIGDIR)
1062         self.displayFeed()
1063
1064     def destroyArticle(self, handle):
1065         handle.destroyWindow()
1066
1067     def mark_item_read(self, key):
1068         it = self.feedItems.get_iter_first()
1069         while it is not None:
1070             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1071             if k == key:
1072                 title = self.fix_title(self.feed.getTitle(key))
1073                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1074                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1075                 break
1076             it = self.feedItems.iter_next(it)
1077
1078     def onArticleClosed(self, object, index):
1079         selection = self.feedList.get_selection()
1080         selection.set_mode(gtk.SELECTION_NONE)
1081         self.mark_item_read(index)
1082
1083     def onArticleDeleted(self, object, index):
1084         self.clear()
1085         self.feed.removeArticle(index)
1086         #self.feed.saveFeed(CONFIGDIR)
1087         self.displayFeed()
1088
1089
1090     def do_update_feed(self):
1091         self.listing.updateFeed (self.key, priority=-1)
1092
1093     def button_update_clicked(self, button):
1094         gobject.idle_add(self.do_update_feed)
1095             
1096     def show_download_bar(self):
1097         if not type(self.downloadDialog).__name__=="DownloadBar":
1098             self.downloadDialog = DownloadBar(self.window)
1099             self.downloadDialog.connect("download-done", self.onDownloadDone)
1100             self.main_vbox.pack_end(self.downloadDialog,
1101                                     expand=False, fill=False)
1102             self.downloadDialog.show()
1103
1104     def onDownloadDone(self, widget, feed):
1105         if feed is not None and hasattr(self, 'feed') and feed == self.feed:
1106             self.feed = self.listing.getFeed(self.key)
1107             self.displayFeed()
1108
1109         if feed is None:
1110             self.downloadDialog.destroy()
1111             self.downloadDialog = False
1112
1113     def buttonReadAllClicked(self, button):
1114         #self.clear()
1115         self.feed.markAllAsRead()
1116         it = self.feedItems.get_iter_first()
1117         while it is not None:
1118             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1119             title = self.fix_title(self.feed.getTitle(k))
1120             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1121             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1122             it = self.feedItems.iter_next(it)
1123         #self.displayFeed()
1124         #for index in self.feed.getIds():
1125         #    self.feed.setEntryRead(index)
1126         #    self.mark_item_read(index)
1127
1128
1129 class FeedingIt:
1130     def __init__(self):
1131         # Init the windows
1132         self.window = hildon.StackableWindow()
1133         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1134
1135         self.config = Config(self.window, CONFIGDIR+"config.ini")
1136
1137         try:
1138             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1139             self.orientation.set_mode(self.config.getOrientation())
1140         except Exception, e:
1141             logger.warn("Could not start rotation manager: %s" % str(e))
1142         
1143         self.window.set_title(__appname__)
1144         self.mainVbox = gtk.VBox(False,10)
1145
1146         if isfile(CONFIGDIR+"/feeds.db"):           
1147             self.introLabel = gtk.Label("Loading...")
1148         else:
1149             self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1150         
1151         self.mainVbox.pack_start(self.introLabel)
1152
1153         self.window.add(self.mainVbox)
1154         self.window.show_all()
1155         gobject.idle_add(self.createWindow)
1156
1157     def createWindow(self):
1158         self.category = 0
1159         self.listing = Listing(self.config, CONFIGDIR)
1160
1161         self.downloadDialog = False
1162
1163         self.introLabel.destroy()
1164         self.pannableListing = hildon.PannableArea()
1165
1166         # The main area is a view consisting of an icon and two
1167         # strings.  The view is bound to the Listing's database via a
1168         # TreeStore.
1169
1170         self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1171         self.feedList = gtk.TreeView(self.feedItems)
1172         self.feedList.connect('row-activated', self.on_feedList_row_activated)
1173
1174         self.pannableListing.add(self.feedList)
1175
1176         icon_renderer = gtk.CellRendererPixbuf()
1177         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1178         icon_column = gtk.TreeViewColumn('', icon_renderer, \
1179                 pixbuf=COLUMN_ICON)
1180         self.feedList.append_column(icon_column)
1181
1182         markup_renderer = gtk.CellRendererText()
1183         markup_column = gtk.TreeViewColumn('', markup_renderer, \
1184                 markup=COLUMN_MARKUP)
1185         self.feedList.append_column(markup_column)
1186         self.mainVbox.pack_start(self.pannableListing)
1187         self.mainVbox.show_all()
1188
1189         self.displayListing()
1190         
1191         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1192         gobject.idle_add(self.late_init)
1193
1194     def update_progress(self, percent_complete,
1195                         completed, in_progress, queued,
1196                         bytes_downloaded, bytes_updated, bytes_per_second,
1197                         updated_feed):
1198         if (in_progress or queued) and not self.downloadDialog:
1199             self.downloadDialog = DownloadBar(self.window)
1200             self.downloadDialog.connect("download-done", self.onDownloadDone)
1201             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1202             self.downloadDialog.show()
1203
1204             if self.__dict__.get ('disp', None):
1205                 self.disp.show_download_bar ()
1206
1207     def onDownloadDone(self, widget, feed):
1208         if feed is None:
1209             self.downloadDialog.destroy()
1210             self.downloadDialog = False
1211             self.displayListing()
1212
1213     def late_init(self):
1214         # Finish building the GUI.
1215         menu = hildon.AppMenu()
1216         # Create a button and add it to the menu
1217         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1218         button.set_label("Update feeds")
1219         button.connect("clicked", self.button_update_clicked, "All")
1220         menu.append(button)
1221         
1222         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1223         button.set_label("Mark all as read")
1224         button.connect("clicked", self.button_markAll)
1225         menu.append(button)
1226
1227         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1228         button.set_label("Add new feed")
1229         button.connect("clicked", lambda b: self.addFeed())
1230         menu.append(button)
1231
1232         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1233         button.set_label("Manage subscriptions")
1234         button.connect("clicked", self.button_organize_clicked)
1235         menu.append(button)
1236
1237         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1238         button.set_label("Settings")
1239         button.connect("clicked", self.button_preferences_clicked)
1240         menu.append(button)
1241        
1242         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1243         button.set_label("About")
1244         button.connect("clicked", self.button_about_clicked)
1245         menu.append(button)
1246         
1247         self.window.set_app_menu(menu)
1248         menu.show_all()
1249
1250         # Initialize the DBus interface.
1251         self.dbusHandler = ServerObject(self)
1252         bus = dbus.SessionBus()
1253         bus.add_signal_receiver(handler_function=self.update_progress,
1254                                 bus_name=None,
1255                                 signal_name='UpdateProgress',
1256                                 dbus_interface='org.marcoz.feedingit',
1257                                 path='/org/marcoz/feedingit/update')
1258
1259         # Check whether auto-update is enabled.
1260         self.autoupdate = False
1261         self.checkAutoUpdate()
1262
1263         gobject.idle_add(self.build_feed_display)
1264
1265     def build_feed_display(self):
1266         if not hasattr(self, 'disp'):
1267             self.disp = DisplayFeed(self.listing, self.config)
1268             self.disp.connect("feed-closed", self.onFeedClosed)
1269
1270     def button_markAll(self, button):
1271         for key in self.listing.getListOfFeeds():
1272             feed = self.listing.getFeed(key)
1273             feed.markAllAsRead()
1274             #for id in feed.getIds():
1275             #    feed.setEntryRead(id)
1276             self.listing.updateUnread(key)
1277         self.displayListing()
1278
1279     def button_about_clicked(self, button):
1280         HeAboutDialog.present(self.window, \
1281                 __appname__, \
1282                 ABOUT_ICON, \
1283                 __version__, \
1284                 __description__, \
1285                 ABOUT_COPYRIGHT, \
1286                 ABOUT_WEBSITE, \
1287                 ABOUT_BUGTRACKER, \
1288                 ABOUT_DONATE)
1289
1290     def button_export_clicked(self, button):
1291         opml = ExportOpmlData(self.window, self.listing)
1292         
1293     def button_import_clicked(self, button):
1294         opml = GetOpmlData(self.window)
1295         feeds = opml.getData()
1296         for (title, url) in feeds:
1297             self.listing.addFeed(title, url)
1298         self.displayListing()
1299
1300     def addFeed(self, urlIn="http://"):
1301         wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1302         ret = wizard.run()
1303         if ret == 2:
1304             (title, url, category) = wizard.getData()
1305             if url:
1306                self.listing.addFeed(title, url, category=category)
1307         wizard.destroy()
1308         self.displayListing()
1309
1310     def button_organize_clicked(self, button):
1311         def after_closing():
1312             self.displayListing()
1313         SortList(self.window, self.listing, self, after_closing)
1314
1315     def do_update_feeds(self):
1316         for k in self.listing.getListOfFeeds():
1317             self.listing.updateFeed (k)
1318
1319     def button_update_clicked(self, button, key):
1320         gobject.idle_add(self.do_update_feeds)
1321
1322     def onDownloadsDone(self, *widget):
1323         self.downloadDialog.destroy()
1324         self.downloadDialog = False
1325         self.displayListing()
1326
1327     def button_preferences_clicked(self, button):
1328         dialog = self.config.createDialog()
1329         dialog.connect("destroy", self.prefsClosed)
1330
1331     def show_confirmation_note(self, parent, title):
1332         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1333
1334         retcode = gtk.Dialog.run(note)
1335         note.destroy()
1336         
1337         if retcode == gtk.RESPONSE_OK:
1338             return True
1339         else:
1340             return False
1341         
1342     def saveExpandedLines(self):
1343        self.expandedLines = []
1344        model = self.feedList.get_model()
1345        model.foreach(self.checkLine)
1346
1347     def checkLine(self, model, path, iter, data = None):
1348        if self.feedList.row_expanded(path):
1349            self.expandedLines.append(path)
1350
1351     def restoreExpandedLines(self):
1352        model = self.feedList.get_model()
1353        model.foreach(self.restoreLine)
1354
1355     def restoreLine(self, model, path, iter, data = None):
1356        if path in self.expandedLines:
1357            self.feedList.expand_row(path, False)
1358         
1359     def displayListing(self):
1360         icon_theme = gtk.icon_theme_get_default()
1361         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1362                 gtk.ICON_LOOKUP_USE_BUILTIN)
1363
1364         self.saveExpandedLines()
1365
1366         self.feedItems.clear()
1367         hideReadFeed = self.config.getHideReadFeeds()
1368         order = self.config.getFeedSortOrder()
1369         
1370         categories = self.listing.getListOfCategories()
1371         if len(categories) > 1:
1372             showCategories = True
1373         else:
1374             showCategories = False
1375         
1376         for categoryId in categories:
1377         
1378             title = self.listing.getCategoryTitle(categoryId)
1379             keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1380             
1381             if showCategories and len(keys)>0:
1382                 category = self.feedItems.append(None, (None, title, categoryId))
1383                 #print "catID" + str(categoryId) + " " + str(self.category)
1384                 if categoryId == self.category:
1385                     #print categoryId
1386                     expandedRow = category
1387     
1388             for key in keys:
1389                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1390                 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1391                 updateTime = self.listing.getFeedUpdateTime(key)
1392                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1393                 if unreadItems:
1394                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1395                 else:
1396                     markup = FEED_TEMPLATE % (title, subtitle)
1397         
1398                 try:
1399                     icon_filename = self.listing.getFavicon(key)
1400                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1401                                                    LIST_ICON_SIZE, LIST_ICON_SIZE)
1402                 except:
1403                     pixbuf = default_pixbuf
1404                 
1405                 if showCategories:
1406                     self.feedItems.append(category, (pixbuf, markup, key))
1407                 else:
1408                     self.feedItems.append(None, (pixbuf, markup, key))
1409                     
1410                 
1411         self.restoreExpandedLines()
1412         #try:
1413             
1414         #    self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1415         #except:
1416         #    pass
1417
1418     def on_feedList_row_activated(self, treeview, path, column):
1419         model = treeview.get_model()
1420         iter = model.get_iter(path)
1421         key = model.get_value(iter, COLUMN_KEY)
1422         
1423         try:
1424             #print "Key: " + str(key)
1425             catId = int(key)
1426             self.category = catId
1427             if treeview.row_expanded(path):
1428                 treeview.collapse_row(path)
1429         #else:
1430         #    treeview.expand_row(path, True)
1431             #treeview.collapse_all()
1432             #treeview.expand_row(path, False)
1433             #for i in range(len(path)):
1434             #    self.feedList.expand_row(path[:i+1], False)
1435             #self.show_confirmation_note(self.window, "Working")
1436             #return True
1437         except:
1438             if key:
1439                 self.openFeed(key)
1440             
1441     def openFeed(self, key):
1442         if key != None:
1443             self.build_feed_display()
1444             self.disp.displayFeed(key)
1445                 
1446     def openArticle(self, key, id):
1447         if key != None:
1448             self.openFeed(key)
1449             self.disp.button_clicked(None, id)
1450
1451     def onFeedClosed(self, object, key):
1452         gobject.idle_add(self.displayListing)
1453         
1454     def quit(self, *args):
1455         self.window.hide()
1456         gtk.main_quit ()
1457
1458     def run(self):
1459         self.window.connect("destroy", self.quit)
1460         gtk.main()
1461
1462     def prefsClosed(self, *widget):
1463         try:
1464             self.orientation.set_mode(self.config.getOrientation())
1465         except:
1466             pass
1467         self.displayListing()
1468         self.checkAutoUpdate()
1469
1470     def checkAutoUpdate(self, *widget):
1471         interval = int(self.config.getUpdateInterval()*3600000)
1472         if self.config.isAutoUpdateEnabled():
1473             if self.autoupdate == False:
1474                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1475                 self.autoupdate = interval
1476             elif not self.autoupdate == interval:
1477                 # If auto-update is enabled, but not at the right frequency
1478                 gobject.source_remove(self.autoupdateId)
1479                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1480                 self.autoupdate = interval
1481         else:
1482             if not self.autoupdate == False:
1483                 gobject.source_remove(self.autoupdateId)
1484                 self.autoupdate = False
1485
1486     def automaticUpdate(self, *widget):
1487         # Need to check for internet connection
1488         # If no internet connection, try again in 10 minutes:
1489         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1490         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1491         #from time import localtime, strftime
1492         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1493         #file.close()
1494         self.button_update_clicked(None, None)
1495         return True
1496     
1497     def getStatus(self):
1498         status = ""
1499         for key in self.listing.getListOfFeeds():
1500             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1501                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1502         if status == "":
1503             status = "No unread items"
1504         return status
1505
1506     def grabFocus(self):
1507         self.window.present()
1508
1509 if __name__ == "__main__":
1510     mainthread.init ()
1511     debugging.init(dot_directory=".feedingit", program_name="feedingit")
1512
1513     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1514     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1515     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1516     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1517     gobject.threads_init()
1518     if not isdir(CONFIGDIR):
1519         try:
1520             mkdir(CONFIGDIR)
1521         except:
1522             logger.error("Error: Can't create configuration directory")
1523             from sys import exit
1524             exit(1)
1525     app = FeedingIt()
1526     app.run()