If a new feed's title is '', don't abort: set the title to that in the feed.
[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 late_init(self):
1138         self.dbusHandler = ServerObject(self)
1139         self.updateDbusHandler = UpdateServerObject(self)
1140
1141         jm = JobManager()
1142         jm.stats_hook_register (self.job_manager_update,
1143                                 run_in_main_thread=True)
1144         JobManager(True)
1145
1146     def button_markAll(self, button):
1147         for key in self.listing.getListOfFeeds():
1148             feed = self.listing.getFeed(key)
1149             feed.markAllAsRead()
1150             #for id in feed.getIds():
1151             #    feed.setEntryRead(id)
1152             self.listing.updateUnread(key)
1153         self.displayListing()
1154
1155     def button_about_clicked(self, button):
1156         HeAboutDialog.present(self.window, \
1157                 __appname__, \
1158                 ABOUT_ICON, \
1159                 __version__, \
1160                 __description__, \
1161                 ABOUT_COPYRIGHT, \
1162                 ABOUT_WEBSITE, \
1163                 ABOUT_BUGTRACKER, \
1164                 ABOUT_DONATE)
1165
1166     def button_export_clicked(self, button):
1167         opml = ExportOpmlData(self.window, self.listing)
1168         
1169     def button_import_clicked(self, button):
1170         opml = GetOpmlData(self.window)
1171         feeds = opml.getData()
1172         for (title, url) in feeds:
1173             self.listing.addFeed(title, url)
1174         self.displayListing()
1175
1176     def addFeed(self, urlIn="http://"):
1177         wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1178         ret = wizard.run()
1179         if ret == 2:
1180             (title, url, category) = wizard.getData()
1181             if url:
1182                self.listing.addFeed(title, url, category=category)
1183         wizard.destroy()
1184         self.displayListing()
1185
1186     def button_organize_clicked(self, button):
1187         def after_closing():
1188             self.displayListing()
1189         SortList(self.window, self.listing, self, after_closing)
1190
1191     def button_update_clicked(self, button, key):
1192         for k in self.listing.getListOfFeeds():
1193             self.listing.updateFeed (k)
1194         #self.displayListing()
1195
1196     def onDownloadsDone(self, *widget):
1197         self.downloadDialog.destroy()
1198         self.downloadDialog = False
1199         self.displayListing()
1200         self.updateDbusHandler.UpdateFinished()
1201         self.updateDbusHandler.ArticleCountUpdated()
1202
1203     def button_preferences_clicked(self, button):
1204         dialog = self.config.createDialog()
1205         dialog.connect("destroy", self.prefsClosed)
1206
1207     def show_confirmation_note(self, parent, title):
1208         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1209
1210         retcode = gtk.Dialog.run(note)
1211         note.destroy()
1212         
1213         if retcode == gtk.RESPONSE_OK:
1214             return True
1215         else:
1216             return False
1217         
1218     def saveExpandedLines(self):
1219        self.expandedLines = []
1220        model = self.feedList.get_model()
1221        model.foreach(self.checkLine)
1222
1223     def checkLine(self, model, path, iter, data = None):
1224        if self.feedList.row_expanded(path):
1225            self.expandedLines.append(path)
1226
1227     def restoreExpandedLines(self):
1228        model = self.feedList.get_model()
1229        model.foreach(self.restoreLine)
1230
1231     def restoreLine(self, model, path, iter, data = None):
1232        if path in self.expandedLines:
1233            self.feedList.expand_row(path, False)
1234         
1235     def displayListing(self):
1236         icon_theme = gtk.icon_theme_get_default()
1237         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1238                 gtk.ICON_LOOKUP_USE_BUILTIN)
1239
1240         self.saveExpandedLines()
1241
1242         self.feedItems.clear()
1243         hideReadFeed = self.config.getHideReadFeeds()
1244         order = self.config.getFeedSortOrder()
1245         
1246         categories = self.listing.getListOfCategories()
1247         if len(categories) > 1:
1248             showCategories = True
1249         else:
1250             showCategories = False
1251         
1252         for categoryId in categories:
1253         
1254             title = self.listing.getCategoryTitle(categoryId)
1255             keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1256             
1257             if showCategories and len(keys)>0:
1258                 category = self.feedItems.append(None, (None, title, categoryId))
1259                 #print "catID" + str(categoryId) + " " + str(self.category)
1260                 if categoryId == self.category:
1261                     #print categoryId
1262                     expandedRow = category
1263     
1264             for key in keys:
1265                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1266                 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1267                 updateTime = self.listing.getFeedUpdateTime(key)
1268                 if updateTime == 0:
1269                     updateTime = "Never"
1270                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1271                 if unreadItems:
1272                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1273                 else:
1274                     markup = FEED_TEMPLATE % (title, subtitle)
1275         
1276                 try:
1277                     icon_filename = self.listing.getFavicon(key)
1278                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1279                                                    LIST_ICON_SIZE, LIST_ICON_SIZE)
1280                 except:
1281                     pixbuf = default_pixbuf
1282                 
1283                 if showCategories:
1284                     self.feedItems.append(category, (pixbuf, markup, key))
1285                 else:
1286                     self.feedItems.append(None, (pixbuf, markup, key))
1287                     
1288                 
1289         self.restoreExpandedLines()
1290         #try:
1291             
1292         #    self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1293         #except:
1294         #    pass
1295
1296     def on_feedList_row_activated(self, treeview, path, column):
1297         model = treeview.get_model()
1298         iter = model.get_iter(path)
1299         key = model.get_value(iter, COLUMN_KEY)
1300         
1301         try:
1302             #print "Key: " + str(key)
1303             catId = int(key)
1304             self.category = catId
1305             if treeview.row_expanded(path):
1306                 treeview.collapse_row(path)
1307         #else:
1308         #    treeview.expand_row(path, True)
1309             #treeview.collapse_all()
1310             #treeview.expand_row(path, False)
1311             #for i in range(len(path)):
1312             #    self.feedList.expand_row(path[:i+1], False)
1313             #self.show_confirmation_note(self.window, "Working")
1314             #return True
1315         except:
1316             if key:
1317                 self.openFeed(key)
1318             
1319     def openFeed(self, key):
1320         try:
1321             self.feed_lock
1322         except:
1323             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1324             if key != None:
1325                 self.feed_lock = get_lock(key)
1326                 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1327                         self.listing.getFeedTitle(key), key, \
1328                         self.config, self.updateDbusHandler)
1329                 self.disp.connect("feed-closed", self.onFeedClosed)
1330                 
1331     def openArticle(self, key, id):
1332         try:
1333             self.feed_lock
1334         except:
1335             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1336             if key != None:
1337                 self.feed_lock = get_lock(key)
1338                 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1339                         self.listing.getFeedTitle(key), key, \
1340                         self.config, self.updateDbusHandler)
1341                 self.disp.button_clicked(None, id)
1342                 self.disp.connect("feed-closed", self.onFeedClosed)
1343         
1344
1345     def onFeedClosed(self, object, key):
1346         #self.listing.saveConfig()
1347         #del self.feed_lock
1348         gobject.idle_add(self.onFeedClosedTimeout)
1349         self.displayListing()
1350         #self.updateDbusHandler.ArticleCountUpdated()
1351         
1352     def onFeedClosedTimeout(self):
1353         del self.feed_lock
1354         self.updateDbusHandler.ArticleCountUpdated()
1355
1356     def quit(self, *args):
1357         self.window.hide()
1358
1359         if hasattr (self, 'app_lock'):
1360             del self.app_lock
1361
1362         # Wait until all slave threads have properly exited before
1363         # terminating the mainloop.
1364         jm = JobManager()
1365         jm.quit ()
1366         stats = jm.stats()
1367         if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1368             gtk.main_quit ()
1369         else:
1370             gobject.timeout_add(500, self.quit)
1371
1372         return False
1373
1374     def run(self):
1375         self.window.connect("destroy", self.quit)
1376         gtk.main()
1377
1378     def prefsClosed(self, *widget):
1379         try:
1380             self.orientation.set_mode(self.config.getOrientation())
1381         except:
1382             pass
1383         self.displayListing()
1384         self.checkAutoUpdate()
1385
1386     def checkAutoUpdate(self, *widget):
1387         interval = int(self.config.getUpdateInterval()*3600000)
1388         if self.config.isAutoUpdateEnabled():
1389             if self.autoupdate == False:
1390                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1391                 self.autoupdate = interval
1392             elif not self.autoupdate == interval:
1393                 # If auto-update is enabled, but not at the right frequency
1394                 gobject.source_remove(self.autoupdateId)
1395                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1396                 self.autoupdate = interval
1397         else:
1398             if not self.autoupdate == False:
1399                 gobject.source_remove(self.autoupdateId)
1400                 self.autoupdate = False
1401
1402     def automaticUpdate(self, *widget):
1403         # Need to check for internet connection
1404         # If no internet connection, try again in 10 minutes:
1405         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1406         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1407         #from time import localtime, strftime
1408         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1409         #file.close()
1410         self.button_update_clicked(None, None)
1411         return True
1412     
1413     def stopUpdate(self):
1414         # Not implemented in the app (see update_feeds.py)
1415         try:
1416             JobManager().cancel ()
1417         except:
1418             pass
1419     
1420     def getStatus(self):
1421         status = ""
1422         for key in self.listing.getListOfFeeds():
1423             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1424                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1425         if status == "":
1426             status = "No unread items"
1427         return status
1428
1429 if __name__ == "__main__":
1430     mainthread.init ()
1431
1432     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1433     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1434     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1435     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1436     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1437     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1438     gobject.threads_init()
1439     if not isdir(CONFIGDIR):
1440         try:
1441             mkdir(CONFIGDIR)
1442         except:
1443             print "Error: Can't create configuration directory"
1444             from sys import exit
1445             exit(1)
1446     app = FeedingIt()
1447     app.run()