""" ItemView.py Module for displaying an item to the user """ __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc." __license__ = """ Straw is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Straw is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """ import os import re import urlparse from xml.sax import saxutils import codecs import pygtk pygtk.require('2.0') import gtk import gtk.glade import gtkhtml2 import utils import dialogs import MessageManager import FeedCategoryList import ImageCache import MVP import Config import error import dialogs class HTMLView(MVP.View): """ Widget: gtkhtml2.View Model: gtkhtml.Document Presenter: HTMLPresenter """ ui = """ """ def _initialize(self): self._widget.set_document(self._model) # Make the article view focusable. gtkhtml2.View scrolls to the first # link of the document which sometimes makes the first part of the # document unviewable if the first link is found at the bottom part of # the document. This is good for now since the template includes a # link at the topmost page of the view. self._widget.set_property('can-focus', True) self._widget.get_vadjustment().set_value(0) self._widget.connect('button_press_event', self._button_press_event) self._css = None self._url = None self._text_selection = None self._popup = None self._init_css() self._create_popup() def _connect_signals(self, widget): " We don't 'auto-connect' since self._widget is not a glade file" pass @property def widget(self): return self._widget def _button_press_event(self, widget, event): if event.button == 1: if self._url: self._presenter.display_url(self._url) self._url = self._text_selection = None elif event.button == 3: self._text_selection = self._presenter.get_html_text_selection() action_group = self._uimanager.get_action_groups()[-1] link_action = action_group.get_action('copy_link_location') open_link_action = action_group.get_action('open_link_location') text_action = action_group.get_action('copy_text') subs_action = action_group.get_action('subscribe') if self._url and self._text_selection: link_action.set_visible(True) open_link_action.set_visible(True) subs_action.set_visible(True) text_action.set_visible(True) elif self._url: text_action.set_visible(False) link_action.set_visible(True) open_link_action.set_visible(True) subs_action.set_visible(True) elif self._text_selection: link_action.set_visible(False) open_link_action.set_visible(False) subs_action.set_visible(False) text_action.set_visible(True) else: link_action.set_visible(False) open_link_action.set_visible(False) subs_action.set_visible(False) text_action.set_visible(False) self._uimanager.ensure_update() self._popup.popup(None, None, None, event.button, gtk.get_current_event_time()) return True def _create_popup(self): actions = [ ("copy_text", gtk.STOCK_COPY, "_Copy", None, None, self._on_copy_text), ("copy_link_location", None, "_Copy Link Location", None, None, self._on_copy_url), ("open_link_location", None, "_Open Link", None, None, self._open_link_location), ("subscribe", None, "_Subscribe", None, None, self._subscribe), ("zoom_in", gtk.STOCK_ZOOM_IN, "Zoom _In", None, None, lambda *args: self._on_magnify("in")), ("zoom_out", gtk.STOCK_ZOOM_OUT, "Zoom _Out", None, None, lambda *args: self._on_magnify("out")), ("zoom_100", gtk.STOCK_ZOOM_100, "_Normal Size", None, None, lambda *args: self._on_magnify("reset")) ] self._uimanager = gtk.UIManager() actiongroup = gtk.ActionGroup("HtmlViewActions") actiongroup.add_actions(actions) self._uimanager.insert_action_group(actiongroup, -1) self._uimanager.add_ui_from_string(self.ui) self._popup = self._uimanager.get_widget("/htmlview_popup") def _on_magnify(self, action): if action == "in": self._widget.zoom_in() elif action == "out": self._widget.zoom_out() else: self._widget.zoom_reset() config = Config.get_instance() config.text_magnification = self._widget.get_magnification() def _on_copy_text(self, *args): self._presenter.set_clipboard_text(self._text_selection) gtkhtml2.html_selection_clear(self._widget) return def _on_copy_url(self, *args): self._presenter.set_clipboard_text(self._url) return def _open_link_location(self, *args): utils.url_show(self._url) def _subscribe(self, *args): import subscribe subscribe.show(url=self._url) return def _init_css(self): if self._css is None: css = file(os.path.join( utils.find_data_dir(), "straw.css")).read() # derive colors for blockquotes and header boxes from # the GTK+ theme # the GTK+ colors are in the range 0-65535 bgdivisor = int(65535/(9.0/10)) fgdivisor = 65535 borderdivisor = int(65535/(6.0/10)) gtkstyle = self._widget.get_style() headerbg = "background-color: #%.2x%.2x%.2x;" % ( (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / bgdivisor, (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / bgdivisor, (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / bgdivisor) headerfg = "color: #%.2x%.2x%.2x;" % ( (gtkstyle.fg[gtk.STATE_NORMAL].red * 255) / fgdivisor, (gtkstyle.fg[gtk.STATE_NORMAL].blue * 255) / fgdivisor, (gtkstyle.fg[gtk.STATE_NORMAL].green * 255) / fgdivisor) headerborder = "border-color: #%.2x%.2x%.2x;" % ( (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / borderdivisor, (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / borderdivisor, (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / borderdivisor) css = re.sub(r"/\*\*\*HEADERBG\*/", headerbg, css) css = re.sub(r"/\*\*\*HEADERFG\*/", headerfg, css) css = re.sub(r"/\*\*\*HEADERBORDERCOLOR\*/", headerborder, css) css = re.sub(r"/\*\*\*BQUOTEBG\*/", headerbg, css) css = re.sub(r"/\*\*\*BQUOTEFG\*/", headerfg, css) css = re.sub(r"/\*\*\*BQUOTEBORDERCOLOR\*/", headerborder, css) self._css = css return def report_error(self, title, description): dialogs.report_error(title, description) def get_css(self): return self._css def get_adjustments(self): return (self._widget.get_vadjustment(), self._widget.get_hadjustment()) def get_widget(self): return self._widget def connect_widget_signal(self, signal, callback): self._widget.connect(signal, callback) def set_on_url(self, url): self._url = url def set_magnification(self, size): self._widget.set_magnification(size) class HTMLPresenter(MVP.BasicPresenter): """ Model: gtkhtml2.Document View: HTMLView """ def _initialize(self): self._model.connect('request-url', self._request_url) self._view.connect_widget_signal('on_url', self._on_url) self._item = None def _on_url(self, view, url): self._view.set_on_url(url) if url: url = utils.complete_url(url, self._item.feed.location) else: url = "" mmgr = MessageManager.get_instance() mmgr.post_message(url) return def _request_url(self, document, url, stream): feed = self._item.feed try: try: url = utils.complete_url(url, self._item.feed.location) if urlparse.urlparse(url)[0] == 'file': # local URL, don't use the cache. f = file(urlparse.urlparse(url)[2]) stream.write(f.read()) f.close() else: image = ImageCache.cache[url] stream.write(image.get_data()) except Exception, ex: error.log("Error reading image in %s: %s" % (url, ex)) finally: stream.close() stream = None return def set_clipboard_text(self, text): utils.set_clipboard_text(text) def get_html_text_selection(self): return gtkhtml2.html_selection_get_text(self.view.widget) def display_url(self, link): link = link.strip() link = utils.complete_url(link, self._item.feed.location) try: utils.url_show(link) except Exception, ex: self._view.report_error(_("Error Loading Browser"), _("Please check your browser settings and try again.")) return def get_view_adjustments(self): return self._view.get_adjustments() def get_view_widget(self): return self._view.get_widget() def display_item(self, item, encoding): self._item = item content = self._htmlify_item(item, encoding) self._prepare_stream(content) return def display_empty_feed(self): content = """

