mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Remove old viewer and coffeescript and viewer resources
Since the PDF output code is also going to be replaced, none of this is required.
This commit is contained in:
parent
2224f8e7ae
commit
070ad5351e
12
resources/coffee-script.js
vendored
12
resources/coffee-script.js
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>blank</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<div> </div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* bookmarks management
|
||||
* Copyright 2008 Kovid Goyal
|
||||
* License: GNU GPL v3
|
||||
*/
|
||||
|
||||
function selector_in_parent(elem) {
|
||||
var num = elem.prevAll().length;
|
||||
var sel = " > *:eq("+num+") ";
|
||||
return sel;
|
||||
}
|
||||
|
||||
function selector(elem) {
|
||||
var obj = elem;
|
||||
var sel = "";
|
||||
while (obj[0] != document) {
|
||||
sel = selector_in_parent(obj) + sel;
|
||||
obj = obj.parent();
|
||||
}
|
||||
if (sel.length > 2 && sel.charAt(1) == ">") sel = sel.substring(2);
|
||||
return sel;
|
||||
}
|
||||
|
||||
function calculate_bookmark(y, node) {
|
||||
var elem = $(node);
|
||||
var sel = selector(elem);
|
||||
var ratio = (y - elem.offset().top)/elem.height();
|
||||
if (ratio > 1) { ratio = 1; }
|
||||
if (ratio < 0) { ratio = 0; }
|
||||
sel = sel + "|" + ratio;
|
||||
return sel;
|
||||
}
|
||||
|
||||
function animated_scrolling_done() {
|
||||
window.py_bridge.animated_scroll_done();
|
||||
}
|
||||
|
||||
function scroll_to_bookmark(bookmark) {
|
||||
bm = bookmark.split("|");
|
||||
var ratio = 0.7 * parseFloat(bm[1]);
|
||||
$.scrollTo($(bm[0]), 1000,
|
||||
{
|
||||
over:ratio,
|
||||
axis: 'y', // Do not scroll in the x direction
|
||||
onAfter:function(){window.py_bridge.animated_scroll_done()}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
28
resources/viewer/hyphenation.js
vendored
28
resources/viewer/hyphenation.js
vendored
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Hyphenation
|
||||
* Copyright 2008 Kovid Goyal
|
||||
* License: GNU GPL v3
|
||||
*/
|
||||
|
||||
function do_hyphenation(lang) {
|
||||
Hyphenator.config(
|
||||
{
|
||||
'minwordlength' : 6,
|
||||
// 'hyphenchar' : '|',
|
||||
'displaytogglebox' : false,
|
||||
'remoteloading' : false,
|
||||
'doframes' : true,
|
||||
'defaultlanguage' : 'en',
|
||||
'storagetype' : 'session',
|
||||
'onerrorhandler' : function (e) {
|
||||
window.py_bridge.debug(e);
|
||||
}
|
||||
});
|
||||
// console.log(lang);
|
||||
Hyphenator.hyphenate(document.body, lang);
|
||||
}
|
||||
|
||||
function hyphenate_text(text, lang) {
|
||||
return Hyphenator.hyphenate(text, lang);
|
||||
}
|
||||
|
@ -1,52 +0,0 @@
|
||||
/*
|
||||
* images management
|
||||
* Copyright 2008 Kovid Goyal
|
||||
* License: GNU GPL v3
|
||||
*/
|
||||
|
||||
function scale_images() {
|
||||
$("img:visible").each(function() {
|
||||
var img = $(this);
|
||||
var offset = img.offset();
|
||||
var avail_width = window.innerWidth - offset.left - 5;
|
||||
var avail_height = window.innerHeight - 5;
|
||||
img.css('width', img.data('orig-width'));
|
||||
img.css('height', img.data('orig-height'));
|
||||
var width = img.width();
|
||||
var height = img.height();
|
||||
var ratio = 0;
|
||||
|
||||
if (width > avail_width) {
|
||||
ratio = avail_width / width;
|
||||
img.css('width', avail_width+'px');
|
||||
img.css('height', (ratio*height) + 'px');
|
||||
height = height * ratio;
|
||||
width = width * ratio;
|
||||
}
|
||||
|
||||
if (height > avail_height) {
|
||||
ratio = avail_height / height;
|
||||
img.css('height', avail_height);
|
||||
img.css('width', width * ratio);
|
||||
}
|
||||
//window.py_bridge.debug(window.getComputedStyle(this, '').getPropertyValue('max-width'));
|
||||
});
|
||||
}
|
||||
|
||||
function store_original_size_attributes() {
|
||||
$("img").each(function() {
|
||||
var img = $(this);
|
||||
img.data('orig-width', img.css('width'));
|
||||
img.data('orig-height', img.css('height'));
|
||||
});
|
||||
}
|
||||
|
||||
function setup_image_scaling_handlers() {
|
||||
store_original_size_attributes();
|
||||
scale_images();
|
||||
$(window).resize(function(){
|
||||
scale_images();
|
||||
});
|
||||
}
|
||||
|
||||
|
6240
resources/viewer/jquery.js
vendored
6240
resources/viewer/jquery.js
vendored
File diff suppressed because it is too large
Load Diff
215
resources/viewer/jquery_scrollTo.js
vendored
215
resources/viewer/jquery_scrollTo.js
vendored
@ -1,215 +0,0 @@
|
||||
/**
|
||||
* jQuery.ScrollTo
|
||||
* Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
|
||||
* Dual licensed under MIT and GPL.
|
||||
* Date: 5/25/2009
|
||||
*
|
||||
* @projectDescription Easy element scrolling using jQuery.
|
||||
* http://flesler.blogspot.com/2007/10/jqueryscrollto.html
|
||||
* Works with jQuery +1.2.6. Tested on FF 2/3, IE 6/7/8, Opera 9.5/6, Safari 3, Chrome 1 on WinXP.
|
||||
*
|
||||
* @author Ariel Flesler
|
||||
* @version 1.4.2
|
||||
*
|
||||
* @id jQuery.scrollTo
|
||||
* @id jQuery.fn.scrollTo
|
||||
* @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements.
|
||||
* The different options for target are:
|
||||
* - A number position (will be applied to all axes).
|
||||
* - A string position ('44', '100px', '+=90', etc ) will be applied to all axes
|
||||
* - A jQuery/DOM element ( logically, child of the element to scroll )
|
||||
* - A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc )
|
||||
* - A hash { top:x, left:y }, x and y can be any kind of number/string like above.
|
||||
* - A percentage of the container's dimension/s, for example: 50% to go to the middle.
|
||||
* - The string 'max' for go-to-end.
|
||||
* @param {Number} duration The OVERALL length of the animation, this argument can be the settings object instead.
|
||||
* @param {Object,Function} settings Optional set of settings or the onAfter callback.
|
||||
* @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'.
|
||||
* @option {Number} duration The OVERALL length of the animation.
|
||||
* @option {String} easing The easing method for the animation.
|
||||
* @option {Boolean} margin If true, the margin of the target element will be deducted from the final position.
|
||||
* @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }.
|
||||
* @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes.
|
||||
* @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends.
|
||||
* @option {Function} onAfter Function to be called after the scrolling ends.
|
||||
* @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends.
|
||||
* @return {jQuery} Returns the same jQuery object, for chaining.
|
||||
*
|
||||
* @desc Scroll to a fixed position
|
||||
* @example $('div').scrollTo( 340 );
|
||||
*
|
||||
* @desc Scroll relatively to the actual position
|
||||
* @example $('div').scrollTo( '+=340px', { axis:'y' } );
|
||||
*
|
||||
* @dec Scroll using a selector (relative to the scrolled element)
|
||||
* @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } );
|
||||
*
|
||||
* @ Scroll to a DOM element (same for jQuery object)
|
||||
* @example var second_child = document.getElementById('container').firstChild.nextSibling;
|
||||
* $('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){
|
||||
* alert('scrolled!!');
|
||||
* }});
|
||||
*
|
||||
* @desc Scroll on both axes, to different values
|
||||
* @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } );
|
||||
*/
|
||||
;(function( $ ){
|
||||
|
||||
var $scrollTo = $.scrollTo = function( target, duration, settings ){
|
||||
$(window).scrollTo( target, duration, settings );
|
||||
};
|
||||
|
||||
$scrollTo.defaults = {
|
||||
axis:'xy',
|
||||
duration: parseFloat($.fn.jquery) >= 1.3 ? 0 : 1
|
||||
};
|
||||
|
||||
// Returns the element that needs to be animated to scroll the window.
|
||||
// Kept for backwards compatibility (specially for localScroll & serialScroll)
|
||||
$scrollTo.window = function( scope ){
|
||||
return $(window)._scrollable();
|
||||
};
|
||||
|
||||
// Hack, hack, hack :)
|
||||
// Returns the real elements to scroll (supports window/iframes, documents and regular nodes)
|
||||
$.fn._scrollable = function(){
|
||||
return this.map(function(){
|
||||
var elem = this,
|
||||
isWin = !elem.nodeName || $.inArray( elem.nodeName.toLowerCase(), ['iframe','#document','html','body'] ) != -1;
|
||||
|
||||
if( !isWin )
|
||||
return elem;
|
||||
|
||||
var doc = (elem.contentWindow || elem).document || elem.ownerDocument || elem;
|
||||
|
||||
return $.browser.safari || doc.compatMode == 'BackCompat' ?
|
||||
doc.body :
|
||||
doc.documentElement;
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.scrollTo = function( target, duration, settings ){
|
||||
if( typeof duration == 'object' ){
|
||||
settings = duration;
|
||||
duration = 0;
|
||||
}
|
||||
if( typeof settings == 'function' )
|
||||
settings = { onAfter:settings };
|
||||
|
||||
if( target == 'max' )
|
||||
target = 9e9;
|
||||
|
||||
settings = $.extend( {}, $scrollTo.defaults, settings );
|
||||
// Speed is still recognized for backwards compatibility
|
||||
duration = duration || settings.speed || settings.duration;
|
||||
// Make sure the settings are given right
|
||||
settings.queue = settings.queue && settings.axis.length > 1;
|
||||
|
||||
if( settings.queue )
|
||||
// Let's keep the overall duration
|
||||
duration /= 2;
|
||||
settings.offset = both( settings.offset );
|
||||
settings.over = both( settings.over );
|
||||
|
||||
return this._scrollable().each(function(){
|
||||
var elem = this,
|
||||
$elem = $(elem),
|
||||
targ = target, toff, attr = {},
|
||||
win = $elem.is('html,body');
|
||||
|
||||
switch( typeof targ ){
|
||||
// A number will pass the regex
|
||||
case 'number':
|
||||
case 'string':
|
||||
if( /^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(targ) ){
|
||||
targ = both( targ );
|
||||
// We are done
|
||||
break;
|
||||
}
|
||||
// Relative selector, no break!
|
||||
targ = $(targ,this);
|
||||
case 'object':
|
||||
// DOMElement / jQuery
|
||||
if( targ.is || targ.style )
|
||||
// Get the real position of the target
|
||||
toff = (targ = $(targ)).offset();
|
||||
}
|
||||
$.each( settings.axis.split(''), function( i, axis ){
|
||||
var Pos = axis == 'x' ? 'Left' : 'Top',
|
||||
pos = Pos.toLowerCase(),
|
||||
key = 'scroll' + Pos,
|
||||
old = elem[key],
|
||||
max = $scrollTo.max(elem, axis);
|
||||
|
||||
if( toff ){// jQuery / DOMElement
|
||||
attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] );
|
||||
|
||||
// If it's a dom element, reduce the margin
|
||||
if( settings.margin ){
|
||||
attr[key] -= parseInt(targ.css('margin'+Pos)) || 0;
|
||||
attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0;
|
||||
}
|
||||
|
||||
attr[key] += settings.offset[pos] || 0;
|
||||
|
||||
if( settings.over[pos] )
|
||||
// Scroll to a fraction of its width/height
|
||||
attr[key] += targ[axis=='x'?'width':'height']() * settings.over[pos];
|
||||
}else{
|
||||
var val = targ[pos];
|
||||
// Handle percentage values
|
||||
attr[key] = val.slice && val.slice(-1) == '%' ?
|
||||
parseFloat(val) / 100 * max
|
||||
: val;
|
||||
}
|
||||
|
||||
// Number or 'number'
|
||||
if( /^\d+$/.test(attr[key]) )
|
||||
// Check the limits
|
||||
attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max );
|
||||
|
||||
// Queueing axes
|
||||
if( !i && settings.queue ){
|
||||
// Don't waste time animating, if there's no need.
|
||||
if( old != attr[key] )
|
||||
// Intermediate animation
|
||||
animate( settings.onAfterFirst );
|
||||
// Don't animate this axis again in the next iteration.
|
||||
delete attr[key];
|
||||
}
|
||||
});
|
||||
|
||||
animate( settings.onAfter );
|
||||
|
||||
function animate( callback ){
|
||||
$elem.animate( attr, duration, settings.easing, callback && function(){
|
||||
callback.call(this, target, settings);
|
||||
});
|
||||
};
|
||||
|
||||
}).end();
|
||||
};
|
||||
|
||||
// Max scrolling position, works on quirks mode
|
||||
// It only fails (not too badly) on IE, quirks mode.
|
||||
$scrollTo.max = function( elem, axis ){
|
||||
var Dim = axis == 'x' ? 'Width' : 'Height',
|
||||
scroll = 'scroll'+Dim;
|
||||
|
||||
if( !$(elem).is('html,body') )
|
||||
return elem[scroll] - $(elem)[Dim.toLowerCase()]();
|
||||
|
||||
var size = 'client' + Dim,
|
||||
html = elem.ownerDocument.documentElement,
|
||||
body = elem.ownerDocument.body;
|
||||
|
||||
return Math.max( html[scroll], body[scroll] )
|
||||
- Math.min( html[size] , body[size] );
|
||||
|
||||
};
|
||||
|
||||
function both( val ){
|
||||
return typeof val == 'object' ? val : { top:val, left:val };
|
||||
};
|
||||
|
||||
})( jQuery );
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
* reference management
|
||||
* Copyright 2008 Kovid Goyal
|
||||
* License: GNU GPL v3
|
||||
*/
|
||||
|
||||
var reference_old_bgcol = "transparent";
|
||||
var reference_prefix = "1.";
|
||||
var reference_last_highlighted_para = null;
|
||||
|
||||
function show_reference_panel(ref) {
|
||||
panel = $("#calibre_reference_panel");
|
||||
if (panel.length < 1) {
|
||||
$(document.body).append('<div id="calibre_reference_panel" style="top:20px; left:20px; padding-left:30px; padding-right:30px; font:monospace normal;text-align:center; z-index:10000; background: beige; border:red ridge 2px; position:absolute; color: black"><h5>Paragraph</h5><p style="text-indent:0pt">None</p></div>');
|
||||
panel = $("#calibre_reference_panel");
|
||||
}
|
||||
$("> p", panel).text(ref);
|
||||
panel.css({top:(window.pageYOffset+20)+"px", left:(window.pageXOffset+20)+"px"});
|
||||
panel.fadeIn(500);
|
||||
}
|
||||
|
||||
function toggle_reference(e) {
|
||||
p = $(this);
|
||||
if (e.type == "mouseenter") {
|
||||
reference_old_bgcol = p.css("background-color");
|
||||
reference_last_highlighted_para = p;
|
||||
p.css({backgroundColor:"beige"});
|
||||
var i = 0;
|
||||
var paras = $("p");
|
||||
for (j = 0; j < paras.length; j++,i++) {
|
||||
if (paras[j] == p[0]) break;
|
||||
}
|
||||
show_reference_panel(reference_prefix+(i+1) );
|
||||
} else {
|
||||
p.css({backgroundColor:reference_old_bgcol});
|
||||
panel = $("#calibre_reference_panel").hide();
|
||||
reference_last_highlighted_para = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function enter_reference_mode() {
|
||||
$("p").bind("mouseenter mouseleave", toggle_reference);
|
||||
}
|
||||
|
||||
function leave_reference_mode() {
|
||||
$("p").unbind("mouseenter mouseleave", toggle_reference);
|
||||
panel = $("#calibre_reference_panel");
|
||||
if (panel.length > 0) panel.hide();
|
||||
if (reference_last_highlighted_para !== null)
|
||||
reference_last_highlighted_para.css({backgroundColor:reference_old_bgcol});
|
||||
}
|
||||
|
||||
function goto_reference(ref) {
|
||||
var tokens = ref.split(".");
|
||||
if (tokens.length != 2) {alert("Invalid reference: "+ref); return;}
|
||||
var num = parseInt(tokens[1]);
|
||||
if (isNaN(num)) {alert("Invalid reference: "+ref); return;}
|
||||
num -= 1;
|
||||
if (num < 0) {alert("Invalid reference: "+ref); return;}
|
||||
var p = $("p");
|
||||
if (num >= p.length) {alert("Reference not found: "+ref); return;}
|
||||
var dest = $(p[num]);
|
||||
if (window.paged_display.in_paged_mode) {
|
||||
var xpos = dest.offset().left;
|
||||
window.paged_display.scroll_to_xpos(xpos, true, true, 1000);
|
||||
} else
|
||||
$.scrollTo(dest, 1000,
|
||||
{onAfter:function(){window.py_bridge.animated_scroll_done();}});
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,6 @@ class Check(Command):
|
||||
'unicodepoints.py', 'krcodepoints.py', 'jacodepoints.py', 'vncodepoints.py', 'zhcodepoints.py') and
|
||||
'prs500/driver.py' not in y) and not f.endswith('_ui.py'):
|
||||
yield y
|
||||
if f.endswith('.coffee'):
|
||||
yield y
|
||||
|
||||
for x in os.walk(self.j(self.d(self.SRC), 'recipes')):
|
||||
for f in x[-1]:
|
||||
@ -90,12 +88,6 @@ class Check(Command):
|
||||
import whats_new
|
||||
whats_new.render_changelog(self.j(self.d(self.SRC), 'Changelog.yaml'))
|
||||
sys.path.remove(self.wn_path)
|
||||
else:
|
||||
from calibre.utils.serve_coffee import check_coffeescript
|
||||
try:
|
||||
check_coffeescript(f)
|
||||
except:
|
||||
return True
|
||||
|
||||
def run(self, opts):
|
||||
self.fhash_cache = {}
|
||||
|
@ -12,7 +12,7 @@ __all__ = [
|
||||
'gui',
|
||||
'git_version',
|
||||
'develop', 'install',
|
||||
'kakasi', 'coffee', 'rapydscript', 'cacerts', 'recent_uas', 'resources',
|
||||
'kakasi', 'rapydscript', 'cacerts', 'recent_uas', 'resources',
|
||||
'check', 'to3', 'unicode_check', 'iterators_check', 'test',
|
||||
'sdist', 'bootstrap',
|
||||
'manual', 'tag_release',
|
||||
@ -63,10 +63,9 @@ iterators_check = IteratorsCheck()
|
||||
from setup.test import Test
|
||||
test = Test()
|
||||
|
||||
from setup.resources import Resources, Kakasi, Coffee, CACerts, RapydScript, RecentUAs
|
||||
from setup.resources import Resources, Kakasi, CACerts, RapydScript, RecentUAs
|
||||
resources = Resources()
|
||||
kakasi = Kakasi()
|
||||
coffee = Coffee()
|
||||
cacerts = CACerts()
|
||||
recent_uas = RecentUAs()
|
||||
rapydscript = RapydScript()
|
||||
|
@ -6,9 +6,8 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, re, shutil, zipfile, glob, time, sys, hashlib, json, errno
|
||||
import os, re, shutil, zipfile, glob, json, errno
|
||||
from zlib import compress
|
||||
from itertools import chain
|
||||
is_ci = os.environ.get('CI', '').lower() == 'true'
|
||||
|
||||
from setup import Command, basenames, __appname__, download_securely, dump_json
|
||||
@ -30,102 +29,6 @@ def get_opts_from_parser(parser):
|
||||
yield x
|
||||
|
||||
|
||||
class Coffee(Command): # {{{
|
||||
|
||||
description = 'Compile coffeescript files into javascript'
|
||||
COFFEE_DIRS = ('ebooks/oeb/display', 'ebooks/oeb/polish')
|
||||
|
||||
def add_options(self, parser):
|
||||
parser.add_option('--watch', '-w', action='store_true', default=False,
|
||||
help='Autocompile when .coffee files are changed')
|
||||
parser.add_option('--show-js', action='store_true', default=False,
|
||||
help='Display the generated javascript')
|
||||
|
||||
def run(self, opts):
|
||||
self.do_coffee_compile(opts)
|
||||
if opts.watch:
|
||||
try:
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
self.do_coffee_compile(opts, timestamp=True,
|
||||
ignore_errors=True)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
def show_js(self, raw):
|
||||
from pygments.lexers import JavascriptLexer
|
||||
from pygments.formatters import TerminalFormatter
|
||||
from pygments import highlight
|
||||
print(highlight(raw, JavascriptLexer(), TerminalFormatter()))
|
||||
|
||||
def do_coffee_compile(self, opts, timestamp=False, ignore_errors=False):
|
||||
from calibre.utils.serve_coffee import compile_coffeescript
|
||||
src_files = {}
|
||||
for src in self.COFFEE_DIRS:
|
||||
for f in glob.glob(self.j(self.SRC, __appname__, src,
|
||||
'*.coffee')):
|
||||
bn = self.b(f).rpartition('.')[0]
|
||||
arcname = src.replace('/', '.') + '.' + bn + '.js'
|
||||
try:
|
||||
with open(f, 'rb') as fs:
|
||||
src_files[arcname] = (f, hashlib.sha1(fs.read()).hexdigest())
|
||||
except EnvironmentError:
|
||||
time.sleep(0.1)
|
||||
with open(f, 'rb') as fs:
|
||||
src_files[arcname] = (f, hashlib.sha1(fs.read()).hexdigest())
|
||||
|
||||
existing = {}
|
||||
dest = self.j(self.RESOURCES, 'compiled_coffeescript.zip')
|
||||
if os.path.exists(dest):
|
||||
with zipfile.ZipFile(dest, 'r') as zf:
|
||||
existing_hashes = {}
|
||||
raw = zf.comment
|
||||
if raw:
|
||||
existing_hashes = json.loads(raw)
|
||||
for info in zf.infolist():
|
||||
if info.filename in existing_hashes and src_files.get(info.filename, (None, None))[1] == existing_hashes[info.filename]:
|
||||
existing[info.filename] = (zf.read(info), info, existing_hashes[info.filename])
|
||||
|
||||
todo = set(src_files) - set(existing)
|
||||
updated = {}
|
||||
for arcname in todo:
|
||||
name = arcname.rpartition('.')[0]
|
||||
print('\t%sCompiling %s'%(time.strftime('[%H:%M:%S] ') if
|
||||
timestamp else '', name))
|
||||
src, sig = src_files[arcname]
|
||||
js, errors = compile_coffeescript(open(src, 'rb').read(), filename=src)
|
||||
if errors:
|
||||
print('\n\tCompilation of %s failed'%name)
|
||||
for line in errors:
|
||||
print(line, file=sys.stderr)
|
||||
if ignore_errors:
|
||||
js = u'# Compilation from coffeescript failed'
|
||||
else:
|
||||
raise SystemExit(1)
|
||||
else:
|
||||
if opts.show_js:
|
||||
self.show_js(js)
|
||||
print('#'*80)
|
||||
print('#'*80)
|
||||
zi = zipfile.ZipInfo()
|
||||
zi.filename = arcname
|
||||
zi.date_time = time.localtime()[:6]
|
||||
updated[arcname] = (js.encode('utf-8'), zi, sig)
|
||||
if updated:
|
||||
hashes = {}
|
||||
with zipfile.ZipFile(dest, 'w', zipfile.ZIP_STORED) as zf:
|
||||
for raw, zi, sig in sorted(chain(itervalues(updated), itervalues(existing)), key=lambda x: x[1].filename):
|
||||
zf.writestr(zi, raw)
|
||||
hashes[zi.filename] = sig
|
||||
zf.comment = json.dumps(hashes)
|
||||
|
||||
def clean(self):
|
||||
x = self.j(self.RESOURCES, 'compiled_coffeescript.zip')
|
||||
if os.path.exists(x):
|
||||
os.remove(x)
|
||||
# }}}
|
||||
|
||||
|
||||
class Kakasi(Command): # {{{
|
||||
|
||||
description = 'Compile resources for unihandecode'
|
||||
@ -296,7 +199,7 @@ class RapydScript(Command): # {{{
|
||||
class Resources(Command): # {{{
|
||||
|
||||
description = 'Compile various needed calibre resources'
|
||||
sub_commands = ['kakasi', 'coffee', 'mathjax', 'rapydscript']
|
||||
sub_commands = ['kakasi', 'mathjax', 'rapydscript']
|
||||
|
||||
def run(self, opts):
|
||||
from calibre.utils.serialize import msgpack_dumps
|
||||
@ -412,9 +315,8 @@ class Resources(Command): # {{{
|
||||
x = self.j(self.RESOURCES, x+'.pickle')
|
||||
if os.path.exists(x):
|
||||
os.remove(x)
|
||||
from setup.commands import kakasi, coffee
|
||||
from setup.commands import kakasi
|
||||
kakasi.clean()
|
||||
coffee.clean()
|
||||
for x in ('builtin_recipes.xml', 'builtin_recipes.zip',
|
||||
'template-functions.json', 'user-manual-translation-stats.json'):
|
||||
x = self.j(self.RESOURCES, x)
|
||||
|
@ -1,671 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2011, Kovid Goyal <kovid@kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
Based on code originally written by Peter Sorotkin
|
||||
http://code.google.com/p/epub-revision/source/browse/trunk/src/samples/cfi/epubcfi.js
|
||||
|
||||
Improvements with respect to that code:
|
||||
1. Works on all browsers (WebKit, Firefox and IE >= 9)
|
||||
2. Works for content in elements that are scrollable (i.e. have their own scrollbars)
|
||||
3. Much more comprehensive testing/error handling
|
||||
4. Properly encodes/decodes assertions
|
||||
5. Handles points in the padding of elements consistently
|
||||
6. Has a utility method to calculate the CFI for the current viewport position robustly
|
||||
|
||||
To check if this script is compatible with the current browser, call
|
||||
window.cfi.is_compatible() it will throw an exception if not compatible.
|
||||
|
||||
Tested on: Firefox 9, IE 9, Chromium 16, Qt WebKit 2.1
|
||||
###
|
||||
|
||||
log = (error) -> # {{{
|
||||
if error
|
||||
if window?.console?.log
|
||||
window.console.log(error)
|
||||
else if process?.stdout?.write
|
||||
process.stdout.write(error + '\n')
|
||||
# }}}
|
||||
|
||||
# CFI escaping {{{
|
||||
escape_for_cfi = (raw) ->
|
||||
if raw
|
||||
for c in ['^', '[', ']', ',', '(', ')', ';', '~', '@', '-', '!']
|
||||
raw = raw.replace(c, '^'+c)
|
||||
raw
|
||||
|
||||
unescape_from_cfi = (raw) ->
|
||||
ans = raw
|
||||
if raw
|
||||
dropped = false
|
||||
ans = []
|
||||
for c in raw
|
||||
if not dropped and c == '^'
|
||||
dropped = true
|
||||
continue
|
||||
dropped = false
|
||||
ans.push(c)
|
||||
ans = ans.join('')
|
||||
ans
|
||||
# }}}
|
||||
|
||||
fstr = (d) -> # {{{
|
||||
# Convert a timestamp floating point number to a string
|
||||
ans = ""
|
||||
if ( d < 0 )
|
||||
ans = "-"
|
||||
d = -d
|
||||
n = Math.floor(d)
|
||||
ans += n
|
||||
n = Math.round((d-n)*100)
|
||||
if( n != 0 )
|
||||
ans += "."
|
||||
ans += if (n % 10 == 0) then (n/10) else n
|
||||
ans
|
||||
# }}}
|
||||
|
||||
get_current_time = (target) -> # {{{
|
||||
ans = 0
|
||||
if target.currentTime != undefined
|
||||
ans = target.currentTime
|
||||
fstr(ans)
|
||||
# }}}
|
||||
|
||||
window_scroll_pos = (win=window) -> # {{{
|
||||
if typeof(win.pageXOffset) == 'number'
|
||||
x = win.pageXOffset
|
||||
y = win.pageYOffset
|
||||
else # IE < 9
|
||||
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
|
||||
x = document.body.scrollLeft
|
||||
y = document.body.scrollTop
|
||||
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
|
||||
y = document.documentElement.scrollTop
|
||||
x = document.documentElement.scrollLeft
|
||||
return [x, y]
|
||||
# }}}
|
||||
|
||||
viewport_to_document = (x, y, doc=window?.document) -> # {{{
|
||||
until doc == window.document
|
||||
# We are in a frame
|
||||
frame = doc.defaultView.frameElement
|
||||
rect = frame.getBoundingClientRect()
|
||||
x += rect.left
|
||||
y += rect.top
|
||||
doc = frame.ownerDocument
|
||||
win = doc.defaultView
|
||||
[wx, wy] = window_scroll_pos(win)
|
||||
x += wx
|
||||
y += wy
|
||||
return [x, y]
|
||||
# }}}
|
||||
|
||||
# Convert point to character offset {{{
|
||||
range_has_point = (range, x, y) ->
|
||||
for rect in range.getClientRects()
|
||||
if (rect.left <= x <= rect.right) and (rect.top <= y <= rect.bottom)
|
||||
return true
|
||||
return false
|
||||
|
||||
offset_in_text_node = (node, range, x, y) ->
|
||||
limits = [0, node.nodeValue.length]
|
||||
while limits[0] != limits[1]
|
||||
pivot = Math.floor( (limits[0] + limits[1]) / 2 )
|
||||
lr = [limits[0], pivot]
|
||||
rr = [pivot+1, limits[1]]
|
||||
range.setStart(node, pivot)
|
||||
range.setEnd(node, pivot+1)
|
||||
if range_has_point(range, x, y)
|
||||
return pivot
|
||||
range.setStart(node, rr[0])
|
||||
range.setEnd(node, rr[1])
|
||||
if range_has_point(range, x, y)
|
||||
limits = rr
|
||||
continue
|
||||
range.setStart(node, lr[0])
|
||||
range.setEnd(node, lr[1])
|
||||
if range_has_point(range, x, y)
|
||||
limits = lr
|
||||
continue
|
||||
break
|
||||
return limits[0]
|
||||
|
||||
find_offset_for_point = (x, y, node, cdoc) ->
|
||||
range = cdoc.createRange()
|
||||
child = node.firstChild
|
||||
while child
|
||||
if child.nodeType in [3, 4, 5, 6] and child.nodeValue?.length
|
||||
range.setStart(child, 0)
|
||||
range.setEnd(child, child.nodeValue.length)
|
||||
if range_has_point(range, x, y)
|
||||
return [child, offset_in_text_node(child, range, x, y)]
|
||||
child = child.nextSibling
|
||||
|
||||
# The point must be after the last bit of text/in the padding/border, we dont know
|
||||
# how to get a good point in this case
|
||||
throw "Point (#{x}, #{y}) is in the padding/border of #{node}, so cannot calculate offset"
|
||||
|
||||
# }}}
|
||||
|
||||
class CanonicalFragmentIdentifier
|
||||
|
||||
###
|
||||
This class is a namespace to expose CFI functions via the window.cfi
|
||||
object. The most important functions are:
|
||||
|
||||
is_compatible(): Throws an error if the browser is not compatible with
|
||||
this script
|
||||
|
||||
at(x, y): Maps a point to a CFI, if possible
|
||||
at_current(): Returns the CFI corresponding to the current viewport scroll location
|
||||
|
||||
scroll_to(cfi): which scrolls the browser to a point corresponding to the
|
||||
given cfi, and returns the x and y co-ordinates of the point.
|
||||
###
|
||||
|
||||
constructor: () -> # {{{
|
||||
if not this instanceof arguments.callee
|
||||
throw new Error('CFI constructor called as function')
|
||||
this.CREATE_RANGE_ERR = "Your browser does not support the createRange function. Update it to a newer version."
|
||||
this.IE_ERR = "Your browser is too old. You need Internet Explorer version 9 or newer."
|
||||
div = document.createElement('div')
|
||||
ver = 3
|
||||
while true
|
||||
div.innerHTML = "<!--[if gt IE #{ ++ver }]><i></i><![endif]-->"
|
||||
if div.getElementsByTagName('i').length == 0
|
||||
break
|
||||
this.iever = ver
|
||||
this.isie = ver > 4
|
||||
|
||||
# }}}
|
||||
|
||||
is_compatible: () -> # {{{
|
||||
if not window.document.createRange
|
||||
throw this.CREATE_RANGE_ERR
|
||||
# Check if Internet Explorer >= 8 as getClientRects returns physical
|
||||
# rather than logical pixels on older IE
|
||||
if this.isie and this.iever < 9
|
||||
# We have IE < 9
|
||||
throw this.IE_ERR
|
||||
# }}}
|
||||
|
||||
set_current_time: (target, val) -> # {{{
|
||||
if target.currentTime == undefined
|
||||
return
|
||||
if target.readyState == 4 or target.readyState == "complete"
|
||||
target.currentTime = val + 0
|
||||
else
|
||||
fn = ()-> target.currentTime = val
|
||||
target.addEventListener("canplay", fn, false)
|
||||
#}}}
|
||||
|
||||
encode: (doc, node, offset, tail) -> # {{{
|
||||
cfi = tail or ""
|
||||
|
||||
# Handle the offset, if any
|
||||
switch node.nodeType
|
||||
when 1 # Element node
|
||||
if typeof(offset) == 'number'
|
||||
node = node.childNodes.item(offset)
|
||||
when 3, 4, 5, 6 # Text/entity/CDATA node
|
||||
offset or= 0
|
||||
while true
|
||||
p = node.previousSibling
|
||||
if not p or p.nodeType > 8
|
||||
break
|
||||
# log("previous sibling:"+ p + " " + p?.nodeType + " length: " + p?.nodeValue?.length)
|
||||
if p.nodeType not in [2, 8] and p.nodeValue?.length?
|
||||
offset += p.nodeValue.length
|
||||
node = p
|
||||
cfi = ":" + offset + cfi
|
||||
else # Not handled
|
||||
log("Offsets for nodes of type #{ node.nodeType } are not handled")
|
||||
|
||||
# Construct the path to node from root
|
||||
until node == doc
|
||||
p = node.parentNode
|
||||
if not p
|
||||
if node.nodeType == 9 # Document node (iframe)
|
||||
win = node.defaultView
|
||||
if win.frameElement
|
||||
node = win.frameElement
|
||||
cfi = "!" + cfi
|
||||
continue
|
||||
break
|
||||
# Find position of node in parent
|
||||
index = 0
|
||||
child = p.firstChild
|
||||
while true
|
||||
index |= 1 # Increment index by 1 if it is even
|
||||
if child.nodeType == 1
|
||||
index++
|
||||
if child == node
|
||||
break
|
||||
child = child.nextSibling
|
||||
|
||||
# Add id assertions for robustness where possible
|
||||
id = node.getAttribute?('id')
|
||||
idspec = if id then "[#{ escape_for_cfi(id) }]" else ''
|
||||
cfi = '/' + index + idspec + cfi
|
||||
node = p
|
||||
|
||||
cfi
|
||||
# }}}
|
||||
|
||||
decode: (cfi, doc=window?.document) -> # {{{
|
||||
simple_node_regex = ///
|
||||
^/(\d+) # The node count
|
||||
(\[[^\]]*\])? # The optional id assertion
|
||||
///
|
||||
error = null
|
||||
node = doc
|
||||
|
||||
until cfi.length < 1 or error
|
||||
if (r = cfi.match(simple_node_regex)) # Path step
|
||||
target = parseInt(r[1])
|
||||
assertion = r[2]
|
||||
if assertion
|
||||
assertion = unescape_from_cfi(assertion.slice(1, assertion.length-1))
|
||||
index = 0
|
||||
child = node.firstChild
|
||||
|
||||
while true
|
||||
if not child
|
||||
if assertion # Try to use the assertion to find the node
|
||||
child = doc.getElementById(assertion)
|
||||
if child
|
||||
node = child
|
||||
if not child
|
||||
error = "No matching child found for CFI: " + cfi
|
||||
cfi = cfi.substr(r[0].length)
|
||||
break
|
||||
index |= 1 # Increment index by 1 if it is even
|
||||
if child.nodeType == 1
|
||||
index++
|
||||
if index == target
|
||||
cfi = cfi.substr(r[0].length)
|
||||
node = child
|
||||
if assertion and node.id != assertion
|
||||
# The found child does not match the id assertion,
|
||||
# trust the id assertion if an element with that id
|
||||
# exists
|
||||
child = doc.getElementById(assertion)
|
||||
if child
|
||||
node = child
|
||||
break
|
||||
child = child.nextSibling
|
||||
|
||||
else if cfi[0] == '!' # Indirection
|
||||
if node.contentDocument
|
||||
node = node.contentDocument
|
||||
cfi = cfi.substr(1)
|
||||
else
|
||||
error = "Cannot reference #{ node.nodeName }'s content:" + cfi
|
||||
|
||||
else
|
||||
break
|
||||
|
||||
if error
|
||||
log(error)
|
||||
return null
|
||||
|
||||
point = {}
|
||||
error = null
|
||||
offset = null
|
||||
|
||||
if (r = cfi.match(/^:(\d+)/)) != null
|
||||
# Character offset
|
||||
offset = parseInt(r[1])
|
||||
cfi = cfi.substr(r[0].length)
|
||||
|
||||
if (r = cfi.match(/^~(-?\d+(\.\d+)?)/)) != null
|
||||
# Temporal offset
|
||||
point.time = r[1] - 0 # Coerce to number
|
||||
cfi = cfi.substr(r[0].length)
|
||||
|
||||
if (r = cfi.match(/^@(-?\d+(\.\d+)?):(-?\d+(\.\d+)?)/)) != null
|
||||
# Spatial offset
|
||||
point.x = r[1] - 0 # Coerce to number
|
||||
point.y = r[3] - 0 # Coerce to number
|
||||
cfi = cfi.substr(r[0].length)
|
||||
|
||||
if( (r = cfi.match(/^\[([^\]]+)\]/)) != null )
|
||||
assertion = r[1]
|
||||
cfi = cfi.substr(r[0].length)
|
||||
if (r = assertion.match(/;s=([ab])$/)) != null
|
||||
if r.index > 0 and assertion[r.index - 1] != '^'
|
||||
assertion = assertion.substr(0, r.index)
|
||||
point.forward = (r[1] == 'a')
|
||||
assertion = unescape_from_cfi(assertion)
|
||||
# TODO: Handle text assertion
|
||||
|
||||
# Find the text node that contains the offset
|
||||
node?.parentNode?.normalize()
|
||||
if offset != null
|
||||
while true
|
||||
len = node.nodeValue.length
|
||||
if offset < len or (not point.forward and offset == len)
|
||||
break
|
||||
next = false
|
||||
while true
|
||||
nn = node.nextSibling
|
||||
if not nn
|
||||
break
|
||||
if nn.nodeType in [3, 4, 5, 6] and nn.nodeValue?.length # Text node, entity, cdata
|
||||
next = nn
|
||||
break
|
||||
node = nn
|
||||
if not next
|
||||
if offset > len
|
||||
error = "Offset out of range: #{ offset }"
|
||||
offset = len
|
||||
break
|
||||
node = next
|
||||
offset -= len
|
||||
point.offset = offset
|
||||
|
||||
point.node = node
|
||||
if error
|
||||
point.error = error
|
||||
else if cfi.length > 0
|
||||
point.error = "Undecoded CFI: #{ cfi }"
|
||||
|
||||
log(point.error)
|
||||
|
||||
point
|
||||
|
||||
# }}}
|
||||
|
||||
at: (x, y, doc=window?.document) -> # {{{
|
||||
# x, y are in viewport co-ordinates
|
||||
cdoc = doc
|
||||
target = null
|
||||
cwin = cdoc.defaultView
|
||||
tail = ''
|
||||
offset = null
|
||||
name = null
|
||||
|
||||
# Drill down into iframes, etc.
|
||||
while true
|
||||
target = cdoc.elementFromPoint x, y
|
||||
if not target or target.localName in ['html', 'body']
|
||||
# We ignore both html and body even though body could
|
||||
# have text nodes under it as performance is very poor if body
|
||||
# has large margins/padding (for e.g. in fullscreen mode)
|
||||
# A possible solution for this is to wrap all text node
|
||||
# children of body in <span> but that is seriously ugly and
|
||||
# might have side effects. Lets do this only if there are lots of
|
||||
# books in the wild that actually have text children of body,
|
||||
# and even in this case it might be better to change the input
|
||||
# plugin to prevent this from happening.
|
||||
# log("No element at (#{ x }, #{ y })")
|
||||
return null
|
||||
|
||||
name = target.localName
|
||||
if name not in ['iframe', 'embed', 'object']
|
||||
break
|
||||
|
||||
cd = target.contentDocument
|
||||
if not cd
|
||||
break
|
||||
|
||||
# We have an embedded document, transforms x, y into the co-prd
|
||||
# system of the embedded document's viewport
|
||||
rect = target.getBoundingClientRect()
|
||||
x -= rect.left
|
||||
y -= rect.top
|
||||
cdoc = cd
|
||||
cwin = cdoc.defaultView
|
||||
|
||||
(if target.parentNode then target.parentNode else target).normalize()
|
||||
|
||||
if name in ['audio', 'video']
|
||||
tail = "~" + get_current_time(target)
|
||||
|
||||
if name in ['img', 'video']
|
||||
rect = target.getBoundingClientRect()
|
||||
px = ((x - rect.left)*100)/target.offsetWidth
|
||||
py = ((y - rect.top)*100)/target.offsetHeight
|
||||
tail = "#{ tail }@#{ fstr px }:#{ fstr py }"
|
||||
else if name != 'audio'
|
||||
# Get the text offset
|
||||
# We use a custom function instead of caretRangeFromPoint as
|
||||
# caretRangeFromPoint does weird things when the point falls in the
|
||||
# padding of the element
|
||||
if cdoc.createRange
|
||||
[target, offset] = find_offset_for_point(x, y, target, cdoc)
|
||||
else
|
||||
throw this.CREATE_RANGE_ERR
|
||||
|
||||
this.encode(doc, target, offset, tail)
|
||||
# }}}
|
||||
|
||||
point: (cfi, doc=window?.document) -> # {{{
|
||||
r = this.decode(cfi, doc)
|
||||
if not r
|
||||
return null
|
||||
node = r.node
|
||||
ndoc = node.ownerDocument
|
||||
if not ndoc
|
||||
log("CFI node has no owner document: #{ cfi } #{ node }")
|
||||
return null
|
||||
|
||||
nwin = ndoc.defaultView
|
||||
x = null
|
||||
y = null
|
||||
range = null
|
||||
|
||||
if typeof(r.offset) == "number"
|
||||
# Character offset
|
||||
if not ndoc.createRange
|
||||
throw this.CREATE_RANGE_ERR
|
||||
range = ndoc.createRange()
|
||||
if r.forward
|
||||
try_list = [{start:0, end:0, a:0.5}, {start:0, end:1, a:1}, {start:-1, end:0, a:0}]
|
||||
else
|
||||
try_list = [{start:0, end:0, a:0.5}, {start:-1, end:0, a:0}, {start:0, end:1, a:1}]
|
||||
a = null
|
||||
rects = null
|
||||
node_len = node.nodeValue.length
|
||||
offset = r.offset
|
||||
for i in [0, 1]
|
||||
# Try reducing the offset by 1 if we get no match as if it refers to the position after the
|
||||
# last character we wont get a match with getClientRects
|
||||
offset = r.offset - i
|
||||
if offset < 0
|
||||
offset = 0
|
||||
k = 0
|
||||
until rects?.length or k >= try_list.length
|
||||
t = try_list[k++]
|
||||
start_offset = offset + t.start
|
||||
end_offset = offset + t.end
|
||||
a = t.a
|
||||
if start_offset < 0 or end_offset >= node_len
|
||||
continue
|
||||
range.setStart(node, start_offset)
|
||||
range.setEnd(node, end_offset)
|
||||
rects = range.getClientRects()
|
||||
if rects?.length
|
||||
break
|
||||
|
||||
|
||||
if not rects?.length
|
||||
log("Could not find caret position: rects: #{ rects } offset: #{ r.offset }")
|
||||
return null
|
||||
|
||||
else
|
||||
[x, y] = [r.x, r.y]
|
||||
|
||||
{x:x, y:y, node:r.node, time:r.time, range:range, a:a}
|
||||
|
||||
# }}}
|
||||
|
||||
scroll_to: (cfi, callback=false, doc=window?.document) -> # {{{
|
||||
if window.mathjax?.math_present and not window.mathjax?.math_loaded
|
||||
window.mathjax.pending_cfi = [cfi, callback]
|
||||
return
|
||||
point = this.point(cfi, doc)
|
||||
if not point
|
||||
log("No point found for cfi: #{ cfi }")
|
||||
return
|
||||
if typeof point.time == 'number'
|
||||
this.set_current_time(point.node, point.time)
|
||||
|
||||
if point.range != null
|
||||
# Character offset
|
||||
r = point.range
|
||||
[so, eo, sc, ec] = [r.startOffset, r.endOffset, r.startContainer, r.endContainer]
|
||||
node = r.startContainer
|
||||
ndoc = node.ownerDocument
|
||||
nwin = ndoc.defaultView
|
||||
span = ndoc.createElement('span')
|
||||
span.setAttribute('style', 'border-width: 0; padding: 0; margin: 0')
|
||||
r.surroundContents(span)
|
||||
span.scrollIntoView()
|
||||
fn = ->
|
||||
# Remove the span and get the new position now that scrolling
|
||||
# has (hopefully) completed
|
||||
#
|
||||
# In WebKit, the boundingrect of the span is wrong in some
|
||||
# situations, whereas in IE resetting the range causes it to
|
||||
# loose bounding info. So we use the range's rects unless they
|
||||
# are absent, in which case we use the span's rect
|
||||
#
|
||||
rect = span.getBoundingClientRect()
|
||||
|
||||
# Remove the span we inserted
|
||||
p = span.parentNode
|
||||
for node in span.childNodes
|
||||
span.removeChild(node)
|
||||
p.insertBefore(node, span)
|
||||
p.removeChild(span)
|
||||
p.normalize()
|
||||
|
||||
# Reset the range to what it was before the span was added
|
||||
r.setStart(sc, so)
|
||||
r.setEnd(ec, eo)
|
||||
rects = r.getClientRects()
|
||||
if rects.length > 0
|
||||
rect = rects[0]
|
||||
|
||||
x = (point.a*rect.left + (1-point.a)*rect.right)
|
||||
y = (rect.top + rect.bottom)/2
|
||||
[x, y] = viewport_to_document(x, y, ndoc)
|
||||
if callback
|
||||
callback(x, y)
|
||||
else
|
||||
node = point.node
|
||||
nwin = node.ownerDocument.defaultView
|
||||
node.scrollIntoView()
|
||||
|
||||
fn = ->
|
||||
r = node.getBoundingClientRect()
|
||||
[x, y] = viewport_to_document(r.left, r.top, node.ownerDocument)
|
||||
if typeof(point.x) == 'number' and node.offsetWidth
|
||||
x += (point.x*node.offsetWidth)/100
|
||||
if typeof(point.y) == 'number' and node.offsetHeight
|
||||
y += (point.y*node.offsetHeight)/100
|
||||
scrollTo(x, y)
|
||||
if callback
|
||||
callback(x, y)
|
||||
|
||||
setTimeout(fn, 10)
|
||||
|
||||
null
|
||||
# }}}
|
||||
|
||||
at_point: (ox, oy) ->
|
||||
# The CFI at the specified point. Different to at() in that this method
|
||||
# returns null if there is an error, and also calculates a point from
|
||||
# the CFI and returns null if the calculated point is far from the
|
||||
# original point.
|
||||
|
||||
dist = (p1, p2) ->
|
||||
Math.sqrt(Math.pow(p1[0]-p2[0], 2), Math.pow(p1[1]-p2[1], 2))
|
||||
|
||||
try
|
||||
cfi = window.cfi.at(ox, oy)
|
||||
point = window.cfi.point(cfi)
|
||||
catch err
|
||||
cfi = null
|
||||
|
||||
|
||||
if cfi
|
||||
if point.range != null
|
||||
r = point.range
|
||||
rect = r.getClientRects()[0]
|
||||
|
||||
x = (point.a*rect.left + (1-point.a)*rect.right)
|
||||
y = (rect.top + rect.bottom)/2
|
||||
[x, y] = viewport_to_document(x, y, r.startContainer.ownerDocument)
|
||||
else
|
||||
node = point.node
|
||||
r = node.getBoundingClientRect()
|
||||
[x, y] = viewport_to_document(r.left, r.top, node.ownerDocument)
|
||||
if typeof(point.x) == 'number' and node.offsetWidth
|
||||
x += (point.x*node.offsetWidth)/100
|
||||
if typeof(point.y) == 'number' and node.offsetHeight
|
||||
y += (point.y*node.offsetHeight)/100
|
||||
|
||||
if dist(viewport_to_document(ox, oy), [x, y]) > 50
|
||||
cfi = null
|
||||
|
||||
return cfi
|
||||
|
||||
at_current: () -> # {{{
|
||||
[winx, winy] = window_scroll_pos()
|
||||
[winw, winh] = [window.innerWidth, window.innerHeight]
|
||||
max = Math.max
|
||||
winw = max(winw, 400)
|
||||
winh = max(winh, 600)
|
||||
deltay = Math.floor(winh/50)
|
||||
deltax = Math.floor(winw/25)
|
||||
miny = max(-winy, -winh)
|
||||
maxy = winh
|
||||
minx = max(-winx, -winw)
|
||||
maxx = winw
|
||||
|
||||
x_loop = (cury) =>
|
||||
for direction in [-1, 1]
|
||||
delta = deltax * direction
|
||||
curx = 0
|
||||
until (direction < 0 and curx < minx) or (direction > 0 and curx > maxx)
|
||||
cfi = this.at_point(curx, cury)
|
||||
if cfi
|
||||
return cfi
|
||||
curx += delta
|
||||
null
|
||||
|
||||
for direction in [-1, 1]
|
||||
delta = deltay * direction
|
||||
cury = 0
|
||||
until (direction < 0 and cury < miny) or (direction > 0 and cury > maxy)
|
||||
cfi = x_loop(cury, -1)
|
||||
if cfi
|
||||
return cfi
|
||||
cury += delta
|
||||
|
||||
# Use a spatial offset on the html element, since we could not find a
|
||||
# normal CFI
|
||||
[x, y] = window_scroll_pos()
|
||||
de = document.documentElement
|
||||
rect = de.getBoundingClientRect()
|
||||
px = (x*100)/rect.width
|
||||
py = (y*100)/rect.height
|
||||
cfi = "/2@#{ fstr px }:#{ fstr py }"
|
||||
|
||||
return cfi
|
||||
|
||||
# }}}
|
||||
|
||||
if window?
|
||||
window.cfi = new CanonicalFragmentIdentifier()
|
||||
else if process?
|
||||
# Some debugging code goes here to be run with the coffee interpreter
|
||||
cfi = new CanonicalFragmentIdentifier()
|
||||
t = 'a^!,1'
|
||||
log(t)
|
||||
log(escape_for_cfi(t))
|
||||
log(unescape_from_cfi(escape_for_cfi(t)))
|
@ -1,211 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2012, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
###
|
||||
|
||||
if window?.calibre_utils
|
||||
log = window.calibre_utils.log
|
||||
|
||||
merge = (node, cnode) ->
|
||||
rules = node.ownerDocument.defaultView.getMatchedCSSRules(node, '')
|
||||
if rules
|
||||
for rule in rules
|
||||
style = rule.style
|
||||
for name in style
|
||||
val = style.getPropertyValue(name)
|
||||
if val and not cnode.style.getPropertyValue(name)
|
||||
cnode.style.setProperty(name, val)
|
||||
|
||||
inline_styles = (node) ->
|
||||
cnode = node.cloneNode(true)
|
||||
merge(node, cnode)
|
||||
nl = node.getElementsByTagName('*')
|
||||
cnl = cnode.getElementsByTagName('*')
|
||||
for node, i in nl
|
||||
merge(node, cnl[i])
|
||||
|
||||
return cnode
|
||||
|
||||
get_epub_type = (node, possible_values) ->
|
||||
# Try to get the value of the epub:type attribute. Complex as we dont
|
||||
# operate in XML mode
|
||||
epub_type = node.getAttributeNS("http://www.idpf.org/2007/ops", 'type') or node.getAttribute('epub:type')
|
||||
if not epub_type
|
||||
for x in node.attributes # consider any xxx:type="noteref" attribute as marking a note
|
||||
if x.nodeName and x.nodeValue in possible_values and x.nodeName.slice(-':type'.length) == ':type'
|
||||
epub_type = x.nodeValue
|
||||
break
|
||||
return epub_type
|
||||
|
||||
get_containing_block = (node) ->
|
||||
until node?.tagName?.toLowerCase() in ['p', 'div', 'li', 'td', 'h1', 'h2', 'h2', 'h3', 'h4', 'h5', 'h6', 'body']
|
||||
node = node.parentNode
|
||||
if not node
|
||||
break
|
||||
return node
|
||||
|
||||
trim = (str) ->
|
||||
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '')
|
||||
|
||||
is_footnote_link = (node, url, linked_to_anchors, prefix) ->
|
||||
if not url or url.substr(0, prefix.length) != prefix
|
||||
return false # Ignore non-local links
|
||||
epub_type = get_epub_type(node, ['noteref'])
|
||||
if epub_type and epub_type.toLowerCase() == 'noteref'
|
||||
return true
|
||||
if epub_type and epub_type == 'link'
|
||||
return false
|
||||
|
||||
# Check if node or any of its first few parents have vertical-align set
|
||||
[x, num] = [node, 3]
|
||||
while x and num > 0
|
||||
style = window.getComputedStyle(x)
|
||||
if not style.display not in ['inline', 'inline-block']
|
||||
break
|
||||
if style.verticalAlign in ['sub', 'super', 'top', 'bottom']
|
||||
return true
|
||||
x = x.parentNode
|
||||
num -= 1
|
||||
|
||||
# Check if node has a single child with the appropriate css
|
||||
children = (x for x in node.childNodes when x.nodeType == Node.ELEMENT_NODE)
|
||||
if children.length == 1
|
||||
style = window.getComputedStyle(children[0])
|
||||
if style.display in ['inline', 'inline-block'] and style.verticalAlign in ['sub', 'super', 'top', 'bottom']
|
||||
text_children = (x for x in node.childNodes when x.nodeType == Node.TEXT_NODE and x.nodeValue and /\S+/.test(x.nodeValue))
|
||||
if not text_children.length
|
||||
return true
|
||||
|
||||
eid = node.getAttribute('id') or node.getAttribute('name')
|
||||
if eid and linked_to_anchors.hasOwnProperty(eid)
|
||||
# An <a href="..." id="..."> link that is linked back from some other
|
||||
# file in the spine, most likely an endnote. We exclude links that are
|
||||
# the only content of their parent block tag, as these are not likely
|
||||
# to be endnotes.
|
||||
cb = get_containing_block(node)
|
||||
if not cb or cb.tagName.toLowerCase() == 'body'
|
||||
return false
|
||||
ltext = node.textContent
|
||||
if not ltext
|
||||
return false
|
||||
ctext = cb.textContent
|
||||
if not ctext
|
||||
return false
|
||||
if trim(ctext) == trim(ltext)
|
||||
return false
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
is_epub_footnote = (node) ->
|
||||
pv = ['note', 'footnote', 'rearnote']
|
||||
epub_type = get_epub_type(node, pv)
|
||||
if epub_type and epub_type.toLowerCase() in pv
|
||||
return true
|
||||
return false
|
||||
|
||||
block_tags = ['p', 'div', 'li', 'td', 'h1', 'h2', 'h2', 'h3', 'h4', 'h5', 'h6', 'body']
|
||||
block_display_styles = ['block', 'list-item', 'table-cell', 'table']
|
||||
|
||||
get_note_container = (node) ->
|
||||
until node.tagName.toLowerCase() in block_tags or is_epub_footnote(node) or getComputedStyle(node).display in block_display_styles
|
||||
node = node.parentNode
|
||||
if not node
|
||||
break
|
||||
return node
|
||||
|
||||
get_parents_and_self = (node) ->
|
||||
ans = []
|
||||
while node and node isnt document.body
|
||||
ans.push(node)
|
||||
node = node.parentNode
|
||||
return ans
|
||||
|
||||
get_page_break = (node) ->
|
||||
style = getComputedStyle(node)
|
||||
ans = {}
|
||||
for x in ['before', 'after']
|
||||
ans[x] = style.getPropertyValue('page-break-'.concat(x)) in ['always', 'left', 'right']
|
||||
return ans
|
||||
|
||||
hide_children = (node) ->
|
||||
for child in node.childNodes
|
||||
if child.nodeType == Node.ELEMENT_NODE
|
||||
if child.do_not_hide
|
||||
hide_children(child)
|
||||
delete child.do_not_hide
|
||||
else
|
||||
child.style.display = 'none'
|
||||
|
||||
unhide_tree = (elem) ->
|
||||
elem.do_not_hide = true
|
||||
for c in elem.getElementsByTagName('*')
|
||||
c.do_not_hide = true
|
||||
|
||||
class CalibreExtract
|
||||
# This class is a namespace to expose functions via the
|
||||
# window.calibre_extract object.
|
||||
|
||||
constructor: () ->
|
||||
if not this instanceof arguments.callee
|
||||
throw new Error('CalibreExtract constructor called as function')
|
||||
this.marked_node = null
|
||||
|
||||
mark: (node) =>
|
||||
this.marked_node = node
|
||||
|
||||
extract: (node=null) =>
|
||||
if node == null
|
||||
node = this.marked_node
|
||||
cnode = inline_styles(node)
|
||||
return cnode.outerHTML
|
||||
|
||||
is_footnote_link: (a, prefix, linked_to_anchors) ->
|
||||
return is_footnote_link(a, a.href, linked_to_anchors, prefix)
|
||||
|
||||
show_footnote: (target, known_targets) ->
|
||||
if not target
|
||||
return
|
||||
start_elem = document.getElementById(target)
|
||||
if not start_elem
|
||||
return
|
||||
start_elem = get_note_container(start_elem)
|
||||
for elem in get_parents_and_self(start_elem)
|
||||
elem.do_not_hide = true
|
||||
style = window.getComputedStyle(elem)
|
||||
if style.display == 'list-item' and style.listStyleType not in ['disc', 'circle', 'square']
|
||||
# We cannot display list numbers since they will be
|
||||
# incorrect as we are removing siblings of this element.
|
||||
elem.style.listStyleType = 'none'
|
||||
if is_epub_footnote(start_elem)
|
||||
unhide_tree(start_elem)
|
||||
else
|
||||
# Try to detect natural boundaries based on markup for this note
|
||||
found_note_start = false
|
||||
for elem in document.body.getElementsByTagName('*')
|
||||
if found_note_start
|
||||
eid = elem.getAttribute('id')
|
||||
if eid != target and known_targets.hasOwnProperty(eid) and get_note_container(elem) != start_elem
|
||||
console.log('Breaking footnote on anchor: ' + elem.getAttribute('id'))
|
||||
delete get_note_container(elem).do_not_hide
|
||||
break
|
||||
pb = get_page_break(elem)
|
||||
if pb['before']
|
||||
console.log('Breaking footnote on page break before')
|
||||
break
|
||||
if pb['after']
|
||||
unhide_tree(elem)
|
||||
console.log('Breaking footnote on page break after')
|
||||
break
|
||||
elem.do_not_hide = true
|
||||
else if elem is start_elem
|
||||
found_note_start = true
|
||||
|
||||
hide_children(document.body)
|
||||
location.hash = '#' + target
|
||||
|
||||
if window?
|
||||
window.calibre_extract = new CalibreExtract()
|
@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2012, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
###
|
||||
|
||||
|
||||
log = window.calibre_utils.log
|
||||
|
||||
class FullScreen
|
||||
# This class is a namespace to expose functions via the
|
||||
# window.full_screen object. The most important functions are:
|
||||
|
||||
constructor: () ->
|
||||
if not this instanceof arguments.callee
|
||||
throw new Error('FullScreen constructor called as function')
|
||||
this.in_full_screen = false
|
||||
this.initial_left_margin = null
|
||||
this.initial_right_margin = null
|
||||
|
||||
save_margins: () ->
|
||||
bs = document.body.style
|
||||
this.initial_left_margin = bs.marginLeft
|
||||
this.initial_right_margin = bs.marginRight
|
||||
|
||||
on: (max_text_width, max_text_height, in_paged_mode) ->
|
||||
if in_paged_mode
|
||||
window.paged_display.max_col_width = max_text_width
|
||||
window.paged_display.max_col_height = max_text_height
|
||||
else
|
||||
s = document.body.style
|
||||
s.maxWidth = max_text_width + 'px'
|
||||
s.marginLeft = 'auto'
|
||||
s.marginRight = 'auto'
|
||||
window.addEventListener('click', this.handle_click, false)
|
||||
|
||||
off: (in_paged_mode) ->
|
||||
window.removeEventListener('click', this.handle_click, false)
|
||||
if in_paged_mode
|
||||
window.paged_display.max_col_width = -1
|
||||
window.paged_display.max_col_height = -1
|
||||
else
|
||||
s = document.body.style
|
||||
s.maxWidth = 'none'
|
||||
if this.initial_left_margin != null
|
||||
s.marginLeft = this.initial_left_margin
|
||||
if this.initial_right_margin != null
|
||||
s.marginRight = this.initial_right_margin
|
||||
|
||||
handle_click: (event) ->
|
||||
if event.target not in [document.documentElement, document.body] or event.button != 0
|
||||
return
|
||||
res = null
|
||||
if window.paged_display.in_paged_mode
|
||||
res = window.paged_display.click_for_page_turn(event)
|
||||
else
|
||||
br = document.body.getBoundingClientRect()
|
||||
if not (br.left <= event.clientX <= br.right)
|
||||
res = event.clientX < br.left
|
||||
if res != null
|
||||
window.py_bridge.page_turn_requested(res)
|
||||
|
||||
if window?
|
||||
window.full_screen = new FullScreen()
|
||||
|
@ -1,132 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2012, Kovid Goyal <kovid@kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
###
|
||||
|
||||
window_scroll_pos = (win=window) -> # {{{
|
||||
if typeof(win.pageXOffset) == 'number'
|
||||
x = win.pageXOffset
|
||||
y = win.pageYOffset
|
||||
else # IE < 9
|
||||
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
|
||||
x = document.body.scrollLeft
|
||||
y = document.body.scrollTop
|
||||
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
|
||||
y = document.documentElement.scrollTop
|
||||
x = document.documentElement.scrollLeft
|
||||
return [x, y]
|
||||
# }}}
|
||||
|
||||
viewport_to_document = (x, y, doc=window?.document) -> # {{{
|
||||
until doc == window.document
|
||||
# We are in a frame
|
||||
frame = doc.defaultView.frameElement
|
||||
rect = frame.getBoundingClientRect()
|
||||
x += rect.left
|
||||
y += rect.top
|
||||
doc = frame.ownerDocument
|
||||
win = doc.defaultView
|
||||
[wx, wy] = window_scroll_pos(win)
|
||||
x += wx
|
||||
y += wy
|
||||
return [x, y]
|
||||
# }}}
|
||||
|
||||
class BookIndexing
|
||||
###
|
||||
This class is a namespace to expose indexing functions via the
|
||||
window.book_indexing object. The most important functions are:
|
||||
|
||||
anchor_positions(): Get the absolute (document co-ordinate system) position
|
||||
for elements with the specified id/name attributes.
|
||||
|
||||
###
|
||||
|
||||
constructor: () ->
|
||||
this.cache = {}
|
||||
this.last_check = [null, null]
|
||||
|
||||
cache_valid: (anchors) ->
|
||||
if not anchors
|
||||
return false
|
||||
for a in anchors
|
||||
if not Object.prototype.hasOwnProperty.call(this.cache, a)
|
||||
return false
|
||||
for p of this.cache
|
||||
if Object.prototype.hasOwnProperty.call(this.cache, p) and p not in anchors
|
||||
return false
|
||||
return true
|
||||
|
||||
anchor_positions: (anchors, use_cache=false) ->
|
||||
body = document.body
|
||||
doc_constant = body.scrollHeight == this.last_check[1] and body.scrollWidth == this.last_check[0]
|
||||
if use_cache and doc_constant and this.cache_valid(anchors)
|
||||
return this.cache
|
||||
|
||||
ans = {}
|
||||
if not anchors
|
||||
return ans
|
||||
for anchor in anchors
|
||||
elem = document.getElementById(anchor)
|
||||
if elem == null
|
||||
# Look for an <a name="anchor"> element
|
||||
try
|
||||
result = document.evaluate(
|
||||
".//*[local-name() = 'a' and @name='#{ anchor }']",
|
||||
body, null,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE, null)
|
||||
elem = result.singleNodeValue
|
||||
catch error
|
||||
# The anchor had a ' or other invalid char
|
||||
elem = null
|
||||
if elem == null
|
||||
pos = [body.scrollWidth+1000, body.scrollHeight+1000]
|
||||
else
|
||||
# Because of a bug in WebKit's getBoundingClientRect() in
|
||||
# column mode, this position can be inaccurate,
|
||||
# see https://bugs.launchpad.net/calibre/+bug/1132641 for a
|
||||
# test case. The usual symptom of the inaccuracy is br.top is
|
||||
# highly negative.
|
||||
br = elem.getBoundingClientRect()
|
||||
pos = viewport_to_document(br.left, br.top, elem.ownerDocument)
|
||||
|
||||
if window.paged_display?.in_paged_mode
|
||||
pos[0] = window.paged_display.column_at(pos[0])
|
||||
ans[anchor] = pos
|
||||
|
||||
this.cache = ans
|
||||
this.last_check = [body.scrollWidth, body.scrollHeight]
|
||||
return ans
|
||||
|
||||
all_links_and_anchors: () ->
|
||||
body = document.body
|
||||
links = []
|
||||
anchors = {}
|
||||
in_paged_mode = window.paged_display?.in_paged_mode
|
||||
|
||||
for a in document.querySelectorAll("body, body a[href], body [id], body a[name]")
|
||||
if in_paged_mode
|
||||
geom = window.paged_display.column_location(a)
|
||||
else
|
||||
br = a.getBoundingClientRect()
|
||||
[left, top] = viewport_to_document(br.left, br.top, a.ownerDocument)
|
||||
geom = {'left':left, 'top':top, 'width':br.right-br.left, 'height':br.bottom-br.top}
|
||||
|
||||
href = a.getAttribute('href')
|
||||
if href
|
||||
links.push([href, geom])
|
||||
id = a.getAttribute("id")
|
||||
if id and not anchors[id]
|
||||
anchors[id] = geom
|
||||
if a.tagName in ['A', "a"]
|
||||
name = a.getAttribute("name")
|
||||
if name and not anchors[name]
|
||||
anchors[name] = geom
|
||||
|
||||
return {'links':links, 'anchors':anchors}
|
||||
|
||||
if window?
|
||||
window.book_indexing = new BookIndexing()
|
@ -1,129 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2012, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
###
|
||||
|
||||
|
||||
log = window.calibre_utils.log
|
||||
|
||||
startswith = (string, q) ->
|
||||
return string.substring(0, q.length) == q
|
||||
|
||||
init_mathjax = () ->
|
||||
orig = window.MathJax.Ajax.fileURL.bind(window.MathJax.Ajax)
|
||||
window.MathJax.Ajax.fileURL = (mathjax_name) ->
|
||||
ans = orig(mathjax_name)
|
||||
if startswith(mathjax_name, '[MathJax]/../fonts')
|
||||
ans = ans.replace('/../fonts', '/fonts')
|
||||
return ans
|
||||
|
||||
|
||||
class MathJax
|
||||
# This class is a namespace to expose functions via the
|
||||
# window.mathjax object. The most important functions are:
|
||||
#
|
||||
|
||||
constructor: () ->
|
||||
if not this instanceof arguments.callee
|
||||
throw new Error('MathJax constructor called as function')
|
||||
this.base = null
|
||||
this.math_present = false
|
||||
this.math_loaded = false
|
||||
this.pending_cfi = null
|
||||
this.hub = null
|
||||
|
||||
load_mathjax: (user_config, is_windows) ->
|
||||
if this.base == null
|
||||
log('You must specify the path to the MathJax installation before trying to load MathJax')
|
||||
return null
|
||||
|
||||
script = document.createElement('script')
|
||||
|
||||
script.type = 'text/javascript'
|
||||
script.onerror = (ev) ->
|
||||
console.log('Failed to load MathJax script: ' + ev.target.src)
|
||||
base = this.base
|
||||
if base.substr(base.length - 1) != '/'
|
||||
base += '/'
|
||||
script.src = base + 'MathJax.js'
|
||||
window.MathJax = {AuthorInit: init_mathjax}
|
||||
script.text = user_config + ('''
|
||||
MathJax.Hub.signal.Interest(function (message) {if (String(message).match(/error/i)) {console.log(message)}});
|
||||
MathJax.Hub.Config({
|
||||
positionToHash: false,
|
||||
showMathMenu: false,
|
||||
extensions: ["tex2jax.js", "asciimath2jax.js", "mml2jax.js"],
|
||||
jax: ["input/TeX","input/MathML","input/AsciiMath","output/CommonHTML"],
|
||||
TeX: {
|
||||
extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
|
||||
}
|
||||
});
|
||||
MathJax.Hub.Startup.onload();
|
||||
MathJax.Hub.Register.StartupHook("End", window.mathjax.load_finished);
|
||||
window.mathjax.hub = MathJax.Hub
|
||||
''')
|
||||
document.head.appendChild(script)
|
||||
|
||||
load_finished: () =>
|
||||
log('MathJax load finished!')
|
||||
this.math_loaded = true
|
||||
if this.pending_cfi != null
|
||||
[cfi, callback] = this.pending_cfi
|
||||
this.pending_cfi = null
|
||||
window.cfi.scroll_to(cfi, callback)
|
||||
|
||||
check_for_math: (is_windows) ->
|
||||
script = null
|
||||
this.math_present = false
|
||||
this.math_loaded = false
|
||||
this.pending_cfi = null
|
||||
user_config = ''
|
||||
for c in document.getElementsByTagName('script')
|
||||
if c.getAttribute('type') == 'text/x-mathjax-config'
|
||||
if c.text
|
||||
user_config += c.text
|
||||
script = c
|
||||
c.parentNode.removeChild(c)
|
||||
|
||||
if script != null or document.getElementsByTagName('math').length > 0
|
||||
this.math_present = true
|
||||
this.remove_math_fallbacks()
|
||||
this.load_mathjax(user_config, is_windows)
|
||||
return this.math_present
|
||||
|
||||
remove_math_fallbacks: () ->
|
||||
# localName no longer exists in Chrome >= 46 so you will need to
|
||||
# investigate a proper solution for this in modern browsers. Probably
|
||||
# use document.evaluate() and node.tagName? IE does not support
|
||||
# document.evaluate() but Edge may or may not, you will need to
|
||||
# experiment
|
||||
for sw in document.getElementsByTagName("epub:switch")
|
||||
non_math = []
|
||||
found_math = false
|
||||
c = sw.firstChild
|
||||
while c
|
||||
if c.localName == 'epub:case'
|
||||
if c.getAttribute('required-namespace') == "http://www.w3.org/1998/Math/MathML"
|
||||
found_math = c
|
||||
else
|
||||
non_math.push(c)
|
||||
else if c.localName == 'epub:default'
|
||||
non_math.push(c)
|
||||
c = c.nextSibling
|
||||
if found_math
|
||||
for c in non_math
|
||||
c.style.display = 'none'
|
||||
|
||||
after_resize: () ->
|
||||
if not this.math_present or this.hub == null
|
||||
return
|
||||
# SVG output does not dynamically reflow on resize, so we manually
|
||||
# rerender, this is slow, but neccessary for tables and equation
|
||||
# numbers.
|
||||
this.hub.Queue(["Rerender",this.hub])
|
||||
|
||||
if window?
|
||||
window.mathjax = new MathJax()
|
@ -1,672 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2012, Kovid Goyal <kovid@kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
###
|
||||
|
||||
log = window.calibre_utils.log
|
||||
|
||||
runscripts = (parent) ->
|
||||
for script in parent.getElementsByTagName('script')
|
||||
eval(script.text || script.textContent || script.innerHTML || '')
|
||||
|
||||
first_child = (parent) ->
|
||||
c = parent.firstChild
|
||||
count = 0
|
||||
while c?.nodeType != 1 and count < 20
|
||||
c = c?.nextSibling
|
||||
count += 1
|
||||
if c?.nodeType == 1
|
||||
return c
|
||||
return null
|
||||
|
||||
has_start_text = (elem) ->
|
||||
# Returns true if elem has some non-whitespace text before its first child
|
||||
# element
|
||||
for c in elem.childNodes
|
||||
if c.nodeType not in [Node.TEXT_NODE, Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE]
|
||||
break
|
||||
if c.nodeType == Node.TEXT_NODE and c.nodeValue != null and /\S/.test(c.nodeValue)
|
||||
return true
|
||||
return false
|
||||
|
||||
create_page_div = (elem) ->
|
||||
div = document.createElement('blank-page-div')
|
||||
div.innerText = ' \n '
|
||||
document.body.appendChild(div)
|
||||
div.style.setProperty('-webkit-column-break-before', 'always')
|
||||
div.style.setProperty('display', 'block')
|
||||
div.style.setProperty('white-space', 'pre')
|
||||
div.style.setProperty('background-color', 'transparent')
|
||||
div.style.setProperty('background-image', 'none')
|
||||
div.style.setProperty('border-width', '0')
|
||||
div.style.setProperty('float', 'none')
|
||||
div.style.setProperty('position', 'static')
|
||||
|
||||
class PagedDisplay
|
||||
# This class is a namespace to expose functions via the
|
||||
# window.paged_display object. The most important functions are:
|
||||
#
|
||||
# set_geometry(): sets the parameters used to layout text in paged mode
|
||||
#
|
||||
# layout(): causes the currently loaded document to be laid out in columns.
|
||||
|
||||
constructor: () ->
|
||||
if not this instanceof arguments.callee
|
||||
throw new Error('PagedDisplay constructor called as function')
|
||||
this.set_geometry()
|
||||
this.page_width = 0
|
||||
this.screen_width = 0
|
||||
this.side_margin = 0
|
||||
this.in_paged_mode = false
|
||||
this.current_margin_side = 0
|
||||
this.is_full_screen_layout = false
|
||||
this.max_col_width = -1
|
||||
this.max_col_height = - 1
|
||||
this.current_page_height = null
|
||||
this.document_margins = null
|
||||
this.use_document_margins = false
|
||||
this.footer_template = null
|
||||
this.header_template = null
|
||||
this.header = null
|
||||
this.footer = null
|
||||
this.hf_style = null
|
||||
|
||||
read_document_margins: () ->
|
||||
# Read page margins from the document. First checks for an @page rule.
|
||||
# If that is not found, side margins are set to the side margins of the
|
||||
# body element.
|
||||
if this.document_margins is null
|
||||
this.document_margins = {left:null, top:null, right:null, bottom:null}
|
||||
tmp = document.createElement('div')
|
||||
tmp.style.visibility = 'hidden'
|
||||
tmp.style.position = 'absolute'
|
||||
document.body.appendChild(tmp)
|
||||
for sheet in document.styleSheets
|
||||
if sheet.rules
|
||||
for rule in sheet.rules
|
||||
if rule.type == CSSRule.PAGE_RULE
|
||||
for prop in ['left', 'top', 'bottom', 'right']
|
||||
val = rule.style.getPropertyValue('margin-'+prop)
|
||||
if val
|
||||
tmp.style.height = val
|
||||
pxval = parseInt(window.getComputedStyle(tmp).height)
|
||||
if not isNaN(pxval)
|
||||
this.document_margins[prop] = pxval
|
||||
document.body.removeChild(tmp)
|
||||
if this.document_margins.left is null
|
||||
val = parseInt(window.getComputedStyle(document.body).marginLeft)
|
||||
if not isNaN(val)
|
||||
this.document_margins.left = val
|
||||
if this.document_margins.right is null
|
||||
val = parseInt(window.getComputedStyle(document.body).marginRight)
|
||||
if not isNaN(val)
|
||||
this.document_margins.right = val
|
||||
|
||||
set_geometry: (cols_per_screen=1, margin_top=20, margin_side=40, margin_bottom=20) ->
|
||||
this.cols_per_screen = cols_per_screen
|
||||
if this.use_document_margins and this.document_margins != null
|
||||
this.margin_top = if this.document_margins.top != null then this.document_margins.top else margin_top
|
||||
this.margin_bottom = if this.document_margins.bottom != null then this.document_margins.bottom else margin_bottom
|
||||
if this.document_margins.left != null
|
||||
this.margin_side = this.document_margins.left
|
||||
else if this.document_margins.right != null
|
||||
this.margin_side = this.document_margins.right
|
||||
else
|
||||
this.margin_side = margin_side
|
||||
this.effective_margin_top = this.margin_top
|
||||
this.effective_margin_bottom = this.margin_bottom
|
||||
else
|
||||
this.margin_top = margin_top
|
||||
this.margin_side = margin_side
|
||||
this.margin_bottom = margin_bottom
|
||||
this.effective_margin_top = this.margin_top
|
||||
this.effective_margin_bottom = this.margin_bottom
|
||||
|
||||
handle_rtl_body: (body_style) ->
|
||||
if body_style.direction == "rtl"
|
||||
for node in document.body.childNodes
|
||||
if node.nodeType == node.ELEMENT_NODE and window.getComputedStyle(node).direction == "rtl"
|
||||
node.style.setProperty("direction", "rtl")
|
||||
document.body.style.direction = "ltr"
|
||||
document.documentElement.style.direction = 'ltr'
|
||||
|
||||
layout: (is_single_page=false) ->
|
||||
# start_time = new Date().getTime()
|
||||
body_style = window.getComputedStyle(document.body)
|
||||
bs = document.body.style
|
||||
first_layout = false
|
||||
if not this.in_paged_mode
|
||||
# Check if the current document is a full screen layout like
|
||||
# cover, if so we treat it specially.
|
||||
single_screen = (document.body.scrollHeight < window.innerHeight + 75)
|
||||
has_svg = document.getElementsByTagName('svg').length > 0
|
||||
only_img = document.getElementsByTagName('img').length == 1 and document.getElementsByTagName('div').length < 3 and document.getElementsByTagName('p').length < 2
|
||||
if only_img
|
||||
has_viewport = document.head and document.head.querySelector('meta[name="viewport"]')
|
||||
if has_viewport
|
||||
# Has a viewport and only an img, is probably a comic, see for
|
||||
# example: https://bugs.launchpad.net/bugs/1667357
|
||||
single_screen = true
|
||||
this.handle_rtl_body(body_style)
|
||||
first_layout = true
|
||||
if not single_screen and this.cols_per_screen > 1
|
||||
num = this.cols_per_screen - 1
|
||||
elems = document.querySelectorAll('body > *')
|
||||
if elems.length == 1
|
||||
# Workaround for the case when the content is wrapped in a
|
||||
# 100% height <div>. This causes the generated page divs to
|
||||
# not be in the correct location. See
|
||||
# https://bugs.launchpad.net/bugs/1594657 for an example.
|
||||
elems[0].style.height = 'auto'
|
||||
while num > 0
|
||||
num -= 1
|
||||
create_page_div()
|
||||
|
||||
ww = window.innerWidth
|
||||
|
||||
# Calculate the column width so that cols_per_screen columns fit in the
|
||||
# window in such a way the right margin of the last column is <=
|
||||
# side_margin (it may be less if the window width is not a
|
||||
# multiple of n*(col_width+2*side_margin).
|
||||
|
||||
n = this.cols_per_screen
|
||||
adjust = ww - Math.floor(ww/n)*n
|
||||
# Ensure that the margins are large enough that the adjustment does not
|
||||
# cause them to become negative semidefinite
|
||||
sm = Math.max(2*adjust, this.margin_side)
|
||||
# Minimum column width, for the cases when the window is too
|
||||
# narrow
|
||||
col_width = Math.max(100, ((ww - adjust)/n) - 2*sm)
|
||||
if this.max_col_width > 0 and col_width > this.max_col_width
|
||||
# Increase the side margin to ensure that col_width is no larger
|
||||
# than max_col_width
|
||||
sm += Math.ceil( (col_width - this.max_col_width) / 2*n )
|
||||
col_width = Math.max(100, ((ww - adjust)/n) - 2*sm)
|
||||
this.col_width = col_width
|
||||
this.page_width = col_width + 2*sm
|
||||
this.side_margin = sm
|
||||
this.screen_width = this.page_width * this.cols_per_screen
|
||||
|
||||
fgcolor = body_style.getPropertyValue('color')
|
||||
|
||||
bs.setProperty('box-sizing', 'content-box')
|
||||
bs.setProperty('-webkit-column-gap', 2*sm + 'px')
|
||||
bs.setProperty('-webkit-column-width', col_width + 'px')
|
||||
bs.setProperty('-webkit-column-rule', '0px inset blue')
|
||||
bs.setProperty('column-fill', 'auto')
|
||||
|
||||
# Without this, webkit bleeds the margin of the first block(s) of body
|
||||
# above the columns, which causes them to effectively be added to the
|
||||
# page margins (the margin collapse algorithm)
|
||||
bs.setProperty('-webkit-margin-collapse', 'separate')
|
||||
c = first_child(document.body)
|
||||
if c != null
|
||||
# Remove page breaks on the first few elements to prevent blank pages
|
||||
# at the start of a chapter
|
||||
c.style.setProperty('-webkit-column-break-before', 'avoid')
|
||||
if c.tagName?.toLowerCase() == 'div'
|
||||
c2 = first_child(c)
|
||||
if c2 != null and not has_start_text(c)
|
||||
# Common pattern of all content being enclosed in a wrapper
|
||||
# <div>, see for example: https://bugs.launchpad.net/bugs/1366074
|
||||
# In this case, we also modify the first child of the div
|
||||
# as long as there was no text before it.
|
||||
c2.style.setProperty('-webkit-column-break-before', 'avoid')
|
||||
|
||||
|
||||
this.effective_margin_top = this.margin_top
|
||||
this.effective_margin_bottom = this.margin_bottom
|
||||
this.current_page_height = window.innerHeight - this.margin_top - this.margin_bottom
|
||||
if this.max_col_height > 0 and this.current_page_height > this.max_col_height
|
||||
eh = Math.ceil((this.current_page_height - this.max_col_height) / 2)
|
||||
this.effective_margin_top += eh
|
||||
this.effective_margin_bottom += eh
|
||||
this.current_page_height -= 2 * eh
|
||||
|
||||
bs.setProperty('overflow', 'visible')
|
||||
bs.setProperty('height', this.current_page_height + 'px')
|
||||
bs.setProperty('width', (window.innerWidth - 2*sm)+'px')
|
||||
bs.setProperty('padding-top', this.effective_margin_top + 'px')
|
||||
bs.setProperty('padding-bottom', this.effective_margin_bottom+'px')
|
||||
bs.setProperty('padding-left', sm+'px')
|
||||
bs.setProperty('padding-right', sm+'px')
|
||||
for edge in ['left', 'right', 'top', 'bottom']
|
||||
bs.setProperty('margin-'+edge, '0px')
|
||||
bs.setProperty('border-'+edge+'-width', '0px')
|
||||
bs.setProperty('min-width', '0')
|
||||
bs.setProperty('max-width', 'none')
|
||||
bs.setProperty('min-height', '0')
|
||||
bs.setProperty('max-height', 'none')
|
||||
|
||||
# Convert page-breaks to column-breaks
|
||||
for sheet in document.styleSheets
|
||||
if sheet.rules
|
||||
for rule in sheet.rules
|
||||
if rule.type == CSSRule.STYLE_RULE
|
||||
for prop in ['page-break-before', 'page-break-after', 'page-break-inside']
|
||||
val = rule.style.getPropertyValue(prop)
|
||||
if val
|
||||
cprop = '-webkit-column-' + prop.substr(5)
|
||||
priority = rule.style.getPropertyPriority(prop)
|
||||
rule.style.setProperty(cprop, val, priority)
|
||||
|
||||
if first_layout
|
||||
# Because of a bug in webkit column mode, svg elements defined with
|
||||
# width 100% are wider than body and lead to a blank page after the
|
||||
# current page (when cols_per_screen == 1). Similarly img elements
|
||||
# with height=100% overflow the first column
|
||||
# We only set full_screen_layout if scrollWidth is in (body_width,
|
||||
# 2*body_width) as if it is <= body_width scrolling will work
|
||||
# anyway and if it is >= 2*body_width it can't be a full screen
|
||||
# layout
|
||||
body_width = document.body.offsetWidth + 2 * sm
|
||||
this.is_full_screen_layout = (only_img or has_svg) and single_screen and document.body.scrollWidth > body_width and document.body.scrollWidth < 2 * body_width
|
||||
if is_single_page
|
||||
this.is_full_screen_layout = true
|
||||
# Prevent the TAB key from shifting focus as it causes partial
|
||||
# scrolling
|
||||
document.documentElement.addEventListener('keydown', (evt) ->
|
||||
if evt.keyCode == 9
|
||||
evt.preventDefault()
|
||||
)
|
||||
|
||||
|
||||
ncols = document.body.scrollWidth / this.page_width
|
||||
if ncols != Math.floor(ncols) and not this.is_full_screen_layout
|
||||
# In Qt 5 WebKit will sometimes adjust the individual column widths for
|
||||
# better text layout. This is allowed as per the CSS spec, so the
|
||||
# only way to ensure fixed column widths is to make sure the body
|
||||
# width is an exact multiple of the column widths
|
||||
bs.setProperty('width', Math.floor(ncols) * this.page_width - 2 * sm)
|
||||
|
||||
this.in_paged_mode = true
|
||||
this.current_margin_side = sm
|
||||
# log('Time to layout:', new Date().getTime() - start_time)
|
||||
return sm
|
||||
|
||||
create_header_footer: (uuid) ->
|
||||
if this.header_template != null
|
||||
this.header = document.createElement('div')
|
||||
this.header.setAttribute('style', "overflow:hidden; display:block; position:absolute; left:#{ this.side_margin }px; top: 0px; height: #{ this.effective_margin_top }px; width: #{ this.col_width }px; margin: 0; padding: 0")
|
||||
this.header.setAttribute('id', 'pdf_page_header_'+uuid)
|
||||
document.body.appendChild(this.header)
|
||||
if this.footer_template != null
|
||||
this.footer = document.createElement('div')
|
||||
this.footer.setAttribute('style', "overflow:hidden; display:block; position:absolute; left:#{ this.side_margin }px; top: #{ window.innerHeight - this.effective_margin_bottom }px; height: #{ this.effective_margin_bottom }px; width: #{ this.col_width }px; margin: 0; padding: 0")
|
||||
this.footer.setAttribute('id', 'pdf_page_footer_'+uuid)
|
||||
document.body.appendChild(this.footer)
|
||||
if this.header != null or this.footer != null
|
||||
this.hf_uuid = uuid
|
||||
this.hf_style = document.createElement('style')
|
||||
this.hf_style.setAttribute('type', 'text/css')
|
||||
document.head.appendChild(this.hf_style)
|
||||
this.update_header_footer(1)
|
||||
|
||||
position_header_footer: () ->
|
||||
[left, top] = calibre_utils.viewport_to_document(0, 0, document.body.ownerDocument)
|
||||
if this.header != null
|
||||
this.header.style.setProperty('left', left+'px')
|
||||
if this.footer != null
|
||||
this.footer.style.setProperty('left', left+'px')
|
||||
|
||||
update_header_footer: (pagenum) ->
|
||||
has_images = false
|
||||
this.header_footer_images = []
|
||||
if this.hf_style != null
|
||||
if pagenum%2 == 1 then cls = "even_page" else cls = "odd_page"
|
||||
this.hf_style.innerHTML = "#pdf_page_header_#{ this.hf_uuid } .#{ cls }, #pdf_page_footer_#{ this.hf_uuid } .#{ cls } { display: none }"
|
||||
title = py_bridge.title()
|
||||
author = py_bridge.author()
|
||||
section = py_bridge.section()
|
||||
tl_section = py_bridge.tl_section()
|
||||
if this.header != null
|
||||
this.header.innerHTML = this.header_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"").replace(/_TOP_LEVEL_SECTION_/g, tl_section+"").replace(/_SECTION_/g, section+"")
|
||||
runscripts(this.header)
|
||||
for img in this.header.getElementsByTagName('img')
|
||||
this.header_footer_images.push(img)
|
||||
has_images = true
|
||||
if this.footer != null
|
||||
this.footer.innerHTML = this.footer_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"").replace(/_TOP_LEVEL_SECTION_/g, tl_section+"").replace(/_SECTION_/g, section+"")
|
||||
runscripts(this.footer)
|
||||
for img in this.header.getElementsByTagName('img')
|
||||
this.header_footer_images.push(img)
|
||||
has_images = true
|
||||
has_images
|
||||
|
||||
header_footer_images_loaded: () ->
|
||||
for img in this.header_footer_images
|
||||
if not img.complete
|
||||
return false
|
||||
this.header_footer_images = []
|
||||
return true
|
||||
|
||||
fit_images: () ->
|
||||
# Ensure no images are wider than the available width in a column. Note
|
||||
# that this method use getBoundingClientRect() which means it will
|
||||
# force a relayout if the render tree is dirty.
|
||||
images = []
|
||||
vimages = []
|
||||
bounding_rects = []
|
||||
img_tags = document.getElementsByTagName('img')
|
||||
for img in img_tags
|
||||
bounding_rects.push(img.getBoundingClientRect())
|
||||
maxh = this.current_page_height
|
||||
for i in [0...img_tags.length]
|
||||
img = img_tags[i]
|
||||
previously_limited = calibre_utils.retrieve(img, 'width-limited', false)
|
||||
data = calibre_utils.retrieve(img, 'img-data', null)
|
||||
br = bounding_rects[i]
|
||||
if data == null
|
||||
data = {'left':br.left, 'right':br.right, 'height':br.height, 'display': img.style.display}
|
||||
calibre_utils.store(img, 'img-data', data)
|
||||
left = calibre_utils.viewport_to_document(br.left, 0, doc=img.ownerDocument)[0]
|
||||
col = this.column_at(left) * this.page_width
|
||||
rleft = left - col - this.current_margin_side
|
||||
width = br.right - br.left
|
||||
rright = rleft + width
|
||||
col_width = this.page_width - 2*this.current_margin_side
|
||||
if previously_limited or rright > col_width
|
||||
images.push([img, col_width - rleft])
|
||||
previously_limited = calibre_utils.retrieve(img, 'height-limited', false)
|
||||
if previously_limited or br.height > maxh
|
||||
vimages.push(img)
|
||||
if previously_limited
|
||||
img.style.setProperty('-webkit-column-break-before', 'auto')
|
||||
img.style.setProperty('display', data.display)
|
||||
img.style.setProperty('-webkit-column-break-inside', 'avoid')
|
||||
|
||||
for [img, max_width] in images
|
||||
img.style.setProperty('max-width', max_width+'px')
|
||||
calibre_utils.store(img, 'width-limited', true)
|
||||
|
||||
for img in vimages
|
||||
data = calibre_utils.retrieve(img, 'img-data', null)
|
||||
img.style.setProperty('-webkit-column-break-before', 'always')
|
||||
img.style.setProperty('max-height', maxh+'px')
|
||||
if data.height > maxh
|
||||
# This is needed to force the image onto a new page, without
|
||||
# it, the webkit algorithm may still decide to split the image
|
||||
# by keeping it part of its parent block
|
||||
img.style.setProperty('display', 'block')
|
||||
calibre_utils.store(img, 'height-limited', true)
|
||||
|
||||
scroll_to_pos: (frac) ->
|
||||
# Scroll to the position represented by frac (number between 0 and 1)
|
||||
xpos = Math.floor(document.body.scrollWidth * frac)
|
||||
this.scroll_to_xpos(xpos)
|
||||
|
||||
scroll_to_xpos: (xpos, animated=false, notify=false, duration=1000) ->
|
||||
# Scroll so that the column containing xpos is the left most column in
|
||||
# the viewport
|
||||
if typeof(xpos) != 'number'
|
||||
log(xpos, 'is not a number, cannot scroll to it!')
|
||||
return
|
||||
if this.is_full_screen_layout
|
||||
window.scrollTo(0, 0)
|
||||
return
|
||||
pos = Math.floor(xpos/this.page_width) * this.page_width
|
||||
limit = document.body.scrollWidth - this.screen_width
|
||||
pos = limit if pos > limit
|
||||
if animated
|
||||
this.animated_scroll(pos, duration=1000, notify=notify)
|
||||
else
|
||||
window.scrollTo(pos, 0)
|
||||
|
||||
scroll_to_column: (number) ->
|
||||
this.scroll_to_xpos(number * this.page_width + 10)
|
||||
|
||||
column_at: (xpos) ->
|
||||
# Return the number of the column that contains xpos
|
||||
return Math.floor(xpos/this.page_width)
|
||||
|
||||
column_location: (elem) ->
|
||||
# Return the location of elem relative to its containing column.
|
||||
# WARNING: This method may cause the viewport to scroll (to workaround
|
||||
# a bug in WebKit).
|
||||
br = elem.getBoundingClientRect()
|
||||
# Because of a bug in WebKit's getBoundingClientRect() in column
|
||||
# mode, this position can be inaccurate, see
|
||||
# https://bugs.launchpad.net/calibre/+bug/1202390 for a test case.
|
||||
# The usual symptom of the inaccuracy is br.top is highly negative.
|
||||
if br.top < -100
|
||||
# We have to actually scroll the element into view to get its
|
||||
# position
|
||||
elem.scrollIntoView()
|
||||
[left, top] = calibre_utils.viewport_to_document(elem.scrollLeft, elem.scrollTop, elem.ownerDocument)
|
||||
else
|
||||
[left, top] = calibre_utils.viewport_to_document(br.left, br.top, elem.ownerDocument)
|
||||
c = this.column_at(left)
|
||||
width = Math.min(br.right, (c+1)*this.page_width) - br.left
|
||||
if br.bottom < br.top
|
||||
br.bottom = window.innerHeight
|
||||
height = Math.min(br.bottom, window.innerHeight) - br.top
|
||||
left -= c*this.page_width
|
||||
return {'column':c, 'left':left, 'top':top, 'width':width, 'height':height}
|
||||
|
||||
column_boundaries: () ->
|
||||
# Return the column numbers at the left edge and after the right edge
|
||||
# of the viewport
|
||||
l = this.column_at(window.pageXOffset + 10)
|
||||
return [l, l + this.cols_per_screen]
|
||||
|
||||
animated_scroll: (pos, duration=1000, notify=true) ->
|
||||
# Scroll the window to X-position pos in an animated fashion over
|
||||
# duration milliseconds. If notify is true, py_bridge.animated_scroll_done is
|
||||
# called.
|
||||
delta = pos - window.pageXOffset
|
||||
interval = 50
|
||||
steps = Math.floor(duration/interval)
|
||||
step_size = Math.floor(delta/steps)
|
||||
this.current_scroll_animation = {target:pos, step_size:step_size, interval:interval, notify:notify, fn: () =>
|
||||
a = this.current_scroll_animation
|
||||
npos = window.pageXOffset + a.step_size
|
||||
completed = false
|
||||
if Math.abs(npos - a.target) < Math.abs(a.step_size)
|
||||
completed = true
|
||||
npos = a.target
|
||||
window.scrollTo(npos, 0)
|
||||
if completed
|
||||
if notify
|
||||
window.py_bridge.animated_scroll_done()
|
||||
else
|
||||
setTimeout(a.fn, a.interval)
|
||||
}
|
||||
this.current_scroll_animation.fn()
|
||||
|
||||
current_pos: (frac) ->
|
||||
# The current scroll position as a fraction between 0 and 1
|
||||
limit = document.body.scrollWidth - window.innerWidth
|
||||
if limit <= 0
|
||||
return 0.0
|
||||
return window.pageXOffset / limit
|
||||
|
||||
current_column_location: () ->
|
||||
# The location of the left edge of the left most column currently
|
||||
# visible in the viewport
|
||||
if this.is_full_screen_layout
|
||||
return 0
|
||||
x = window.pageXOffset + Math.max(10, this.current_margin_side)
|
||||
return Math.floor(x/this.page_width) * this.page_width
|
||||
|
||||
next_screen_location: () ->
|
||||
# The position to scroll to for the next screen (which could contain
|
||||
# more than one pages). Returns -1 if no further scrolling is possible.
|
||||
if this.is_full_screen_layout
|
||||
return -1
|
||||
cc = this.current_column_location()
|
||||
ans = cc + this.screen_width
|
||||
if this.cols_per_screen > 1
|
||||
width_left = document.body.scrollWidth - (window.pageXOffset + window.innerWidth)
|
||||
pages_left = width_left / this.page_width
|
||||
if Math.ceil(pages_left) < this.cols_per_screen
|
||||
return -1 # Only blank, dummy pages left
|
||||
limit = document.body.scrollWidth - window.innerWidth
|
||||
if ans > limit
|
||||
ans = if window.pageXOffset < limit then limit else -1
|
||||
return ans
|
||||
|
||||
previous_screen_location: () ->
|
||||
# The position to scroll to for the previous screen (which could contain
|
||||
# more than one pages). Returns -1 if no further scrolling is possible.
|
||||
if this.is_full_screen_layout
|
||||
return -1
|
||||
cc = this.current_column_location()
|
||||
ans = cc - this.screen_width
|
||||
if ans < 0
|
||||
# We ignore small scrolls (less than 15px) when going to previous
|
||||
# screen
|
||||
ans = if window.pageXOffset > 15 then 0 else -1
|
||||
return ans
|
||||
|
||||
next_col_location: () ->
|
||||
# The position to scroll to for the next column (same as
|
||||
# next_screen_location() if columns per screen == 1). Returns -1 if no
|
||||
# further scrolling is possible.
|
||||
if this.is_full_screen_layout
|
||||
return -1
|
||||
cc = this.current_column_location()
|
||||
ans = cc + this.page_width
|
||||
limit = document.body.scrollWidth - window.innerWidth
|
||||
if ans > limit
|
||||
ans = if window.pageXOffset < limit then limit else -1
|
||||
return ans
|
||||
|
||||
previous_col_location: () ->
|
||||
# The position to scroll to for the previous column (same as
|
||||
# previous_screen_location() if columns per screen == 1). Returns -1 if
|
||||
# no further scrolling is possible.
|
||||
if this.is_full_screen_layout
|
||||
return -1
|
||||
cc = this.current_column_location()
|
||||
ans = cc - this.page_width
|
||||
if ans < 0
|
||||
ans = if window.pageXOffset > 0 then 0 else -1
|
||||
return ans
|
||||
|
||||
jump_to_anchor: (name) ->
|
||||
# Jump to the element identified by anchor name. Ensures that the left
|
||||
# most column in the viewport is the column containing the start of the
|
||||
# element and that the scroll position is at the start of the column.
|
||||
elem = document.getElementById(name)
|
||||
if not elem
|
||||
elems = document.getElementsByName(name)
|
||||
if elems
|
||||
elem = elems[0]
|
||||
if not elem
|
||||
return
|
||||
if window.mathjax?.math_present
|
||||
# MathJax links to children of SVG tags and scrollIntoView doesn't
|
||||
# work properly for them, so if this link points to something
|
||||
# inside an <svg> tag we instead scroll the parent of the svg tag
|
||||
# into view.
|
||||
parent = elem
|
||||
while parent and parent?.tagName?.toLowerCase() != 'svg'
|
||||
parent = parent.parentNode
|
||||
if parent?.tagName?.toLowerCase() == 'svg'
|
||||
elem = parent.parentNode
|
||||
elem.scrollIntoView()
|
||||
if this.in_paged_mode
|
||||
# Ensure we are scrolled to the column containing elem
|
||||
|
||||
# Because of a bug in WebKit's getBoundingClientRect() in column
|
||||
# mode, this position can be inaccurate, see
|
||||
# https://bugs.launchpad.net/calibre/+bug/1132641 for a test case.
|
||||
# The usual symptom of the inaccuracy is br.top is highly negative.
|
||||
br = elem.getBoundingClientRect()
|
||||
if br.top < -100
|
||||
# This only works because of the preceding call to
|
||||
# elem.scrollIntoView(). However, in some cases it gives
|
||||
# inaccurate results, so we prefer the bounding client rect,
|
||||
# when possible.
|
||||
left = elem.scrollLeft
|
||||
else
|
||||
left = br.left
|
||||
this.scroll_to_xpos(calibre_utils.viewport_to_document(
|
||||
left+this.margin_side, elem.scrollTop, elem.ownerDocument)[0])
|
||||
|
||||
snap_to_selection: () ->
|
||||
# Ensure that the viewport is positioned at the start of the column
|
||||
# containing the start of the current selection
|
||||
if this.in_paged_mode
|
||||
sel = window.getSelection()
|
||||
r = sel.getRangeAt(0).getBoundingClientRect()
|
||||
node = sel.anchorNode
|
||||
left = calibre_utils.viewport_to_document(r.left, r.top, doc=node.ownerDocument)[0]
|
||||
|
||||
# Ensure we are scrolled to the column containing the start of the
|
||||
# selection
|
||||
this.scroll_to_xpos(left+5)
|
||||
|
||||
jump_to_cfi: (cfi, job_id=-1) ->
|
||||
# Jump to the position indicated by the specified conformal fragment
|
||||
# indicator (requires the cfi.coffee library). When in paged mode, the
|
||||
# scroll is performed so that the column containing the position
|
||||
# pointed to by the cfi is the left most column in the viewport
|
||||
window.cfi.scroll_to(cfi, (x, y) =>
|
||||
if this.in_paged_mode
|
||||
this.scroll_to_xpos(x)
|
||||
else
|
||||
window.scrollTo(0, y)
|
||||
if window.py_bridge
|
||||
window.py_bridge.jump_to_cfi_finished(job_id)
|
||||
)
|
||||
|
||||
current_cfi: () ->
|
||||
# The Conformal Fragment Identifier at the current position, returns
|
||||
# null if it could not be calculated. Requires the cfi.coffee library.
|
||||
ans = null
|
||||
if not window.cfi? or (window.mathjax?.math_present and not window.mathjax?.math_loaded)
|
||||
# If MathJax is loading, it is changing the DOM, so we cannot
|
||||
# reliably generate a CFI
|
||||
return ans
|
||||
if this.in_paged_mode
|
||||
c = this.current_column_location()
|
||||
for x in [c, c-this.page_width, c+this.page_width]
|
||||
# Try the current column, the previous column and the next
|
||||
# column. Each column is tried from top to bottom.
|
||||
[left, right] = [x, x + this.page_width]
|
||||
if left < 0 or right > document.body.scrollWidth
|
||||
continue
|
||||
deltax = Math.floor(this.page_width/25)
|
||||
deltay = Math.floor(window.innerHeight/25)
|
||||
cury = this.effective_margin_top
|
||||
until cury >= (window.innerHeight - this.effective_margin_bottom)
|
||||
curx = left + this.current_margin_side
|
||||
until curx >= (right - this.current_margin_side)
|
||||
cfi = window.cfi.at_point(curx-window.pageXOffset, cury-window.pageYOffset)
|
||||
if cfi
|
||||
log('Viewport cfi:', cfi)
|
||||
return cfi
|
||||
curx += deltax
|
||||
cury += deltay
|
||||
else
|
||||
try
|
||||
ans = window.cfi.at_current()
|
||||
if not ans
|
||||
ans = null
|
||||
catch err
|
||||
log(err)
|
||||
if ans
|
||||
log('Viewport cfi:', ans)
|
||||
return ans
|
||||
|
||||
click_for_page_turn: (event) ->
|
||||
# Check if the click event should generate a page turn. Returns
|
||||
# null if it should not, true if it is a backwards page turn, false if
|
||||
# it is a forward page turn.
|
||||
left_boundary = this.current_margin_side
|
||||
right_bondary = this.screen_width - this.current_margin_side
|
||||
if left_boundary > event.clientX
|
||||
return true
|
||||
if right_bondary < event.clientX
|
||||
return false
|
||||
return null
|
||||
|
||||
if window?
|
||||
window.paged_display = new PagedDisplay()
|
||||
|
||||
# TODO:
|
||||
# Highlight on jump_to_anchor
|
Binary file not shown.
@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2011, Kovid Goyal <kovid@kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
###
|
||||
|
||||
log = (error) ->
|
||||
if error
|
||||
if window?.console?.log
|
||||
window.console.log(error)
|
||||
else if process?.stdout?.write
|
||||
process.stdout.write(error + '\n')
|
||||
|
||||
show_cfi = () ->
|
||||
if window.current_cfi
|
||||
fn = (x, y) ->
|
||||
ms = document.getElementById("marker").style
|
||||
ms.display = 'block'
|
||||
ms.top = y - 30 + 'px'
|
||||
ms.left = x - 1 + 'px'
|
||||
|
||||
window.cfi.scroll_to(window.current_cfi, fn)
|
||||
null
|
||||
|
||||
window_ypos = (pos=null) ->
|
||||
if pos == null
|
||||
return window.pageYOffset
|
||||
window.scrollTo(0, pos)
|
||||
|
||||
mark_and_reload = (evt) ->
|
||||
x = evt.clientX
|
||||
y = evt.clientY
|
||||
if evt.button == 2
|
||||
return # Right mouse click, generated only in firefox
|
||||
|
||||
if document.elementFromPoint(x, y)?.getAttribute('id') in ['reset', 'viewport_mode']
|
||||
return
|
||||
|
||||
# Remove image in case the click was on the image itself, we want the cfi to
|
||||
# be on the underlying element
|
||||
ms = document.getElementById("marker")
|
||||
ms.style.display = 'none'
|
||||
|
||||
if document.getElementById('viewport_mode').checked
|
||||
cfi = window.cfi.at_current()
|
||||
window.cfi.scroll_to(cfi)
|
||||
return
|
||||
|
||||
fn = () ->
|
||||
try
|
||||
window.current_cfi = window.cfi.at(x, y)
|
||||
catch err
|
||||
alert("Failed to calculate cfi: #{ err }")
|
||||
return
|
||||
if window.current_cfi
|
||||
epubcfi = "epubcfi(#{ window.current_cfi })"
|
||||
ypos = window_ypos()
|
||||
newloc = window.location.href.replace(/#.*$/, '') + "#" + ypos + epubcfi
|
||||
window.location.replace(newloc)
|
||||
window.location.reload()
|
||||
|
||||
setTimeout(fn, 1)
|
||||
null
|
||||
|
||||
frame_clicked = (evt) ->
|
||||
iframe = evt.target.ownerDocument.defaultView.frameElement
|
||||
# We know that the offset parent of the iframe is body
|
||||
# So we can easily calculate the event co-ords w.r.t. the browser window
|
||||
rect = iframe.getBoundingClientRect()
|
||||
x = evt.clientX + rect.left
|
||||
y = evt.clientY + rect.top
|
||||
mark_and_reload({'clientX':x, 'clientY':y, 'button':evt.button})
|
||||
|
||||
window.onload = ->
|
||||
try
|
||||
window.cfi.is_compatible()
|
||||
catch error
|
||||
alert(error)
|
||||
return
|
||||
document.onclick = mark_and_reload
|
||||
for iframe in document.getElementsByTagName("iframe")
|
||||
iframe.contentWindow.document.onclick = frame_clicked
|
||||
|
||||
r = location.hash.match(/#(\d*)epubcfi\((.+)\)$/)
|
||||
if r
|
||||
window.current_cfi = r[2]
|
||||
ypos = if r[1] then 0+r[1] else 0
|
||||
base = document.getElementById('first-h1').innerHTML
|
||||
document.title = base + ": " + window.current_cfi
|
||||
fn = () ->
|
||||
show_cfi()
|
||||
window_ypos(ypos)
|
||||
setTimeout(fn, 100)
|
||||
null
|
||||
|
@ -1,74 +0,0 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
from __python__ import hash_literals
|
||||
|
||||
from cfi import scroll_to, at_current, at
|
||||
|
||||
def show_cfi():
|
||||
if window.current_cfi:
|
||||
scroll_to(window.current_cfi, def(x, y):
|
||||
ms = document.getElementById("marker").style
|
||||
ms.display = 'block'
|
||||
ms.top = y - 30 + 'px'
|
||||
ms.left = x - 1 + 'px'
|
||||
)
|
||||
|
||||
def mark_and_reload(evt):
|
||||
x = evt.clientX
|
||||
y = evt.clientY
|
||||
if evt.button is 2:
|
||||
return # Right mouse click, generated only in firefox
|
||||
|
||||
elem = document.elementFromPoint(x, y)
|
||||
if elem and elem.getAttribute('id') in ['reset', 'viewport_mode']:
|
||||
return
|
||||
|
||||
# Remove image in case the click was on the image itself, we want the cfi to
|
||||
# be on the underlying element
|
||||
ms = document.getElementById("marker")
|
||||
ms.style.display = 'none'
|
||||
|
||||
if document.getElementById('viewport_mode').checked:
|
||||
cfi = at_current()
|
||||
scroll_to(cfi)
|
||||
return
|
||||
|
||||
def fn():
|
||||
try:
|
||||
window.current_cfi = at(x, y)
|
||||
except Exception as err:
|
||||
alert(str.format("Failed to calculate cfi: {}", err.message))
|
||||
return
|
||||
if window.current_cfi:
|
||||
epubcfi = 'epubcfi(' + window.current_cfi + ')'
|
||||
ypos = window.pageYOffset
|
||||
newloc = window.location.href.replace(/#.*$/, '') + "#" + ypos + epubcfi
|
||||
window.location.replace(newloc)
|
||||
window.location.reload()
|
||||
|
||||
setTimeout(fn, 1)
|
||||
|
||||
def frame_clicked(evt):
|
||||
iframe = evt.target.ownerDocument.defaultView.frameElement
|
||||
# We know that the offset parent of the iframe is body
|
||||
# So we can easily calculate the event co-ords w.r.t. the browser window
|
||||
rect = iframe.getBoundingClientRect()
|
||||
x = evt.clientX + rect.left
|
||||
y = evt.clientY + rect.top
|
||||
mark_and_reload({'clientX':x, 'clientY':y, 'button':evt.button})
|
||||
|
||||
window.onload = def():
|
||||
document.onclick = mark_and_reload
|
||||
for iframe in document.getElementsByTagName("iframe"):
|
||||
iframe.contentWindow.document.onclick = frame_clicked
|
||||
|
||||
r = window.location.hash.match(/#(\d*)epubcfi\((.+)\)$/)
|
||||
if r:
|
||||
window.current_cfi = r[2]
|
||||
ypos = 0+r[1] if r[1] else 0
|
||||
base = document.getElementById('first-h1').innerHTML
|
||||
document.title = base + ": " + window.current_cfi
|
||||
setTimeout(def():
|
||||
show_cfi()
|
||||
window.scrollTo(0, ypos)
|
||||
, 100)
|
@ -1,32 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Testing EPUB CFI</title>
|
||||
</head>
|
||||
<body>
|
||||
<p id="whitespace">But I must explain to you how all this mistaken
|
||||
idea of denouncing pleasure and praising pain was born and I will
|
||||
give you a complete account of the system, and expound the actual
|
||||
teachings of the great explorer of the truth, the master-builder of
|
||||
human happiness. No one rejects, dislikes, or avoids pleasure
|
||||
itself, because it is pleasure, but because those who do not know
|
||||
how to pursue pleasure rationally encounter consequences that are
|
||||
extremely painful. Nor again is there anyone who <b>loves or
|
||||
pursues or desires</b> to obtain pain of itself, because it is
|
||||
pain, but because occasionally circumstances occur in which toil
|
||||
and pain can procure him some great pleasure. To take a trivial
|
||||
example, which of us ever undertakes laborious physical exercise,
|
||||
except to obtain some advantage from it? But who has any right to
|
||||
find fault with a man who chooses to enjoy a pleasure that has no
|
||||
annoying consequences, or one who avoids a pain that produces no
|
||||
resultant pleasure? On the other hand, we denounce with righteous
|
||||
indignation and dislike men who are so beguiled and demoralized by
|
||||
the charms of pleasure of the moment, so blinded by desire, that
|
||||
they cannot foresee</p>
|
||||
<p><img src="marker.png" width="300" height="300" alt="Test image"/></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -1,150 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing cfi.coffee</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<script type="text/javascript" src="cfi-test.js"></script>
|
||||
<script type="text/javascript" src="cfi.coffee"></script>
|
||||
<script type="text/javascript" src="cfi-test.coffee"></script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background-color: white;
|
||||
padding-bottom: 500px;
|
||||
}
|
||||
|
||||
h1, h2 { color: #005a9c }
|
||||
|
||||
h2 {
|
||||
border-top: solid 2px #005a9c;
|
||||
margin-top: 4ex;
|
||||
}
|
||||
|
||||
#container {
|
||||
max-width: 30em;
|
||||
margin-right: auto;
|
||||
margin-left: 2em;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
#overflow {
|
||||
max-height: 100px;
|
||||
overflow: scroll;
|
||||
border: solid 1px black;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#whitespace {
|
||||
border: 20px solid gray;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#reset {
|
||||
color: blue;
|
||||
text-decoration: none
|
||||
}
|
||||
#reset:hover { color: red }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<h1 id="first-h1">Testing cfi.coffee</h1>
|
||||
<p>Click anywhere and the location will be marked with a marker, whose position is set via a CFI.</p>
|
||||
<p>
|
||||
<a id="reset" href="/">Reset CFI to None</a>
|
||||
|
||||
Test viewport location calculation:
|
||||
<input type="checkbox" id="viewport_mode" title=
|
||||
"Checking this will cause the window to scroll to a position based on a CFI calculated for the windows current position."/>
|
||||
</p>
|
||||
<h2>A div with scrollbars</h2>
|
||||
<p>Scroll down and click on some elements. Make sure to hit both
|
||||
bold and not bold text as well as different points on the image</p>
|
||||
<div id="overflow">But I must explain to you how all this mistaken
|
||||
idea of denouncing pleasure and praising pain was born and I
|
||||
will give you a complete account of the system, and expound the
|
||||
actual teachings of the great explorer of the truth, the
|
||||
master-builder of human happiness. No one rejects, dislikes, or
|
||||
avoids pleasure itself, because it is pleasure, but because
|
||||
those who do not know how to pursue pleasure rationally
|
||||
encounter consequences that are extremely painful. Nor again is
|
||||
there anyone who <b>loves or pursues or desires</b> to obtain
|
||||
pain of itself, because it is pain, but because occasionally
|
||||
circumstances occur in which toil and pain can procure him some
|
||||
great pleasure. To take a trivial example, which of us ever
|
||||
undertakes laborious physical exercise, except to obtain some
|
||||
advantage from it? But who has any right to find fault with a
|
||||
man who chooses to enjoy a pleasure that has no annoying
|
||||
consequences, or one who avoids a pain that produces no
|
||||
resultant pleasure? On the other hand, we denounce with
|
||||
righteous indignation and dislike men who are so beguiled and
|
||||
demoralized by the charms of pleasure of the moment, so blinded
|
||||
by desire, that they cannot foresee
|
||||
<img src="marker.png" width="150" height="200" alt="Test Image"
|
||||
style="border: solid 1px black; display:block"/>
|
||||
|
||||
</div>
|
||||
<h2>Some entities and comments</h2>
|
||||
<p>Entities: & © § > some text after entities</p>
|
||||
<p>An invisible Comment: <!-- aaaaaa --> followed by some text</p>
|
||||
<p>An invalid (in HTML) CDATA: <![CDATA[CDATA]]> followed by some text</p>
|
||||
<h2>Margins padding borders</h2>
|
||||
<p>Try clicking in the margins, borders and padding. CFI
|
||||
calculation should fail.</p>
|
||||
|
||||
<p id="whitespace">But I must explain to you how all this mistaken
|
||||
idea of denouncing pleasure and praising pain was born and I will
|
||||
give you a complete account of the system, and expound the actual
|
||||
teachings of the great explorer of the truth, the master-builder of
|
||||
human happiness. No one rejects, dislikes, or avoids pleasure
|
||||
itself, because it is pleasure, but because those who do not know
|
||||
how to pursue pleasure rationally encounter consequences that are
|
||||
extremely painful. Nor again is there anyone who <b>loves or
|
||||
pursues or desires</b> to obtain pain of itself, because it is
|
||||
pain, but because occasionally circumstances occur in which toil
|
||||
and pain can procure him some great pleasure. To take a trivial
|
||||
example, which of us ever undertakes laborious physical exercise,
|
||||
except to obtain some advantage from it? But who has any right to
|
||||
find fault with a man who chooses to enjoy a pleasure that has no
|
||||
annoying consequences, or one who avoids a pain that produces no
|
||||
resultant pleasure? On the other hand, we denounce with righteous
|
||||
indignation and dislike men who are so beguiled and demoralized by
|
||||
the charms of pleasure of the moment, so blinded by desire, that
|
||||
they cannot foresee</p>
|
||||
|
||||
<h2>Lots of collapsed whitespace</h2>
|
||||
<p>Try clicking the A character after the colon:
|
||||
A suffix</p>
|
||||
|
||||
<h2>Lots of nested/sibling tags</h2>
|
||||
<p>A <span>bunch of <span>nested<span> and</span> <span>sibling</span>
|
||||
tags, all </span> mixed together</span>. <span>Click all</span>
|
||||
over this paragraph to test<span> things.</span></p>
|
||||
|
||||
<h2>Images</h2>
|
||||
<p>Try clicking at different points along the image. Also try
|
||||
changing the magnification and then hitting reload.</p>
|
||||
<img src="marker.png" width="150" height="200" alt="Test Image"
|
||||
style="border: solid 1px black"/>
|
||||
|
||||
<h2>Iframes</h2>
|
||||
<p>Try clicking anywhere in the iframe below:</p>
|
||||
<iframe src="iframe.html"></iframe>
|
||||
|
||||
<h2>Video</h2>
|
||||
<p>Try clicking on this video while it is playing. The page should
|
||||
reload with the video paused at the point it was at when you
|
||||
clicked. To play the video you should right click on it and select
|
||||
play (otherwise the click will cause a reload).
|
||||
</p>
|
||||
<video width="320" height="240" controls="controls" preload="auto"
|
||||
src="birds.webm" type="video/webm"></video>
|
||||
|
||||
</div>
|
||||
<img id="marker" style="position: absolute; display:none; z-index:10"
|
||||
src="marker.png" alt="Marker" />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 751 B |
@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
from calibre.utils.serve_coffee import serve
|
||||
except ImportError:
|
||||
import init_calibre
|
||||
if False:
|
||||
init_calibre, serve
|
||||
from calibre.utils.serve_coffee import serve
|
||||
|
||||
|
||||
def run_devel_server():
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
serve(resources={'cfi.coffee':'../cfi.coffee', '/':'index.html'})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_devel_server()
|
@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, shutil, tempfile
|
||||
|
||||
from polyglot import socketserver
|
||||
from polyglot.http_server import SimpleHTTPRequestHandler
|
||||
|
||||
|
||||
def run_devel_server():
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
tdir = tempfile.gettempdir()
|
||||
dest = os.path.join(tdir, os.path.basename(base))
|
||||
if os.path.exists(dest):
|
||||
shutil.rmtree(dest)
|
||||
shutil.copytree(base, dest)
|
||||
for i in range(5):
|
||||
base = os.path.dirname(base)
|
||||
shutil.copy(os.path.join(base, 'pyj', 'read_book', 'cfi.pyj'), dest)
|
||||
os.chdir(dest)
|
||||
from calibre.utils.rapydscript import compile_pyj
|
||||
with lopen('cfi-test.pyj', 'rb') as f, lopen('cfi-test.js', 'wb') as js:
|
||||
js.write(compile_pyj(f.read()).encode('utf-8'))
|
||||
PORT = 8000
|
||||
Handler = SimpleHTTPRequestHandler
|
||||
httpd = socketserver.TCPServer(("", PORT), Handler)
|
||||
print('Serving CFI test at http://localhost:%d' % PORT)
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_devel_server()
|
@ -1,151 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2012, Kovid Goyal <kovid@kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
###
|
||||
|
||||
class CalibreUtils
|
||||
# This class is a namespace to expose functions via the
|
||||
# window.calibre_utils object.
|
||||
|
||||
constructor: () ->
|
||||
if not this instanceof arguments.callee
|
||||
throw new Error('CalibreUtils constructor called as function')
|
||||
this.dom_attr = 'calibre_f3fa75ca98eb4413a4ee413f20f60226'
|
||||
this.dom_data = []
|
||||
|
||||
# Data API {{{
|
||||
|
||||
retrieve: (node, key, def=null) ->
|
||||
# Retrieve data previously stored on node (a DOM node) with key (a
|
||||
# string). If no such data is found then return the value of def.
|
||||
idx = parseInt(node.getAttribute(this.dom_attr))
|
||||
if isNaN(idx)
|
||||
return def
|
||||
data = this.dom_data[idx]
|
||||
if not data.hasOwnProperty(key)
|
||||
return def
|
||||
return data[key]
|
||||
|
||||
store: (node, key, val) ->
|
||||
# Store arbitrary javscript object val on DOM node node with key (a
|
||||
# string). This can be later retrieved by the retrieve method.
|
||||
idx = parseInt(node.getAttribute(this.dom_attr))
|
||||
if isNaN(idx)
|
||||
idx = this.dom_data.length
|
||||
node.setAttribute(this.dom_attr, idx+'')
|
||||
this.dom_data.push({})
|
||||
this.dom_data[idx][key] = val
|
||||
# }}}
|
||||
|
||||
log: (args...) -> # {{{
|
||||
# Output args to the window.console object. args are automatically
|
||||
# coerced to strings
|
||||
if args
|
||||
msg = args.join(' ')
|
||||
if window?.console?.log
|
||||
window.console.log(msg)
|
||||
else if process?.stdout?.write
|
||||
process.stdout.write(msg + '\n')
|
||||
# }}}
|
||||
|
||||
stack_trace: () -> # {{{
|
||||
currentFunction = arguments.callee.caller
|
||||
while (currentFunction)
|
||||
fn = currentFunction.toString()
|
||||
this.log(fn)
|
||||
currentFunction = currentFunction.caller
|
||||
|
||||
# }}}
|
||||
|
||||
window_scroll_pos: (win=window) -> # {{{
|
||||
# The current scroll position of the browser window
|
||||
if typeof(win.pageXOffset) == 'number'
|
||||
x = win.pageXOffset
|
||||
y = win.pageYOffset
|
||||
else # IE < 9
|
||||
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
|
||||
x = document.body.scrollLeft
|
||||
y = document.body.scrollTop
|
||||
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
|
||||
y = document.documentElement.scrollTop
|
||||
x = document.documentElement.scrollLeft
|
||||
return [x, y]
|
||||
# }}}
|
||||
|
||||
viewport_to_document: (x, y, doc=window?.document) -> # {{{
|
||||
# Convert x, y from the viewport (window) co-ordinate system to the
|
||||
# document (body) co-ordinate system
|
||||
until doc == window.document
|
||||
# We are in a frame
|
||||
frame = doc.defaultView.frameElement
|
||||
rect = frame.getBoundingClientRect()
|
||||
x += rect.left
|
||||
y += rect.top
|
||||
doc = frame.ownerDocument
|
||||
win = doc.defaultView
|
||||
[wx, wy] = this.window_scroll_pos(win)
|
||||
x += wx
|
||||
y += wy
|
||||
return [x, y]
|
||||
# }}}
|
||||
|
||||
absleft: (elem) -> # {{{
|
||||
# The left edge of elem in document co-ords. Works in all
|
||||
# circumstances, including column layout. Note that this will cause
|
||||
# a relayout if the render tree is dirty. Also, because of a bug in the
|
||||
# version of WebKit bundled with Qt 4.8, this does not always work, see
|
||||
# https://bugs.launchpad.net/bugs/1132641 for a test case.
|
||||
r = elem.getBoundingClientRect()
|
||||
return this.viewport_to_document(r.left, 0, elem.ownerDocument)[0]
|
||||
# }}}
|
||||
|
||||
abstop: (elem) -> # {{{
|
||||
# The left edge of elem in document co-ords. Works in all
|
||||
# circumstances, including column layout. Note that this will cause
|
||||
# a relayout if the render tree is dirty. Also, because of a bug in the
|
||||
# version of WebKit bundled with Qt 4.8, this does not always work, see
|
||||
# https://bugs.launchpad.net/bugs/1132641 for a test case.
|
||||
r = elem.getBoundingClientRect()
|
||||
return this.viewport_to_document(r.top, 0, elem.ownerDocument)[0]
|
||||
# }}}
|
||||
|
||||
word_at_point: (x, y) -> # {{{
|
||||
# Return the word at the specified point (in viewport co-ordinates)
|
||||
range = if document.caretPositionFromPoint then document.caretPositionFromPoint(x, y) else document.caretRangeFromPoint(x, y)
|
||||
if range == null
|
||||
return null
|
||||
node = range.startContainer
|
||||
if node?.nodeType != Node.TEXT_NODE
|
||||
return null
|
||||
offset = range.startOffset
|
||||
range = document.createRange()
|
||||
range.selectNodeContents(node)
|
||||
try
|
||||
range.setStart(node, offset)
|
||||
range.setEnd(node, offset+1)
|
||||
catch error # Happens if offset is invalid
|
||||
null
|
||||
range.expand('word')
|
||||
ans = range.toString().trim()
|
||||
range.detach()
|
||||
matches = ans.split(/\b/)
|
||||
return if matches.length > 0 then matches[0] else null
|
||||
|
||||
# }}}
|
||||
|
||||
setup_epub_reading_system: (name, version, layout, features) -> # {{{
|
||||
window.navigator.epubReadingSystem = {
|
||||
'name':name, 'version':version, 'layoutStyle':layout,
|
||||
'hasFeature': (feature, version=1.0) ->
|
||||
if (version == null or version == 1.0) and feature.toLowerCase() in features
|
||||
return true
|
||||
return false
|
||||
}
|
||||
# }}}
|
||||
|
||||
if window?
|
||||
window.calibre_utils = new CalibreUtils()
|
||||
|
@ -1346,23 +1346,6 @@ def event_type_name(ev_or_etype):
|
||||
return 'UnknownEventType'
|
||||
|
||||
|
||||
def secure_web_page(qwebpage_or_qwebsettings):
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
settings = qwebpage_or_qwebsettings if isinstance(qwebpage_or_qwebsettings, QWebSettings) else qwebpage_or_qwebsettings.settings()
|
||||
settings.setAttribute(QWebSettings.JavaEnabled, False)
|
||||
settings.setAttribute(QWebSettings.PluginsEnabled, False)
|
||||
settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False)
|
||||
settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, False)
|
||||
settings.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False) # ensure javascript cannot read from local files
|
||||
settings.setAttribute(QWebSettings.NotificationsEnabled, False)
|
||||
settings.setThirdPartyCookiePolicy(QWebSettings.AlwaysBlockThirdPartyCookies)
|
||||
settings.setAttribute(QWebSettings.OfflineStorageDatabaseEnabled, False)
|
||||
settings.setAttribute(QWebSettings.LocalStorageEnabled, False)
|
||||
QWebSettings.setOfflineStorageDefaultQuota(0)
|
||||
QWebSettings.setOfflineStoragePath(None)
|
||||
return settings
|
||||
|
||||
|
||||
empty_model = QStringListModel([''])
|
||||
empty_index = empty_model.index(0)
|
||||
|
||||
|
@ -90,4 +90,4 @@ Highlighter = create_highlighter('JavascriptHighlighter', JavascriptLexer)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from calibre.gui2.tweak_book.editor.widget import launch_editor
|
||||
launch_editor(P('viewer/images.js'), syntax='javascript')
|
||||
launch_editor(P('viewer.js'), syntax='javascript')
|
||||
|
@ -1,235 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import json
|
||||
|
||||
from PyQt5.Qt import (
|
||||
Qt, QListWidget, QListWidgetItem, QItemSelectionModel, QAction,
|
||||
QGridLayout, QPushButton, QIcon, QWidget, pyqtSignal, QLabel)
|
||||
|
||||
from calibre.gui2 import choose_save_file, choose_files
|
||||
from calibre.utils.icu import sort_key
|
||||
from polyglot.builtins import unicode_type, range
|
||||
|
||||
|
||||
class BookmarksList(QListWidget):
|
||||
|
||||
changed = pyqtSignal()
|
||||
bookmark_activated = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QListWidget.__init__(self, parent)
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropMode(self.InternalMove)
|
||||
self.setDefaultDropAction(Qt.MoveAction)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setStyleSheet('QListView::item { padding: 0.5ex }')
|
||||
self.viewport().setAcceptDrops(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
self.ac_edit = ac = QAction(QIcon(I('edit_input.png')), _('Edit this bookmark'), self)
|
||||
self.addAction(ac)
|
||||
self.ac_delete = ac = QAction(QIcon(I('trash.png')), _('Remove this bookmark'), self)
|
||||
self.addAction(ac)
|
||||
self.ac_sort = ac = QAction(_('Sort by name'), self)
|
||||
self.addAction(ac)
|
||||
self.ac_sort_pos = ac = QAction(_('Sort by position in book'), self)
|
||||
self.addAction(ac)
|
||||
|
||||
def dropEvent(self, ev):
|
||||
QListWidget.dropEvent(self, ev)
|
||||
if ev.isAccepted():
|
||||
self.changed.emit()
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() in (Qt.Key_Enter, Qt.Key_Return):
|
||||
i = self.currentItem()
|
||||
if i is not None:
|
||||
self.bookmark_activated.emit(i)
|
||||
ev.accept()
|
||||
return
|
||||
if ev.key() in (Qt.Key_Delete, Qt.Key_Backspace):
|
||||
i = self.currentItem()
|
||||
if i is not None:
|
||||
self.ac_delete.trigger()
|
||||
ev.accept()
|
||||
return
|
||||
return QListWidget.keyPressEvent(self, ev)
|
||||
|
||||
|
||||
class BookmarkManager(QWidget):
|
||||
|
||||
edited = pyqtSignal(object)
|
||||
activated = pyqtSignal(object)
|
||||
create_requested = pyqtSignal()
|
||||
|
||||
def __init__(self, parent):
|
||||
QWidget.__init__(self, parent)
|
||||
self.l = l = QGridLayout(self)
|
||||
l.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(l)
|
||||
|
||||
self.bookmarks_list = bl = BookmarksList(self)
|
||||
bl.itemChanged.connect(self.item_changed)
|
||||
l.addWidget(bl, 0, 0, 1, -1)
|
||||
bl.itemClicked.connect(self.item_activated)
|
||||
bl.bookmark_activated.connect(self.item_activated)
|
||||
bl.changed.connect(lambda : self.edited.emit(self.get_bookmarks()))
|
||||
bl.ac_edit.triggered.connect(self.edit_bookmark)
|
||||
bl.ac_sort.triggered.connect(self.sort_by_name)
|
||||
bl.ac_sort_pos.triggered.connect(self.sort_by_pos)
|
||||
bl.ac_delete.triggered.connect(self.delete_bookmark)
|
||||
|
||||
self.la = la = QLabel(_(
|
||||
'Double click to edit and drag-and-drop to re-order the bookmarks'))
|
||||
la.setWordWrap(True)
|
||||
l.addWidget(la, l.rowCount(), 0, 1, -1)
|
||||
|
||||
self.button_new = b = QPushButton(QIcon(I('bookmarks.png')), _('&New'), self)
|
||||
b.clicked.connect(self.create_requested)
|
||||
b.setToolTip(_('Create a new bookmark at the current location'))
|
||||
l.addWidget(b)
|
||||
|
||||
self.button_delete = b = QPushButton(QIcon(I('trash.png')), _('&Remove'), self)
|
||||
b.setToolTip(_('Remove the currently selected bookmark'))
|
||||
b.clicked.connect(self.delete_bookmark)
|
||||
l.addWidget(b, l.rowCount() - 1, 1)
|
||||
|
||||
self.button_delete = b = QPushButton(_('Sort by &name'), self)
|
||||
b.setToolTip(_('Sort bookmarks by name'))
|
||||
b.clicked.connect(self.sort_by_name)
|
||||
l.addWidget(b)
|
||||
|
||||
self.button_delete = b = QPushButton(_('Sort by &position'), self)
|
||||
b.setToolTip(_('Sort bookmarks by position in book'))
|
||||
b.clicked.connect(self.sort_by_pos)
|
||||
l.addWidget(b, l.rowCount() - 1, 1)
|
||||
|
||||
self.button_export = b = QPushButton(QIcon(I('back.png')), _('E&xport'), self)
|
||||
b.clicked.connect(self.export_bookmarks)
|
||||
l.addWidget(b)
|
||||
|
||||
self.button_import = b = QPushButton(QIcon(I('forward.png')), _('&Import'), self)
|
||||
b.clicked.connect(self.import_bookmarks)
|
||||
l.addWidget(b, l.rowCount() - 1, 1)
|
||||
|
||||
def item_activated(self, item):
|
||||
bm = self.item_to_bm(item)
|
||||
self.activated.emit(bm)
|
||||
|
||||
def set_bookmarks(self, bookmarks=()):
|
||||
self.bookmarks_list.clear()
|
||||
for bm in bookmarks:
|
||||
if bm['title'] != 'calibre_current_page_bookmark':
|
||||
i = QListWidgetItem(bm['title'])
|
||||
i.setData(Qt.UserRole, self.bm_to_item(bm))
|
||||
i.setFlags(i.flags() | Qt.ItemIsEditable)
|
||||
self.bookmarks_list.addItem(i)
|
||||
if self.bookmarks_list.count() > 0:
|
||||
self.bookmarks_list.setCurrentItem(self.bookmarks_list.item(0), QItemSelectionModel.ClearAndSelect)
|
||||
|
||||
def set_current_bookmark(self, bm):
|
||||
for i, q in enumerate(self):
|
||||
if bm == q:
|
||||
l = self.bookmarks_list
|
||||
item = l.item(i)
|
||||
l.setCurrentItem(item, QItemSelectionModel.ClearAndSelect)
|
||||
l.scrollToItem(item)
|
||||
|
||||
def __iter__(self):
|
||||
for i in range(self.bookmarks_list.count()):
|
||||
yield self.item_to_bm(self.bookmarks_list.item(i))
|
||||
|
||||
def item_changed(self, item):
|
||||
self.bookmarks_list.blockSignals(True)
|
||||
title = unicode_type(item.data(Qt.DisplayRole))
|
||||
if not title:
|
||||
title = _('Unknown')
|
||||
item.setData(Qt.DisplayRole, title)
|
||||
bm = self.item_to_bm(item)
|
||||
bm['title'] = title
|
||||
item.setData(Qt.UserRole, self.bm_to_item(bm))
|
||||
self.bookmarks_list.blockSignals(False)
|
||||
self.edited.emit(self.get_bookmarks())
|
||||
|
||||
def delete_bookmark(self):
|
||||
row = self.bookmarks_list.currentRow()
|
||||
if row > -1:
|
||||
self.bookmarks_list.takeItem(row)
|
||||
self.edited.emit(self.get_bookmarks())
|
||||
|
||||
def edit_bookmark(self):
|
||||
item = self.bookmarks_list.currentItem()
|
||||
if item is not None:
|
||||
self.bookmarks_list.editItem(item)
|
||||
|
||||
def sort_by_name(self):
|
||||
bm = self.get_bookmarks()
|
||||
bm.sort(key=lambda x:sort_key(x['title']))
|
||||
self.set_bookmarks(bm)
|
||||
self.edited.emit(bm)
|
||||
|
||||
def sort_by_pos(self):
|
||||
from calibre.ebooks.epub.cfi.parse import cfi_sort_key
|
||||
|
||||
def pos_key(b):
|
||||
if b.get('type', None) == 'cfi':
|
||||
return b['spine'], cfi_sort_key(b['pos'])
|
||||
return (None, None)
|
||||
bm = self.get_bookmarks()
|
||||
bm.sort(key=pos_key)
|
||||
self.set_bookmarks(bm)
|
||||
self.edited.emit(bm)
|
||||
|
||||
def bm_to_item(self, bm):
|
||||
return bm.copy()
|
||||
|
||||
def item_to_bm(self, item):
|
||||
return item.data(Qt.UserRole).copy()
|
||||
|
||||
def get_bookmarks(self):
|
||||
return list(self)
|
||||
|
||||
def export_bookmarks(self):
|
||||
filename = choose_save_file(
|
||||
self, 'export-viewer-bookmarks', _('Export bookmarks'),
|
||||
filters=[(_('Saved bookmarks'), ['calibre-bookmarks'])], all_files=False, initial_filename='bookmarks.calibre-bookmarks')
|
||||
if filename:
|
||||
data = json.dumps(self.get_bookmarks(), indent=True)
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
with lopen(filename, 'wb') as fileobj:
|
||||
fileobj.write(data)
|
||||
|
||||
def import_bookmarks(self):
|
||||
files = choose_files(self, 'export-viewer-bookmarks', _('Import bookmarks'),
|
||||
filters=[(_('Saved bookmarks'), ['calibre-bookmarks'])], all_files=False, select_only_single_file=True)
|
||||
if not files:
|
||||
return
|
||||
filename = files[0]
|
||||
|
||||
imported = None
|
||||
with lopen(filename, 'rb') as fileobj:
|
||||
imported = json.load(fileobj)
|
||||
|
||||
if imported is not None:
|
||||
bad = False
|
||||
try:
|
||||
for bm in imported:
|
||||
if 'title' not in bm:
|
||||
bad = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not bad:
|
||||
bookmarks = self.get_bookmarks()
|
||||
for bm in imported:
|
||||
if bm not in bookmarks:
|
||||
bookmarks.append(bm)
|
||||
self.set_bookmarks([bm for bm in bookmarks if bm['title'] != 'calibre_current_page_bookmark'])
|
||||
self.edited.emit(self.get_bookmarks())
|
@ -1,456 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import zipfile
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QFont, QDialog, Qt, QColor, QColorDialog, QMenu, QInputDialog,
|
||||
QListWidgetItem, QFormLayout, QLabel, QLineEdit, QDialogButtonBox)
|
||||
|
||||
from calibre.constants import isxp
|
||||
from calibre.utils.config import Config, StringConfig, JSONConfig
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.localization import get_language, calibre_langcode_to_name
|
||||
from calibre.gui2 import min_available_height, error_dialog
|
||||
from calibre.gui2.languages import LanguagesEdit
|
||||
from calibre.gui2.shortcuts import ShortcutConfig
|
||||
from calibre.gui2.viewer.config_ui import Ui_Dialog
|
||||
from polyglot.builtins import iteritems, unicode_type
|
||||
|
||||
|
||||
def config(defaults=None):
|
||||
desc = _('Options to customize the e-book viewer')
|
||||
if defaults is None:
|
||||
c = Config('viewer', desc)
|
||||
else:
|
||||
c = StringConfig(defaults, desc)
|
||||
|
||||
c.add_opt('remember_window_size', default=False,
|
||||
help=_('Remember last used window size'))
|
||||
c.add_opt('user_css', default='',
|
||||
help=_('Set the user CSS stylesheet. This can be used to customize the look of all books.'))
|
||||
c.add_opt('max_fs_width', default=800,
|
||||
help=_("Set the maximum width that the book's text and pictures will take"
|
||||
" when in fullscreen mode. This allows you to read the book text"
|
||||
" without it becoming too wide."))
|
||||
c.add_opt('max_fs_height', default=-1,
|
||||
help=_("Set the maximum height that the book's text and pictures will take"
|
||||
" when in fullscreen mode. This allows you to read the book text"
|
||||
" without it becoming too tall. Note that this setting only takes effect in paged mode (which is the default mode)."))
|
||||
c.add_opt('fit_images', default=True,
|
||||
help=_('Resize images larger than the viewer window to fit inside it'))
|
||||
c.add_opt('hyphenate', default=False, help=_('Hyphenate text'))
|
||||
c.add_opt('hyphenate_default_lang', default='en',
|
||||
help=_('Default language for hyphenation rules'))
|
||||
c.add_opt('search_online_url', default='https://www.google.com/search?q={text}',
|
||||
help=_('The URL to use when searching for selected text online'))
|
||||
c.add_opt('remember_current_page', default=True,
|
||||
help=_('Save the current position in the document, when quitting'))
|
||||
c.add_opt('copy_bookmarks_to_file', default=True,
|
||||
help=_('Copy bookmarks to the e-book file for easy sharing, if possible'))
|
||||
c.add_opt('wheel_flips_pages', default=False,
|
||||
help=_('Have the mouse wheel turn pages'))
|
||||
c.add_opt('wheel_scroll_fraction', default=100,
|
||||
help=_('Control how much the mouse wheel scrolls by in flow mode'))
|
||||
c.add_opt('line_scroll_fraction', default=100,
|
||||
help=_('Control how much the arrow keys scroll by in flow mode'))
|
||||
c.add_opt('tap_flips_pages', default=True,
|
||||
help=_('Tapping on the screen turns pages'))
|
||||
c.add_opt('line_scrolling_stops_on_pagebreaks', default=False,
|
||||
help=_('Prevent the up and down arrow keys from scrolling past '
|
||||
'page breaks'))
|
||||
c.add_opt('page_flip_duration', default=0.5,
|
||||
help=_('The time, in seconds, for the page flip animation. Default'
|
||||
' is half a second.'))
|
||||
c.add_opt('font_magnification_step', default=0.2,
|
||||
help=_('The amount by which to change the font size when clicking'
|
||||
' the font larger/smaller buttons. Should be a number between '
|
||||
'0 and 1.'))
|
||||
c.add_opt('fullscreen_clock', default=False, action='store_true',
|
||||
help=_('Show a clock in fullscreen mode.'))
|
||||
c.add_opt('fullscreen_pos', default=False, action='store_true',
|
||||
help=_('Show reading position in fullscreen mode.'))
|
||||
c.add_opt('fullscreen_scrollbar', default=True, action='store_false',
|
||||
help=_('Show the scrollbar in fullscreen mode.'))
|
||||
c.add_opt('start_in_fullscreen', default=False, action='store_true',
|
||||
help=_('Start viewer in full screen mode'))
|
||||
c.add_opt('show_fullscreen_help', default=True, action='store_false',
|
||||
help=_('Show full screen usage help'))
|
||||
c.add_opt('cols_per_screen', default=1)
|
||||
c.add_opt('cols_per_screen_portrait', default=1)
|
||||
c.add_opt('cols_per_screen_landscape', default=1)
|
||||
c.add_opt('cols_per_screen_migrated', default=False, action='store_true')
|
||||
c.add_opt('use_book_margins', default=False, action='store_true')
|
||||
c.add_opt('top_margin', default=20)
|
||||
c.add_opt('side_margin', default=40)
|
||||
c.add_opt('bottom_margin', default=20)
|
||||
c.add_opt('text_color', default=None)
|
||||
c.add_opt('background_color', default=None)
|
||||
c.add_opt('show_controls', default=True)
|
||||
|
||||
fonts = c.add_group('FONTS', _('Font options'))
|
||||
fonts('serif_family', default='Liberation Serif',
|
||||
help=_('The serif font family'))
|
||||
fonts('sans_family', default='Liberation Sans',
|
||||
help=_('The sans-serif font family'))
|
||||
fonts('mono_family', default='Liberation Mono',
|
||||
help=_('The monospace font family'))
|
||||
fonts('default_font_size', default=20, help=_('The standard font size in px'))
|
||||
fonts('mono_font_size', default=16, help=_('The monospace font size in px'))
|
||||
fonts('standard_font', default='serif', help=_('The standard font type'))
|
||||
fonts('minimum_font_size', default=8, help=_('The minimum font size in px'))
|
||||
|
||||
oparse = c.parse
|
||||
|
||||
def parse():
|
||||
ans = oparse()
|
||||
if not ans.cols_per_screen_migrated:
|
||||
ans.cols_per_screen_portrait = ans.cols_per_screen_landscape = ans.cols_per_screen
|
||||
return ans
|
||||
c.parse = parse
|
||||
|
||||
return c
|
||||
|
||||
|
||||
def load_themes():
|
||||
return JSONConfig('viewer_themes')
|
||||
|
||||
|
||||
class ConfigDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, shortcuts, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
for x in ('text', 'background'):
|
||||
getattr(self, 'change_%s_color_button'%x).clicked.connect(
|
||||
partial(self.change_color, x, reset=False))
|
||||
getattr(self, 'reset_%s_color_button'%x).clicked.connect(
|
||||
partial(self.change_color, x, reset=True))
|
||||
self.css.setToolTip(_('Set the user CSS stylesheet. This can be used to customize the look of all books.'))
|
||||
|
||||
self.shortcuts = shortcuts
|
||||
self.shortcut_config = ShortcutConfig(shortcuts, parent=self)
|
||||
bb = self.buttonBox
|
||||
bb.button(bb.RestoreDefaults).clicked.connect(self.restore_defaults)
|
||||
|
||||
with zipfile.ZipFile(P('viewer/hyphenate/patterns.zip',
|
||||
allow_user_override=False), 'r') as zf:
|
||||
pats = [x.split('.')[0].replace('-', '_') for x in zf.namelist()]
|
||||
|
||||
lang_pats = {
|
||||
'el_monoton': get_language('el').partition(';')[0] + _(' monotone'), 'el_polyton':get_language('el').partition(';')[0] + _(' polytone'),
|
||||
'sr_cyrl': get_language('sr') + _(' cyrillic'), 'sr_latn': get_language('sr') + _(' latin'),
|
||||
}
|
||||
|
||||
def gl(pat):
|
||||
return lang_pats.get(pat, get_language(pat))
|
||||
names = list(map(gl, pats))
|
||||
pmap = {}
|
||||
for i in range(len(pats)):
|
||||
pmap[names[i]] = pats[i]
|
||||
for x in sorted(names):
|
||||
self.hyphenate_default_lang.addItem(x, pmap[x])
|
||||
self.hyphenate_pats = pats
|
||||
self.hyphenate_names = names
|
||||
p = self.tabs.widget(1)
|
||||
p.layout().addWidget(self.shortcut_config)
|
||||
|
||||
if isxp:
|
||||
self.hyphenate.setVisible(False)
|
||||
self.hyphenate_default_lang.setVisible(False)
|
||||
self.hyphenate_label.setVisible(False)
|
||||
|
||||
self.themes = load_themes()
|
||||
self.save_theme_button.clicked.connect(self.save_theme)
|
||||
self.load_theme_button.m = m = QMenu()
|
||||
self.load_theme_button.setMenu(m)
|
||||
m.triggered.connect(self.load_theme)
|
||||
self.delete_theme_button.m = m = QMenu()
|
||||
self.delete_theme_button.setMenu(m)
|
||||
m.triggered.connect(self.delete_theme)
|
||||
|
||||
opts = config().parse()
|
||||
self.load_options(opts)
|
||||
self.init_load_themes()
|
||||
self.init_dictionaries()
|
||||
|
||||
self.clear_search_history_button.clicked.connect(self.clear_search_history)
|
||||
self.resize(self.width(), min(self.height(), max(575, min_available_height()-25)))
|
||||
|
||||
for x in 'add remove change'.split():
|
||||
getattr(self, x + '_dictionary_website_button').clicked.connect(getattr(self, x + '_dictionary_website'))
|
||||
|
||||
def clear_search_history(self):
|
||||
from calibre.gui2 import config
|
||||
config['viewer_search_history'] = []
|
||||
config['viewer_toc_search_history'] = []
|
||||
|
||||
def save_theme(self):
|
||||
themename, ok = QInputDialog.getText(self, _('Theme name'),
|
||||
_('Choose a name for this theme'))
|
||||
if not ok:
|
||||
return
|
||||
themename = unicode_type(themename).strip()
|
||||
if not themename:
|
||||
return
|
||||
c = config('')
|
||||
c.add_opt('theme_name_xxx', default=themename)
|
||||
self.save_options(c)
|
||||
self.themes['theme_'+themename] = c.src
|
||||
self.init_load_themes()
|
||||
self.theming_message.setText(_('Saved settings as the theme named: %s')%
|
||||
themename)
|
||||
|
||||
def init_load_themes(self):
|
||||
for x in ('load', 'delete'):
|
||||
m = getattr(self, '%s_theme_button'%x).menu()
|
||||
m.clear()
|
||||
for x in self.themes:
|
||||
title = x[len('theme_'):]
|
||||
ac = m.addAction(title)
|
||||
ac.theme_id = x
|
||||
|
||||
def load_theme(self, ac):
|
||||
theme = ac.theme_id
|
||||
raw = self.themes[theme]
|
||||
self.load_options(config(raw).parse())
|
||||
self.theming_message.setText(_('Loaded settings from the theme %s')%
|
||||
theme[len('theme_'):])
|
||||
|
||||
def delete_theme(self, ac):
|
||||
theme = ac.theme_id
|
||||
del self.themes[theme]
|
||||
self.init_load_themes()
|
||||
self.theming_message.setText(_('Deleted the theme named: %s')%
|
||||
theme[len('theme_'):])
|
||||
|
||||
def init_dictionaries(self):
|
||||
from calibre.gui2.viewer.main import dprefs
|
||||
self.word_lookups = dprefs['word_lookups']
|
||||
|
||||
@property
|
||||
def word_lookups(self):
|
||||
return dict(self.dictionary_list.item(i).data(Qt.UserRole) for i in range(self.dictionary_list.count()))
|
||||
|
||||
@word_lookups.setter
|
||||
def word_lookups(self, wl):
|
||||
self.dictionary_list.clear()
|
||||
for langcode, url in sorted(iteritems(wl), key=lambda lc_url:sort_key(calibre_langcode_to_name(lc_url[0]))):
|
||||
i = QListWidgetItem('%s: %s' % (calibre_langcode_to_name(langcode), url), self.dictionary_list)
|
||||
i.setData(Qt.UserRole, (langcode, url))
|
||||
|
||||
def add_dictionary_website(self):
|
||||
class AD(QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setWindowTitle(_('Add a dictionary website'))
|
||||
self.l = l = QFormLayout(self)
|
||||
self.la = la = QLabel('<p>'+
|
||||
_('Choose a language and enter the website address (URL) for it below.'
|
||||
' The URL must have the placeholder <b>%s</b> in it, which will be replaced by the actual word being'
|
||||
' looked up') % '{word}')
|
||||
la.setWordWrap(True)
|
||||
l.addRow(la)
|
||||
self.le = LanguagesEdit(self)
|
||||
l.addRow(_('&Language:'), self.le)
|
||||
self.url = u = QLineEdit(self)
|
||||
u.setMinimumWidth(350)
|
||||
u.setPlaceholderText(_('For example: %s') % 'https://dictionary.com/{word}')
|
||||
l.addRow(_('&URL:'), u)
|
||||
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
|
||||
l.addRow(bb)
|
||||
bb.accepted.connect(self.accept), bb.rejected.connect(self.reject)
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def accept(self):
|
||||
if '{word}' not in self.url.text():
|
||||
return error_dialog(self, _('Invalid URL'), _(
|
||||
'The URL {0} does not have the placeholder <b>{1}</b> in it.').format(self.url.text(), '{word}'), show=True)
|
||||
QDialog.accept(self)
|
||||
|
||||
d = AD(self)
|
||||
if d.exec_() == d.Accepted:
|
||||
url = d.url.text()
|
||||
if url:
|
||||
wl = self.word_lookups
|
||||
for lc in d.le.lang_codes:
|
||||
wl[lc] = url
|
||||
self.word_lookups = wl
|
||||
|
||||
def remove_dictionary_website(self):
|
||||
idx = self.dictionary_list.currentIndex()
|
||||
if idx.isValid():
|
||||
lc, url = idx.data(Qt.UserRole)
|
||||
wl = self.word_lookups
|
||||
wl.pop(lc, None)
|
||||
self.word_lookups = wl
|
||||
|
||||
def change_dictionary_website(self):
|
||||
idx = self.dictionary_list.currentIndex()
|
||||
if idx.isValid():
|
||||
lc, url = idx.data(Qt.UserRole)
|
||||
url, ok = QInputDialog.getText(self, _('Enter new website'), 'URL:', text=url)
|
||||
if ok:
|
||||
wl = self.word_lookups
|
||||
wl[lc] = url
|
||||
self.word_lookups = wl
|
||||
|
||||
def restore_defaults(self):
|
||||
opts = config('').parse()
|
||||
self.load_options(opts)
|
||||
from calibre.gui2.viewer.main import dprefs, vprefs
|
||||
self.word_lookups = dprefs.defaults['word_lookups']
|
||||
self.opt_singleinstance.setChecked(vprefs.defaults['singleinstance'])
|
||||
|
||||
def load_options(self, opts):
|
||||
self.opt_remember_window_size.setChecked(opts.remember_window_size)
|
||||
self.opt_remember_current_page.setChecked(opts.remember_current_page)
|
||||
self.opt_copy_bookmarks_to_file.setChecked(opts.copy_bookmarks_to_file)
|
||||
self.opt_wheel_flips_pages.setChecked(opts.wheel_flips_pages)
|
||||
self.opt_wheel_scroll_fraction.setValue(opts.wheel_scroll_fraction)
|
||||
self.opt_line_scroll_fraction.setValue(opts.line_scroll_fraction)
|
||||
self.opt_tap_flips_pages.setChecked(opts.tap_flips_pages)
|
||||
self.opt_page_flip_duration.setValue(opts.page_flip_duration)
|
||||
fms = opts.font_magnification_step
|
||||
if fms < 0.01 or fms > 1:
|
||||
fms = 0.2
|
||||
self.opt_font_mag_step.setValue(int(fms*100))
|
||||
self.opt_line_scrolling_stops_on_pagebreaks.setChecked(
|
||||
opts.line_scrolling_stops_on_pagebreaks)
|
||||
self.serif_family.setCurrentFont(QFont(opts.serif_family))
|
||||
self.sans_family.setCurrentFont(QFont(opts.sans_family))
|
||||
self.mono_family.setCurrentFont(QFont(opts.mono_family))
|
||||
self.default_font_size.setValue(opts.default_font_size)
|
||||
self.minimum_font_size.setValue(opts.minimum_font_size)
|
||||
self.mono_font_size.setValue(opts.mono_font_size)
|
||||
self.standard_font.setCurrentIndex(
|
||||
{'serif':0, 'sans':1, 'mono':2}[opts.standard_font])
|
||||
self.css.setPlainText(opts.user_css)
|
||||
self.max_fs_width.setValue(opts.max_fs_width)
|
||||
self.max_fs_height.setValue(opts.max_fs_height)
|
||||
pats, names = self.hyphenate_pats, self.hyphenate_names
|
||||
try:
|
||||
idx = pats.index(opts.hyphenate_default_lang)
|
||||
except ValueError:
|
||||
idx = pats.index('en_us')
|
||||
idx = self.hyphenate_default_lang.findText(names[idx])
|
||||
self.hyphenate_default_lang.setCurrentIndex(idx)
|
||||
self.hyphenate.setChecked(opts.hyphenate)
|
||||
self.hyphenate_default_lang.setEnabled(opts.hyphenate)
|
||||
self.search_online_url.setText(opts.search_online_url or '')
|
||||
self.opt_fit_images.setChecked(opts.fit_images)
|
||||
self.opt_fullscreen_clock.setChecked(opts.fullscreen_clock)
|
||||
self.opt_fullscreen_scrollbar.setChecked(opts.fullscreen_scrollbar)
|
||||
self.opt_start_in_fullscreen.setChecked(opts.start_in_fullscreen)
|
||||
self.opt_show_fullscreen_help.setChecked(opts.show_fullscreen_help)
|
||||
self.opt_fullscreen_pos.setChecked(opts.fullscreen_pos)
|
||||
self.opt_cols_per_screen_portrait.setValue(opts.cols_per_screen_portrait)
|
||||
self.opt_cols_per_screen_landscape.setValue(opts.cols_per_screen_landscape)
|
||||
self.opt_override_book_margins.setChecked(not opts.use_book_margins)
|
||||
for x in ('top', 'bottom', 'side'):
|
||||
getattr(self, 'opt_%s_margin'%x).setValue(getattr(opts,
|
||||
x+'_margin'))
|
||||
for x in ('text', 'background'):
|
||||
setattr(self, 'current_%s_color'%x, getattr(opts, '%s_color'%x))
|
||||
self.update_sample_colors()
|
||||
self.opt_show_controls.setChecked(opts.show_controls)
|
||||
from calibre.gui2.viewer.main import vprefs
|
||||
self.opt_singleinstance.setChecked(bool(vprefs['singleinstance']))
|
||||
|
||||
def change_color(self, which, reset=False):
|
||||
if reset:
|
||||
setattr(self, 'current_%s_color'%which, None)
|
||||
else:
|
||||
initial = getattr(self, 'current_%s_color'%which)
|
||||
if initial:
|
||||
initial = QColor(initial)
|
||||
else:
|
||||
initial = Qt.black if which == 'text' else Qt.white
|
||||
title = (_('Choose text color') if which == 'text' else
|
||||
_('Choose background color'))
|
||||
col = QColorDialog.getColor(initial, self,
|
||||
title, QColorDialog.ShowAlphaChannel)
|
||||
if col.isValid():
|
||||
name = unicode_type(col.name())
|
||||
setattr(self, 'current_%s_color'%which, name)
|
||||
self.update_sample_colors()
|
||||
|
||||
def update_sample_colors(self):
|
||||
for x in ('text', 'background'):
|
||||
val = getattr(self, 'current_%s_color'%x)
|
||||
if not val:
|
||||
val = 'inherit' if x == 'text' else 'transparent'
|
||||
ss = 'QLabel { %s: %s }'%('background-color' if x == 'background'
|
||||
else 'color', val)
|
||||
getattr(self, '%s_color_sample'%x).setStyleSheet(ss)
|
||||
|
||||
def accept(self, *args):
|
||||
if self.shortcut_config.is_editing:
|
||||
from calibre.gui2 import info_dialog
|
||||
info_dialog(self, _('Still editing'),
|
||||
_('You are in the middle of editing a keyboard shortcut.'
|
||||
' First complete that by clicking outside the'
|
||||
' shortcut editing box.'), show=True)
|
||||
return
|
||||
self.save_options(config())
|
||||
return QDialog.accept(self, *args)
|
||||
|
||||
def save_options(self, c):
|
||||
c.set('serif_family', unicode_type(self.serif_family.currentFont().family()))
|
||||
c.set('sans_family', unicode_type(self.sans_family.currentFont().family()))
|
||||
c.set('mono_family', unicode_type(self.mono_family.currentFont().family()))
|
||||
c.set('default_font_size', self.default_font_size.value())
|
||||
c.set('minimum_font_size', self.minimum_font_size.value())
|
||||
c.set('mono_font_size', self.mono_font_size.value())
|
||||
c.set('standard_font', {0:'serif', 1:'sans', 2:'mono'}[
|
||||
self.standard_font.currentIndex()])
|
||||
c.set('user_css', unicode_type(self.css.toPlainText()))
|
||||
c.set('remember_window_size', self.opt_remember_window_size.isChecked())
|
||||
c.set('fit_images', self.opt_fit_images.isChecked())
|
||||
c.set('max_fs_width', int(self.max_fs_width.value()))
|
||||
max_fs_height = self.max_fs_height.value()
|
||||
if max_fs_height <= self.max_fs_height.minimum():
|
||||
max_fs_height = -1
|
||||
c.set('max_fs_height', max_fs_height)
|
||||
c.set('hyphenate', self.hyphenate.isChecked())
|
||||
c.set('remember_current_page', self.opt_remember_current_page.isChecked())
|
||||
c.set('copy_bookmarks_to_file', self.opt_copy_bookmarks_to_file.isChecked())
|
||||
c.set('wheel_flips_pages', self.opt_wheel_flips_pages.isChecked())
|
||||
c.set('wheel_scroll_fraction', self.opt_wheel_scroll_fraction.value())
|
||||
c.set('line_scroll_fraction', self.opt_line_scroll_fraction.value())
|
||||
c.set('tap_flips_pages', self.opt_tap_flips_pages.isChecked())
|
||||
c.set('page_flip_duration', self.opt_page_flip_duration.value())
|
||||
c.set('font_magnification_step',
|
||||
float(self.opt_font_mag_step.value())/100.)
|
||||
idx = self.hyphenate_default_lang.currentIndex()
|
||||
c.set('hyphenate_default_lang',
|
||||
self.hyphenate_default_lang.itemData(idx))
|
||||
c.set('line_scrolling_stops_on_pagebreaks',
|
||||
self.opt_line_scrolling_stops_on_pagebreaks.isChecked())
|
||||
c.set('search_online_url', self.search_online_url.text().strip())
|
||||
c.set('fullscreen_clock', self.opt_fullscreen_clock.isChecked())
|
||||
c.set('fullscreen_pos', self.opt_fullscreen_pos.isChecked())
|
||||
c.set('fullscreen_scrollbar', self.opt_fullscreen_scrollbar.isChecked())
|
||||
c.set('show_fullscreen_help', self.opt_show_fullscreen_help.isChecked())
|
||||
c.set('cols_per_screen_migrated', True)
|
||||
c.set('cols_per_screen_portrait', int(self.opt_cols_per_screen_portrait.value()))
|
||||
c.set('cols_per_screen_landscape', int(self.opt_cols_per_screen_landscape.value()))
|
||||
c.set('start_in_fullscreen', self.opt_start_in_fullscreen.isChecked())
|
||||
c.set('use_book_margins', not
|
||||
self.opt_override_book_margins.isChecked())
|
||||
c.set('text_color', self.current_text_color)
|
||||
c.set('background_color', self.current_background_color)
|
||||
c.set('show_controls', self.opt_show_controls.isChecked())
|
||||
for x in ('top', 'bottom', 'side'):
|
||||
c.set(x+'_margin', int(getattr(self, 'opt_%s_margin'%x).value()))
|
||||
from calibre.gui2.viewer.main import dprefs, vprefs
|
||||
dprefs['word_lookups'] = self.word_lookups
|
||||
vprefs['singleinstance'] = self.opt_singleinstance.isChecked()
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,187 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from PyQt5.Qt import QNetworkReply, QNetworkAccessManager, QUrl, QNetworkRequest, QTimer, pyqtSignal, QByteArray
|
||||
|
||||
from calibre import guess_type as _guess_type, prints
|
||||
from calibre.constants import FAKE_HOST, FAKE_PROTOCOL, DEBUG
|
||||
from calibre.ebooks.oeb.base import OEB_DOCS
|
||||
from calibre.ebooks.oeb.display.webview import cleanup_html, load_as_html
|
||||
from calibre.utils.short_uuid import uuid4
|
||||
from polyglot.builtins import unicode_type
|
||||
|
||||
|
||||
def guess_type(x):
|
||||
return _guess_type(x)[0] or 'application/octet-stream'
|
||||
|
||||
|
||||
cc_header = QByteArray(b'Cache-Control'), QByteArray(b'max-age=864001')
|
||||
|
||||
|
||||
class NetworkReply(QNetworkReply):
|
||||
|
||||
def __init__(self, parent, request, mime_type, data):
|
||||
QNetworkReply.__init__(self, parent)
|
||||
self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered)
|
||||
self.setRequest(request)
|
||||
self.setUrl(request.url())
|
||||
self._aborted = False
|
||||
self.__data = data
|
||||
self.setHeader(QNetworkRequest.ContentTypeHeader, mime_type)
|
||||
self.setHeader(QNetworkRequest.ContentLengthHeader, len(self.__data))
|
||||
self.setRawHeader(*cc_header)
|
||||
QTimer.singleShot(0, self.finalize_reply)
|
||||
|
||||
def bytesAvailable(self):
|
||||
return len(self.__data)
|
||||
|
||||
def isSequential(self):
|
||||
return True
|
||||
|
||||
def abort(self):
|
||||
pass
|
||||
|
||||
def readData(self, maxlen):
|
||||
if maxlen >= len(self.__data):
|
||||
ans, self.__data = self.__data, b''
|
||||
return ans
|
||||
ans, self.__data = self.__data[:maxlen], self.__data[maxlen:]
|
||||
return ans
|
||||
read = readData
|
||||
|
||||
def finalize_reply(self):
|
||||
self.setFinished(True)
|
||||
self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
|
||||
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok")
|
||||
self.metaDataChanged.emit()
|
||||
self.downloadProgress.emit(len(self.__data), len(self.__data))
|
||||
self.readyRead.emit()
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class NotFound(QNetworkReply):
|
||||
|
||||
def __init__(self, parent, request):
|
||||
QNetworkReply.__init__(self, parent)
|
||||
self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered)
|
||||
self.setHeader(QNetworkRequest.ContentTypeHeader, 'application/octet-stream')
|
||||
self.setHeader(QNetworkRequest.ContentLengthHeader, 0)
|
||||
self.setRequest(request)
|
||||
self.setUrl(request.url())
|
||||
QTimer.singleShot(0, self.finalize_reply)
|
||||
|
||||
def bytesAvailable(self):
|
||||
return 0
|
||||
|
||||
def isSequential(self):
|
||||
return True
|
||||
|
||||
def abort(self):
|
||||
pass
|
||||
|
||||
def readData(self, maxlen):
|
||||
return b''
|
||||
|
||||
def finalize_reply(self):
|
||||
self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 404)
|
||||
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Not Found")
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
def normpath(p):
|
||||
return os.path.normcase(os.path.abspath(p))
|
||||
|
||||
|
||||
class NetworkAccessManager(QNetworkAccessManager):
|
||||
|
||||
load_error = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QNetworkAccessManager.__init__(self, parent)
|
||||
self.mathjax_prefix = unicode_type(uuid4())
|
||||
self.mathjax_base = '%s://%s/%s/' % (FAKE_PROTOCOL, FAKE_HOST, self.mathjax_prefix)
|
||||
self.root = self.orig_root = os.path.dirname(P('viewer/blank.html', allow_user_override=False))
|
||||
self.mime_map, self.single_pages, self.codec_map = {}, set(), {}
|
||||
self.mathjax_dir = P('mathjax', allow_user_override=False)
|
||||
|
||||
def set_book_data(self, root, spine):
|
||||
self.orig_root = root
|
||||
self.root = normpath(root)
|
||||
self.mime_map, self.single_pages, self.codec_map = {}, set(), {}
|
||||
for p in spine:
|
||||
mt = getattr(p, 'mime_type', None)
|
||||
key = normpath(p)
|
||||
if mt is not None:
|
||||
self.mime_map[key] = mt
|
||||
self.codec_map[key] = getattr(p, 'encoding', 'utf-8')
|
||||
if getattr(p, 'is_single_page', False):
|
||||
self.single_pages.add(key)
|
||||
|
||||
def is_single_page(self, path):
|
||||
if not path:
|
||||
return False
|
||||
key = normpath(path)
|
||||
return key in self.single_pages
|
||||
|
||||
def as_abspath(self, qurl):
|
||||
name = qurl.path()[1:]
|
||||
return os.path.join(self.orig_root, *name.split('/'))
|
||||
|
||||
def as_url(self, abspath):
|
||||
name = os.path.relpath(abspath, self.root).replace('\\', '/')
|
||||
ans = QUrl()
|
||||
ans.setScheme(FAKE_PROTOCOL), ans.setAuthority(FAKE_HOST), ans.setPath('/' + name)
|
||||
return ans
|
||||
|
||||
def guess_type(self, name):
|
||||
mime_type = guess_type(name)
|
||||
mime_type = {
|
||||
# Prevent warning in console about mimetype of fonts
|
||||
'application/vnd.ms-opentype':'application/x-font-ttf',
|
||||
'application/x-font-truetype':'application/x-font-ttf',
|
||||
'application/x-font-opentype':'application/x-font-ttf',
|
||||
'application/x-font-otf':'application/x-font-ttf',
|
||||
'application/font-sfnt': 'application/x-font-ttf',
|
||||
}.get(mime_type, mime_type)
|
||||
return mime_type
|
||||
|
||||
def preprocess_data(self, data, path):
|
||||
mt = self.mime_map.get(path, self.guess_type(path))
|
||||
if mt.lower() in OEB_DOCS:
|
||||
enc = self.codec_map.get(path, 'utf-8')
|
||||
html = data.decode(enc, 'replace')
|
||||
html = cleanup_html(html)
|
||||
data = html.encode('utf-8')
|
||||
if load_as_html(html):
|
||||
mt = 'text/html; charset=utf-8'
|
||||
else:
|
||||
mt = 'application/xhtml+xml; charset=utf-8'
|
||||
return data, mt
|
||||
|
||||
def createRequest(self, operation, request, data):
|
||||
qurl = request.url()
|
||||
if operation == QNetworkAccessManager.GetOperation and qurl.host() == FAKE_HOST:
|
||||
name = qurl.path()[1:]
|
||||
if name.startswith(self.mathjax_prefix):
|
||||
base = normpath(self.mathjax_dir)
|
||||
path = normpath(os.path.join(base, name.partition('/')[2]))
|
||||
else:
|
||||
base = self.root
|
||||
path = normpath(os.path.join(self.root, name))
|
||||
if path.startswith(base) and os.path.exists(path):
|
||||
try:
|
||||
with lopen(path, 'rb') as f:
|
||||
data = f.read()
|
||||
data, mime_type = self.preprocess_data(data, path)
|
||||
return NetworkReply(self, request, mime_type, data)
|
||||
except Exception:
|
||||
import traceback
|
||||
self.load_error.emit(name, traceback.format_exc())
|
||||
if DEBUG:
|
||||
prints('URL not found in book: %r' % qurl.toString())
|
||||
return NotFound(self, request)
|
||||
return QNetworkAccessManager.createRequest(self, operation, request)
|
@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt5.Qt import QWidget, QPainter, QPropertyAnimation, QEasingCurve, \
|
||||
QRect, QPixmap, Qt, pyqtProperty
|
||||
|
||||
|
||||
class SlideFlip(QWidget):
|
||||
|
||||
# API {{{
|
||||
|
||||
# In addition the isVisible() and setVisible() methods must be present
|
||||
|
||||
def __init__(self, parent):
|
||||
QWidget.__init__(self, parent)
|
||||
|
||||
self.setGeometry(0, 0, 1, 1)
|
||||
self._current_width = 0
|
||||
self.before_image = self.after_image = None
|
||||
self.animation = QPropertyAnimation(self, b'current_width', self)
|
||||
self.setVisible(False)
|
||||
self.animation.valueChanged.connect(self.update)
|
||||
self.animation.finished.connect(self.finished)
|
||||
self.flip_forwards = True
|
||||
self.setAttribute(Qt.WA_OpaquePaintEvent)
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
'True iff animation is currently running'
|
||||
return self.animation.state() == self.animation.Running
|
||||
|
||||
def initialize(self, image, forwards=True):
|
||||
'''
|
||||
Initialize the flipper, causes the flipper to show itself displaying
|
||||
the full `image`.
|
||||
|
||||
:param image: The image to display as background
|
||||
:param forwards: If True flipper will flip forwards, otherwise
|
||||
backwards
|
||||
|
||||
'''
|
||||
self.flip_forwards = forwards
|
||||
self.before_image = QPixmap.fromImage(image)
|
||||
self.after_image = None
|
||||
self.setGeometry(0, 0, image.width(), image.height())
|
||||
self.setVisible(True)
|
||||
|
||||
def __call__(self, image, duration=0.5):
|
||||
'''
|
||||
Start the animation. You must have called :meth:`initialize` first.
|
||||
|
||||
:param duration: Animation duration in seconds.
|
||||
|
||||
'''
|
||||
if self.running:
|
||||
return
|
||||
self.after_image = QPixmap.fromImage(image)
|
||||
|
||||
if self.flip_forwards:
|
||||
self.animation.setStartValue(image.width())
|
||||
self.animation.setEndValue(0)
|
||||
t = self.before_image
|
||||
self.before_image = self.after_image
|
||||
self.after_image = t
|
||||
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.InExpo))
|
||||
else:
|
||||
self.animation.setStartValue(0)
|
||||
self.animation.setEndValue(image.width())
|
||||
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
|
||||
|
||||
self.animation.setDuration(duration * 1000)
|
||||
self.animation.start()
|
||||
|
||||
# }}}
|
||||
|
||||
def finished(self):
|
||||
self.setVisible(False)
|
||||
self.before_image = self.after_image = None
|
||||
|
||||
def paintEvent(self, ev):
|
||||
if self.before_image is None:
|
||||
return
|
||||
canvas_size = self.rect()
|
||||
p = QPainter(self)
|
||||
p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
|
||||
|
||||
p.drawPixmap(canvas_size, self.before_image,
|
||||
self.before_image.rect())
|
||||
if self.after_image is not None:
|
||||
width = self._current_width
|
||||
iw = self.after_image.width()
|
||||
sh = min(self.after_image.height(), canvas_size.height())
|
||||
|
||||
if self.flip_forwards:
|
||||
source = QRect(max(0, iw - width), 0, width, sh)
|
||||
else:
|
||||
source = QRect(0, 0, width, sh)
|
||||
|
||||
target = QRect(source)
|
||||
target.moveLeft(0)
|
||||
p.drawPixmap(target, self.after_image, source)
|
||||
|
||||
p.end()
|
||||
|
||||
def set_current_width(self, val):
|
||||
self._current_width = val
|
||||
|
||||
current_width = pyqtProperty('int',
|
||||
fget=lambda self: self._current_width,
|
||||
fset=set_current_width
|
||||
)
|
@ -1,155 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QUrl, QWidget, QHBoxLayout, QSize, pyqtSlot, QVBoxLayout, QToolButton,
|
||||
QIcon, pyqtSignal)
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
|
||||
from calibre import prints
|
||||
from calibre.constants import DEBUG, FAKE_PROTOCOL, FAKE_HOST
|
||||
from calibre.ebooks.oeb.display.webview import load_html
|
||||
from polyglot.builtins import unicode_type
|
||||
|
||||
|
||||
class FootnotesPage(QWebPage):
|
||||
|
||||
def __init__(self, parent):
|
||||
QWebPage.__init__(self, parent)
|
||||
self.js_loader = None
|
||||
self._footnote_data = ''
|
||||
from calibre.gui2.viewer.documentview import apply_basic_settings
|
||||
settings = self.settings()
|
||||
apply_basic_settings(settings)
|
||||
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, False)
|
||||
self.setLinkDelegationPolicy(self.DelegateAllLinks)
|
||||
self.mainFrame().javaScriptWindowObjectCleared.connect(self.add_window_objects)
|
||||
self.add_window_objects()
|
||||
|
||||
def add_window_objects(self, add_ready_listener=True):
|
||||
self.mainFrame().addToJavaScriptWindowObject("py_bridge", self)
|
||||
evaljs = self.mainFrame().evaluateJavaScript
|
||||
if self.js_loader is not None:
|
||||
for x in 'utils extract'.split():
|
||||
evaljs(self.js_loader.get(x))
|
||||
|
||||
@pyqtSlot(str)
|
||||
def debug(self, msg):
|
||||
prints(msg)
|
||||
|
||||
@pyqtSlot(result=str)
|
||||
def footnote_data(self):
|
||||
return self._footnote_data
|
||||
|
||||
def set_footnote_data(self, target, known_targets):
|
||||
self._footnote_data = json.dumps({'target':target, 'known_targets':known_targets})
|
||||
if self._footnote_data:
|
||||
self.mainFrame().evaluateJavaScript(
|
||||
'data = JSON.parse(py_bridge.footnote_data()); calibre_extract.show_footnote(data["target"], data["known_targets"])')
|
||||
|
||||
def javaScriptAlert(self, frame, msg):
|
||||
prints('FootnoteView:alert::', msg)
|
||||
|
||||
def javaScriptConsoleMessage(self, msg, lineno, source_id):
|
||||
if DEBUG:
|
||||
prints('FootnoteView:%s:%s:'%(unicode_type(source_id), lineno), unicode_type(msg))
|
||||
|
||||
|
||||
class FootnotesView(QWidget):
|
||||
|
||||
follow_link = pyqtSignal()
|
||||
close_view = pyqtSignal()
|
||||
|
||||
def __init__(self, parent):
|
||||
QWidget.__init__(self, parent)
|
||||
self.l = l = QHBoxLayout(self)
|
||||
self.vl = vl = QVBoxLayout()
|
||||
self.view = v = QWebView(self)
|
||||
self._page = FootnotesPage(v)
|
||||
v.setPage(self._page)
|
||||
l.addWidget(v), l.addLayout(vl)
|
||||
self.goto_button = b = QToolButton(self)
|
||||
b.setIcon(QIcon(I('forward.png'))), b.setToolTip(_('Go to this footnote in the main view'))
|
||||
b.clicked.connect(self.follow_link)
|
||||
vl.addWidget(b)
|
||||
self.close_button = b = QToolButton(self)
|
||||
b.setIcon(QIcon(I('window-close.png'))), b.setToolTip(_('Close the footnotes window'))
|
||||
b.clicked.connect(self.close_view)
|
||||
vl.addWidget(b)
|
||||
|
||||
def page(self):
|
||||
return self._page
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(400, 200)
|
||||
|
||||
|
||||
class Footnotes(object):
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
self.clear()
|
||||
|
||||
def set_footnotes_view(self, fv):
|
||||
self.footnotes_view = fv
|
||||
self.clone_settings()
|
||||
fv.page().linkClicked.connect(self.view.footnote_link_clicked)
|
||||
fv.page().js_loader = self.view.document.js_loader
|
||||
|
||||
def clone_settings(self):
|
||||
source = self.view.document.settings()
|
||||
settings = self.footnotes_view.page().settings()
|
||||
for x in 'DefaultFontSize DefaultFixedFontSize MinimumLogicalFontSize MinimumFontSize StandardFont SerifFont SansSerifFont FixedFont'.split():
|
||||
func = 'setFontSize' if x.endswith('FontSize') else 'setFontFamily'
|
||||
getattr(settings, func)(getattr(QWebSettings, x), getattr(source, 'f' + func[4:])(getattr(QWebSettings, x)))
|
||||
settings.setUserStyleSheetUrl(source.userStyleSheetUrl())
|
||||
|
||||
def clear(self):
|
||||
self.known_footnote_targets = defaultdict(set)
|
||||
self.showing_url = None
|
||||
|
||||
def spine_path(self, path):
|
||||
try:
|
||||
si = self.view.manager.iterator.spine.index(path)
|
||||
return self.view.manager.iterator.spine[si]
|
||||
except (AttributeError, ValueError):
|
||||
pass
|
||||
|
||||
def get_footnote_data(self, a, qurl):
|
||||
current_path = self.view.path()
|
||||
if not current_path:
|
||||
return # Not viewing a local file
|
||||
dest_path = self.spine_path(self.view.path(qurl))
|
||||
if dest_path is not None:
|
||||
if dest_path == current_path:
|
||||
# We deliberately ignore linked to anchors if the destination is
|
||||
# the same as the source, because many books have section ToCs
|
||||
# that are linked back from their destinations, for example,
|
||||
# the calibre User Manual
|
||||
linked_to_anchors = {}
|
||||
else:
|
||||
linked_to_anchors = {anchor:0 for path, anchor in dest_path.verified_links if path == current_path}
|
||||
if a.evaluateJavaScript('calibre_extract.is_footnote_link(this, "%s://%s", %s)' % (FAKE_PROTOCOL, FAKE_HOST, json.dumps(linked_to_anchors))):
|
||||
if dest_path not in self.known_footnote_targets:
|
||||
self.known_footnote_targets[dest_path] = s = set()
|
||||
for item in self.view.manager.iterator.spine:
|
||||
for path, target in item.verified_links:
|
||||
if target and path == dest_path:
|
||||
s.add(target)
|
||||
return (dest_path, qurl.fragment(QUrl.FullyDecoded), qurl)
|
||||
|
||||
def show_footnote(self, fd):
|
||||
path, target, self.showing_url = fd
|
||||
|
||||
if hasattr(self, 'footnotes_view'):
|
||||
if load_html(path, self.footnotes_view.view, codec=getattr(path, 'encoding', 'utf-8'),
|
||||
mime_type=getattr(path, 'mime_type', 'text/html')):
|
||||
self.footnotes_view.page().set_footnote_data(target, {k:True for k in self.known_footnote_targets[path]})
|
@ -1,377 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import time, sys
|
||||
from functools import partial
|
||||
from PyQt5.Qt import (
|
||||
QObject, QPointF, pyqtSignal, QEvent, QApplication, QMouseEvent, Qt,
|
||||
QContextMenuEvent, QDialog, QDialogButtonBox, QLabel, QVBoxLayout)
|
||||
|
||||
from calibre.constants import iswindows
|
||||
from polyglot.builtins import itervalues, map
|
||||
|
||||
touch_supported = False
|
||||
if iswindows and sys.getwindowsversion()[:2] >= (6, 2): # At least windows 7
|
||||
touch_supported = True
|
||||
|
||||
SWIPE_HOLD_INTERVAL = 0.5 # seconds
|
||||
HOLD_THRESHOLD = 1.0 # seconds
|
||||
TAP_THRESHOLD = 50 # manhattan pixels
|
||||
SWIPE_DISTANCE = 100 # manhattan pixels
|
||||
PINCH_CENTER_THRESHOLD = 150 # manhattan pixels
|
||||
PINCH_SQUEEZE_FACTOR = 2.5 # smaller length must be less that larger length / squeeze factor
|
||||
|
||||
Tap, TapAndHold, Pinch, Swipe, SwipeAndHold = 'Tap', 'TapAndHold', 'Pinch', 'Swipe', 'SwipeAndHold'
|
||||
Left, Right, Up, Down = 'Left', 'Right', 'Up', 'Down'
|
||||
In, Out = 'In', 'Out'
|
||||
|
||||
|
||||
class Help(QDialog): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
self.l = l = QVBoxLayout(self)
|
||||
self.setLayout(l)
|
||||
|
||||
self.la = la = QLabel(
|
||||
'''
|
||||
<style>
|
||||
h2 { text-align: center }
|
||||
dt { font-weight: bold }
|
||||
dd { margin-bottom: 1.5em }
|
||||
</style>
|
||||
|
||||
''' + _(
|
||||
'''
|
||||
<h2>The list of available gestures</h2>
|
||||
<dl>
|
||||
<dt>Single tap</dt>
|
||||
<dd>A single tap on the right two thirds of the page will turn to the next page
|
||||
and on the left one-third of the page will turn to the previous page. Single tapping
|
||||
on a link will activate the link.</dd>
|
||||
|
||||
<dt>Swipe</dt>
|
||||
<dd>Swipe to the left to go to the next page and to the right to go to the previous page.
|
||||
This mimics turning pages in a paper book. When the viewer is not in paged mode, swiping
|
||||
scrolls the text line by line instead of page by page.</dd>
|
||||
|
||||
<dt>Pinch</dt>
|
||||
<dd>Pinch in or out to decrease or increase the font size</dd>
|
||||
|
||||
<dt>Swipe and hold</dt>
|
||||
<dd>If you swipe and the hold your finger down instead of lifting it, pages will be turned
|
||||
rapidly allowing for quickly scanning through large numbers of pages.</dd>
|
||||
|
||||
<dt>Tap and hold</dt>
|
||||
<dd>Bring up the context (right-click) menu</dd>
|
||||
</dl>
|
||||
'''
|
||||
))
|
||||
la.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||
la.setWordWrap(True)
|
||||
l.addWidget(la, Qt.AlignTop|Qt.AlignLeft)
|
||||
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
l.addWidget(bb)
|
||||
self.resize(600, 500)
|
||||
# }}}
|
||||
|
||||
|
||||
class TouchPoint(object):
|
||||
|
||||
def __init__(self, tp):
|
||||
self.creation_time = self.last_update_time = self.time_of_last_move = time.time()
|
||||
self.start_screen_position = self.current_screen_position = self.previous_screen_position = QPointF(tp.screenPos())
|
||||
self.time_since_last_update = -1
|
||||
self.total_movement = 0
|
||||
|
||||
def update(self, tp):
|
||||
now = time.time()
|
||||
self.time_since_last_update = now - self.last_update_time
|
||||
self.last_update_time = now
|
||||
self.previous_screen_position, self.current_screen_position = self.current_screen_position, QPointF(tp.screenPos())
|
||||
movement = (self.current_screen_position - self.previous_screen_position).manhattanLength()
|
||||
self.total_movement += movement
|
||||
if movement > 5:
|
||||
self.time_of_last_move = now
|
||||
|
||||
@property
|
||||
def swipe_type(self):
|
||||
x_movement = self.current_screen_position.x() - self.start_screen_position.x()
|
||||
y_movement = self.current_screen_position.y() - self.start_screen_position.y()
|
||||
xabs, yabs = map(abs, (x_movement, y_movement))
|
||||
if max(xabs, yabs) < SWIPE_DISTANCE or min(xabs/max(yabs, 0.01), yabs/max(xabs, 0.01)) > 0.3:
|
||||
return
|
||||
d = x_movement if xabs > yabs else y_movement
|
||||
axis = (Left, Right) if xabs > yabs else (Up, Down)
|
||||
return axis[0 if d < 0 else 1]
|
||||
|
||||
@property
|
||||
def swipe_live(self):
|
||||
x_movement = self.current_screen_position.x() - self.previous_screen_position.x()
|
||||
y_movement = self.current_screen_position.y() - self.previous_screen_position.y()
|
||||
return (x_movement, y_movement)
|
||||
|
||||
|
||||
def get_pinch(p1, p2):
|
||||
starts = [p1.start_screen_position, p2.start_screen_position]
|
||||
ends = [p1.current_screen_position, p2.current_screen_position]
|
||||
start_center = (starts[0] + starts[1]) / 2.0
|
||||
end_center = (ends[0] + ends[1]) / 2.0
|
||||
if (start_center - end_center).manhattanLength() > PINCH_CENTER_THRESHOLD:
|
||||
return None
|
||||
start_length = (starts[0] - starts[1]).manhattanLength()
|
||||
end_length = (ends[0] - ends[1]).manhattanLength()
|
||||
if min(start_length, end_length) > max(start_length, end_length) / PINCH_SQUEEZE_FACTOR:
|
||||
return None
|
||||
return In if start_length > end_length else Out
|
||||
|
||||
|
||||
class State(QObject):
|
||||
|
||||
tapped = pyqtSignal(object)
|
||||
swiped = pyqtSignal(object)
|
||||
swiping = pyqtSignal(object, object)
|
||||
pinched = pyqtSignal(object)
|
||||
tap_hold_started = pyqtSignal(object)
|
||||
tap_hold_updated = pyqtSignal(object)
|
||||
swipe_hold_started = pyqtSignal(object)
|
||||
swipe_hold_updated = pyqtSignal(object)
|
||||
tap_hold_finished = pyqtSignal(object)
|
||||
swipe_hold_finished = pyqtSignal(object)
|
||||
|
||||
def __init__(self):
|
||||
QObject.__init__(self)
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
self.possible_gestures = set()
|
||||
self.touch_points = {}
|
||||
self.hold_started = False
|
||||
self.hold_data = None
|
||||
|
||||
def start(self):
|
||||
self.clear()
|
||||
self.possible_gestures = {Tap, TapAndHold, Swipe, Pinch, SwipeAndHold}
|
||||
|
||||
def update(self, ev, boundary='update'):
|
||||
if boundary == 'start':
|
||||
self.start()
|
||||
|
||||
for tp in ev.touchPoints():
|
||||
tpid = tp.id()
|
||||
if tpid not in self.touch_points:
|
||||
self.touch_points[tpid] = TouchPoint(tp)
|
||||
else:
|
||||
self.touch_points[tpid].update(tp)
|
||||
|
||||
if len(self.touch_points) > 2:
|
||||
self.possible_gestures.clear()
|
||||
elif len(self.touch_points) > 1:
|
||||
self.possible_gestures &= {Pinch}
|
||||
|
||||
if boundary == 'end':
|
||||
self.check_for_holds()
|
||||
self.finalize()
|
||||
self.clear()
|
||||
else:
|
||||
self.check_for_holds()
|
||||
if {Swipe, SwipeAndHold} & self.possible_gestures:
|
||||
tp = next(itervalues(self.touch_points))
|
||||
self.swiping.emit(*tp.swipe_live)
|
||||
|
||||
def check_for_holds(self):
|
||||
if not {SwipeAndHold, TapAndHold} & self.possible_gestures:
|
||||
return
|
||||
now = time.time()
|
||||
tp = next(itervalues(self.touch_points))
|
||||
if now - tp.time_of_last_move < HOLD_THRESHOLD:
|
||||
return
|
||||
if self.hold_started:
|
||||
if TapAndHold in self.possible_gestures:
|
||||
self.tap_hold_updated.emit(tp)
|
||||
if SwipeAndHold in self.possible_gestures:
|
||||
self.swipe_hold_updated.emit(self.hold_data[1])
|
||||
else:
|
||||
self.possible_gestures &= {TapAndHold, SwipeAndHold}
|
||||
if tp.total_movement > TAP_THRESHOLD:
|
||||
st = tp.swipe_type
|
||||
if st is None:
|
||||
self.possible_gestures.clear()
|
||||
else:
|
||||
self.hold_started = True
|
||||
self.possible_gestures = {SwipeAndHold}
|
||||
self.hold_data = (now, st)
|
||||
self.swipe_hold_started.emit(st)
|
||||
else:
|
||||
self.possible_gestures = {TapAndHold}
|
||||
self.hold_started = True
|
||||
self.hold_data = now
|
||||
self.tap_hold_started.emit(tp)
|
||||
|
||||
def finalize(self):
|
||||
if Tap in self.possible_gestures:
|
||||
tp = next(itervalues(self.touch_points))
|
||||
if tp.total_movement <= TAP_THRESHOLD:
|
||||
self.tapped.emit(tp)
|
||||
return
|
||||
|
||||
if Swipe in self.possible_gestures:
|
||||
tp = next(itervalues(self.touch_points))
|
||||
st = tp.swipe_type
|
||||
if st is not None:
|
||||
self.swiped.emit(st)
|
||||
return
|
||||
|
||||
if Pinch in self.possible_gestures:
|
||||
points = tuple(itervalues(self.touch_points))
|
||||
if len(points) == 2:
|
||||
pinch_dir = get_pinch(*points)
|
||||
if pinch_dir is not None:
|
||||
self.pinched.emit(pinch_dir)
|
||||
|
||||
if not self.hold_started:
|
||||
return
|
||||
|
||||
if TapAndHold in self.possible_gestures:
|
||||
tp = next(itervalues(self.touch_points))
|
||||
self.tap_hold_finished.emit(tp)
|
||||
return
|
||||
|
||||
if SwipeAndHold in self.possible_gestures:
|
||||
self.swipe_hold_finished.emit(self.hold_data[1])
|
||||
return
|
||||
|
||||
|
||||
class GestureHandler(QObject):
|
||||
|
||||
def __init__(self, view):
|
||||
QObject.__init__(self, view)
|
||||
self.state = State()
|
||||
self.last_swipe_hold_update = None
|
||||
self.state.swiped.connect(self.handle_swipe)
|
||||
self.state.tapped.connect(self.handle_tap)
|
||||
self.state.swiping.connect(self.handle_swiping)
|
||||
self.state.tap_hold_started.connect(partial(self.handle_tap_hold, 'start'))
|
||||
self.state.tap_hold_updated.connect(partial(self.handle_tap_hold, 'update'))
|
||||
self.state.tap_hold_finished.connect(partial(self.handle_tap_hold, 'end'))
|
||||
self.state.swipe_hold_started.connect(partial(self.handle_swipe_hold, 'start'))
|
||||
self.state.swipe_hold_updated.connect(partial(self.handle_swipe_hold, 'update'))
|
||||
self.state.swipe_hold_finished.connect(partial(self.handle_swipe_hold, 'end'))
|
||||
self.state.pinched.connect(self.handle_pinch)
|
||||
self.evmap = {QEvent.TouchBegin: 'start', QEvent.TouchUpdate: 'update', QEvent.TouchEnd: 'end'}
|
||||
|
||||
def __call__(self, ev):
|
||||
if not touch_supported:
|
||||
return False
|
||||
etype = ev.type()
|
||||
if etype in (
|
||||
QEvent.MouseMove, QEvent.MouseButtonPress,
|
||||
QEvent.MouseButtonRelease, QEvent.MouseButtonDblClick) and ev.source() != Qt.MouseEventNotSynthesized:
|
||||
# swallow fake mouse events generated from touch events
|
||||
ev.accept()
|
||||
return True
|
||||
boundary = self.evmap.get(etype, None)
|
||||
if boundary is None:
|
||||
return False
|
||||
self.state.update(ev, boundary=boundary)
|
||||
ev.accept()
|
||||
return True
|
||||
|
||||
def close_open_menu(self):
|
||||
m = getattr(self.parent(), 'context_menu', None)
|
||||
if m is not None and m.isVisible():
|
||||
m.close()
|
||||
return True
|
||||
|
||||
def handle_swipe(self, direction):
|
||||
if self.close_open_menu():
|
||||
return
|
||||
view = self.parent()
|
||||
if not view.document.in_paged_mode:
|
||||
return
|
||||
func = {Left:'next_page', Right: 'previous_page', Down:'goto_previous_section', Up:'goto_next_section'}[direction]
|
||||
getattr(view, func)()
|
||||
|
||||
def handle_swiping(self, x, y):
|
||||
if max(abs(x), abs(y)) < 1:
|
||||
return
|
||||
view = self.parent()
|
||||
if view.document.in_paged_mode:
|
||||
return
|
||||
ydirection = Up if y < 0 else Down
|
||||
if view.manager is not None and abs(y) > 0:
|
||||
if ydirection is Up and view.document.at_bottom:
|
||||
view.manager.next_document()
|
||||
return
|
||||
elif ydirection is Down and view.document.at_top:
|
||||
view.manager.previous_document()
|
||||
return
|
||||
view.scroll_by(x=-x, y=-y)
|
||||
if view.manager is not None:
|
||||
view.manager.scrolled(view.scroll_fraction)
|
||||
|
||||
def current_position(self, tp):
|
||||
return self.parent().mapFromGlobal(tp.current_screen_position.toPoint())
|
||||
|
||||
def handle_tap(self, tp):
|
||||
if self.close_open_menu():
|
||||
return
|
||||
view = self.parent()
|
||||
mf = view.document.mainFrame()
|
||||
r = mf.hitTestContent(self.current_position(tp))
|
||||
if r.linkElement().isNull():
|
||||
if view.document.tap_flips_pages:
|
||||
threshold = view.width() / 3.0
|
||||
attr = 'previous' if self.current_position(tp).x() <= threshold else 'next'
|
||||
getattr(view, '%s_page'%attr)()
|
||||
else:
|
||||
for etype in (QEvent.MouseButtonPress, QEvent.MouseButtonRelease):
|
||||
ev = QMouseEvent(etype, self.current_position(tp), tp.current_screen_position.toPoint(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
|
||||
QApplication.sendEvent(view, ev)
|
||||
|
||||
def handle_tap_hold(self, action, tp):
|
||||
etype = {'start':QEvent.MouseButtonPress, 'update':QEvent.MouseMove, 'end':QEvent.MouseButtonRelease}[action]
|
||||
ev = QMouseEvent(etype, self.current_position(tp), tp.current_screen_position.toPoint(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
|
||||
QApplication.sendEvent(self.parent(), ev)
|
||||
if action == 'end':
|
||||
ev = QContextMenuEvent(QContextMenuEvent.Other, self.current_position(tp), tp.current_screen_position.toPoint())
|
||||
# We have to use post event otherwise the popup remains an alien widget and does not receive events
|
||||
QApplication.postEvent(self.parent(), ev)
|
||||
|
||||
def handle_swipe_hold(self, action, direction):
|
||||
view = self.parent()
|
||||
if not view.document.in_paged_mode:
|
||||
return
|
||||
if action == 'start':
|
||||
self.last_swipe_hold_update = time.time()
|
||||
try:
|
||||
self.handle_swipe(direction)
|
||||
finally:
|
||||
view.is_auto_repeat_event = False
|
||||
elif action == 'update' and self.last_swipe_hold_update is not None and time.time() - self.last_swipe_hold_update > SWIPE_HOLD_INTERVAL:
|
||||
view.is_auto_repeat_event = True
|
||||
self.last_swipe_hold_update = time.time()
|
||||
try:
|
||||
self.handle_swipe(direction)
|
||||
finally:
|
||||
view.is_auto_repeat_event = False
|
||||
elif action == 'end':
|
||||
self.last_swipe_hold_update = None
|
||||
|
||||
def handle_pinch(self, direction):
|
||||
attr = 'magnify' if direction is Out else 'shrink'
|
||||
getattr(self.parent(), '%s_fonts' % attr)()
|
||||
|
||||
def show_help(self):
|
||||
Help(self.parent()).exec_()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication([])
|
||||
Help().exec_()
|
@ -1,177 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt5.Qt import (QDialog, QPixmap, QUrl, QScrollArea, QLabel, QSizePolicy,
|
||||
QDialogButtonBox, QVBoxLayout, QPalette, QApplication, QSize, QIcon,
|
||||
Qt, QTransform, QSvgRenderer, QImage, QPainter)
|
||||
|
||||
from calibre.gui2 import choose_save_file, gprefs, NO_URL_FORMATTING, max_available_height
|
||||
from polyglot.builtins import unicode_type
|
||||
|
||||
|
||||
def render_svg(widget, path):
|
||||
img = QPixmap()
|
||||
rend = QSvgRenderer()
|
||||
if rend.load(path):
|
||||
dpr = getattr(widget, 'devicePixelRatioF', widget.devicePixelRatio)()
|
||||
sz = rend.defaultSize()
|
||||
h = (max_available_height() - 50)
|
||||
w = int(h * sz.height() / float(sz.width()))
|
||||
pd = QImage(w * dpr, h * dpr, QImage.Format_RGB32)
|
||||
pd.fill(Qt.white)
|
||||
p = QPainter(pd)
|
||||
rend.render(p)
|
||||
p.end()
|
||||
img = QPixmap.fromImage(pd)
|
||||
img.setDevicePixelRatio(dpr)
|
||||
return img
|
||||
|
||||
|
||||
class ImageView(QDialog):
|
||||
|
||||
def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry'):
|
||||
QDialog.__init__(self)
|
||||
dw = QApplication.instance().desktop()
|
||||
self.avail_geom = dw.availableGeometry(parent if parent is not None else self)
|
||||
self.current_img = current_img
|
||||
self.current_url = current_url
|
||||
self.factor = 1.0
|
||||
self.geom_name = geom_name
|
||||
|
||||
self.label = l = QLabel()
|
||||
l.setBackgroundRole(QPalette.Base)
|
||||
l.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
|
||||
l.setScaledContents(True)
|
||||
|
||||
self.scrollarea = sa = QScrollArea()
|
||||
sa.setBackgroundRole(QPalette.Dark)
|
||||
sa.setWidget(l)
|
||||
|
||||
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
self.zi_button = zi = bb.addButton(_('Zoom &in'), bb.ActionRole)
|
||||
self.zo_button = zo = bb.addButton(_('Zoom &out'), bb.ActionRole)
|
||||
self.save_button = so = bb.addButton(_('&Save as'), bb.ActionRole)
|
||||
self.rotate_button = ro = bb.addButton(_('&Rotate'), bb.ActionRole)
|
||||
zi.setIcon(QIcon(I('plus.png')))
|
||||
zo.setIcon(QIcon(I('minus.png')))
|
||||
so.setIcon(QIcon(I('save.png')))
|
||||
ro.setIcon(QIcon(I('rotate-right.png')))
|
||||
zi.clicked.connect(self.zoom_in)
|
||||
zo.clicked.connect(self.zoom_out)
|
||||
so.clicked.connect(self.save_image)
|
||||
ro.clicked.connect(self.rotate_image)
|
||||
|
||||
self.l = l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
l.addWidget(sa)
|
||||
l.addWidget(bb)
|
||||
|
||||
def zoom_in(self):
|
||||
self.factor *= 1.25
|
||||
self.adjust_image(1.25)
|
||||
|
||||
def zoom_out(self):
|
||||
self.factor *= 0.8
|
||||
self.adjust_image(0.8)
|
||||
|
||||
def save_image(self):
|
||||
filters=[('Images', ['png', 'jpeg', 'jpg'])]
|
||||
f = choose_save_file(self, 'viewer image view save dialog',
|
||||
_('Choose a file to save to'), filters=filters,
|
||||
all_files=False)
|
||||
if f:
|
||||
from calibre.utils.img import save_image
|
||||
save_image(self.current_img.toImage(), f)
|
||||
|
||||
def adjust_image(self, factor):
|
||||
self.label.resize(self.factor * self.current_img.size())
|
||||
self.zi_button.setEnabled(self.factor <= 3)
|
||||
self.zo_button.setEnabled(self.factor >= 0.3333)
|
||||
self.adjust_scrollbars(factor)
|
||||
|
||||
def adjust_scrollbars(self, factor):
|
||||
for sb in (self.scrollarea.horizontalScrollBar(),
|
||||
self.scrollarea.verticalScrollBar()):
|
||||
sb.setValue(int(factor*sb.value()) + ((factor - 1) * sb.pageStep()/2))
|
||||
|
||||
def rotate_image(self):
|
||||
pm = self.label.pixmap()
|
||||
t = QTransform()
|
||||
t.rotate(90)
|
||||
pm = self.current_img = pm.transformed(t)
|
||||
self.label.setPixmap(pm)
|
||||
self.label.adjustSize()
|
||||
self.factor = 1
|
||||
for sb in (self.scrollarea.horizontalScrollBar(),
|
||||
self.scrollarea.verticalScrollBar()):
|
||||
sb.setValue(0)
|
||||
|
||||
def __call__(self, use_exec=False):
|
||||
geom = self.avail_geom
|
||||
self.label.setPixmap(self.current_img)
|
||||
self.label.adjustSize()
|
||||
self.resize(QSize(int(geom.width()/2.5), geom.height()-50))
|
||||
geom = gprefs.get(self.geom_name, None)
|
||||
if geom is not None:
|
||||
self.restoreGeometry(geom)
|
||||
try:
|
||||
self.current_image_name = unicode_type(self.current_url.toString(NO_URL_FORMATTING)).rpartition('/')[-1]
|
||||
except AttributeError:
|
||||
self.current_image_name = self.current_url
|
||||
title = _('View image: %s')%self.current_image_name
|
||||
self.setWindowTitle(title)
|
||||
if use_exec:
|
||||
self.exec_()
|
||||
else:
|
||||
self.show()
|
||||
|
||||
def done(self, e):
|
||||
gprefs[self.geom_name] = bytearray(self.saveGeometry())
|
||||
return QDialog.done(self, e)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
d = event.angleDelta().y()
|
||||
if abs(d) > 0 and not self.scrollarea.verticalScrollBar().isVisible():
|
||||
event.accept()
|
||||
(self.zoom_out if d < 0 else self.zoom_in)()
|
||||
|
||||
|
||||
class ImagePopup(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self.current_img = QPixmap()
|
||||
self.current_url = QUrl()
|
||||
self.parent = parent
|
||||
self.dialogs = []
|
||||
|
||||
def __call__(self):
|
||||
if self.current_img.isNull():
|
||||
return
|
||||
d = ImageView(self.parent, self.current_img, self.current_url)
|
||||
self.dialogs.append(d)
|
||||
d.finished.connect(self.cleanup, type=Qt.QueuedConnection)
|
||||
d()
|
||||
|
||||
def cleanup(self):
|
||||
for d in tuple(self.dialogs):
|
||||
if not d.isVisible():
|
||||
self.dialogs.remove(d)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from calibre.gui2 import Application
|
||||
app = Application([])
|
||||
p = QPixmap()
|
||||
p.load(sys.argv[-1])
|
||||
u = QUrl.fromLocalFile(sys.argv[-1])
|
||||
d = ImageView(None, p, u)
|
||||
d()
|
||||
app.exec_()
|
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from PyQt5.Qt import QDialog, QDialogButtonBox, QVBoxLayout, QIcon, Qt
|
||||
from PyQt5.QtWebKitWidgets import QWebInspector
|
||||
|
||||
from calibre.gui2 import gprefs
|
||||
|
||||
|
||||
class WebInspector(QDialog):
|
||||
|
||||
def __init__(self, parent, page):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setWindowFlags(self.windowFlags() | Qt.WindowMinMaxButtonsHint)
|
||||
self.setWindowTitle(_('Inspect book code'))
|
||||
self.setWindowIcon(QIcon(I('debug.png')))
|
||||
l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
|
||||
self.inspector = QWebInspector(self)
|
||||
self.inspector.setPage(page)
|
||||
l.addWidget(self.inspector)
|
||||
|
||||
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
l.addWidget(bb)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
geom = gprefs.get('viewer_inspector_geom', None)
|
||||
if geom is not None:
|
||||
self.restoreGeometry(geom)
|
||||
|
||||
def save_geometry(self):
|
||||
gprefs['viewer_inspector_geom'] = bytearray(self.saveGeometry())
|
||||
|
||||
def closeEvent(self, ev):
|
||||
self.save_geometry()
|
||||
return QDialog.closeEvent(self, ev)
|
||||
|
||||
def accept(self):
|
||||
self.save_geometry()
|
||||
QDialog.accept(self)
|
||||
|
||||
def reject(self):
|
||||
self.save_geometry()
|
||||
QDialog.reject(self)
|
||||
|
@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
|
||||
import calibre
|
||||
from calibre.utils.resources import compiled_coffeescript, load_hyphenator_dicts
|
||||
from polyglot.builtins import iteritems
|
||||
|
||||
|
||||
class JavaScriptLoader(object):
|
||||
|
||||
JS = {x:('viewer/%s.js'%x if y is None else y) for x, y in iteritems({
|
||||
|
||||
'bookmarks':None,
|
||||
'referencing':None,
|
||||
'hyphenation':None,
|
||||
'jquery':'viewer/jquery.js',
|
||||
'jquery_scrollTo':None,
|
||||
'hyphenator':'viewer/hyphenate/Hyphenator.js',
|
||||
'images':None
|
||||
|
||||
})}
|
||||
|
||||
CS = {
|
||||
'cfi':'ebooks.oeb.display.cfi',
|
||||
'indexing':'ebooks.oeb.display.indexing',
|
||||
'paged':'ebooks.oeb.display.paged',
|
||||
'utils':'ebooks.oeb.display.utils',
|
||||
'fs':'ebooks.oeb.display.full_screen',
|
||||
'math': 'ebooks.oeb.display.mathjax',
|
||||
'extract': 'ebooks.oeb.display.extract',
|
||||
}
|
||||
|
||||
ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images',
|
||||
'hyphenation', 'hyphenator', 'utils', 'cfi', 'indexing', 'paged',
|
||||
'fs', 'math', 'extract')
|
||||
|
||||
def __init__(self, dynamic_coffeescript=False):
|
||||
self._dynamic_coffeescript = dynamic_coffeescript
|
||||
if self._dynamic_coffeescript:
|
||||
try:
|
||||
from calibre.utils.serve_coffee import compile_coffeescript
|
||||
compile_coffeescript
|
||||
except:
|
||||
self._dynamic_coffeescript = False
|
||||
print('WARNING: Failed to load serve_coffee, not compiling '
|
||||
'coffeescript dynamically.')
|
||||
|
||||
self._cache = {}
|
||||
self._hp_cache = {}
|
||||
|
||||
def get(self, name):
|
||||
ans = self._cache.get(name, None)
|
||||
if ans is None:
|
||||
src = self.CS.get(name, None)
|
||||
if src is None:
|
||||
src = self.JS.get(name, None)
|
||||
if src is None:
|
||||
raise KeyError('No such resource: %s'%name)
|
||||
ans = P(src, data=True,
|
||||
allow_user_override=False).decode('utf-8')
|
||||
else:
|
||||
dynamic = self._dynamic_coffeescript and calibre.__file__ and not calibre.__file__.endswith('.pyo') and os.path.exists(calibre.__file__)
|
||||
ans = compiled_coffeescript(src, dynamic=dynamic).decode('utf-8')
|
||||
self._cache[name] = ans
|
||||
|
||||
return ans
|
||||
|
||||
def __call__(self, evaljs, lang, default_lang):
|
||||
for x in self.ORDER:
|
||||
src = self.get(x)
|
||||
evaljs(src)
|
||||
|
||||
js, lang = load_hyphenator_dicts(self._hp_cache, lang, default_lang)
|
||||
evaljs(js)
|
||||
return lang
|
@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.constants import isosx
|
||||
|
||||
SHORTCUTS = {
|
||||
'Next Page' : (['PgDown', 'Space'],
|
||||
_('Scroll to the next page')),
|
||||
|
||||
'Previous Page' : (['PgUp', 'Backspace', 'Shift+Space'],
|
||||
_('Scroll to the previous page')),
|
||||
|
||||
'Next Section' : (['Ctrl+PgDown', 'Ctrl+Down'],
|
||||
_('Scroll to the next section')),
|
||||
|
||||
'Previous Section' : (['Ctrl+PgUp', 'Ctrl+Up'],
|
||||
_('Scroll to the previous section')),
|
||||
|
||||
'Section Bottom' : (['End'],
|
||||
_('Scroll to the bottom of the section')),
|
||||
|
||||
'Section Top' : (['Home'],
|
||||
_('Scroll to the top of the section')),
|
||||
|
||||
'Document Bottom' : (['Ctrl+End'],
|
||||
_('Scroll to the end of the document')),
|
||||
|
||||
'Document Top' : (['Ctrl+Home'],
|
||||
_('Scroll to the start of the document')),
|
||||
|
||||
'Down' : (['J', 'Down'],
|
||||
_('Scroll down')),
|
||||
|
||||
'Up' : (['K', 'Up'],
|
||||
_('Scroll up')),
|
||||
|
||||
'Left' : (['H', 'Left'],
|
||||
_('Scroll left')),
|
||||
|
||||
'Right' : (['L', 'Right'],
|
||||
_('Scroll right')),
|
||||
|
||||
'Back': (['Alt+Left'],
|
||||
_('Back')),
|
||||
|
||||
'Forward': (['Alt+Right'],
|
||||
_('Forward')),
|
||||
|
||||
'Quit': (['Ctrl+Q', 'Ctrl+W', 'Alt+F4'],
|
||||
_('Quit')),
|
||||
|
||||
'Focus Search': (['/', 'Ctrl+F'],
|
||||
_('Start search')),
|
||||
|
||||
'Show metadata': (['Ctrl+I'],
|
||||
_('Show metadata')),
|
||||
|
||||
'Font larger': (['Ctrl+='],
|
||||
_('Font size larger')),
|
||||
|
||||
'Font smaller': (['Ctrl+-'],
|
||||
_('Font size smaller')),
|
||||
|
||||
'Fullscreen': ((['Ctrl+Meta+F'] if isosx else ['Ctrl+Shift+F', 'F11']),
|
||||
_('Fullscreen')),
|
||||
|
||||
'Find next': (['F3'],
|
||||
_('Find next')),
|
||||
|
||||
'Find previous': (['Shift+F3'],
|
||||
_('Find previous')),
|
||||
|
||||
'Search online': (['Ctrl+E'],
|
||||
_('Search online for word')),
|
||||
|
||||
'Table of Contents': (['Ctrl+T'],
|
||||
_('Show/hide the Table of Contents')),
|
||||
|
||||
'Lookup word': (['Ctrl+L'],
|
||||
_('Lookup word in dictionary')),
|
||||
|
||||
'Next occurrence': (['Ctrl+S'],
|
||||
_('Go to next occurrence of selected word')),
|
||||
|
||||
'Bookmark': (['Ctrl+B'],
|
||||
_('Bookmark the current location')),
|
||||
|
||||
'Toggle bookmarks': (['Ctrl+Alt+B'],
|
||||
_('Show/hide bookmarks')),
|
||||
|
||||
'Reload': (['Ctrl+R', 'F5'],
|
||||
_('Reload the current book')),
|
||||
|
||||
'Print': (['Ctrl+P'],
|
||||
_('Print the current book')),
|
||||
|
||||
'Show/hide controls': (['Ctrl+F11'],
|
||||
_('Show/hide controls')),
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import json, time, numbers
|
||||
|
||||
from PyQt5.Qt import QApplication, QEventLoop
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
|
||||
|
||||
class PagePosition(object):
|
||||
|
||||
def __init__(self, document):
|
||||
self.document = document
|
||||
document.jump_to_cfi_listeners.add(self)
|
||||
self.cfi_job_id = 0
|
||||
self.pending_scrolls = set()
|
||||
|
||||
@property
|
||||
def viewport_cfi(self):
|
||||
ans = self.document.mainFrame().evaluateJavaScript('''
|
||||
ans = 'undefined';
|
||||
if (window.paged_display) {
|
||||
ans = window.paged_display.current_cfi();
|
||||
if (!ans) ans = 'undefined';
|
||||
}
|
||||
ans;
|
||||
''')
|
||||
if ans in {'', 'undefined'}:
|
||||
ans = None
|
||||
return ans
|
||||
|
||||
def scroll_to_cfi(self, cfi):
|
||||
if cfi:
|
||||
jid = self.cfi_job_id
|
||||
self.cfi_job_id += 1
|
||||
cfi = json.dumps(cfi)
|
||||
self.pending_scrolls.add(jid)
|
||||
self.document.mainFrame().evaluateJavaScript(
|
||||
'paged_display.jump_to_cfi(%s, %d)' % (cfi, jid))
|
||||
# jump_to_cfi is async, so we wait for it to complete
|
||||
st = time.time()
|
||||
WAIT = 1 # seconds
|
||||
while jid in self.pending_scrolls and time.time() - st < WAIT:
|
||||
QApplication.processEvents(QEventLoop.ExcludeUserInputEvents | QEventLoop.ExcludeSocketNotifiers)
|
||||
time.sleep(0.01)
|
||||
if jid in self.pending_scrolls:
|
||||
self.pending_scrolls.discard(jid)
|
||||
if DEBUG:
|
||||
print('jump_to_cfi() failed to complete after %s seconds' % WAIT)
|
||||
|
||||
@property
|
||||
def current_pos(self):
|
||||
ans = self.viewport_cfi
|
||||
if not ans:
|
||||
ans = self.document.scroll_fraction
|
||||
return ans
|
||||
|
||||
def __enter__(self):
|
||||
self.save()
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.restore()
|
||||
|
||||
def __call__(self, cfi_job_id):
|
||||
self.pending_scrolls.discard(cfi_job_id)
|
||||
|
||||
def save(self, overwrite=True):
|
||||
if not overwrite and self._cpos is not None:
|
||||
return
|
||||
self._cpos = self.current_pos
|
||||
|
||||
def restore(self):
|
||||
if self._cpos is None:
|
||||
return
|
||||
self.to_pos(self._cpos)
|
||||
self._cpos = None
|
||||
|
||||
def to_pos(self, pos):
|
||||
if isinstance(pos, numbers.Number):
|
||||
self.document.scroll_fraction = pos
|
||||
else:
|
||||
self.scroll_to_cfi(pos)
|
||||
|
||||
def set_pos(self, pos):
|
||||
self._cpos = pos
|
@ -1,230 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os, subprocess, sys
|
||||
from threading import Thread
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QFormLayout, QLineEdit, QToolButton, QHBoxLayout, QLabel, QIcon, QPrinter,
|
||||
QPageSize, QComboBox, QDoubleSpinBox, QCheckBox, QProgressDialog, QTimer)
|
||||
|
||||
from calibre import sanitize_file_name
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.ebooks.conversion.plugins.pdf_output import PAPER_SIZES
|
||||
from calibre.gui2 import elided_text, error_dialog, choose_save_file, Application, open_local_file, dynamic
|
||||
from calibre.gui2.widgets2 import Dialog
|
||||
from calibre.gui2.viewer.main import vprefs
|
||||
from calibre.utils.icu import numeric_sort_key
|
||||
from calibre.utils.ipc.simple_worker import start_pipe_worker
|
||||
from calibre.utils.serialize import msgpack_dumps, msgpack_loads
|
||||
|
||||
|
||||
class PrintDialog(Dialog):
|
||||
|
||||
OUTPUT_NAME = 'print-to-pdf-choose-file'
|
||||
|
||||
def __init__(self, book_title, parent=None, prefs=vprefs):
|
||||
self.book_title = book_title
|
||||
self.default_file_name = sanitize_file_name(book_title[:75] + '.pdf')
|
||||
self.paper_size_map = {a:getattr(QPageSize, a.capitalize()) for a in PAPER_SIZES}
|
||||
Dialog.__init__(self, _('Print to PDF'), 'print-to-pdf', prefs=prefs, parent=parent)
|
||||
|
||||
def setup_ui(self):
|
||||
self.l = l = QFormLayout(self)
|
||||
l.addRow(QLabel(_('Print %s to a PDF file') % elided_text(self.book_title)))
|
||||
self.h = h = QHBoxLayout()
|
||||
self.file_name = f = QLineEdit(self)
|
||||
val = dynamic.get(self.OUTPUT_NAME, None)
|
||||
if not val:
|
||||
val = os.path.expanduser('~')
|
||||
else:
|
||||
val = os.path.dirname(val)
|
||||
f.setText(os.path.abspath(os.path.join(val, self.default_file_name)))
|
||||
self.browse_button = b = QToolButton(self)
|
||||
b.setIcon(QIcon(I('document_open.png'))), b.setToolTip(_('Choose location for PDF file'))
|
||||
b.clicked.connect(self.choose_file)
|
||||
h.addWidget(f), h.addWidget(b)
|
||||
f.setMinimumWidth(350)
|
||||
w = QLabel(_('&File:'))
|
||||
l.addRow(w, h), w.setBuddy(f)
|
||||
|
||||
self.paper_size = ps = QComboBox(self)
|
||||
ps.addItems([a.upper() for a in sorted(self.paper_size_map, key=numeric_sort_key)])
|
||||
previous_size = vprefs.get('print-to-pdf-page-size', None)
|
||||
if previous_size not in self.paper_size_map:
|
||||
previous_size = (QPrinter().pageLayout().pageSize().name() or '').lower()
|
||||
if previous_size not in self.paper_size_map:
|
||||
previous_size = 'a4'
|
||||
ps.setCurrentIndex(ps.findText(previous_size.upper()))
|
||||
l.addRow(_('Paper &size:'), ps)
|
||||
tmap = {
|
||||
'left':_('&Left margin:'),
|
||||
'top':_('&Top margin:'),
|
||||
'right':_('&Right margin:'),
|
||||
'bottom':_('&Bottom margin:'),
|
||||
}
|
||||
for edge in 'left top right bottom'.split():
|
||||
m = QDoubleSpinBox(self)
|
||||
m.setSuffix(' ' + _('inches'))
|
||||
m.setMinimum(0), m.setMaximum(3), m.setSingleStep(0.1)
|
||||
val = vprefs.get('print-to-pdf-%s-margin' % edge, 1)
|
||||
m.setValue(val)
|
||||
setattr(self, '%s_margin' % edge, m)
|
||||
l.addRow(tmap[edge], m)
|
||||
self.pnum = pnum = QCheckBox(_('Add page &number to printed pages'), self)
|
||||
pnum.setChecked(vprefs.get('print-to-pdf-page-numbers', True))
|
||||
l.addRow(pnum)
|
||||
|
||||
self.show_file = sf = QCheckBox(_('&Open PDF file after printing'), self)
|
||||
sf.setChecked(vprefs.get('print-to-pdf-show-file', True))
|
||||
l.addRow(sf)
|
||||
|
||||
l.addRow(self.bb)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
fpath = self.file_name.text().strip()
|
||||
head, tail = os.path.split(fpath)
|
||||
tail = sanitize_file_name(tail)
|
||||
fpath = tail
|
||||
if head:
|
||||
fpath = os.path.join(head, tail)
|
||||
ans = {
|
||||
'output': fpath,
|
||||
'paper_size': self.paper_size.currentText().lower(),
|
||||
'page_numbers':self.pnum.isChecked(),
|
||||
'show_file':self.show_file.isChecked(),
|
||||
}
|
||||
for edge in 'left top right bottom'.split():
|
||||
ans['margin_' + edge] = getattr(self, '%s_margin' % edge).value()
|
||||
return ans
|
||||
|
||||
def choose_file(self):
|
||||
ans = choose_save_file(self, self.OUTPUT_NAME, _('PDF file'), filters=[(_('PDF file'), ['pdf'])],
|
||||
all_files=False, initial_filename=self.default_file_name)
|
||||
if ans:
|
||||
self.file_name.setText(ans)
|
||||
|
||||
def save_used_values(self):
|
||||
data = self.data
|
||||
vprefs['print-to-pdf-page-size'] = data['paper_size']
|
||||
vprefs['print-to-pdf-page-numbers'] = data['page_numbers']
|
||||
vprefs['print-to-pdf-show-file'] = data['show_file']
|
||||
for edge in 'left top right bottom'.split():
|
||||
vprefs['print-to-pdf-%s-margin' % edge] = data['margin_' + edge]
|
||||
|
||||
def accept(self):
|
||||
fname = self.file_name.text().strip()
|
||||
if not fname:
|
||||
return error_dialog(self, _('No filename specified'), _(
|
||||
'You must specify a filename for the PDF file to generate'), show=True)
|
||||
if not fname.lower().endswith('.pdf'):
|
||||
return error_dialog(self, _('Incorrect filename specified'), _(
|
||||
'The filename for the PDF file must end with .pdf'), show=True)
|
||||
self.save_used_values()
|
||||
return Dialog.accept(self)
|
||||
|
||||
|
||||
class DoPrint(Thread):
|
||||
|
||||
daemon = True
|
||||
|
||||
def __init__(self, data):
|
||||
Thread.__init__(self, name='DoPrint')
|
||||
self.data = data
|
||||
self.tb = self.log = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
with PersistentTemporaryFile('print-to-pdf-log.txt') as f:
|
||||
p = self.worker = start_pipe_worker('from calibre.gui2.viewer.printing import do_print; do_print()', stdout=f, stderr=subprocess.STDOUT)
|
||||
p.stdin.write(msgpack_dumps(self.data)), p.stdin.flush(), p.stdin.close()
|
||||
rc = p.wait()
|
||||
if rc != 0:
|
||||
f.seek(0)
|
||||
self.log = f.read().decode('utf-8', 'replace')
|
||||
try:
|
||||
os.remove(f.name)
|
||||
except EnvironmentError:
|
||||
pass
|
||||
except Exception:
|
||||
import traceback
|
||||
self.tb = traceback.format_exc()
|
||||
|
||||
|
||||
def do_print():
|
||||
from calibre.customize.ui import plugin_for_input_format
|
||||
stdin = getattr(sys.stdin, 'buffer', sys.stdin)
|
||||
data = msgpack_loads(stdin.read())
|
||||
ext = data['input'].lower().rpartition('.')[-1]
|
||||
input_plugin = plugin_for_input_format(ext)
|
||||
args = ['ebook-convert', data['input'], data['output'], '--paper-size', data['paper_size'], '--pdf-add-toc',
|
||||
'--disable-remove-fake-margins', '--chapter-mark', 'none', '-vv']
|
||||
if input_plugin.is_image_collection:
|
||||
args.append('--no-process')
|
||||
else:
|
||||
args.append('--disable-font-rescaling')
|
||||
args.append('--page-breaks-before=/')
|
||||
if data['page_numbers']:
|
||||
args.append('--pdf-page-numbers')
|
||||
for edge in 'left top right bottom'.split():
|
||||
args.append('--pdf-page-margin-' + edge), args.append('%.1f' % (data['margin_' + edge] * 72))
|
||||
from calibre.ebooks.conversion.cli import main
|
||||
main(args)
|
||||
|
||||
|
||||
class Printing(QProgressDialog):
|
||||
|
||||
def __init__(self, thread, show_file, parent=None):
|
||||
QProgressDialog.__init__(self, _('Printing, this will take a while, please wait...'), _('&Cancel'), 0, 0, parent)
|
||||
self.show_file = show_file
|
||||
self.setWindowTitle(_('Printing...'))
|
||||
self.setWindowIcon(QIcon(I('print.png')))
|
||||
self.thread = thread
|
||||
self.timer = t = QTimer(self)
|
||||
t.timeout.connect(self.check)
|
||||
self.canceled.connect(self.do_cancel)
|
||||
t.start(100)
|
||||
|
||||
def check(self):
|
||||
if self.thread.is_alive():
|
||||
return
|
||||
if self.thread.tb or self.thread.log:
|
||||
error_dialog(self, _('Failed to convert to PDF'), _(
|
||||
'Failed to generate PDF file, click "Show details" for more information.'), det_msg=self.thread.tb or self.thread.log, show=True)
|
||||
else:
|
||||
if self.show_file:
|
||||
open_local_file(self.thread.data['output'])
|
||||
self.accept()
|
||||
|
||||
def do_cancel(self):
|
||||
if hasattr(self.thread, 'worker'):
|
||||
try:
|
||||
if self.thread.worker.poll() is None:
|
||||
self.thread.worker.kill()
|
||||
except EnvironmentError:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.timer.stop()
|
||||
self.reject()
|
||||
|
||||
|
||||
def print_book(path_to_book, parent=None, book_title=None):
|
||||
book_title = book_title or os.path.splitext(os.path.basename(path_to_book))[0]
|
||||
d = PrintDialog(book_title, parent)
|
||||
if d.exec_() == d.Accepted:
|
||||
data = d.data
|
||||
data['input'] = path_to_book
|
||||
t = DoPrint(data)
|
||||
t.start()
|
||||
Printing(t, data['show_file'], parent).exec_()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = Application([])
|
||||
print_book(sys.argv[-1])
|
||||
del app
|
@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt5.Qt import (QDialog, QDialogButtonBox, QVBoxLayout, QApplication,
|
||||
QSize, QIcon, Qt)
|
||||
from PyQt5.QtWebKitWidgets import QWebView
|
||||
|
||||
from calibre.gui2 import gprefs, error_dialog
|
||||
|
||||
|
||||
class TableView(QDialog):
|
||||
|
||||
def __init__(self, parent, font_magnification_step):
|
||||
QDialog.__init__(self, parent)
|
||||
self.font_magnification_step = font_magnification_step
|
||||
dw = QApplication.instance().desktop()
|
||||
self.avail_geom = dw.availableGeometry(parent)
|
||||
|
||||
self.view = QWebView(self)
|
||||
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
self.zi_button = zi = bb.addButton(_('Zoom &in'), bb.ActionRole)
|
||||
self.zo_button = zo = bb.addButton(_('Zoom &out'), bb.ActionRole)
|
||||
zi.setIcon(QIcon(I('plus.png')))
|
||||
zo.setIcon(QIcon(I('minus.png')))
|
||||
zi.clicked.connect(self.zoom_in)
|
||||
zo.clicked.connect(self.zoom_out)
|
||||
|
||||
self.l = l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
l.addWidget(self.view)
|
||||
l.addWidget(bb)
|
||||
|
||||
def zoom_in(self):
|
||||
self.view.setZoomFactor(self.view.zoomFactor() +
|
||||
self.font_magnification_step)
|
||||
|
||||
def zoom_out(self):
|
||||
self.view.setZoomFactor(max(0.1, self.view.zoomFactor() - self.font_magnification_step))
|
||||
|
||||
def __call__(self, html, baseurl):
|
||||
self.view.setHtml(
|
||||
'<!DOCTYPE html><html><body bgcolor="white">%s<body></html>'%html,
|
||||
baseurl)
|
||||
geom = self.avail_geom
|
||||
self.resize(QSize(int(geom.width()/2.5), geom.height()-50))
|
||||
geom = gprefs.get('viewer_table_popup_geometry', None)
|
||||
if geom is not None:
|
||||
self.restoreGeometry(geom)
|
||||
self.setWindowTitle(_('View table'))
|
||||
self.show()
|
||||
|
||||
def done(self, e):
|
||||
gprefs['viewer_table_popup_geometry'] = bytearray(self.saveGeometry())
|
||||
return QDialog.done(self, e)
|
||||
|
||||
|
||||
class TablePopup(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.dialogs = []
|
||||
|
||||
def __call__(self, html, baseurl, font_magnification_step):
|
||||
if not html:
|
||||
return error_dialog(self.parent, _('No table found'),
|
||||
_('No table was found'), show=True)
|
||||
d = TableView(self.parent, font_magnification_step)
|
||||
self.dialogs.append(d)
|
||||
d.finished.connect(self.cleanup, type=Qt.QueuedConnection)
|
||||
d(html, baseurl)
|
||||
|
||||
def cleanup(self):
|
||||
for d in tuple(self.dialogs):
|
||||
if not d.isVisible():
|
||||
self.dialogs.remove(d)
|
@ -1,411 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QStandardItem, QStandardItemModel, Qt, QFont, QTreeView, QWidget,
|
||||
QHBoxLayout, QToolButton, QIcon, QModelIndex, pyqtSignal, QMenu,
|
||||
QStyledItemDelegate, QToolTip, QApplication)
|
||||
|
||||
from calibre.ebooks.metadata.toc import TOC as MTOC
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.search_box import SearchBox2
|
||||
from calibre.utils.icu import primary_contains
|
||||
from polyglot.builtins import iteritems
|
||||
|
||||
|
||||
class Delegate(QStyledItemDelegate):
|
||||
|
||||
def helpEvent(self, ev, view, option, index):
|
||||
# Show a tooltip only if the item is truncated
|
||||
if not ev or not view:
|
||||
return False
|
||||
if ev.type() == ev.ToolTip:
|
||||
rect = view.visualRect(index)
|
||||
size = self.sizeHint(option, index)
|
||||
if rect.width() < size.width():
|
||||
tooltip = index.data(Qt.DisplayRole)
|
||||
QToolTip.showText(ev.globalPos(), tooltip, view)
|
||||
return True
|
||||
return QStyledItemDelegate.helpEvent(self, ev, view, option, index)
|
||||
|
||||
|
||||
class TOCView(QTreeView):
|
||||
|
||||
searched = pyqtSignal(object)
|
||||
|
||||
def __init__(self, *args):
|
||||
QTreeView.__init__(self, *args)
|
||||
self.delegate = Delegate(self)
|
||||
self.setItemDelegate(self.delegate)
|
||||
self.setMinimumWidth(80)
|
||||
self.header().close()
|
||||
self.setMouseTracking(True)
|
||||
self.setStyleSheet('''
|
||||
QTreeView {
|
||||
background-color: palette(window);
|
||||
color: palette(window-text);
|
||||
border: none;
|
||||
}
|
||||
|
||||
QTreeView::item {
|
||||
border: 1px solid transparent;
|
||||
padding-top:0.5ex;
|
||||
padding-bottom:0.5ex;
|
||||
}
|
||||
|
||||
QTreeView::item:hover {
|
||||
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
|
||||
border: 1px solid #bfcde4;
|
||||
border-radius: 6px;
|
||||
}
|
||||
''')
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.context_menu)
|
||||
|
||||
def mouseMoveEvent(self, ev):
|
||||
if self.indexAt(ev.pos()).isValid():
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
else:
|
||||
self.unsetCursor()
|
||||
return QTreeView.mouseMoveEvent(self, ev)
|
||||
|
||||
def expand_tree(self, index):
|
||||
self.expand(index)
|
||||
i = -1
|
||||
while True:
|
||||
i += 1
|
||||
child = index.child(i, 0)
|
||||
if not child.isValid():
|
||||
break
|
||||
self.expand_tree(child)
|
||||
|
||||
def context_menu(self, pos):
|
||||
index = self.indexAt(pos)
|
||||
m = QMenu(self)
|
||||
if index.isValid():
|
||||
m.addAction(_('Expand all items under %s') % index.data(), partial(self.expand_tree, index))
|
||||
m.addSeparator()
|
||||
m.addAction(_('Expand all items'), self.expandAll)
|
||||
m.addAction(_('Collapse all items'), self.collapseAll)
|
||||
m.addSeparator()
|
||||
m.addAction(_('Copy table of contents to clipboard'), self.copy_to_clipboard)
|
||||
m.exec_(self.mapToGlobal(pos))
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
try:
|
||||
if self.handle_shortcuts(event):
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
return QTreeView.keyPressEvent(self, event)
|
||||
|
||||
def copy_to_clipboard(self):
|
||||
m = self.model()
|
||||
QApplication.clipboard().setText(getattr(m, 'as_plain_text', ''))
|
||||
|
||||
|
||||
class TOCSearch(QWidget):
|
||||
|
||||
def __init__(self, toc_view, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.toc_view = toc_view
|
||||
self.l = l = QHBoxLayout(self)
|
||||
self.search = s = SearchBox2(self)
|
||||
self.search.setMinimumContentsLength(15)
|
||||
self.search.initialize('viewer_toc_search_history', help_text=_('Search Table of Contents'))
|
||||
self.search.setToolTip(_('Search for text in the Table of Contents'))
|
||||
s.search.connect(self.do_search)
|
||||
self.go = b = QToolButton(self)
|
||||
b.setIcon(QIcon(I('search.png')))
|
||||
b.clicked.connect(s.do_search)
|
||||
b.setToolTip(_('Find next match'))
|
||||
l.addWidget(s), l.addWidget(b)
|
||||
|
||||
def do_search(self, text):
|
||||
if not text or not text.strip():
|
||||
return
|
||||
index = self.toc_view.model().search(text)
|
||||
if index.isValid():
|
||||
self.toc_view.searched.emit(index)
|
||||
else:
|
||||
error_dialog(self.toc_view, _('No matches found'), _(
|
||||
'There are no Table of Contents entries matching: %s') % text, show=True)
|
||||
self.search.search_done(True)
|
||||
|
||||
|
||||
class TOCItem(QStandardItem):
|
||||
|
||||
def __init__(self, spine, toc, depth, all_items, parent=None):
|
||||
text = toc.text
|
||||
if text:
|
||||
text = re.sub(r'\s', ' ', text)
|
||||
self.title = text
|
||||
self.parent = parent
|
||||
self.href = toc.href
|
||||
QStandardItem.__init__(self, text if text else '')
|
||||
self.abspath = toc.abspath if toc.href else None
|
||||
self.fragment = toc.fragment
|
||||
all_items.append(self)
|
||||
self.emphasis_font = QFont(self.font())
|
||||
self.emphasis_font.setBold(True), self.emphasis_font.setItalic(True)
|
||||
self.normal_font = self.font()
|
||||
for t in toc:
|
||||
self.appendRow(TOCItem(spine, t, depth+1, all_items, parent=self))
|
||||
self.setFlags(Qt.ItemIsEnabled)
|
||||
self.is_current_search_result = False
|
||||
spos = 0
|
||||
for i, si in enumerate(spine):
|
||||
if si == self.abspath:
|
||||
spos = i
|
||||
break
|
||||
am = {}
|
||||
if self.abspath is not None:
|
||||
try:
|
||||
am = getattr(spine[i], 'anchor_map', {})
|
||||
except UnboundLocalError:
|
||||
# Spine was empty?
|
||||
pass
|
||||
frag = self.fragment if (self.fragment and self.fragment in am) else None
|
||||
self.starts_at = spos
|
||||
self.start_anchor = frag
|
||||
self.start_src_offset = am.get(frag, 0)
|
||||
self.depth = depth
|
||||
self.is_being_viewed = False
|
||||
|
||||
@property
|
||||
def ancestors(self):
|
||||
parent = self.parent
|
||||
while parent is not None:
|
||||
yield parent
|
||||
parent = parent.parent
|
||||
|
||||
@classmethod
|
||||
def type(cls):
|
||||
return QStandardItem.UserType+10
|
||||
|
||||
def update_indexing_state(self, spine_index, viewport_rect, anchor_map,
|
||||
in_paged_mode):
|
||||
if in_paged_mode:
|
||||
self.update_indexing_state_paged(spine_index, viewport_rect,
|
||||
anchor_map)
|
||||
else:
|
||||
self.update_indexing_state_unpaged(spine_index, viewport_rect,
|
||||
anchor_map)
|
||||
|
||||
def update_indexing_state_unpaged(self, spine_index, viewport_rect,
|
||||
anchor_map):
|
||||
is_being_viewed = False
|
||||
top, bottom = viewport_rect[1], viewport_rect[3]
|
||||
# We use bottom-25 in the checks below to account for the case where
|
||||
# the next entry has some invisible margin that just overlaps with the
|
||||
# bottom of the screen. In this case it will appear to the user that
|
||||
# the entry is not visible on the screen. Of course, the margin could
|
||||
# be larger than 25, but that's a decent compromise. Also we dont want
|
||||
# to count a partial line as being visible.
|
||||
|
||||
# We only care about y position
|
||||
anchor_map = {k:v[1] for k, v in iteritems(anchor_map)}
|
||||
|
||||
if spine_index >= self.starts_at and spine_index <= self.ends_at:
|
||||
# The position at which this anchor is present in the document
|
||||
start_pos = anchor_map.get(self.start_anchor, 0)
|
||||
psp = []
|
||||
if self.ends_at == spine_index:
|
||||
# Anchors that could possibly indicate the start of the next
|
||||
# section and therefore the end of this section.
|
||||
# self.possible_end_anchors is a set of anchors belonging to
|
||||
# toc entries with depth <= self.depth that are also not
|
||||
# ancestors of this entry.
|
||||
psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors]
|
||||
psp = [x for x in psp if x >= start_pos]
|
||||
# The end position. The first anchor whose pos is >= start_pos
|
||||
# or if the end is not in this spine item, we set it to the bottom
|
||||
# of the window +1
|
||||
end_pos = min(psp) if psp else (bottom+1 if self.ends_at >=
|
||||
spine_index else 0)
|
||||
if spine_index > self.starts_at and spine_index < self.ends_at:
|
||||
# The entire spine item is contained in this entry
|
||||
is_being_viewed = True
|
||||
elif (spine_index == self.starts_at and bottom-25 >= start_pos and
|
||||
# This spine item contains the start
|
||||
# The start position is before the end of the viewport
|
||||
(spine_index != self.ends_at or top < end_pos)):
|
||||
# The end position is after the start of the viewport
|
||||
is_being_viewed = True
|
||||
elif (spine_index == self.ends_at and top < end_pos and
|
||||
# This spine item contains the end
|
||||
# The end position is after the start of the viewport
|
||||
(spine_index != self.starts_at or bottom-25 >= start_pos)):
|
||||
# The start position is before the end of the viewport
|
||||
is_being_viewed = True
|
||||
|
||||
changed = is_being_viewed != self.is_being_viewed
|
||||
self.is_being_viewed = is_being_viewed
|
||||
if changed:
|
||||
self.setFont(self.emphasis_font if is_being_viewed else self.normal_font)
|
||||
|
||||
def update_indexing_state_paged(self, spine_index, viewport_rect,
|
||||
anchor_map):
|
||||
is_being_viewed = False
|
||||
|
||||
left, right = viewport_rect[0], viewport_rect[2]
|
||||
left, right = (left, 0), (right, -1)
|
||||
|
||||
if spine_index >= self.starts_at and spine_index <= self.ends_at:
|
||||
# The position at which this anchor is present in the document
|
||||
start_pos = anchor_map.get(self.start_anchor, (0, 0))
|
||||
psp = []
|
||||
if self.ends_at == spine_index:
|
||||
# Anchors that could possibly indicate the start of the next
|
||||
# section and therefore the end of this section.
|
||||
# self.possible_end_anchors is a set of anchors belonging to
|
||||
# toc entries with depth <= self.depth that are also not
|
||||
# ancestors of this entry.
|
||||
psp = [anchor_map.get(x, (0, 0)) for x in self.possible_end_anchors]
|
||||
psp = [x for x in psp if x >= start_pos]
|
||||
# The end position. The first anchor whose pos is >= start_pos
|
||||
# or if the end is not in this spine item, we set it to the column
|
||||
# after the right edge of the viewport
|
||||
end_pos = min(psp) if psp else (right if self.ends_at >=
|
||||
spine_index else (0, 0))
|
||||
if spine_index > self.starts_at and spine_index < self.ends_at:
|
||||
# The entire spine item is contained in this entry
|
||||
is_being_viewed = True
|
||||
elif (spine_index == self.starts_at and right > start_pos and
|
||||
# This spine item contains the start
|
||||
# The start position is before the end of the viewport
|
||||
(spine_index != self.ends_at or left < end_pos)):
|
||||
# The end position is after the start of the viewport
|
||||
is_being_viewed = True
|
||||
elif (spine_index == self.ends_at and left < end_pos and
|
||||
# This spine item contains the end
|
||||
# The end position is after the start of the viewport
|
||||
(spine_index != self.starts_at or right > start_pos)):
|
||||
# The start position is before the end of the viewport
|
||||
is_being_viewed = True
|
||||
|
||||
changed = is_being_viewed != self.is_being_viewed
|
||||
self.is_being_viewed = is_being_viewed
|
||||
if changed:
|
||||
self.setFont(self.emphasis_font if is_being_viewed else self.normal_font)
|
||||
|
||||
def set_current_search_result(self, yes):
|
||||
if yes and not self.is_current_search_result:
|
||||
self.setText(self.text() + ' ◄')
|
||||
self.is_current_search_result = True
|
||||
elif not yes and self.is_current_search_result:
|
||||
self.setText(self.text()[:-2])
|
||||
self.is_current_search_result = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment)
|
||||
|
||||
def __str__(self):
|
||||
return repr(self)
|
||||
|
||||
|
||||
class TOC(QStandardItemModel):
|
||||
|
||||
def __init__(self, spine, toc=None):
|
||||
QStandardItemModel.__init__(self)
|
||||
self.current_query = {'text':'', 'index':-1, 'items':()}
|
||||
if toc is None:
|
||||
toc = MTOC()
|
||||
self.all_items = depth_first = []
|
||||
for t in toc:
|
||||
self.appendRow(TOCItem(spine, t, 0, depth_first))
|
||||
|
||||
for x in depth_first:
|
||||
possible_enders = [t for t in depth_first if t.depth <= x.depth and
|
||||
t.starts_at >= x.starts_at and t is not x and t not in
|
||||
x.ancestors]
|
||||
if possible_enders:
|
||||
min_spine = min(t.starts_at for t in possible_enders)
|
||||
possible_enders = {t.fragment for t in possible_enders if
|
||||
t.starts_at == min_spine}
|
||||
else:
|
||||
min_spine = len(spine) - 1
|
||||
possible_enders = set()
|
||||
x.ends_at = min_spine
|
||||
x.possible_end_anchors = possible_enders
|
||||
|
||||
self.currently_viewed_entry = None
|
||||
|
||||
def update_indexing_state(self, *args):
|
||||
items_being_viewed = []
|
||||
for t in self.all_items:
|
||||
t.update_indexing_state(*args)
|
||||
if t.is_being_viewed:
|
||||
items_being_viewed.append(t)
|
||||
self.currently_viewed_entry = t
|
||||
return items_being_viewed
|
||||
|
||||
def next_entry(self, spine_pos, anchor_map, viewport_rect, in_paged_mode,
|
||||
backwards=False, current_entry=None):
|
||||
current_entry = (self.currently_viewed_entry if current_entry is None
|
||||
else current_entry)
|
||||
if current_entry is None:
|
||||
return
|
||||
items = reversed(self.all_items) if backwards else self.all_items
|
||||
found = False
|
||||
|
||||
if in_paged_mode:
|
||||
start = viewport_rect[0]
|
||||
anchor_map = {k:v[0] for k, v in iteritems(anchor_map)}
|
||||
else:
|
||||
start = viewport_rect[1]
|
||||
anchor_map = {k:v[1] for k, v in iteritems(anchor_map)}
|
||||
|
||||
for item in items:
|
||||
if found:
|
||||
start_pos = anchor_map.get(item.start_anchor, 0)
|
||||
if backwards and item.is_being_viewed and start_pos >= start:
|
||||
# This item will not cause any scrolling
|
||||
continue
|
||||
if item.starts_at != spine_pos or item.start_anchor:
|
||||
return item
|
||||
if item is current_entry:
|
||||
found = True
|
||||
|
||||
def find_items(self, query):
|
||||
for item in self.all_items:
|
||||
if primary_contains(query, item.text()):
|
||||
yield item
|
||||
|
||||
def find_indices_by_href(self, query):
|
||||
for item in self.all_items:
|
||||
q = (item.href or '')
|
||||
if item.fragment:
|
||||
q += '#' + item.fragment
|
||||
if primary_contains(query, q):
|
||||
yield self.indexFromItem(item)
|
||||
|
||||
def search(self, query):
|
||||
cq = self.current_query
|
||||
if cq['items'] and -1 < cq['index'] < len(cq['items']):
|
||||
cq['items'][cq['index']].set_current_search_result(False)
|
||||
if cq['text'] != query:
|
||||
items = tuple(self.find_items(query))
|
||||
cq.update({'text':query, 'items':items, 'index':-1})
|
||||
if len(cq['items']) > 0:
|
||||
cq['index'] = (cq['index'] + 1) % len(cq['items'])
|
||||
item = cq['items'][cq['index']]
|
||||
item.set_current_search_result(True)
|
||||
index = self.indexFromItem(item)
|
||||
return index
|
||||
return QModelIndex()
|
||||
|
||||
@property
|
||||
def as_plain_text(self):
|
||||
lines = []
|
||||
for item in self.all_items:
|
||||
lines.append(' ' * (4 * item.depth) + (item.title or ''))
|
||||
return '\n'.join(lines)
|
@ -1,425 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import textwrap
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QIcon, QWidget, Qt, QGridLayout, QScrollBar, QToolBar, QAction,
|
||||
QToolButton, QMenu, QDoubleSpinBox, pyqtSignal, QLineEdit,
|
||||
QRegExpValidator, QRegExp, QPalette, QColor, QBrush, QPainter,
|
||||
QDockWidget, QSize, QWebView, QLabel, QVBoxLayout)
|
||||
|
||||
from calibre.gui2 import rating_font, error_dialog, safe_open_url
|
||||
from calibre.gui2.main_window import MainWindow
|
||||
from calibre.gui2.search_box import SearchBox2
|
||||
from calibre.gui2.viewer.documentview import DocumentView
|
||||
from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
|
||||
from calibre.gui2.viewer.toc import TOCView, TOCSearch
|
||||
from calibre.gui2.viewer.footnote import FootnotesView
|
||||
from calibre.utils.localization import is_rtl
|
||||
from polyglot.builtins import unicode_type, range
|
||||
|
||||
|
||||
class DoubleSpinBox(QDoubleSpinBox): # {{{
|
||||
|
||||
value_changed = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
QDoubleSpinBox.__init__(self, *args, **kwargs)
|
||||
self.tt = _('Position in book')
|
||||
self.setToolTip(self.tt)
|
||||
|
||||
def set_value(self, val):
|
||||
self.blockSignals(True)
|
||||
self.setValue(val)
|
||||
try:
|
||||
self.setToolTip(self.tt +
|
||||
' [{0:.0%}]'.format(float(val)/self.maximum()))
|
||||
except ZeroDivisionError:
|
||||
self.setToolTip(self.tt)
|
||||
self.blockSignals(False)
|
||||
self.value_changed.emit(self.value(), self.maximum())
|
||||
# }}}
|
||||
|
||||
|
||||
class Reference(QLineEdit): # {{{
|
||||
|
||||
goto = pyqtSignal(object)
|
||||
|
||||
def __init__(self, *args):
|
||||
QLineEdit.__init__(self, *args)
|
||||
self.setValidator(QRegExpValidator(QRegExp(r'\d+\.\d+'), self))
|
||||
self.setToolTip(textwrap.fill('<p>'+_(
|
||||
'Go to a reference. To get reference numbers, use the <i>reference '
|
||||
'mode</i>, by clicking the reference mode button in the toolbar.')))
|
||||
if hasattr(self, 'setPlaceholderText'):
|
||||
self.setPlaceholderText(_('Go to a reference number...'))
|
||||
self.editingFinished.connect(self.editing_finished)
|
||||
|
||||
def editing_finished(self):
|
||||
text = unicode_type(self.text())
|
||||
self.setText('')
|
||||
self.goto.emit(text)
|
||||
# }}}
|
||||
|
||||
|
||||
class Metadata(QWebView): # {{{
|
||||
|
||||
def __init__(self, parent):
|
||||
QWebView.__init__(self, parent)
|
||||
s = self.settings()
|
||||
s.setAttribute(s.JavascriptEnabled, False)
|
||||
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
|
||||
self.page().linkClicked.connect(self.link_clicked)
|
||||
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
|
||||
palette = self.palette()
|
||||
palette.setBrush(QPalette.Base, Qt.transparent)
|
||||
self.page().setPalette(palette)
|
||||
self.setVisible(False)
|
||||
|
||||
def link_clicked(self, qurl):
|
||||
if qurl.scheme() in ('http', 'https'):
|
||||
return safe_open_url(qurl)
|
||||
|
||||
def update_layout(self):
|
||||
self.setGeometry(0, 0, self.parent().width(), self.parent().height())
|
||||
|
||||
def show_metadata(self, mi, ext=''):
|
||||
from calibre.gui2 import default_author_link
|
||||
from calibre.gui2.book_details import render_html, css
|
||||
from calibre.ebooks.metadata.book.render import mi_to_html
|
||||
|
||||
def render_data(mi, use_roman_numbers=True, all_fields=False, pref_name='book_display_fields'):
|
||||
return mi_to_html(
|
||||
mi, use_roman_numbers=use_roman_numbers, rating_font=rating_font(), rtl=is_rtl(),
|
||||
default_author_link=default_author_link()
|
||||
)
|
||||
|
||||
html = render_html(mi, css(), True, self, render_data_func=render_data)
|
||||
self.setHtml(html)
|
||||
|
||||
def setVisible(self, x):
|
||||
if x:
|
||||
self.update_layout()
|
||||
QWebView.setVisible(self, x)
|
||||
|
||||
def paintEvent(self, ev):
|
||||
p = QPainter(self)
|
||||
p.fillRect(ev.region().boundingRect(), QBrush(QColor(200, 200, 200, 247), Qt.SolidPattern))
|
||||
p.end()
|
||||
QWebView.paintEvent(self, ev)
|
||||
# }}}
|
||||
|
||||
|
||||
class History(list): # {{{
|
||||
|
||||
def __init__(self, action_back=None, action_forward=None):
|
||||
self.action_back = action_back
|
||||
self.action_forward = action_forward
|
||||
super(History, self).__init__(self)
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
del self[:]
|
||||
self.insert_pos = 0
|
||||
self.back_pos = None
|
||||
self.forward_pos = None
|
||||
self.set_actions()
|
||||
|
||||
def set_actions(self):
|
||||
if self.action_back is not None:
|
||||
self.action_back.setDisabled(self.back_pos is None)
|
||||
if self.action_forward is not None:
|
||||
self.action_forward.setDisabled(self.forward_pos is None)
|
||||
|
||||
def back(self, item_when_clicked):
|
||||
# Back clicked
|
||||
if self.back_pos is None:
|
||||
return None
|
||||
item = self[self.back_pos]
|
||||
self.forward_pos = self.back_pos+1
|
||||
if self.forward_pos >= len(self):
|
||||
# We are at the head of the stack, append item to the stack so that
|
||||
# clicking forward again will take us to where we were when we
|
||||
# clicked back
|
||||
self.append(item_when_clicked)
|
||||
self.forward_pos = len(self) - 1
|
||||
self.insert_pos = self.forward_pos
|
||||
self.back_pos = None if self.back_pos == 0 else self.back_pos - 1
|
||||
self.set_actions()
|
||||
return item
|
||||
|
||||
def forward(self, item_when_clicked):
|
||||
# Forward clicked
|
||||
if self.forward_pos is None:
|
||||
return None
|
||||
item = self[self.forward_pos]
|
||||
self.back_pos = self.forward_pos - 1
|
||||
if self.back_pos < 0:
|
||||
self.back_pos = None
|
||||
self.insert_pos = min(len(self) - 1, (self.back_pos or 0) + 1)
|
||||
self.forward_pos = None if self.forward_pos > len(self) - 2 else self.forward_pos + 1
|
||||
self.set_actions()
|
||||
return item
|
||||
|
||||
def add(self, item):
|
||||
# Link clicked
|
||||
self[self.insert_pos:] = []
|
||||
while self.insert_pos > 0 and self[self.insert_pos-1] == item:
|
||||
self.insert_pos -= 1
|
||||
self[self.insert_pos:] = []
|
||||
self.insert(self.insert_pos, item)
|
||||
# The next back must go to item
|
||||
self.back_pos = self.insert_pos
|
||||
self.insert_pos += 1
|
||||
# There can be no forward
|
||||
self.forward_pos = None
|
||||
self.set_actions()
|
||||
|
||||
def __str__(self):
|
||||
return 'History: Items=%s back_pos=%s insert_pos=%s forward_pos=%s' % (tuple(self), self.back_pos, self.insert_pos, self.forward_pos)
|
||||
|
||||
|
||||
def test_history():
|
||||
h = History()
|
||||
for i in range(4):
|
||||
h.add(i)
|
||||
for i in reversed(h):
|
||||
h.back(i)
|
||||
h.forward(0)
|
||||
h.add(9)
|
||||
assert h == [0, 9]
|
||||
# }}}
|
||||
|
||||
|
||||
class ToolBar(QToolBar): # {{{
|
||||
|
||||
def contextMenuEvent(self, ev):
|
||||
ac = self.actionAt(ev.pos())
|
||||
if ac is None:
|
||||
return
|
||||
ch = self.widgetForAction(ac)
|
||||
sm = getattr(ch, 'showMenu', None)
|
||||
if callable(sm):
|
||||
ev.accept()
|
||||
sm()
|
||||
# }}}
|
||||
|
||||
|
||||
class Main(MainWindow):
|
||||
|
||||
def __init__(self, debug_javascript):
|
||||
MainWindow.__init__(self, None)
|
||||
self.setWindowTitle(_('E-book viewer'))
|
||||
self.base_window_title = unicode_type(self.windowTitle())
|
||||
self.setObjectName('EbookViewer')
|
||||
self.setWindowIcon(QIcon(I('viewer.png')))
|
||||
self.setDockOptions(self.AnimatedDocks | self.AllowTabbedDocks)
|
||||
|
||||
self.centralwidget = c = QWidget(self)
|
||||
c.setObjectName('centralwidget')
|
||||
self.setCentralWidget(c)
|
||||
self.central_layout = cl = QGridLayout(c)
|
||||
cl.setSpacing(0)
|
||||
c.setLayout(cl), cl.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.view = v = DocumentView(self)
|
||||
v.setMinimumSize(100, 100)
|
||||
self.view.initialize_view(debug_javascript)
|
||||
v.setObjectName('view')
|
||||
cl.addWidget(v)
|
||||
|
||||
self.vertical_scrollbar = vs = QScrollBar(c)
|
||||
vs.setOrientation(Qt.Vertical), vs.setObjectName("vertical_scrollbar")
|
||||
cl.addWidget(vs, 0, 1, 2, 1)
|
||||
|
||||
self.horizontal_scrollbar = hs = QScrollBar(c)
|
||||
hs.setOrientation(Qt.Horizontal), hs.setObjectName("horizontal_scrollbar")
|
||||
cl.addWidget(hs, 1, 0, 1, 1)
|
||||
|
||||
self.tool_bar = tb = ToolBar(self)
|
||||
tb.setObjectName('tool_bar'), tb.setIconSize(QSize(32, 32))
|
||||
self.addToolBar(Qt.LeftToolBarArea, tb)
|
||||
|
||||
self.tool_bar2 = tb2 = QToolBar(self)
|
||||
tb2.setObjectName('tool_bar2')
|
||||
self.addToolBar(Qt.TopToolBarArea, tb2)
|
||||
self.tool_bar.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||
self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
|
||||
self.pos = DoubleSpinBox()
|
||||
self.pos.setDecimals(1)
|
||||
self.pos.setSuffix('/'+_('Unknown')+' ')
|
||||
self.pos.setMinimum(1.)
|
||||
self.tool_bar2.addWidget(self.pos)
|
||||
self.tool_bar2.addSeparator()
|
||||
self.reference = Reference()
|
||||
self.tool_bar2.addWidget(self.reference)
|
||||
self.tool_bar2.addSeparator()
|
||||
self.search = SearchBox2(self)
|
||||
self.search.setMinimumContentsLength(20)
|
||||
self.search.initialize('viewer_search_history')
|
||||
self.search.setToolTip(_('Search for text in book'))
|
||||
self.search.setMinimumWidth(200)
|
||||
self.tool_bar2.addWidget(self.search)
|
||||
|
||||
self.toc_dock = d = QDockWidget(_('Table of Contents'), self)
|
||||
d.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.toc_container = w = QWidget(self)
|
||||
w.l = QVBoxLayout(w)
|
||||
self.toc = TOCView(w)
|
||||
self.toc_search = TOCSearch(self.toc, parent=w)
|
||||
w.l.addWidget(self.toc), w.l.addWidget(self.toc_search), w.l.setContentsMargins(0, 0, 0, 0)
|
||||
d.setObjectName('toc-dock')
|
||||
d.setWidget(w)
|
||||
d.close() # starts out hidden
|
||||
self.addDockWidget(Qt.LeftDockWidgetArea, d)
|
||||
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
|
||||
|
||||
self.bookmarks_dock = d = QDockWidget(_('Bookmarks'), self)
|
||||
d.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.bookmarks = BookmarkManager(self)
|
||||
d.setObjectName('bookmarks-dock')
|
||||
d.setWidget(self.bookmarks)
|
||||
d.close() # starts out hidden
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, d)
|
||||
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
|
||||
|
||||
self.footnotes_dock = d = QDockWidget(_('Footnotes'), self)
|
||||
d.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.footnotes_view = FootnotesView(self)
|
||||
self.footnotes_view.follow_link.connect(self.view.follow_footnote_link)
|
||||
self.footnotes_view.close_view.connect(d.close)
|
||||
self.view.footnotes.set_footnotes_view(self.footnotes_view)
|
||||
d.setObjectName('footnotes-dock')
|
||||
d.setWidget(self.footnotes_view)
|
||||
d.close() # starts out hidden
|
||||
self.addDockWidget(Qt.BottomDockWidgetArea, d)
|
||||
d.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea | Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
|
||||
|
||||
self.create_actions()
|
||||
self.themes_menu.aboutToShow.connect(self.themes_menu_shown, type=Qt.QueuedConnection)
|
||||
|
||||
self.metadata = Metadata(self.centralwidget)
|
||||
self.history = History(self.action_back, self.action_forward)
|
||||
|
||||
self.full_screen_label = QLabel('''
|
||||
<center>
|
||||
<h1>%s</h1>
|
||||
<h3>%s</h3>
|
||||
<h3>%s</h3>
|
||||
<h3>%s</h3>
|
||||
</center>
|
||||
'''%(_('Full screen mode'),
|
||||
_('Right click to show controls'),
|
||||
_('Tap in the left or right page margin to turn pages'),
|
||||
_('Press Esc to quit')),
|
||||
self.centralWidget())
|
||||
self.full_screen_label.setVisible(False)
|
||||
self.full_screen_label.final_height = 200
|
||||
self.full_screen_label.setFocusPolicy(Qt.NoFocus)
|
||||
self.full_screen_label.setStyleSheet('''
|
||||
QLabel {
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
color: black;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 20px;
|
||||
}
|
||||
''')
|
||||
self.clock_label = QLabel('99:99', self.centralWidget())
|
||||
self.clock_label.setVisible(False)
|
||||
self.clock_label.setFocusPolicy(Qt.NoFocus)
|
||||
self.info_label_style = '''
|
||||
QLabel {
|
||||
text-align: center;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 8px;
|
||||
background-color: %s;
|
||||
color: %s;
|
||||
font-family: monospace;
|
||||
font-size: larger;
|
||||
padding: 5px;
|
||||
}'''
|
||||
self.pos_label = QLabel('2000/4000', self.centralWidget())
|
||||
self.pos_label.setVisible(False)
|
||||
self.pos_label.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
self.resize(653, 746)
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
if self.metadata.isVisible():
|
||||
self.metadata.update_layout()
|
||||
return MainWindow.resizeEvent(self, ev)
|
||||
|
||||
def initialize_dock_state(self):
|
||||
self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
|
||||
self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
|
||||
self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)
|
||||
self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)
|
||||
self.footnotes_dock.close()
|
||||
|
||||
def themes_menu_shown(self):
|
||||
if len(self.themes_menu.actions()) == 0:
|
||||
self.themes_menu.hide()
|
||||
error_dialog(self, _('No themes'), _(
|
||||
'You must first create some themes in the viewer preferences'), show=True)
|
||||
|
||||
def create_actions(self):
|
||||
def a(name, text, icon, tb=None, sc_name=None, menu_name=None, popup_mode=QToolButton.MenuButtonPopup):
|
||||
name = 'action_' + name
|
||||
if isinstance(text, QDockWidget):
|
||||
ac = text.toggleViewAction()
|
||||
ac.setIcon(QIcon(I(icon)))
|
||||
else:
|
||||
ac = QAction(QIcon(I(icon)), text, self)
|
||||
setattr(self, name, ac)
|
||||
ac.setObjectName(name)
|
||||
(tb or self.tool_bar).addAction(ac)
|
||||
if sc_name:
|
||||
ac.setToolTip(unicode_type(ac.text()) + (' [%s]' % _(' or ').join(self.view.shortcuts.get_shortcuts(sc_name))))
|
||||
if menu_name is not None:
|
||||
menu_name += '_menu'
|
||||
m = QMenu()
|
||||
setattr(self, menu_name, m)
|
||||
ac.setMenu(m)
|
||||
w = (tb or self.tool_bar).widgetForAction(ac)
|
||||
w.setPopupMode(popup_mode)
|
||||
return ac
|
||||
|
||||
a('back', _('Back'), 'back.png')
|
||||
a('forward', _('Forward'), 'forward.png')
|
||||
self.tool_bar.addSeparator()
|
||||
|
||||
a('open_ebook', _('Open e-book'), 'document_open.png', menu_name='open_history')
|
||||
a('copy', _('Copy to clipboard'), 'edit-copy.png').setDisabled(True)
|
||||
a('font_size_larger', _('Increase font size'), 'font_size_larger.png')
|
||||
a('font_size_smaller', _('Decrease font size'), 'font_size_smaller.png')
|
||||
a('table_of_contents', self.toc_dock, 'toc.png', sc_name='Table of Contents')
|
||||
a('full_screen', _('Toggle full screen'), 'page.png', sc_name='Fullscreen').setCheckable(True)
|
||||
self.tool_bar.addSeparator()
|
||||
|
||||
a('previous_page', _('Previous page'), 'previous.png')
|
||||
a('next_page', _('Next page'), 'next.png')
|
||||
self.tool_bar.addSeparator()
|
||||
|
||||
a('bookmark', _('Bookmark'), 'bookmarks.png', menu_name='bookmarks', popup_mode=QToolButton.InstantPopup)
|
||||
a('reference_mode', _('Reference mode'), 'lookfeel.png').setCheckable(True)
|
||||
self.tool_bar.addSeparator()
|
||||
|
||||
a('preferences', _('Preferences'), 'config.png')
|
||||
a('metadata', _('Show book metadata'), 'metadata.png').setCheckable(True)
|
||||
a('load_theme', _('Load a theme'), 'wizard.png', menu_name='themes', popup_mode=QToolButton.InstantPopup)
|
||||
self.tool_bar.addSeparator()
|
||||
|
||||
a('print', _('Print to PDF file'), 'print.png')
|
||||
|
||||
a('find_next', _('Find next occurrence'), 'arrow-down.png', tb=self.tool_bar2)
|
||||
a('find_previous', _('Find previous occurrence'), 'arrow-up.png', tb=self.tool_bar2)
|
||||
a('toggle_paged_mode', _('Toggle paged mode'), 'scroll.png', tb=self.tool_bar2).setCheckable(True)
|
@ -10,7 +10,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import sys, os
|
||||
|
||||
from calibre import config_dir
|
||||
from polyglot.builtins import builtins, itervalues
|
||||
from polyglot.builtins import builtins
|
||||
|
||||
|
||||
class PathResolver(object):
|
||||
@ -83,72 +83,5 @@ def get_image_path(path, data=False, allow_user_override=True):
|
||||
return get_path('images/'+path, data=data, allow_user_override=allow_user_override)
|
||||
|
||||
|
||||
def js_name_to_path(name, ext='.coffee'):
|
||||
path = ('/'.join(name.split('.'))) + ext
|
||||
d = os.path.dirname
|
||||
base = d(d(os.path.abspath(__file__)))
|
||||
return os.path.join(base, path)
|
||||
|
||||
|
||||
def _compile_coffeescript(name):
|
||||
from calibre.utils.serve_coffee import compile_coffeescript
|
||||
src = js_name_to_path(name)
|
||||
with open(src, 'rb') as f:
|
||||
cs, errors = compile_coffeescript(f.read(), src)
|
||||
if errors:
|
||||
for line in errors:
|
||||
print(line)
|
||||
raise Exception('Failed to compile coffeescript'
|
||||
': %s'%src)
|
||||
return cs
|
||||
|
||||
|
||||
def compiled_coffeescript(name, dynamic=False):
|
||||
import zipfile
|
||||
zipf = get_path('compiled_coffeescript.zip', allow_user_override=False)
|
||||
with zipfile.ZipFile(zipf, 'r') as zf:
|
||||
if dynamic:
|
||||
import json
|
||||
existing_hash = json.loads(zf.comment or '{}').get(name + '.js')
|
||||
if existing_hash is not None:
|
||||
import hashlib
|
||||
with open(js_name_to_path(name), 'rb') as f:
|
||||
if existing_hash == hashlib.sha1(f.read()).hexdigest():
|
||||
return zf.read(name + '.js')
|
||||
return _compile_coffeescript(name)
|
||||
else:
|
||||
return zf.read(name+'.js')
|
||||
|
||||
|
||||
def load_hyphenator_dicts(hp_cache, lang, default_lang='en'):
|
||||
from calibre.utils.localization import lang_as_iso639_1
|
||||
import zipfile
|
||||
if not lang:
|
||||
lang = default_lang or 'en'
|
||||
|
||||
def lang_name(l):
|
||||
l = l.lower()
|
||||
l = lang_as_iso639_1(l)
|
||||
if not l:
|
||||
l = 'en'
|
||||
l = {'en':'en-us', 'nb':'nb-no', 'el':'el-monoton'}.get(l, l)
|
||||
return l.lower().replace('_', '-')
|
||||
|
||||
if not hp_cache:
|
||||
with zipfile.ZipFile(P('viewer/hyphenate/patterns.zip',
|
||||
allow_user_override=False), 'r') as zf:
|
||||
for pat in zf.namelist():
|
||||
raw = zf.read(pat).decode('utf-8')
|
||||
hp_cache[pat.partition('.')[0]] = raw
|
||||
|
||||
if lang_name(lang) not in hp_cache:
|
||||
lang = lang_name(default_lang)
|
||||
|
||||
lang = lang_name(lang)
|
||||
|
||||
js = '\n\n'.join(itervalues(hp_cache))
|
||||
return js, lang
|
||||
|
||||
|
||||
builtins.__dict__['P'] = get_path
|
||||
builtins.__dict__['I'] = get_image_path
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user