Intermediate work commit.
[straw/fork.git] / straw / ItemView.py
blob4908dd0f36d5f0cfe8887194d2a881a1639aab42
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 ImageCache
32 import MVP
33 import Config
34 import error
36 import straw
37 from straw import helpers
39 class HTMLView(MVP.View):
41 def _initialize(self):
42 self._widget.set_document(self._model)
43 # Make the article view focusable. gtkhtml2.View scrolls to the first
44 # link of the document which sometimes makes the first part of the
45 # document unviewable if the first link is found at the bottom part of
46 # the document. This is good for now since the template includes a
47 # link at the topmost page of the view.
48 self._widget.set_property('can-focus', True)
49 self._widget.get_vadjustment().set_value(0)
50 self._widget.connect('button_press_event', self._button_press_event)
51 self._css = None
52 self._url = None
53 self._text_selection = None
54 self._init_css()
55 self.uifactory = helpers.UIFactory('HtmlViewActions')
56 action = self.uifactory.get_action('/htmlview_popup/text_copy')
57 action.connect('activate', self.on_copy_text_cb)
58 action = self.uifactory.get_action('/htmlview_popup/link_copy')
59 action.connect('activate', self.on_copy_url_cb)
60 action = self.uifactory.get_action('/htmlview_popup/link_open')
61 action.connect('activate', self.on_open_link_location_cb)
62 action = self.uifactory.get_action('/htmlview_popup/subscribe')
63 action.connect('activate', self.on_subscribe_cb)
64 action = self.uifactory.get_action('/htmlview_popup/zoom_in')
65 action.connect('activate', lambda *args: self.on_magnify('in'))
66 action = self.uifactory.get_action('/htmlview_popup/zoom_out')
67 action.connect('activate', lambda *args: self.on_magnify('out'))
68 action = self.uifactory.get_action('/htmlview_popup/zoom_100')
69 action.connect('activate', lambda *args: self.on_magnify('reset'))
70 self.popup = self.uifactory.get_popup('/htmlview_popup')
72 @property
73 def widget(self):
74 return self._widget
76 def _connect_signals(self, *args): pass
78 def _button_press_event(self, widget, event):
79 if event.button == 1:
80 if self._url:
81 self._presenter.display_url(self._url)
82 self._url = self._text_selection = None
83 elif event.button == 3:
84 self._text_selection = self._presenter.get_html_text_selection()
85 link_action = self.uifactory.get_action('/htmlview_popup/link_copy')
86 open_link_action = self.uifactory.get_action('/htmlview_popup/link_open')
87 subs_action = self.uifactory.get_action('/htmlview_popup/subscribe')
88 text_action = self.uifactory.get_action('/htmlview_popup/text_copy')
89 if self._url and self._text_selection:
90 link_action.set_sensitive(True)
91 open_link_action.set_sensitive(True)
92 subs_action.set_sensitive(True)
93 text_action.set_sensitive(True)
94 elif self._url:
95 text_action.set_sensitive(False)
96 link_action.set_sensitive(True)
97 open_link_action.set_sensitive(True)
98 subs_action.set_sensitive(True)
99 elif self._text_selection:
100 link_action.set_sensitive(False)
101 open_link_action.set_sensitive(False)
102 subs_action.set_sensitive(False)
103 text_action.set_sensitive(True)
104 else:
105 link_action.set_sensitive(False)
106 open_link_action.set_sensitive(False)
107 subs_action.set_sensitive(False)
108 text_action.set_sensitive(False)
109 self.uifactory.ensure_update()
110 self.popup.popup(None, None, None, event.button,
111 gtk.get_current_event_time())
112 return True
114 def on_magnify(self, action):
115 if action == "in":
116 self._widget.zoom_in()
117 elif action == "out":
118 self._widget.zoom_out()
119 else:
120 self._widget.zoom_reset()
121 config = Config.get_instance()
122 config.text_magnification = self._widget.get_magnification()
124 def on_copy_text_cb(self, *args):
125 print args
126 self._presenter.set_clipboard_text(self._text_selection)
127 gtkhtml2.html_selection_clear(self._widget)
128 return
130 def on_copy_url_cb(self, *args):
131 self._presenter.set_clipboard_text(self._url)
132 return
134 def on_open_link_location_cb(self, *args):
135 helpers.url_show(self._url)
137 def on_subscribe_cb(self, *args):
138 import subscribe
139 subscribe.show(url=self._url)
140 return
143 def _init_css(self):
144 if self._css is None:
145 css = file(os.path.join(straw.STRAW_DATA_DIR, "straw.css")).read()
146 # derive colors for blockquotes and header boxes from
147 # the GTK+ theme
148 # the GTK+ colors are in the range 0-65535
149 bgdivisor = int(65535/(9.0/10))
150 fgdivisor = 65535
151 borderdivisor = int(65535/(6.0/10))
152 gtkstyle = self._widget.get_style()
154 headerbg = "background-color: #%.2x%.2x%.2x;" % (
155 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / bgdivisor,
156 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / bgdivisor,
157 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / bgdivisor)
159 headerfg = "color: #%.2x%.2x%.2x;" % (
160 (gtkstyle.fg[gtk.STATE_NORMAL].red * 255) / fgdivisor,
161 (gtkstyle.fg[gtk.STATE_NORMAL].blue * 255) / fgdivisor,
162 (gtkstyle.fg[gtk.STATE_NORMAL].green * 255) / fgdivisor)
164 headerborder = "border-color: #%.2x%.2x%.2x;" % (
165 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / borderdivisor,
166 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / borderdivisor,
167 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / borderdivisor)
170 css = re.sub(r"/\*\*\*HEADERBG\*/", headerbg, css)
171 css = re.sub(r"/\*\*\*HEADERFG\*/", headerfg, css)
172 css = re.sub(r"/\*\*\*HEADERBORDERCOLOR\*/",
173 headerborder, css)
175 css = re.sub(r"/\*\*\*BQUOTEBG\*/", headerbg, css)
176 css = re.sub(r"/\*\*\*BQUOTEFG\*/", headerfg, css)
177 css = re.sub(r"/\*\*\*BQUOTEBORDERCOLOR\*/",
178 headerborder, css)
179 self._css = css
180 return
182 def report_error(self, title, description):
183 helpers.report_error(title, description)
185 def get_css(self):
186 return self._css
188 def get_adjustments(self):
189 return (self._widget.get_vadjustment(), self._widget.get_hadjustment())
191 def get_widget(self):
192 return self._widget
194 def connect_widget_signal(self, signal, callback):
195 self._widget.connect(signal, callback)
197 def set_on_url(self, url):
198 self._url = url
200 def set_magnification(self, size):
201 self._widget.set_magnification(size)
203 class HTMLPresenter(MVP.BasicPresenter):
205 Model: gtkhtml2.Document
206 View: HTMLView
208 def _initialize(self):
209 self._model.connect('request-url', self._request_url)
210 self._view.connect_widget_signal('on_url', self._on_url)
211 self._item = None
213 def _on_url(self, view, url):
214 self._view.set_on_url(url)
215 if url:
216 url = helpers.complete_url(url, self._item.feed.location)
217 else:
218 url = ""
219 straw.post_status_message(url)
220 return
222 def _request_url(self, document, url, stream):
223 feed = self._item.feed
224 try:
225 try:
226 url = helpers.complete_url(url, self._item.feed.location)
227 if urlparse.urlparse(url)[0] == 'file':
228 # local URL, don't use the cache.
229 f = file(urlparse.urlparse(url)[2])
230 stream.write(f.read())
231 f.close()
232 else:
233 image = ImageCache.cache[url]
234 stream.write(image.get_data())
235 except Exception, ex:
236 #error.log("Error reading image in %s: %s" % (url, ex))
237 pass
238 finally:
239 stream.close()
240 stream = None
241 return
243 def set_clipboard_text(self, text):
244 helpers.set_clipboard_text(text)
246 def get_html_text_selection(self):
247 return gtkhtml2.html_selection_get_text(self.view.widget)
249 def display_url(self, link):
250 link = link.strip()
251 link = helpers.complete_url(link, self._item.feed.location)
252 try:
253 helpers.url_show(link)
254 except Exception, ex:
255 print ex
256 self._view.report_error(_("Error Loading Browser"),
257 _("Please check your browser settings and try again."))
258 return
260 def get_view_adjustments(self):
261 return self._view.get_adjustments()
263 def get_view_widget(self):
264 return self._view.get_widget()
266 def display_item(self, item, encoding):
267 self._item = item
268 content = self._htmlify_item(item, encoding)
269 self._prepare_stream(content)
270 return
272 def display_empty_feed(self):
273 content = """<p class=\"emptyfeed\"/>"""# _("No data yet, need to poll first.") </p>"""
274 self._prepare_stream(content)
276 def display_empty_search(self):
277 content = """
278 <h2>Search Subscriptions</h2>
280 Begin searching by typing your text on the text box on the left side.
281 </p>
283 self._prepare_stream(content)
284 return
286 def set_view_magnification(self, size):
287 self.view.set_magnification(size)
289 def _encode_for_html(self, unicode_data, encoding='utf-8'):
290 """ From Python Cookbook, 2/ed, section 1.23
291 'html_replace' is in the utils module
293 return unicode_data.encode(encoding, 'html_replace')
295 def _prepare_stream(self, content):
296 html = self._generate_html(content)
297 html = self._encode_for_html(html)
298 self._model.clear()
299 self._model.open_stream("text/html")
300 self._model.write_stream(html)
301 self._model.close_stream()
302 return
304 def _generate_html(self, body):
305 # heading
306 html = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
307 <html>
308 <head><title>title</title>
309 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
311 # stylesheet
312 if Config.get_instance().reload_css:
313 html += """<link rel="stylesheet" type="text/css" href="file://"
314 """ + os.path.join(straw.STRAW_DATA_DIR, "straw.css") + """/>"""
315 else:
316 html += """<style type="text/css">""" + self._view.get_css() + """</style>"""
318 # body
319 html += "</head><body>%s</body></html>" % body
320 return html
322 def _htmlify_item(self, item, encoding):
323 feed = item.feed
324 ret = []
326 # item header
327 ret.append('<div id="itemheader">')
328 if item.title is not None:
329 if item.link is not None:
330 ret.append('<div class="title"><a href="%s">%s</a></div>' % (item.link,item.title))
331 else:
332 ret.append(item.title)
333 ret.append('<table id="itemhead" cellspacing="0" cellpadding="0">')
334 if item.pub_date is not None:
335 timestr = helpers.format_date(
336 item.pub_date, helpers.get_date_format(), encoding)
337 ret.append(''.join(('<tr><td class="headleft" id="date">%s</td><td class="headright"></td></tr>' % str(timestr))))
339 ret.append('</table>')
340 ret.append('</div>')
342 # item body
343 if item.description is not None:
344 item.description.replace('\n', '<br/>')
345 ret.append('<div class="description">%s</div>' % item.description)
347 if item.publication_name is not None:
348 ret.append('<div class="description">')
349 ret.append('<b>%s:</b> %s<br/>' % (_("Publication"),
350 item.publication_name))
351 if item.publication_volume is not None:
352 ret.append('<b>%s:</b> %s ' % (_("Volume"),
353 item.publication_volume))
354 if item.publication_number is not None:
355 ret.append('( %s )<br />' % item.publication_number)
356 if item.publication_section is not None:
357 ret.append('<b>%s:</b> %s<br />' % (_("Section"),
358 item.publication_section))
359 if item.publication_starting_page is not None:
360 ret.append('<b>%s:</b> %s' % (_("Starting Page"),
361 item.publication_starting_page))
362 ret.append('</div>')
364 # freshmeat fields
365 freshmeat_data = []
366 if item.fm_license != '' and item.fm_license is not None:
367 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
368 (_("Software license"), item.fm_license))
369 if item.fm_changes != '' and item.fm_changes is not None:
370 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
371 (_("Changes"), item.fm_changes))
372 if len(freshmeat_data) > 0:
373 ret.append('<div class="description">')
374 ret.extend(freshmeat_data)
375 ret.append('</div>')
376 # empty paragraph to make sure that we get space here
377 ret.append('<p></p>')
378 # Additional information
379 dcret = []
381 # RSS Enclosures
383 if item.enclosures:
384 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><table>' % _("Enclosed Media"))
385 for enc in item.enclosures: # rss 2.0 defines only one enclosure per item
386 size = int(enc.length)
387 unit = _('bytes')
388 if size > 1024:
389 unit = _('KB')
390 size /= 1024.0
391 if size > 1024:
392 unit = _('MB')
393 size /= 1024.0
394 link_text = enc['href'].split('/')[-1]
396 # find what kind of media is that. enc[type] will have something like audio/mp3 or video/mpeg (mimetypes)
397 # 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...
398 kind = enc['type'].split('/')[0]
399 if kind == 'audio':
400 icon_name = 'audio-x-generic'
401 elif kind == 'video':
402 icon_name = 'video-x-generic'
403 elif kind == 'image':
404 icon_name = 'image-x-generic'
405 elif kind == 'application':
406 icon_name = 'binary'
407 elif kind == 'text':
408 icon_name = 'text-x-generic'
409 else:
410 icon_name = "unknown"
412 it = gtk.icon_theme_get_default()
413 ii = it.lookup_icon(icon_name, 32, gtk.ICON_LOOKUP_NO_SVG)
414 if ii:
415 imgsrc = 'file://' + ii.get_filename()
416 else:
417 imgsrc = "file://%s/%s" % (straw.STRAW_DATA_DIR, 'image-missing.svg')
418 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']))
419 dcret.append('</table></td></tr>')
421 if item.creator is not None:
422 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))
423 if item.contributors is not None and len(item.contributors):
424 for c in item.contributors:
425 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
426 % (_("Contributor:"), c.name))
427 if item.source:
428 url = helpers.get_url_location(item.source['url'])
429 text = saxutils.escape(url)
430 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>' %
431 (_("Item Source"), url, text))
433 if item.guid is not None and item.guid != "" and item.guidislink:
434 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))
435 # check for not guidislink for the case where there is guid but
436 # isPermalink="false" and yet link is the same as guid (link is
437 # always assumed to be a valid link)
438 if item.link != "" and item.link is not None and (item.link != item.guid or not item.guidislink):
439 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>' %
440 (_("Complete story"), item.link, item.link))
442 if item.license_urls:
443 for l in item.license_urls:
444 if l:
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>' % (_("License"), l, l))
447 if len(dcret):
448 ret.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
449 ret.append("".join(dcret))
450 ret.append('</table>')
451 ret.append('</div>')
452 return "".join(ret)
454 class ScrollView(MVP.WidgetView):
456 Widget: html_scrolled_window
458 def set_adjustments(self, vadjustment, hadjustment):
459 self._widget.set_hadjustment(hadjustment)
460 self._widget.set_vadjustment(vadjustment)
461 return
463 def add_child(self, widget):
464 self._widget.add(widget)
465 return
467 def show(self):
468 self._widget.show_all()
469 return
471 def adjust_vertical_adjustment(self):
472 va = self._widget.get_vadjustment()
473 va.set_value(va.lower)
474 return
476 def get_vadjustment(self):
477 return self._widget.get_vadjustment()
479 class ScrollPresenter(MVP.BasicPresenter):
481 View: ScrollView
483 def set_view_adjustments(self, vadjustment, hadjustment):
484 self._view.set_adjustments(vadjustment, hadjustment)
485 return
487 def update_view(self):
488 self._view.adjust_vertical_adjustment()
489 return
491 def scroll_down(self):
492 va = self._view.get_vadjustment()
493 old_value = va.get_value()
494 new_value = old_value + va.page_increment
495 limit = va.upper - va.page_size
496 if new_value > limit:
497 new_value = limit
498 va.set_value(new_value)
499 return new_value > old_value
501 def show_view(self):
502 self._view.show()
503 return
505 class ItemView:
506 def __init__(self, item_view_container):
507 self._encoding = helpers.get_locale_encoding()
508 widget_tree = gtk.glade.get_widget_tree(item_view_container)
509 document = gtkhtml2.Document()
510 widget = gtkhtml2.View()
511 html_view = HTMLView(widget, document)
512 self._html_presenter = HTMLPresenter(document, html_view)
514 widget = widget_tree.get_widget('html_scrolled_window')
515 scroll_view = ScrollView(widget)
516 self._scroll_presenter = ScrollPresenter(view=scroll_view)
518 vadj, hadj = self._html_presenter.get_view_adjustments()
519 child = self._html_presenter.get_view_widget()
520 self._scroll_presenter.set_view_adjustments(vadj, hadj)
521 self._scroll_presenter.view.add_child(child)
522 self._scroll_presenter.show_view()
524 config = Config.get_instance()
525 self._html_presenter.set_view_magnification(config.text_magnification)
527 def itemlist_selection_changed(self, selection, column):
528 (model, treeiter) = selection.get_selected()
529 if not treeiter: return # .. or display a template page?
530 item = model.get_value(treeiter, column)
531 self._display_item(item)
533 def _display_item(self, item):
534 self._html_presenter.display_item(item, self._encoding)
535 self._scroll_presenter.update_view()
537 def display_empty_feed(self):
538 self._html_presenter.display_empty_feed()
540 def display_empty_search(self):
541 self._html_presenter.display_empty_search()
543 def scroll_down(self):
544 return self._scroll_presenter.scroll_down()
546 def get_selected_text(self):
547 return self._html_presenter.get_html_text_selection()