├── po ├── POTFILES.in ├── Makefile ├── chmviewkit.pot └── ar.po ├── waqf2-ar.pdf ├── AUTHORS ├── chmviewkit ├── chmviewkit.desktop.in ├── TODO ├── README ├── Makefile ├── setup.py ├── chmviewkit.spec ├── chmviewkit.py └── chmviewkit.svg /po/POTFILES.in: -------------------------------------------------------------------------------- 1 | chmviewkit.desktop.in 2 | chmviewkit.py 3 | 4 | -------------------------------------------------------------------------------- /waqf2-ar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojuba-org/chmviewkit/master/waqf2-ar.pdf -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Muayyad Saleh Alsadi 2 | Ehab El-Gedawy 3 | -------------------------------------------------------------------------------- /chmviewkit: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from chmviewkit import main 5 | 6 | main() 7 | 8 | -------------------------------------------------------------------------------- /chmviewkit.desktop.in: -------------------------------------------------------------------------------- 1 | 2 | [Desktop Entry] 3 | _Name=ChmViewKit 4 | _GenericName=CHM Viewer 5 | _Comment=HTML Help(CHM) viewer 6 | Exec=chmviewkit %F 7 | Terminal=false 8 | Type=Application 9 | Icon=chmviewkit 10 | StartupNotify=true 11 | Categories=Office;Viewer; 12 | MimeType=application/chm;application/x-chm; 13 | -------------------------------------------------------------------------------- /po/Makefile: -------------------------------------------------------------------------------- 1 | POTFILE=$(shell cat POTFILES.in) 2 | SOURCES=$(addprefix ../, $(POTFILE)) 3 | POFILES=$(wildcard *.po) 4 | MOFILES=$(patsubst %.po,%.mo,$(POFILES)) 5 | 6 | all: chmviewkit.pot $(MOFILES) 7 | 8 | chmviewkit.pot: $(SOURCES) 9 | intltool-update -g chmviewkit -p 10 | 11 | %.mo: %.po 12 | msgfmt $*.po -o $*.mo 13 | mkdir -p ../locale/$*/LC_MESSAGES/ || : 14 | cp $*.mo ../locale/$*/LC_MESSAGES/chmviewkit.mo 15 | 16 | %.po: chmviewkit.pot 17 | intltool-update -g chmviewkit -d $* 18 | 19 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | بسم الله الرحمن الرحيم 2 | - fix spec file 3 | - Fix those lines: 4 | 162: # FIXME: use another icon 5 | 186: # FIXME: AttributeError: 'function' object has no attribute 'font_desc' [DONE: Not working!] 6 | 464: # FIXME: fix entry colors [DONE: Not at all stats!] 7 | 729: # FIXME: have a single method for this 8 | 731: fn=self.app.get_toc(key)[0]['local'] # FIXME: just put cursor to first is_page 9 | - Search in arabic 10 | - Search with special characters ( as spaces or ' " ( \ / ? , ...etc) 11 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | What is CHM View Kit ? 2 | =============================================== 3 | 4 | CHM View kit is a simple and fast CHM viewer 5 | 6 | it got a server and web application and a GUI 7 | 8 | How different from other applications ? 9 | =============================================== 10 | 11 | chmsee wast disk space by extracting CHM file content 12 | into ~/.chmsee/bookshelf and view it while chmviewkit 13 | works on the fly 14 | 15 | gnochm uses xul (from mozilla), while we use webkit, a very lightweight library 16 | 17 | and compared to both we got a very rich UI 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR?=/ 2 | datadir?=$(DESTDIR)/usr/share 3 | INSTALL=install 4 | 5 | SOURCES=$(wildcard *.desktop.in) 6 | TARGETS=${SOURCES:.in=} 7 | 8 | all: $(TARGETS) icons 9 | 10 | icons: 11 | for i in 96 72 64 48 36 32 24 22 16; do \ 12 | convert -background none chmviewkit.svg -resize $${i}x$${i} chmviewkit-$${i}.png; \ 13 | done 14 | pos: 15 | make -C po all 16 | 17 | install: all 18 | python setup.py install -O2 --root $(DESTDIR) 19 | $(INSTALL) -d $(datadir)/applications/ 20 | $(INSTALL) -m 0644 chmviewkit.desktop $(datadir)/applications/ 21 | $(INSTALL) -m 0644 -D chmviewkit.svg $(datadir)/icons/hicolor/scalable/apps/chmviewkit.svg; 22 | for i in 96 72 64 48 36 32 24 22 16; do \ 23 | install -d $(datadir)/icons/hicolor/$${i}x$${i}/apps; \ 24 | $(INSTALL) -m 0644 -D chmviewkit-$${i}.png $(datadir)/icons/hicolor/$${i}x$${i}/apps/chmviewkit.png; \ 25 | done 26 | 27 | %.desktop: %.desktop.in pos 28 | intltool-merge -d po $< $@ 29 | 30 | clean: 31 | rm -f $(TARGETS) 32 | for i in 96 72 64 48 36 32 24 22 16; do \ 33 | rm -f chmviewkit-$${i}.png; \ 34 | done 35 | 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | import sys, os, os.path 3 | from distutils.core import setup 4 | from glob import glob 5 | 6 | # to install type: 7 | # python setup.py install --root=/ 8 | 9 | def no_empty(l): 10 | return filter(lambda (i,j): j, l) 11 | 12 | def recusive_data_dir(to, src, l=None): 13 | D=glob(os.path.join(src,'*')) 14 | files=filter( lambda i: os.path.isfile(i), D ) 15 | dirs=filter( lambda i: os.path.isdir(i), D ) 16 | if l==None: l=[] 17 | l.append( (to , files ) ) 18 | for d in dirs: recusive_data_dir( os.path.join(to,os.path.basename(d)), d , l) 19 | return l 20 | 21 | locales=map(lambda i: ('share/'+i,[''+i+'/chmviewkit.mo',]),glob('locale/*/LC_MESSAGES')) 22 | #data_files=no_empty(recusive_data_dir('share/chmviewkit/chmviewkit-files', 'chmviewkit-files')) 23 | data_files=[] 24 | data_files.extend(locales) 25 | setup (name='chmviewkit', version='3.0.10', 26 | description='Webkit/Gtk-based CHM viewer', 27 | author='Muayyad Saleh Alsadi', 28 | author_email='alsadi@ojuba.org', 29 | url='http://chmviewkit.ojuba.org/', 30 | license='Waqf', 31 | #packages=['chmviewkit'], 32 | py_modules = ['chmviewkit'], 33 | scripts=['chmviewkit'], 34 | classifiers=[ 35 | 'Development Status :: 4 - Beta', 36 | 'Intended Audience :: End Users/Desktop', 37 | 'Operating System :: POSIX', 38 | 'Programming Language :: Python', 39 | ], 40 | data_files=data_files 41 | ) 42 | 43 | 44 | -------------------------------------------------------------------------------- /chmviewkit.spec: -------------------------------------------------------------------------------- 1 | %global owner ojuba-org 2 | %global commit #Write commit number here 3 | 4 | Name: chmviewkit 5 | Summary: Webkit/Gtk-based CHM viewer 6 | Summary(ar): عارض ملفات CHM 7 | URL: http://ojuba.org/ 8 | Version: 0.2.3 9 | Release: 3%{?dist} 10 | Source: https://github.com/%{owner}/%{name}/archive/%{commit}/%{name}-%{commit}.tar.gz 11 | License: WAQFv2 12 | BuildArch: noarch 13 | Requires: python2 14 | Requires: python-paste 15 | Requires: python-chm 16 | Requires: pygobject3 >= 3.0.2 17 | BuildRequires: gettext 18 | BuildRequires: intltool 19 | BuildRequires: ImageMagick 20 | BuildRequires: python2-devel 21 | 22 | %description 23 | Webkit/Gtk-based CHM viewer. 24 | 25 | %description -l ar 26 | عارض ملفات CHM متوافق مع ويبكيت و ج.ت.ك. 27 | 28 | %prep 29 | %setup -q -n %{name}-%{commit} 30 | 31 | %build 32 | make %{?_smp_mflags} 33 | 34 | %install 35 | %make_install 36 | 37 | %post 38 | touch --no-create %{_datadir}/icons/hicolor || : 39 | if [ -x %{_bindir}/gtk-update-icon-cache ] ; then 40 | %{_bindir}/gtk-update-icon-cache --quiet %{_datadir}/icons/hicolor || : 41 | fi 42 | 43 | %postun 44 | touch --no-create %{_datadir}/icons/hicolor || : 45 | if [ -x %{_bindir}/gtk-update-icon-cache ] ; then 46 | %{_bindir}/gtk-update-icon-cache --quiet %{_datadir}/icons/hicolor || : 47 | fi 48 | 49 | %files 50 | %license waqf2-ar.pdf 51 | %doc README TODO AUTHORS 52 | %{_bindir}/chmviewkit 53 | %{python2_sitelib}/chmviewkit* 54 | %{python2_sitelib}/*.egg-info 55 | %{_datadir}/icons/hicolor/*/apps/*.png 56 | %{_datadir}/icons/hicolor/*/apps/*.svg 57 | %{_datadir}/applications/*.desktop 58 | %{_datadir}/locale/*/*/*.mo 59 | 60 | %changelog 61 | * Wed Jul 22 2015 Mosaab Alzoubi - 0.2.3-3 62 | - General Rivision 63 | - Remove group tag 64 | - Remove old ATTR way 65 | - Use %%make_install and %%license 66 | - Add Arabic summary and description 67 | - Fix requires and BRs 68 | 69 | * Tue Feb 18 2014 Mosaab Alzoubi - 0.2.3-2 70 | - General Revision. 71 | 72 | * Sat Jun 2 2012 Muayyad Saleh AlSadi - 0.2.3-1 73 | - port to gtk3, webkit3 74 | 75 | * Fri Jan 13 2012 Muayyad Saleh AlSadi - 0.2.2-1 76 | - new release with recent support 77 | 78 | * Sat Jul 2 2011 Muayyad Saleh AlSadi - 0.2.0-1 79 | - fully featured stable release 80 | 81 | * Sun Jun 19 2011 Muayyad Saleh AlSadi - 0.1.0-1 82 | - initial packing 83 | -------------------------------------------------------------------------------- /po/chmviewkit.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2011-08-26 17:38+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: ../chmviewkit.desktop.in.h:1 21 | msgid "CHM Viewer" 22 | msgstr "" 23 | 24 | #: ../chmviewkit.desktop.in.h:2 25 | msgid "ChmViewKit" 26 | msgstr "" 27 | 28 | #: ../chmviewkit.desktop.in.h:3 29 | msgid "HTML Help(CHM) viewer" 30 | msgstr "" 31 | 32 | #: ../chmviewkit.py:100 ../chmviewkit.py:109 33 | #, python-format 34 | msgid "open [%s] in external browser" 35 | msgstr "" 36 | 37 | #: ../chmviewkit.py:287 38 | msgid "Open Link in New Tab" 39 | msgstr "" 40 | 41 | #: ../chmviewkit.py:385 42 | msgid "Topics Tree" 43 | msgstr "" 44 | 45 | #: ../chmviewkit.py:386 46 | msgid "Index" 47 | msgstr "" 48 | 49 | #: ../chmviewkit.py:387 50 | msgid "Search" 51 | msgstr "" 52 | 53 | #: ../chmviewkit.py:510 54 | msgid "CHM (Compiled HTML) Files Viewer" 55 | msgstr "" 56 | 57 | #: ../chmviewkit.py:545 ../chmviewkit.py:560 58 | msgid "CHM View Kit" 59 | msgstr "" 60 | 61 | #: ../chmviewkit.py:566 62 | msgid "Open a CHM file" 63 | msgstr "" 64 | 65 | #: ../chmviewkit.py:572 66 | msgid "Print current page" 67 | msgstr "" 68 | 69 | #: ../chmviewkit.py:581 70 | msgid "Go Back" 71 | msgstr "" 72 | 73 | #: ../chmviewkit.py:589 74 | msgid "Go Forward" 75 | msgstr "" 76 | 77 | #: ../chmviewkit.py:598 78 | msgid "Zoom in" 79 | msgstr "" 80 | 81 | #: ../chmviewkit.py:603 82 | msgid "Makes things appear bigger" 83 | msgstr "" 84 | 85 | #: ../chmviewkit.py:609 86 | msgid "Zoom out" 87 | msgstr "" 88 | 89 | #: ../chmviewkit.py:612 90 | msgid "Makes things appear smaller" 91 | msgstr "" 92 | 93 | #: ../chmviewkit.py:618 94 | msgid "1:1 Zoom" 95 | msgstr "" 96 | 97 | #: ../chmviewkit.py:621 98 | msgid "restore original zoom factor" 99 | msgstr "" 100 | 101 | #: ../chmviewkit.py:642 102 | msgid "About CHM View Kit" 103 | msgstr "" 104 | 105 | #: ../chmviewkit.py:683 106 | msgid "CHM Files" 107 | msgstr "" 108 | 109 | #: ../chmviewkit.py:687 110 | msgid "All files" 111 | msgstr "" 112 | 113 | #. FIXME: just put cursor to first is_page 114 | #: ../chmviewkit.py:709 115 | #, python-format 116 | msgid "unable to open file [%s]!" 117 | msgstr "" 118 | -------------------------------------------------------------------------------- /po/ar.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2012-01-13 21:23+0200\n" 12 | "PO-Revision-Date: 2011-06-14 15:23+0300\n" 13 | "Last-Translator: Muayyad Alsadi\n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: ar\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: ../chmviewkit.desktop.in.h:1 21 | msgid "CHM Viewer" 22 | msgstr "عارض CHM" 23 | 24 | #: ../chmviewkit.desktop.in.h:2 25 | msgid "ChmViewKit" 26 | msgstr "" 27 | 28 | #: ../chmviewkit.desktop.in.h:3 29 | msgid "HTML Help(CHM) viewer" 30 | msgstr "عارض ملفات المساعدة CHM" 31 | 32 | #: ../chmviewkit.py:100 ../chmviewkit.py:109 33 | #, python-format 34 | msgid "open [%s] in external browser" 35 | msgstr "فتح [%s] في متصفح خارجي" 36 | 37 | #: ../chmviewkit.py:287 38 | msgid "Open Link in New Tab" 39 | msgstr "فتح في لسان جديد" 40 | 41 | #: ../chmviewkit.py:385 42 | msgid "Topics Tree" 43 | msgstr "شجرة الموضوعات" 44 | 45 | #: ../chmviewkit.py:386 46 | msgid "Index" 47 | msgstr "الفهرس" 48 | 49 | #: ../chmviewkit.py:387 50 | msgid "Search" 51 | msgstr "بحث" 52 | 53 | #: ../chmviewkit.py:510 54 | msgid "CHM (Compiled HTML) Files Viewer" 55 | msgstr "عراض لملفات CHM المصنفة" 56 | 57 | #: ../chmviewkit.py:545 ../chmviewkit.py:560 58 | msgid "CHM View Kit" 59 | msgstr "" 60 | 61 | #: ../chmviewkit.py:566 62 | msgid "Open a CHM file" 63 | msgstr "فتح ملف CHM" 64 | 65 | #: ../chmviewkit.py:572 66 | msgid "Print current page" 67 | msgstr "طباعة الصفحة الحالية" 68 | 69 | #: ../chmviewkit.py:581 70 | msgid "Go Back" 71 | msgstr "عودة" 72 | 73 | #: ../chmviewkit.py:589 74 | msgid "Go Forward" 75 | msgstr "أمام" 76 | 77 | #: ../chmviewkit.py:598 78 | msgid "Zoom in" 79 | msgstr "تكبير" 80 | 81 | #: ../chmviewkit.py:603 82 | msgid "Makes things appear bigger" 83 | msgstr "يظهر الأشياء أكبر" 84 | 85 | #: ../chmviewkit.py:609 86 | msgid "Zoom out" 87 | msgstr "تصغير" 88 | 89 | #: ../chmviewkit.py:612 90 | msgid "Makes things appear smaller" 91 | msgstr "يظهر الأشياء أصغر" 92 | 93 | #: ../chmviewkit.py:618 94 | msgid "1:1 Zoom" 95 | msgstr "تقريب 1:1" 96 | 97 | #: ../chmviewkit.py:621 98 | msgid "restore original zoom factor" 99 | msgstr "استعادة معامل التقريب" 100 | 101 | #: ../chmviewkit.py:642 102 | msgid "About CHM View Kit" 103 | msgstr "" 104 | 105 | #: ../chmviewkit.py:683 106 | msgid "CHM Files" 107 | msgstr "ملفات CHM" 108 | 109 | #: ../chmviewkit.py:688 110 | msgid "All files" 111 | msgstr "كل الملفات" 112 | 113 | #. FIXME: just put cursor to first is_page 114 | #: ../chmviewkit.py:713 115 | #, python-format 116 | msgid "unable to open file [%s]!" 117 | msgstr "غير قادر على فتح الملف [%s]!" 118 | -------------------------------------------------------------------------------- /chmviewkit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # -*- Mode: Python; py-indent-offset: 4 -*- 3 | """ 4 | CHM View Kit - chm viewer based on gtk webkit libraries 5 | 6 | Copyright © 2011, Ojuba Team 7 | 8 | Released under terms of Waqf Public License. 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the latest version Waqf Public License as 11 | published by Ojuba.org. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | 17 | The Latest version of the license can be found on 18 | "http://waqf.ojuba.org/license" 19 | 20 | """ 21 | import sys, os, os.path, time, re, sqlite3, hashlib 22 | 23 | import shutil, tempfile 24 | import threading, socket 25 | import gettext 26 | from gi.repository import GObject 27 | from gi.repository import Gtk, Gdk, Pango 28 | from gi.repository import WebKit 29 | from subprocess import Popen, PIPE 30 | from urllib import unquote 31 | from urlparse import urlparse, urlsplit 32 | from htmlentitydefs import entitydefs 33 | 34 | from paste import httpserver 35 | from chm import chm, chmlib 36 | 37 | def async_gtk_call(f): 38 | def worker((function, args, kwargs)): 39 | function(*args, **kwargs) 40 | def f2(*args, **kwargs): 41 | GObject.idle_add(worker, (f, args, kwargs)) 42 | return f2 43 | 44 | setsid = getattr(os, 'setsid', None) 45 | if not setsid: setsid = getattr(os, 'setpgrp', None) 46 | _ps = [] 47 | 48 | def run_in_bg(cmd): 49 | global _ps 50 | setsid = getattr(os, 'setsid', None) 51 | if not setsid: setsid = getattr(os, 'setpgrp', None) 52 | _ps = filter(lambda x: x.poll() != None, _ps) # remove terminated processes from _ps list 53 | _ps.append(Popen(cmd, 0, '/bin/sh', shell = True, preexec_fn = setsid)) 54 | 55 | 56 | def get_exec_full_path(fn): 57 | a = filter(lambda p: os.access(p, os.X_OK), 58 | map(lambda p: os.path.join(p, fn), 59 | os.environ['PATH'].split(os.pathsep))) 60 | if a: return a[0] 61 | return None 62 | 63 | 64 | def guess_browser(): 65 | e = get_exec_full_path("xdg-open") 66 | if not e: 67 | e = get_exec_full_path("firefox") 68 | if not e: 69 | e = "start" 70 | return e 71 | 72 | broswer = guess_browser() 73 | 74 | def sure(msg, w = None): 75 | dlg = Gtk.MessageDialog(w, 76 | Gtk.DialogFlags.MODAL, 77 | Gtk.MessageType.QUESTION, 78 | Gtk.ButtonsType.YES_NO, msg) 79 | dlg.connect("response", lambda *args: dlg.hide()) 80 | r = dlg.run() 81 | dlg.destroy() 82 | return r == Gtk.ResponseType.YES 83 | 84 | def error(msg, w=None): 85 | dlg = Gtk.MessageDialog(w, 86 | Gtk.DialogFlags.MODAL, 87 | Gtk.MessageType.ERROR, 88 | Gtk.ButtonsType.OK, msg) 89 | dlg.connect("response", lambda *args: dlg.hide()) 90 | r = dlg.run() 91 | dlg.destroy() 92 | return r == Gtk.ResponseType.OK 93 | 94 | class WV(WebKit.WebView): 95 | def __init__(self, key): 96 | WebKit.WebView.__init__(self) 97 | self._lock = threading.Lock() 98 | self.key = key 99 | self.links_prompt = True 100 | #self.set_view_source_mode(True) 101 | self.set_full_content_zoom(True) 102 | self.connect_after("populate-popup", self.populate_popup) 103 | self.connect("navigation-requested", self._navigation_requested_cb) 104 | #self.connect("navigation-policy-decision-requested", self._navigation_policy_cb) 105 | 106 | def _navigation_policy_cb(self, view, frame, networkRequest, action, policy, *a, **kw): 107 | uri = networkRequest.get_uri() 108 | u = urlparse(uri) 109 | if u.scheme != 'file' and u.hostname != '127.0.0.1' and u.hostname != 'localhost': 110 | policy.ignore() 111 | if view.links_prompt and not sure(_("open [%s] in external browser") % uri, None): 112 | return True 113 | run_in_bg("%s '%s'" % (broswer ,uri)) 114 | return True 115 | return False 116 | 117 | def _navigation_requested_cb(self, view, frame, networkRequest): 118 | uri = networkRequest.get_uri() 119 | u = urlparse(uri) 120 | if u.scheme != 'file' and u.hostname != '127.0.0.1' and u.hostname != 'localhost': 121 | if view.links_prompt and not sure(_("open [%s] in external browser") % uri, None): 122 | return 1 123 | run_in_bg("%s '%s'" % (broswer ,uri)) 124 | return 1 125 | return 0 126 | 127 | def eval_js(self, e): 128 | """ 129 | can be used to eval a javascript expression 130 | eg. to obtain value of a javascript variable given its name 131 | """ 132 | self._lock.acquire() 133 | self.execute_script('$_eval_js_old_title=document.title;document.title=%s;' % e) 134 | r = self.get_main_frame().get_title() 135 | self.execute_script('document.title=$_eval_js_old_title;') 136 | self._lock.release() 137 | return r 138 | 139 | 140 | def populate_popup(self, view, menu): 141 | menu.append(Gtk.SeparatorMenuItem.new()) 142 | i = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_ZOOM_IN, None) 143 | i.connect('activate', lambda m,v,*a,**k: v.zoom_in(), view) 144 | i.set_always_show_image(True) 145 | menu.append(i) 146 | i = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_ZOOM_OUT, None) 147 | i.connect('activate', lambda m,v,**k: v.zoom_out(), view) 148 | i.set_always_show_image(True) 149 | menu.append(i) 150 | i = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_ZOOM_100, None) 151 | i.set_always_show_image(True) 152 | i.connect('activate', lambda m,v,*a,**k: v.get_zoom_level() == 1.0 or v.set_zoom_level(1.0), view) 153 | menu.append(i) 154 | 155 | menu.show_all() 156 | return False 157 | 158 | class TabLabel (Gtk.HBox): 159 | """A class for Tab labels""" 160 | 161 | __gsignals__ = { 162 | "close": (GObject.SIGNAL_RUN_FIRST, 163 | GObject.TYPE_NONE, 164 | (GObject.TYPE_OBJECT,)) 165 | } 166 | 167 | def __init__ (self, title, child): 168 | """initialize the tab label""" 169 | Gtk.HBox.__init__(self) 170 | self.title = title 171 | self.child = child 172 | self.label = Gtk.Label(title) 173 | self.label.props.max_width_chars = 30 174 | self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) 175 | self.label.set_alignment(0.0, 0.5) 176 | # FIXME: use another icon 177 | icon = Gtk.Image.new_from_icon_name("chmviewkit", Gtk.IconSize.MENU) 178 | close_image = Gtk.Image.new_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU) 179 | close_button = Gtk.Button() 180 | close_button.set_relief(Gtk.ReliefStyle.NONE) 181 | close_button.connect("clicked", self._close_tab, child) 182 | close_button.add(close_image) 183 | self.pack_start(icon, False, False, 0) 184 | self.pack_start(self.label, True, True, 0) 185 | self.pack_start(close_button, False, False, 0) 186 | 187 | self.set_data("label", self.label) 188 | self.set_data("close-button", close_button) 189 | self.connect("style-set", tab_label_style_set_cb) 190 | 191 | def set_label_text (self, text): 192 | """sets the text of this label""" 193 | if text: self.label.set_label(text) 194 | 195 | def _close_tab (self, widget, child): 196 | self.emit("close", child) 197 | 198 | def tab_label_style_set_cb(tab_label, style): 199 | context = tab_label.get_pango_context() 200 | # FIXME: AttributeError: 'function' object has no attribute 'font_desc' 201 | font_desc = Pango.font_description_from_string(tab_label.label.get_label()) 202 | metrics = context.get_metrics(font_desc, context.get_language()) 203 | char_width = metrics.get_approximate_digit_width() 204 | (Bool, width, height) = Gtk.icon_size_lookup_for_settings(tab_label.get_settings(), 205 | Gtk.IconSize.MENU) 206 | #tab_label.set_size_request(20 * Pango.PIXELS(char_width) + 2 * width, -1) 207 | tab_label.set_size_request(20 * char_width + 2 * width, -1) 208 | button = tab_label.get_data("close-button") 209 | button.set_size_request(width + 4, height + 4) 210 | 211 | class ContentPane (Gtk.HPaned): 212 | __gsignals__ = { 213 | "focus-view-title-changed": (GObject.SIGNAL_RUN_FIRST, 214 | GObject.TYPE_NONE, 215 | (GObject.TYPE_OBJECT, 216 | GObject.TYPE_STRING,)), 217 | "focus-view-load-committed": (GObject.SIGNAL_RUN_FIRST, 218 | GObject.TYPE_NONE, 219 | (GObject.TYPE_OBJECT, 220 | GObject.TYPE_OBJECT,)), 221 | "new-window-requested": (GObject.SIGNAL_RUN_FIRST, 222 | GObject.TYPE_NONE, 223 | (GObject.TYPE_OBJECT,)) 224 | } 225 | 226 | def __init__ (self, 227 | win, 228 | default_url = None, 229 | default_title = None, 230 | hp = Gtk.PolicyType.NEVER, 231 | vp = Gtk.PolicyType.AUTOMATIC): 232 | """initialize the content pane""" 233 | Gtk.HPaned.__init__(self) 234 | self.win = win 235 | self.tabs = Gtk.Notebook() 236 | self.sidepane = Gtk.Notebook() 237 | self.add1(self.sidepane) 238 | self.add2(self.tabs) 239 | self.sidepane.set_show_tabs(False) 240 | self.tabs.set_scrollable(True) 241 | self.default_url = default_url 242 | self.default_title = default_title 243 | self.hp = hp 244 | self.vp = vp 245 | self.tabs.props.scrollable = True 246 | #self.tabs.props.homogeneous = True 247 | self.tabs.connect("switch-page", self._switch_page) 248 | 249 | self.show_all() 250 | self._hovered_uri = None 251 | 252 | def load (self, uri): 253 | """load the given uri in the current web view""" 254 | child = self.tabs.get_nth_page(self.tabs.get_current_page()) 255 | wv = child.get_child() 256 | wv.open(uri) 257 | 258 | def new_tab_with_webview (self, webview): 259 | """creates a new tab with the given webview as its child""" 260 | self.tabs._construct_tab_view(webview) 261 | 262 | def new_tab (self, url = None, key = None): 263 | """creates a new page in a new tab""" 264 | # create the tab content 265 | wv = WV(key) 266 | #if url: wv.open(url) 267 | self._construct_tab_view(wv, url) 268 | return wv 269 | 270 | def _update_buttons(self, view): 271 | self.win.go_back_b.set_sensitive(view.can_go_back()) 272 | self.win.go_forward_b.set_sensitive(view.can_go_forward()) 273 | 274 | def _construct_tab_view (self, wv, url=None, title=None): 275 | wv.connect("hovering-over-link", self._hovering_over_link_cb) 276 | wv.connect("populate-popup", self._populate_page_popup_cb) 277 | wv.connect("load-committed", self._view_load_committed_cb) 278 | wv.connect("load-finished", self._view_load_finished_cb) 279 | wv.connect("create-web-view", self._new_web_view_request_cb) 280 | 281 | # load the content 282 | self._hovered_uri = None 283 | if not url: url = self.default_url 284 | else: wv.open(url) 285 | #elif url!=wv.get_property("uri"): wv.open(url) 286 | 287 | scrolled_window = Gtk.ScrolledWindow() 288 | scrolled_window.props.hscrollbar_policy = self.hp 289 | scrolled_window.props.vscrollbar_policy = self.vp 290 | scrolled_window.add(wv) 291 | scrolled_window.show_all() 292 | 293 | # create the tab 294 | if not title: title = self.default_title 295 | if not title: title = url 296 | label = TabLabel(title, scrolled_window) 297 | label.connect("close", self._close_tab) 298 | label.show_all() 299 | 300 | new_tab_number = self.tabs.append_page(scrolled_window, label) 301 | self.tabs.set_tab_reorderable(scrolled_window, True) 302 | #self.tabs.set_tab_label_packing(scrolled_window, False, False, Gtk.PackType.START) 303 | self.tabs.set_tab_label(scrolled_window, label) 304 | 305 | # hide the tab if there's only one 306 | self.tabs.set_show_tabs(self.tabs.get_n_pages() > 1) 307 | 308 | self.show_all() 309 | self.tabs.set_current_page(new_tab_number) 310 | 311 | def _populate_page_popup_cb(self, view, menu): 312 | # misc 313 | if self._hovered_uri: 314 | open_in_new_tab = Gtk.MenuItem(_("Open Link in New Tab")) 315 | open_in_new_tab.connect("activate", self._open_in_new_tab, view) 316 | menu.insert(open_in_new_tab, 0) 317 | menu.show_all() 318 | 319 | def _open_in_new_tab (self, menuitem, view): 320 | self.new_tab(self._hovered_uri, key=view.key) 321 | 322 | def _close_tab (self, label, child): 323 | page_num = self.tabs.page_num(child) 324 | if page_num != -1: 325 | view = child.get_child() 326 | view.destroy() 327 | self.tabs.remove_page(page_num) 328 | self.tabs.set_show_tabs(self.tabs.get_n_pages() > 1) 329 | 330 | def _switch_page (self, notebook, page, page_num): 331 | child = self.tabs.get_nth_page(page_num) 332 | view = child.get_child() 333 | frame = view.get_main_frame() 334 | self.emit("focus-view-load-committed", view, frame) 335 | key = view.key 336 | if key and self.win.app.chm[key].has_key("pane"): 337 | n = self.sidepane.page_num(self.win.app.chm[key]["pane"]) 338 | if n >= 0: self.sidepane.set_current_page(n) 339 | self._update_buttons(view) 340 | 341 | def _hovering_over_link_cb (self, view, title, uri): 342 | self._hovered_uri = uri 343 | 344 | def _view_load_committed_cb (self, view, frame): 345 | self.emit("focus-view-load-committed", view, frame) 346 | self._update_buttons(view) 347 | self._update_sidepan(frame) 348 | 349 | def _update_sidepan_old(self, frame): 350 | # FIXME: use other method to do this 351 | ## This function not used anymore!! 352 | '''Update sidpan according to frame url''' 353 | l=frame.get_uri().split('/', 3) 354 | if len(l)!=4: return 355 | l=l[3].split('$/', 1) 356 | if len(l)!=2: return 357 | key,sub_uri = l 358 | def checkLine(model, path, i, tree): 359 | if sub_uri == store.get_value(i,2): 360 | tree.expand_to_path(path) 361 | tree.scroll_to_cell(path) 362 | tree.get_selection().select_iter(i) 363 | return True 364 | pane = self.win.app.chm[key]["pane"] 365 | pane.working = True 366 | for t in (pane.tree, pane.ix, pane.results): 367 | store = t.get_model() 368 | store.foreach(checkLine, t) 369 | pane.working = False 370 | 371 | def _update_sidepan(self, frame): 372 | '''Update sidpan according to frame url''' 373 | l=frame.get_uri().split('/', 3) 374 | if len(l) != 4: return 375 | l = l[3].split('$/', 1) 376 | if len(l) != 2: return 377 | key,sub_uri = l 378 | pane = self.win.app.chm[key]["pane"] 379 | pane.working = True 380 | for t, c in ((pane.tree, pane.tree_cont), 381 | (pane.results, pane.result_cont), 382 | (pane.ix, pane.ix_cont)): 383 | sel=t.get_selection() 384 | sel.unselect_all() 385 | if c.has_key(sub_uri): 386 | p, i = c[sub_uri] 387 | t.expand_to_path(p) 388 | t.scroll_to_cell(p) 389 | sel.select_iter(i) 390 | pane.working = False 391 | 392 | def _view_load_finished_cb(self, view, frame): 393 | child = self.tabs.get_nth_page(self.tabs.get_current_page()) 394 | label = self.tabs.get_tab_label(child) 395 | title = frame.get_title() 396 | if not title: 397 | title = frame.get_uri() 398 | label.set_label_text(title) 399 | self.win._do_highlight(self.win.search_e.get_text()) 400 | 401 | def _new_web_view_request_cb (self, web_view, web_frame): 402 | view = self.new_tab(key = web_view.key) 403 | view.connect("web-view-ready", self._new_web_view_ready_cb) 404 | return view 405 | 406 | def _new_web_view_ready_cb (self, web_view): 407 | self.emit("new-window-requested", web_view) 408 | 409 | normalize_tb={ 410 | 65: 97, 66: 98, 67: 99, 68: 100, 69: 101, 70: 102, 71: 103, 72: 104, 73: 105, 74: 106, 75: 107, 76: 108, 77: 109, 78: 110, 79: 111, 80: 112, 81: 113, 82: 114, 83: 115, 84: 116, 85: 117, 86: 118, 87: 119, 88: 120, 89: 121, 90: 122, 411 | 1600: None, 1569: 1575, 1570: 1575, 1571: 1575, 1572: 1575, 1573: 1575, 1574: 1575, 1577: 1607, 1611: None, 1612: None, 1613: None, 1614: None, 1615: None, 1616: None, 1617: None, 1618: None, 1609: 1575} 412 | 413 | def normalize(s): 414 | return s.translate(normalize_tb) 415 | 416 | def _build_entiries_re(): 417 | p=[] 418 | for k,v in entitydefs.items(): 419 | if v.startswith('&'): continue 420 | p.append(re.escape(k)) 421 | return re.compile("&(%s);" % "|".join(p), re.I) 422 | 423 | _entities_re = _build_entiries_re() 424 | 425 | def _fix_entities(s): 426 | if not s: return "" 427 | return _entities_re.sub(lambda m: entitydefs.get(m.group(1), m.group(1)), s) 428 | 429 | class BookSidePane(Gtk.Notebook): 430 | def __init__(self, win, app, key): 431 | Gtk.Notebook.__init__(self) 432 | self.working = False 433 | self.win = win 434 | self.app = app 435 | self.key = key 436 | self.tree_cont = {} 437 | self.result_cont = {} 438 | self.ix_cont = {} 439 | self.append_page(self.build_toc_tree(), Gtk.Label(_('Topics Tree'))) 440 | self.append_page(self.build_ix(), Gtk.Label(_('Index'))) 441 | self.append_page(self.build_search_pane(), Gtk.Label(_('Search'))) 442 | 443 | def build_ix(self): 444 | app,key=self.app,self.key 445 | s = Gtk.ListStore(str, str, str, bool, float) # label, normalized, url, is_page, scale 446 | self.ix = Gtk.TreeView() 447 | self.ix.set_model(s) 448 | col = Gtk.TreeViewColumn('Index', Gtk.CellRendererText(), markup = 0, scale = 4) 449 | col.mark_up = True 450 | col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) 451 | col.set_resizable(True) 452 | col.set_expand(True) 453 | self.ix.insert_column(col, -1) 454 | self.ix.set_enable_search(True) 455 | self.ix.set_search_column(1) 456 | self.ix.set_headers_visible(False) 457 | self.ix.set_tooltip_column(0) 458 | p=[None] 459 | l=[] 460 | for e in self.app.get_ix(key): 461 | while(l and l[-1] >= e['level']): p.pop(); l.pop() 462 | it = s.append(( 463 | (" "*len(l))+e['name.utf8'], 464 | normalize(e['name.utf8'].lower()), 465 | e.get('local', ''), 466 | e['is_page'], 467 | max(0.5, 1.0-0.0625*len(l)), )) 468 | p.append(it) 469 | l.append(e['level']) 470 | self.ix_cont[e['local']] = [s.get_path(it), it] 471 | scroll = Gtk.ScrolledWindow() 472 | scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 473 | scroll.add(self.ix) 474 | self.ix.connect("cursor-changed", self._toc_cb) 475 | return scroll 476 | 477 | def build_toc_tree(self): 478 | app,key = self.app,self.key 479 | s = Gtk.TreeStore(str, str, str, bool) # label, normalized, url, is_page 480 | self.tree = Gtk.TreeView() 481 | self.tree.set_model(s) 482 | col = Gtk.TreeViewColumn('Topics', Gtk.CellRendererText(), markup = 0) 483 | col.mark_up = True 484 | col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) 485 | col.set_resizable(True) 486 | col.set_expand(True) 487 | self.tree.insert_column(col, -1) 488 | self.tree.set_enable_search(True) 489 | self.tree.set_search_column(1) 490 | self.tree.set_headers_visible(False) 491 | self.tree.set_tooltip_column(0) 492 | p = [None] 493 | l = [] 494 | for e in self.app.get_toc(key): 495 | while(l and l[-1] >= e['level']): p.pop(); l.pop() 496 | l.append(e['level']) 497 | it = s.append(p[-1],(e['name.utf8'], normalize(e['name.utf8'].lower()), e.get('local', ''), e['is_page'])) 498 | p.append(it) 499 | if e.has_key('local'): self.tree_cont[e['local']] = [s.get_path(it), it] 500 | scroll = Gtk.ScrolledWindow() 501 | scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 502 | scroll.add(self.tree) 503 | self.tree.connect("cursor-changed", self._toc_cb) 504 | return scroll 505 | 506 | def _search_cb(self, e): 507 | m = self.results.get_model() 508 | m.clear() 509 | self.result_cont = {} 510 | e.modify_fg(Gtk.StateType.NORMAL, None) 511 | txt = e.get_text().strip() 512 | self.win.search_e.set_text(txt) 513 | enc = self.app.get_encoding(self.key) 514 | s,r = None,None 515 | try: s,r = self.app.get_chmf(self.key).Search(txt.encode(enc)) 516 | except UnicodeDecodeError: pass 517 | if not s: 518 | print "no res", s, r 519 | e.modify_fg(Gtk.StateType.NORMAL, Gdk.color_parse("#FF0000")) 520 | return 521 | print len(r), "Found!" 522 | for k in r: 523 | k = _fix_entities(k) 524 | try: ku = k.decode('utf-8') 525 | except UnicodeDecodeError: ku = k.decode(enc) 526 | i = m.append(((ku, normalize(ku), r[k], True, 1.0, ))) 527 | self.result_cont[r[k]] = [m.get_path(i),i] 528 | 529 | def build_search_pane(self): 530 | vb = Gtk.VBox() 531 | hb = Gtk.HBox(); vb.pack_start(hb, False, False, 2) 532 | self.search_e = e = Gtk.Entry() 533 | hb.pack_start(e, False, False, 2) 534 | s = Gtk.ListStore(str, str, str, bool, float) # label, normalized, url, is_page, scale 535 | self.results = Gtk.TreeView() 536 | self.results.set_model(s) 537 | col = Gtk.TreeViewColumn('Index', Gtk.CellRendererText(), markup = 0, scale = 4) 538 | col.mark_up = True 539 | col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) 540 | col.set_resizable(True) 541 | col.set_expand(True) 542 | self.results.insert_column(col, -1) 543 | self.results.set_enable_search(True) 544 | self.results.set_search_column(1) 545 | self.results.set_headers_visible(False) 546 | self.results.set_tooltip_column(0) 547 | self.results.connect("cursor-changed", self._toc_cb) 548 | e.connect('activate', self._search_cb) 549 | 550 | scroll = Gtk.ScrolledWindow() 551 | scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 552 | scroll.add(self.results) 553 | vb.pack_start(scroll, True, True, 2) 554 | return vb 555 | 556 | def _toc_cb(self, tree, *a): 557 | if self.working: return 558 | try: 559 | s,i = tree.get_selection().get_selected() 560 | except AttributeError, e: 561 | print "Error:", e 562 | return 563 | is_page = self.win.gen_url(self.key, s.get_value(i, 3)) 564 | if is_page: 565 | url = self.win.gen_url(self.key, s.get_value(i, 2)) 566 | self.win._content.load(url) 567 | 568 | class About(Gtk.AboutDialog): 569 | def __init__(self, parent): 570 | Gtk.AboutDialog.__init__(self, parent=parent) 571 | self.set_default_response(Gtk.ResponseType.CLOSE) 572 | self.connect('delete-event', lambda w, *a: w.hide() or True) 573 | self.connect('response', lambda w, *a: w.hide() or True) 574 | try: self.set_program_name("CHM View Kit") 575 | except AttributeError: pass 576 | self.set_logo_icon_name('chmviewkit') 577 | self.set_name("CHM View Kit") 578 | #self.set_version(version) 579 | self.set_copyright("Copyright © 2011, Ojuba Team ") 580 | self.set_comments(_("CHM (Compiled HTML) Files Viewer")) 581 | self.set_license(""" 582 | Released under terms of Waqf Public License. 583 | This program is free software; you can redistribute it and/or modify 584 | it under the terms of the latest version Waqf Public License as 585 | published by Ojuba.org. 586 | 587 | This program is distributed in the hope that it will be useful, 588 | but WITHOUT ANY WARRANTY; without even the implied warranty of 589 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 590 | 591 | The Latest version of the license can be found on 592 | "http://waqf.ojuba.org/license" 593 | 594 | """) 595 | self.set_website("http://git.ojuba.org/") 596 | self.set_website_label("http://git.ojuba.org") 597 | self.set_authors(["Muayyad Saleh Alsadi ", "Ehab El-Gedawy "]) 598 | self.run() 599 | self.destroy() 600 | # self.set_documenters(documenters) 601 | # self.set_artists(artists) 602 | # self.set_translator_credits(translator_credits) 603 | # self.set_logo(logo) 604 | 605 | 606 | 607 | class MainWindow(Gtk.Window): 608 | def __init__(self, app, port, server): 609 | self.app = app 610 | self.port = port 611 | self.server = server # we need this to quit the server when closing main window 612 | self._open_dlg = None 613 | 614 | Gtk.Window.set_default_icon_name('chmviewkit') 615 | Gtk.Window.__init__(self) 616 | self.set_title(_('CHM View Kit')) 617 | self.set_default_size(600, 480) 618 | 619 | self.maximize() 620 | # add drag-data-recived action 621 | targets = Gtk.TargetList.new([]) 622 | targets.add_uri_targets((1<<5)-1) 623 | self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) 624 | self.drag_dest_set_target_list(targets) 625 | self.connect('drag-data-received', self.drop_data_cb) 626 | self.axl = Gtk.AccelGroup() 627 | self.add_accel_group(self.axl) 628 | 629 | vb = Gtk.VBox(); self.add(vb) 630 | 631 | tools = Gtk.Toolbar() 632 | vb.pack_start(tools, False, False, 2) 633 | 634 | self._content = ContentPane(self, None, _("CHM View Kit")) 635 | vb.pack_start(self._content,True, True, 2) 636 | 637 | ACCEL_CTRL_KEY, ACCEL_CTRL_MOD = Gtk.accelerator_parse("") 638 | ACCEL_SHFT_KEY, ACCEL_SHFT_MOD = Gtk.accelerator_parse("") 639 | b=Gtk.ToolButton.new_from_stock(Gtk.STOCK_OPEN) 640 | b.connect('clicked', self._open_cb) 641 | b.add_accelerator("clicked",self.axl,ord('o'), ACCEL_CTRL_MOD,Gtk.AccelFlags.VISIBLE) 642 | b.set_tooltip_text("%s\t‪%s‬" % (_("Open a CHM file"), "(Ctrl+O)" )) 643 | tools.insert(b, -1) 644 | 645 | b = Gtk.ToolButton.new_from_stock(Gtk.STOCK_PRINT) 646 | b.connect('clicked', lambda a: self._do_in_current_view("execute_script", 'window.print();')) 647 | b.add_accelerator("clicked",self.axl,ord('p'), ACCEL_CTRL_MOD,Gtk.AccelFlags.VISIBLE) 648 | b.set_tooltip_text("%s\t‪%s‬" % (_("Print current page"), "(Ctrl+P)" )) 649 | tools.insert(b, -1) 650 | 651 | tools.insert(Gtk.SeparatorToolItem(), -1) 652 | 653 | self.go_back_b = b = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_BACK) 654 | b.set_sensitive(False) 655 | b.connect('clicked', lambda a: self._do_in_current_view("go_back")) 656 | b.add_accelerator("clicked",self.axl, Gdk.KEY_Left, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 657 | b.set_tooltip_text("%s\t‪%s‬" % (_("Go Back"), "(Alt+Left)")) 658 | 659 | tools.insert(b, -1) 660 | 661 | self.go_forward_b = b = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_FORWARD) 662 | b.set_sensitive(False) 663 | b.connect('clicked', lambda a: self._do_in_current_view("go_forward")) 664 | b.add_accelerator("clicked",self.axl, Gdk.KEY_Right, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 665 | b.set_tooltip_text("%s\t‪%s‬" % (_("Go Forward"), "(Alt+Right)")) 666 | tools.insert(b, -1) 667 | 668 | tools.insert(Gtk.SeparatorToolItem(), -1) 669 | 670 | #tools.insert(Gtk.SeparatorToolItem(), -1) 671 | 672 | img = Gtk.Image() 673 | img.set_from_stock(Gtk.STOCK_ZOOM_IN, Gtk.IconSize.BUTTON) 674 | b = Gtk.ToolButton(icon_widget=img, label=_("Zoom in")) 675 | b.set_is_important(True) 676 | b.add_accelerator("clicked",self.axl,Gdk.KEY_equal, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 677 | b.add_accelerator("clicked",self.axl,Gdk.KEY_plus, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 678 | b.add_accelerator("clicked",self.axl,Gdk.KEY_KP_Add, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 679 | b.set_tooltip_text("%s\t‪%s‬" % (_("Makes things appear bigger"), "(Ctrl++)")) 680 | b.connect('clicked', lambda a: self._do_in_current_view("zoom_in")) 681 | tools.insert(b, -1) 682 | 683 | img = Gtk.Image() 684 | img.set_from_stock(Gtk.STOCK_ZOOM_OUT, Gtk.IconSize.BUTTON) 685 | b = Gtk.ToolButton(icon_widget=img, label=_("Zoom out")) 686 | b.add_accelerator("clicked",self.axl,Gdk.KEY_minus, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 687 | b.add_accelerator("clicked",self.axl,Gdk.KEY_KP_Subtract, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 688 | b.set_tooltip_text("%s\t‪%s‬" % (_("Makes things appear smaller"), "(Ctrl+-)")) 689 | b.connect('clicked', lambda a: self._do_in_current_view("zoom_out")) 690 | tools.insert(b, -1) 691 | 692 | img = Gtk.Image() 693 | img.set_from_stock(Gtk.STOCK_ZOOM_100, Gtk.IconSize.BUTTON) 694 | b = Gtk.ToolButton(icon_widget = img, label = _("1:1 Zoom")) 695 | b.add_accelerator("clicked",self.axl,ord('0'), ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 696 | b.add_accelerator("clicked",self.axl,Gdk.KEY_KP_0, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 697 | b.set_tooltip_text("%s\t‪%s‬" % (_("Restore original zoom factor"), "(Ctrl+0)")) 698 | b.connect('clicked', lambda a: self._do_in_current_view("set_zoom_level",1.0)) 699 | tools.insert(b, -1) 700 | 701 | tools.insert(Gtk.SeparatorToolItem(), -1) 702 | 703 | self.search_e = e = Gtk.Entry() 704 | e.connect('activate', self.search_cb) 705 | e.add_accelerator("activate",self.axl,Gdk.KEY_g, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE) 706 | b = Gtk.ToolItem() 707 | b.add(e) 708 | tools.insert(b, -1) 709 | 710 | # Add CTRL+F accelerator for focusing search entry 711 | self.axl.connect(Gdk.KEY_F, ACCEL_CTRL_MOD, Gtk.AccelFlags.VISIBLE, self.find_cb) 712 | # Add CTRL+SHIFT+G accelerator for backward search 713 | self.axl.connect(Gdk.KEY_g, ACCEL_CTRL_MOD|ACCEL_SHFT_MOD, \ 714 | Gtk.AccelFlags.VISIBLE, lambda *a: self.search_cb(None, False)) 715 | 716 | tools.insert(Gtk.SeparatorToolItem(), -1) 717 | b = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ABOUT) 718 | b.connect('clicked', self._show_about_dlg) 719 | b.set_tooltip_text(_("About CHM View Kit")) 720 | tools.insert(b, -1) 721 | 722 | self.connect("delete_event", self.quit) 723 | self.connect("destroy", self.quit) 724 | self.show_all() 725 | 726 | def find_cb(self, *a): 727 | if not self.search_e.is_focus(): 728 | self.search_e.set_text(self._do_in_current_view('eval_js', 'document.getSelection().toString()')) 729 | self.search_e.grab_focus() 730 | self.search_e.select_region(0, len(self.search_e.get_text())) 731 | self.search_cb(self.search_e) 732 | 733 | def _do_highlight(self, txt): 734 | view = self._get_current_view() 735 | view.set_highlight_text_matches(False) 736 | view.unmark_text_matches() 737 | view.mark_text_matches(txt, False, False) 738 | view.set_highlight_text_matches(True) 739 | 740 | def search_cb(self, e, forward = True): 741 | txt = self.search_e.get_text() 742 | view = self._get_current_view() 743 | self.search_e.modify_fg(Gtk.StateType.NORMAL, None) 744 | if not view or not txt: return None 745 | # returns False if not found 746 | s = view.search_text(txt, False, forward, True) # txt, case, forward, wrap 747 | self._do_highlight(txt) 748 | if not s: 749 | self.search_e.modify_fg(Gtk.StateType.NORMAL, Gdk.color_parse("#FF0000")) 750 | 751 | def _show_about_dlg(self, *a): 752 | return About(self) 753 | 754 | def _show_open_dlg(self, *a): 755 | if self._open_dlg: 756 | return self._open_dlg.run() 757 | self._open_dlg = Gtk.FileChooserDialog("Select files to import", 758 | parent = self, 759 | buttons = (Gtk.STOCK_CANCEL, 760 | Gtk.ResponseType.REJECT, 761 | Gtk.STOCK_OK, 762 | Gtk.ResponseType.ACCEPT)) 763 | ff = Gtk.FileFilter() 764 | ff.set_name(_('CHM Files')) 765 | ff.add_mime_type('application/x-chm') 766 | ff.add_mime_type('application/chm') 767 | self._open_dlg.add_filter(ff) 768 | ff = Gtk.FileFilter() 769 | ff.set_name(_('All files')) 770 | ff.add_pattern('*') 771 | self._open_dlg.add_filter(ff) 772 | self._open_dlg.set_select_multiple(False) 773 | self._open_dlg.connect('delete-event', lambda w,*a: w.hide() or True) 774 | self._open_dlg.connect('response', lambda w,*a: w.hide() or True) 775 | return self._open_dlg.run() 776 | 777 | def gen_url(self, key, fn): 778 | return "http://127.0.0.1:%d/%s$/%s" % (self.port, key, fn) 779 | 780 | def _open_cb(self, *a): 781 | if self._show_open_dlg() != Gtk.ResponseType.ACCEPT: return 782 | chmfn = self._open_dlg.get_filename() 783 | if os.path.exists(chmfn): 784 | manager = Gtk.RecentManager.get_default() 785 | manager.add_item(chmfn) 786 | self._do_open(chmfn) 787 | 788 | def _do_open(self, chmfn): 789 | fn = "" 790 | try: 791 | # FIXME: have a single method for this 792 | key = self.app.load_chm(chmfn) 793 | fn = self.app.get_toc(key)[0]['local'] # FIXME: just put cursor to first is_page 794 | except IOError: error(_("unable to open file [%s]!") % chmfn,None); return 795 | except KeyError: pass 796 | except IndexError: pass 797 | self._content.new_tab(self.gen_url(key, fn), key) 798 | pane = BookSidePane(self, self.app, key) 799 | self.app.chm[key]["pane"] = pane 800 | l = Gtk.Label('sss') 801 | n = self._content.sidepane.append_page(pane, l) 802 | self._content.sidepane.get_nth_page(n).show_all() 803 | self._content.sidepane.set_current_page(n) 804 | 805 | def _get_current_view(self): 806 | n = self._content.tabs.get_current_page() 807 | if n < 0: 808 | return None 809 | return self._content.tabs.get_nth_page(n).get_child() 810 | 811 | 812 | def _do_in_current_view (self, action, *a, **kw): 813 | view = self._get_current_view() 814 | if not view: return None 815 | return getattr(view, action)(*a,**kw) 816 | 817 | def _do_in_all_views (self, action, *a, **kw): 818 | for n in range(self._content.tabs.get_n_pages()): 819 | view = self._content.tabs.get_nth_page(n).get_child() 820 | getattr(view, action)(*a,**kw) 821 | 822 | def drop_data_cb(self, widget, dc, x, y, selection_data, info, t): 823 | for chmfn in selection_data.get_uris(): 824 | if chmfn.startswith('file://'): 825 | f = unquote(chmfn[7:]) 826 | self._do_open(f) 827 | else: 828 | print "Protocol not supported in [%s]" % chmfn 829 | #dc.drop_finish (True, t); 830 | 831 | def quit(self,*args): 832 | self.server.running = False 833 | Gtk.main_quit() 834 | return False 835 | 836 | CHM_HIGH_PORT = 18080 837 | 838 | def launchServer(app): 839 | launched = False 840 | port = CHM_HIGH_PORT 841 | while(not launched): 842 | try: 843 | server = httpserver.serve(app, 844 | host = '127.0.0.1', 845 | port = port, 846 | start_loop = False) 847 | except socket.error: 848 | port+=1 849 | else: 850 | launched = True 851 | return port, server 852 | 853 | class ChmWebApp: 854 | _mimeByExtension = { 855 | 'html': 'text/html', 'htm': 'text/html', 'txt': 'text/plain', 856 | 'css': 'text/css', 'js':'application/javascript', 857 | 'ico': 'image/x-icon', 'png': 'image/png', 'gif': 'image/gif', 858 | 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg' 859 | } 860 | _li_re = re.compile(r'''(]*>)''', re.I | re.S | re.M) 861 | _p_re = re.compile(r''']+)>''', re.I | re.S | re.M) 862 | _kv_re = re.compile(r'''(\S+)\s*=\s*("([^"]*)"|'([^']*)')''', re.I | re.S | re.M) 863 | _href_re = re.compile(r'''(<[^<>]+(?:href|src)=(["'])/)''', re.I | re.S | re.M) 864 | _chr_re = re.compile(r''']*content\s*=\s*['"]?text/html;\s*charset\s*=\s*([^ '">]+)\s*['"]?[^>]*>''', re.I | re.S | re.M) 865 | 866 | def __init__(self): 867 | self.key2file = {} 868 | self.chm = {} 869 | #self.chmf.LoadCHM('sayed-elkhater.chm') 870 | 871 | 872 | def __call__(self, environ, start_response): 873 | uri = environ['PATH_INFO'] 874 | l = uri[1:].split('$/', 1) # we have the a key followed by $/ then the rest of the uri 875 | if len(l) == 2: 876 | key,fn = l 877 | fn = '/'+fn 878 | else: 879 | # in case of no key guess it from referrer 880 | ref = environ.get('HTTP_REFERER','') 881 | (scheme, netloc, path, query, fragment) = urlsplit(ref) 882 | if ref and '$' in path: 883 | l = path[1:].split('$', 1) 884 | key = l[0] 885 | fn = uri 886 | # we can continue without redirect, 887 | # but it's better to redirect so that we always have a valid referrer 888 | start_response("302 moved", [('content-type', 'text/plain'), 889 | ('Location', "/"+key+"$"+fn)]) 890 | return ("moved",) 891 | else: 892 | # in case of no key and no valid referrer give 404 893 | start_response("404 Not found", [('content-type', 'text/plain')]) 894 | return ('not found',) 895 | 896 | ext = fn[fn.rfind('.'):][1:].lower() 897 | mime = self._mimeByExtension.get(ext,"application/octet-stream") 898 | chmf = self.get_chmf(key) 899 | s,u = chmf.ResolveObject(fn) 900 | if s != 0: 901 | start_response("404 Not found", [('content-type', 'text/plain')]) 902 | return ('not found',) 903 | l,data = chmf.RetrieveObject(u) 904 | start_response("200 OK", [('content-type', mime)]) 905 | # to test referrer fix comment out next line 906 | if ext == 'htm': 907 | data = self._href_re.sub(r'\1'+key+'$/',data) 908 | self.get_encoding(key, data) 909 | return (data,) 910 | 911 | def load_chm(self, fn): 912 | key = hashlib.md5(fn).digest().encode('base64')[:-3].replace('/', '_').replace('+', '-') 913 | if self.key2file.has_key(key): 914 | return key 915 | self.key2file[key] = fn 916 | return key 917 | 918 | def get_chmf(self, key): 919 | if not self.chm.has_key(key): 920 | self.chm[key] = {} 921 | if not self.chm[key].has_key('chmf'): 922 | fn = self.key2file[key] 923 | chmf = chm.CHMFile() 924 | s = chmf.LoadCHM(fn) 925 | if s != 1: 926 | raise IOError 927 | self.chm[key]['chmf'] = chmf 928 | return self.chm[key]['chmf'] 929 | 930 | def get_encoding(self, key, html=''): 931 | if self.chm[key].has_key('encoding'): 932 | return self.chm[key]['encoding'] 933 | f, e = self.guess_encoding(html) 934 | if f and e: 935 | self.chm[key]['encoding'] = e 936 | else: 937 | e = 'windows-1256' 938 | return e 939 | 940 | def guess_encoding(self, html = ''): 941 | m = self._chr_re.search(html) 942 | if m: 943 | return True, m.group(1).strip() 944 | e = 'UTF-8' 945 | try: 946 | t = html.decode(e) 947 | except UnicodeDecodeError: 948 | return False, e 949 | return False, None 950 | 951 | def _parse_toc_html(self, html, home = None, title = None): 952 | html = _fix_entities(html) 953 | li = self._li_re 954 | p = self._p_re 955 | level = 0 956 | toc = [] 957 | home = home.lstrip('/') 958 | home_found = False 959 | for i in li.split(html or ""): 960 | e = {} 961 | ul = i.lower() 962 | if ul.startswith(' 2 | 3 | 4 | 20 | 22 | 25 | 29 | 33 | 34 | 37 | 41 | 45 | 46 | 49 | 53 | 57 | 58 | 61 | 65 | 69 | 70 | 73 | 77 | 81 | 82 | 85 | 89 | 93 | 94 | 96 | 100 | 104 | 105 | 108 | 112 | 116 | 117 | 119 | 123 | 127 | 131 | 135 | 136 | 138 | 142 | 146 | 150 | 154 | 158 | 162 | 166 | 167 | 170 | 174 | 178 | 179 | 182 | 186 | 190 | 191 | 194 | 198 | 202 | 203 | 206 | 210 | 214 | 215 | 218 | 222 | 226 | 227 | 230 | 234 | 238 | 239 | 241 | 245 | 249 | 250 | 260 | 270 | 280 | 291 | 301 | 311 | 321 | 331 | 341 | 351 | 361 | 371 | 381 | 391 | 401 | 411 | 421 | 431 | 441 | 447 | 451 | 455 | 459 | 463 | 467 | 468 | 477 | 487 | 497 | 507 | 517 | 527 | 537 | 548 | 559 | 570 | 580 | 591 | 601 | 611 | 622 | 632 | 643 | 654 | 665 | 667 | 671 | 675 | 676 | 678 | 682 | 686 | 687 | 690 | 694 | 698 | 699 | 701 | 705 | 709 | 710 | 713 | 717 | 721 | 722 | 724 | 728 | 732 | 733 | 735 | 739 | 743 | 744 | 746 | 750 | 754 | 755 | 757 | 761 | 765 | 769 | 770 | 772 | 776 | 780 | 781 | 783 | 787 | 791 | 795 | 796 | 798 | 802 | 806 | 807 | 809 | 813 | 817 | 821 | 825 | 826 | 837 | 847 | 857 | 867 | 877 | 887 | 896 | 906 | 916 | 926 | 936 | 946 | 956 | 966 | 976 | 986 | 996 | 1006 | 1016 | 1026 | 1037 | 1047 | 1057 | 1067 | 1077 | 1087 | 1097 | 1107 | 1117 | 1127 | 1138 | 1148 | 1158 | 1168 | 1178 | 1188 | 1198 | 1208 | 1218 | 1228 | 1238 | 1248 | 1258 | 1268 | 1277 | 1286 | 1295 | 1304 | 1313 | 1322 | 1331 | 1340 | 1350 | 1351 | 1374 | 1378 | 1382 | 1383 | 1385 | 1386 | 1388 | image/svg+xml 1389 | 1391 | 1392 | 1393 | 1394 | 1398 | 1401 | 1407 | 1413 | 1423 | 1429 | 1435 | 1441 | 1447 | 1453 | 1459 | 1463 | 1469 | 1475 | 1481 | 1487 | 1493 | 1499 | 1500 | 1506 | 1512 | 1518 | 1524 | 1530 | 1536 | 1542 | 1548 | 1554 | 1560 | 1565 | 1571 | 1577 | 1583 | 1589 | 1595 | 1600 | 1605 | 1611 | 1612 | 1615 | 1622 | 1623 | 1627 | 1634 | 1635 | 1642 | 1646 | 1651 | 1656 | 1661 | 1662 | 1663 | 1664 | --------------------------------------------------------------------------------