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