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