Cleaned up item view.
[straw.git] / straw / ItemView.py
blob0b88e849fda2fe248b615ba5ef9b72b6f380795b
1 """ ItemView.py
3 Module for displaying an item to the user
4 """
5 __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
6 __license__ = """
7 Straw is free software; you can redistribute it and/or modify it under the
8 terms of the GNU General Public License as published by the Free Software
9 Foundation; either version 2 of the License, or (at your option) any later
10 version.
12 Straw is distributed in the hope that it will be useful, but WITHOUT ANY
13 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License along with
17 this program; if not, write to the Free Software Foundation, Inc., 59 Temple
18 Place - Suite 330, Boston, MA 02111-1307, USA. """
20 from Constants import *
21 from MessageManager import post_status_message
22 from straw import helpers
23 from xml.sax import saxutils
24 import Config
25 import ImageCache
26 import MVP
27 import codecs
28 import error
29 import gtk
30 import gtk.glade
31 import os
32 import pygtk
33 import re
34 import straw.defs
35 import urlparse
36 pygtk.require('2.0')
38 log = error.get_logger()
40 class HTMLView(MVP.View):
42 def _initialize(self):
43 # self._widget.set_document(self._model)
44 # Make the article view focusable. gtkhtml2.View scrolls to the first
45 # link of the document which sometimes makes the first part of the
46 # document unviewable if the first link is found at the bottom part of
47 # the document. This is good for now since the template includes a
48 # link at the topmost page of the view.
49 self._widget.set_property('can-focus', True)
50 # self._widget.get_vadjustment().set_value(0)
51 self._widget.connect('button_press_event', self._button_press_event)
52 self._css = None
53 self._url = None
54 self._text_selection = None
55 self._init_css()
56 self.uifactory = helpers.UIFactory('HtmlViewActions')
57 action = self.uifactory.get_action('/htmlview_popup/text_copy')
58 action.connect('activate', self.on_copy_text_cb)
59 action = self.uifactory.get_action('/htmlview_popup/link_copy')
60 action.connect('activate', self.on_copy_url_cb)
61 action = self.uifactory.get_action('/htmlview_popup/link_open')
62 action.connect('activate', self.on_open_link_location_cb)
63 action = self.uifactory.get_action('/htmlview_popup/subscribe')
64 action.connect('activate', self.on_subscribe_cb)
65 action = self.uifactory.get_action('/htmlview_popup/zoom_in')
66 action.connect('activate', lambda *args: self.on_magnify('in'))
67 action = self.uifactory.get_action('/htmlview_popup/zoom_out')
68 action.connect('activate', lambda *args: self.on_magnify('out'))
69 action = self.uifactory.get_action('/htmlview_popup/zoom_100')
70 action.connect('activate', lambda *args: self.on_magnify('reset'))
71 self.popup = self.uifactory.get_popup('/htmlview_popup')
73 @property
74 def widget(self):
75 return self._widget
77 def _connect_signals(self, *args): pass
79 def _button_press_event(self, widget, event):
80 if event.button == 1:
81 if self._url:
82 self._presenter.display_url(self._url)
83 self._url = self._text_selection = None
84 elif event.button == 3:
85 self._text_selection = self._presenter.get_html_text_selection()
86 link_action = self.uifactory.get_action('/htmlview_popup/link_copy')
87 open_link_action = self.uifactory.get_action('/htmlview_popup/link_open')
88 subs_action = self.uifactory.get_action('/htmlview_popup/subscribe')
89 text_action = self.uifactory.get_action('/htmlview_popup/text_copy')
90 if self._url and self._text_selection:
91 link_action.set_sensitive(True)
92 open_link_action.set_sensitive(True)
93 subs_action.set_sensitive(True)
94 text_action.set_sensitive(True)
95 elif self._url:
96 text_action.set_sensitive(False)
97 link_action.set_sensitive(True)
98 open_link_action.set_sensitive(True)
99 subs_action.set_sensitive(True)
100 elif self._text_selection:
101 link_action.set_sensitive(False)
102 open_link_action.set_sensitive(False)
103 subs_action.set_sensitive(False)
104 text_action.set_sensitive(True)
105 else:
106 link_action.set_sensitive(False)
107 open_link_action.set_sensitive(False)
108 subs_action.set_sensitive(False)
109 text_action.set_sensitive(False)
110 self.uifactory.ensure_update()
111 self.popup.popup(None, None, None, event.button,
112 gtk.get_current_event_time())
113 return True
115 def on_magnify(self, action):
116 if action == "in":
117 self._widget.zoom_in()
118 elif action == "out":
119 self._widget.zoom_out()
120 else:
121 self._widget.zoom_reset()
123 # TODO: use text magnification
124 #config.text_magnification = self._widget.get_magnification()
126 def on_copy_text_cb(self, *args):
127 print args
128 self._presenter.set_clipboard_text(self._text_selection)
130 def on_copy_url_cb(self, *args):
131 self._presenter.set_clipboard_text(self._url)
133 def on_open_link_location_cb(self, *args):
134 helpers.url_show(self._url)
136 def on_subscribe_cb(self, *args):
137 import subscribe
138 subscribe.show(url=self._url)
140 def _init_css(self):
141 if self._css is None:
142 css = file(os.path.join(straw.defs.STRAW_DATA_DIR, "straw.css")).read()
143 # derive colors for blockquotes and header boxes from
144 # the GTK+ theme
145 # the GTK+ colors are in the range 0-65535
146 bgdivisor = int(65535/(9.0/10))
147 fgdivisor = 65535
148 borderdivisor = int(65535/(6.0/10))
149 gtkstyle = self._widget.get_style()
151 headerbg = "background-color: #%.2x%.2x%.2x;" % (
152 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / bgdivisor,
153 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / bgdivisor,
154 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / bgdivisor)
156 headerfg = "color: #%.2x%.2x%.2x;" % (
157 (gtkstyle.fg[gtk.STATE_NORMAL].red * 255) / fgdivisor,
158 (gtkstyle.fg[gtk.STATE_NORMAL].blue * 255) / fgdivisor,
159 (gtkstyle.fg[gtk.STATE_NORMAL].green * 255) / fgdivisor)
161 headerborder = "border-color: #%.2x%.2x%.2x;" % (
162 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / borderdivisor,
163 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / borderdivisor,
164 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / borderdivisor)
167 css = re.sub(r"/\*\*\*HEADERBG\*/", headerbg, css)
168 css = re.sub(r"/\*\*\*HEADERFG\*/", headerfg, css)
169 css = re.sub(r"/\*\*\*HEADERBORDERCOLOR\*/",
170 headerborder, css)
172 css = re.sub(r"/\*\*\*BQUOTEBG\*/", headerbg, css)
173 css = re.sub(r"/\*\*\*BQUOTEFG\*/", headerfg, css)
174 css = re.sub(r"/\*\*\*BQUOTEBORDERCOLOR\*/",
175 headerborder, css)
176 self._css = css
177 return
179 def report_error(self, title, description):
180 helpers.report_error(title, description)
182 def get_css(self):
183 return self._css
185 def get_adjustments(self):
186 return (self._widget.get_vadjustment(), self._widget.get_hadjustment())
188 def get_widget(self):
189 return self._widget
191 def connect_widget_signal(self, signal, callback):
192 self._widget.connect(signal, callback)
194 def set_on_url(self, url):
195 self._url = url
197 def set_magnification(self, size):
198 # self._widget.set_magnification(size)
199 pass
201 class HTMLPresenter(MVP.BasicPresenter):
203 Model: gtkhtml2.Document
204 View: HTMLView
206 def _initialize(self):
207 # self._model.connect('request-url', self._request_url)
208 # self._view.connect_widget_signal('on_url', self._on_url)
209 self._model.connect('status_changed', self._status_changed)
210 self._model.connect('open_uri', self._open_uri)
211 self._item = None
213 def _open_uri(self, document, url):
214 try:
215 self.display_url(url)
216 finally:
217 return True
219 def _status_changed(self, document, status):
220 post_status_message(status)
222 def _on_url(self, view, url):
223 self._view.set_on_url(url)
224 if url:
225 url = helpers.complete_url(url, self._item.feed.location)
226 else:
227 url = ""
228 post_status_message(url)
229 return
231 def _request_url(self, document, url, stream):
232 feed = self._item.feed
233 try:
234 try:
235 url = helpers.complete_url(url, self._item.feed.location)
236 if urlparse.urlparse(url)[0] == 'file':
237 # local URL, don't use the cache.
238 f = file(urlparse.urlparse(url)[2])
239 stream.write(f.read())
240 f.close()
241 else:
242 image = ImageCache.cache[url]
243 stream.write(image.get_data())
244 except Exception, ex:
245 log.error("Error reading image in %s: %s" % (url, ex))
246 finally:
247 stream.close()
248 stream = None
249 return
251 def set_clipboard_text(self, text):
252 helpers.set_clipboard_text(text)
254 def get_html_text_selection(self):
255 pass
257 def display_url(self, link):
258 link = link.strip()
259 link = helpers.complete_url(link, self._item.feed.location)
260 try:
261 helpers.url_show(link)
262 except Exception, ex:
263 print ex
264 self._view.report_error(_("Error Loading Browser"),
265 _("Please check your browser settings and try again."))
266 return
268 def get_view_adjustments(self):
269 return self._view.get_adjustments()
271 def get_view_widget(self):
272 return self._view.get_widget()
274 def display_item(self, item, encoding):
275 self._item = item
276 content = self._htmlify_item(item, encoding)
277 self._prepare_stream(content)
278 return
280 def display_empty_feed(self):
281 content = """<p class=\"emptyfeed\"/>"""# _("No data yet, need to poll first.") </p>"""
282 self._prepare_stream(content)
284 def display_empty_search(self):
285 content = """
286 <h2>Search Subscriptions</h2>
288 Begin searching by typing your text on the text box on the left side.
289 </p>
291 self._prepare_stream(content)
292 return
294 def set_view_magnification(self, size):
295 self.view.set_magnification(size)
297 def _encode_for_html(self, unicode_data, encoding='utf-8'):
298 """ From Python Cookbook, 2/ed, section 1.23
299 'html_replace' is in the utils module
301 return unicode_data.encode(encoding, 'html_replace')
303 def _prepare_stream(self, content):
304 html = self._generate_html(content)
305 html = self._encode_for_html(html)
306 # self._model.clear()
307 # self._model.open_stream("text/html")
308 # self._model.write_stream(html)
309 # self._model.close_stream()
310 self._model.render_data(html, self._item.feed.location, "text/html")
311 return
313 def _generate_html(self, body):
314 # heading
315 html = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
316 <html>
317 <head><title>title</title>
318 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
320 # stylesheet
321 if False:#Config.get_instance().reload_css:
322 html += """<link rel="stylesheet" type="text/css" href="file://"
323 """ + os.path.join(straw.defs.STRAW_DATA_DIR, "straw.css") + """/>"""
324 else:
325 html += """<style type="text/css">""" + self._view.get_css() + """</style>"""
327 # body
328 html += "</head><body>%s</body></html>" % body
329 return html
331 def _not_empty(self, text):
332 return text is not None and len(text) > 0
334 def _htmlify_item(self, item, encoding):
335 ret = []
337 # item header
338 ret.append('<div id="itemheader">')
339 if item.title is not None:
340 if item.link is not None:
341 ret.append('<div class="title"><a href="%s">%s</a></div>' % (item.link,item.title))
342 else:
343 ret.append(item.title)
344 ret.append('<table id="itemhead" cellspacing="0" cellpadding="0">')
345 if item.pub_date is not None:
346 timestr = helpers.format_date(
347 item.pub_date, helpers.get_date_format(), encoding)
348 ret.append(''.join(('<tr><td class="headleft" id="date">%s</td><td class="headright"></td></tr>' % str(timestr))))
350 ret.append('</table>')
351 ret.append('</div>')
353 # item body
354 if self._not_empty(item.description):
355 item.description.replace('\n', '<br/>')
356 ret.append('<div class="description">%s</div>' % item.description)
358 if self._not_empty(item.publication_name):
359 ret.append('<div class="description">')
360 ret.append('<b>%s:</b> %s<br/>' % (_("Publication"),
361 item.publication_name))
362 if item.publication_volume is not None:
363 ret.append('<b>%s:</b> %s ' % (_("Volume"),
364 item.publication_volume))
365 if item.publication_number is not None:
366 ret.append('( %s )<br />' % item.publication_number)
367 if item.publication_section is not None:
368 ret.append('<b>%s:</b> %s<br />' % (_("Section"),
369 item.publication_section))
370 if item.publication_starting_page is not None:
371 ret.append('<b>%s:</b> %s' % (_("Starting Page"),
372 item.publication_starting_page))
373 ret.append('</div>')
375 # freshmeat fields
376 freshmeat_data = []
377 if item.fm_license != '' and item.fm_license is not None:
378 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
379 (_("Software license"), item.fm_license))
380 if item.fm_changes != '' and item.fm_changes is not None:
381 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
382 (_("Changes"), item.fm_changes))
383 if len(freshmeat_data) > 0:
384 ret.append('<div class="description">')
385 ret.extend(freshmeat_data)
386 ret.append('</div>')
387 # empty paragraph to make sure that we get space here
388 ret.append('<p></p>')
389 # Additional information
390 dcret = []
392 # RSS Enclosures
394 if item.enclosures:
395 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><table>' % _("Enclosed Media"))
396 for enc in item.enclosures: # rss 2.0 defines only one enclosure per item
397 size = int(enc.length)
398 unit = _('bytes')
399 if size > 1024:
400 unit = _('KB')
401 size /= 1024.0
402 if size > 1024:
403 unit = _('MB')
404 size /= 1024.0
405 link_text = enc['href'].split('/')[-1]
407 # find what kind of media is that. enc[type] will have something like audio/mp3 or video/mpeg (mimetypes)
408 # 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...
409 kind = enc['type'].split('/')[0]
410 if kind == 'audio':
411 icon_name = 'audio-x-generic'
412 elif kind == 'video':
413 icon_name = 'video-x-generic'
414 elif kind == 'image':
415 icon_name = 'image-x-generic'
416 elif kind == 'application':
417 icon_name = 'binary'
418 elif kind == 'text':
419 icon_name = 'text-x-generic'
420 else:
421 icon_name = "unknown"
423 it = gtk.icon_theme_get_default()
424 ii = it.lookup_icon(icon_name, 32, gtk.ICON_LOOKUP_NO_SVG)
425 if ii:
426 imgsrc = 'file://' + ii.get_filename()
427 else:
428 imgsrc = "file://%s/%s" % (straw.defs.STRAW_DATA_DIR, 'image-missing.svg')
429 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']))
430 dcret.append('</table></td></tr>')
432 if self._not_empty(item.creator):
433 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))
434 if item.contributors is not None and len(item.contributors):
435 for c in item.contributors:
436 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
437 % (_("Contributor:"), c.name))
438 if item.source:
439 url = helpers.get_url_location(item.source['url'])
440 text = saxutils.escape(url)
441 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>' %
442 (_("Item Source"), url, text))
444 if self._not_empty(item.guid) and item.guidislink:
445 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))
446 # check for not guidislink for the case where there is guid but
447 # isPermalink="false" and yet link is the same as guid (link is
448 # always assumed to be a valid link)
449 if self._not_empty(item.link) and (item.link != item.guid or not item.guidislink):
450 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>' %
451 (_("Complete story"), item.link, item.link))
453 if item.license_urls:
454 for l in item.license_urls:
455 if l:
456 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))
458 if len(dcret):
459 ret.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
460 ret.append("".join(dcret))
461 ret.append('</table>')
462 ret.append('</div>')
463 return "".join(ret)
465 class ScrollView(MVP.WidgetView):
467 Widget: html_scrolled_window
469 def set_adjustments(self, vadjustment, hadjustment):
470 self._widget.set_hadjustment(hadjustment)
471 self._widget.set_vadjustment(vadjustment)
472 return
474 def add_child(self, widget):
475 self._widget.add(widget)
476 return
478 def show(self):
479 self._widget.show_all()
480 return
482 def adjust_vertical_adjustment(self):
483 va = self._widget.get_vadjustment()
484 va.set_value(va.lower)
485 return
487 def get_vadjustment(self):
488 return self._widget.get_vadjustment()
490 class ScrollPresenter(MVP.BasicPresenter):
492 View: ScrollView
494 def set_view_adjustments(self, vadjustment, hadjustment):
495 self._view.set_adjustments(vadjustment, hadjustment)
496 return
498 def update_view(self):
499 self._view.adjust_vertical_adjustment()
500 return
502 def scroll_down(self):
503 va = self._view.get_vadjustment()
504 old_value = va.get_value()
505 new_value = old_value + va.page_increment
506 limit = va.upper - va.page_size
507 if new_value > limit:
508 new_value = limit
509 va.set_value(new_value)
510 return new_value > old_value
512 def show_view(self):
513 self._view.show()
514 return
516 class ItemView:
517 def __init__(self, item_view_container):
518 self._encoding = helpers.get_locale_encoding()
519 widget_tree = gtk.glade.get_widget_tree(item_view_container)
520 # document = gtkhtml2.Document()
521 # widget = gtkhtml2.View()
522 import documentviews
523 engine_name = os.getenv('STRAW_HTML')
525 log.debug("STRAW_HTML = %s" % engine_name)
527 if engine_name:
528 engine = documentviews.create_documentviews[engine_name]
529 else:
530 engine = documentviews.default_documentview
532 document = engine(Config.straw_home())
534 log.debug("document = %s" % document)
536 widget = document.widget()
537 html_view = HTMLView(widget, document)
538 self._html_presenter = HTMLPresenter(document, html_view)
540 widget = widget_tree.get_widget('html_scrolled_window')
541 parent = widget.parent
542 parent.remove(widget)
543 widget = gtk.Frame()
544 widget.set_shadow_type(gtk.SHADOW_IN)
545 parent.add(widget)
547 # scroll_view = ScrollView(widget)
548 # self._scroll_presenter = ScrollPresenter(view=scroll_view)
550 # vadj, hadj = self._html_presenter.get_view_adjustments()
551 child = self._html_presenter.get_view_widget()
552 # self._scroll_presenter.set_view_adjustments(vadj, hadj)
553 # self._scroll_presenter.view.add_child(child)
554 # self._scroll_presenter.show_view()
556 widget.add(child)
557 # gtkmozembed visibility workaround
558 child.show()
559 widget.show()
560 # gtkhtml visibility workaround
561 widget.show_all()
563 #config = Config.get_instance()
564 #elf._html_presenter.set_view_magnification(config.text_magnification)
566 def itemlist_selection_changed(self, selection, column):
567 (model, treeiter) = selection.get_selected()
568 if not treeiter: return # .. or display a template page?
569 item = model.get_value(treeiter, column)
570 self._display_item(item)
572 def _display_item(self, item):
573 self._html_presenter.display_item(item, self._encoding)
574 # self._scroll_presenter.update_view()
575 item.set_property("is_read", True)
577 def display_empty_feed(self):
578 self._html_presenter.display_empty_feed()
580 def display_empty_search(self):
581 self._html_presenter.display_empty_search()
583 def scroll_down(self):
584 # return self._scroll_presenter.scroll_down()
585 return False
587 def get_selected_text(self):
588 return self._html_presenter.get_html_text_selection()