1#!/usr/bin/env python 2# -*- mode: python; coding: utf-8; -*- 3# --------------------------------------------------------------------------- 4# 5# Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer 6# Copyright (C) 2003 Mt. Hood Playing Card Co. 7# Copyright (C) 2005-2009 Skomoroh 8# 9# This program is free software: you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation, either version 3 of the License, or 12# (at your option) any later version. 13# 14# This program is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program. If not, see <http://www.gnu.org/licenses/>. 21# 22# --------------------------------------------------------------------------- 23 24import htmllib 25import os 26import sys 27import traceback 28 29import gobject 30 31import gtk 32from gtk import gdk 33 34import pango 35 36import pysollib.formatter 37from pysollib.mfxutil import Struct, openURL 38from pysollib.mygettext import _ 39from pysollib.settings import TITLE 40 41import six 42 43from tkwidget import MfxMessageDialog 44 45if __name__ == '__main__': 46 d = os.path.abspath(os.path.join(sys.path[0], '..', '..')) 47 sys.path.append(d) 48 import gettext 49 gettext.install('pysol', d, unicode=True) 50 51REMOTE_PROTOCOLS = ('ftp:', 'gopher:', 'http:', 'mailto:', 'news:', 'telnet:') 52 53 54# ************************************************************************ 55# * 56# ************************************************************************ 57 58class tkHTMLWriter(pysollib.formatter.NullWriter): 59 def __init__(self, text, viewer, app): 60 pysollib.formatter.NullWriter.__init__(self) 61 62 self.text = text # gtk.TextBuffer 63 self.viewer = viewer # HTMLViewer 64 65 self.anchor = None 66 self.anchor_mark = None 67 68 self.font = None 69 self.font_mark = None 70 self.indent = '' 71 72 def write(self, data): 73 data = six.text_type(data) 74 self.text.insert(self.text.get_end_iter(), data, len(data)) 75 76 def anchor_bgn(self, href, name, type): 77 if href: 78 # self.text.update_idletasks() # update display during parsing 79 self.anchor = (href, name, type) 80 self.anchor_mark = self.text.get_end_iter().get_offset() 81 82 def anchor_end(self): 83 if self.anchor: 84 href = self.anchor[0] 85 tag_name = 'href_' + href 86 if tag_name in self.viewer.anchor_tags: 87 tag = self.viewer.anchor_tags[tag_name][0] 88 else: 89 tag = self.text.create_tag(tag_name, foreground='blue', 90 underline=pango.UNDERLINE_SINGLE) 91 self.viewer.anchor_tags[tag_name] = (tag, href) 92 tag.connect('event', self.viewer.anchor_event, href) 93 u = self.viewer.normurl(href, with_protocol=False) 94 if u in self.viewer.visited_urls: 95 tag.set_property('foreground', '#660099') 96 start = self.text.get_iter_at_offset(self.anchor_mark) 97 end = self.text.get_end_iter() 98 # print 'apply_tag href >>', start.get_offset(), end.get_offset() 99 self.text.apply_tag(tag, start, end) 100 101 self.anchor = None 102 103 def new_font(self, font): 104 # end the current font 105 if self.font: 106 # print 'end_font(%s)' % `self.font` 107 start = self.text.get_iter_at_offset(self.font_mark) 108 end = self.text.get_end_iter() 109 # print 'apply_tag font >>', start.get_offset(), end.get_offset() 110 self.text.apply_tag_by_name(self.font, start, end) 111 self.font = None 112 # start the new font 113 if font: 114 # print 'start_font(%s)' % `font` 115 self.font_mark = self.text.get_end_iter().get_offset() 116 if font[0] in self.viewer.fontmap: 117 self.font = font[0] 118 elif font[3]: 119 self.font = 'pre' 120 elif font[2]: 121 self.font = 'bold' 122 elif font[1]: 123 self.font = 'italic' 124 else: 125 self.font = None 126 127 def new_margin(self, margin, level): 128 self.indent = ' ' * level 129 130 def send_label_data(self, data): 131 # self.write(self.indent + data + ' ') 132 self.write(self.indent) 133 if data == '*': # <li> 134 img = self.viewer.symbols_img.get('disk') 135 if img: 136 self.text.insert_pixbuf(self.text.get_end_iter(), img) 137 else: 138 self.write('*') # unichr(0x2022) 139 else: 140 self.write(data) 141 self.write(' ') 142 143 def send_paragraph(self, blankline): 144 self.write('\n' * blankline) 145 146 def send_line_break(self): 147 self.write('\n') 148 149 def send_hor_rule(self, *args): 150 # ~ width = int(int(self.text['width']) * 0.9) 151 width = 70 152 self.write('_' * width) 153 self.write('\n') 154 155 def send_literal_data(self, data): 156 self.write(data) 157 158 def send_flowing_data(self, data): 159 self.write(data) 160 161 162# ************************************************************************ 163# * 164# ************************************************************************ 165 166class tkHTMLParser(htmllib.HTMLParser): 167 def anchor_bgn(self, href, name, type): 168 self.formatter.flush_softspace() 169 htmllib.HTMLParser.anchor_bgn(self, href, name, type) 170 self.formatter.writer.anchor_bgn(href, name, type) 171 172 def anchor_end(self): 173 if self.anchor: 174 self.anchor = None 175 self.formatter.writer.anchor_end() 176 177 def do_dt(self, attrs): 178 self.formatter.end_paragraph(1) 179 self.ddpop() 180 181 def handle_image(self, src, alt, ismap, align, width, height): 182 self.formatter.writer.viewer.showImage( 183 src, alt, ismap, align, width, height) 184 185 186# ************************************************************************ 187# * 188# ************************************************************************ 189 190class HTMLViewer: 191 symbols_fn = {} # filenames, loaded in Application.loadImages3 192 symbols_img = {} 193 194 def __init__(self, parent, app=None, home=None): 195 self.parent = parent 196 self.app = app 197 self.home = home 198 self.url = None 199 self.history = Struct( 200 list=[], 201 index=0, 202 ) 203 self.visited_urls = [] 204 self.images = {} 205 self.anchor_tags = {} 206 207 # create buttons 208 vbox = gtk.VBox() 209 parent.table.attach( 210 vbox, 211 0, 1, 0, 1, 212 gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL | gtk.SHRINK, 213 0, 0) 214 215 buttons_box = gtk.HBox() 216 vbox.pack_start(buttons_box, fill=True, expand=False) 217 for name, label, callback in ( 218 ('homeButton', _('Index'), self.goHome), 219 ('backButton', _('Back'), self.goBack), 220 ('forwardButton', _('Forward'), self.goForward), 221 ('closeButton', _('Close'), self.destroy) 222 ): 223 button = gtk.Button(label) 224 button.show() 225 button.connect('clicked', callback) 226 buttons_box.pack_start(button, fill=True, expand=False) 227 button.set_property('can-focus', False) 228 setattr(self, name, button) 229 230 # create text widget 231 self.textview = gtk.TextView() 232 self.textview.show() 233 self.textview.set_left_margin(10) 234 self.textview.set_right_margin(10) 235 self.textview.set_cursor_visible(False) 236 self.textview.set_editable(False) 237 self.textview.set_wrap_mode(gtk.WRAP_WORD) 238 self.textbuffer = self.textview.get_buffer() 239 240 sw = gtk.ScrolledWindow() 241 sw.set_property('hscrollbar-policy', gtk.POLICY_AUTOMATIC) 242 sw.set_property('vscrollbar-policy', gtk.POLICY_AUTOMATIC) 243 sw.set_property('border-width', 0) 244 sw.add(self.textview) 245 sw.show() 246 vbox.pack_start(sw, fill=True, expand=True) 247 self.vadjustment = sw.get_vadjustment() 248 self.hadjustment = sw.get_hadjustment() 249 250 # statusbar 251 self.statusbar = gtk.Statusbar() 252 self.statusbar.show() 253 vbox.pack_start(self.statusbar, fill=True, expand=False) 254 255 # load images 256 for name, fn in self.symbols_fn.items(): 257 self.symbols_img[name] = self.getImage(fn) 258 259 # bindings 260 parent.connect('key-press-event', self.key_press_event) 261 parent.connect('destroy', self.destroy) 262 self.textview.connect('motion-notify-event', self.motion_notify_event) 263 self.textview.connect('leave-notify-event', self.leave_event) 264 self.textview.connect('enter-notify-event', self.motion_notify_event) 265 266 self._changed_cursor = False 267 268 self.createFontMap() 269 270 # cursor 271 self.defcursor = gdk.XTERM 272 self.handcursor = gdk.HAND2 273 # self.textview.realize() 274 # window = self.textview.get_window(gtk.TEXT_WINDOW_TEXT) 275 # window.set_cursor(gdk.Cursor(self.defcursor)) 276 277 parent.set_default_size(600, 440) 278 parent.show_all() 279 gobject.idle_add(gtk.main) 280 281 def motion_notify_event(self, widget, event): 282 x, y, _ = widget.window.get_pointer() 283 x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y) 284 tags = widget.get_iter_at_location(x, y).get_tags() 285 is_over_anchor = False 286 for tag, href in self.anchor_tags.values(): 287 if tag in tags: 288 is_over_anchor = True 289 break 290 if is_over_anchor: 291 if not self._changed_cursor: 292 # print 'set cursor hand' 293 window = widget.get_window(gtk.TEXT_WINDOW_TEXT) 294 window.set_cursor(gdk.Cursor(self.handcursor)) 295 self._changed_cursor = True 296 self.statusbar.pop(0) 297 href = self.normurl(href) 298 self.statusbar.push(0, href) 299 else: 300 if self._changed_cursor: 301 # print 'set cursor xterm' 302 window = widget.get_window(gtk.TEXT_WINDOW_TEXT) 303 window.set_cursor(gdk.Cursor(self.defcursor)) 304 self._changed_cursor = False 305 self.statusbar.pop(0) 306 return False 307 308 def leave_event(self, widget, event): 309 if self._changed_cursor: 310 # print 'set cursor xterm' 311 window = widget.get_window(gtk.TEXT_WINDOW_TEXT) 312 window.set_cursor(gdk.Cursor(self.defcursor)) 313 self._changed_cursor = False 314 self.statusbar.pop(0) 315 316 def anchor_event(self, tag, textview, event, iter, href): 317 # print 'anchor_event:', args 318 if event.type == gdk.BUTTON_PRESS and event.button == 1: 319 self.updateHistoryXYView() 320 self.display(href) 321 return True 322 return False 323 324 def key_press_event(self, w, e): 325 if gdk.keyval_name(e.keyval) == 'Escape': 326 self.destroy() 327 328 def createFontMap(self): 329 try: # if app 330 default_font = self.app.getFont('sans') 331 fixed_font = self.app.getFont('fixed') 332 except Exception: 333 traceback.print_exc() 334 default_font = ('times new roman', 12) 335 fixed_font = ('courier', 12) 336 size = default_font[1] 337 sign = 1 338 if size < 0: 339 sign = -1 340 self.fontmap = { 341 'h1': (default_font[0], size + 12*sign, 'bold'), 342 'h2': (default_font[0], size + 8*sign, 'bold'), 343 'h3': (default_font[0], size + 6*sign, 'bold'), 344 'h4': (default_font[0], size + 4*sign, 'bold'), 345 'h5': (default_font[0], size + 2*sign, 'bold'), 346 'h6': (default_font[0], size + 1*sign, 'bold'), 347 'bold': (default_font[0], size, 'bold'), 348 } 349 350 for tag_name in self.fontmap.keys(): 351 font = self.fontmap[tag_name] 352 font = font[0]+' '+str(font[1]) 353 tag = self.textbuffer.create_tag(tag_name, font=font) 354 tag.set_property('weight', pango.WEIGHT_BOLD) 355 356 font = font[0]+' '+str(font[1]) 357 tag = self.textbuffer.create_tag('italic', style=pango.STYLE_ITALIC) 358 self.fontmap['italic'] = (font[0], size, 'italic') 359 font = fixed_font[0]+' '+str(fixed_font[1]) 360 self.textbuffer.create_tag('pre', font=font) 361 self.fontmap['pre'] = fixed_font 362 # set default font 363 fd = pango.FontDescription(default_font[0]+' '+str(default_font[1])) 364 if 'bold' in default_font: 365 fd.set_weight(pango.WEIGHT_BOLD) 366 if 'italic' in default_font: 367 fd.set_style(pango.STYLE_ITALIC) 368 self.textview.modify_font(fd) 369 370 def destroy(self, *event): 371 self.parent.destroy() 372 self.parent = None 373 374 def get_position(self): 375 pos = self.hadjustment.get_value(), self.vadjustment.get_value() 376 return pos 377 378 def set_position(self, pos): 379 def callback(pos, hadj, vadj): 380 hadj.set_value(pos[0]) 381 vadj.set_value(pos[1]) 382 gobject.idle_add(callback, pos, self.hadjustment, self.vadjustment) 383 384 # locate a file relative to the current self.url 385 def basejoin(self, url, baseurl=None, relpath=1): 386 if baseurl is None: 387 baseurl = self.url 388 if 0: 389 import urllib 390 url = urllib.pathname2url(url) 391 if relpath and self.url: 392 url = urllib.basejoin(baseurl, url) 393 else: 394 url = os.path.normpath(url) 395 if relpath and baseurl and not os.path.isabs(url): 396 h1, t1 = os.path.split(url) 397 h2, t2 = os.path.split(baseurl) 398 if h1 != h2: 399 url = os.path.join(h2, h1, t1) 400 url = os.path.normpath(url) 401 return url 402 403 def normurl(self, url, with_protocol=True): 404 for p in REMOTE_PROTOCOLS: 405 if url.startswith(p): 406 break 407 else: 408 url = self.basejoin(url) 409 if with_protocol: 410 if os.name == 'nt': 411 url = url.replace('\\', '/') 412 url = 'file://'+url 413 return url 414 415 def openfile(self, url): 416 if url[-1:] == '/' or os.path.isdir(url): 417 url = os.path.join(url, 'index.html') 418 url = os.path.normpath(url) 419 return open(url, 'rb'), url 420 421 def display(self, url, add=1, relpath=1, position=(0, 0)): 422 # print 'display:', url, position 423 # for some reason we have to stop the PySol demo 424 # (is this a multithread problem with tkinter ?) 425 try: 426 # self.app.game.stopDemo() 427 # self.app.game._cancelDrag() 428 pass 429 except Exception: 430 pass 431 432 # ftp: and http: would work if we use urllib, but this widget is 433 # far too limited to display anything but our documentation... 434 for p in REMOTE_PROTOCOLS: 435 if url.startswith(p): 436 if not openURL(url): 437 self.errorDialog(_('''%(app)s HTML limitation: 438The %(protocol)s protocol is not supported yet. 439 440Please use your standard web browser 441to open the following URL: 442%(url)s 443''') % {'app': TITLE, 'protocol': p, 'url': url}) 444 return 445 446 # locate the file relative to the current url 447 url = self.basejoin(url, relpath=relpath) 448 449 # read the file 450 try: 451 file = None 452 if 0: 453 import urllib 454 file = urllib.urlopen(url) 455 else: 456 file, url = self.openfile(url) 457 data = file.read() 458 file.close() 459 file = None 460 except Exception as ex: 461 if file: 462 file.close() 463 self.errorDialog( 464 _('Unable to service request:\n') + url + '\n\n' + str(ex)) 465 return 466 except Exception: 467 if file: 468 file.close() 469 self.errorDialog(_('Unable to service request:\n') + url) 470 return 471 472 self.url = url 473 if self.home is None: 474 self.home = self.url 475 if add: 476 self.addHistory(self.url, position=position) 477 478 # print self.history.index, self.history.list 479 if self.history.index > 1: 480 self.backButton.set_sensitive(True) 481 else: 482 self.backButton.set_sensitive(False) 483 if self.history.index < len(self.history.list): 484 self.forwardButton.set_sensitive(True) 485 else: 486 self.forwardButton.set_sensitive(False) 487 488 start, end = self.textbuffer.get_bounds() 489 self.textbuffer.delete(start, end) 490 491 writer = tkHTMLWriter(self.textbuffer, self, self.app) 492 fmt = pysollib.formatter.AbstractFormatter(writer) 493 parser = tkHTMLParser(fmt) 494 parser.feed(data) 495 parser.close() 496 497 self.set_position(position) 498 499 self.parent.set_title(parser.title) 500 501 def addHistory(self, url, position=(0, 0)): 502 if url not in self.visited_urls: 503 self.visited_urls.append(url) 504 if self.history.index > 0: 505 u, pos = self.history.list[self.history.index-1] 506 if u == url: 507 self.updateHistoryXYView() 508 return 509 del self.history.list[self.history.index:] 510 self.history.list.append((url, position)) 511 self.history.index = self.history.index + 1 512 513 def updateHistoryXYView(self): 514 if self.history.index > 0: 515 url, position = self.history.list[self.history.index-1] 516 position = self.get_position() 517 self.history.list[self.history.index-1] = (url, position) 518 519 def goBack(self, *event): 520 if self.history.index > 1: 521 self.updateHistoryXYView() 522 self.history.index = self.history.index - 1 523 url, position = self.history.list[self.history.index-1] 524 self.display(url, add=0, relpath=0, position=position) 525 526 def goForward(self, *event): 527 if self.history.index < len(self.history.list): 528 self.updateHistoryXYView() 529 url, position = self.history.list[self.history.index] 530 self.history.index = self.history.index + 1 531 self.display(url, add=0, relpath=0, position=position) 532 533 def goHome(self, *event): 534 if self.home and self.home != self.url: 535 self.updateHistoryXYView() 536 self.display(self.home, relpath=0) 537 538 def errorDialog(self, msg): 539 MfxMessageDialog( 540 self.parent, title=TITLE+' HTML Problem', 541 text=msg, bitmap='warning', 542 strings=(_('&OK'),), default=0) 543 544 def getImage(self, fn): 545 if fn in self.images: 546 return self.images[fn] 547 try: 548 img = gdk.pixbuf_new_from_file(fn) 549 except Exception: 550 img = None 551 self.images[fn] = img 552 return img 553 554 def showImage(self, src, alt, ismap, align, width, height): 555 url = self.basejoin(src) 556 img = self.getImage(url) 557 if img: 558 iter = self.textbuffer.get_end_iter() 559 self.textbuffer.insert_pixbuf(iter, img) 560 561 562# ************************************************************************ 563# * 564# ************************************************************************ 565 566 567def tkhtml_main(args): 568 try: 569 url = args[1] 570 except Exception: 571 url = os.path.join(os.pardir, os.pardir, 'data', 'html', 'index.html') 572 top = gtk.Window() 573 table = gtk.Table() 574 table.show() 575 top.add(table) 576 top.table = table 577 viewer = HTMLViewer(top) 578 viewer.app = None 579 viewer.display(url) 580 top.connect('destroy', lambda w: gtk.main_quit()) 581 gtk.main() 582 return 0 583 584 585if __name__ == '__main__': 586 sys.exit(tkhtml_main(sys.argv)) 587