Move download management from frontends to rss_sqlite.py.
[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.8.0'
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
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 updatedbus import UpdateServerObject, get_lock
46 from config import Config
47 from cgi import escape
48 import weakref
49
50 from rss_sqlite import Listing
51 from opml import GetOpmlData, ExportOpmlData
52
53 import mainthread
54 from jobmanager import JobManager
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 ##
129 # Removes HTML or XML character references and entities from a text string.
130 #
131 # @param text The HTML (or XML) source text.
132 # @return The plain text, as a Unicode string, if necessary.
133 # http://effbot.org/zone/re-sub.htm#unescape-html
134 def unescape(text):
135     def fixup(m):
136         text = m.group(0)
137         if text[:2] == "&#":
138             # character reference
139             try:
140                 if text[:3] == "&#x":
141                     return unichr(int(text[3:-1], 16))
142                 else:
143                     return unichr(int(text[2:-1]))
144             except ValueError:
145                 pass
146         else:
147             # named entity
148             try:
149                 text = unichr(name2codepoint[text[1:-1]])
150             except KeyError:
151                 pass
152         return text # leave as is
153     return sub("&#?\w+;", fixup, text)
154
155
156 class AddWidgetWizard(gtk.Dialog):
157     def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
158         gtk.Dialog.__init__(self)
159         self.set_transient_for(parent)
160         
161         #self.category = categories[0]
162         self.category = currentCat
163
164         if isEdit:
165             self.set_title('Edit RSS feed')
166         else:
167             self.set_title('Add new RSS feed')
168
169         if isEdit:
170             self.btn_add = self.add_button('Save', 2)
171         else:
172             self.btn_add = self.add_button('Add', 2)
173
174         self.set_default_response(2)
175
176         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
177         self.nameEntry.set_placeholder('Feed name')
178         if not titleIn == None:
179             self.nameEntry.set_text(titleIn)
180             self.nameEntry.select_region(-1, -1)
181
182         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
183         self.urlEntry.set_placeholder('Feed URL')
184         self.urlEntry.set_text(urlIn)
185         self.urlEntry.select_region(-1, -1)
186         self.urlEntry.set_activates_default(True)
187
188         self.table = gtk.Table(3, 2, False)
189         self.table.set_col_spacings(5)
190         label = gtk.Label('Name:')
191         label.set_alignment(1., .5)
192         self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
193         self.table.attach(self.nameEntry, 1, 2, 0, 1)
194         label = gtk.Label('URL:')
195         label.set_alignment(1., .5)
196         self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
197         self.table.attach(self.urlEntry, 1, 2, 1, 2)
198         selector = self.create_selector(categories, listing)
199         picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
200         picker.set_selector(selector)
201         picker.set_title("Select category")
202         #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
203         picker.set_name('HildonButton-finger')
204         picker.set_alignment(0,0,1,1)
205         
206         self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
207         
208         self.vbox.pack_start(self.table)
209
210         self.show_all()
211
212     def getData(self):
213         return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
214     
215     def create_selector(self, choices, listing):
216         #self.pickerDialog = hildon.PickerDialog(self.parent)
217         selector = hildon.TouchSelector(text=True)
218         index = 0
219         self.map = {}
220         for item in choices:
221             title = listing.getCategoryTitle(item)
222             iter = selector.append_text(str(title))
223             if self.category == item: 
224                 selector.set_active(0, index)
225             self.map[title] = item
226             index += 1
227         selector.connect("changed", self.selection_changed)
228         #self.pickerDialog.set_selector(selector)
229         return selector
230
231     def selection_changed(self, selector, button):
232         current_selection = selector.get_current_text()
233         if current_selection:
234             self.category = self.map[current_selection]
235
236 class AddCategoryWizard(gtk.Dialog):
237     def __init__(self, parent, titleIn=None, isEdit=False):
238         gtk.Dialog.__init__(self)
239         self.set_transient_for(parent)
240
241         if isEdit:
242             self.set_title('Edit Category')
243         else:
244             self.set_title('Add Category')
245
246         if isEdit:
247             self.btn_add = self.add_button('Save', 2)
248         else:
249             self.btn_add = self.add_button('Add', 2)
250
251         self.set_default_response(2)
252
253         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
254         self.nameEntry.set_placeholder('Category name')
255         if not titleIn == None:
256             self.nameEntry.set_text(titleIn)
257             self.nameEntry.select_region(-1, -1)
258
259         self.table = gtk.Table(1, 2, False)
260         self.table.set_col_spacings(5)
261         label = gtk.Label('Name:')
262         label.set_alignment(1., .5)
263         self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
264         self.table.attach(self.nameEntry, 1, 2, 0, 1)
265         #label = gtk.Label('URL:')
266         #label.set_alignment(1., .5)
267         #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
268         #self.table.attach(self.urlEntry, 1, 2, 1, 2)
269         self.vbox.pack_start(self.table)
270
271         self.show_all()
272
273     def getData(self):
274         return self.nameEntry.get_text()
275         
276 class DownloadBar(gtk.ProgressBar):
277     @classmethod
278     def class_init(cls):
279         if hasattr (cls, 'class_init_done'):
280             return
281
282         jm = JobManager ()
283         jm.stats_hook_register (cls.update_progress,
284                                 run_in_main_thread=True)
285
286         cls.downloadbars = []
287         # Total number of jobs we are monitoring.
288         cls.total = 0
289         # Number of jobs complete (of those that we are monitoring).
290         cls.done = 0
291         # Percent complete.
292         cls.progress = 0
293
294         cls.class_init_done = True
295
296     def __init__(self, parent):
297         self.class_init ()
298
299         gtk.ProgressBar.__init__(self)
300
301         self.downloadbars.append(weakref.ref (self))
302         self.set_fraction(0)
303         self.__class__.update_bars()
304         self.show_all()
305
306     @classmethod
307     def downloading(cls):
308         return hasattr (cls, 'jobs_at_start')
309
310     @classmethod
311     def update_progress(cls, jm, old_stats, new_stats, updated_feed):
312         if not cls.downloading():
313             cls.jobs_at_start = old_stats['jobs-completed']
314
315         if not cls.downloadbars:
316             return
317
318         if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
319             del cls.jobs_at_start
320             for ref in cls.downloadbars:
321                 bar = ref ()
322                 if bar is None:
323                     # The download bar disappeared.
324                     cls.downloadbars.remove (ref)
325                 else:
326                     bar.emit("download-done", None)
327             return
328
329         # This should never be called if new_stats['jobs'] is 0, but
330         # just in case...
331         cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
332         cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
333         cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
334                             + new_stats['jobs-queued']) / cls.total
335         cls.update_bars()
336
337         if updated_feed:
338             for ref in cls.downloadbars:
339                 bar = ref ()
340                 if bar is None:
341                     # The download bar disappeared.
342                     cls.downloadbars.remove (ref)
343                 else:
344                     bar.emit("download-done", updated_feed)
345
346     @classmethod
347     def update_bars(cls):
348         # In preparation for i18n/l10n
349         def N_(a, b, n):
350             return (a if n == 1 else b)
351
352         text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
353                    cls.total)
354                 % (cls.done, cls.total))
355
356         for ref in cls.downloadbars:
357             bar = ref ()
358             if bar is None:
359                 # The download bar disappeared.
360                 cls.downloadbars.remove (ref)
361             else:
362                 bar.set_text(text)
363                 bar.set_fraction(cls.progress)
364
365 class SortList(hildon.StackableWindow):
366     def __init__(self, parent, listing, feedingit, after_closing, category=None):
367         hildon.StackableWindow.__init__(self)
368         self.set_transient_for(parent)
369         if category:
370             self.isEditingCategories = False
371             self.category = category
372             self.set_title(listing.getCategoryTitle(category))
373         else:
374             self.isEditingCategories = True
375             self.set_title('Categories')
376         self.listing = listing
377         self.feedingit = feedingit
378         self.after_closing = after_closing
379         if after_closing:
380             self.connect('destroy', lambda w: self.after_closing())
381         self.vbox2 = gtk.VBox(False, 2)
382
383         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
384         button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
385         button.connect("clicked", self.buttonUp)
386         self.vbox2.pack_start(button, expand=False, fill=False)
387
388         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
389         button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
390         button.connect("clicked", self.buttonDown)
391         self.vbox2.pack_start(button, expand=False, fill=False)
392
393         self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
394
395         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
396         button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
397         button.connect("clicked", self.buttonAdd)
398         self.vbox2.pack_start(button, expand=False, fill=False)
399
400         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
401         button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
402         button.connect("clicked", self.buttonEdit)
403         self.vbox2.pack_start(button, expand=False, fill=False)
404
405         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
406         button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
407         button.connect("clicked", self.buttonDelete)
408         self.vbox2.pack_start(button, expand=False, fill=False)
409
410         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
411         #button.set_label("Done")
412         #button.connect("clicked", self.buttonDone)
413         #self.vbox.pack_start(button)
414         self.hbox2= gtk.HBox(False, 10)
415         self.pannableArea = hildon.PannableArea()
416         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
417         self.treeview = gtk.TreeView(self.treestore)
418         self.hbox2.pack_start(self.pannableArea, expand=True)
419         self.displayFeeds()
420         self.hbox2.pack_end(self.vbox2, expand=False)
421         self.set_default_size(-1, 600)
422         self.add(self.hbox2)
423
424         menu = hildon.AppMenu()
425         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
426         button.set_label("Import from OPML")
427         button.connect("clicked", self.feedingit.button_import_clicked)
428         menu.append(button)
429
430         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
431         button.set_label("Export to OPML")
432         button.connect("clicked", self.feedingit.button_export_clicked)
433         menu.append(button)
434         self.set_app_menu(menu)
435         menu.show_all()
436         
437         self.show_all()
438         #self.connect("destroy", self.buttonDone)
439         
440     def displayFeeds(self):
441         self.treeview.destroy()
442         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
443         self.treeview = gtk.TreeView()
444         
445         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
446         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
447         self.refreshList()
448         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
449
450         self.pannableArea.add(self.treeview)
451
452         #self.show_all()
453
454     def refreshList(self, selected=None, offset=0):
455         #rect = self.treeview.get_visible_rect()
456         #y = rect.y+rect.height
457         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
458         if self.isEditingCategories:
459             for key in self.listing.getListOfCategories():
460                 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
461                 if key == selected:
462                     selectedItem = item
463         else:
464             for key in self.listing.getListOfFeeds(category=self.category):
465                 item = self.treestore.append([self.listing.getFeedTitle(key), key])
466                 if key == selected:
467                     selectedItem = item
468         self.treeview.set_model(self.treestore)
469         if not selected == None:
470             self.treeview.get_selection().select_iter(selectedItem)
471             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
472         self.pannableArea.show_all()
473
474     def getSelectedItem(self):
475         (model, iter) = self.treeview.get_selection().get_selected()
476         if not iter:
477             return None
478         return model.get_value(iter, 1)
479
480     def findIndex(self, key):
481         after = None
482         before = None
483         found = False
484         for row in self.treestore:
485             if found:
486                 return (before, row.iter)
487             if key == list(row)[0]:
488                 found = True
489             else:
490                 before = row.iter
491         return (before, None)
492
493     def buttonUp(self, button):
494         key  = self.getSelectedItem()
495         if not key == None:
496             if self.isEditingCategories:
497                 self.listing.moveCategoryUp(key)
498             else:
499                 self.listing.moveUp(key)
500             self.refreshList(key, -10)
501
502     def buttonDown(self, button):
503         key = self.getSelectedItem()
504         if not key == None:
505             if self.isEditingCategories:
506                 self.listing.moveCategoryDown(key)
507             else:
508                 self.listing.moveDown(key)
509             self.refreshList(key, 10)
510
511     def buttonDelete(self, button):
512         key = self.getSelectedItem()
513
514         message = 'Really remove this feed and its entries?'
515         dlg = hildon.hildon_note_new_confirmation(self, message)
516         response = dlg.run()
517         dlg.destroy()
518         if response == gtk.RESPONSE_OK:
519             if self.isEditingCategories:
520                 self.listing.removeCategory(key)
521             else:
522                 self.listing.removeFeed(key)
523             self.refreshList()
524
525     def buttonEdit(self, button):
526         key = self.getSelectedItem()
527
528         if key == 'ArchivedArticles':
529             message = 'Cannot edit the archived articles feed.'
530             hildon.hildon_banner_show_information(self, '', message)
531             return
532         if self.isEditingCategories:
533             if key is not None:
534                 SortList(self.parent, self.listing, self.feedingit, None, category=key)
535         else:
536             if key is not None:
537                 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
538                 ret = wizard.run()
539                 if ret == 2:
540                     (title, url, category) = wizard.getData()
541                     if (not title == '') and (not url == ''):
542                         self.listing.editFeed(key, title, url, category=category)
543                         self.refreshList()
544                 wizard.destroy()
545
546     def buttonDone(self, *args):
547         self.destroy()
548         
549     def buttonAdd(self, button, urlIn="http://"):
550         if self.isEditingCategories:
551             wizard = AddCategoryWizard(self)
552             ret = wizard.run()
553             if ret == 2:
554                 title = wizard.getData()
555                 if (not title == ''): 
556                    self.listing.addCategory(title)
557         else:
558             wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
559             ret = wizard.run()
560             if ret == 2:
561                 (title, url, category) = wizard.getData()
562                 if (not title == '') and (not url == ''): 
563                    self.listing.addFeed(title, url, category=category)
564         wizard.destroy()
565         self.refreshList()
566                
567
568 class DisplayArticle(hildon.StackableWindow):
569     def __init__(self, feed, id, key, config, listing):
570         hildon.StackableWindow.__init__(self)
571         #self.imageDownloader = ImageDownloader()
572         self.feed = feed
573         self.listing=listing
574         self.key = key
575         self.id = id
576         #self.set_title(feed.getTitle(id))
577         self.set_title(self.listing.getFeedTitle(key))
578         self.config = config
579         self.set_for_removal = False
580         
581         # Init the article display
582         #if self.config.getWebkitSupport():
583         self.view = WebView()
584             #self.view.set_editable(False)
585         #else:
586         #    import gtkhtml2
587         #    self.view = gtkhtml2.View()
588         #    self.document = gtkhtml2.Document()
589         #    self.view.set_document(self.document)
590         #    self.document.connect("link_clicked", self._signal_link_clicked)
591         self.pannable_article = hildon.PannableArea()
592         self.pannable_article.add(self.view)
593         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
594         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
595
596         #if self.config.getWebkitSupport():
597         contentLink = self.feed.getContentLink(self.id)
598         self.feed.setEntryRead(self.id)
599         #if key=="ArchivedArticles":
600         self.loadedArticle = False
601         if contentLink.startswith("/home/user/"):
602             self.view.open("file://%s" % contentLink)
603             self.currentUrl = self.feed.getExternalLink(self.id)
604         else:
605             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
606             self.currentUrl = "%s" % contentLink
607         self.view.connect("motion-notify-event", lambda w,ev: True)
608         self.view.connect('load-started', self.load_started)
609         self.view.connect('load-finished', self.load_finished)
610
611         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
612         
613         menu = hildon.AppMenu()
614         # Create a button and add it to the menu
615         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
616         button.set_label("Allow horizontal scrolling")
617         button.connect("clicked", self.horiz_scrolling_button)
618         menu.append(button)
619         
620         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
621         button.set_label("Open in browser")
622         button.connect("clicked", self.open_in_browser)
623         menu.append(button)
624         
625         if key == "ArchivedArticles":
626             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
627             button.set_label("Remove from archived articles")
628             button.connect("clicked", self.remove_archive_button)
629         else:
630             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631             button.set_label("Add to archived articles")
632             button.connect("clicked", self.archive_button)
633         menu.append(button)
634         
635         self.set_app_menu(menu)
636         menu.show_all()
637         
638         self.add(self.pannable_article)
639         
640         self.pannable_article.show_all()
641
642         self.destroyId = self.connect("destroy", self.destroyWindow)
643         
644         #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
645         ## Still using an old version of WebKit, so using navigation-requested signal
646         self.view.connect('navigation-requested', self.navigation_requested)
647         
648         self.view.connect("button_press_event", self.button_pressed)
649         self.gestureId = self.view.connect("button_release_event", self.button_released)
650
651     #def navigation_policy_decision(self, wv, fr, req, action, decision):
652     def navigation_requested(self, wv, fr, req):
653         if self.config.getOpenInExternalBrowser():
654             self.open_in_browser(None, req.get_uri())
655             return True
656         else:
657             return False
658
659     def load_started(self, *widget):
660         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
661         
662     def load_finished(self, *widget):
663         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
664         frame = self.view.get_main_frame()
665         if self.loadedArticle:
666             self.currentUrl = frame.get_uri()
667         else:
668             self.loadedArticle = True
669
670     def button_pressed(self, window, event):
671         #print event.x, event.y
672         self.coords = (event.x, event.y)
673         
674     def button_released(self, window, event):
675         x = self.coords[0] - event.x
676         y = self.coords[1] - event.y
677         
678         if (2*abs(y) < abs(x)):
679             if (x > 15):
680                 self.emit("article-previous", self.id)
681             elif (x<-15):
682                 self.emit("article-next", self.id)   
683
684     def destroyWindow(self, *args):
685         self.disconnect(self.destroyId)
686         if self.set_for_removal:
687             self.emit("article-deleted", self.id)
688         else:
689             self.emit("article-closed", self.id)
690         #self.imageDownloader.stopAll()
691         self.destroy()
692         
693     def horiz_scrolling_button(self, *widget):
694         self.pannable_article.disconnect(self.gestureId)
695         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
696         
697     def archive_button(self, *widget):
698         # Call the listing.addArchivedArticle
699         self.listing.addArchivedArticle(self.key, self.id)
700         
701     def remove_archive_button(self, *widget):
702         self.set_for_removal = True
703
704     def open_in_browser(self, object, link=None):
705         import dbus
706         bus = dbus.SessionBus()
707         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
708         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
709         if link == None:
710             iface.open_new_window(self.currentUrl)
711         else:
712             iface.open_new_window(link)
713
714 class DisplayFeed(hildon.StackableWindow):
715     def __init__(self, listing, feed, title, key, config, updateDbusHandler):
716         hildon.StackableWindow.__init__(self)
717         self.listing = listing
718         self.feed = feed
719         self.feedTitle = title
720         self.set_title(title)
721         self.key=key
722         self.current = list()
723         self.config = config
724         self.updateDbusHandler = updateDbusHandler
725         
726         self.downloadDialog = False
727         
728         #self.listing.setCurrentlyDisplayedFeed(self.key)
729         
730         self.disp = False
731         
732         menu = hildon.AppMenu()
733         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
734         button.set_label("Update feed")
735         button.connect("clicked", self.button_update_clicked)
736         menu.append(button)
737         
738         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
739         button.set_label("Mark all as read")
740         button.connect("clicked", self.buttonReadAllClicked)
741         menu.append(button)
742         
743         if key=="ArchivedArticles":
744             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
745             button.set_label("Delete read articles")
746             button.connect("clicked", self.buttonPurgeArticles)
747             menu.append(button)
748         
749         self.set_app_menu(menu)
750         menu.show_all()
751         
752         self.main_vbox = gtk.VBox(False, 0)
753         self.add(self.main_vbox)
754
755         self.pannableFeed = None
756         self.displayFeed()
757
758         if DownloadBar.downloading ():
759             self.show_download_bar ()
760         
761         self.connect('configure-event', self.on_configure_event)
762         self.connect("destroy", self.destroyWindow)
763
764     def on_configure_event(self, window, event):
765         if getattr(self, 'markup_renderer', None) is None:
766             return
767
768         # Fix up the column width for wrapping the text when the window is
769         # resized (i.e. orientation changed)
770         self.markup_renderer.set_property('wrap-width', event.width-20)  
771         it = self.feedItems.get_iter_first()
772         while it is not None:
773             markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
774             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
775             it = self.feedItems.iter_next(it)
776
777     def destroyWindow(self, *args):
778         #self.feed.saveUnread(CONFIGDIR)
779         self.listing.updateUnread(self.key)
780         self.emit("feed-closed", self.key)
781         self.destroy()
782         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
783         #self.listing.closeCurrentlyDisplayedFeed()
784
785     def fix_title(self, title):
786         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
787
788     def displayFeed(self):
789         if self.pannableFeed:
790             self.pannableFeed.destroy()
791
792         self.pannableFeed = hildon.PannableArea()
793
794         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
795
796         self.feedItems = gtk.ListStore(str, str)
797         #self.feedList = gtk.TreeView(self.feedItems)
798         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
799         self.feedList.set_rules_hint(True)
800
801         selection = self.feedList.get_selection()
802         selection.set_mode(gtk.SELECTION_NONE)
803         #selection.connect("changed", lambda w: True)
804         
805         self.feedList.set_model(self.feedItems)
806         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
807
808         
809         self.feedList.set_hover_selection(False)
810         #self.feedList.set_property('enable-grid-lines', True)
811         #self.feedList.set_property('hildon-mode', 1)
812         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
813         
814         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
815
816         vbox= gtk.VBox(False, 10)
817         vbox.pack_start(self.feedList)
818         
819         self.pannableFeed.add_with_viewport(vbox)
820
821         self.markup_renderer = gtk.CellRendererText()
822         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
823         self.markup_renderer.set_property('background', bg_color) #"#333333")
824         (width, height) = self.get_size()
825         self.markup_renderer.set_property('wrap-width', width-20)
826         self.markup_renderer.set_property('ypad', 8)
827         self.markup_renderer.set_property('xpad', 5)
828         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
829                 markup=FEED_COLUMN_MARKUP)
830         self.feedList.append_column(markup_column)
831
832         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
833         hideReadArticles = self.config.getHideReadArticles()
834         if hideReadArticles:
835             articles = self.feed.getIds(onlyUnread=True)
836         else:
837             articles = self.feed.getIds()
838         
839         hasArticle = False
840         self.current = list()
841         for id in articles:
842             isRead = False
843             try:
844                 isRead = self.feed.isEntryRead(id)
845             except:
846                 pass
847             if not ( isRead and hideReadArticles ):
848                 title = self.fix_title(self.feed.getTitle(id))
849                 self.current.append(id)
850                 if isRead:
851                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
852                 else:
853                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
854     
855                 self.feedItems.append((markup, id))
856                 hasArticle = True
857         if hasArticle:
858             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
859         else:
860             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
861             self.feedItems.append((markup, ""))
862
863         self.main_vbox.pack_start(self.pannableFeed)
864         self.show_all()
865
866     def clear(self):
867         self.pannableFeed.destroy()
868         #self.remove(self.pannableFeed)
869
870     def on_feedList_row_activated(self, treeview, path): #, column):
871         selection = self.feedList.get_selection()
872         selection.set_mode(gtk.SELECTION_SINGLE)
873         self.feedList.get_selection().select_path(path)
874         model = treeview.get_model()
875         iter = model.get_iter(path)
876         key = model.get_value(iter, FEED_COLUMN_KEY)
877         # Emulate legacy "button_clicked" call via treeview
878         gobject.idle_add(self.button_clicked, treeview, key)
879         #return True
880
881     def button_clicked(self, button, index, previous=False, next=False):
882         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
883         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
884         stack = hildon.WindowStack.get_default()
885         if previous:
886             tmp = stack.peek()
887             stack.pop_and_push(1, newDisp, tmp)
888             newDisp.show()
889             gobject.timeout_add(200, self.destroyArticle, tmp)
890             #print "previous"
891             self.disp = newDisp
892         elif next:
893             newDisp.show_all()
894             if type(self.disp).__name__ == "DisplayArticle":
895                 gobject.timeout_add(200, self.destroyArticle, self.disp)
896             self.disp = newDisp
897         else:
898             self.disp = newDisp
899             self.disp.show_all()
900         
901         self.ids = []
902         if self.key == "ArchivedArticles":
903             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
904         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
905         self.ids.append(self.disp.connect("article-next", self.nextArticle))
906         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
907
908     def buttonPurgeArticles(self, *widget):
909         self.clear()
910         self.feed.purgeReadArticles()
911         #self.feed.saveFeed(CONFIGDIR)
912         self.displayFeed()
913
914     def destroyArticle(self, handle):
915         handle.destroyWindow()
916
917     def mark_item_read(self, key):
918         it = self.feedItems.get_iter_first()
919         while it is not None:
920             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
921             if k == key:
922                 title = self.fix_title(self.feed.getTitle(key))
923                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
924                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
925                 break
926             it = self.feedItems.iter_next(it)
927
928     def nextArticle(self, object, index):
929         self.mark_item_read(index)
930         id = self.feed.getNextId(index)
931         while id not in self.current and id != index:
932             id = self.feed.getNextId(id)
933         if id != index:
934             self.button_clicked(object, id, next=True)
935
936     def previousArticle(self, object, index):
937         self.mark_item_read(index)
938         id = self.feed.getPreviousId(index)
939         while id not in self.current and id != index:
940             id = self.feed.getPreviousId(id)
941         if id != index:
942             self.button_clicked(object, id, previous=True)
943
944     def onArticleClosed(self, object, index):
945         selection = self.feedList.get_selection()
946         selection.set_mode(gtk.SELECTION_NONE)
947         self.mark_item_read(index)
948
949     def onArticleDeleted(self, object, index):
950         self.clear()
951         self.feed.removeArticle(index)
952         #self.feed.saveFeed(CONFIGDIR)
953         self.displayFeed()
954
955     def button_update_clicked(self, button):
956         self.listing.updateFeed (self.key, priority=-1)
957             
958     def show_download_bar(self):
959         if not type(self.downloadDialog).__name__=="DownloadBar":
960             self.downloadDialog = DownloadBar(self.window)
961             self.downloadDialog.connect("download-done", self.onDownloadDone)
962             self.main_vbox.pack_end(self.downloadDialog,
963                                     expand=False, fill=False)
964             self.show_all()
965         
966     def onDownloadDone(self, widget, feed):
967         if feed == self.feed or feed is None:
968             self.downloadDialog.destroy()
969             self.downloadDialog = False
970             self.feed = self.listing.getFeed(self.key)
971             self.displayFeed()
972             self.updateDbusHandler.ArticleCountUpdated()
973
974     def buttonReadAllClicked(self, button):
975         #self.clear()
976         self.feed.markAllAsRead()
977         it = self.feedItems.get_iter_first()
978         while it is not None:
979             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
980             title = self.fix_title(self.feed.getTitle(k))
981             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
982             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
983             it = self.feedItems.iter_next(it)
984         #self.displayFeed()
985         #for index in self.feed.getIds():
986         #    self.feed.setEntryRead(index)
987         #    self.mark_item_read(index)
988
989
990 class FeedingIt:
991     def __init__(self):
992         # Init the windows
993         self.window = hildon.StackableWindow()
994         self.window.set_title(__appname__)
995         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
996         self.mainVbox = gtk.VBox(False,10)
997         
998         if isfile(CONFIGDIR+"/feeds.db"):           
999             self.introLabel = gtk.Label("Loading...")
1000         else:
1001             self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1002         
1003         self.mainVbox.pack_start(self.introLabel)
1004
1005         self.window.add(self.mainVbox)
1006         self.window.show_all()
1007         self.config = Config(self.window, CONFIGDIR+"config.ini")
1008         gobject.idle_add(self.createWindow)
1009         
1010     def createWindow(self):
1011         self.category = 0
1012         
1013         self.app_lock = get_lock("app_lock")
1014         if self.app_lock == None:
1015             try:
1016                 self.stopButton.set_sensitive(True)
1017             except:
1018                 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1019                 self.stopButton.set_text("Stop update","")
1020                 self.stopButton.connect("clicked", self.stop_running_update)
1021                 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1022                 self.window.show_all()
1023             self.introLabel.set_label("Update in progress, please wait.")
1024             gobject.timeout_add_seconds(3, self.createWindow)
1025             return False
1026         try:
1027             self.stopButton.destroy()
1028         except:
1029             pass
1030         self.listing = Listing(self.config, CONFIGDIR)
1031
1032         self.downloadDialog = False
1033         try:
1034             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1035             self.orientation.set_mode(self.config.getOrientation())
1036         except:
1037             print "Could not start rotation manager"
1038         
1039         menu = hildon.AppMenu()
1040         # Create a button and add it to the menu
1041         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1042         button.set_label("Update feeds")
1043         button.connect("clicked", self.button_update_clicked, "All")
1044         menu.append(button)
1045         
1046         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1047         button.set_label("Mark all as read")
1048         button.connect("clicked", self.button_markAll)
1049         menu.append(button)
1050
1051         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1052         button.set_label("Add new feed")
1053         button.connect("clicked", lambda b: self.addFeed())
1054         menu.append(button)
1055
1056         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1057         button.set_label("Manage subscriptions")
1058         button.connect("clicked", self.button_organize_clicked)
1059         menu.append(button)
1060
1061         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1062         button.set_label("Settings")
1063         button.connect("clicked", self.button_preferences_clicked)
1064         menu.append(button)
1065        
1066         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1067         button.set_label("About")
1068         button.connect("clicked", self.button_about_clicked)
1069         menu.append(button)
1070         
1071         self.window.set_app_menu(menu)
1072         menu.show_all()
1073         
1074         #self.feedWindow = hildon.StackableWindow()
1075         #self.articleWindow = hildon.StackableWindow()
1076         self.introLabel.destroy()
1077         self.pannableListing = hildon.PannableArea()
1078         self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1079         self.feedList = gtk.TreeView(self.feedItems)
1080         self.feedList.connect('row-activated', self.on_feedList_row_activated)
1081         #self.feedList.set_enable_tree_lines(True)                                                                                           
1082         #self.feedList.set_show_expanders(True)
1083         self.pannableListing.add(self.feedList)
1084
1085         icon_renderer = gtk.CellRendererPixbuf()
1086         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1087         icon_column = gtk.TreeViewColumn('', icon_renderer, \
1088                 pixbuf=COLUMN_ICON)
1089         self.feedList.append_column(icon_column)
1090
1091         markup_renderer = gtk.CellRendererText()
1092         markup_column = gtk.TreeViewColumn('', markup_renderer, \
1093                 markup=COLUMN_MARKUP)
1094         self.feedList.append_column(markup_column)
1095         self.mainVbox.pack_start(self.pannableListing)
1096         self.mainVbox.show_all()
1097
1098         self.displayListing()
1099         self.autoupdate = False
1100         self.checkAutoUpdate()
1101         
1102         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1103         gobject.idle_add(self.late_init)
1104         
1105     def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
1106         if (not self.downloadDialog
1107             and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
1108             self.updateDbusHandler.UpdateStarted()
1109
1110             self.downloadDialog = DownloadBar(self.window)
1111             self.downloadDialog.connect("download-done", self.onDownloadDone)
1112             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1113             self.mainVbox.show_all()
1114
1115             if self.__dict__.get ('disp', None):
1116                 self.disp.show_download_bar ()
1117
1118     def onDownloadDone(self, widget, feed):
1119         if feed is None:
1120             self.downloadDialog.destroy()
1121             self.downloadDialog = False
1122             self.displayListing()
1123             self.updateDbusHandler.UpdateFinished()
1124             self.updateDbusHandler.ArticleCountUpdated()
1125
1126     def stop_running_update(self, button):
1127         self.stopButton.set_sensitive(False)
1128         import dbus
1129         bus=dbus.SessionBus()
1130         remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1131                                "/org/marcoz/feedingit/update" # Object's path
1132                               )
1133         iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1134         iface.StopUpdate()
1135     
1136     def late_init(self):
1137         self.dbusHandler = ServerObject(self)
1138         self.updateDbusHandler = UpdateServerObject(self)
1139
1140         jm = JobManager()
1141         jm.stats_hook_register (self.job_manager_update,
1142                                 run_in_main_thread=True)
1143         JobManager(True)
1144
1145     def button_markAll(self, button):
1146         for key in self.listing.getListOfFeeds():
1147             feed = self.listing.getFeed(key)
1148             feed.markAllAsRead()
1149             #for id in feed.getIds():
1150             #    feed.setEntryRead(id)
1151             self.listing.updateUnread(key)
1152         self.displayListing()
1153
1154     def button_about_clicked(self, button):
1155         HeAboutDialog.present(self.window, \
1156                 __appname__, \
1157                 ABOUT_ICON, \
1158                 __version__, \
1159                 __description__, \
1160                 ABOUT_COPYRIGHT, \
1161                 ABOUT_WEBSITE, \
1162                 ABOUT_BUGTRACKER, \
1163                 ABOUT_DONATE)
1164
1165     def button_export_clicked(self, button):
1166         opml = ExportOpmlData(self.window, self.listing)
1167         
1168     def button_import_clicked(self, button):
1169         opml = GetOpmlData(self.window)
1170         feeds = opml.getData()
1171         for (title, url) in feeds:
1172             self.listing.addFeed(title, url)
1173         self.displayListing()
1174
1175     def addFeed(self, urlIn="http://"):
1176         wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1177         ret = wizard.run()
1178         if ret == 2:
1179             (title, url, category) = wizard.getData()
1180             if (not title == '') and (not url == ''): 
1181                self.listing.addFeed(title, url, category=category)
1182         wizard.destroy()
1183         self.displayListing()
1184
1185     def button_organize_clicked(self, button):
1186         def after_closing():
1187             self.displayListing()
1188         SortList(self.window, self.listing, self, after_closing)
1189
1190     def button_update_clicked(self, button, key):
1191         for k in self.listing.getListOfFeeds():
1192             self.listing.updateFeed (k)
1193         #self.displayListing()
1194
1195     def onDownloadsDone(self, *widget):
1196         self.downloadDialog.destroy()
1197         self.downloadDialog = False
1198         self.displayListing()
1199         self.updateDbusHandler.UpdateFinished()
1200         self.updateDbusHandler.ArticleCountUpdated()
1201
1202     def button_preferences_clicked(self, button):
1203         dialog = self.config.createDialog()
1204         dialog.connect("destroy", self.prefsClosed)
1205
1206     def show_confirmation_note(self, parent, title):
1207         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1208
1209         retcode = gtk.Dialog.run(note)
1210         note.destroy()
1211         
1212         if retcode == gtk.RESPONSE_OK:
1213             return True
1214         else:
1215             return False
1216         
1217     def saveExpandedLines(self):
1218        self.expandedLines = []
1219        model = self.feedList.get_model()
1220        model.foreach(self.checkLine)
1221
1222     def checkLine(self, model, path, iter, data = None):
1223        if self.feedList.row_expanded(path):
1224            self.expandedLines.append(path)
1225
1226     def restoreExpandedLines(self):
1227        model = self.feedList.get_model()
1228        model.foreach(self.restoreLine)
1229
1230     def restoreLine(self, model, path, iter, data = None):
1231        if path in self.expandedLines:
1232            self.feedList.expand_row(path, False)
1233         
1234     def displayListing(self):
1235         icon_theme = gtk.icon_theme_get_default()
1236         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1237                 gtk.ICON_LOOKUP_USE_BUILTIN)
1238
1239         self.saveExpandedLines()
1240
1241         self.feedItems.clear()
1242         hideReadFeed = self.config.getHideReadFeeds()
1243         order = self.config.getFeedSortOrder()
1244         
1245         categories = self.listing.getListOfCategories()
1246         if len(categories) > 1:
1247             showCategories = True
1248         else:
1249             showCategories = False
1250         
1251         for categoryId in categories:
1252         
1253             title = self.listing.getCategoryTitle(categoryId)
1254             keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1255             
1256             if showCategories and len(keys)>0:
1257                 category = self.feedItems.append(None, (None, title, categoryId))
1258                 #print "catID" + str(categoryId) + " " + str(self.category)
1259                 if categoryId == self.category:
1260                     #print categoryId
1261                     expandedRow = category
1262     
1263             for key in keys:
1264                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1265                 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1266                 updateTime = self.listing.getFeedUpdateTime(key)
1267                 if updateTime == 0:
1268                     updateTime = "Never"
1269                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1270                 if unreadItems:
1271                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1272                 else:
1273                     markup = FEED_TEMPLATE % (title, subtitle)
1274         
1275                 try:
1276                     icon_filename = self.listing.getFavicon(key)
1277                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1278                                                    LIST_ICON_SIZE, LIST_ICON_SIZE)
1279                 except:
1280                     pixbuf = default_pixbuf
1281                 
1282                 if showCategories:
1283                     self.feedItems.append(category, (pixbuf, markup, key))
1284                 else:
1285                     self.feedItems.append(None, (pixbuf, markup, key))
1286                     
1287                 
1288         self.restoreExpandedLines()
1289         #try:
1290             
1291         #    self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1292         #except:
1293         #    pass
1294
1295     def on_feedList_row_activated(self, treeview, path, column):
1296         model = treeview.get_model()
1297         iter = model.get_iter(path)
1298         key = model.get_value(iter, COLUMN_KEY)
1299         
1300         try:
1301             #print "Key: " + str(key)
1302             catId = int(key)
1303             self.category = catId
1304             if treeview.row_expanded(path):
1305                 treeview.collapse_row(path)
1306         #else:
1307         #    treeview.expand_row(path, True)
1308             #treeview.collapse_all()
1309             #treeview.expand_row(path, False)
1310             #for i in range(len(path)):
1311             #    self.feedList.expand_row(path[:i+1], False)
1312             #self.show_confirmation_note(self.window, "Working")
1313             #return True
1314         except:
1315             if key:
1316                 self.openFeed(key)
1317             
1318     def openFeed(self, key):
1319         try:
1320             self.feed_lock
1321         except:
1322             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1323             if key != None:
1324                 self.feed_lock = get_lock(key)
1325                 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1326                         self.listing.getFeedTitle(key), key, \
1327                         self.config, self.updateDbusHandler)
1328                 self.disp.connect("feed-closed", self.onFeedClosed)
1329                 
1330     def openArticle(self, key, id):
1331         try:
1332             self.feed_lock
1333         except:
1334             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1335             if key != None:
1336                 self.feed_lock = get_lock(key)
1337                 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1338                         self.listing.getFeedTitle(key), key, \
1339                         self.config, self.updateDbusHandler)
1340                 self.disp.button_clicked(None, id)
1341                 self.disp.connect("feed-closed", self.onFeedClosed)
1342         
1343
1344     def onFeedClosed(self, object, key):
1345         #self.listing.saveConfig()
1346         #del self.feed_lock
1347         gobject.idle_add(self.onFeedClosedTimeout)
1348         self.displayListing()
1349         #self.updateDbusHandler.ArticleCountUpdated()
1350         
1351     def onFeedClosedTimeout(self):
1352         del self.feed_lock
1353         self.updateDbusHandler.ArticleCountUpdated()
1354
1355     def quit(self, *args):
1356         self.window.hide()
1357
1358         if hasattr (self, 'app_lock'):
1359             del self.app_lock
1360
1361         # Wait until all slave threads have properly exited before
1362         # terminating the mainloop.
1363         jm = JobManager()
1364         jm.quit ()
1365         stats = jm.stats()
1366         if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1367             gtk.main_quit ()
1368         else:
1369             gobject.timeout_add(500, self.quit)
1370
1371         return False
1372
1373     def run(self):
1374         self.window.connect("destroy", self.quit)
1375         gtk.main()
1376
1377     def prefsClosed(self, *widget):
1378         try:
1379             self.orientation.set_mode(self.config.getOrientation())
1380         except:
1381             pass
1382         self.displayListing()
1383         self.checkAutoUpdate()
1384
1385     def checkAutoUpdate(self, *widget):
1386         interval = int(self.config.getUpdateInterval()*3600000)
1387         if self.config.isAutoUpdateEnabled():
1388             if self.autoupdate == False:
1389                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1390                 self.autoupdate = interval
1391             elif not self.autoupdate == interval:
1392                 # If auto-update is enabled, but not at the right frequency
1393                 gobject.source_remove(self.autoupdateId)
1394                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1395                 self.autoupdate = interval
1396         else:
1397             if not self.autoupdate == False:
1398                 gobject.source_remove(self.autoupdateId)
1399                 self.autoupdate = False
1400
1401     def automaticUpdate(self, *widget):
1402         # Need to check for internet connection
1403         # If no internet connection, try again in 10 minutes:
1404         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1405         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1406         #from time import localtime, strftime
1407         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1408         #file.close()
1409         self.button_update_clicked(None, None)
1410         return True
1411     
1412     def stopUpdate(self):
1413         # Not implemented in the app (see update_feeds.py)
1414         try:
1415             JobManager().cancel ()
1416         except:
1417             pass
1418     
1419     def getStatus(self):
1420         status = ""
1421         for key in self.listing.getListOfFeeds():
1422             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1423                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1424         if status == "":
1425             status = "No unread items"
1426         return status
1427
1428 if __name__ == "__main__":
1429     mainthread.init ()
1430
1431     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1432     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1433     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1434     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1435     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1436     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1437     gobject.threads_init()
1438     if not isdir(CONFIGDIR):
1439         try:
1440             mkdir(CONFIGDIR)
1441         except:
1442             print "Error: Can't create configuration directory"
1443             from sys import exit
1444             exit(1)
1445     app = FeedingIt()
1446     app.run()