#
# Copyright (c) 2007-2008 INdT.
+# Copyright (c) 2011 Neal H. Walfield
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
#
# ============================================================================
-# Name : FeedingIt.py
-# Author : Yves Marcoz
-# Version : 0.5.0
-# Description : Simple RSS Reader
+__appname__ = 'FeedingIt'
+__author__ = 'Yves Marcoz'
+__version__ = '0.9.1~woodchuck'
+__description__ = 'A simple RSS Reader for Maemo 5'
# ============================================================================
import gtk
-import feedparser
import pango
import hildon
#import gtkhtml2
-try:
- import webkit
- has_webkit=True
-except:
- import gtkhtml2
- has_webkit=False
-import time
-import dbus
-import pickle
-from os.path import isfile, isdir
-from os import mkdir
-import sys
-import urllib2
+#try:
+from webkit import WebView
+# has_webkit=True
+#except:
+# import gtkhtml2
+# has_webkit=False
+from os.path import isfile, isdir, exists
+from os import mkdir, remove, stat, environ
import gobject
+from aboutdialog import HeAboutDialog
from portrait import FremantleRotation
-import threading
-import thread
from feedingitdbus import ServerObject
from config import Config
+from cgi import escape
+import weakref
+import dbus
+import debugging
+import logging
+logger = logging.getLogger(__name__)
-from rss import *
+from rss_sqlite import Listing
from opml import GetOpmlData, ExportOpmlData
-
-import socket
+
+import mainthread
+
+from socket import setdefaulttimeout
timeout = 5
-socket.setdefaulttimeout(timeout)
+setdefaulttimeout(timeout)
+del timeout
+
+import xml.sax
+
+LIST_ICON_SIZE = 32
+LIST_ICON_BORDER = 10
+
+USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
+ABOUT_ICON = 'feedingit'
+ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
+ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
+ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
+ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
+
+color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
+unread_color = color_style.lookup_color('ActiveTextColor')
+read_color = color_style.lookup_color('DefaultTextColor')
+del color_style
CONFIGDIR="/home/user/.feedingit/"
+LOCK = CONFIGDIR + "update.lock"
-class AddWidgetWizard(hildon.WizardDialog):
-
- def __init__(self, parent, urlIn, titleIn=None):
- # Create a Notebook
- self.notebook = gtk.Notebook()
+from re import sub
+from htmlentitydefs import name2codepoint
+
+COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
+
+FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
+
+import style
+
+MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
+MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
+MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
+
+# Build the markup template for the Maemo 5 text style
+head_font = style.get_font_desc('SystemFont')
+sub_font = style.get_font_desc('SmallSystemFont')
+
+#head_color = style.get_color('ButtonTextColor')
+head_color = style.get_color('DefaultTextColor')
+sub_color = style.get_color('DefaultTextColor')
+active_color = style.get_color('ActiveTextColor')
+
+bg_color = style.get_color('DefaultBackgroundColor').to_string()
+c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
+c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
+c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
+bg_color = "#" + c1 + c2 + c3
+
+
+head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
+normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
+
+entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
+entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
+
+active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
+active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
+
+entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
+entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
+
+FEED_TEMPLATE = '\n'.join((head, normal_sub))
+FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
+
+ENTRY_TEMPLATE = entry_head
+ENTRY_TEMPLATE_UNREAD = entry_active_head
+
+notification_iface = None
+def notify(message):
+ def get_iface():
+ global notification_iface
+
+ bus = dbus.SessionBus()
+ proxy = bus.get_object('org.freedesktop.Notifications',
+ '/org/freedesktop/Notifications')
+ notification_iface \
+ = dbus.Interface(proxy, 'org.freedesktop.Notifications')
+
+ def doit():
+ notification_iface.SystemNoteInfoprint("FeedingIt: " + message)
+
+ if notification_iface is None:
+ get_iface()
+
+ try:
+ doit()
+ except dbus.DBusException:
+ # Rebind the name and try again.
+ get_iface()
+ doit()
+
+def open_in_browser(link):
+ bus = dbus.SessionBus()
+ b_proxy = bus.get_object("com.nokia.osso_browser",
+ "/com/nokia/osso_browser/request")
+ b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
+
+ notify("Opening %s" % link)
+
+ # We open the link asynchronously: if the web browser is not
+ # already running, this can take a while.
+ def error_handler():
+ """
+ Something went wrong opening the URL.
+ """
+ def e(exception):
+ notify("Error opening %s: %s" % (link, str(exception)))
+ return e
+
+ b_iface.open_new_window(link,
+ reply_handler=lambda *args: None,
+ error_handler=error_handler())
+
+##
+# Removes HTML or XML character references and entities from a text string.
+#
+# @param text The HTML (or XML) source text.
+# @return The plain text, as a Unicode string, if necessary.
+# http://effbot.org/zone/re-sub.htm#unescape-html
+def unescape(text):
+ def fixup(m):
+ text = m.group(0)
+ if text[:2] == "&#":
+ # character reference
+ try:
+ if text[:3] == "&#x":
+ return unichr(int(text[3:-1], 16))
+ else:
+ return unichr(int(text[2:-1]))
+ except ValueError:
+ pass
+ else:
+ # named entity
+ try:
+ text = unichr(name2codepoint[text[1:-1]])
+ except KeyError:
+ pass
+ return text # leave as is
+ return sub("&#?\w+;", fixup, text)
+
+
+class AddWidgetWizard(gtk.Dialog):
+ def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
+ gtk.Dialog.__init__(self)
+ self.set_transient_for(parent)
+
+ #self.category = categories[0]
+ self.category = currentCat
+
+ if isEdit:
+ self.set_title('Edit RSS feed')
+ else:
+ self.set_title('Add new RSS feed')
+
+ if isEdit:
+ self.btn_add = self.add_button('Save', 2)
+ else:
+ self.btn_add = self.add_button('Add', 2)
+
+ self.set_default_response(2)
self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
- self.nameEntry.set_placeholder("Enter Feed Name")
- vbox = gtk.VBox(False,10)
- label = gtk.Label("Enter Feed Name:")
- vbox.pack_start(label)
- vbox.pack_start(self.nameEntry)
- if not titleIn == None:
+ self.nameEntry.set_placeholder('Feed name')
+ # If titleIn matches urlIn, there is no title.
+ if not titleIn == None and titleIn != urlIn:
self.nameEntry.set_text(titleIn)
- self.notebook.append_page(vbox, None)
-
+ self.nameEntry.select_region(-1, -1)
+
self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
- self.urlEntry.set_placeholder("Enter a URL")
+ self.urlEntry.set_placeholder('Feed URL')
self.urlEntry.set_text(urlIn)
- self.urlEntry.select_region(0,-1)
-
- vbox = gtk.VBox(False,10)
- label = gtk.Label("Enter Feed URL:")
- vbox.pack_start(label)
- vbox.pack_start(self.urlEntry)
- self.notebook.append_page(vbox, None)
-
- labelEnd = gtk.Label("Success")
-
- self.notebook.append_page(labelEnd, None)
-
- hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
-
- # Set a handler for "switch-page" signal
- #self.notebook.connect("switch_page", self.on_page_switch, self)
-
- # Set a function to decide if user can go to next page
- self.set_forward_page_func(self.some_page_func)
-
- self.show_all()
+ self.urlEntry.select_region(-1, -1)
+ self.urlEntry.set_activates_default(True)
+
+ self.table = gtk.Table(3, 2, False)
+ self.table.set_col_spacings(5)
+ label = gtk.Label('Name:')
+ label.set_alignment(1., .5)
+ self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
+ self.table.attach(self.nameEntry, 1, 2, 0, 1)
+ label = gtk.Label('URL:')
+ label.set_alignment(1., .5)
+ self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
+ self.table.attach(self.urlEntry, 1, 2, 1, 2)
+ selector = self.create_selector(categories, listing)
+ picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
+ picker.set_selector(selector)
+ picker.set_title("Select category")
+ #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
+ picker.set_name('HildonButton-finger')
+ picker.set_alignment(0,0,1,1)
- def getData(self):
- return (self.nameEntry.get_text(), self.urlEntry.get_text())
+ self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
- def on_page_switch(self, notebook, page, num, dialog):
- return True
-
- def some_page_func(self, nb, current, userdata):
- # Validate data for 1st page
- if current == 0:
- return len(self.nameEntry.get_text()) != 0
- elif current == 1:
- # Check the url is not null, and starts with http
- return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
- elif current != 2:
- return False
- else:
- return True
+ self.vbox.pack_start(self.table)
+
+ self.show_all()
-class GetImage(threading.Thread):
- def __init__(self, url, stream):
- threading.Thread.__init__(self)
- self.url = url
- self.stream = stream
+ def getData(self):
+ return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
- def run(self):
- f = urllib2.urlopen(self.url)
- data = f.read()
- f.close()
- self.stream.write(data)
- self.stream.close()
+ def create_selector(self, choices, listing):
+ #self.pickerDialog = hildon.PickerDialog(self.parent)
+ selector = hildon.TouchSelector(text=True)
+ index = 0
+ self.map = {}
+ for item in choices:
+ title = listing.getCategoryTitle(item)
+ iter = selector.append_text(str(title))
+ if self.category == item:
+ selector.set_active(0, index)
+ self.map[title] = item
+ index += 1
+ selector.connect("changed", self.selection_changed)
+ #self.pickerDialog.set_selector(selector)
+ return selector
+
+ def selection_changed(self, selector, button):
+ current_selection = selector.get_current_text()
+ if current_selection:
+ self.category = self.map[current_selection]
+
+class AddCategoryWizard(gtk.Dialog):
+ def __init__(self, parent, titleIn=None, isEdit=False):
+ gtk.Dialog.__init__(self)
+ self.set_transient_for(parent)
+
+ if isEdit:
+ self.set_title('Edit Category')
+ else:
+ self.set_title('Add Category')
-class ImageDownloader():
- def __init__(self):
- self.images = []
- self.downloading = False
-
- def queueImage(self, url, stream):
- self.images.append((url, stream))
- if not self.downloading:
- self.downloading = True
- gobject.timeout_add(50, self.checkQueue)
-
- def checkQueue(self):
- for i in range(4-threading.activeCount()):
- if len(self.images) > 0:
- (url, stream) = self.images.pop()
- GetImage(url, stream).start()
- if len(self.images)>0:
- gobject.timeout_add(200, self.checkQueue)
+ if isEdit:
+ self.btn_add = self.add_button('Save', 2)
else:
- self.downloading=False
-
- def stopAll(self):
- self.images = []
-
-
-class Download(threading.Thread):
- def __init__(self, listing, key, config):
- threading.Thread.__init__(self)
- self.listing = listing
- self.key = key
- self.config = config
-
- def run (self):
- self.listing.updateFeed(self.key, self.config.getExpiry())
+ self.btn_add = self.add_button('Add', 2)
+
+ self.set_default_response(2)
+
+ self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
+ self.nameEntry.set_placeholder('Category name')
+ if not titleIn == None:
+ self.nameEntry.set_text(titleIn)
+ self.nameEntry.select_region(-1, -1)
+
+ self.table = gtk.Table(1, 2, False)
+ self.table.set_col_spacings(5)
+ label = gtk.Label('Name:')
+ label.set_alignment(1., .5)
+ self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
+ self.table.attach(self.nameEntry, 1, 2, 0, 1)
+ #label = gtk.Label('URL:')
+ #label.set_alignment(1., .5)
+ #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
+ #self.table.attach(self.urlEntry, 1, 2, 1, 2)
+ self.vbox.pack_start(self.table)
+ self.show_all()
+
+ def getData(self):
+ return self.nameEntry.get_text()
class DownloadBar(gtk.ProgressBar):
- def __init__(self, parent, listing, listOfKeys, config, single=False):
+ @classmethod
+ def class_init(cls):
+ if hasattr (cls, 'class_init_done'):
+ return
+
+ cls.downloadbars = []
+ # Total number of jobs we are monitoring.
+ cls.total = 0
+ # Number of jobs complete (of those that we are monitoring).
+ cls.done = 0
+ # Percent complete.
+ cls.progress = 0
+
+ cls.class_init_done = True
+
+ bus = dbus.SessionBus()
+ bus.add_signal_receiver(handler_function=cls.update_progress,
+ bus_name=None,
+ signal_name='UpdateProgress',
+ dbus_interface='org.marcoz.feedingit',
+ path='/org/marcoz/feedingit/update')
+
+ def __init__(self, parent):
+ self.class_init ()
+
gtk.ProgressBar.__init__(self)
- self.listOfKeys = listOfKeys[:]
- self.listing = listing
- self.total = len(self.listOfKeys)
- self.config = config
- self.current = 0
- self.single = single
-
- if self.total>0:
- #self.progress = gtk.ProgressBar()
- #self.waitingWindow = hildon.Note("cancel", parent, "Downloading",
- # progressbar=self.progress)
- self.set_text("Updating...")
- self.fraction = 0
- self.set_fraction(self.fraction)
- self.show_all()
- # Create a timeout
- self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
- #self.waitingWindow.show_all()
- #response = self.waitingWindow.run()
- #self.listOfKeys = []
- #while threading.activeCount() > 1:
- # Wait for current downloads to finish
- # time.sleep(0.1)
- #self.waitingWindow.destroy()
-
- def update_progress_bar(self):
- #self.progress_bar.pulse()
- if threading.activeCount() < 4:
- x = threading.activeCount() - 1
- k = len(self.listOfKeys)
- fin = self.total - k - x
- fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
- #print x, k, fin, fraction
- self.set_fraction(fraction)
-
- if len(self.listOfKeys)>0:
- self.current = self.current+1
- key = self.listOfKeys.pop()
- if (not self.listing.getCurrentlyDisplayedFeed() == key) or (self.single == True):
- # Check if the feed is being displayed
- download = Download(self.listing, key, self.config)
- download.start()
- return True
- elif threading.activeCount() > 1:
- return True
+
+ self.downloadbars.append(weakref.ref (self))
+ self.set_fraction(0)
+ self.__class__.update_bars()
+
+ @classmethod
+ def downloading(cls):
+ cls.class_init ()
+ return cls.done != cls.total
+
+ @classmethod
+ def update_progress(cls, percent_complete,
+ completed, in_progress, queued,
+ bytes_downloaded, bytes_updated, bytes_per_second,
+ feed_updated):
+ if not cls.downloadbars:
+ return
+
+ cls.total = completed + in_progress + queued
+ cls.done = completed
+ cls.progress = percent_complete / 100.
+ if cls.progress < 0: cls.progress = 0
+ if cls.progress > 1: cls.progress = 1
+
+ if feed_updated:
+ for ref in cls.downloadbars:
+ bar = ref ()
+ if bar is None:
+ # The download bar disappeared.
+ cls.downloadbars.remove (ref)
+ else:
+ bar.emit("download-done", feed_updated)
+
+ if in_progress == 0 and queued == 0:
+ for ref in cls.downloadbars:
+ bar = ref ()
+ if bar is None:
+ # The download bar disappeared.
+ cls.downloadbars.remove (ref)
+ else:
+ bar.emit("download-done", None)
+ return
+
+ cls.update_bars()
+
+ @classmethod
+ def update_bars(cls):
+ # In preparation for i18n/l10n
+ def N_(a, b, n):
+ return (a if n == 1 else b)
+
+ text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
+ cls.total)
+ % (cls.done, cls.total))
+
+ for ref in cls.downloadbars:
+ bar = ref ()
+ if bar is None:
+ # The download bar disappeared.
+ cls.downloadbars.remove (ref)
else:
- #self.waitingWindow.destroy()
- #self.destroy()
- self.emit("download-done", "success")
- return False
- return True
-
-
-class SortList(gtk.Dialog):
- def __init__(self, parent, listing):
- gtk.Dialog.__init__(self, "Organizer", parent)
+ bar.set_text(text)
+ bar.set_fraction(cls.progress)
+
+class SortList(hildon.StackableWindow):
+ def __init__(self, parent, listing, feedingit, after_closing, category=None):
+ hildon.StackableWindow.__init__(self)
+ self.set_transient_for(parent)
+ if category:
+ self.isEditingCategories = False
+ self.category = category
+ self.set_title(listing.getCategoryTitle(category))
+ else:
+ self.isEditingCategories = True
+ self.set_title('Categories')
self.listing = listing
-
- self.vbox2 = gtk.VBox(False, 10)
-
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Move Up")
+ self.feedingit = feedingit
+ self.after_closing = after_closing
+ if after_closing:
+ self.connect('destroy', lambda w: self.after_closing())
+ self.vbox2 = gtk.VBox(False, 2)
+
+ button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
button.connect("clicked", self.buttonUp)
self.vbox2.pack_start(button, expand=False, fill=False)
-
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Move Down")
+
+ button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
button.connect("clicked", self.buttonDown)
self.vbox2.pack_start(button, expand=False, fill=False)
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Add Feed")
+ self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
+
+ button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
button.connect("clicked", self.buttonAdd)
self.vbox2.pack_start(button, expand=False, fill=False)
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Edit Feed")
+ button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
button.connect("clicked", self.buttonEdit)
self.vbox2.pack_start(button, expand=False, fill=False)
-
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Delete")
+
+ button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
button.connect("clicked", self.buttonDelete)
self.vbox2.pack_start(button, expand=False, fill=False)
-
+
#button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
#button.set_label("Done")
#button.connect("clicked", self.buttonDone)
self.displayFeeds()
self.hbox2.pack_end(self.vbox2, expand=False)
self.set_default_size(-1, 600)
- self.vbox.pack_start(self.hbox2)
+ self.add(self.hbox2)
+
+ menu = hildon.AppMenu()
+ button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+ button.set_label("Import from OPML")
+ button.connect("clicked", self.feedingit.button_import_clicked)
+ menu.append(button)
+
+ button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+ button.set_label("Export to OPML")
+ button.connect("clicked", self.feedingit.button_export_clicked)
+ menu.append(button)
+ self.set_app_menu(menu)
+ menu.show_all()
self.show_all()
#self.connect("destroy", self.buttonDone)
#self.show_all()
def refreshList(self, selected=None, offset=0):
- rect = self.treeview.get_visible_rect()
- y = rect.y+rect.height
+ #rect = self.treeview.get_visible_rect()
+ #y = rect.y+rect.height
self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
- for key in self.listing.getListOfFeeds():
- item = self.treestore.append([self.listing.getFeedTitle(key), key])
- if key == selected:
- selectedItem = item
+ if self.isEditingCategories:
+ for key in self.listing.getListOfCategories():
+ item = self.treestore.append([self.listing.getCategoryTitle(key), key])
+ if key == selected:
+ selectedItem = item
+ else:
+ for key in self.listing.getListOfFeeds(category=self.category):
+ item = self.treestore.append([self.listing.getFeedTitle(key), key])
+ if key == selected:
+ selectedItem = item
self.treeview.set_model(self.treestore)
if not selected == None:
self.treeview.get_selection().select_iter(selectedItem)
def buttonUp(self, button):
key = self.getSelectedItem()
if not key == None:
- self.listing.moveUp(key)
+ if self.isEditingCategories:
+ self.listing.moveCategoryUp(key)
+ else:
+ self.listing.moveUp(key)
self.refreshList(key, -10)
def buttonDown(self, button):
key = self.getSelectedItem()
if not key == None:
- self.listing.moveDown(key)
+ if self.isEditingCategories:
+ self.listing.moveCategoryDown(key)
+ else:
+ self.listing.moveDown(key)
self.refreshList(key, 10)
def buttonDelete(self, button):
key = self.getSelectedItem()
- if not key == None:
- self.listing.removeFeed(key)
- self.refreshList()
+
+ message = 'Really remove this feed and its entries?'
+ dlg = hildon.hildon_note_new_confirmation(self, message)
+ response = dlg.run()
+ dlg.destroy()
+ if response == gtk.RESPONSE_OK:
+ if self.isEditingCategories:
+ self.listing.removeCategory(key)
+ else:
+ self.listing.removeFeed(key)
+ self.refreshList()
def buttonEdit(self, button):
key = self.getSelectedItem()
- if not key == None:
- wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
- ret = wizard.run()
- if ret == 2:
- (title, url) = wizard.getData()
- if (not title == '') and (not url == ''):
- self.listing.editFeed(key, title, url)
- wizard.destroy()
- self.refreshList()
+
+ if key == 'ArchivedArticles':
+ message = 'Cannot edit the archived articles feed.'
+ hildon.hildon_banner_show_information(self, '', message)
+ return
+ if self.isEditingCategories:
+ if key is not None:
+ SortList(self.parent, self.listing, self.feedingit, None, category=key)
+ else:
+ if key is not None:
+ wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
+ ret = wizard.run()
+ if ret == 2:
+ (title, url, category) = wizard.getData()
+ if url != '':
+ self.listing.editFeed(key, title, url, category=category)
+ self.refreshList()
+ wizard.destroy()
def buttonDone(self, *args):
self.destroy()
def buttonAdd(self, button, urlIn="http://"):
- wizard = AddWidgetWizard(self, urlIn)
- ret = wizard.run()
- if ret == 2:
- (title, url) = wizard.getData()
- if (not title == '') and (not url == ''):
- self.listing.addFeed(title, url)
+ if self.isEditingCategories:
+ wizard = AddCategoryWizard(self)
+ ret = wizard.run()
+ if ret == 2:
+ title = wizard.getData()
+ if (not title == ''):
+ self.listing.addCategory(title)
+ else:
+ wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
+ ret = wizard.run()
+ if ret == 2:
+ (title, url, category) = wizard.getData()
+ if url:
+ self.listing.addFeed(title, url, category=category)
wizard.destroy()
self.refreshList()
class DisplayArticle(hildon.StackableWindow):
- def __init__(self, title, text, link, index, key, listing, config):
+ """
+ A Widget for displaying an article.
+ """
+ def __init__(self, article_id, feed, feed_key, articles, config, listing):
+ """
+ article_id - The identifier of the article to load.
+
+ feed - The feed object containing the article (an
+ rss_sqlite:Feed object).
+
+ feed_key - The feed's identifier.
+
+ articles - A list of articles from the feed to display.
+ Needed for selecting the next/previous article (article_next).
+
+ config - A configuration object (config:Config).
+
+ listing - The listing object (rss_sqlite:Listing) that
+ contains the feed and article.
+ """
hildon.StackableWindow.__init__(self)
- self.imageDownloader = ImageDownloader()
- self.listing=listing
- self.key = key
- self.index = index
- self.text = text
- self.link = link
- self.set_title(title)
+
+ self.article_id = None
+ self.feed = feed
+ self.feed_key = feed_key
+ self.articles = articles
self.config = config
- self.images = []
-
+ self.listing = listing
+
+ self.set_title(self.listing.getFeedTitle(feed_key))
+
# Init the article display
- if self.config.getWebkitSupport():
- self.view = webkit.WebView()
- #self.view.set_editable(False)
- else:
- self.view = gtkhtml2.View()
- self.document = gtkhtml2.Document()
- self.view.set_document(self.document)
- self.document.connect("link_clicked", self._signal_link_clicked)
+ self.view = WebView()
+ self.view.set_zoom_level(float(config.getArtFontSize())/10.)
+ self.view.connect("motion-notify-event", lambda w,ev: True)
+ self.view.connect('load-started', self.load_started)
+ self.view.connect('load-finished', self.load_finished)
+ self.view.connect('navigation-requested', self.navigation_requested)
+ self.view.connect("button_press_event", self.button_pressed)
+ self.gestureId = self.view.connect(
+ "button_release_event", self.button_released)
+
self.pannable_article = hildon.PannableArea()
self.pannable_article.add(self.view)
- #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
- #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
- if self.config.getWebkitSupport():
- self.view.load_string(self.text, "text/html", "utf-8", self.link)
- self.view.set_zoom_level(float(config.getArtFontSize())/10.)
- else:
- if not key == "1295627ef630df9d239abeb0ba631c3f":
- # Do not download images if the feed is "Archived Articles"
- self.document.connect("request-url", self._signal_request_url)
-
- self.document.clear()
- self.document.open_stream("text/html")
- self.document.write_stream(self.text)
- self.document.close_stream()
-
+ self.add(self.pannable_article)
+
+ self.pannable_article.show_all()
+
+ # Create the menu.
menu = hildon.AppMenu()
- # Create a button and add it to the menu
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Allow Horizontal Scrolling")
- button.connect("clicked", self.horiz_scrolling_button)
- menu.append(button)
-
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Open in Browser")
- button.connect("clicked", self._signal_link_clicked, self.link)
- menu.append(button)
-
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Add to Archived Articles")
- button.connect("clicked", self.archive_button)
- menu.append(button)
+
+ def menu_button(label, callback):
+ button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+ button.set_label(label)
+ button.connect("clicked", callback)
+ menu.append(button)
+
+ menu_button("Allow horizontal scrolling", self.horiz_scrolling_button)
+ menu_button("Open in browser", self.open_in_browser)
+ if feed_key == "ArchivedArticles":
+ menu_button(
+ "Remove from archived articles", self.remove_archive_button)
+ else:
+ menu_button("Add to archived articles", self.archive_button)
self.set_app_menu(menu)
menu.show_all()
- #self.event_box = gtk.EventBox()
- #self.event_box.add(self.pannable_article)
- self.add(self.pannable_article)
-
-
- self.pannable_article.show_all()
-
self.destroyId = self.connect("destroy", self.destroyWindow)
-
- self.view.connect("button_press_event", self.button_pressed)
- self.view.connect("button_release_event", self.button_released)
- #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
+
+ self.article_open(article_id)
+
+ def article_open(self, article_id):
+ """
+ Load the article with the specified id.
+ """
+ # If an article was open, close it.
+ if self.article_id is not None:
+ self.article_closed()
+
+ self.article_id = article_id
+ self.set_for_removal = False
+ self.loadedArticle = False
+ self.initial_article_load = True
+
+ contentLink = self.feed.getContentLink(self.article_id)
+ if contentLink.startswith("/home/user/"):
+ self.view.open("file://%s" % contentLink)
+ self.currentUrl = self.feed.getExternalLink(self.article_id)
+ else:
+ self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
+ self.currentUrl = str(contentLink)
+
+ self.feed.setEntryRead(self.article_id)
+
+ def article_closed(self):
+ """
+ The user has navigated away from the article. Execute any
+ pending actions.
+ """
+ if self.set_for_removal:
+ self.emit("article-deleted", self.article_id)
+ else:
+ self.emit("article-closed", self.article_id)
+
+
+ def navigation_requested(self, wv, fr, req):
+ """
+ http://webkitgtk.org/reference/webkitgtk-webkitwebview.html#WebKitWebView-navigation-requested
+
+ wv - a WebKitWebView
+ fr - a WebKitWebFrame
+ req - WebKitNetworkRequest
+ """
+ if self.initial_article_load:
+ # Always initially load an article in the internal
+ # browser.
+ self.initial_article_load = False
+ return False
+
+ # When following a link, only use the internal browser if so
+ # configured. Otherwise, launch an external browser.
+ if self.config.getOpenInExternalBrowser():
+ self.open_in_browser(None, req.get_uri())
+ return True
+ else:
+ return False
+
+ def load_started(self, *widget):
+ hildon.hildon_gtk_window_set_progress_indicator(self, 1)
+
+ def load_finished(self, *widget):
+ hildon.hildon_gtk_window_set_progress_indicator(self, 0)
+ frame = self.view.get_main_frame()
+ if self.loadedArticle:
+ self.currentUrl = frame.get_uri()
+ else:
+ self.loadedArticle = True
def button_pressed(self, window, event):
- #print event.x, event.y
+ """
+ The user pressed a "mouse button" (in our case, this means the
+ user likely started to drag with the finger).
+
+ We are only interested in whether the user performs a drag.
+ We record the starting position and when the user "releases
+ the button," we see how far the mouse moved.
+ """
self.coords = (event.x, event.y)
def button_released(self, window, event):
x = self.coords[0] - event.x
y = self.coords[1] - event.y
- if (abs(y) < 20):
- if (x > 30):
- self.emit("article-previous", self.index)
- elif (x<-30):
- self.emit("article-next", self.index)
- #print x, y
- #print "Released"
-
- #def gesture(self, widget, direction, startx, starty):
- # if (direction == 3):
- # self.emit("article-next", self.index)
- # if (direction == 2):
- # self.emit("article-previous", self.index)
- #print startx, starty
- #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
+ if (2*abs(y) < abs(x)):
+ if (x > 15):
+ self.article_next(forward=False)
+ elif (x<-15):
+ self.article_next(forward=True)
+
+ # We handled the event. Don't propagate it further.
+ return True
+
+ def article_next(self, forward=True):
+ """
+ Advance to the next (or, if forward is false, the previous)
+ article.
+ """
+ first_id = None
+ id = self.article_id
+ i = 0
+ while True:
+ i += 1
+ id = self.feed.getNextId(id, forward)
+ if id == first_id:
+ # We looped.
+ break
+
+ if first_id is None:
+ first_id = id
+
+ if id in self.articles:
+ self.article_open(id)
+ break
def destroyWindow(self, *args):
+ self.article_closed()
self.disconnect(self.destroyId)
- self.emit("article-closed", self.index)
- self.imageDownloader.stopAll()
self.destroy()
def horiz_scrolling_button(self, *widget):
def archive_button(self, *widget):
# Call the listing.addArchivedArticle
- self.listing.addArchivedArticle(self.key, self.index)
-
- #def reloadArticle(self, *widget):
- # if threading.activeCount() > 1:
- # Image thread are still running, come back in a bit
- # return True
- # else:
- # for (stream, imageThread) in self.images:
- # imageThread.join()
- # stream.write(imageThread.data)
- # stream.close()
- # return False
- # self.show_all()
-
- def _signal_link_clicked(self, object, link):
- bus = dbus.SystemBus()
- proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
- iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
- iface.load_url(link)
-
- def _signal_request_url(self, object, url, stream):
- #print url
- self.imageDownloader.queueImage(url, stream)
- #imageThread = GetImage(url)
- #imageThread.start()
- #self.images.append((stream, imageThread))
+ self.listing.addArchivedArticle(self.feed_key, self.article_id)
+
+ def remove_archive_button(self, *widget):
+ self.set_for_removal = True
+ def open_in_browser(self, object, link=None):
+ """
+ Open the specified link using the system's browser. If not
+ link is specified, reopen the current page using the system's
+ browser.
+ """
+ if link == None:
+ link = self.currentUrl
+
+ open_in_browser(link)
class DisplayFeed(hildon.StackableWindow):
- def __init__(self, listing, feed, title, key, config):
+ def __init__(self, listing, config):
hildon.StackableWindow.__init__(self)
+ self.connect('configure-event', self.on_configure_event)
+ self.connect("delete_event", self.delete_window)
+ self.connect("destroy", self.destroyWindow)
+
self.listing = listing
- self.feed = feed
- self.feedTitle = title
- self.set_title(title)
- self.key=key
self.config = config
+
+ # Articles to show.
+ #
+ # If hide read articles is set, this is set to the set of
+ # unread articles at the time that feed is loaded. The last
+ # bit is important: when the user selects the next article,
+ # but then decides to move back, previous should select the
+ # just read article.
+ self.articles = list()
self.downloadDialog = False
- self.listing.setCurrentlyDisplayedFeed(self.key)
+ #self.listing.setCurrentlyDisplayedFeed(self.key)
self.disp = False
menu = hildon.AppMenu()
button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Update Feed")
+ button.set_label("Update feed")
button.connect("clicked", self.button_update_clicked)
menu.append(button)
button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Mark All As Read")
+ button.set_label("Mark all as read")
button.connect("clicked", self.buttonReadAllClicked)
menu.append(button)
+
+ button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+ button.set_label("Delete read articles")
+ button.connect("clicked", self.buttonPurgeArticles)
+ menu.append(button)
+ self.archived_article_buttons = [button]
+
self.set_app_menu(menu)
menu.show_all()
- self.displayFeed()
-
- self.connect("destroy", self.destroyWindow)
+ self.feedItems = gtk.ListStore(str, str)
+
+ self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
+ self.feedList.set_model(self.feedItems)
+ self.feedList.set_rules_hint(True)
+ self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
+ self.feedList.set_hover_selection(False)
+ self.feedList.connect('hildon-row-tapped',
+ self.on_feedList_row_activated)
+
+ self.markup_renderer = gtk.CellRendererText()
+ self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
+ self.markup_renderer.set_property('background', bg_color) #"#333333")
+ (width, height) = self.get_size()
+ self.markup_renderer.set_property('wrap-width', width-20)
+ self.markup_renderer.set_property('ypad', 8)
+ self.markup_renderer.set_property('xpad', 5)
+ markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
+ markup=FEED_COLUMN_MARKUP)
+ self.feedList.append_column(markup_column)
+
+ vbox = gtk.VBox(False, 10)
+ vbox.pack_start(self.feedList)
+ self.pannableFeed = hildon.PannableArea()
+ self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
+ self.pannableFeed.add_with_viewport(vbox)
+
+ self.main_vbox = gtk.VBox(False, 0)
+ self.main_vbox.pack_start(self.pannableFeed)
+
+ self.add(self.main_vbox)
+
+ self.main_vbox.show_all()
+
+ def on_configure_event(self, window, event):
+ if getattr(self, 'markup_renderer', None) is None:
+ return
+
+ # Fix up the column width for wrapping the text when the window is
+ # resized (i.e. orientation changed)
+ self.markup_renderer.set_property('wrap-width', event.width-20)
+ it = self.feedItems.get_iter_first()
+ while it is not None:
+ markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
+ self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
+ it = self.feedItems.iter_next(it)
+
+ def delete_window(self, *args):
+ """
+ Prevent the window from being deleted.
+ """
+ self.hide()
+
+ try:
+ key = self.key
+ except AttributeError:
+ key = None
+
+ if key is not None:
+ self.listing.updateUnread(key)
+ self.emit("feed-closed", key)
+ self.key = None
+ self.feedItems.clear()
+
+ return True
+
def destroyWindow(self, *args):
- self.feed.saveUnread(CONFIGDIR)
- self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
- self.emit("feed-closed", self.key)
- self.destroy()
- #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
- self.listing.closeCurrentlyDisplayedFeed()
+ try:
+ key = self.key
+ except AttributeError:
+ key = None
- def displayFeed(self):
- self.vboxFeed = gtk.VBox(False, 10)
- self.pannableFeed = hildon.PannableArea()
- self.pannableFeed.add_with_viewport(self.vboxFeed)
- self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
- self.buttons = {}
- for id in self.feed.getIds():
- button = gtk.Button(self.feed.getTitle(id))
- button.set_alignment(0,0)
- label = button.child
- if self.feed.isEntryRead(id):
- #label.modify_font(pango.FontDescription("sans 16"))
- label.modify_font(pango.FontDescription(self.config.getReadFont()))
- else:
- #print self.listing.getFont() + " bold"
- label.modify_font(pango.FontDescription(self.config.getUnreadFont()))
- #label.modify_font(pango.FontDescription("sans bold 23"))
- #"sans bold 16"
- label.set_line_wrap(True)
-
- label.set_size_request(self.get_size()[0]-50, -1)
- button.connect("clicked", self.button_clicked, id)
- self.buttons[id] = button
-
- self.vboxFeed.pack_start(button, expand=False)
+ self.destroy()
- self.add(self.pannableFeed)
- self.show_all()
+ def fix_title(self, title):
+ return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
+
+ def displayFeed(self, key=None):
+ """
+ Select and display a feed. If feed, title and key are None,
+ reloads the current feed.
+ """
+ if key:
+ try:
+ old_key = self.key
+ except AttributeError:
+ old_key = None
+
+ if old_key:
+ self.listing.updateUnread(self.key)
+ self.emit("feed-closed", self.key)
+
+ self.feed = self.listing.getFeed(key)
+ self.feedTitle = self.listing.getFeedTitle(key)
+ self.key = key
+
+ self.set_title(self.feedTitle)
+
+ selection = self.feedList.get_selection()
+ if selection is not None:
+ selection.set_mode(gtk.SELECTION_NONE)
+
+ for b in self.archived_article_buttons:
+ if key == "ArchivedArticles":
+ b.show()
+ else:
+ b.hide()
+
+ #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
+ hideReadArticles = self.config.getHideReadArticles()
+ if hideReadArticles:
+ articles = self.feed.getIds(onlyUnread=True)
+ else:
+ articles = self.feed.getIds()
+ self.articles[:] = []
+
+ self.feedItems.clear()
+ for id in articles:
+ try:
+ isRead = self.feed.isEntryRead(id)
+ except Exception:
+ isRead = False
+ if not ( isRead and hideReadArticles ):
+ title = self.fix_title(self.feed.getTitle(id))
+ self.articles.append(id)
+ if isRead:
+ markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
+ else:
+ markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
+
+ self.feedItems.append((markup, id))
+ if not articles:
+ # No articles.
+ markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
+ self.feedItems.append((markup, ""))
+
+ self.show()
+
def clear(self):
- self.remove(self.pannableFeed)
+ self.pannableFeed.destroy()
+ #self.remove(self.pannableFeed)
+
+ def on_feedList_row_activated(self, treeview, path): #, column):
+ if not self.articles:
+ # There are not actually any articles. Ignore.
+ return False
+
+ selection = self.feedList.get_selection()
+ selection.set_mode(gtk.SELECTION_SINGLE)
+ self.feedList.get_selection().select_path(path)
+ model = treeview.get_model()
+ iter = model.get_iter(path)
+ key = model.get_value(iter, FEED_COLUMN_KEY)
+ # Emulate legacy "button_clicked" call via treeview
+ gobject.idle_add(self.button_clicked, treeview, key)
+ #return True
def button_clicked(self, button, index, previous=False, next=False):
- newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
+ newDisp = DisplayArticle(index, self.feed, self.key, self.articles, self.config, self.listing)
stack = hildon.WindowStack.get_default()
if previous:
tmp = stack.peek()
gobject.timeout_add(200, self.destroyArticle, tmp)
#print "previous"
self.disp = newDisp
-
- #stack.push(tmp)
- #if not self.disp == False:
- # self.disp.destroyWindow()
elif next:
- #print type(self.disp).__name__
-
- #self.disp.destroyWindow()
- #stack.pop_and_push(1,newDisp)
- #else:
- # stack.push(newDisp)
- #self.disp = newDisp
newDisp.show_all()
if type(self.disp).__name__ == "DisplayArticle":
gobject.timeout_add(200, self.destroyArticle, self.disp)
self.disp = newDisp
- #self.disp.show_all()
- #if not self.disp == False:
- # self.disp.destroyWindow()
else:
self.disp = newDisp
self.disp.show_all()
self.ids = []
+ if self.key == "ArchivedArticles":
+ self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
- self.ids.append(self.disp.connect("article-next", self.nextArticle))
- self.ids.append(self.disp.connect("article-previous", self.previousArticle))
+
+ def buttonPurgeArticles(self, *widget):
+ self.clear()
+ self.feed.purgeReadArticles()
+ #self.feed.saveFeed(CONFIGDIR)
+ self.displayFeed()
def destroyArticle(self, handle):
handle.destroyWindow()
- def nextArticle(self, object, index):
- label = self.buttons[index].child
- label.modify_font(pango.FontDescription(self.config.getReadFont()))
- id = self.feed.getNextId(index)
- self.button_clicked(object, id, next=True)
-
- def previousArticle(self, object, index):
- label = self.buttons[index].child
- label.modify_font(pango.FontDescription(self.config.getReadFont()))
- id = self.feed.getPreviousId(index)
- self.button_clicked(object, id, previous=True)
+ def mark_item_read(self, key):
+ it = self.feedItems.get_iter_first()
+ while it is not None:
+ k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
+ if k == key:
+ title = self.fix_title(self.feed.getTitle(key))
+ markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
+ self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
+ break
+ it = self.feedItems.iter_next(it)
def onArticleClosed(self, object, index):
- label = self.buttons[index].child
- label.modify_font(pango.FontDescription(self.config.getReadFont()))
- self.buttons[index].show()
+ selection = self.feedList.get_selection()
+ selection.set_mode(gtk.SELECTION_NONE)
+ self.mark_item_read(index)
+
+ def onArticleDeleted(self, object, index):
+ self.clear()
+ self.feed.removeArticle(index)
+ #self.feed.saveFeed(CONFIGDIR)
+ self.displayFeed()
+
+
+ def do_update_feed(self):
+ self.listing.updateFeed (self.key, priority=-1)
def button_update_clicked(self, button):
- #bar = DownloadBar(self, self.listing, [self.key,], self.config )
- if not type(self.downloadDialog).__name__=="DownloadBar":
- self.pannableFeed.destroy()
- self.vbox = gtk.VBox(False, 10)
- self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
- self.downloadDialog.connect("download-done", self.onDownloadsDone)
- self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
- self.add(self.vbox)
- self.show_all()
+ gobject.idle_add(self.do_update_feed)
- def onDownloadsDone(self, *widget):
- self.vbox.destroy()
- self.displayFeed()
- #self.feed.updateFeed()
- # self.clear()
- # self.displayFeed()
-
+ def show_download_bar(self):
+ if not type(self.downloadDialog).__name__=="DownloadBar":
+ self.downloadDialog = DownloadBar(self.window)
+ self.downloadDialog.connect("download-done", self.onDownloadDone)
+ self.main_vbox.pack_end(self.downloadDialog,
+ expand=False, fill=False)
+ self.downloadDialog.show()
+
+ def onDownloadDone(self, widget, feed):
+ if feed is not None and hasattr(self, 'feed') and feed == self.feed:
+ self.feed = self.listing.getFeed(self.key)
+ self.displayFeed()
+
+ if feed is None:
+ self.downloadDialog.destroy()
+ self.downloadDialog = False
+
def buttonReadAllClicked(self, button):
- for index in self.feed.getIds():
- self.feed.setEntryRead(index)
- label = self.buttons[index].child
- label.modify_font(pango.FontDescription(self.config.getReadFont()))
- self.buttons[index].show()
+ #self.clear()
+ self.feed.markAllAsRead()
+ it = self.feedItems.get_iter_first()
+ while it is not None:
+ k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
+ title = self.fix_title(self.feed.getTitle(k))
+ markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
+ self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
+ it = self.feedItems.iter_next(it)
+ #self.displayFeed()
+ #for index in self.feed.getIds():
+ # self.feed.setEntryRead(index)
+ # self.mark_item_read(index)
class FeedingIt:
def __init__(self):
# Init the windows
self.window = hildon.StackableWindow()
- self.window.set_title("FeedingIt")
hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
+
+ self.config = Config(self.window, CONFIGDIR+"config.ini")
+
+ try:
+ self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
+ self.orientation.set_mode(self.config.getOrientation())
+ except Exception, e:
+ logger.warn("Could not start rotation manager: %s" % str(e))
+
+ self.window.set_title(__appname__)
self.mainVbox = gtk.VBox(False,10)
- self.pannableListing = gtk.Label("Loading...")
- self.mainVbox.pack_start(self.pannableListing)
+
+ if isfile(CONFIGDIR+"/feeds.db"):
+ self.introLabel = gtk.Label("Loading...")
+ else:
+ self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
+
+ self.mainVbox.pack_start(self.introLabel)
+
self.window.add(self.mainVbox)
self.window.show_all()
- self.config = Config(self.window, CONFIGDIR+"config.ini", has_webkit)
gobject.idle_add(self.createWindow)
-
+
def createWindow(self):
- self.listing = Listing(CONFIGDIR)
-
+ self.category = 0
+ self.listing = Listing(self.config, CONFIGDIR)
+
self.downloadDialog = False
- self.orientation = FremantleRotation("FeedingIt", main_window=self.window)
- self.orientation.set_mode(self.config.getOrientation())
+
+ self.introLabel.destroy()
+ self.pannableListing = hildon.PannableArea()
+
+ # The main area is a view consisting of an icon and two
+ # strings. The view is bound to the Listing's database via a
+ # TreeStore.
+
+ self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
+ self.feedList = gtk.TreeView(self.feedItems)
+ self.feedList.connect('row-activated', self.on_feedList_row_activated)
+
+ self.pannableListing.add(self.feedList)
+
+ icon_renderer = gtk.CellRendererPixbuf()
+ icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
+ icon_column = gtk.TreeViewColumn('', icon_renderer, \
+ pixbuf=COLUMN_ICON)
+ self.feedList.append_column(icon_column)
+
+ markup_renderer = gtk.CellRendererText()
+ markup_column = gtk.TreeViewColumn('', markup_renderer, \
+ markup=COLUMN_MARKUP)
+ self.feedList.append_column(markup_column)
+ self.mainVbox.pack_start(self.pannableListing)
+ self.mainVbox.show_all()
+
+ self.displayListing()
+ hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
+ gobject.idle_add(self.late_init)
+
+ def update_progress(self, percent_complete,
+ completed, in_progress, queued,
+ bytes_downloaded, bytes_updated, bytes_per_second,
+ updated_feed):
+ if (in_progress or queued) and not self.downloadDialog:
+ self.downloadDialog = DownloadBar(self.window)
+ self.downloadDialog.connect("download-done", self.onDownloadDone)
+ self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
+ self.downloadDialog.show()
+
+ if self.__dict__.get ('disp', None):
+ self.disp.show_download_bar ()
+
+ def onDownloadDone(self, widget, feed):
+ if feed is None:
+ self.downloadDialog.destroy()
+ self.downloadDialog = False
+ self.displayListing()
+
+ def late_init(self):
+ # Finish building the GUI.
menu = hildon.AppMenu()
# Create a button and add it to the menu
button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Update All Feeds")
+ button.set_label("Update feeds")
button.connect("clicked", self.button_update_clicked, "All")
menu.append(button)
button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Mark All As Read")
+ button.set_label("Mark all as read")
button.connect("clicked", self.button_markAll)
menu.append(button)
-
+
+ button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+ button.set_label("Add new feed")
+ button.connect("clicked", lambda b: self.addFeed())
+ menu.append(button)
+
button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Organize Feeds")
+ button.set_label("Manage subscriptions")
button.connect("clicked", self.button_organize_clicked)
menu.append(button)
button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Preferences")
+ button.set_label("Settings")
button.connect("clicked", self.button_preferences_clicked)
menu.append(button)
button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Import Feeds")
- button.connect("clicked", self.button_import_clicked)
- menu.append(button)
-
- button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
- button.set_label("Export Feeds")
- button.connect("clicked", self.button_export_clicked)
+ button.set_label("About")
+ button.connect("clicked", self.button_about_clicked)
menu.append(button)
self.window.set_app_menu(menu)
menu.show_all()
-
- #self.feedWindow = hildon.StackableWindow()
- #self.articleWindow = hildon.StackableWindow()
- self.displayListing()
+ # Initialize the DBus interface.
+ self.dbusHandler = ServerObject(self)
+ bus = dbus.SessionBus()
+ bus.add_signal_receiver(handler_function=self.update_progress,
+ bus_name=None,
+ signal_name='UpdateProgress',
+ dbus_interface='org.marcoz.feedingit',
+ path='/org/marcoz/feedingit/update')
+
+ # Check whether auto-update is enabled.
self.autoupdate = False
- self.checkAutoUpdate()
- hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
+ #self.checkAutoUpdate()
+
+ gobject.idle_add(self.build_feed_display)
+ gobject.idle_add(self.check_for_woodchuck)
+
+ def build_feed_display(self):
+ if not hasattr(self, 'disp'):
+ self.disp = DisplayFeed(self.listing, self.config)
+ self.disp.connect("feed-closed", self.onFeedClosed)
+
+ def check_for_woodchuck(self):
+ if self.config.getAskedAboutWoodchuck():
+ return
+
+ try:
+ import woodchuck
+ # It is already installed successfully.
+ self.config.setAskedAboutWoodchuck(True)
+ return
+ except ImportError:
+ pass
+
+ note = hildon.hildon_note_new_confirmation(
+ self.window,
+ "\nFeedingIt can use Woodchuck, a network transfer "
+ + "daemon, to schedule transfers more intelligently.\n"
+ + "You can also use the FeedingIt widget for time-based\n"
+ + "updates.\n\n"
+ + "Install Woodchuck?\n")
+ note.set_button_texts("Install", "Cancel")
+ note.add_button("Learn More", 42)
+
+ while True:
+ response = gtk.Dialog.run(note)
+ if response == 42:
+ open_in_browser("http://hssl.cs.jhu.edu/~neal/woodchuck")
+ continue
+
+ break
+
+ note.destroy()
+
+ if response == gtk.RESPONSE_OK:
+ open_in_browser("http://maemo.org/downloads/product/raw/Maemo5/murmeltier?get_installfile")
+ self.config.setAskedAboutWoodchuck(True)
def button_markAll(self, button):
for key in self.listing.getListOfFeeds():
feed = self.listing.getFeed(key)
- for index in range(feed.getNumberOfEntries()):
- feed.setEntryRead(index)
- self.refreshList()
+ feed.markAllAsRead()
+ #for id in feed.getIds():
+ # feed.setEntryRead(id)
+ self.listing.updateUnread(key)
+ self.displayListing()
+
+ def button_about_clicked(self, button):
+ HeAboutDialog.present(self.window, \
+ __appname__, \
+ ABOUT_ICON, \
+ __version__, \
+ __description__, \
+ ABOUT_COPYRIGHT, \
+ ABOUT_WEBSITE, \
+ ABOUT_BUGTRACKER, \
+ ABOUT_DONATE)
def button_export_clicked(self, button):
opml = ExportOpmlData(self.window, self.listing)
self.displayListing()
def addFeed(self, urlIn="http://"):
- wizard = AddWidgetWizard(self.window, urlIn)
+ wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
ret = wizard.run()
if ret == 2:
- (title, url) = wizard.getData()
- if (not title == '') and (not url == ''):
- self.listing.addFeed(title, url)
+ (title, url, category) = wizard.getData()
+ if url:
+ self.listing.addFeed(title, url, category=category)
wizard.destroy()
self.displayListing()
def button_organize_clicked(self, button):
- org = SortList(self.window, self.listing)
- org.run()
- org.destroy()
- self.listing.saveConfig()
- self.displayListing()
-
+ def after_closing():
+ self.displayListing()
+ SortList(self.window, self.listing, self, after_closing)
+
+ def do_update_feeds(self):
+ for k in self.listing.getListOfFeeds():
+ self.listing.updateFeed (k)
+
def button_update_clicked(self, button, key):
- if not type(self.downloadDialog).__name__=="DownloadBar":
- self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
- self.downloadDialog.connect("download-done", self.onDownloadsDone)
- self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
- self.mainVbox.show_all()
- #self.displayListing()
+ gobject.idle_add(self.do_update_feeds)
def onDownloadsDone(self, *widget):
self.downloadDialog.destroy()
self.downloadDialog = False
- #self.displayListing()
- self.refreshList()
+ self.displayListing()
def button_preferences_clicked(self, button):
dialog = self.config.createDialog()
else:
return False
+ def saveExpandedLines(self):
+ self.expandedLines = []
+ model = self.feedList.get_model()
+ model.foreach(self.checkLine)
+
+ def checkLine(self, model, path, iter, data = None):
+ if self.feedList.row_expanded(path):
+ self.expandedLines.append(path)
+
+ def restoreExpandedLines(self):
+ model = self.feedList.get_model()
+ model.foreach(self.restoreLine)
+
+ def restoreLine(self, model, path, iter, data = None):
+ if path in self.expandedLines:
+ self.feedList.expand_row(path, False)
+
def displayListing(self):
+ icon_theme = gtk.icon_theme_get_default()
+ default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
+ gtk.ICON_LOOKUP_USE_BUILTIN)
+
+ self.saveExpandedLines()
+
+ self.feedItems.clear()
+ hideReadFeed = self.config.getHideReadFeeds()
+ order = self.config.getFeedSortOrder()
+
+ categories = self.listing.getListOfCategories()
+ if len(categories) > 1:
+ showCategories = True
+ else:
+ showCategories = False
+
+ for categoryId in categories:
+
+ title = self.listing.getCategoryTitle(categoryId)
+ keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
+
+ if showCategories and len(keys)>0:
+ category = self.feedItems.append(None, (None, title, categoryId))
+ #print "catID" + str(categoryId) + " " + str(self.category)
+ if categoryId == self.category:
+ #print categoryId
+ expandedRow = category
+
+ for key in keys:
+ unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
+ title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
+ updateTime = self.listing.getFeedUpdateTime(key)
+ subtitle = '%s / %d unread items' % (updateTime, unreadItems)
+ if unreadItems:
+ markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
+ else:
+ markup = FEED_TEMPLATE % (title, subtitle)
+
+ try:
+ icon_filename = self.listing.getFavicon(key)
+ pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
+ LIST_ICON_SIZE, LIST_ICON_SIZE)
+ except:
+ pixbuf = default_pixbuf
+
+ if showCategories:
+ self.feedItems.append(category, (pixbuf, markup, key))
+ else:
+ self.feedItems.append(None, (pixbuf, markup, key))
+
+
+ self.restoreExpandedLines()
+ #try:
+
+ # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
+ #except:
+ # pass
+
+ def on_feedList_row_activated(self, treeview, path, column):
+ model = treeview.get_model()
+ iter = model.get_iter(path)
+ key = model.get_value(iter, COLUMN_KEY)
+
try:
- self.mainVbox.remove(self.pannableListing)
+ #print "Key: " + str(key)
+ catId = int(key)
+ self.category = catId
+ if treeview.row_expanded(path):
+ treeview.collapse_row(path)
+ #else:
+ # treeview.expand_row(path, True)
+ #treeview.collapse_all()
+ #treeview.expand_row(path, False)
+ #for i in range(len(path)):
+ # self.feedList.expand_row(path[:i+1], False)
+ #self.show_confirmation_note(self.window, "Working")
+ #return True
except:
- pass
- self.vboxListing = gtk.VBox(False,10)
- self.pannableListing = hildon.PannableArea()
- self.pannableListing.add_with_viewport(self.vboxListing)
-
- self.buttons = {}
- list = self.listing.getListOfFeeds()[:]
- #list.reverse()
- for key in list:
- #button = gtk.Button(item)
- button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
- hildon.BUTTON_ARRANGEMENT_VERTICAL)
- button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / "
- + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
- button.set_alignment(0,0,1,1)
- button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
- self.vboxListing.pack_start(button, expand=False)
- self.buttons[key] = button
+ if key:
+ self.openFeed(key)
- #if type(self.downloadDialog).__name__=="DownloadBar":
- # self.vboxListing.pack_start(self.downloadDialog)
- self.mainVbox.pack_start(self.pannableListing)
- self.window.show_all()
-
- def refreshList(self):
- for key in self.listing.getListOfFeeds():
- if self.buttons.has_key(key):
- button = self.buttons[key]
- button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / "
- + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
- else:
- self.displayListing()
- break
-
- def buttonFeedClicked(widget, button, self, window, key):
- disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key, self.config)
- disp.connect("feed-closed", self.onFeedClosed)
+ def openFeed(self, key):
+ if key != None:
+ self.build_feed_display()
+ self.disp.displayFeed(key)
+
+ def openArticle(self, key, id):
+ if key != None:
+ self.openFeed(key)
+ self.disp.button_clicked(None, id)
def onFeedClosed(self, object, key):
- #self.displayListing()
- self.listing.saveConfig()
- self.refreshList()
- #self.buttons[key].set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / "
- # + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
- #self.buttons[key].show()
-
+ gobject.idle_add(self.displayListing)
+
+ def quit(self, *args):
+ self.window.hide()
+ gtk.main_quit ()
+
def run(self):
- self.window.connect("destroy", gtk.main_quit)
+ self.window.connect("destroy", self.quit)
gtk.main()
- #for key in self.listing.getListOfFeeds():
- # self.listing.getFeed(key).saveFeed(CONFIGDIR)
- self.listing.saveConfig()
def prefsClosed(self, *widget):
- self.orientation.set_mode(self.config.getOrientation())
+ try:
+ self.orientation.set_mode(self.config.getOrientation())
+ except:
+ pass
+ self.displayListing()
self.checkAutoUpdate()
def checkAutoUpdate(self, *widget):
# Need to check for internet connection
# If no internet connection, try again in 10 minutes:
# gobject.timeout_add(int(5*3600000), self.automaticUpdate)
+ #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
+ #from time import localtime, strftime
+ #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
+ #file.close()
self.button_update_clicked(None, None)
return True
status = "No unread items"
return status
+ def grabFocus(self):
+ self.window.present()
+
if __name__ == "__main__":
+ mainthread.init ()
+ debugging.init(dot_directory=".feedingit", program_name="feedingit")
+
gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
- gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
- gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
+ gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
gobject.threads_init()
if not isdir(CONFIGDIR):
try:
mkdir(CONFIGDIR)
except:
- print "Error: Can't create configuration directory"
- sys.exit(1)
+ logger.error("Error: Can't create configuration directory")
+ from sys import exit
+ exit(1)
app = FeedingIt()
- dbusHandler = ServerObject(app)
app.run()