Integrate BeautifulSoup version 3.2.0.
[feedingit] / src / FeedingIt.py
1 #!/usr/bin/env python2.5
2
3
4 # Copyright (c) 2007-2008 INdT.
5 # Copyright (c) 2011 Neal H. Walfield
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 #  This program is distributed in the hope that it will be useful,
12 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #  GNU Lesser General Public License for more details.
15 #
16 #  You should have received a copy of the GNU Lesser General Public License
17 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 # ============================================================================
21 __appname__ = 'FeedingIt'
22 __author__  = 'Yves Marcoz'
23 __version__ = '0.8.0'
24 __description__ = 'A simple RSS Reader for Maemo 5'
25 # ============================================================================
26
27 import gtk
28 from pango import FontDescription
29 import pango
30 import hildon
31 #import gtkhtml2
32 #try:
33 from webkit import WebView
34 #    has_webkit=True
35 #except:
36 #    import gtkhtml2
37 #    has_webkit=False
38 from os.path import isfile, isdir, exists
39 from os import mkdir, remove, stat
40 import gobject
41 from aboutdialog import HeAboutDialog
42 from portrait import FremantleRotation
43 from threading import Thread, activeCount
44 from feedingitdbus import ServerObject
45 from updatedbus import UpdateServerObject, get_lock
46 from config import Config
47 from cgi import escape
48 import weakref
49
50 from rss_sqlite import Listing
51 from opml import GetOpmlData, ExportOpmlData
52
53 import mainthread
54 from jobmanager import JobManager
55
56 from socket import setdefaulttimeout
57 timeout = 5
58 setdefaulttimeout(timeout)
59 del timeout
60
61 import xml.sax
62
63 LIST_ICON_SIZE = 32
64 LIST_ICON_BORDER = 10
65
66 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
67 ABOUT_ICON = 'feedingit'
68 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
69 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
70 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
71 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
72
73 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
74 unread_color = color_style.lookup_color('ActiveTextColor')
75 read_color = color_style.lookup_color('DefaultTextColor')
76 del color_style
77
78 CONFIGDIR="/home/user/.feedingit/"
79 LOCK = CONFIGDIR + "update.lock"
80
81 from re import sub
82 from htmlentitydefs import name2codepoint
83
84 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
85
86 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
87
88 import style
89
90 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
91 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
92 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
93
94 # Build the markup template for the Maemo 5 text style
95 head_font = style.get_font_desc('SystemFont')
96 sub_font = style.get_font_desc('SmallSystemFont')
97
98 #head_color = style.get_color('ButtonTextColor')
99 head_color = style.get_color('DefaultTextColor')
100 sub_color = style.get_color('DefaultTextColor')
101 active_color = style.get_color('ActiveTextColor')
102
103 bg_color = style.get_color('DefaultBackgroundColor').to_string()
104 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
105 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
106 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
107 bg_color = "#" + c1 + c2 + c3
108
109
110 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
111 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
112
113 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
114 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
115
116 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
117 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
118
119 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
120 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
121
122 FEED_TEMPLATE = '\n'.join((head, normal_sub))
123 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
124
125 ENTRY_TEMPLATE = entry_head
126 ENTRY_TEMPLATE_UNREAD = entry_active_head
127
128 ##
129 # Removes HTML or XML character references and entities from a text string.
130 #
131 # @param text The HTML (or XML) source text.
132 # @return The plain text, as a Unicode string, if necessary.
133 # http://effbot.org/zone/re-sub.htm#unescape-html
134 def unescape(text):
135     def fixup(m):
136         text = m.group(0)
137         if text[:2] == "&#":
138             # character reference
139             try:
140                 if text[:3] == "&#x":
141                     return unichr(int(text[3:-1], 16))
142                 else:
143                     return unichr(int(text[2:-1]))
144             except ValueError:
145                 pass
146         else:
147             # named entity
148             try:
149                 text = unichr(name2codepoint[text[1:-1]])
150             except KeyError:
151                 pass
152         return text # leave as is
153     return sub("&#?\w+;", fixup, text)
154
155
156 class AddWidgetWizard(gtk.Dialog):
157     def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
158         gtk.Dialog.__init__(self)
159         self.set_transient_for(parent)
160         
161         #self.category = categories[0]
162         self.category = currentCat
163
164         if isEdit:
165             self.set_title('Edit RSS feed')
166         else:
167             self.set_title('Add new RSS feed')
168
169         if isEdit:
170             self.btn_add = self.add_button('Save', 2)
171         else:
172             self.btn_add = self.add_button('Add', 2)
173
174         self.set_default_response(2)
175
176         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
177         self.nameEntry.set_placeholder('Feed name')
178         # If 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     def createWindow(self):
1012         self.category = 0
1013         
1014         self.app_lock = get_lock("app_lock")
1015         if self.app_lock == None:
1016             try:
1017                 self.stopButton.set_sensitive(True)
1018             except:
1019                 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1020                 self.stopButton.set_text("Stop update","")
1021                 self.stopButton.connect("clicked", self.stop_running_update)
1022                 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1023                 self.window.show_all()
1024             self.introLabel.set_label("Update in progress, please wait.")
1025             gobject.timeout_add_seconds(3, self.createWindow)
1026             return False
1027         try:
1028             self.stopButton.destroy()
1029         except:
1030             pass
1031         self.listing = Listing(self.config, CONFIGDIR)
1032
1033         self.downloadDialog = False
1034         try:
1035             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1036             self.orientation.set_mode(self.config.getOrientation())
1037         except:
1038             print "Could not start rotation manager"
1039         
1040         menu = hildon.AppMenu()
1041         # Create a button and add it to the menu
1042         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1043         button.set_label("Update feeds")
1044         button.connect("clicked", self.button_update_clicked, "All")
1045         menu.append(button)
1046         
1047         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1048         button.set_label("Mark all as read")
1049         button.connect("clicked", self.button_markAll)
1050         menu.append(button)
1051
1052         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1053         button.set_label("Add new feed")
1054         button.connect("clicked", lambda b: self.addFeed())
1055         menu.append(button)
1056
1057         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1058         button.set_label("Manage subscriptions")
1059         button.connect("clicked", self.button_organize_clicked)
1060         menu.append(button)
1061
1062         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1063         button.set_label("Settings")
1064         button.connect("clicked", self.button_preferences_clicked)
1065         menu.append(button)
1066        
1067         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1068         button.set_label("About")
1069         button.connect("clicked", self.button_about_clicked)
1070         menu.append(button)
1071         
1072         self.window.set_app_menu(menu)
1073         menu.show_all()
1074         
1075         #self.feedWindow = hildon.StackableWindow()
1076         #self.articleWindow = hildon.StackableWindow()
1077         self.introLabel.destroy()
1078         self.pannableListing = hildon.PannableArea()
1079         self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1080         self.feedList = gtk.TreeView(self.feedItems)
1081         self.feedList.connect('row-activated', self.on_feedList_row_activated)
1082         #self.feedList.set_enable_tree_lines(True)                                                                                           
1083         #self.feedList.set_show_expanders(True)
1084         self.pannableListing.add(self.feedList)
1085
1086         icon_renderer = gtk.CellRendererPixbuf()
1087         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1088         icon_column = gtk.TreeViewColumn('', icon_renderer, \
1089                 pixbuf=COLUMN_ICON)
1090         self.feedList.append_column(icon_column)
1091
1092         markup_renderer = gtk.CellRendererText()
1093         markup_column = gtk.TreeViewColumn('', markup_renderer, \
1094                 markup=COLUMN_MARKUP)
1095         self.feedList.append_column(markup_column)
1096         self.mainVbox.pack_start(self.pannableListing)
1097         self.mainVbox.show_all()
1098
1099         self.displayListing()
1100         self.autoupdate = False
1101         self.checkAutoUpdate()
1102         
1103         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1104         gobject.idle_add(self.late_init)
1105         
1106     def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
1107         if (not self.downloadDialog
1108             and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
1109             self.updateDbusHandler.UpdateStarted()
1110
1111             self.downloadDialog = DownloadBar(self.window)
1112             self.downloadDialog.connect("download-done", self.onDownloadDone)
1113             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1114             self.mainVbox.show_all()
1115
1116             if self.__dict__.get ('disp', None):
1117                 self.disp.show_download_bar ()
1118
1119     def onDownloadDone(self, widget, feed):
1120         if feed is None:
1121             self.downloadDialog.destroy()
1122             self.downloadDialog = False
1123             self.displayListing()
1124             self.updateDbusHandler.UpdateFinished()
1125             self.updateDbusHandler.ArticleCountUpdated()
1126
1127     def stop_running_update(self, button):
1128         self.stopButton.set_sensitive(False)
1129         import dbus
1130         bus=dbus.SessionBus()
1131         remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1132                                "/org/marcoz/feedingit/update" # Object's path
1133                               )
1134         iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1135         iface.StopUpdate()
1136
1137     def increase_download_parallelism(self):
1138         # The system has been idle for a while.  Enable parallel
1139         # downloads.
1140         JobManager().num_threads = 4
1141         gobject.source_remove (self.increase_download_parallelism_id)
1142         del self.increase_download_parallelism_id
1143         return False
1144
1145     def system_inactivity_ind(self, idle):
1146         # The system's idle state changed.
1147         if (self.am_idle and idle) or (not self.am_idle and not idle):
1148             # No change.
1149             return
1150
1151         if not idle:
1152             if hasattr (self, 'increase_download_parallelism_id'):
1153                 gobject.source_remove (self.increase_download_parallelism_id)
1154                 del self.increase_download_parallelism_id
1155         else:
1156             self.increase_download_parallelism_id = \
1157                 gobject.timeout_add_seconds(
1158                     60, self.increase_download_parallelism)
1159
1160         if not idle:
1161             JobManager().num_threads = 1
1162
1163         self.am_idle = idle
1164
1165     def late_init(self):
1166         self.dbusHandler = ServerObject(self)
1167         self.updateDbusHandler = UpdateServerObject(self)
1168
1169         jm = JobManager()
1170         jm.stats_hook_register (self.job_manager_update,
1171                                 run_in_main_thread=True)
1172         jm.num_threads = 1
1173         self.am_idle = False
1174         JobManager(True)
1175
1176         import dbus
1177         bus = dbus.SystemBus()
1178         proxy = bus.get_object('com.nokia.mce',
1179                                '/com/nokia/mce/signal')
1180         iface = dbus.Interface(proxy, 'com.nokia.mce.signal')
1181         iface.connect_to_signal('system_inactivity_ind',
1182                                 self.system_inactivity_ind)
1183
1184     def button_markAll(self, button):
1185         for key in self.listing.getListOfFeeds():
1186             feed = self.listing.getFeed(key)
1187             feed.markAllAsRead()
1188             #for id in feed.getIds():
1189             #    feed.setEntryRead(id)
1190             self.listing.updateUnread(key)
1191         self.displayListing()
1192
1193     def button_about_clicked(self, button):
1194         HeAboutDialog.present(self.window, \
1195                 __appname__, \
1196                 ABOUT_ICON, \
1197                 __version__, \
1198                 __description__, \
1199                 ABOUT_COPYRIGHT, \
1200                 ABOUT_WEBSITE, \
1201                 ABOUT_BUGTRACKER, \
1202                 ABOUT_DONATE)
1203
1204     def button_export_clicked(self, button):
1205         opml = ExportOpmlData(self.window, self.listing)
1206         
1207     def button_import_clicked(self, button):
1208         opml = GetOpmlData(self.window)
1209         feeds = opml.getData()
1210         for (title, url) in feeds:
1211             self.listing.addFeed(title, url)
1212         self.displayListing()
1213
1214     def addFeed(self, urlIn="http://"):
1215         wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1216         ret = wizard.run()
1217         if ret == 2:
1218             (title, url, category) = wizard.getData()
1219             if url:
1220                self.listing.addFeed(title, url, category=category)
1221         wizard.destroy()
1222         self.displayListing()
1223
1224     def button_organize_clicked(self, button):
1225         def after_closing():
1226             self.displayListing()
1227         SortList(self.window, self.listing, self, after_closing)
1228
1229     def button_update_clicked(self, button, key):
1230         for k in self.listing.getListOfFeeds():
1231             self.listing.updateFeed (k)
1232         #self.displayListing()
1233
1234     def onDownloadsDone(self, *widget):
1235         self.downloadDialog.destroy()
1236         self.downloadDialog = False
1237         self.displayListing()
1238         self.updateDbusHandler.UpdateFinished()
1239         self.updateDbusHandler.ArticleCountUpdated()
1240
1241     def button_preferences_clicked(self, button):
1242         dialog = self.config.createDialog()
1243         dialog.connect("destroy", self.prefsClosed)
1244
1245     def show_confirmation_note(self, parent, title):
1246         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1247
1248         retcode = gtk.Dialog.run(note)
1249         note.destroy()
1250         
1251         if retcode == gtk.RESPONSE_OK:
1252             return True
1253         else:
1254             return False
1255         
1256     def saveExpandedLines(self):
1257        self.expandedLines = []
1258        model = self.feedList.get_model()
1259        model.foreach(self.checkLine)
1260
1261     def checkLine(self, model, path, iter, data = None):
1262        if self.feedList.row_expanded(path):
1263            self.expandedLines.append(path)
1264
1265     def restoreExpandedLines(self):
1266        model = self.feedList.get_model()
1267        model.foreach(self.restoreLine)
1268
1269     def restoreLine(self, model, path, iter, data = None):
1270        if path in self.expandedLines:
1271            self.feedList.expand_row(path, False)
1272         
1273     def displayListing(self):
1274         icon_theme = gtk.icon_theme_get_default()
1275         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1276                 gtk.ICON_LOOKUP_USE_BUILTIN)
1277
1278         self.saveExpandedLines()
1279
1280         self.feedItems.clear()
1281         hideReadFeed = self.config.getHideReadFeeds()
1282         order = self.config.getFeedSortOrder()
1283         
1284         categories = self.listing.getListOfCategories()
1285         if len(categories) > 1:
1286             showCategories = True
1287         else:
1288             showCategories = False
1289         
1290         for categoryId in categories:
1291         
1292             title = self.listing.getCategoryTitle(categoryId)
1293             keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1294             
1295             if showCategories and len(keys)>0:
1296                 category = self.feedItems.append(None, (None, title, categoryId))
1297                 #print "catID" + str(categoryId) + " " + str(self.category)
1298                 if categoryId == self.category:
1299                     #print categoryId
1300                     expandedRow = category
1301     
1302             for key in keys:
1303                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1304                 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1305                 updateTime = self.listing.getFeedUpdateTime(key)
1306                 if updateTime == 0:
1307                     updateTime = "Never"
1308                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1309                 if unreadItems:
1310                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1311                 else:
1312                     markup = FEED_TEMPLATE % (title, subtitle)
1313         
1314                 try:
1315                     icon_filename = self.listing.getFavicon(key)
1316                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1317                                                    LIST_ICON_SIZE, LIST_ICON_SIZE)
1318                 except:
1319                     pixbuf = default_pixbuf
1320                 
1321                 if showCategories:
1322                     self.feedItems.append(category, (pixbuf, markup, key))
1323                 else:
1324                     self.feedItems.append(None, (pixbuf, markup, key))
1325                     
1326                 
1327         self.restoreExpandedLines()
1328         #try:
1329             
1330         #    self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1331         #except:
1332         #    pass
1333
1334     def on_feedList_row_activated(self, treeview, path, column):
1335         model = treeview.get_model()
1336         iter = model.get_iter(path)
1337         key = model.get_value(iter, COLUMN_KEY)
1338         
1339         try:
1340             #print "Key: " + str(key)
1341             catId = int(key)
1342             self.category = catId
1343             if treeview.row_expanded(path):
1344                 treeview.collapse_row(path)
1345         #else:
1346         #    treeview.expand_row(path, True)
1347             #treeview.collapse_all()
1348             #treeview.expand_row(path, False)
1349             #for i in range(len(path)):
1350             #    self.feedList.expand_row(path[:i+1], False)
1351             #self.show_confirmation_note(self.window, "Working")
1352             #return True
1353         except:
1354             if key:
1355                 self.openFeed(key)
1356             
1357     def openFeed(self, key):
1358         try:
1359             self.feed_lock
1360         except:
1361             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1362             if key != None:
1363                 self.feed_lock = get_lock(key)
1364                 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1365                         self.listing.getFeedTitle(key), key, \
1366                         self.config, self.updateDbusHandler)
1367                 self.disp.connect("feed-closed", self.onFeedClosed)
1368                 
1369     def openArticle(self, key, id):
1370         try:
1371             self.feed_lock
1372         except:
1373             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1374             if key != None:
1375                 self.feed_lock = get_lock(key)
1376                 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1377                         self.listing.getFeedTitle(key), key, \
1378                         self.config, self.updateDbusHandler)
1379                 self.disp.button_clicked(None, id)
1380                 self.disp.connect("feed-closed", self.onFeedClosed)
1381         
1382
1383     def onFeedClosed(self, object, key):
1384         #self.listing.saveConfig()
1385         #del self.feed_lock
1386         gobject.idle_add(self.onFeedClosedTimeout)
1387         self.displayListing()
1388         #self.updateDbusHandler.ArticleCountUpdated()
1389         
1390     def onFeedClosedTimeout(self):
1391         del self.feed_lock
1392         self.updateDbusHandler.ArticleCountUpdated()
1393
1394     def quit(self, *args):
1395         self.window.hide()
1396
1397         if hasattr (self, 'app_lock'):
1398             del self.app_lock
1399
1400         # Wait until all slave threads have properly exited before
1401         # terminating the mainloop.
1402         jm = JobManager()
1403         jm.quit ()
1404         stats = jm.stats()
1405         if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1406             gtk.main_quit ()
1407         else:
1408             gobject.timeout_add(500, self.quit)
1409
1410         return False
1411
1412     def run(self):
1413         self.window.connect("destroy", self.quit)
1414         gtk.main()
1415
1416     def prefsClosed(self, *widget):
1417         try:
1418             self.orientation.set_mode(self.config.getOrientation())
1419         except:
1420             pass
1421         self.displayListing()
1422         self.checkAutoUpdate()
1423
1424     def checkAutoUpdate(self, *widget):
1425         interval = int(self.config.getUpdateInterval()*3600000)
1426         if self.config.isAutoUpdateEnabled():
1427             if self.autoupdate == False:
1428                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1429                 self.autoupdate = interval
1430             elif not self.autoupdate == interval:
1431                 # If auto-update is enabled, but not at the right frequency
1432                 gobject.source_remove(self.autoupdateId)
1433                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1434                 self.autoupdate = interval
1435         else:
1436             if not self.autoupdate == False:
1437                 gobject.source_remove(self.autoupdateId)
1438                 self.autoupdate = False
1439
1440     def automaticUpdate(self, *widget):
1441         # Need to check for internet connection
1442         # If no internet connection, try again in 10 minutes:
1443         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1444         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1445         #from time import localtime, strftime
1446         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1447         #file.close()
1448         self.button_update_clicked(None, None)
1449         return True
1450     
1451     def stopUpdate(self):
1452         # Not implemented in the app (see update_feeds.py)
1453         try:
1454             JobManager().cancel ()
1455         except:
1456             pass
1457     
1458     def getStatus(self):
1459         status = ""
1460         for key in self.listing.getListOfFeeds():
1461             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1462                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1463         if status == "":
1464             status = "No unread items"
1465         return status
1466
1467 if __name__ == "__main__":
1468     mainthread.init ()
1469
1470     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1471     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1472     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1473     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1474     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1475     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1476     gobject.threads_init()
1477     if not isdir(CONFIGDIR):
1478         try:
1479             mkdir(CONFIGDIR)
1480         except:
1481             print "Error: Can't create configuration directory"
1482             from sys import exit
1483             exit(1)
1484     app = FeedingIt()
1485     app.run()