Fixed exception that occured in some cases when removing nodes.
[straw.git] / straw / ItemView.py
blobac3de4e4373aee84a2cedc8b9f93a0ca19783e12
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 ImageCache
31 import MVP
32 import Config
33 import error
35 import straw
36 from straw import helpers
38 class HTMLView(MVP.View):
40 def _initialize(self):
41 # self._widget.set_document(self._model)
42 # Make the article view focusable. gtkhtml2.View scrolls to the first
43 # link of the document which sometimes makes the first part of the
44 # document unviewable if the first link is found at the bottom part of
45 # the document. This is good for now since the template includes a
46 # link at the topmost page of the view.
47 self._widget.set_property('can-focus', True)
48 # self._widget.get_vadjustment().set_value(0)
49 self._widget.connect('button_press_event', self._button_press_event)
50 self._css = None
51 self._url = None
52 self._text_selection = None
53 self._init_css()
54 self.uifactory = helpers.UIFactory('HtmlViewActions')
55 action = self.uifactory.get_action('/htmlview_popup/text_copy')
56 action.connect('activate', self.on_copy_text_cb)
57 action = self.uifactory.get_action('/htmlview_popup/link_copy')
58 action.connect('activate', self.on_copy_url_cb)
59 action = self.uifactory.get_action('/htmlview_popup/link_open')
60 action.connect('activate', self.on_open_link_location_cb)
61 action = self.uifactory.get_action('/htmlview_popup/subscribe')
62 action.connect('activate', self.on_subscribe_cb)
63 action = self.uifactory.get_action('/htmlview_popup/zoom_in')
64 action.connect('activate', lambda *args: self.on_magnify('in'))
65 action = self.uifactory.get_action('/htmlview_popup/zoom_out')
66 action.connect('activate', lambda *args: self.on_magnify('out'))
67 action = self.uifactory.get_action('/htmlview_popup/zoom_100')
68 action.connect('activate', lambda *args: self.on_magnify('reset'))
69 self.popup = self.uifactory.get_popup('/htmlview_popup')
71 @property
72 def widget(self):
73 return self._widget
75 def _connect_signals(self, *args): pass
77 def _button_press_event(self, widget, event):
78 if event.button == 1:
79 if self._url:
80 self._presenter.display_url(self._url)
81 self._url = self._text_selection = None
82 elif event.button == 3:
83 self._text_selection = self._presenter.get_html_text_selection()
84 link_action = self.uifactory.get_action('/htmlview_popup/link_copy')
85 open_link_action = self.uifactory.get_action('/htmlview_popup/link_open')
86 subs_action = self.uifactory.get_action('/htmlview_popup/subscribe')
87 text_action = self.uifactory.get_action('/htmlview_popup/text_copy')
88 if self._url and self._text_selection:
89 link_action.set_sensitive(True)
90 open_link_action.set_sensitive(True)
91 subs_action.set_sensitive(True)
92 text_action.set_sensitive(True)
93 elif self._url:
94 text_action.set_sensitive(False)
95 link_action.set_sensitive(True)
96 open_link_action.set_sensitive(True)
97 subs_action.set_sensitive(True)
98 elif self._text_selection:
99 link_action.set_sensitive(False)
100 open_link_action.set_sensitive(False)
101 subs_action.set_sensitive(False)
102 text_action.set_sensitive(True)
103 else:
104 link_action.set_sensitive(False)
105 open_link_action.set_sensitive(False)
106 subs_action.set_sensitive(False)
107 text_action.set_sensitive(False)
108 self.uifactory.ensure_update()
109 self.popup.popup(None, None, None, event.button,
110 gtk.get_current_event_time())
111 return True
113 def on_magnify(self, action):
114 if action == "in":
115 self._widget.zoom_in()
116 elif action == "out":
117 self._widget.zoom_out()
118 else:
119 self._widget.zoom_reset()
120 config = Config.get_instance()
121 config.text_magnification = self._widget.get_magnification()
123 def on_copy_text_cb(self, *args):
124 print args
125 self._presenter.set_clipboard_text(self._text_selection)
126 gtkhtml2.html_selection_clear(self._widget)
127 return
129 def on_copy_url_cb(self, *args):
130 self._presenter.set_clipboard_text(self._url)
131 return
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)
139 return
142 def _init_css(self):
143 if self._css is None:
144 css = file(os.path.join(straw.STRAW_DATA_DIR, "straw.css")).read()
145 # derive colors for blockquotes and header boxes from
146 # the GTK+ theme
147 # the GTK+ colors are in the range 0-65535
148 bgdivisor = int(65535/(9.0/10))
149 fgdivisor = 65535
150 borderdivisor = int(65535/(6.0/10))
151 gtkstyle = self._widget.get_style()
153 headerbg = "background-color: #%.2x%.2x%.2x;" % (
154 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / bgdivisor,
155 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / bgdivisor,
156 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / bgdivisor)
158 headerfg = "color: #%.2x%.2x%.2x;" % (
159 (gtkstyle.fg[gtk.STATE_NORMAL].red * 255) / fgdivisor,
160 (gtkstyle.fg[gtk.STATE_NORMAL].blue * 255) / fgdivisor,
161 (gtkstyle.fg[gtk.STATE_NORMAL].green * 255) / fgdivisor)
163 headerborder = "border-color: #%.2x%.2x%.2x;" % (
164 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / borderdivisor,
165 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / borderdivisor,
166 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / borderdivisor)
169 css = re.sub(r"/\*\*\*HEADERBG\*/", headerbg, css)
170 css = re.sub(r"/\*\*\*HEADERFG\*/", headerfg, css)
171 css = re.sub(r"/\*\*\*HEADERBORDERCOLOR\*/",
172 headerborder, css)
174 css = re.sub(r"/\*\*\*BQUOTEBG\*/", headerbg, css)
175 css = re.sub(r"/\*\*\*BQUOTEFG\*/", headerfg, css)
176 css = re.sub(r"/\*\*\*BQUOTEBORDERCOLOR\*/",
177 headerborder, css)
178 self._css = css
179 return
181 def report_error(self, title, description):
182 helpers.report_error(title, description)
184 def get_css(self):
185 return self._css
187 def get_adjustments(self):
188 return (self._widget.get_vadjustment(), self._widget.get_hadjustment())
190 def get_widget(self):
191 return self._widget
193 def connect_widget_signal(self, signal, callback):
194 self._widget.connect(signal, callback)
196 def set_on_url(self, url):
197 self._url = url
199 def set_magnification(self, size):
200 # self._widget.set_magnification(size)
201 pass
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 self._model.render_data(html, self._item.feed.location, "text/html")
303 return
305 def _generate_html(self, body):
306 # heading
307 html = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
308 <html>
309 <head><title>title</title>
310 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
312 # stylesheet
313 if Config.get_instance().reload_css:
314 html += """<link rel="stylesheet" type="text/css" href="file://"
315 """ + os.path.join(straw.STRAW_DATA_DIR, "straw.css") + """/>"""
316 else:
317 html += """<style type="text/css">""" + self._view.get_css() + """</style>"""
319 # body
320 html += "</head><body>%s</body></html>" % body
321 return html
323 def _htmlify_item(self, item, encoding):
324 feed = item.feed
325 ret = []
327 # item header
328 ret.append('<div id="itemheader">')
329 if item.title is not None:
330 if item.link is not None:
331 ret.append('<div class="title"><a href="%s">%s</a></div>' % (item.link,item.title))
332 else:
333 ret.append(item.title)
334 ret.append('<table id="itemhead" cellspacing="0" cellpadding="0">')
335 if item.pub_date is not None:
336 timestr = helpers.format_date(
337 item.pub_date, helpers.get_date_format(), encoding)
338 ret.append(''.join(('<tr><td class="headleft" id="date">%s</td><td class="headright"></td></tr>' % str(timestr))))
340 ret.append('</table>')
341 ret.append('</div>')
343 # item body
344 if item.description is not None:
345 item.description.replace('\n', '<br/>')
346 ret.append('<div class="description">%s</div>' % item.description)
348 if len(item.publication_name):
349 ret.append('<div class="description">')
350 ret.append('<b>%s:</b> %s<br/>' % (_("Publication"),
351 item.publication_name))
352 if item.publication_volume is not None:
353 ret.append('<b>%s:</b> %s ' % (_("Volume"),
354 item.publication_volume))
355 if item.publication_number is not None:
356 ret.append('( %s )<br />' % item.publication_number)
357 if item.publication_section is not None:
358 ret.append('<b>%s:</b> %s<br />' % (_("Section"),
359 item.publication_section))
360 if item.publication_starting_page is not None:
361 ret.append('<b>%s:</b> %s' % (_("Starting Page"),
362 item.publication_starting_page))
363 ret.append('</div>')
365 # freshmeat fields
366 freshmeat_data = []
367 if item.fm_license != '' and item.fm_license is not None:
368 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
369 (_("Software license"), item.fm_license))
370 if item.fm_changes != '' and item.fm_changes is not None:
371 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
372 (_("Changes"), item.fm_changes))
373 if len(freshmeat_data) > 0:
374 ret.append('<div class="description">')
375 ret.extend(freshmeat_data)
376 ret.append('</div>')
377 # empty paragraph to make sure that we get space here
378 ret.append('<p></p>')
379 # Additional information
380 dcret = []
382 # RSS Enclosures
384 if item.enclosures:
385 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><table>' % _("Enclosed Media"))
386 for enc in item.enclosures: # rss 2.0 defines only one enclosure per item
387 size = int(enc.length)
388 unit = _('bytes')
389 if size > 1024:
390 unit = _('KB')
391 size /= 1024.0
392 if size > 1024:
393 unit = _('MB')
394 size /= 1024.0
395 link_text = enc['href'].split('/')[-1]
397 # find what kind of media is that. enc[type] will have something like audio/mp3 or video/mpeg (mimetypes)
398 # 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...
399 kind = enc['type'].split('/')[0]
400 if kind == 'audio':
401 icon_name = 'audio-x-generic'
402 elif kind == 'video':
403 icon_name = 'video-x-generic'
404 elif kind == 'image':
405 icon_name = 'image-x-generic'
406 elif kind == 'application':
407 icon_name = 'binary'
408 elif kind == 'text':
409 icon_name = 'text-x-generic'
410 else:
411 icon_name = "unknown"
413 it = gtk.icon_theme_get_default()
414 ii = it.lookup_icon(icon_name, 32, gtk.ICON_LOOKUP_NO_SVG)
415 if ii:
416 imgsrc = 'file://' + ii.get_filename()
417 else:
418 imgsrc = "file://%s/%s" % (straw.STRAW_DATA_DIR, 'image-missing.svg')
419 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']))
420 dcret.append('</table></td></tr>')
422 if item.creator is not None:
423 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))
424 if item.contributors is not None and len(item.contributors):
425 for c in item.contributors:
426 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
427 % (_("Contributor:"), c.name))
428 if item.source:
429 url = helpers.get_url_location(item.source['url'])
430 text = saxutils.escape(url)
431 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>' %
432 (_("Item Source"), url, text))
434 if item.guid is not None and item.guid != "" and item.guidislink:
435 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))
436 # check for not guidislink for the case where there is guid but
437 # isPermalink="false" and yet link is the same as guid (link is
438 # always assumed to be a valid link)
439 if item.link != "" and item.link is not None and (item.link != item.guid or not item.guidislink):
440 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>' %
441 (_("Complete story"), item.link, item.link))
443 if item.license_urls:
444 for l in item.license_urls:
445 if l:
446 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))
448 if len(dcret):
449 ret.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
450 ret.append("".join(dcret))
451 ret.append('</table>')
452 ret.append('</div>')
453 return "".join(ret)
455 class ScrollView(MVP.WidgetView):
457 Widget: html_scrolled_window
459 def set_adjustments(self, vadjustment, hadjustment):
460 self._widget.set_hadjustment(hadjustment)
461 self._widget.set_vadjustment(vadjustment)
462 return
464 def add_child(self, widget):
465 self._widget.add(widget)
466 return
468 def show(self):
469 self._widget.show_all()
470 return
472 def adjust_vertical_adjustment(self):
473 va = self._widget.get_vadjustment()
474 va.set_value(va.lower)
475 return
477 def get_vadjustment(self):
478 return self._widget.get_vadjustment()
480 class ScrollPresenter(MVP.BasicPresenter):
482 View: ScrollView
484 def set_view_adjustments(self, vadjustment, hadjustment):
485 self._view.set_adjustments(vadjustment, hadjustment)
486 return
488 def update_view(self):
489 self._view.adjust_vertical_adjustment()
490 return
492 def scroll_down(self):
493 va = self._view.get_vadjustment()
494 old_value = va.get_value()
495 new_value = old_value + va.page_increment
496 limit = va.upper - va.page_size
497 if new_value > limit:
498 new_value = limit
499 va.set_value(new_value)
500 return new_value > old_value
502 def show_view(self):
503 self._view.show()
504 return
506 class ItemView:
507 def __init__(self, item_view_container):
508 self._encoding = helpers.get_locale_encoding()
509 widget_tree = gtk.glade.get_widget_tree(item_view_container)
510 # document = gtkhtml2.Document()
511 # widget = gtkhtml2.View()
512 import documentviews
513 engine_name = os.getenv('STRAW_HTML')
514 if engine_name:
515 engine = documentviews.create_documentviews[engine_name]
516 else:
517 engine = documentviews.default_documentview
518 document = engine(Config.straw_home())
519 widget = document.widget()
520 html_view = HTMLView(widget, document)
521 self._html_presenter = HTMLPresenter(document, html_view)
523 widget = widget_tree.get_widget('html_scrolled_window')
524 parent = widget.parent
525 parent.remove(widget)
526 widget = gtk.Frame()
527 widget.set_shadow_type(gtk.SHADOW_IN)
528 parent.add(widget)
530 # scroll_view = ScrollView(widget)
531 # self._scroll_presenter = ScrollPresenter(view=scroll_view)
533 # vadj, hadj = self._html_presenter.get_view_adjustments()
534 child = self._html_presenter.get_view_widget()
535 # self._scroll_presenter.set_view_adjustments(vadj, hadj)
536 # self._scroll_presenter.view.add_child(child)
537 # self._scroll_presenter.show_view()
539 widget.add(child)
540 # gtkmozembed visibility workaround
541 child.show()
542 widget.show()
543 # gtkhtml visibility workaround
544 widget.show_all()
546 config = Config.get_instance()
547 self._html_presenter.set_view_magnification(config.text_magnification)
549 def itemlist_selection_changed(self, selection, column):
550 (model, treeiter) = selection.get_selected()
551 if not treeiter: return # .. or display a template page?
552 item = model.get_value(treeiter, column)
553 self._display_item(item)
555 def _display_item(self, item):
556 self._html_presenter.display_item(item, self._encoding)
557 # self._scroll_presenter.update_view()
559 def display_empty_feed(self):
560 self._html_presenter.display_empty_feed()
562 def display_empty_search(self):
563 self._html_presenter.display_empty_search()
565 def scroll_down(self):
566 # return self._scroll_presenter.scroll_down()
567 return False
569 def get_selected_text(self):
570 return self._html_presenter.get_html_text_selection()