"""# _("No data yet, need to poll first.")

""" self._prepare_stream(content) def display_empty_search(self): content = """

Search Subscriptions

Begin searching by typing your text on the text box on the left side.

""" self._prepare_stream(content) return def set_view_magnification(self, size): self.view.set_magnification(size) def _encode_for_html(self, unicode_data, encoding='utf-8'): """ From Python Cookbook, 2/ed, section 1.23 'html_replace' is in the utils module """ return unicode_data.encode(encoding, 'html_replace') def _prepare_stream(self, content): html = self._generate_html(content) html = self._encode_for_html(html) self._model.clear() self._model.open_stream("text/html") self._model.write_stream(html) self._model.close_stream() return def _generate_html(self, body): # heading html = """ title """ # stylesheet if Config.get_instance().reload_css: html += """""" else: html += """""" # body html += "%s" % body return html def _htmlify_item(self, item, encoding): feed = item.feed ret = [] # item header ret.append('
') if item.title is not None: if item.link is not None: ret.append('
%s
' % (item.link,item.title)) else: ret.append(item.title) ret.append('') if item.pub_date is not None: timestr = utils.format_date( item.pub_date, utils.get_date_format(), encoding) ret.append(''.join(('' % str(timestr)))) ret.append('
%s
') ret.append('
') # item body if item.description is not None: item.description.replace('\n', '
') ret.append('
%s
' % item.description) if item.publication_name is not None: ret.append('
') ret.append('%s: %s
' % (_("Publication"), item.publication_name)) if item.publication_volume is not None: ret.append('%s: %s ' % (_("Volume"), item.publication_volume)) if item.publication_number is not None: ret.append('( %s )
' % item.publication_number) if item.publication_section is not None: ret.append('%s: %s
' % (_("Section"), item.publication_section)) if item.publication_starting_page is not None: ret.append('%s: %s' % (_("Starting Page"), item.publication_starting_page)) ret.append('
') # freshmeat fields freshmeat_data = [] if item.fm_license != '' and item.fm_license is not None: freshmeat_data.append('

%s: %s

' % (_("Software license"), item.fm_license)) if item.fm_changes != '' and item.fm_changes is not None: freshmeat_data.append('

%s: %s

