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