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