Experimental support for PyWebKitGtk and GtkMozEmbed via documentviews.py
[straw.git] / straw / ItemView.py
blob05301e93917f551d0a137f77823ad4cc3790a51e
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. """
21 import os
22 import re
23 import urlparse
24 from xml.sax import saxutils
25 import codecs
26 import pygtk
27 pygtk.require('2.0')
28 import gtk
29 import gtk.glade
30 import gtkhtml2
31 import utils
32 import dialogs
33 import MessageManager
34 import ImageCache
35 import MVP
36 import Config
37 import error
38 import dialogs
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 = dialogs.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()
122 config = Config.get_instance()
123 config.text_magnification = self._widget.get_magnification()
125 def on_copy_text_cb(self, *args):
126 print args
127 self._presenter.set_clipboard_text(self._text_selection)
128 gtkhtml2.html_selection_clear(self._widget)
129 return
131 def on_copy_url_cb(self, *args):
132 self._presenter.set_clipboard_text(self._url)
133 return
135 def on_open_link_location_cb(self, *args):
136 utils.url_show(self._url)
138 def on_subscribe_cb(self, *args):
139 import subscribe
140 subscribe.show(url=self._url)
141 return
144 def _init_css(self):
145 if self._css is None:
146 css = file(os.path.join(
147 utils.find_data_dir(), "straw.css")).read()
148 # derive colors for blockquotes and header boxes from
149 # the GTK+ theme
150 # the GTK+ colors are in the range 0-65535
151 bgdivisor = int(65535/(9.0/10))
152 fgdivisor = 65535
153 borderdivisor = int(65535/(6.0/10))
154 gtkstyle = self._widget.get_style()
156 headerbg = "background-color: #%.2x%.2x%.2x;" % (
157 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / bgdivisor,
158 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / bgdivisor,
159 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / bgdivisor)
161 headerfg = "color: #%.2x%.2x%.2x;" % (
162 (gtkstyle.fg[gtk.STATE_NORMAL].red * 255) / fgdivisor,
163 (gtkstyle.fg[gtk.STATE_NORMAL].blue * 255) / fgdivisor,
164 (gtkstyle.fg[gtk.STATE_NORMAL].green * 255) / fgdivisor)
166 headerborder = "border-color: #%.2x%.2x%.2x;" % (
167 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / borderdivisor,
168 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / borderdivisor,
169 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / borderdivisor)
172 css = re.sub(r"/\*\*\*HEADERBG\*/", headerbg, css)
173 css = re.sub(r"/\*\*\*HEADERFG\*/", headerfg, css)
174 css = re.sub(r"/\*\*\*HEADERBORDERCOLOR\*/",
175 headerborder, css)
177 css = re.sub(r"/\*\*\*BQUOTEBG\*/", headerbg, css)
178 css = re.sub(r"/\*\*\*BQUOTEFG\*/", headerfg, css)
179 css = re.sub(r"/\*\*\*BQUOTEBORDERCOLOR\*/",
180 headerborder, css)
181 self._css = css
182 return
184 def report_error(self, title, description):
185 dialogs.report_error(title, description)
187 def get_css(self):
188 return self._css
190 def get_adjustments(self):
191 return (self._widget.get_vadjustment(), self._widget.get_hadjustment())
193 def get_widget(self):
194 return self._widget
196 def connect_widget_signal(self, signal, callback):
197 self._widget.connect(signal, callback)
199 def set_on_url(self, url):
200 self._url = url
202 def set_magnification(self, size):
203 # self._widget.set_magnification(size)
204 pass
206 class HTMLPresenter(MVP.BasicPresenter):
208 Model: gtkhtml2.Document
209 View: HTMLView
211 def _initialize(self):
212 # self._model.connect('request-url', self._request_url)
213 # self._view.connect_widget_signal('on_url', self._on_url)
214 self._item = None
216 def _on_url(self, view, url):
217 self._view.set_on_url(url)
218 if url:
219 url = utils.complete_url(url, self._item.feed.location)
220 else:
221 url = ""
222 mmgr = MessageManager.get_instance()
223 mmgr.post_message(url)
224 return
226 def _request_url(self, document, url, stream):
227 feed = self._item.feed
228 try:
229 try:
230 url = utils.complete_url(url, self._item.feed.location)
231 if urlparse.urlparse(url)[0] == 'file':
232 # local URL, don't use the cache.
233 f = file(urlparse.urlparse(url)[2])
234 stream.write(f.read())
235 f.close()
236 else:
237 image = ImageCache.cache[url]
238 stream.write(image.get_data())
239 except Exception, ex:
240 error.log("Error reading image in %s: %s" % (url, ex))
241 finally:
242 stream.close()
243 stream = None
244 return
246 def set_clipboard_text(self, text):
247 utils.set_clipboard_text(text)
249 def get_html_text_selection(self):
250 return gtkhtml2.html_selection_get_text(self.view.widget)
252 def display_url(self, link):
253 link = link.strip()
254 link = utils.complete_url(link, self._item.feed.location)
255 try:
256 utils.url_show(link)
257 except Exception, ex:
258 self._view.report_error(_("Error Loading Browser"),
259 _("Please check your browser settings and try again."))
260 return
262 def get_view_adjustments(self):
263 return self._view.get_adjustments()
265 def get_view_widget(self):
266 return self._view.get_widget()
268 def display_item(self, item, encoding):
269 self._item = item
270 content = self._htmlify_item(item, encoding)
271 self._prepare_stream(content)
272 return
274 def display_empty_feed(self):
275 content = """<p class=\"emptyfeed\"/>"""# _("No data yet, need to poll first.") </p>"""
276 self._prepare_stream(content)
278 def display_empty_search(self):
279 content = """
280 <h2>Search Subscriptions</h2>
282 Begin searching by typing your text on the text box on the left side.
283 </p>
285 self._prepare_stream(content)
286 return
288 def set_view_magnification(self, size):
289 self.view.set_magnification(size)
291 def _encode_for_html(self, unicode_data, encoding='utf-8'):
292 """ From Python Cookbook, 2/ed, section 1.23
293 'html_replace' is in the utils module
295 return unicode_data.encode(encoding, 'html_replace')
297 def _prepare_stream(self, content):
298 html = self._generate_html(content)
299 html = self._encode_for_html(html)
300 # self._model.clear()
301 # self._model.open_stream("text/html")
302 # self._model.write_stream(html)
303 # self._model.close_stream()
304 self._model.render_data(html, self._item.feed.location, "text/html")
305 return
307 def _generate_html(self, body):
308 # heading
309 html = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
310 <html>
311 <head><title>title</title>
312 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
314 # stylesheet
315 if Config.get_instance().reload_css:
316 html += """<link rel="stylesheet" type="text/css" href="file://"
317 """ + os.path.join(utils.find_data_dir(), "straw.css") + """/>"""
318 else:
319 html += """<style type="text/css">""" + self._view.get_css() + """</style>"""
321 # body
322 html += "</head><body>%s</body></html>" % body
323 return html
325 def _htmlify_item(self, item, encoding):
326 feed = item.feed
327 ret = []
329 # item header
330 ret.append('<div id="itemheader">')
331 if item.title is not None:
332 if item.link is not None:
333 ret.append('<div class="title"><a href="%s">%s</a></div>' % (item.link,item.title))
334 else:
335 ret.append(item.title)
336 ret.append('<table id="itemhead" cellspacing="0" cellpadding="0">')
337 if item.pub_date is not None:
338 timestr = utils.format_date(
339 item.pub_date, utils.get_date_format(), encoding)
340 ret.append(''.join(('<tr><td class="headleft" id="date">%s</td><td class="headright"></td></tr>' % str(timestr))))
342 ret.append('</table>')
343 ret.append('</div>')
345 # item body
346 if item.description is not None:
347 item.description.replace('\n', '<br/>')
348 ret.append('<div class="description">%s</div>' % item.description)
350 if item.publication_name is not None:
351 ret.append('<div class="description">')
352 ret.append('<b>%s:</b> %s<br/>' % (_("Publication"),
353 item.publication_name))
354 if item.publication_volume is not None:
355 ret.append('<b>%s:</b> %s ' % (_("Volume"),
356 item.publication_volume))
357 if item.publication_number is not None:
358 ret.append('( %s )<br />' % item.publication_number)
359 if item.publication_section is not None:
360 ret.append('<b>%s:</b> %s<br />' % (_("Section"),
361 item.publication_section))
362 if item.publication_starting_page is not None:
363 ret.append('<b>%s:</b> %s' % (_("Starting Page"),
364 item.publication_starting_page))
365 ret.append('</div>')
367 # freshmeat fields
368 freshmeat_data = []
369 if item.fm_license != '' and item.fm_license is not None:
370 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
371 (_("Software license"), item.fm_license))
372 if item.fm_changes != '' and item.fm_changes is not None:
373 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
374 (_("Changes"), item.fm_changes))
375 if len(freshmeat_data) > 0:
376 ret.append('<div class="description">')
377 ret.extend(freshmeat_data)
378 ret.append('</div>')
379 # empty paragraph to make sure that we get space here
380 ret.append('<p></p>')
381 # Additional information
382 dcret = []
384 # RSS Enclosures
386 if item.enclosures:
387 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><table>' % _("Enclosed Media"))
388 for enc in item.enclosures: # rss 2.0 defines only one enclosure per item
389 size = int(enc.length)
390 unit = _('bytes')
391 if size > 1024:
392 unit = _('KB')
393 size /= 1024.0
394 if size > 1024:
395 unit = _('MB')
396 size /= 1024.0
397 link_text = enc['href'].split('/')[-1]
399 # find what kind of media is that. enc[type] will have something like audio/mp3 or video/mpeg (mimetypes)
400 # 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...
401 kind = enc['type'].split('/')[0]
402 if kind == 'audio':
403 icon_name = 'audio-x-generic'
404 elif kind == 'video':
405 icon_name = 'video-x-generic'
406 elif kind == 'image':
407 icon_name = 'image-x-generic'
408 elif kind == 'application':
409 icon_name = 'binary'
410 elif kind == 'text':
411 icon_name = 'text-x-generic'
412 else:
413 icon_name = "unknown"
415 it = gtk.icon_theme_get_default()
416 ii = it.lookup_icon(icon_name, 32, gtk.ICON_LOOKUP_NO_SVG)
417 if ii:
418 imgsrc = 'file://' + ii.get_filename()
419 else:
420 imgsrc = "file://%s/%s" % (utils.find_image_dir(), 'image-missing.svg')
421 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']))
422 dcret.append('</table></td></tr>')
424 if item.creator is not None:
425 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))
426 if item.contributors is not None and len(item.contributors):
427 for c in item.contributors:
428 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
429 % (_("Contributor:"), c.name))
430 if item.source:
431 url = utils.get_url_location(item.source['url'])
432 text = saxutils.escape(url)
433 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>' %
434 (_("Item Source"), url, text))
436 if item.guid is not None and item.guid != "" and item.guidislink:
437 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))
438 # check for not guidislink for the case where there is guid but
439 # isPermalink="false" and yet link is the same as guid (link is
440 # always assumed to be a valid link)
441 if item.link != "" and item.link is not None and (item.link != item.guid or not item.guidislink):
442 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>' %
443 (_("Complete story"), item.link, item.link))
445 if item.license_urls:
446 for l in item.license_urls:
447 if l:
448 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))
450 if len(dcret):
451 ret.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
452 ret.append("".join(dcret))
453 ret.append('</table>')
454 ret.append('</div>')
455 return "".join(ret)
457 class ScrollView(MVP.WidgetView):
459 Widget: html_scrolled_window
461 def set_adjustments(self, vadjustment, hadjustment):
462 self._widget.set_hadjustment(hadjustment)
463 self._widget.set_vadjustment(vadjustment)
464 return
466 def add_child(self, widget):
467 self._widget.add(widget)
468 return
470 def show(self):
471 self._widget.show_all()
472 return
474 def adjust_vertical_adjustment(self):
475 va = self._widget.get_vadjustment()
476 va.set_value(va.lower)
477 return
479 def get_vadjustment(self):
480 return self._widget.get_vadjustment()
482 class ScrollPresenter(MVP.BasicPresenter):
484 View: ScrollView
486 def set_view_adjustments(self, vadjustment, hadjustment):
487 self._view.set_adjustments(vadjustment, hadjustment)
488 return
490 def update_view(self):
491 self._view.adjust_vertical_adjustment()
492 return
494 def scroll_down(self):
495 va = self._view.get_vadjustment()
496 old_value = va.get_value()
497 new_value = old_value + va.page_increment
498 limit = va.upper - va.page_size
499 if new_value > limit:
500 new_value = limit
501 va.set_value(new_value)
502 return new_value > old_value
504 def show_view(self):
505 self._view.show()
506 return
508 class ItemView:
509 def __init__(self, item_view_container):
510 self._encoding = utils.get_locale_encoding()
511 widget_tree = gtk.glade.get_widget_tree(item_view_container)
512 # document = gtkhtml2.Document()
513 # widget = gtkhtml2.View()
514 import documentviews
515 document = documentviews.default_documentview(self, Config.straw_home())
516 widget = document.widget()
517 html_view = HTMLView(widget, document)
518 self._html_presenter = HTMLPresenter(document, html_view)
520 widget = widget_tree.get_widget('html_scrolled_window')
521 parent = widget.parent
522 parent.remove(widget)
523 widget = gtk.Frame()
524 widget.set_shadow_type(gtk.SHADOW_IN)
525 parent.add(widget)
527 # scroll_view = ScrollView(widget)
528 # self._scroll_presenter = ScrollPresenter(view=scroll_view)
530 # vadj, hadj = self._html_presenter.get_view_adjustments()
531 child = self._html_presenter.get_view_widget()
532 # self._scroll_presenter.set_view_adjustments(vadj, hadj)
533 # self._scroll_presenter.view.add_child(child)
534 # self._scroll_presenter.show_view()
536 widget.add(child)
537 widget.show_all()
539 config = Config.get_instance()
540 self._html_presenter.set_view_magnification(config.text_magnification)
542 def itemlist_selection_changed(self, selection, column):
543 (model, treeiter) = selection.get_selected()
544 if not treeiter: return # .. or display a template page?
545 item = model.get_value(treeiter, column)
546 self._display_item(item)
548 def _display_item(self, item):
549 self._html_presenter.display_item(item, self._encoding)
550 # self._scroll_presenter.update_view()
552 def display_empty_feed(self):
553 self._html_presenter.display_empty_feed()
555 def display_empty_search(self):
556 self._html_presenter.display_empty_search()
558 def scroll_down(self):
559 # return self._scroll_presenter.scroll_down()
560 return False
562 def get_selected_text(self):
563 return self._html_presenter.get_html_text_selection()
565 # callbacks from documentview XXX .application too
566 def location_changed(self, uri):
567 pass
568 def loading_started(self):
569 pass
570 def loading_finished(self):
571 pass
572 def status_changed(self, text):
573 pass
574 def open_uri_handler(self, uri):
575 return False