Updated Arabic Translation by Djihed Afifi.
[straw.git] / src / lib / ItemView.py
blobd8064397ea5144c4c49eccac60a0f25ebbdcc54d
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 gtkhtml2
31 import utils
32 import dialogs
33 import MessageManager
34 import FeedCategoryList
35 import ImageCache
36 import MVP
37 import Config
38 import error
39 import dialogs
41 class HTMLView(MVP.View):
42 """
43 Widget: gtkhtml2.View
44 Model: gtkhtml.Document
45 Presenter: HTMLPresenter
46 """
47 ui = """
48 <ui>
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\"/>
57 </popup>
58 </ui>
59 """
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)
71 self._css = None
72 self._url = None
73 self._text_selection = None
74 self._popup = None
75 self._init_css()
76 self._create_popup()
79 def _connect_signals(self, widget):
80 " We don't 'auto-connect' since self._widget is not a glade file"
81 pass
83 @property
84 def widget(self):
85 return self._widget
87 def _button_press_event(self, widget, event):
88 if event.button == 1:
89 if self._url:
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)
104 elif self._url:
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)
114 else:
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())
122 return True
124 def _create_popup(self):
125 actions = [
126 ("copy_text", gtk.STOCK_COPY, "_Copy", None, None,
127 self._on_copy_text),
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,
133 self._subscribe),
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):
150 if action == "in":
151 self._widget.zoom_in()
152 elif action == "out":
153 self._widget.zoom_out()
154 else:
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)
162 return
164 def _on_copy_url(self, *args):
165 self._presenter.set_clipboard_text(self._url)
166 return
168 def _open_link_location(self, *args):
169 utils.url_show(self._url)
171 def _subscribe(self, *args):
172 import subscribe
173 subscribe.show(url=self._url)
174 return
177 def _init_css(self):
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
182 # the GTK+ theme
183 # the GTK+ colors are in the range 0-65535
184 bgdivisor = int(65535/(9.0/10))
185 fgdivisor = 65535
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\*/",
208 headerborder, css)
210 css = re.sub(r"/\*\*\*BQUOTEBG\*/", headerbg, css)
211 css = re.sub(r"/\*\*\*BQUOTEFG\*/", headerfg, css)
212 css = re.sub(r"/\*\*\*BQUOTEBORDERCOLOR\*/",
213 headerborder, css)
214 self._css = css
215 return
217 def report_error(self, title, description):
218 dialogs.report_error(title, description)
220 def get_css(self):
221 return self._css
223 def get_adjustments(self):
224 return (self._widget.get_vadjustment(), self._widget.get_hadjustment())
226 def get_widget(self):
227 return self._widget
229 def connect_widget_signal(self, signal, callback):
230 self._widget.connect(signal, callback)
232 def set_on_url(self, url):
233 self._url = url
235 def set_magnification(self, size):
236 self._widget.set_magnification(size)
238 class HTMLPresenter(MVP.BasicPresenter):
240 Model: gtkhtml2.Document
241 View: HTMLView
243 def _initialize(self):
244 self._model.connect('request-url', self._request_url)
245 self._view.connect_widget_signal('on_url', self._on_url)
246 self._item = None
248 def _on_url(self, view, url):
249 self._view.set_on_url(url)
250 if url:
251 url = utils.complete_url(url, self._item.feed.location)
252 else:
253 url = ""
254 mmgr = MessageManager.get_instance()
255 mmgr.post_message(url)
256 return
258 def _request_url(self, document, url, stream):
259 feed = self._item.feed
260 try:
261 try:
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())
267 f.close()
268 else:
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))
273 finally:
274 stream.close()
275 stream = None
276 return
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):
285 link = link.strip()
286 link = utils.complete_url(link, self._item.feed.location)
287 try:
288 utils.url_show(link)
289 except Exception, ex:
290 self._view.report_error(_("Error Loading Browser"),
291 _("Please check your browser settings and try again."))
292 return
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):
301 self._item = item
302 content = self._htmlify_item(item, encoding)
303 self._prepare_stream(content)
304 return
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):
311 content = """
312 <h2>Search Subscriptions</h2>
314 Begin searching by typing your text on the text box on the left side.
315 </p>
317 self._prepare_stream(content)
318 return
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)
332 self._model.clear()
333 self._model.open_stream("text/html")
334 self._model.write_stream(html)
335 self._model.close_stream()
336 return
338 def _generate_html(self, body):
339 # heading
340 html = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
341 <html>
342 <head><title>title</title>
343 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />"""
345 # stylesheet
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") + """/>"""
349 else:
350 html += """<style type="text/css">""" + self._view.get_css() + """</style>"""
352 # body
353 html += "</head><body>%s</body></html>" % body
354 return html
356 def _htmlify_item(self, item, encoding):
357 feed = item.feed
358 ret = []
360 # item header
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))
365 else:
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>')
374 ret.append('</div>')
376 # item body
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))
396 ret.append('</div>')
398 # freshmeat fields
399 freshmeat_data = []
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)
409 ret.append('</div>')
410 # empty paragraph to make sure that we get space here
411 ret.append('<p></p>')
412 # Additional information
413 dcret = []
414 if item.creator is not None:
415 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))
416 if item.contributors is not None and len(item.contributors):
417 for c in item.contributors:
418 dcret.append('<tr class="tr.dc"><td class="dcname"><span>%s</span></td><td class="dcvalue"><span>%s</span></td></tr>' \
419 % (_("Contributor:"), c.name))
420 if item.source:
421 url = utils.get_url_location(item.source['url'])
422 text = saxutils.escape(url)
423 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>' %
424 (_("Item Source"), url, text))
426 if item.guid is not None and item.guid != "" and item.guidislink:
427 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))
428 # check for not guidislink for the case where there is guid but
429 # isPermalink="false" and yet link is the same as guid (link is
430 # always assumed to be a valid link)
431 if item.link != "" and item.link is not None and (item.link != item.guid or not item.guidislink):
432 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>' %
433 (_("Complete story"), item.link, item.link))
435 if item.license_urls:
436 for l in item.license_urls:
437 if l:
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>' % (_("License"), l, l))
440 if len(dcret):
441 ret.append('<div class="dcinfo">%s<table class="dc" id="footer">' % _("Additional information"))
442 ret.append("".join(dcret))
443 ret.append('</table>')
444 ret.append('</div>')
445 return "".join(ret)
447 class ScrollView(MVP.WidgetView):
449 Widget: html_scrolled_window
451 def set_adjustments(self, vadjustment, hadjustment):
452 self._widget.set_hadjustment(hadjustment)
453 self._widget.set_vadjustment(vadjustment)
454 return
456 def add_child(self, widget):
457 self._widget.add(widget)
458 return
460 def show(self):
461 self._widget.show_all()
462 return
464 def adjust_vertical_adjustment(self):
465 va = self._widget.get_vadjustment()
466 va.set_value(va.lower)
467 return
469 def get_vadjustment(self):
470 return self._widget.get_vadjustment()
472 class ScrollPresenter(MVP.BasicPresenter):
474 View: ScrollView
476 def set_view_adjustments(self, vadjustment, hadjustment):
477 self._view.set_adjustments(vadjustment, hadjustment)
478 return
480 def update_view(self):
481 self._view.adjust_vertical_adjustment()
482 return
484 def scroll_down(self):
485 va = self._view.get_vadjustment()
486 old_value = va.get_value()
487 new_value = old_value + va.page_increment
488 limit = va.upper - va.page_size
489 if new_value > limit:
490 new_value = limit
491 va.set_value(new_value)
492 return new_value > old_value
494 def show_view(self):
495 self._view.show()
496 return
498 class ItemView:
499 def __init__(self, item_view_container):
500 self._encoding = utils.get_locale_encoding()
501 widget_tree = gtk.glade.get_widget_tree(item_view_container)
502 document = gtkhtml2.Document()
503 widget = gtkhtml2.View()
504 html_view = HTMLView(widget, document)
505 self._html_presenter = HTMLPresenter(document, html_view)
507 widget = widget_tree.get_widget('html_scrolled_window')
508 scroll_view = ScrollView(widget)
509 self._scroll_presenter = ScrollPresenter(view=scroll_view)
511 vadj, hadj = self._html_presenter.get_view_adjustments()
512 child = self._html_presenter.get_view_widget()
513 self._scroll_presenter.set_view_adjustments(vadj, hadj)
514 self._scroll_presenter.view.add_child(child)
515 self._scroll_presenter.show_view()
517 config = Config.get_instance()
518 self._html_presenter.set_view_magnification(config.text_magnification)
520 def item_selection_changed(self, signal):
521 if signal.item:
522 self._display_item(signal.item)
524 def _display_item(self, item):
525 self._html_presenter.display_item(item, self._encoding)
526 self._scroll_presenter.update_view()
528 def display_empty_feed(self):
529 self._html_presenter.display_empty_feed()
531 def display_empty_search(self):
532 self._html_presenter.display_empty_search()
534 def scroll_down(self):
535 return self._scroll_presenter.scroll_down()
537 def get_selected_text(self):
538 return self._html_presenter.get_html_text_selection()