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