3 Module for displaying an item to the user
5 __copyright__
= "Copyright (c) 2002-2005 Free Software Foundation, Inc."
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
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
38 log
= error
.get_logger()
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
)
54 self
._text
_selection
= None
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')
77 def _connect_signals(self
, *args
): pass
79 def _button_press_event(self
, widget
, event
):
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)
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)
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())
115 def on_magnify(self
, action
):
117 self
._widget
.zoom_in()
118 elif action
== "out":
119 self
._widget
.zoom_out()
121 self
._widget
.zoom_reset()
123 # TODO: use text magnification
124 #config.text_magnification = self._widget.get_magnification()
126 def on_copy_text_cb(self
, *args
):
128 self
._presenter
.set_clipboard_text(self
._text
_selection
)
130 def on_copy_url_cb(self
, *args
):
131 self
._presenter
.set_clipboard_text(self
._url
)
133 def on_open_link_location_cb(self
, *args
):
134 helpers
.url_show(self
._url
)
136 def on_subscribe_cb(self
, *args
):
138 subscribe
.show(url
=self
._url
)
141 if self
._css
is None:
142 css
= file(os
.path
.join(straw
.defs
.STRAW_DATA_DIR
, "straw.css")).read()
143 # derive colors for blockquotes and header boxes from
145 # the GTK+ colors are in the range 0-65535
146 bgdivisor
= int(65535/(9.0/10))
148 borderdivisor
= int(65535/(6.0/10))
149 gtkstyle
= self
._widget
.get_style()
151 headerbg
= "background-color: #%.2x%.2x%.2x;" % (
152 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].red
* 255) / bgdivisor
,
153 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].blue
* 255) / bgdivisor
,
154 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].green
* 255) / bgdivisor
)
156 headerfg
= "color: #%.2x%.2x%.2x;" % (
157 (gtkstyle
.fg
[gtk
.STATE_NORMAL
].red
* 255) / fgdivisor
,
158 (gtkstyle
.fg
[gtk
.STATE_NORMAL
].blue
* 255) / fgdivisor
,
159 (gtkstyle
.fg
[gtk
.STATE_NORMAL
].green
* 255) / fgdivisor
)
161 headerborder
= "border-color: #%.2x%.2x%.2x;" % (
162 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].red
* 255) / borderdivisor
,
163 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].blue
* 255) / borderdivisor
,
164 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].green
* 255) / borderdivisor
)
167 css
= re
.sub(r
"/\*\*\*HEADERBG\*/", headerbg
, css
)
168 css
= re
.sub(r
"/\*\*\*HEADERFG\*/", headerfg
, css
)
169 css
= re
.sub(r
"/\*\*\*HEADERBORDERCOLOR\*/",
172 css
= re
.sub(r
"/\*\*\*BQUOTEBG\*/", headerbg
, css
)
173 css
= re
.sub(r
"/\*\*\*BQUOTEFG\*/", headerfg
, css
)
174 css
= re
.sub(r
"/\*\*\*BQUOTEBORDERCOLOR\*/",
179 def report_error(self
, title
, description
):
180 helpers
.report_error(title
, description
)
185 def get_adjustments(self
):
186 return (self
._widget
.get_vadjustment(), self
._widget
.get_hadjustment())
188 def get_widget(self
):
191 def connect_widget_signal(self
, signal
, callback
):
192 self
._widget
.connect(signal
, callback
)
194 def set_on_url(self
, url
):
197 def set_magnification(self
, size
):
198 # self._widget.set_magnification(size)
201 class HTMLPresenter(MVP
.BasicPresenter
):
203 Model: gtkhtml2.Document
206 def _initialize(self
):
207 # self._model.connect('request-url', self._request_url)
208 # self._view.connect_widget_signal('on_url', self._on_url)
209 self
._model
.connect('status_changed', self
._status
_changed
)
210 self
._model
.connect('open_uri', self
._open
_uri
)
213 def _open_uri(self
, document
, url
):
215 self
.display_url(url
)
219 def _status_changed(self
, document
, status
):
220 post_status_message(status
)
222 def _on_url(self
, view
, url
):
223 self
._view
.set_on_url(url
)
225 url
= helpers
.complete_url(url
, self
._item
.feed
.location
)
228 post_status_message(url
)
231 def _request_url(self
, document
, url
, stream
):
232 feed
= self
._item
.feed
235 url
= helpers
.complete_url(url
, self
._item
.feed
.location
)
236 if urlparse
.urlparse(url
)[0] == 'file':
237 # local URL, don't use the cache.
238 f
= file(urlparse
.urlparse(url
)[2])
239 stream
.write(f
.read())
242 image
= ImageCache
.cache
[url
]
243 stream
.write(image
.get_data())
244 except Exception, ex
:
245 log
.error("Error reading image in %s: %s" % (url
, ex
))
251 def set_clipboard_text(self
, text
):
252 helpers
.set_clipboard_text(text
)
254 def get_html_text_selection(self
):
257 def display_url(self
, link
):
259 link
= helpers
.complete_url(link
, self
._item
.feed
.location
)
261 helpers
.url_show(link
)
262 except Exception, ex
:
264 self
._view
.report_error(_("Error Loading Browser"),
265 _("Please check your browser settings and try again."))
268 def get_view_adjustments(self
):
269 return self
._view
.get_adjustments()
271 def get_view_widget(self
):
272 return self
._view
.get_widget()
274 def display_item(self
, item
, encoding
):
276 content
= self
._htmlify
_item
(item
, encoding
)
277 self
._prepare
_stream
(content
)
280 def display_empty_feed(self
):
281 content
= """<p class=\"emptyfeed\"/>"""# _("No data yet, need to poll first.") </p>"""
282 self
._prepare
_stream
(content
)
284 def display_empty_search(self
):
286 <h2>Search Subscriptions</h2>
288 Begin searching by typing your text on the text box on the left side.
291 self
._prepare
_stream
(content
)
294 def set_view_magnification(self
, size
):
295 self
.view
.set_magnification(size
)
297 def _encode_for_html(self
, unicode_data
, encoding
='utf-8'):
298 """ From Python Cookbook, 2/ed, section 1.23
299 'html_replace' is in the utils module
301 return unicode_data
.encode(encoding
, 'html_replace')
303 def _prepare_stream(self
, content
):
304 html
= self
._generate
_html
(content
)
305 html
= self
._encode
_for
_html
(html
)
306 # self._model.clear()
307 # self._model.open_stream("text/html")
308 # self._model.write_stream(html)
309 # self._model.close_stream()
310 self
._model
.render_data(html
, self
._item
.feed
.location
, "text/html")
313 def _generate_html(self
, body
):
315 html
= """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
317 <head><title>title</title>
318 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
321 if False:#Config.get_instance().reload_css:
322 html
+= """<link rel="stylesheet" type="text/css" href="file://"
323 """ + os
.path
.join(straw
.defs
.STRAW_DATA_DIR
, "straw.css") + """/>"""
325 html
+= """<style type="text/css">""" + self
._view
.get_css() + """</style>"""
328 html
+= "</head><body>%s</body></html>" % body
331 def _htmlify_item(self
, item
, encoding
):
335 ret
.append('<div id="itemheader">')
336 if item
.title
is not None:
337 if item
.link
is not None:
338 ret
.append('<div class="title"><a href="%s">%s</a></div>' % (item
.link
,item
.title
))
340 ret
.append(item
.title
)
341 ret
.append('<table id="itemhead" cellspacing="0" cellpadding="0">')
342 if item
.pub_date
is not None:
343 timestr
= helpers
.format_date(
344 item
.pub_date
, helpers
.get_date_format(), encoding
)
345 ret
.append(''.join(('<tr><td class="headleft" id="date">%s</td><td class="headright"></td></tr>' % str(timestr
))))
347 ret
.append('</table>')
351 if item
.description
is not None:
352 item
.description
.replace('\n', '<br/>')
353 ret
.append('<div class="description">%s</div>' % item
.description
)
355 if len(item
.publication_name
):
356 ret
.append('<div class="description">')
357 ret
.append('<b>%s:</b> %s<br/>' % (_("Publication"),
358 item
.publication_name
))
359 if item
.publication_volume
is not None:
360 ret
.append('<b>%s:</b> %s ' % (_("Volume"),
361 item
.publication_volume
))
362 if item
.publication_number
is not None:
363 ret
.append('( %s )<br />' % item
.publication_number
)
364 if item
.publication_section
is not None:
365 ret
.append('<b>%s:</b> %s<br />' % (_("Section"),
366 item
.publication_section
))
367 if item
.publication_starting_page
is not None:
368 ret
.append('<b>%s:</b> %s' % (_("Starting Page"),
369 item
.publication_starting_page
))
374 if item
.fm_license
!= '' and item
.fm_license
is not None:
375 freshmeat_data
.append('<p><b>%s:</b> %s</p>' %
376 (_("Software license"), item
.fm_license
))
377 if item
.fm_changes
!= '' and item
.fm_changes
is not None:
378 freshmeat_data
.append('<p><b>%s:</b> %s</p>' %
379 (_("Changes"), item
.fm_changes
))
380 if len(freshmeat_data
) > 0:
381 ret
.append('<div class="description">')
382 ret
.extend(freshmeat_data
)
384 # empty paragraph to make sure that we get space here
385 ret
.append('<p></p>')
386 # Additional information
392 dcret
.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><table>' % _("Enclosed Media"))
393 for enc
in item
.enclosures
: # rss 2.0 defines only one enclosure per item
394 size
= int(enc
.length
)
402 link_text
= enc
['href'].split('/')[-1]
404 # find what kind of media is that. enc[type] will have something like audio/mp3 or video/mpeg (mimetypes)
405 # 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...
406 kind
= enc
['type'].split('/')[0]
408 icon_name
= 'audio-x-generic'
409 elif kind
== 'video':
410 icon_name
= 'video-x-generic'
411 elif kind
== 'image':
412 icon_name
= 'image-x-generic'
413 elif kind
== 'application':
416 icon_name
= 'text-x-generic'
418 icon_name
= "unknown"
420 it
= gtk
.icon_theme_get_default()
421 ii
= it
.lookup_icon(icon_name
, 32, gtk
.ICON_LOOKUP_NO_SVG
)
423 imgsrc
= 'file://' + ii
.get_filename()
425 imgsrc
= "file://%s/%s" % (straw
.defs
.STRAW_DATA_DIR
, 'image-missing.svg')
426 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']))
427 dcret
.append('</table></td></tr>')
429 if item
.creator
is not None:
430 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
))
431 if item
.contributors
is not None and len(item
.contributors
):
432 for c
in item
.contributors
:
433 dcret
.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
434 % (_("Contributor:"), c
.name
))
436 url
= helpers
.get_url_location(item
.source
['url'])
437 text
= saxutils
.escape(url
)
438 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>' %
439 (_("Item Source"), url
, text
))
441 if item
.guid
is not None and item
.guid
!= "" and 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>' % (_("Permalink"), item
.guid
, item
.guid
))
443 # check for not guidislink for the case where there is guid but
444 # isPermalink="false" and yet link is the same as guid (link is
445 # always assumed to be a valid link)
446 if item
.link
!= "" and item
.link
is not None and (item
.link
!= item
.guid
or not item
.guidislink
):
447 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>' %
448 (_("Complete story"), item
.link
, item
.link
))
450 if item
.license_urls
:
451 for l
in item
.license_urls
:
453 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
))
456 ret
.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
457 ret
.append("".join(dcret
))
458 ret
.append('</table>')
462 class ScrollView(MVP
.WidgetView
):
464 Widget: html_scrolled_window
466 def set_adjustments(self
, vadjustment
, hadjustment
):
467 self
._widget
.set_hadjustment(hadjustment
)
468 self
._widget
.set_vadjustment(vadjustment
)
471 def add_child(self
, widget
):
472 self
._widget
.add(widget
)
476 self
._widget
.show_all()
479 def adjust_vertical_adjustment(self
):
480 va
= self
._widget
.get_vadjustment()
481 va
.set_value(va
.lower
)
484 def get_vadjustment(self
):
485 return self
._widget
.get_vadjustment()
487 class ScrollPresenter(MVP
.BasicPresenter
):
491 def set_view_adjustments(self
, vadjustment
, hadjustment
):
492 self
._view
.set_adjustments(vadjustment
, hadjustment
)
495 def update_view(self
):
496 self
._view
.adjust_vertical_adjustment()
499 def scroll_down(self
):
500 va
= self
._view
.get_vadjustment()
501 old_value
= va
.get_value()
502 new_value
= old_value
+ va
.page_increment
503 limit
= va
.upper
- va
.page_size
504 if new_value
> limit
:
506 va
.set_value(new_value
)
507 return new_value
> old_value
514 def __init__(self
, item_view_container
):
515 self
._encoding
= helpers
.get_locale_encoding()
516 widget_tree
= gtk
.glade
.get_widget_tree(item_view_container
)
517 # document = gtkhtml2.Document()
518 # widget = gtkhtml2.View()
520 engine_name
= os
.getenv('STRAW_HTML')
522 log
.debug("STRAW_HTML = %s" % engine_name
)
525 engine
= documentviews
.create_documentviews
[engine_name
]
527 engine
= documentviews
.default_documentview
529 document
= engine(Config
.straw_home())
531 log
.debug("document = %s" % document
)
533 widget
= document
.widget()
534 html_view
= HTMLView(widget
, document
)
535 self
._html
_presenter
= HTMLPresenter(document
, html_view
)
537 widget
= widget_tree
.get_widget('html_scrolled_window')
538 parent
= widget
.parent
539 parent
.remove(widget
)
541 widget
.set_shadow_type(gtk
.SHADOW_IN
)
544 # scroll_view = ScrollView(widget)
545 # self._scroll_presenter = ScrollPresenter(view=scroll_view)
547 # vadj, hadj = self._html_presenter.get_view_adjustments()
548 child
= self
._html
_presenter
.get_view_widget()
549 # self._scroll_presenter.set_view_adjustments(vadj, hadj)
550 # self._scroll_presenter.view.add_child(child)
551 # self._scroll_presenter.show_view()
554 # gtkmozembed visibility workaround
557 # gtkhtml visibility workaround
560 #config = Config.get_instance()
561 #elf._html_presenter.set_view_magnification(config.text_magnification)
563 def itemlist_selection_changed(self
, selection
, column
):
564 (model
, treeiter
) = selection
.get_selected()
565 if not treeiter
: return # .. or display a template page?
566 item
= model
.get_value(treeiter
, column
)
567 self
._display
_item
(item
)
569 def _display_item(self
, item
):
570 self
._html
_presenter
.display_item(item
, self
._encoding
)
571 # self._scroll_presenter.update_view()
572 item
.set_property("is_read", True)
574 def display_empty_feed(self
):
575 self
._html
_presenter
.display_empty_feed()
577 def display_empty_search(self
):
578 self
._html
_presenter
.display_empty_search()
580 def scroll_down(self
):
581 # return self._scroll_presenter.scroll_down()
584 def get_selected_text(self
):
585 return self
._html
_presenter
.get_html_text_selection()