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