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