926f139380a1c1cffeee58fb9c71bb72c8289b26
[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         menu = hildon.AppMenu()
1164         # Create a button and add it to the menu
1165         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1166         button.set_label("Update feeds")
1167         button.connect("clicked", self.button_update_clicked, "All")
1168         menu.append(button)
1169         
1170         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1171         button.set_label("Mark all as read")
1172         button.connect("clicked", self.button_markAll)
1173         menu.append(button)
1174
1175         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1176         button.set_label("Add new feed")
1177         button.connect("clicked", lambda b: self.addFeed())
1178         menu.append(button)
1179
1180         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1181         button.set_label("Manage subscriptions")
1182         button.connect("clicked", self.button_organize_clicked)
1183         menu.append(button)
1184
1185         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1186         button.set_label("Settings")
1187         button.connect("clicked", self.button_preferences_clicked)
1188         menu.append(button)
1189        
1190         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1191         button.set_label("About")
1192         button.connect("clicked", self.button_about_clicked)
1193         menu.append(button)
1194         
1195         self.window.set_app_menu(menu)
1196         menu.show_all()
1197         
1198         #self.feedWindow = hildon.StackableWindow()
1199         #self.articleWindow = hildon.StackableWindow()
1200         self.introLabel.destroy()
1201         self.pannableListing = hildon.PannableArea()
1202         self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1203         self.feedList = gtk.TreeView(self.feedItems)
1204         self.feedList.connect('row-activated', self.on_feedList_row_activated)
1205         #self.feedList.set_enable_tree_lines(True)                                                                                           
1206         #self.feedList.set_show_expanders(True)
1207         self.pannableListing.add(self.feedList)
1208
1209         icon_renderer = gtk.CellRendererPixbuf()
1210         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1211         icon_column = gtk.TreeViewColumn('', icon_renderer, \
1212                 pixbuf=COLUMN_ICON)
1213         self.feedList.append_column(icon_column)
1214
1215         markup_renderer = gtk.CellRendererText()
1216         markup_column = gtk.TreeViewColumn('', markup_renderer, \
1217                 markup=COLUMN_MARKUP)
1218         self.feedList.append_column(markup_column)
1219         self.mainVbox.pack_start(self.pannableListing)
1220         self.mainVbox.show_all()
1221
1222         self.displayListing()
1223         self.autoupdate = False
1224         self.checkAutoUpdate()
1225         
1226         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1227         gobject.idle_add(self.late_init)
1228         
1229     def update_progress(self, percent_complete,
1230                         completed, in_progress, queued,
1231                         bytes_downloaded, bytes_updated, bytes_per_second,
1232                         updated_feed):
1233         if (in_progress or queued) and not self.downloadDialog:
1234             self.downloadDialog = DownloadBar(self.window)
1235             self.downloadDialog.connect("download-done", self.onDownloadDone)
1236             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1237             self.downloadDialog.show()
1238
1239             if self.__dict__.get ('disp', None):
1240                 self.disp.show_download_bar ()
1241
1242     def onDownloadDone(self, widget, feed):
1243         if feed is None:
1244             self.downloadDialog.destroy()
1245             self.downloadDialog = False
1246             self.displayListing()
1247
1248     def late_init(self):
1249         self.dbusHandler = ServerObject(self)
1250         bus = dbus.SessionBus()
1251         bus.add_signal_receiver(handler_function=self.update_progress,
1252                                 bus_name=None,
1253                                 signal_name='UpdateProgress',
1254                                 dbus_interface='org.marcoz.feedingit',
1255                                 path='/org/marcoz/feedingit/update')
1256
1257         gobject.idle_add(self.build_feed_display)
1258
1259     def build_feed_display(self):
1260         if not hasattr(self, 'disp'):
1261             self.disp = DisplayFeed(self.listing, self.config)
1262             self.disp.connect("feed-closed", self.onFeedClosed)
1263
1264     def button_markAll(self, button):
1265         for key in self.listing.getListOfFeeds():
1266             feed = self.listing.getFeed(key)
1267             feed.markAllAsRead()
1268             #for id in feed.getIds():
1269             #    feed.setEntryRead(id)
1270             self.listing.updateUnread(key)
1271         self.displayListing()
1272
1273     def button_about_clicked(self, button):
1274         HeAboutDialog.present(self.window, \
1275                 __appname__, \
1276                 ABOUT_ICON, \
1277                 __version__, \
1278                 __description__, \
1279                 ABOUT_COPYRIGHT, \
1280                 ABOUT_WEBSITE, \
1281                 ABOUT_BUGTRACKER, \
1282                 ABOUT_DONATE)
1283
1284     def button_export_clicked(self, button):
1285         opml = ExportOpmlData(self.window, self.listing)
1286         
1287     def button_import_clicked(self, button):
1288         opml = GetOpmlData(self.window)
1289         feeds = opml.getData()
1290         for (title, url) in feeds:
1291             self.listing.addFeed(title, url)
1292         self.displayListing()
1293
1294     def addFeed(self, urlIn="http://"):
1295         wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1296         ret = wizard.run()
1297         if ret == 2:
1298             (title, url, category) = wizard.getData()
1299             if url:
1300                self.listing.addFeed(title, url, category=category)
1301         wizard.destroy()
1302         self.displayListing()
1303
1304     def button_organize_clicked(self, button):
1305         def after_closing():
1306             self.displayListing()
1307         SortList(self.window, self.listing, self, after_closing)
1308
1309     def do_update_feeds(self):
1310         for k in self.listing.getListOfFeeds():
1311             self.listing.updateFeed (k)
1312
1313     def button_update_clicked(self, button, key):
1314         gobject.idle_add(self.do_update_feeds)
1315
1316     def onDownloadsDone(self, *widget):
1317         self.downloadDialog.destroy()
1318         self.downloadDialog = False
1319         self.displayListing()
1320
1321     def button_preferences_clicked(self, button):
1322         dialog = self.config.createDialog()
1323         dialog.connect("destroy", self.prefsClosed)
1324
1325     def show_confirmation_note(self, parent, title):
1326         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1327
1328         retcode = gtk.Dialog.run(note)
1329         note.destroy()
1330         
1331         if retcode == gtk.RESPONSE_OK:
1332             return True
1333         else:
1334             return False
1335         
1336     def saveExpandedLines(self):
1337        self.expandedLines = []
1338        model = self.feedList.get_model()
1339        model.foreach(self.checkLine)
1340
1341     def checkLine(self, model, path, iter, data = None):
1342        if self.feedList.row_expanded(path):
1343            self.expandedLines.append(path)
1344
1345     def restoreExpandedLines(self):
1346        model = self.feedList.get_model()
1347        model.foreach(self.restoreLine)
1348
1349     def restoreLine(self, model, path, iter, data = None):
1350        if path in self.expandedLines:
1351            self.feedList.expand_row(path, False)
1352         
1353     def displayListing(self):
1354         icon_theme = gtk.icon_theme_get_default()
1355         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1356                 gtk.ICON_LOOKUP_USE_BUILTIN)
1357
1358         self.saveExpandedLines()
1359
1360         self.feedItems.clear()
1361         hideReadFeed = self.config.getHideReadFeeds()
1362         order = self.config.getFeedSortOrder()
1363         
1364         categories = self.listing.getListOfCategories()
1365         if len(categories) > 1:
1366             showCategories = True
1367         else:
1368             showCategories = False
1369         
1370         for categoryId in categories:
1371         
1372             title = self.listing.getCategoryTitle(categoryId)
1373             keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1374             
1375             if showCategories and len(keys)>0:
1376                 category = self.feedItems.append(None, (None, title, categoryId))
1377                 #print "catID" + str(categoryId) + " " + str(self.category)
1378                 if categoryId == self.category:
1379                     #print categoryId
1380                     expandedRow = category
1381     
1382             for key in keys:
1383                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1384                 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1385                 updateTime = self.listing.getFeedUpdateTime(key)
1386                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1387                 if unreadItems:
1388                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1389                 else:
1390                     markup = FEED_TEMPLATE % (title, subtitle)
1391         
1392                 try:
1393                     icon_filename = self.listing.getFavicon(key)
1394                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1395                                                    LIST_ICON_SIZE, LIST_ICON_SIZE)
1396                 except:
1397                     pixbuf = default_pixbuf
1398                 
1399                 if showCategories:
1400                     self.feedItems.append(category, (pixbuf, markup, key))
1401                 else:
1402                     self.feedItems.append(None, (pixbuf, markup, key))
1403                     
1404                 
1405         self.restoreExpandedLines()
1406         #try:
1407             
1408         #    self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1409         #except:
1410         #    pass
1411
1412     def on_feedList_row_activated(self, treeview, path, column):
1413         model = treeview.get_model()
1414         iter = model.get_iter(path)
1415         key = model.get_value(iter, COLUMN_KEY)
1416         
1417         try:
1418             #print "Key: " + str(key)
1419             catId = int(key)
1420             self.category = catId
1421             if treeview.row_expanded(path):
1422                 treeview.collapse_row(path)
1423         #else:
1424         #    treeview.expand_row(path, True)
1425             #treeview.collapse_all()
1426             #treeview.expand_row(path, False)
1427             #for i in range(len(path)):
1428             #    self.feedList.expand_row(path[:i+1], False)
1429             #self.show_confirmation_note(self.window, "Working")
1430             #return True
1431         except:
1432             if key:
1433                 self.openFeed(key)
1434             
1435     def openFeed(self, key):
1436         if key != None:
1437             self.build_feed_display()
1438             self.disp.displayFeed(key)
1439                 
1440     def openArticle(self, key, id):
1441         if key != None:
1442             self.openFeed(key)
1443             self.disp.button_clicked(None, id)
1444
1445     def onFeedClosed(self, object, key):
1446         gobject.idle_add(self.displayListing)
1447         
1448     def quit(self, *args):
1449         self.window.hide()
1450         gtk.main_quit ()
1451
1452     def run(self):
1453         self.window.connect("destroy", self.quit)
1454         gtk.main()
1455
1456     def prefsClosed(self, *widget):
1457         try:
1458             self.orientation.set_mode(self.config.getOrientation())
1459         except:
1460             pass
1461         self.displayListing()
1462         self.checkAutoUpdate()
1463
1464     def checkAutoUpdate(self, *widget):
1465         interval = int(self.config.getUpdateInterval()*3600000)
1466         if self.config.isAutoUpdateEnabled():
1467             if self.autoupdate == False:
1468                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1469                 self.autoupdate = interval
1470             elif not self.autoupdate == interval:
1471                 # If auto-update is enabled, but not at the right frequency
1472                 gobject.source_remove(self.autoupdateId)
1473                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1474                 self.autoupdate = interval
1475         else:
1476             if not self.autoupdate == False:
1477                 gobject.source_remove(self.autoupdateId)
1478                 self.autoupdate = False
1479
1480     def automaticUpdate(self, *widget):
1481         # Need to check for internet connection
1482         # If no internet connection, try again in 10 minutes:
1483         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1484         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1485         #from time import localtime, strftime
1486         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1487         #file.close()
1488         self.button_update_clicked(None, None)
1489         return True
1490     
1491     def getStatus(self):
1492         status = ""
1493         for key in self.listing.getListOfFeeds():
1494             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1495                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1496         if status == "":
1497             status = "No unread items"
1498         return status
1499
1500     def grabFocus(self):
1501         self.window.present()
1502
1503 if __name__ == "__main__":
1504     mainthread.init ()
1505     debugging.init(dot_directory=".feedingit", program_name="feedingit")
1506
1507     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1508     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1509     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1510     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1511     gobject.threads_init()
1512     if not isdir(CONFIGDIR):
1513         try:
1514             mkdir(CONFIGDIR)
1515         except:
1516             logger.error("Error: Can't create configuration directory")
1517             from sys import exit
1518             exit(1)
1519     app = FeedingIt()
1520     app.run()