Work on feed properties dialog, added some properties to Feed model class.
[straw.git] / straw / ItemView.py
blob2133cf9b1b42639d75599bf606980b268e1300c2
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.defs
36 from MessageManager import post_status_message
38 from straw import helpers
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()
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 helpers.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(straw.defs.STRAW_DATA_DIR, "straw.css")).read()
147 # derive colors for blockquotes and header boxes from
148 # the GTK+ theme
149 # the GTK+ colors are in the range 0-65535
150 bgdivisor = int(65535/(9.0/10))
151 fgdivisor = 65535
152 borderdivisor = int(65535/(6.0/10))
153 gtkstyle = self._widget.get_style()
155 headerbg = "background-color: #%.2x%.2x%.2x;" % (
156 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / bgdivisor,
157 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / bgdivisor,
158 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / bgdivisor)
160 headerfg = "color: #%.2x%.2x%.2x;" % (
161 (gtkstyle.fg[gtk.STATE_NORMAL].red * 255) / fgdivisor,
162 (gtkstyle.fg[gtk.STATE_NORMAL].blue * 255) / fgdivisor,
163 (gtkstyle.fg[gtk.STATE_NORMAL].green * 255) / fgdivisor)
165 headerborder = "border-color: #%.2x%.2x%.2x;" % (
166 (gtkstyle.bg[gtk.STATE_NORMAL].red * 255) / borderdivisor,
167 (gtkstyle.bg[gtk.STATE_NORMAL].blue * 255) / borderdivisor,
168 (gtkstyle.bg[gtk.STATE_NORMAL].green * 255) / borderdivisor)
171 css = re.sub(r"/\*\*\*HEADERBG\*/", headerbg, css)
172 css = re.sub(r"/\*\*\*HEADERFG\*/", headerfg, css)
173 css = re.sub(r"/\*\*\*HEADERBORDERCOLOR\*/",
174 headerborder, css)
176 css = re.sub(r"/\*\*\*BQUOTEBG\*/", headerbg, css)
177 css = re.sub(r"/\*\*\*BQUOTEFG\*/", headerfg, css)
178 css = re.sub(r"/\*\*\*BQUOTEBORDERCOLOR\*/",
179 headerborder, css)
180 self._css = css
181 return
183 def report_error(self, title, description):
184 helpers.report_error(title, description)
186 def get_css(self):
187 return self._css
189 def get_adjustments(self):
190 return (self._widget.get_vadjustment(), self._widget.get_hadjustment())
192 def get_widget(self):
193 return self._widget
195 def connect_widget_signal(self, signal, callback):
196 self._widget.connect(signal, callback)
198 def set_on_url(self, url):
199 self._url = url
201 def set_magnification(self, size):
202 # self._widget.set_magnification(size)
203 pass
205 class HTMLPresenter(MVP.BasicPresenter):
207 Model: gtkhtml2.Document
208 View: HTMLView
210 def _initialize(self):
211 # self._model.connect('request-url', self._request_url)
212 # self._view.connect_widget_signal('on_url', self._on_url)
213 self._item = None
215 def _on_url(self, view, url):
216 self._view.set_on_url(url)
217 if url:
218 url = helpers.complete_url(url, self._item.feed.location)
219 else:
220 url = ""
221 post_status_message(url)
222 return
224 def _request_url(self, document, url, stream):
225 feed = self._item.feed
226 try:
227 try:
228 url = helpers.complete_url(url, self._item.feed.location)
229 if urlparse.urlparse(url)[0] == 'file':
230 # local URL, don't use the cache.
231 f = file(urlparse.urlparse(url)[2])
232 stream.write(f.read())
233 f.close()
234 else:
235 image = ImageCache.cache[url]
236 stream.write(image.get_data())
237 except Exception, ex:
238 #error.log("Error reading image in %s: %s" % (url, ex))
239 pass
240 finally:
241 stream.close()
242 stream = None
243 return
245 def set_clipboard_text(self, text):
246 helpers.set_clipboard_text(text)
248 def get_html_text_selection(self):
249 return gtkhtml2.html_selection_get_text(self.view.widget)
251 def display_url(self, link):
252 link = link.strip()
253 link = helpers.complete_url(link, self._item.feed.location)
254 try:
255 helpers.url_show(link)
256 except Exception, ex:
257 print 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(straw.defs.STRAW_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 = helpers.format_date(
339 item.pub_date, helpers.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 len(item.publication_name):
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" % (straw.defs.STRAW_DATA_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 = helpers.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 = helpers.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 engine_name = os.getenv('STRAW_HTML')
516 if engine_name:
517 engine = documentviews.create_documentviews[engine_name]
518 else:
519 engine = documentviews.default_documentview
520 document = engine(Config.straw_home())
521 widget = document.widget()
522 html_view = HTMLView(widget, document)
523 self._html_presenter = HTMLPresenter(document, html_view)
525 widget = widget_tree.get_widget('html_scrolled_window')
526 parent = widget.parent
527 parent.remove(widget)
528 widget = gtk.Frame()
529 widget.set_shadow_type(gtk.SHADOW_IN)
530 parent.add(widget)
532 # scroll_view = ScrollView(widget)
533 # self._scroll_presenter = ScrollPresenter(view=scroll_view)
535 # vadj, hadj = self._html_presenter.get_view_adjustments()
536 child = self._html_presenter.get_view_widget()
537 # self._scroll_presenter.set_view_adjustments(vadj, hadj)
538 # self._scroll_presenter.view.add_child(child)
539 # self._scroll_presenter.show_view()
541 widget.add(child)
542 # gtkmozembed visibility workaround
543 child.show()
544 widget.show()
545 # gtkhtml visibility workaround
546 widget.show_all()
548 config = Config.get_instance()
549 self._html_presenter.set_view_magnification(config.text_magnification)
551 def itemlist_selection_changed(self, selection, column):
552 (model, treeiter) = selection.get_selected()
553 if not treeiter: return # .. or display a template page?
554 item = model.get_value(treeiter, column)
555 self._display_item(item)
557 def _display_item(self, item):
558 self._html_presenter.display_item(item, self._encoding)
559 # self._scroll_presenter.update_view()
561 def display_empty_feed(self):
562 self._html_presenter.display_empty_feed()
564 def display_empty_search(self):
565 self._html_presenter.display_empty_search()
567 def scroll_down(self):
568 # return self._scroll_presenter.scroll_down()
569 return False
571 def get_selected_text(self):
572 return self._html_presenter.get_html_text_selection()