Changed external page version, and some widget usability modifications
[feedingit] / src / feedingit_widget.py
1 #!/usr/bin/env python2.5
2
3
4 # Copyright (c) 2007-2008 INdT.
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public License
16 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18
19 # ============================================================================
20 # Name        : FeedingIt.py
21 # Author      : Yves Marcoz
22 # Version     : 0.6.0
23 # Description : Simple RSS Reader
24 # ============================================================================
25 #import sys
26
27 import sqlite3
28 from re import sub
29 from htmlentitydefs import name2codepoint
30
31 import gtk, pickle, gobject, dbus
32 import hildondesktop, hildon
33 #from rss import Listing
34
35 # Create a session bus.
36 import dbus
37 from dbus.mainloop.glib import DBusGMainLoop
38 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
39 #bus = dbus.SessionBus()
40
41 from os import environ, remove
42 bus = dbus.bus.BusConnection(environ["DBUS_SESSION_BUS_ADDRESS"])
43 from os.path import isfile
44 from cgi import escape
45
46 settings = gtk.settings_get_default()
47 color_style = gtk.rc_get_style_by_paths( settings, 'GtkButton', 'osso-logical-colors', gtk.Button)
48 active_color = color_style.lookup_color('ActiveTextColor')
49 default_color = color_style.lookup_color('DefaultTextColor')
50 font_desc = gtk.rc_get_style_by_paths(settings, 'HomeSystemFont', None, None).font_desc
51
52 del color_style
53
54 CONFIGDIR="/home/user/.feedingit/"
55 SOURCE=CONFIGDIR + "source"
56
57 #DBusConnection *hd_home_plugin_item_get_dbus_connection ( HDHomePluginItem *item, DBusBusType type, DBusError *error);
58 #import ctypes
59 #libc = ctypes.CDLL('libc.so.6')
60 #libc.printf('Hello world!')
61
62 def get_font_desc(logicalfontname):
63     settings = gtk.settings_get_default()
64     font_style = gtk.rc_get_style_by_paths(settings, logicalfontname, \
65             None, None)
66     font_desc = font_style.font_desc
67     return font_desc
68
69 def unescape(text):
70     def fixup(m):
71         text = m.group(0)
72         if text[:2] == "&#":
73             # character reference
74             try:
75                 if text[:3] == "&#x":
76                     return unichr(int(text[3:-1], 16))
77                 else:
78                     return unichr(int(text[2:-1]))
79             except ValueError:
80                 pass
81         else:
82             # named entity
83             try:
84                 text = unichr(name2codepoint[text[1:-1]])
85             except KeyError:
86                 pass
87         return text # leave as is
88     return sub("&#?\w+;", fixup, text)
89
90 def fix_title(title):
91     return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
92
93
94 class FeedingItHomePlugin(hildondesktop.HomePluginItem):
95     def __init__(self):
96       __gsignals__ = {
97       'destroy' : 'override'
98       }
99
100       try:
101         hildondesktop.HomePluginItem.__init__(self)
102         self.set_settings(True)
103         self.connect("show-settings", self.show_settings)
104         self.feed_list = {}
105         self.total = 0
106         self.status = 0 # 0=Showing feeds, 1=showing articles
107         self.updateStatus = 0 # 0=Not updating, 1=Update in progress
108         self.pageStatus = 0
109         if isfile(SOURCE):
110             file = open(SOURCE)
111             self.autoupdateId = int(file.read())
112             file.close() 
113         else:
114             self.autoupdateId=False
115         
116         vbox = gtk.VBox(False, 0)
117         
118         ## Prepare the main HBox
119         self.hbox1 = gtk.HBox(False, 0)
120         #self.button = gtk.Button()
121         self.buttonApp = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
122         #self.buttonApp.set_text("FeedingIt","")
123         #self.button.set_sensitive(False)
124         #self.label1 = self.buttonApp.child.child.get_children()[0].get_children()[0]
125         #self.label2 = self.button.child.child.get_children()[0].get_children()[1]
126         #self.label1.modify_fg(gtk.STATE_INSENSITIVE, default_color)
127         #self.label1.modify_font(font_desc)
128         #self.label2.modify_fg(gtk.STATE_INSENSITIVE, active_color)
129         icon_theme = gtk.icon_theme_get_default()
130         pixbuf = icon_theme.load_icon("feedingit", 20, gtk.ICON_LOOKUP_USE_BUILTIN )
131         image = gtk.Image()
132         image.set_from_pixbuf(pixbuf)
133         self.buttonApp.set_image(image)
134         self.buttonApp.set_image_position(gtk.POS_RIGHT)
135         #button = gtk.Button("Update")
136         self.buttonApp.connect("clicked", self.button_clicked)
137         
138         self.buttonUpdate = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
139         self.buttonUpdate.set_image(gtk.image_new_from_icon_name('general_refresh', gtk.ICON_SIZE_BUTTON))
140         self.buttonUpdate.connect("clicked", self.buttonUpdate_clicked)
141         
142         self.buttonUp = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
143         self.buttonUp.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
144         self.buttonUp.set_sensitive(False)
145         self.buttonUp.connect("clicked", self.buttonUp_clicked)
146         
147         self.buttonDown = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
148         self.buttonDown.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
149         self.buttonDown.set_sensitive(False)
150         self.buttonDown.connect("clicked", self.buttonDown_clicked)
151         
152         self.hbox1.pack_start(self.buttonUpdate, expand=False)
153         self.hbox1.pack_start(self.buttonUp, expand=False)
154         self.hbox1.pack_start(self.buttonDown, expand=False)
155         self.hbox1.pack_start(self.buttonApp, expand=False)
156         
157         #button.show_all()
158                
159         
160         #for feed in ["Slashdot", "Engadget", "Cheez"]:
161         #    self.treestore.append([feed, "0"])
162         self.treeview = gtk.TreeView()
163         self.update_list()
164         name_renderer = gtk.CellRendererText()
165         name_renderer.set_property("font-desc", font_desc)
166         name_renderer.set_property('background', "#333333")
167         self.unread_renderer = gtk.CellRendererText()
168         self.unread_renderer.set_property("font-desc", font_desc)
169         self.unread_renderer.set_property("xalign", 1.0)
170         self.unread_renderer.set_property('background', "#333333")
171         column_unread = gtk.TreeViewColumn('Unread Items', self.unread_renderer, text = 1)
172         column_unread.set_expand(False)
173         column_name = gtk.TreeViewColumn('Feed Name', name_renderer, text = 0)
174         column_name.set_expand(True)
175         self.treeview.append_column(column_name)
176         self.treeview.append_column(column_unread)
177         #selection = self.treeview.get_selection()
178         #selection.set_mode(gtk.SELECTION_NONE)
179         #self.treeview.get_selection().set_mode(gtk.SELECTION_NONE)
180         #hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_NORMAL)
181         
182         vbox.pack_start(self.treeview)
183         vbox.pack_start(self.hbox1, expand=False) 
184         
185         self.add(vbox)
186         self.treeview.connect("hildon-row-tapped", self.row_activated)
187         #self.treeview.connect("cursor-changed", self.cursor_changed)
188         vbox.show_all()
189         self.setupDbus()
190         #gobject.timeout_add_seconds(30*60, self.update_list)
191       except:
192           import traceback
193           file = open("/home/user/feedingit_widget.log", "a")
194           traceback.print_exc(file=file)
195           file.close()
196           
197     def do_destroy(self):
198         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
199         #file.write("Do_destroy: ")
200         if (not self.autoupdateId==False):
201             gobject.source_remove(self.autoupdateId)
202             self.autoupdateId=False
203             #file.write("Destroyed %s\n" %self.autoupdateId)
204             remove(SOURCE)
205         hildondesktop.HomePluginItem.do_destroy(self)
206         #file.write("End destroy\n")
207         #file.close()
208
209     def button_clicked(self, *widget):
210         #self.button.set_sensitive(False)
211         #self.label1.modify_fg(gtk.STATE_NORMAL, default_color)
212         #self.label2.modify_fg(gtk.STATE_NORMAL, active_color)
213         #self.update_label("Stopping")
214         if self.status == 0:
215             remote_object = bus.get_object("org.maemo.feedingit", # Connection name
216                                    "/org/maemo/feedingit" # Object's path
217                                   )
218             iface = dbus.Interface(remote_object, 'org.maemo.feedingit')
219             iface.OpenFeed(None)
220         else:
221             self.status = 0
222             self.pageStatus = 0
223             self.buttonUp.set_sensitive(False)
224             self.buttonDown.set_sensitive(False)
225             self.treeview.append_column(gtk.TreeViewColumn('Unread Items', self.unread_renderer, text = 1))
226             self.update_list()
227         #iface.StopUpdate()
228         
229     def buttonUpdate_clicked(self, *widget):
230         remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
231                         "/org/marcoz/feedingit/update" # Object's path
232                         )
233         iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
234         if self.updateStatus == 0:
235             iface.UpdateAll()
236         else:
237             iface.StopUpdate()
238             
239     def buttonUp_clicked(self, *widget):
240         if self.pageStatus > 0:
241             self.pageStatus -= 1
242         self.show_articles()
243         
244     def buttonDown_clicked(self, *widget):
245         self.pageStatus += 1
246         self.show_articles()
247         
248     def update_label(self, value=None):
249         if value != None:
250             self.buttonApp.set_title(str(value))
251         else:
252             self.buttonApp.set_title("")
253
254     #def row_activated(self, treeview, treepath): #, column):
255     #    (model, iter) = self.treeview.get_selection().get_selected()
256     #    key = model.get_value(iter, 2)
257         # Create an object that will proxy for a particular remote object.
258     #    remote_object = bus.get_object("org.maemo.feedingit", # Connection name
259     #                           "/org/maemo/feedingit" # Object's path
260     #                          )
261     #    iface = dbus.Interface(remote_object, 'org.maemo.feedingit')
262     #    iface.OpenFeed(key)
263    
264     def show_articles(self):
265         db = sqlite3.connect(CONFIGDIR+self.key+".d/"+self.key+".db")
266         count = db.execute("SELECT count(*) FROM feed WHERE read=0;").fetchone()[0]
267         if count>0:
268             maxPage = count/10
269             if self.pageStatus > maxPage:
270                 self.pageStatus = maxPage
271         rows = db.execute("SELECT id, title FROM feed WHERE read=0 ORDER BY date DESC LIMIT 10 OFFSET ?;", (self.pageStatus*10,) )
272         treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
273         for row in rows:
274             title = fix_title(row[1][0:32])
275             id = row[0]
276             treestore.append((title, id))
277         self.treeview.set_model(treestore)
278     
279     def row_activated(self, treeview, treepath):
280         if self.status == 0:
281             self.status = 1
282             self.pageStatus = 0
283             (model, iter) = self.treeview.get_selection().get_selected()
284             self.key = model.get_value(iter, 2)
285             treeviewcolumn = self.treeview.get_column(1)
286             self.treeview.remove_column(treeviewcolumn)
287             self.show_articles()
288             self.buttonApp.set_image(gtk.image_new_from_icon_name('general_back', gtk.ICON_SIZE_BUTTON))
289             self.buttonUp.set_sensitive(True)
290             self.buttonDown.set_sensitive(True)
291         else:
292             (model, iter) = self.treeview.get_selection().get_selected()
293             id = model.get_value(iter, 1)
294             # Create an object that will proxy for a particular remote object.
295             remote_object = bus.get_object("org.maemo.feedingit", # Connection name
296                                    "/org/maemo/feedingit" # Object's path
297                                   )
298             iface = dbus.Interface(remote_object, 'org.maemo.feedingit')
299             iface.OpenArticle(self.key, id)
300
301
302     def update_list(self, *widget):
303         #listing = Listing(CONFIGDIR)
304         if self.status == 0:
305             treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
306             
307             if self.feed_list == {}:
308                 self.load_config()
309     
310             if self.feed_list == None:
311                 treestore.append(["No feeds added yet", "", None])
312                 treestore.append(["Start Application", "", None])
313                 #self.update_label("No feeds added yet")
314                 self.treeview.set_model(treestore)
315                 
316             else:
317                 list = []
318                 oldtotal = self.total
319                 self.total = 0
320                 #for key in listOfFeeds["feedingit-order"]:
321                 db = sqlite3.connect(CONFIGDIR+"feeds.db")
322                 for key in self.feed_list.keys():
323                     try:
324                         countUnread = db.execute("SELECT unread FROM feeds WHERE id=?;", (key,)).fetchone()[0]
325                         list.append([self.feed_list[key][0:25], countUnread, key])
326                         self.total += countUnread
327                     except:
328                         pass
329                 list = sorted(list, key=lambda item: item[1], reverse=True)
330                 count = 0
331                 for item in list[0:10]:
332                     count += 1
333                     treestore.append(item)
334                 for i in range(count, 10):
335                     treestore.append( ("", "", None) )
336                 self.treeview.set_model(treestore)
337                 self.buttonApp.set_image(gtk.image_new_from_icon_name('feedingit', gtk.ICON_SIZE_BUTTON))
338                 #self.update_label(self.total)
339         return True
340
341     def create_selector(self, choices, setting):
342         #self.pickerDialog = hildon.PickerDialog(self.parent)
343         selector = hildon.TouchSelector(text=True)
344         index = 0
345         for item in choices:
346             iter = selector.append_text(str(item))
347             if str(self.autoupdate) == str(item): 
348                 selector.set_active(0, index)
349             index += 1
350         selector.connect("changed", self.selection_changed, setting)
351         #self.pickerDialog.set_selector(selector)
352         return selector
353         
354     def selection_changed(self, selector, button, setting):
355         tmp = selector.get_current_text()
356         if tmp == "Disabled":
357             self.autoupdate = 0
358         else:
359             self.autoupdate = tmp
360         #current_selection = selector.get_current_text()
361         #if current_selection:
362         #    self.config[setting] = current_selection
363         #gobject.idle_add(self.updateButton, setting)
364         #self.saveConfig()
365         
366     def create_autoupdate_picker(self):
367             picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
368             selector = self.create_selector(["Disabled", 0.5, 1, 2, 4, 12, 24], "autoupdate")
369             picker.set_selector(selector)
370             picker.set_title("Frequency of updates from the widget")
371             picker.set_text("Setup Feed Auto-updates","Update every %s hours" %str(self.autoupdate) )
372             picker.set_name('HildonButton-finger')
373             picker.set_alignment(0,0,1,1)
374             #self.buttons[setting] = picker
375             #vbox.pack_start(picker, expand=False)
376             return picker
377         
378     def show_settings(self, widget):
379         if isfile(CONFIGDIR+"feeds.db"):
380             db = sqlite3.connect(CONFIGDIR+"feeds.db")
381             
382             dialog = gtk.Dialog("Choose feeds to display", None, gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
383     
384             self.pannableArea = hildon.PannableArea()
385             
386             #self.treestore_settings = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
387             self.treeview_settings = gtk.TreeView()
388             
389             self.treeview_settings.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
390             hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview_settings, gtk.HILDON_UI_MODE_EDIT)
391             dialog.vbox.pack_start(self.pannableArea)
392             
393             self.treeview_settings.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
394             self.treestore_settings = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
395             self.treeview_settings.set_model(self.treestore_settings)
396             
397             feeds = db.execute("SELECT title, id FROM feeds;")
398             
399             for feed in feeds:
400                 # feed is (id, title)
401                 item = self.treestore_settings.append(feed)
402                 if feed[1] in self.feed_list:
403                     self.treeview_settings.get_selection().select_iter(item)
404                 
405             self.pannableArea.add(self.treeview_settings)
406             self.pannableArea.show_all()
407             dialog.set_default_size(-1, 600)
408             
409             dialog.action_area.pack_start(self.create_autoupdate_picker())
410             
411             dialog.show_all()
412             response = dialog.run()
413     
414             if response == gtk.RESPONSE_ACCEPT:
415                 self.feed_list = self.getItems()
416             dialog.destroy()
417             self.save_config()
418             self.update_list()
419         else:
420             dialog = gtk.Dialog("Please add feeds first", None, gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
421             label = gtk.Label("Please add feeds through the main application")
422             dialog.vbox.pack_start(label)
423             dialog.show_all()
424             response = dialog.run()
425             dialog.destroy()
426         #self.treeview_settings.get_selection().select_all()
427         
428     def getItems(self):
429         list = {}
430         treeselection = self.treeview_settings.get_selection()
431         (model, pathlist) = treeselection.get_selected_rows()
432         for path in pathlist:
433             list[model.get_value(model.get_iter(path),1)] = model.get_value(model.get_iter(path),0)
434         return list
435         
436     def setupDbus(self):
437         bus.add_signal_receiver(self.update_list, dbus_interface="org.marcoz.feedingit",
438                         signal_name="ArticleCountUpdated", path="/org/marcoz/feedingit/update")
439         bus.add_signal_receiver(self.update_started, dbus_interface="org.marcoz.feedingit",
440                         signal_name="UpdateStarted", path="/org/marcoz/feedingit/update")
441         bus.add_signal_receiver(self.update_finished, dbus_interface="org.marcoz.feedingit",
442                         signal_name="UpdateFinished", path="/org/marcoz/feedingit/update")
443
444     def update_started(self, *widget):
445         self.buttonUpdate.set_image(gtk.image_new_from_icon_name('general_stop', gtk.ICON_SIZE_BUTTON))
446         self.updateStatus = 1
447
448     def update_finished(self, *widget):
449         self.updateStatus = 0
450         self.buttonUpdate.set_image(gtk.image_new_from_icon_name('general_refresh', gtk.ICON_SIZE_BUTTON))
451         
452     def start_update(self):
453         try:
454             if self.autoupdate >0:
455                 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
456                 #from time import localtime, strftime
457                 #import os
458                 #file.write("Widget: pid:%s ppid:%s time:%s\n" % (os.getpid(), os.getppid(), strftime("%a, %d %b %Y %H:%M:%S +0000", localtime())))
459                 #file.close()
460                 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
461                               "/org/marcoz/feedingit/update" # Object's path
462                               )
463                 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
464                 iface.UpdateAll()
465             return True
466         except:
467             import traceback
468             file = open("/home/user/.feedingit/feedingit_widget.log", "a")
469             traceback.print_exc(file=file)
470             file.close()
471
472     def save_config(self):
473             from os.path import isdir
474             if not isdir(CONFIGDIR):
475                 from os import mkdir
476                 mkdir(CONFIGDIR)
477             file = open(CONFIGDIR+"widget", "w")
478             pickle.dump(self.feed_list, file )
479             pickle.dump(self.autoupdate, file)
480             file.close()
481             self.setup_autoupdate()
482
483     def setup_autoupdate(self):
484         if (float(self.autoupdate) > 0):
485             if (not self.autoupdateId==False):
486                 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
487                 #file.write("Disabling %s\n" % self.autoupdateId)
488                 #file.close()
489                 gobject.source_remove(self.autoupdateId)
490                 remove(SOURCE)
491             self.autoupdateId = gobject.timeout_add_seconds(int(float(self.autoupdate)*3600), self.start_update)
492             file = open(SOURCE, "w")
493             file.write(str(self.autoupdateId))
494             file.close()
495             #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
496             #file.write("Started %s\n" % self.autoupdateId)
497             #file.close()
498         else:
499             if (not self.autoupdateId==False):
500                 gobject.source_remove(self.autoupdateId)
501                 self.autoupdateId=False
502                 remove(SOURCE)
503
504     def load_config(self):
505             if isfile(CONFIGDIR+"widget"):
506                 file = open(CONFIGDIR+"widget", "r")
507                 self.feed_list = pickle.load( file )
508                 self.autoupdate = pickle.load( file )
509                 file.close()
510                 self.setup_autoupdate()
511             elif isfile(CONFIGDIR+"feeds.db"):
512                 db = sqlite3.connect(CONFIGDIR+"feeds.db")
513                 feeds = db.execute("SELECT id, title FROM feeds;")
514             
515                 for feed in feeds:
516                     self.feed_list[feed[0]] = feed[1]
517                 self.autoupdate = 0
518             else:
519                 self.feed_list = None
520
521
522 hd_plugin_type = FeedingItHomePlugin
523
524 # The code below is just for testing purposes.
525 # It allows to run the widget as a standalone process.
526 if __name__ == "__main__":
527     import gobject
528     gobject.type_register(hd_plugin_type)
529     obj = gobject.new(hd_plugin_type, plugin_id="plugin_id")
530     obj.show_all()
531     gtk.main()