""" 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 = """
<ui>
<popup name=\"htmlview_popup\">
<menuitem action=\"open_link_location\"/>
<menuitem action=\"copy_link_location\"/>
<menuitem action=\"copy_text\"/>
<menuitem action=\"subscribe\"/>
<menuitem action=\"zoom_in\"/>
<menuitem action=\"zoom_out\"/>
<menuitem action=\"zoom_100\"/>
</popup>
</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 = """<p class=\"emptyfeed\"/>"""# _("No data yet, need to poll first.") </p>"""
self._prepare_stream(content)
def display_empty_search(self):
content = """
<h2>Search Subscriptions</h2>
<p>
Begin searching by typing your text on the text box on the left side.
</p>
"""
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 = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head><title>title</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
# stylesheet
if Config.get_instance().reload_css:
html += """<link rel="stylesheet" type="text/css" href="file://"
""" + os.path.join(utils.find_data_dir(), "straw.css") + """/>"""
else:
html += """<style type="text/css">""" + self._view.get_css() + """</style>"""
# body
html += "</head><body>%s</body></html>" % body
return html
def _htmlify_item(self, item, encoding):
feed = item.feed
ret = []
# item header
ret.append('<div id="itemheader">')
if item.title is not None:
if item.link is not None:
ret.append('<div class="title"><a href="%s">%s</a></div>' % (item.link,item.title))
else:
ret.append(item.title)
ret.append('<table id="itemhead" cellspacing="0" cellpadding="0">')
if item.pub_date is not None:
timestr = utils.format_date(
item.pub_date, utils.get_date_format(), encoding)
ret.append(''.join(('<tr><td class="headleft" id="date">%s</td><td class="headright"></td></tr>' % str(timestr))))
ret.append('</table>')
ret.append('</div>')
# item body
if item.description is not None:
item.description.replace('\n', '<br/>')
ret.append('<div class="description">%s</div>' % item.description)
if item.publication_name is not None:
ret.append('<div class="description">')
ret.append('<b>%s:</b> %s<br/>' % (_("Publication"),
item.publication_name))
if item.publication_volume is not None:
ret.append('<b>%s:</b> %s ' % (_("Volume"),
item.publication_volume))
if item.publication_number is not None:
ret.append('( %s )<br />' % item.publication_number)
if item.publication_section is not None:
ret.append('<b>%s:</b> %s<br />' % (_("Section"),
item.publication_section))
if item.publication_starting_page is not None:
ret.append('<b>%s:</b> %s' % (_("Starting Page"),
item.publication_starting_page))
ret.append('</div>')
# freshmeat fields
freshmeat_data = []
if item.fm_license != '' and item.fm_license is not None:
freshmeat_data.append('<p><b>%s:</b> %s</p>' %
(_("Software license"), item.fm_license))
if item.fm_changes != '' and item.fm_changes is not None:
freshmeat_data.append('<p><b>%s:</b> %s</p>' %
(_("Changes"), item.fm_changes))
if len(freshmeat_data) > 0:
ret.append('<div class="description">')
ret.extend(freshmeat_data)
ret.append('</div>')
# empty paragraph to make sure that we get space here
ret.append('<p></p>')
# Additional information
dcret = []
# RSS Enclosures
if item.enclosures:
dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><table>' % _("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('<tr><td><div style="vertical-align: middle"><a class="dclink" href="%s" style="vertical-align: middle"><img style="padding: 0px 15px 5px 0px" src=%s /> %s</a> (%.2f %s - %s)</div></td></tr>' % (enc['href'], imgsrc, link_text, size, unit, enc['type']))
dcret.append('</table></td></tr>')
if item.creator is not None:
dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' % (_("Posted by"), item.creator))
if item.contributors is not None and len(item.contributors):
for c in item.contributors:
dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
% (_("Contributor:"), c.name))
if item.source:
url = utils.get_url_location(item.source['url'])
text = saxutils.escape(url)
dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><a class="dclink" href="%s"><span>%s</span></a></td></tr>' %
(_("Item Source"), url, text))
if item.guid is not None and item.guid != "" and item.guidislink:
dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><a class="dclink" href="%s"><span>%s</span></a></td></tr>' % (_("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('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><a class="dclink" href="%s"><span>%s</span></a></td></tr>' %
(_("Complete story"), item.link, item.link))
if item.license_urls:
for l in item.license_urls:
if l:
dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><a class="dclink" href="%s"><span>%s</span></a></td></tr>' % (_("License"), l, l))
if len(dcret):
ret.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
ret.append("".join(dcret))
ret.append('</table>')
ret.append('</div>')
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()
syntax highlighted by Code2HTML, v. 0.9.1