Cleaned up the logging system a bit.
[straw.git] / straw / ItemView.py
blob9fb22077979366cd1f02662ddbf6225cc2fd6e9b
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 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()
121 # TODO: use text magnification
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)
128 def on_copy_url_cb(self, *args):
129 self._presenter.set_clipboard_text(self._url)
131 def on_open_link_location_cb(self, *args):
132 helpers.url_show(self._url)
134 def on_subscribe_cb(self, *args):
135 import subscribe
136 subscribe.show(url=self._url)
138 def _init_css(self):
139 if self._css is None:
140 css = file(os.path.join(straw.defs.STRAW_DATA_DIR, "straw.css")).read()
141 # derive colors for blockquotes and header boxes from
142 # the GTK+ theme
143 # the GTK+ colors are in the range 0-65535
144 bgdivisor = int(65535/(9.0/10))
145 fgdivisor = 65535
146 borderdivisor = int(65535/(6.0/10))
147 gtkstyle = self._widget.get_style()
149 headerbg = "background-color: #%.2x%.2x%.2x;" % (
150 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / bgdivisor,
151 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / bgdivisor,
152 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / bgdivisor)
154 headerfg = "color: #%.2x%.2x%.2x;" % (
155 (gtkstyle.fg[gtk.STATE_NORMAL].red * 255) / fgdivisor,
156 (gtkstyle.fg[gtk.STATE_NORMAL].blue * 255) / fgdivisor,
157 (gtkstyle.fg[gtk.STATE_NORMAL].green * 255) / fgdivisor)
159 headerborder = "border-color: #%.2x%.2x%.2x;" % (
160 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / borderdivisor,
161 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / borderdivisor,
162 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / borderdivisor)
165 css = re.sub(r"/\*\*\*HEADERBG\*/", headerbg, css)
166 css = re.sub(r"/\*\*\*HEADERFG\*/", headerfg, css)
167 css = re.sub(r"/\*\*\*HEADERBORDERCOLOR\*/",
168 headerborder, css)
170 css = re.sub(r"/\*\*\*BQUOTEBG\*/", headerbg, css)
171 css = re.sub(r"/\*\*\*BQUOTEFG\*/", headerfg, css)
172 css = re.sub(r"/\*\*\*BQUOTEBORDERCOLOR\*/",
173 headerborder, css)
174 self._css = css
175 return
177 def report_error(self, title, description):
178 helpers.report_error(title, description)
180 def get_css(self):
181 return self._css
183 def get_adjustments(self):
184 return (self._widget.get_vadjustment(), self._widget.get_hadjustment())
186 def get_widget(self):
187 return self._widget
189 def connect_widget_signal(self, signal, callback):
190 self._widget.connect(signal, callback)
192 def set_on_url(self, url):
193 self._url = url
195 def set_magnification(self, size):
196 # self._widget.set_magnification(size)
197 pass
199 class HTMLPresenter(MVP.BasicPresenter):
201 Model: gtkhtml2.Document
202 View: HTMLView
204 def _initialize(self):
205 # self._model.connect('request-url', self._request_url)
206 # self._view.connect_widget_signal('on_url', self._on_url)
207 self._model.connect('status_changed', self._status_changed)
208 self._model.connect('open_uri', self._open_uri)
209 self._item = None
211 def _open_uri(self, document, url):
212 try:
213 self.display_url(url)
214 finally:
215 return True
217 def _status_changed(self, document, status):
218 post_status_message(status)
220 def _on_url(self, view, url):
221 self._view.set_on_url(url)
222 if url:
223 url = helpers.complete_url(url, self._item.feed.location)
224 else:
225 url = ""
226 post_status_message(url)
227 return
229 def _request_url(self, document, url, stream):
230 feed = self._item.feed
231 try:
232 try:
233 url = helpers.complete_url(url, self._item.feed.location)
234 if urlparse.urlparse(url)[0] == 'file':
235 # local URL, don't use the cache.
236 f = file(urlparse.urlparse(url)[2])
237 stream.write(f.read())
238 f.close()
239 else:
240 image = ImageCache.cache[url]
241 stream.write(image.get_data())
242 except Exception, ex:
243 #error.log("Error reading image in %s: %s" % (url, ex))
244 pass
245 finally:
246 stream.close()
247 stream = None
248 return
250 def set_clipboard_text(self, text):
251 helpers.set_clipboard_text(text)
253 def get_html_text_selection(self):
254 pass
256 def display_url(self, link):
257 link = link.strip()
258 link = helpers.complete_url(link, self._item.feed.location)
259 try:
260 helpers.url_show(link)
261 except Exception, ex:
262 print ex
263 self._view.report_error(_("Error Loading Browser"),
264 _("Please check your browser settings and try again."))
265 return
267 def get_view_adjustments(self):
268 return self._view.get_adjustments()
270 def get_view_widget(self):
271 return self._view.get_widget()
273 def display_item(self, item, encoding):
274 self._item = item
275 content = self._htmlify_item(item, encoding)
276 self._prepare_stream(content)
277 return
279 def display_empty_feed(self):
280 content = """<p class=\"emptyfeed\"/>"""# _("No data yet, need to poll first.") </p>"""
281 self._prepare_stream(content)
283 def display_empty_search(self):
284 content = """
285 <h2>Search Subscriptions</h2>
287 Begin searching by typing your text on the text box on the left side.
288 </p>
290 self._prepare_stream(content)
291 return
293 def set_view_magnification(self, size):
294 self.view.set_magnification(size)
296 def _encode_for_html(self, unicode_data, encoding='utf-8'):
297 """ From Python Cookbook, 2/ed, section 1.23
298 'html_replace' is in the utils module
300 return unicode_data.encode(encoding, 'html_replace')
302 def _prepare_stream(self, content):
303 html = self._generate_html(content)
304 html = self._encode_for_html(html)
305 # self._model.clear()
306 # self._model.open_stream("text/html")
307 # self._model.write_stream(html)
308 # self._model.close_stream()
309 self._model.render_data(html, self._item.feed.location, "text/html")
310 return
312 def _generate_html(self, body):
313 # heading
314 html = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
315 <html>
316 <head><title>title</title>
317 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
319 # stylesheet
320 if False:#Config.get_instance().reload_css:
321 html += """<link rel="stylesheet" type="text/css" href="file://"
322 """ + os.path.join(straw.defs.STRAW_DATA_DIR, "straw.css") + """/>"""
323 else:
324 html += """<style type="text/css">""" + self._view.get_css() + """</style>"""
326 # body
327 html += "</head><body>%s</body></html>" % body
328 return html
330 def _htmlify_item(self, item, encoding):
331 ret = []
333 # item header
334 ret.append('<div id="itemheader">')
335 if item.title is not None:
336 if item.link is not None:
337 ret.append('<div class="title"><a href="%s">%s</a></div>' % (item.link,item.title))
338 else:
339 ret.append(item.title)
340 ret.append('<table id="itemhead" cellspacing="0" cellpadding="0">')
341 if item.pub_date is not None:
342 timestr = helpers.format_date(
343 item.pub_date, helpers.get_date_format(), encoding)
344 ret.append(''.join(('<tr><td class="headleft" id="date">%s</td><td class="headright"></td></tr>' % str(timestr))))
346 ret.append('</table>')
347 ret.append('</div>')
349 # item body
350 if item.description is not None:
351 item.description.replace('\n', '<br/>')
352 ret.append('<div class="description">%s</div>' % item.description)
354 if len(item.publication_name):
355 ret.append('<div class="description">')
356 ret.append('<b>%s:</b> %s<br/>' % (_("Publication"),
357 item.publication_name))
358 if item.publication_volume is not None:
359 ret.append('<b>%s:</b> %s ' % (_("Volume"),
360 item.publication_volume))
361 if item.publication_number is not None:
362 ret.append('( %s )<br />' % item.publication_number)
363 if item.publication_section is not None:
364 ret.append('<b>%s:</b> %s<br />' % (_("Section"),
365 item.publication_section))
366 if item.publication_starting_page is not None:
367 ret.append('<b>%s:</b> %s' % (_("Starting Page"),
368 item.publication_starting_page))
369 ret.append('</div>')
371 # freshmeat fields
372 freshmeat_data = []
373 if item.fm_license != '' and item.fm_license is not None:
374 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
375 (_("Software license"), item.fm_license))
376 if item.fm_changes != '' and item.fm_changes is not None:
377 freshmeat_data.append('<p><b>%s:</b> %s</p>' %
378 (_("Changes"), item.fm_changes))
379 if len(freshmeat_data) > 0:
380 ret.append('<div class="description">')
381 ret.extend(freshmeat_data)
382 ret.append('</div>')
383 # empty paragraph to make sure that we get space here
384 ret.append('<p></p>')
385 # Additional information
386 dcret = []
388 # RSS Enclosures
390 if item.enclosures:
391 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><table>' % _("Enclosed Media"))
392 for enc in item.enclosures: # rss 2.0 defines only one enclosure per item
393 size = int(enc.length)
394 unit = _('bytes')
395 if size > 1024:
396 unit = _('KB')
397 size /= 1024.0
398 if size > 1024:
399 unit = _('MB')
400 size /= 1024.0
401 link_text = enc['href'].split('/')[-1]
403 # find what kind of media is that. enc[type] will have something like audio/mp3 or video/mpeg (mimetypes)
404 # 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...
405 kind = enc['type'].split('/')[0]
406 if kind == 'audio':
407 icon_name = 'audio-x-generic'
408 elif kind == 'video':
409 icon_name = 'video-x-generic'
410 elif kind == 'image':
411 icon_name = 'image-x-generic'
412 elif kind == 'application':
413 icon_name = 'binary'
414 elif kind == 'text':
415 icon_name = 'text-x-generic'
416 else:
417 icon_name = "unknown"
419 it = gtk.icon_theme_get_default()
420 ii = it.lookup_icon(icon_name, 32, gtk.ICON_LOOKUP_NO_SVG)
421 if ii:
422 imgsrc = 'file://' + ii.get_filename()
423 else:
424 imgsrc = "file://%s/%s" % (straw.defs.STRAW_DATA_DIR, 'image-missing.svg')
425 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']))
426 dcret.append('</table></td></tr>')
428 if item.creator is not None:
429 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))
430 if item.contributors is not None and len(item.contributors):
431 for c in item.contributors:
432 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
433 % (_("Contributor:"), c.name))
434 if item.source:
435 url = helpers.get_url_location(item.source['url'])
436 text = saxutils.escape(url)
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>' %
438 (_("Item Source"), url, text))
440 if item.guid is not None and item.guid != "" and item.guidislink:
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>' % (_("Permalink"), item.guid, item.guid))
442 # check for not guidislink for the case where there is guid but
443 # isPermalink="false" and yet link is the same as guid (link is
444 # always assumed to be a valid link)
445 if item.link != "" and item.link is not None and (item.link != item.guid or not item.guidislink):
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>' %
447 (_("Complete story"), item.link, item.link))
449 if item.license_urls:
450 for l in item.license_urls:
451 if l:
452 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))
454 if len(dcret):
455 ret.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
456 ret.append("".join(dcret))
457 ret.append('</table>')
458 ret.append('</div>')
459 return "".join(ret)
461 class ScrollView(MVP.WidgetView):
463 Widget: html_scrolled_window
465 def set_adjustments(self, vadjustment, hadjustment):
466 self._widget.set_hadjustment(hadjustment)
467 self._widget.set_vadjustment(vadjustment)
468 return
470 def add_child(self, widget):
471 self._widget.add(widget)
472 return
474 def show(self):
475 self._widget.show_all()
476 return
478 def adjust_vertical_adjustment(self):
479 va = self._widget.get_vadjustment()
480 va.set_value(va.lower)
481 return
483 def get_vadjustment(self):
484 return self._widget.get_vadjustment()
486 class ScrollPresenter(MVP.BasicPresenter):
488 View: ScrollView
490 def set_view_adjustments(self, vadjustment, hadjustment):
491 self._view.set_adjustments(vadjustment, hadjustment)
492 return
494 def update_view(self):
495 self._view.adjust_vertical_adjustment()
496 return
498 def scroll_down(self):
499 va = self._view.get_vadjustment()
500 old_value = va.get_value()
501 new_value = old_value + va.page_increment
502 limit = va.upper - va.page_size
503 if new_value > limit:
504 new_value = limit
505 va.set_value(new_value)
506 return new_value > old_value
508 def show_view(self):
509 self._view.show()
510 return
512 class ItemView:
513 def __init__(self, item_view_container):
514 self._encoding = helpers.get_locale_encoding()
515 widget_tree = gtk.glade.get_widget_tree(item_view_container)
516 # document = gtkhtml2.Document()
517 # widget = gtkhtml2.View()
518 import documentviews
519 engine_name = os.getenv('STRAW_HTML')
520 if engine_name:
521 engine = documentviews.create_documentviews[engine_name]
522 else:
523 engine = documentviews.default_documentview
524 document = engine(Config.straw_home())
525 widget = document.widget()
526 html_view = HTMLView(widget, document)
527 self._html_presenter = HTMLPresenter(document, html_view)
529 widget = widget_tree.get_widget('html_scrolled_window')
530 parent = widget.parent
531 parent.remove(widget)
532 widget = gtk.Frame()
533 widget.set_shadow_type(gtk.SHADOW_IN)
534 parent.add(widget)
536 # scroll_view = ScrollView(widget)
537 # self._scroll_presenter = ScrollPresenter(view=scroll_view)
539 # vadj, hadj = self._html_presenter.get_view_adjustments()
540 child = self._html_presenter.get_view_widget()
541 # self._scroll_presenter.set_view_adjustments(vadj, hadj)
542 # self._scroll_presenter.view.add_child(child)
543 # self._scroll_presenter.show_view()
545 widget.add(child)
546 # gtkmozembed visibility workaround
547 child.show()
548 widget.show()
549 # gtkhtml visibility workaround
550 widget.show_all()
552 #config = Config.get_instance()
553 #elf._html_presenter.set_view_magnification(config.text_magnification)
555 def itemlist_selection_changed(self, selection, column):
556 (model, treeiter) = selection.get_selected()
557 if not treeiter: return # .. or display a template page?
558 item = model.get_value(treeiter, column)
559 self._display_item(item)
561 def _display_item(self, item):
562 self._html_presenter.display_item(item, self._encoding)
563 # self._scroll_presenter.update_view()
565 def display_empty_feed(self):
566 self._html_presenter.display_empty_feed()
568 def display_empty_search(self):
569 self._html_presenter.display_empty_search()
571 def scroll_down(self):
572 # return self._scroll_presenter.scroll_down()
573 return False
575 def get_selected_text(self):
576 return self._html_presenter.get_html_text_selection()