1 #!/usr/bin/env python2.5
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.
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.
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/>.
19 # ============================================================================
21 # Author : Yves Marcoz
23 # Description : Simple RSS Reader
24 # ============================================================================
29 from htmlentitydefs import name2codepoint
31 import gtk, pickle, gobject, dbus
32 import hildondesktop, hildon
33 from pango import ELLIPSIZE_END
34 #from rss import Listing
36 # Create a session bus.
38 from dbus.mainloop.glib import DBusGMainLoop
39 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
40 #bus = dbus.SessionBus()
42 from os import environ, remove
43 bus = dbus.bus.BusConnection(environ["DBUS_SESSION_BUS_ADDRESS"])
44 from os.path import isfile
45 from cgi import escape
47 settings = gtk.settings_get_default()
48 color_style = gtk.rc_get_style_by_paths( settings, 'GtkButton', 'osso-logical-colors', gtk.Button)
49 active_color = color_style.lookup_color('ActiveTextColor')
50 default_color = color_style.lookup_color('DefaultTextColor')
51 font_desc = gtk.rc_get_style_by_paths(settings, 'HomeSystemFont', None, None).font_desc
52 bg_color = color_style.lookup_color('DefaultBackgroundColor').to_string()
53 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
54 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
55 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
56 bg_color = "#" + c1 + c2 + c3
58 del color_style, c1, c2, c3, settings
60 CONFIGDIR="/home/user/.feedingit/"
61 SOURCE=CONFIGDIR + "source"
63 #DBusConnection *hd_home_plugin_item_get_dbus_connection ( HDHomePluginItem *item, DBusBusType type, DBusError *error);
65 #libc = ctypes.CDLL('libc.so.6')
66 #libc.printf('Hello world!')
68 def get_font_desc(logicalfontname):
69 settings = gtk.settings_get_default()
70 font_style = gtk.rc_get_style_by_paths(settings, logicalfontname, \
72 font_desc = font_style.font_desc
82 return unichr(int(text[3:-1], 16))
84 return unichr(int(text[2:-1]))
90 text = unichr(name2codepoint[text[1:-1]])
93 return text # leave as is
94 return sub("&#?\w+;", fixup, text)
97 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
100 class FeedingItHomePlugin(hildondesktop.HomePluginItem):
103 'destroy' : 'override'
107 hildondesktop.HomePluginItem.__init__(self)
108 self.set_settings(True)
109 self.connect("show-settings", self.show_settings)
110 self.initialized = False
112 self.status = 0 # 0=Showing feeds, 1=showing articles
113 self.updateStatus = 0 # 0=Not updating, 1=Update in progress
116 self.size = 1 # 1=Big widget, 0=small widget
120 self.autoupdateId = int(file.read())
123 self.autoupdateId=False
126 vbox = gtk.VBox(False, 0)
128 ## Prepare the main HBox
129 self.hbox1 = gtk.HBox(False, 0)
130 #self.button = gtk.Button()
131 self.buttonApp = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
132 #self.buttonApp.set_text("FeedingIt","")
133 #self.button.set_sensitive(False)
134 #self.label1 = self.buttonApp.child.child.get_children()[0].get_children()[0]
135 #self.label2 = self.button.child.child.get_children()[0].get_children()[1]
136 #self.label1.modify_fg(gtk.STATE_INSENSITIVE, default_color)
137 #self.label1.modify_font(font_desc)
138 #self.label2.modify_fg(gtk.STATE_INSENSITIVE, active_color)
139 icon_theme = gtk.icon_theme_get_default()
140 pixbuf = icon_theme.load_icon("feedingit", 20, gtk.ICON_LOOKUP_USE_BUILTIN )
142 image.set_from_pixbuf(pixbuf)
143 self.buttonApp.set_image(image)
144 self.buttonApp.set_image_position(gtk.POS_RIGHT)
145 #button = gtk.Button("Update")
146 self.buttonApp.connect("clicked", self.button_clicked)
148 self.buttonUpdate = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
149 self.buttonUpdate.set_image(gtk.image_new_from_icon_name('general_refresh', gtk.ICON_SIZE_BUTTON))
150 self.buttonUpdate.connect("clicked", self.buttonUpdate_clicked)
153 self.buttonUp = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
154 self.buttonUp.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
155 self.buttonUp.set_sensitive(False)
156 self.buttonUp.connect("clicked", self.buttonUp_clicked)
158 self.buttonDown = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
159 self.buttonDown.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
160 self.buttonDown.set_sensitive(False)
161 self.buttonDown.connect("clicked", self.buttonDown_clicked)
163 self.hbox1.pack_start(self.buttonUpdate, expand=False)
165 self.hbox1.pack_start(self.buttonUp, expand=False)
166 self.hbox1.pack_start(self.buttonDown, expand=False)
167 self.hbox1.pack_start(self.buttonApp, expand=False)
172 #for feed in ["Slashdot", "Engadget", "Cheez"]:
173 # self.treestore.append([feed, "0"])
174 self.treeview = gtk.TreeView()
176 name_renderer = gtk.CellRendererText()
177 name_renderer.set_property("font-desc", font_desc)
178 name_renderer.set_property('background', bg_color) #"#333333")
179 name_renderer.set_property('ellipsize', ELLIPSIZE_END)
180 self.unread_renderer = gtk.CellRendererText()
181 self.unread_renderer.set_property("font-desc", font_desc)
182 self.unread_renderer.set_property("xalign", 1.0)
183 self.unread_renderer.set_property('background', bg_color) # "#333333")
184 column_unread = gtk.TreeViewColumn('Unread Items', self.unread_renderer, text = 1)
185 column_unread.set_expand(False)
186 column_name = gtk.TreeViewColumn('Feed Name', name_renderer, markup = 0)
187 column_name.set_expand(True)
188 self.treeview.append_column(column_name)
189 self.treeview.append_column(column_unread)
190 #selection = self.treeview.get_selection()
191 #selection.set_mode(gtk.SELECTION_NONE)
192 #self.treeview.get_selection().set_mode(gtk.SELECTION_NONE)
193 #hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_NORMAL)
195 vbox.pack_start(self.treeview)
196 vbox.pack_start(self.hbox1, expand=False)
199 self.treeview.connect("hildon-row-tapped", self.row_activated)
200 #self.treeview.connect("cursor-changed", self.cursor_changed)
202 self.set_resize_mode(gtk.RESIZE_QUEUE)
204 #gobject.timeout_add_seconds(30*60, self.update_list)
207 file = open("/home/user/feedingit_widget.log", "a")
208 traceback.print_exc(file=file)
211 def do_destroy(self):
212 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
213 #file.write("Do_destroy: ")
214 if (not self.autoupdateId==False):
215 gobject.source_remove(self.autoupdateId)
216 self.autoupdateId=False
217 #file.write("Destroyed %s\n" %self.autoupdateId)
219 hildondesktop.HomePluginItem.do_destroy(self)
220 #file.write("End destroy\n")
223 def button_clicked(self, *widget):
224 #self.button.set_sensitive(False)
225 #self.label1.modify_fg(gtk.STATE_NORMAL, default_color)
226 #self.label2.modify_fg(gtk.STATE_NORMAL, active_color)
227 #self.update_label("Stopping")
229 remote_object = bus.get_object("org.maemo.feedingit", # Connection name
230 "/org/maemo/feedingit" # Object's path
232 iface = dbus.Interface(remote_object, 'org.maemo.feedingit')
238 self.buttonUp.set_sensitive(False)
239 self.buttonDown.set_sensitive(False)
240 self.treeview.append_column(gtk.TreeViewColumn('Unread Items', self.unread_renderer, text = 1))
244 def buttonUpdate_clicked(self, *widget):
245 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
246 "/org/marcoz/feedingit/update" # Object's path
248 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
249 if self.updateStatus == 0:
254 def buttonUp_clicked(self, *widget):
255 if self.pageStatus > 0:
259 def buttonDown_clicked(self, *widget):
263 def update_label(self, value=None):
265 self.buttonApp.set_title(str(value))
267 self.buttonApp.set_title("")
269 #def row_activated(self, treeview, treepath): #, column):
270 # (model, iter) = self.treeview.get_selection().get_selected()
271 # key = model.get_value(iter, 2)
272 # Create an object that will proxy for a particular remote object.
273 # remote_object = bus.get_object("org.maemo.feedingit", # Connection name
274 # "/org/maemo/feedingit" # Object's path
276 # iface = dbus.Interface(remote_object, 'org.maemo.feedingit')
277 # iface.OpenFeed(key)
279 def show_articles(self):
280 db = sqlite3.connect(CONFIGDIR+self.key+".d/"+self.key+".db")
281 count = db.execute("SELECT count(*) FROM feed WHERE read=0;").fetchone()[0]
283 maxPage = count/self.pageSize
285 if self.pageStatus > maxPage:
286 self.pageStatus = maxPage
287 print self.pageStatus
288 rows = db.execute("SELECT id, title FROM feed WHERE read=0 ORDER BY date DESC LIMIT ? OFFSET ?;", (self.pageSize, self.pageStatus*self.pageSize,) )
289 treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
291 #title = fix_title(row[1][0:32])
292 title = fix_title(row[1])
294 treestore.append((title, id))
295 self.treeview.set_model(treestore)
297 def row_activated(self, treeview, treepath):
302 (model, iter) = self.treeview.get_selection().get_selected()
303 self.key = model.get_value(iter, 2)
304 treeviewcolumn = self.treeview.get_column(1)
305 self.treeview.remove_column(treeviewcolumn)
307 self.buttonApp.set_image(gtk.image_new_from_icon_name('general_back', gtk.ICON_SIZE_BUTTON))
308 self.buttonUp.set_sensitive(True)
309 self.buttonDown.set_sensitive(True)
311 (model, iter) = self.treeview.get_selection().get_selected()
312 id = model.get_value(iter, 2)
313 remote_object = bus.get_object("org.maemo.feedingit", # Connection name
314 "/org/maemo/feedingit" # Object's path
316 iface = dbus.Interface(remote_object, 'org.maemo.feedingit')
319 (model, iter) = self.treeview.get_selection().get_selected()
320 id = model.get_value(iter, 1)
321 # Create an object that will proxy for a particular remote object.
322 remote_object = bus.get_object("org.maemo.feedingit", # Connection name
323 "/org/maemo/feedingit" # Object's path
325 iface = dbus.Interface(remote_object, 'org.maemo.feedingit')
326 iface.OpenArticle(self.key, id)
328 def check_db(self, db):
329 table = db.execute("SELECT sql FROM sqlite_master").fetchone()
330 from string import find
331 if find(table[0], "widget")==-1:
332 db.execute("ALTER TABLE feeds ADD COLUMN widget int;")
333 db.execute("UPDATE feeds SET widget=1;")
336 def no_feeds_available(self, treestore):
337 treestore.append(["No feeds added yet", "", None])
338 treestore.append(["Start Application", "", None])
339 #self.update_label("No feeds added yet")
340 self.treeview.set_model(treestore)
342 def update_list(self, *widget):
343 #listing = Listing(CONFIGDIR)
346 treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
348 #if self.feed_list == {}:
349 if self.initialized == False:
352 db = sqlite3.connect(CONFIGDIR+"feeds.db")
354 self.no_feeds_available(treestore)
357 rows = db.execute("SELECT id, title, unread FROM feeds WHERE widget=1 ORDER BY unread DESC LIMIT 10;")
360 rows = db.execute("SELECT id, title, unread FROM feeds WHERE widget=1 ORDER BY unread DESC LIMIT 10;")
367 treestore.append([name, countUnread, id])
368 self.treeview.set_model(treestore)
369 self.buttonApp.set_image(gtk.image_new_from_icon_name('feedingit', gtk.ICON_SIZE_BUTTON))
373 file = open("/home/user/feedingit_widget.log", "a")
374 traceback.print_exc(file=file)
377 def create_selector(self, choices, setting):
378 #self.pickerDialog = hildon.PickerDialog(self.parent)
379 selector = hildon.TouchSelector(text=True)
382 iter = selector.append_text(str(item))
383 if str(self.autoupdate) == str(item):
384 selector.set_active(0, index)
386 selector.connect("changed", self.selection_changed, setting)
387 #self.pickerDialog.set_selector(selector)
390 def selection_changed(self, selector, button, setting):
391 tmp = selector.get_current_text()
392 if setting == "autoupdate":
393 if tmp == "Disabled":
396 self.autoupdate = tmp
397 elif setting == "sizing":
402 #current_selection = selector.get_current_text()
403 #if current_selection:
404 # self.config[setting] = current_selection
405 #gobject.idle_add(self.updateButton, setting)
408 def create_autoupdate_picker(self):
409 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
410 selector = self.create_selector(["Disabled", 0.5, 1, 2, 4, 12, 24], "autoupdate")
411 picker.set_selector(selector)
412 picker.set_title("Frequency of updates from the widget")
413 picker.set_text("Setup Feed Auto-updates","Update every %s hours" %str(self.autoupdate) )
414 picker.set_name('HildonButton-finger')
415 picker.set_alignment(0,0,1,1)
416 #self.buttons[setting] = picker
417 #vbox.pack_start(picker, expand=False)
420 def create_size_picker(self):
421 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
422 selector = self.create_selector(["Small", "Large"], "sizing")
423 picker.set_selector(selector)
424 picker.set_title("Default Size For The Widget")
429 picker.set_text("Widget Size (requires restart)", tmp)
430 picker.set_name('HildonButton-finger')
431 picker.set_alignment(0,0,1,1)
432 #self.buttons[setting] = picker
433 #vbox.pack_start(picker, expand=False)
436 def show_settings(self, widget):
437 if isfile(CONFIGDIR+"feeds.db"):
438 db = sqlite3.connect(CONFIGDIR+"feeds.db")
440 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))
442 self.pannableArea = hildon.PannableArea()
444 #self.treestore_settings = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
445 self.treeview_settings = gtk.TreeView()
447 self.treeview_settings.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
448 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview_settings, gtk.HILDON_UI_MODE_EDIT)
449 dialog.vbox.pack_start(self.pannableArea)
451 self.treeview_settings.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
452 self.treestore_settings = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
453 self.treeview_settings.set_model(self.treestore_settings)
455 feeds = db.execute("SELECT title, id, widget FROM feeds;")
458 # feed is (id, title)
459 item = self.treestore_settings.append(feed[0:2])
461 self.treeview_settings.get_selection().select_iter(item)
463 self.pannableArea.add(self.treeview_settings)
464 self.pannableArea.show_all()
465 dialog.set_default_size(-1, 600)
467 dialog.action_area.pack_start(self.create_autoupdate_picker())
468 dialog.action_area.pack_start(self.create_size_picker())
471 response = dialog.run()
473 if response == gtk.RESPONSE_ACCEPT:
479 dialog = gtk.Dialog("Please add feeds first", None, gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
480 label = gtk.Label("Please add feeds through the main application")
481 dialog.vbox.pack_start(label)
483 response = dialog.run()
485 #self.treeview_settings.get_selection().select_all()
489 treeselection = self.treeview_settings.get_selection()
490 (model, pathlist) = treeselection.get_selected_rows()
491 db = sqlite3.connect(CONFIGDIR+"feeds.db")
492 db.execute("UPDATE feeds SET widget=0;")
493 for path in pathlist:
494 id = model.get_value(model.get_iter(path),1)
495 db.execute("UPDATE feeds SET widget=1 WHERE id=?;", (id,) )
497 #title = model.get_value(model.get_iter(path),0)
498 #list[model.get_value(model.get_iter(path),1)] = model.get_value(model.get_iter(path),0)
502 bus.add_signal_receiver(self.update_list, dbus_interface="org.marcoz.feedingit",
503 signal_name="ArticleCountUpdated", path="/org/marcoz/feedingit/update")
504 bus.add_signal_receiver(self.update_started, dbus_interface="org.marcoz.feedingit",
505 signal_name="UpdateStarted", path="/org/marcoz/feedingit/update")
506 bus.add_signal_receiver(self.update_finished, dbus_interface="org.marcoz.feedingit",
507 signal_name="UpdateFinished", path="/org/marcoz/feedingit/update")
509 def update_started(self, *widget):
510 self.buttonUpdate.set_image(gtk.image_new_from_icon_name('general_stop', gtk.ICON_SIZE_BUTTON))
511 self.updateStatus = 1
513 def update_finished(self, *widget):
514 self.updateStatus = 0
515 self.buttonUpdate.set_image(gtk.image_new_from_icon_name('general_refresh', gtk.ICON_SIZE_BUTTON))
517 def start_update(self):
519 if self.autoupdate >0:
520 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
521 #from time import localtime, strftime
523 #file.write("Widget: pid:%s ppid:%s time:%s\n" % (os.getpid(), os.getppid(), strftime("%a, %d %b %Y %H:%M:%S +0000", localtime())))
525 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
526 "/org/marcoz/feedingit/update" # Object's path
528 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
533 file = open("/home/user/.feedingit/feedingit_widget.log", "a")
534 traceback.print_exc(file=file)
537 def save_config(self):
538 from os.path import isdir
539 if not isdir(CONFIGDIR):
542 file = open(CONFIGDIR+"widget2", "w")
543 pickle.dump(self.size, file )
544 pickle.dump(self.autoupdate, file)
546 self.setup_autoupdate()
548 def setup_autoupdate(self):
549 if (float(self.autoupdate) > 0):
550 if (not self.autoupdateId==False):
551 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
552 #file.write("Disabling %s\n" % self.autoupdateId)
554 gobject.source_remove(self.autoupdateId)
556 self.autoupdateId = gobject.timeout_add_seconds(int(float(self.autoupdate)*3600), self.start_update)
557 file = open(SOURCE, "w")
558 file.write(str(self.autoupdateId))
560 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
561 #file.write("Started %s\n" % self.autoupdateId)
564 if (not self.autoupdateId==False):
565 gobject.source_remove(self.autoupdateId)
566 self.autoupdateId=False
569 def load_config(self):
570 if isfile(CONFIGDIR+"widget2"):
571 file = open(CONFIGDIR+"widget2", "r")
572 self.size = pickle.load( file )
573 self.autoupdate = pickle.load( file )
575 self.setup_autoupdate()
576 elif isfile(CONFIGDIR+"widget"):
577 file = open(CONFIGDIR+"widget", "r")
578 feed_list = pickle.load( file )
579 self.autoupdate = pickle.load( file )
581 self.setup_autoupdate()
584 remove(CONFIGDIR+"widget")
586 #self.feed_list = None
589 self.initialized = True
592 hd_plugin_type = FeedingItHomePlugin
594 # The code below is just for testing purposes.
595 # It allows to run the widget as a standalone process.
596 if __name__ == "__main__":
598 gobject.type_register(hd_plugin_type)
599 obj = gobject.new(hd_plugin_type, plugin_id="plugin_id")