' % (_("Changes"), item.fm_changes)) if len(freshmeat_data) > 0: ret.append('
') ret.extend(freshmeat_data) ret.append('
') # empty paragraph to make sure that we get space here ret.append('

') # Additional information dcret = [] # RSS Enclosures if item.enclosures: dcret.append('%s' % _("Enclosed Media")) for enc in item.enclosures: # rss 2.0 defines only one enclosure per item size = int(enc.length) unit = _('bytes') if size > 1024: unit = _('KB') size /= 1024.0 if size > 1024: unit = _('MB') size /= 1024.0 link_text = enc['href'].split('/')[-1] # find what kind of media is that. enc[type] will have something like audio/mp3 or video/mpeg (mimetypes) # some symlinks are not present on the tango icon theme mimetype dir. audio and application are 2 good examples. So I am not relying on the symlinks now... kind = enc['type'].split('/')[0] if kind == 'audio': icon_name = 'audio-x-generic' elif kind == 'video': icon_name = 'video-x-generic' elif kind == 'image': icon_name = 'image-x-generic' elif kind == 'application': icon_name = 'binary' elif kind == 'text': icon_name = 'text-x-generic' else: icon_name = "unknown" it = gtk.icon_theme_get_default() ii = it.lookup_icon(icon_name, 32, gtk.ICON_LOOKUP_NO_SVG) imgsrc = 'file://' + ii.get_filename() dcret.append('' % (enc['href'], imgsrc, link_text, size, unit, enc['type'])) dcret.append('
%s (%.2f %s - %s)
') if item.creator is not None: dcret.append('%s%s' % (_("Posted by"), item.creator)) if item.contributors is not None and len(item.contributors): for c in item.contributors: dcret.append('%s%s' \ % (_("Contributor:"), c.name)) if item.source: url = utils.get_url_location(item.source['url']) text = saxutils.escape(url) dcret.append('%s%s' % (_("Item Source"), url, text)) if item.guid is not None and item.guid != "" and item.guidislink: dcret.append('%s%s' % (_("Permalink"), item.guid, item.guid)) # check for not guidislink for the case where there is guid but # isPermalink="false" and yet link is the same as guid (link is # always assumed to be a valid link) if item.link != "" and item.link is not None and (item.link != item.guid or not item.guidislink): dcret.append('%s%s' % (_("Complete story"), item.link, item.link)) if item.license_urls: for l in item.license_urls: if l: dcret.append('%s%s' % (_("License"), l, l)) if len(dcret): ret.append('
%s' % _("Additional information")) ret.append("".join(dcret)) ret.append('') ret.append('
') return "".join(ret) class ScrollView(MVP.WidgetView): """ Widget: html_scrolled_window """ def set_adjustments(self, vadjustment, hadjustment): self._widget.set_hadjustment(hadjustment) self._widget.set_vadjustment(vadjustment) return def add_child(self, widget): self._widget.add(widget) return def show(self): self._widget.show_all() return def adjust_vertical_adjustment(self): va = self._widget.get_vadjustment() va.set_value(va.lower) return def get_vadjustment(self): return self._widget.get_vadjustment() class ScrollPresenter(MVP.BasicPresenter): """ View: ScrollView """ def set_view_adjustments(self, vadjustment, hadjustment): self._view.set_adjustments(vadjustment, hadjustment) return def update_view(self): self._view.adjust_vertical_adjustment() return def scroll_down(self): va = self._view.get_vadjustment() old_value = va.get_value() new_value = old_value + va.page_increment limit = va.upper - va.page_size if new_value > limit: new_value = limit va.set_value(new_value) return new_value > old_value def show_view(self): self._view.show() return class ItemView: def __init__(self, item_view_container): self._encoding = utils.get_locale_encoding() widget_tree = gtk.glade.get_widget_tree(item_view_container) document = gtkhtml2.Document() widget = gtkhtml2.View() html_view = HTMLView(widget, document) self._html_presenter = HTMLPresenter(document, html_view) widget = widget_tree.get_widget('html_scrolled_window') scroll_view = ScrollView(widget) self._scroll_presenter = ScrollPresenter(view=scroll_view) vadj, hadj = self._html_presenter.get_view_adjustments() child = self._html_presenter.get_view_widget() self._scroll_presenter.set_view_adjustments(vadj, hadj) self._scroll_presenter.view.add_child(child) self._scroll_presenter.show_view() config = Config.get_instance() self._html_presenter.set_view_magnification(config.text_magnification) def item_selection_changed(self, signal): if signal.item: self._display_item(signal.item) def _display_item(self, item): self._html_presenter.display_item(item, self._encoding) self._scroll_presenter.update_view() def display_empty_feed(self): self._html_presenter.display_empty_feed() def display_empty_search(self): self._html_presenter.display_empty_search() def scroll_down(self): return self._scroll_presenter.scroll_down() def get_selected_text(self): return self._html_presenter.get_html_text_selection()