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. """
24 from xml
.sax
import saxutils
34 import FeedCategoryList
41 class HTMLView(MVP
.View
):
44 Model: gtkhtml.Document
45 Presenter: HTMLPresenter
49 <popup name=\"htmlview_popup\">
50 <menuitem action=\"open_link_location\"/>
51 <menuitem action=\"copy_link_location\"/>
52 <menuitem action=\"copy_text\"/>
53 <menuitem action=\"subscribe\"/>
54 <menuitem action=\"zoom_in\"/>
55 <menuitem action=\"zoom_out\"/>
56 <menuitem action=\"zoom_100\"/>
61 def _initialize(self
):
62 self
._widget
.set_document(self
._model
)
63 # Make the article view focusable. gtkhtml2.View scrolls to the first
64 # link of the document which sometimes makes the first part of the
65 # document unviewable if the first link is found at the bottom part of
66 # the document. This is good for now since the template includes a
67 # link at the topmost page of the view.
68 self
._widget
.set_property('can-focus', True)
69 self
._widget
.get_vadjustment().set_value(0)
70 self
._widget
.connect('button_press_event', self
._button
_press
_event
)
73 self
._text
_selection
= None
79 def _connect_signals(self
, widget
):
80 " We don't 'auto-connect' since self._widget is not a glade file"
87 def _button_press_event(self
, widget
, event
):
90 self
._presenter
.display_url(self
._url
)
91 self
._url
= self
._text
_selection
= None
92 elif event
.button
== 3:
93 self
._text
_selection
= self
._presenter
.get_html_text_selection()
94 action_group
= self
._uimanager
.get_action_groups()[-1]
95 link_action
= action_group
.get_action('copy_link_location')
96 open_link_action
= action_group
.get_action('open_link_location')
97 text_action
= action_group
.get_action('copy_text')
98 subs_action
= action_group
.get_action('subscribe')
99 if self
._url
and self
._text
_selection
:
100 link_action
.set_visible(True)
101 open_link_action
.set_visible(True)
102 subs_action
.set_visible(True)
103 text_action
.set_visible(True)
105 text_action
.set_visible(False)
106 link_action
.set_visible(True)
107 open_link_action
.set_visible(True)
108 subs_action
.set_visible(True)
109 elif self
._text
_selection
:
110 link_action
.set_visible(False)
111 open_link_action
.set_visible(False)
112 subs_action
.set_visible(False)
113 text_action
.set_visible(True)
115 link_action
.set_visible(False)
116 open_link_action
.set_visible(False)
117 subs_action
.set_visible(False)
118 text_action
.set_visible(False)
119 self
._uimanager
.ensure_update()
120 self
._popup
.popup(None, None, None, event
.button
,
121 gtk
.get_current_event_time())
124 def _create_popup(self
):
126 ("copy_text", gtk
.STOCK_COPY
, "_Copy", None, None,
128 ("copy_link_location", None, "_Copy Link Location",
129 None, None, self
._on
_copy
_url
),
130 ("open_link_location", None, "_Open Link",
131 None, None, self
._open
_link
_location
),
132 ("subscribe", None, "_Subscribe", None, None,
134 ("zoom_in", gtk
.STOCK_ZOOM_IN
, "Zoom _In", None, None,
135 lambda *args
: self
._on
_magnify
("in")),
136 ("zoom_out", gtk
.STOCK_ZOOM_OUT
, "Zoom _Out", None, None,
137 lambda *args
: self
._on
_magnify
("out")),
138 ("zoom_100", gtk
.STOCK_ZOOM_100
, "_Normal Size", None, None,
139 lambda *args
: self
._on
_magnify
("reset"))
141 self
._uimanager
= gtk
.UIManager()
142 actiongroup
= gtk
.ActionGroup("HtmlViewActions")
143 actiongroup
.add_actions(actions
)
144 self
._uimanager
.insert_action_group(actiongroup
, -1)
145 self
._uimanager
.add_ui_from_string(self
.ui
)
146 self
._popup
= self
._uimanager
.get_widget("/htmlview_popup")
149 def _on_magnify(self
, action
):
151 self
._widget
.zoom_in()
152 elif action
== "out":
153 self
._widget
.zoom_out()
155 self
._widget
.zoom_reset()
156 config
= Config
.get_instance()
157 config
.text_magnification
= self
._widget
.get_magnification()
159 def _on_copy_text(self
, *args
):
160 self
._presenter
.set_clipboard_text(self
._text
_selection
)
161 gtkhtml2
.html_selection_clear(self
._widget
)
164 def _on_copy_url(self
, *args
):
165 self
._presenter
.set_clipboard_text(self
._url
)
168 def _open_link_location(self
, *args
):
169 utils
.url_show(self
._url
)
171 def _subscribe(self
, *args
):
173 subscribe
.show(url
=self
._url
)
178 if self
._css
is None:
179 css
= file(os
.path
.join(
180 utils
.find_data_dir(), "straw.css")).read()
181 # derive colors for blockquotes and header boxes from
183 # the GTK+ colors are in the range 0-65535
184 bgdivisor
= int(65535/(9.0/10))
186 borderdivisor
= int(65535/(6.0/10))
187 gtkstyle
= self
._widget
.get_style()
189 headerbg
= "background-color: #%.2x%.2x%.2x;" % (
190 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].red
* 255) / bgdivisor
,
191 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].blue
* 255) / bgdivisor
,
192 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].green
* 255) / bgdivisor
)
194 headerfg
= "color: #%.2x%.2x%.2x;" % (
195 (gtkstyle
.fg
[gtk
.STATE_NORMAL
].red
* 255) / fgdivisor
,
196 (gtkstyle
.fg
[gtk
.STATE_NORMAL
].blue
* 255) / fgdivisor
,
197 (gtkstyle
.fg
[gtk
.STATE_NORMAL
].green
* 255) / fgdivisor
)
199 headerborder
= "border-color: #%.2x%.2x%.2x;" % (
200 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].red
* 255) / borderdivisor
,
201 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].blue
* 255) / borderdivisor
,
202 (gtkstyle
.bg
[gtk
.STATE_NORMAL
].green
* 255) / borderdivisor
)
205 css
= re
.sub(r
"/\*\*\*HEADERBG\*/", headerbg
, css
)
206 css
= re
.sub(r
"/\*\*\*HEADERFG\*/", headerfg
, css
)
207 css
= re
.sub(r
"/\*\*\*HEADERBORDERCOLOR\*/",
210 css
= re
.sub(r
"/\*\*\*BQUOTEBG\*/", headerbg
, css
)
211 css
= re
.sub(r
"/\*\*\*BQUOTEFG\*/", headerfg
, css
)
212 css
= re
.sub(r
"/\*\*\*BQUOTEBORDERCOLOR\*/",
217 def report_error(self
, title
, description
):
218 dialogs
.report_error(title
, description
)
223 def get_adjustments(self
):
224 return (self
._widget
.get_vadjustment(), self
._widget
.get_hadjustment())
226 def get_widget(self
):
229 def connect_widget_signal(self
, signal
, callback
):
230 self
._widget
.connect(signal
, callback
)
232 def set_on_url(self
, url
):
235 def set_magnification(self
, size
):
236 self
._widget
.set_magnification(size
)
238 class HTMLPresenter(MVP
.BasicPresenter
):
240 Model: gtkhtml2.Document
243 def _initialize(self
):
244 self
._model
.connect('request-url', self
._request
_url
)
245 self
._view
.connect_widget_signal('on_url', self
._on
_url
)
248 def _on_url(self
, view
, url
):
249 self
._view
.set_on_url(url
)
251 url
= utils
.complete_url(url
, self
._item
.feed
.location
)
254 mmgr
= MessageManager
.get_instance()
255 mmgr
.post_message(url
)
258 def _request_url(self
, document
, url
, stream
):
259 feed
= self
._item
.feed
262 url
= utils
.complete_url(url
, self
._item
.feed
.location
)
263 if urlparse
.urlparse(url
)[0] == 'file':
264 # local URL, don't use the cache.
265 f
= file(urlparse
.urlparse(url
)[2])
266 stream
.write(f
.read())
269 image
= ImageCache
.cache
[url
]
270 stream
.write(image
.get_data())
271 except Exception, ex
:
272 error
.log("Error reading image in %s: %s" % (url
, ex
))
278 def set_clipboard_text(self
, text
):
279 utils
.set_clipboard_text(text
)
281 def get_html_text_selection(self
):
282 return gtkhtml2
.html_selection_get_text(self
.view
.widget
)
284 def display_url(self
, link
):
286 link
= utils
.complete_url(link
, self
._item
.feed
.location
)
289 except Exception, ex
:
290 self
._view
.report_error(_("Error Loading Browser"),
291 _("Please check your browser settings and try again."))
294 def get_view_adjustments(self
):
295 return self
._view
.get_adjustments()
297 def get_view_widget(self
):
298 return self
._view
.get_widget()
300 def display_item(self
, item
, encoding
):
302 content
= self
._htmlify
_item
(item
, encoding
)
303 self
._prepare
_stream
(content
)
306 def display_empty_feed(self
):
307 content
= """<p class=\"emptyfeed\"/>"""# _("No data yet, need to poll first.") </p>"""
308 self
._prepare
_stream
(content
)
310 def display_empty_search(self
):
312 <h2>Search Subscriptions</h2>
314 Begin searching by typing your text on the text box on the left side.
317 self
._prepare
_stream
(content
)
320 def set_view_magnification(self
, size
):
321 self
.view
.set_magnification(size
)
323 def _encode_for_html(self
, unicode_data
, encoding
='utf-8'):
324 """ From Python Cookbook, 2/ed, section 1.23
325 'html_replace' is in the utils module
327 return unicode_data
.encode(encoding
, 'html_replace')
329 def _prepare_stream(self
, content
):
330 html
= self
._generate
_html
(content
)
331 html
= self
._encode
_for
_html
(html
)
333 self
._model
.open_stream("text/html")
334 self
._model
.write_stream(html
)
335 self
._model
.close_stream()
338 def _generate_html(self
, body
):
340 html
= """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
342 <head><title>title</title>
343 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
346 if Config
.get_instance().reload_css
:
347 html
+= """<link rel="stylesheet" type="text/css" href="file://"
348 """ + os
.path
.join(utils
.find_data_dir(), "straw.css") + """/>"""
350 html
+= """<style type="text/css">""" + self
._view
.get_css() + """</style>"""
353 html
+= "</head><body>%s</body></html>" % body
356 def _htmlify_item(self
, item
, encoding
):
361 ret
.append('<div id="itemheader">')
362 if item
.title
is not None:
363 if item
.link
is not None:
364 ret
.append('<div class="title"><a href="%s">%s</a></div>' % (item
.link
,item
.title
))
366 ret
.append(item
.title
)
367 ret
.append('<table id="itemhead" cellspacing="0" cellpadding="0">')
368 if item
.pub_date
is not None:
369 timestr
= utils
.format_date(
370 item
.pub_date
, utils
.get_date_format(), encoding
)
371 ret
.append(''.join(('<tr><td class="headleft" id="date">%s</td><td class="headright"></td></tr>' % str(timestr
))))
373 ret
.append('</table>')
377 if item
.description
is not None:
378 item
.description
.replace('\n', '<br/>')
379 ret
.append('<div class="description">%s</div>' % item
.description
)
381 if item
.publication_name
is not None:
382 ret
.append('<div class="description">')
383 ret
.append('<b>%s:</b> %s<br/>' % (_("Publication"),
384 item
.publication_name
))
385 if item
.publication_volume
is not None:
386 ret
.append('<b>%s:</b> %s ' % (_("Volume"),
387 item
.publication_volume
))
388 if item
.publication_number
is not None:
389 ret
.append('( %s )<br />' % item
.publication_number
)
390 if item
.publication_section
is not None:
391 ret
.append('<b>%s:</b> %s<br />' % (_("Section"),
392 item
.publication_section
))
393 if item
.publication_starting_page
is not None:
394 ret
.append('<b>%s:</b> %s' % (_("Starting Page"),
395 item
.publication_starting_page
))
400 if item
.fm_license
!= '' and item
.fm_license
is not None:
401 freshmeat_data
.append('<p><b>%s:</b> %s</p>' %
402 (_("Software license"), item
.fm_license
))
403 if item
.fm_changes
!= '' and item
.fm_changes
is not None:
404 freshmeat_data
.append('<p><b>%s:</b> %s</p>' %
405 (_("Changes"), item
.fm_changes
))
406 if len(freshmeat_data
) > 0:
407 ret
.append('<div class="description">')
408 ret
.extend(freshmeat_data
)
410 # empty paragraph to make sure that we get space here
411 ret
.append('<p></p>')
412 # Additional information
418 dcret
.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><table>' % _("Enclosed Media"))
419 for enc
in item
.enclosures
: # rss 2.0 defines only one enclosure per item
420 size
= int(enc
.length
)
428 link_text
= enc
['href'].split('/')[-1]
430 # find what kind of media is that. enc[type] will have something like audio/mp3 or video/mpeg (mimetypes)
431 # 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...
432 kind
= enc
['type'].split('/')[0]
434 icon_name
= 'audio-x-generic'
435 elif kind
== 'video':
436 icon_name
= 'video-x-generic'
437 elif kind
== 'image':
438 icon_name
= 'image-x-generic'
439 elif kind
== 'application':
442 icon_name
= 'text-x-generic'
444 icon_name
= "unknown"
446 it
= gtk
.icon_theme_get_default()
447 ii
= it
.lookup_icon(icon_name
, 32, gtk
.ICON_LOOKUP_NO_SVG
)
449 imgsrc
= 'file://' + ii
.get_filename()
451 imgsrc
= "file://%s/%s" % (utils
.find_image_dir(), 'image-missing.svg')
452 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']))
453 dcret
.append('</table></td></tr>')
455 if item
.creator
is not None:
456 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
))
457 if item
.contributors
is not None and len(item
.contributors
):
458 for c
in item
.contributors
:
459 dcret
.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
460 % (_("Contributor:"), c
.name
))
462 url
= utils
.get_url_location(item
.source
['url'])
463 text
= saxutils
.escape(url
)
464 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>' %
465 (_("Item Source"), url
, text
))
467 if item
.guid
is not None and item
.guid
!= "" and item
.guidislink
:
468 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
))
469 # check for not guidislink for the case where there is guid but
470 # isPermalink="false" and yet link is the same as guid (link is
471 # always assumed to be a valid link)
472 if item
.link
!= "" and item
.link
is not None and (item
.link
!= item
.guid
or not item
.guidislink
):
473 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>' %
474 (_("Complete story"), item
.link
, item
.link
))
476 if item
.license_urls
:
477 for l
in item
.license_urls
:
479 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
))
482 ret
.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
483 ret
.append("".join(dcret
))
484 ret
.append('</table>')
488 class ScrollView(MVP
.WidgetView
):
490 Widget: html_scrolled_window
492 def set_adjustments(self
, vadjustment
, hadjustment
):
493 self
._widget
.set_hadjustment(hadjustment
)
494 self
._widget
.set_vadjustment(vadjustment
)
497 def add_child(self
, widget
):
498 self
._widget
.add(widget
)
502 self
._widget
.show_all()
505 def adjust_vertical_adjustment(self
):
506 va
= self
._widget
.get_vadjustment()
507 va
.set_value(va
.lower
)
510 def get_vadjustment(self
):
511 return self
._widget
.get_vadjustment()
513 class ScrollPresenter(MVP
.BasicPresenter
):
517 def set_view_adjustments(self
, vadjustment
, hadjustment
):
518 self
._view
.set_adjustments(vadjustment
, hadjustment
)
521 def update_view(self
):
522 self
._view
.adjust_vertical_adjustment()
525 def scroll_down(self
):
526 va
= self
._view
.get_vadjustment()
527 old_value
= va
.get_value()
528 new_value
= old_value
+ va
.page_increment
529 limit
= va
.upper
- va
.page_size
530 if new_value
> limit
:
532 va
.set_value(new_value
)
533 return new_value
> old_value
540 def __init__(self
, item_view_container
):
541 self
._encoding
= utils
.get_locale_encoding()
542 widget_tree
= gtk
.glade
.get_widget_tree(item_view_container
)
543 document
= gtkhtml2
.Document()
544 widget
= gtkhtml2
.View()
545 html_view
= HTMLView(widget
, document
)
546 self
._html
_presenter
= HTMLPresenter(document
, html_view
)
548 widget
= widget_tree
.get_widget('html_scrolled_window')
549 scroll_view
= ScrollView(widget
)
550 self
._scroll
_presenter
= ScrollPresenter(view
=scroll_view
)
552 vadj
, hadj
= self
._html
_presenter
.get_view_adjustments()
553 child
= self
._html
_presenter
.get_view_widget()
554 self
._scroll
_presenter
.set_view_adjustments(vadj
, hadj
)
555 self
._scroll
_presenter
.view
.add_child(child
)
556 self
._scroll
_presenter
.show_view()
558 config
= Config
.get_instance()
559 self
._html
_presenter
.set_view_magnification(config
.text_magnification
)
561 def itemlist_selection_changed(self
, selection
, column
):
562 (model
, treeiter
) = selection
.get_selected()
563 item
= model
.get_value(treeiter
, column
)
564 self
._display
_item
(item
)
566 def _display_item(self
, item
):
567 self
._html
_presenter
.display_item(item
, self
._encoding
)
568 self
._scroll
_presenter
.update_view()
570 def display_empty_feed(self
):
571 self
._html
_presenter
.display_empty_feed()
573 def display_empty_search(self
):
574 self
._html
_presenter
.display_empty_search()
576 def scroll_down(self
):
577 return self
._scroll
_presenter
.scroll_down()
579 def get_selected_text(self
):
580 return self
._html
_presenter
.get_html_text_selection()