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