')
+ ]
diff --git a/resources/content_server/read/monocle.js b/resources/content_server/read/monocle.js
index 8d58ba8a06..560e4af641 100644
--- a/resources/content_server/read/monocle.js
+++ b/resources/content_server/read/monocle.js
@@ -1,5 +1,5 @@
Monocle = {
- VERSION: "1.0.0"
+ VERSION: "2.0.0"
};
@@ -170,7 +170,8 @@ Monocle.Browser.has.iframeTouchBug = Monocle.Browser.iOSVersionBelow("4.2");
Monocle.Browser.has.selectThruBug = Monocle.Browser.iOSVersionBelow("4.2");
Monocle.Browser.has.mustScrollSheaf = Monocle.Browser.is.MobileSafari;
-Monocle.Browser.has.iframeDoubleWidthBug = Monocle.Browser.has.mustScrollSheaf;
+Monocle.Browser.has.iframeDoubleWidthBug =
+ Monocle.Browser.has.mustScrollSheaf || Monocle.Browser.on.Kindle3;
Monocle.Browser.has.floatColumnBug = Monocle.Browser.is.WebKit;
@@ -181,6 +182,11 @@ Monocle.Browser.has.jumpFlickerBug =
Monocle.Browser.on.MacOSX && Monocle.Browser.is.WebKit;
+Monocle.Browser.has.columnOverflowPaintBug = Monocle.Browser.is.WebKit &&
+ !Monocle.Browser.is.MobileSafari &&
+ navigator.userAgent.indexOf("AppleWebKit/534") > 0;
+
+
if (typeof window.console == "undefined") {
window.console = {
messages: [],
@@ -241,6 +247,7 @@ Monocle.Factory = function (element, label, index, reader) {
function initialize() {
+ if (!p.label) { return; }
var node = p.reader.properties.graph;
node[p.label] = node[p.label] || [];
if (typeof p.index == 'undefined' && node[p.label][p.index]) {
@@ -274,7 +281,11 @@ Monocle.Factory = function (element, label, index, reader) {
function make(tagName, oLabel, index_or_options, or_options) {
var oIndex, options;
- if (arguments.length == 2) {
+ if (arguments.length == 1) {
+ oLabel = null,
+ oIndex = 0;
+ options = {};
+ } else if (arguments.length == 2) {
oIndex = 0;
options = {};
} else if (arguments.length == 4) {
@@ -376,6 +387,22 @@ Monocle.pieceLoaded('factory');
Monocle.Events = {}
+Monocle.Events.dispatch = function (elem, evtType, data, cancelable) {
+ if (!document.createEvent) {
+ return true;
+ }
+ var evt = document.createEvent("Events");
+ evt.initEvent(evtType, false, cancelable || false);
+ evt.m = data;
+ try {
+ return elem.dispatchEvent(evt);
+ } catch(e) {
+ console.warn("Failed to dispatch event: "+evtType);
+ return false;
+ }
+}
+
+
Monocle.Events.listen = function (elem, evtType, fn, useCapture) {
if (elem.addEventListener) {
return elem.addEventListener(evtType, fn, useCapture || false);
@@ -405,7 +432,7 @@ Monocle.Events.listenForContact = function (elem, fns, options) {
pageY: ci.pageY
};
- var target = evt.target || window.srcElement;
+ var target = evt.target || evt.srcElement;
while (target.nodeType != 1 && target.parentNode) {
target = target.parentNode;
}
@@ -527,13 +554,18 @@ Monocle.Events.deafenForContact = function (elem, listeners) {
}
-Monocle.Events.listenForTap = function (elem, fn) {
+Monocle.Events.listenForTap = function (elem, fn, activeClass) {
var startPos;
if (Monocle.Browser.on.Kindle3) {
Monocle.Events.listen(elem, 'click', function () {});
}
+ var annul = function () {
+ startPos = null;
+ if (activeClass && elem.dom) { elem.dom.removeClass(activeClass); }
+ }
+
var annulIfOutOfBounds = function (evt) {
if (evt.type.match(/^mouse/)) {
return;
@@ -545,7 +577,7 @@ Monocle.Events.listenForTap = function (elem, fn) {
evt.m.registrantX < 0 || evt.m.registrantX > elem.offsetWidth ||
evt.m.registrantY < 0 || evt.m.registrantY > elem.offsetHeight
) {
- startPos = null;
+ annul();
} else {
evt.preventDefault();
}
@@ -557,6 +589,7 @@ Monocle.Events.listenForTap = function (elem, fn) {
start: function (evt) {
startPos = [evt.m.pageX, evt.m.pageY];
evt.preventDefault();
+ if (activeClass && elem.dom) { elem.dom.addClass(activeClass); }
},
move: annulIfOutOfBounds,
end: function (evt) {
@@ -565,10 +598,9 @@ Monocle.Events.listenForTap = function (elem, fn) {
evt.m.startOffset = startPos;
fn(evt);
}
+ annul();
},
- cancel: function (evt) {
- startPos = null;
- }
+ cancel: annul
},
{
useCapture: false
@@ -997,6 +1029,9 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) {
createReaderElements();
p.defaultStyles = addPageStyles(k.DEFAULT_STYLE_RULES, false);
+ if (options.stylesheet) {
+ p.initialStyles = addPageStyles(options.stylesheet, false);
+ }
primeFrames(options.primeURL, function () {
applyStyles();
@@ -1077,6 +1112,7 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) {
if (Monocle.Browser.is.WebKit) {
frame.contentDocument.documentElement.style.overflow = "hidden";
}
+ dispatchEvent('monocle:frameprimed', { frame: frame, pageIndex: pageCount });
if ((pageCount += 1) == pageMax) {
Monocle.defer(callback);
}
@@ -1131,6 +1167,7 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) {
var pageCount = 0;
if (typeof callback == 'function') {
var watcher = function (evt) {
+ dispatchEvent('monocle:firstcomponentchange', evt.m);
if ((pageCount += 1) == p.flipper.pageCount) {
deafen('monocle:componentchange', watcher);
callback();
@@ -1239,7 +1276,7 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) {
page.appendChild(runner);
ctrlData.elements.push(runner);
}
- } else if (cType == "modal" || cType == "popover") {
+ } else if (cType == "modal" || cType == "popover" || cType == "hud") {
ctrlElem = ctrl.createControlElements(overlay);
overlay.appendChild(ctrlElem);
ctrlData.elements.push(ctrlElem);
@@ -1312,24 +1349,33 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) {
var controlData = dataForControl(ctrl);
if (!controlData) {
console.warn("No data for control: " + ctrl);
- return;
+ return false;
}
- if (controlData.hidden == false) {
- return;
+
+ if (showingControl(ctrl)) {
+ return false;
}
+
+ var overlay = dom.find('overlay');
+ if (controlData.usesOverlay && controlData.controlType != "hud") {
+ for (var i = 0, ii = p.controls.length; i < ii; ++i) {
+ if (p.controls[i].usesOverlay && !p.controls[i].hidden) {
+ return false;
+ }
+ }
+ overlay.style.display = "block";
+ }
+
for (var i = 0; i < controlData.elements.length; ++i) {
controlData.elements[i].style.display = "block";
}
- var overlay = dom.find('overlay');
- if (controlData.usesOverlay) {
- overlay.style.display = "block";
- }
+
if (controlData.controlType == "popover") {
overlay.listeners = Monocle.Events.listenForContact(
overlay,
{
start: function (evt) {
- obj = evt.target || window.event.srcElement;
+ var obj = evt.target || window.event.srcElement;
do {
if (obj == controlData.elements[0]) { return true; }
} while (obj && (obj = obj.parentNode));
@@ -1346,22 +1392,18 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) {
ctrl.properties.hidden = false;
}
dispatchEvent('controlshow', ctrl, false);
+ return true;
+ }
+
+
+ function showingControl(ctrl) {
+ var controlData = dataForControl(ctrl);
+ return controlData.hidden == false;
}
function dispatchEvent(evtType, data, cancelable) {
- if (!document.createEvent) {
- return true;
- }
- var evt = document.createEvent("Events");
- evt.initEvent(evtType, false, cancelable || false);
- evt.m = data;
- try {
- return dom.find('box').dispatchEvent(evt);
- } catch(e) {
- console.warn("Failed to dispatch event: " + evtType);
- return false;
- }
+ return Monocle.Events.dispatch(dom.find('box'), evtType, data, cancelable);
}
@@ -1502,6 +1544,7 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) {
API.addControl = addControl;
API.hideControl = hideControl;
API.showControl = showControl;
+ API.showingControl = showingControl;
API.dispatchEvent = dispatchEvent;
API.listen = listen;
API.deafen = deafen;
@@ -1527,22 +1570,32 @@ Monocle.Reader.DEFAULT_CLASS_PREFIX = 'monelem_'
Monocle.Reader.FLIPPER_DEFAULT_CLASS = "Slider";
Monocle.Reader.FLIPPER_LEGACY_CLASS = "Legacy";
Monocle.Reader.DEFAULT_STYLE_RULES = [
- "html * {" +
+ "html#RS\\:monocle * {" +
+ "-webkit-font-smoothing: subpixel-antialiased;" +
"text-rendering: auto !important;" +
"word-wrap: break-word !important;" +
+ "overflow: visible !important;" +
(Monocle.Browser.has.floatColumnBug ? "float: none !important;" : "") +
- "}" +
- "body {" +
+ "}",
+ "html#RS\\:monocle body {" +
"margin: 0 !important;" +
"padding: 0 !important;" +
"-webkit-text-size-adjust: none;" +
- "}" +
- "table, img {" +
+ "}",
+ "html#RS\\:monocle body * {" +
"max-width: 100% !important;" +
- "max-height: 90% !important;" +
+ "}",
+ "html#RS\\:monocle img, html#RS\\:monocle video, html#RS\\:monocle object {" +
+ "max-height: 95% !important;" +
"}"
]
+if (Monocle.Browser.has.columnOverflowPaintBug) {
+ Monocle.Reader.DEFAULT_STYLE_RULES.push(
+ "::-webkit-scrollbar { width: 0; height: 0; }"
+ )
+}
+
Monocle.pieceLoaded('reader');
/* BOOK */
@@ -1586,6 +1639,16 @@ Monocle.Book = function (dataSource) {
locus.load = true;
locus.componentId = p.componentIds[0];
return locus;
+ } else if (
+ cIndex < 0 &&
+ locus.componentId &&
+ currComponent.properties.id != locus.componentId
+ ) {
+ pageDiv.m.reader.dispatchEvent(
+ "monocle:notfound",
+ { href: locus.componentId }
+ );
+ return null;
} else if (cIndex < 0) {
component = currComponent;
locus.componentId = pageDiv.m.activeFrame.m.component.properties.id;
@@ -1619,6 +1682,8 @@ Monocle.Book = function (dataSource) {
result.page += locus.direction;
} else if (typeof(locus.anchor) == "string") {
result.page = component.pageForChapter(locus.anchor, pageDiv);
+ } else if (typeof(locus.xpath) == "string") {
+ result.page = component.pageForXPath(locus.xpath, pageDiv);
} else if (typeof(locus.position) == "string") {
if (locus.position == "start") {
result.page = 1;
@@ -1638,6 +1703,7 @@ Monocle.Book = function (dataSource) {
if (result.page < 1) {
if (cIndex == 0) {
result.page = 1;
+ result.boundarystart = true;
} else {
result.load = true;
result.componentId = p.componentIds[cIndex - 1];
@@ -1647,6 +1713,7 @@ Monocle.Book = function (dataSource) {
} else if (result.page > lastPageNum['new']) {
if (cIndex == p.lastCIndex) {
result.page = lastPageNum['new'];
+ result.boundaryend = true;
} else {
result.load = true;
result.componentId = p.componentIds[cIndex + 1];
@@ -1660,18 +1727,25 @@ Monocle.Book = function (dataSource) {
function setPageAt(pageDiv, locus) {
locus = pageNumberAt(pageDiv, locus);
- if (!locus.load) {
- var component = p.components[p.componentIds.indexOf(locus.componentId)];
- pageDiv.m.place = pageDiv.m.place || new Monocle.Place();
- pageDiv.m.place.setPlace(component, locus.page);
+ if (locus && !locus.load) {
+ var evtData = { locus: locus, page: pageDiv }
+ if (locus.boundarystart) {
+ pageDiv.m.reader.dispatchEvent('monocle:boundarystart', evtData);
+ } else if (locus.boundaryend) {
+ pageDiv.m.reader.dispatchEvent('monocle:boundaryend', evtData);
+ } else {
+ var component = p.components[p.componentIds.indexOf(locus.componentId)];
+ pageDiv.m.place = pageDiv.m.place || new Monocle.Place();
+ pageDiv.m.place.setPlace(component, locus.page);
- var evtData = {
- page: pageDiv,
- locus: locus,
- pageNumber: pageDiv.m.place.pageNumber(),
- componentId: locus.componentId
+ var evtData = {
+ page: pageDiv,
+ locus: locus,
+ pageNumber: pageDiv.m.place.pageNumber(),
+ componentId: locus.componentId
+ }
+ pageDiv.m.reader.dispatchEvent("monocle:pagechange", evtData);
}
- pageDiv.m.reader.dispatchEvent("monocle:pagechange", evtData);
}
return locus;
}
@@ -1683,6 +1757,10 @@ Monocle.Book = function (dataSource) {
locus = pageNumberAt(pageDiv, locus);
}
+ if (!locus) {
+ return;
+ }
+
if (!locus.load) {
callback(locus);
return;
@@ -1690,7 +1768,9 @@ Monocle.Book = function (dataSource) {
var findPageNumber = function () {
locus = setPageAt(pageDiv, locus);
- if (locus.load) {
+ if (!locus) {
+ return;
+ } else if (locus.load) {
loadPageAt(pageDiv, locus, callback, progressCallback)
} else {
callback(locus);
@@ -1715,10 +1795,12 @@ Monocle.Book = function (dataSource) {
}
- function setOrLoadPageAt(pageDiv, locus, callback, progressCallback) {
+ function setOrLoadPageAt(pageDiv, locus, callback, onProgress, onFail) {
locus = setPageAt(pageDiv, locus);
- if (locus.load) {
- loadPageAt(pageDiv, locus, callback, progressCallback);
+ if (!locus) {
+ if (onFail) { onFail(); }
+ } else if (locus.load) {
+ loadPageAt(pageDiv, locus, callback, onProgress);
} else {
callback(locus);
}
@@ -1864,13 +1946,18 @@ Monocle.Place = function () {
}
- function percentageThrough() {
+ function percentAtTopOfPage() {
+ return p.percent - 1.0 / p.component.lastPageNumber();
+ }
+
+
+ function percentAtBottomOfPage() {
return p.percent;
}
- function pageAtPercentageThrough(pc) {
- return Math.max(Math.round(p.component.lastPageNumber() * pc), 1);
+ function pageAtPercentageThrough(percent) {
+ return Math.max(Math.round(p.component.lastPageNumber() * percent), 1);
}
@@ -1911,6 +1998,8 @@ Monocle.Place = function () {
}
if (options.direction) {
locus.page += options.direction;
+ } else {
+ locus.percent = percentAtBottomOfPage();
}
return locus;
}
@@ -1942,7 +2031,9 @@ Monocle.Place = function () {
API.setPlace = setPlace;
API.setPercentageThrough = setPercentageThrough;
API.componentId = componentId;
- API.percentageThrough = percentageThrough;
+ API.percentAtTopOfPage = percentAtTopOfPage;
+ API.percentAtBottomOfPage = percentAtBottomOfPage;
+ API.percentageThrough = percentAtBottomOfPage;
API.pageAtPercentageThrough = pageAtPercentageThrough;
API.pageNumber = pageNumber;
API.chapterInfo = chapterInfo;
@@ -2158,11 +2249,13 @@ Monocle.Component = function (book, id, index, chapters, source) {
if (p.chapters[0] && typeof p.chapters[0].percent == "number") {
return;
}
+ var doc = pageDiv.m.activeFrame.contentDocument;
for (var i = 0; i < p.chapters.length; ++i) {
var chp = p.chapters[i];
chp.percent = 0;
if (chp.fragment) {
- chp.percent = pageDiv.m.dimensions.percentageThroughOfId(chp.fragment);
+ var node = doc.getElementById(chp.fragment);
+ chp.percent = pageDiv.m.dimensions.percentageThroughOfNode(node);
}
}
return p.chapters;
@@ -2187,14 +2280,37 @@ Monocle.Component = function (book, id, index, chapters, source) {
if (!fragment) {
return 1;
}
- var pc2pn = function (pc) { return Math.floor(pc * p.pageLength) + 1 }
for (var i = 0; i < p.chapters.length; ++i) {
if (p.chapters[i].fragment == fragment) {
- return pc2pn(p.chapters[i].percent);
+ return percentToPageNumber(p.chapters[i].percent);
}
}
- var percent = pageDiv.m.dimensions.percentageThroughOfId(fragment);
- return pc2pn(percent);
+ var doc = pageDiv.m.activeFrame.contentDocument;
+ var node = doc.getElementById(fragment);
+ var percent = pageDiv.m.dimensions.percentageThroughOfNode(node);
+ return percentToPageNumber(percent);
+ }
+
+
+ function pageForXPath(xpath, pageDiv) {
+ var doc = pageDiv.m.activeFrame.contentDocument;
+ var percent = 0;
+ if (typeof doc.evaluate == "function") {
+ var node = doc.evaluate(
+ xpath,
+ doc,
+ null,
+ 9,
+ null
+ ).singleNodeValue;
+ var percent = pageDiv.m.dimensions.percentageThroughOfNode(node);
+ }
+ return percentToPageNumber(percent);
+ }
+
+
+ function percentToPageNumber(pc) {
+ return Math.floor(pc * p.pageLength) + 1;
}
@@ -2207,6 +2323,7 @@ Monocle.Component = function (book, id, index, chapters, source) {
API.updateDimensions = updateDimensions;
API.chapterForPage = chapterForPage;
API.pageForChapter = pageForChapter;
+ API.pageForXPath = pageForXPath;
API.lastPageNumber = lastPageNumber;
return API;
@@ -2415,9 +2532,11 @@ Monocle.Dimensions.Vert = function (pageDiv) {
}
- function percentageThroughOfId(id) {
+ function percentageThroughOfNode(target) {
+ if (!target) {
+ return 0;
+ }
var doc = p.page.m.activeFrame.contentDocument;
- var target = doc.getElementById(id);
var offset = 0;
if (target.getBoundingClientRect) {
offset = target.getBoundingClientRect().top;
@@ -2456,7 +2575,7 @@ Monocle.Dimensions.Vert = function (pageDiv) {
API.hasChanged = hasChanged;
API.measure = measure;
API.pages = pages;
- API.percentageThroughOfId = percentageThroughOfId;
+ API.percentageThroughOfNode = percentageThroughOfNode;
API.locusToOffset = locusToOffset;
initialize();
@@ -2713,8 +2832,7 @@ Monocle.Dimensions.Columns = function (pageDiv) {
(!p.measurements) ||
(p.measurements.width != newMeasurements.width) ||
(p.measurements.height != newMeasurements.height) ||
- (p.measurements.scrollWidth != newMeasurements.scrollWidth) ||
- (p.measurements.fontSize != newMeasurements.fontSize)
+ (p.measurements.scrollWidth != newMeasurements.scrollWidth)
);
}
@@ -2736,10 +2854,16 @@ Monocle.Dimensions.Columns = function (pageDiv) {
if (!lc || !lc.getBoundingClientRect) {
console.warn('Empty document for page['+p.page.m.pageIndex+']');
p.measurements.scrollWidth = p.measurements.width;
- } else if (lc.getBoundingClientRect().bottom > p.measurements.height) {
- p.measurements.scrollWidth = p.measurements.width * 2;
} else {
- p.measurements.scrollWidth = p.measurements.width;
+ var bcr = lc.getBoundingClientRect();
+ if (
+ bcr.right > p.measurements.width ||
+ bcr.bottom > p.measurements.height
+ ) {
+ p.measurements.scrollWidth = p.measurements.width * 2;
+ } else {
+ p.measurements.scrollWidth = p.measurements.width;
+ }
}
}
@@ -2758,12 +2882,11 @@ Monocle.Dimensions.Columns = function (pageDiv) {
}
- function percentageThroughOfId(id) {
- var doc = p.page.m.activeFrame.contentDocument;
- var target = doc.getElementById(id);
+ function percentageThroughOfNode(target) {
if (!target) {
return 0;
}
+ var doc = p.page.m.activeFrame.contentDocument;
var offset = 0;
if (target.getBoundingClientRect) {
offset = target.getBoundingClientRect().left;
@@ -2785,20 +2908,30 @@ Monocle.Dimensions.Columns = function (pageDiv) {
function componentChanged(evt) {
if (evt.m['page'] != p.page) { return; }
var doc = evt.m['document'];
- Monocle.Styles.applyRules(doc.body, k.BODY_STYLES);
+ if (Monocle.Browser.has.columnOverflowPaintBug) {
+ var div = doc.createElement('div');
+ Monocle.Styles.applyRules(div, k.BODY_STYLES);
+ div.style.cssText += "overflow: scroll !important;";
+ while (doc.body.childNodes.length) {
+ div.appendChild(doc.body.firstChild);
+ }
+ doc.body.appendChild(div);
+ } else {
+ Monocle.Styles.applyRules(doc.body, k.BODY_STYLES);
- if (Monocle.Browser.is.WebKit) {
- doc.documentElement.style.overflow = 'hidden';
+ if (Monocle.Browser.is.WebKit) {
+ doc.documentElement.style.overflow = 'hidden';
+ }
}
+
p.dirty = true;
}
function setColumnWidth() {
var cw = p.page.m.sheafDiv.clientWidth;
- var doc = p.page.m.activeFrame.contentDocument;
if (currBodyStyleValue('column-width') != cw+"px") {
- Monocle.Styles.affix(doc.body, 'column-width', cw+"px");
+ Monocle.Styles.affix(columnedElement(), 'column-width', cw+"px");
p.dirty = true;
}
}
@@ -2809,8 +2942,7 @@ Monocle.Dimensions.Columns = function (pageDiv) {
return {
width: sheaf.clientWidth,
height: sheaf.clientHeight,
- scrollWidth: scrollerWidth(),
- fontSize: currBodyStyleValue('font-size')
+ scrollWidth: scrollerWidth()
}
}
@@ -2819,16 +2951,24 @@ Monocle.Dimensions.Columns = function (pageDiv) {
if (Monocle.Browser.has.mustScrollSheaf) {
return p.page.m.sheafDiv;
} else {
- return p.page.m.activeFrame.contentDocument.body;
+ return columnedElement();
}
}
+ function columnedElement() {
+ var elem = p.page.m.activeFrame.contentDocument.body;
+ return Monocle.Browser.has.columnOverflowPaintBug ? elem.firstChild : elem;
+ }
+
+
function scrollerWidth() {
var bdy = p.page.m.activeFrame.contentDocument.body;
if (Monocle.Browser.has.iframeDoubleWidthBug) {
- if (Monocle.Browser.on.Android) {
- return bdy.scrollWidth * 1.5; // I actually have no idea why 1.5.
+ if (Monocle.Browser.on.Kindle3) {
+ return scrollerElement().scrollWidth;
+ } else if (Monocle.Browser.on.Android) {
+ return bdy.scrollWidth;
} else if (Monocle.Browser.iOSVersion < "4.1") {
var hbw = bdy.scrollWidth / 2;
var sew = scrollerElement().scrollWidth;
@@ -2838,15 +2978,18 @@ Monocle.Dimensions.Columns = function (pageDiv) {
var hbw = bdy.scrollWidth / 2;
return hbw;
}
- } else if (Monocle.Browser.is.Gecko) {
- var lc = bdy.lastChild;
- while (lc && lc.nodeType != 1) {
- lc = lc.previousSibling;
- }
- if (lc && lc.getBoundingClientRect) {
- return lc.getBoundingClientRect().right;
+ } else if (bdy.getBoundingClientRect) {
+ var elems = bdy.getElementsByTagName('*');
+ var bdyRect = bdy.getBoundingClientRect();
+ var l = bdyRect.left, r = bdyRect.right;
+ for (var i = elems.length - 1; i >= 0; --i) {
+ var rect = elems[i].getBoundingClientRect();
+ l = Math.min(l, rect.left);
+ r = Math.max(r, rect.right);
}
+ return Math.abs(l) + Math.abs(r);
}
+
return scrollerElement().scrollWidth;
}
@@ -2867,8 +3010,14 @@ Monocle.Dimensions.Columns = function (pageDiv) {
function translateToLocus(locus) {
var offset = locusToOffset(locus);
- var bdy = p.page.m.activeFrame.contentDocument.body;
- Monocle.Styles.affix(bdy, "transform", "translateX("+offset+"px)");
+ p.page.m.offset = 0 - offset;
+ if (k.SETX && !Monocle.Browser.has.columnOverflowPaintBug) {
+ var bdy = p.page.m.activeFrame.contentDocument.body;
+ Monocle.Styles.affix(bdy, "transform", "translateX("+offset+"px)");
+ } else {
+ var scrElem = scrollerElement();
+ scrElem.scrollLeft = 0 - offset;
+ }
return offset;
}
@@ -2876,7 +3025,7 @@ Monocle.Dimensions.Columns = function (pageDiv) {
API.hasChanged = hasChanged;
API.measure = measure;
API.pages = pages;
- API.percentageThroughOfId = percentageThroughOfId;
+ API.percentageThroughOfNode = percentageThroughOfNode;
API.locusToOffset = locusToOffset;
API.translateToLocus = translateToLocus;
@@ -2898,6 +3047,8 @@ Monocle.Dimensions.Columns.BODY_STYLES = {
"column-fill": "auto"
}
+Monocle.Dimensions.Columns.SETX = true; // Set to false for scrollLeft.
+
if (Monocle.Browser.has.iframeDoubleWidthBug) {
Monocle.Dimensions.Columns.BODY_STYLES["min-width"] = "200%";
} else {
@@ -2924,6 +3075,8 @@ Monocle.Flippers.Slider = function (reader) {
function addPage(pageDiv) {
pageDiv.m.dimensions = new Monocle.Dimensions.Columns(pageDiv);
+
+ Monocle.Styles.setX(pageDiv, "0px");
}
@@ -2963,6 +3116,7 @@ Monocle.Flippers.Slider = function (reader) {
function interactiveMode(bState) {
+ p.reader.dispatchEvent('monocle:interactive:'+(bState ? 'on' : 'off'));
if (!Monocle.Browser.has.selectThruBug) {
return;
}
@@ -2994,10 +3148,10 @@ Monocle.Flippers.Slider = function (reader) {
function moveTo(locus, callback) {
var fn = function () {
- prepareNextPage(announceTurn);
- if (typeof callback == "function") {
- callback();
- }
+ prepareNextPage(function () {
+ if (typeof callback == "function") { callback(); }
+ announceTurn();
+ });
}
setPage(upperPage(), locus, fn);
}
@@ -3045,12 +3199,26 @@ Monocle.Flippers.Slider = function (reader) {
if (dir == k.FORWARDS) {
if (getPlace().onLastPageOfBook()) {
+ p.reader.dispatchEvent(
+ 'monocle:boundaryend',
+ {
+ locus: getPlace().getLocus({ direction : dir }),
+ page: upperPage()
+ }
+ );
resetTurnData();
return;
}
onGoingForward(boxPointX);
} else if (dir == k.BACKWARDS) {
if (getPlace().onFirstPageOfBook()) {
+ p.reader.dispatchEvent(
+ 'monocle:boundarystart',
+ {
+ locus: getPlace().getLocus({ direction : dir }),
+ page: upperPage()
+ }
+ );
resetTurnData();
return;
}
@@ -3215,14 +3383,14 @@ Monocle.Flippers.Slider = function (reader) {
function announceTurn() {
- hideWaitControl(upperPage());
- hideWaitControl(lowerPage());
p.reader.dispatchEvent('monocle:turn');
resetTurnData();
}
function resetTurnData() {
+ hideWaitControl(upperPage());
+ hideWaitControl(lowerPage());
p.turnData = {};
}
@@ -3268,7 +3436,7 @@ Monocle.Flippers.Slider = function (reader) {
(new Date()).getTime() - stamp > duration ||
Math.abs(currX - finalX) <= Math.abs((currX + step) - finalX)
) {
- clearTimeout(elem.setXTransitionInterval)
+ clearTimeout(elem.setXTransitionInterval);
Monocle.Styles.setX(elem, finalX);
if (elem.setXTCB) {
elem.setXTCB();
@@ -3366,13 +3534,17 @@ Monocle.Flippers.Slider = function (reader) {
function jumpIn(pageDiv, callback) {
var dur = Monocle.Browser.has.jumpFlickerBug ? 1 : 0;
- setX(pageDiv, 0, { duration: dur }, callback);
+ Monocle.defer(function () {
+ setX(pageDiv, 0, { duration: dur }, callback);
+ });
}
function jumpOut(pageDiv, callback) {
var dur = Monocle.Browser.has.jumpFlickerBug ? 1 : 0;
- setX(pageDiv, 0 - pageDiv.offsetWidth, { duration: dur }, callback);
+ Monocle.defer(function () {
+ setX(pageDiv, 0 - pageDiv.offsetWidth, { duration: dur }, callback);
+ });
}
@@ -3382,7 +3554,9 @@ Monocle.Flippers.Slider = function (reader) {
duration: k.durations.SLIDE,
timing: 'ease-in'
};
- setX(upperPage(), 0, slideOpts, callback);
+ Monocle.defer(function () {
+ setX(upperPage(), 0, slideOpts, callback);
+ });
}
@@ -3391,7 +3565,9 @@ Monocle.Flippers.Slider = function (reader) {
duration: k.durations.SLIDE,
timing: 'ease-in'
};
- setX(upperPage(), 0 - upperPage().offsetWidth, slideOpts, callback);
+ Monocle.defer(function () {
+ setX(upperPage(), 0 - upperPage().offsetWidth, slideOpts, callback);
+ });
}
@@ -3418,13 +3594,13 @@ Monocle.Flippers.Slider = function (reader) {
function showWaitControl(page) {
var ctrl = p.reader.dom.find('flippers_slider_wait', page.m.pageIndex);
- ctrl.style.opacity = 0.5;
+ ctrl.style.visibility = "visible";
}
function hideWaitControl(page) {
var ctrl = p.reader.dom.find('flippers_slider_wait', page.m.pageIndex);
- ctrl.style.opacity = 0;
+ ctrl.style.visibility = "hidden";
}
API.pageCount = p.pageCount;
diff --git a/resources/images/devices/itunes.png b/resources/images/devices/itunes.png
index cd8579d492..cc0493d9eb 100644
Binary files a/resources/images/devices/itunes.png and b/resources/images/devices/itunes.png differ
diff --git a/resources/images/plugins/mobileread.png b/resources/images/plugins/mobileread.png
new file mode 100644
index 0000000000..ec04625351
Binary files /dev/null and b/resources/images/plugins/mobileread.png differ
diff --git a/resources/images/plugins/plugin_deprecated.png b/resources/images/plugins/plugin_deprecated.png
new file mode 100644
index 0000000000..bf5e3e2d21
Binary files /dev/null and b/resources/images/plugins/plugin_deprecated.png differ
diff --git a/resources/images/plugins/plugin_disabled_invalid.png b/resources/images/plugins/plugin_disabled_invalid.png
new file mode 100644
index 0000000000..f905703564
Binary files /dev/null and b/resources/images/plugins/plugin_disabled_invalid.png differ
diff --git a/resources/images/plugins/plugin_disabled_ok.png b/resources/images/plugins/plugin_disabled_ok.png
new file mode 100644
index 0000000000..0ae52697cd
Binary files /dev/null and b/resources/images/plugins/plugin_disabled_ok.png differ
diff --git a/resources/images/plugins/plugin_disabled_valid.png b/resources/images/plugins/plugin_disabled_valid.png
new file mode 100644
index 0000000000..f3c7ea5fb9
Binary files /dev/null and b/resources/images/plugins/plugin_disabled_valid.png differ
diff --git a/resources/images/plugins/plugin_new.png b/resources/images/plugins/plugin_new.png
new file mode 100644
index 0000000000..cf76fd0f67
Binary files /dev/null and b/resources/images/plugins/plugin_new.png differ
diff --git a/resources/images/plugins/plugin_new_invalid.png b/resources/images/plugins/plugin_new_invalid.png
new file mode 100644
index 0000000000..ac9143b2b2
Binary files /dev/null and b/resources/images/plugins/plugin_new_invalid.png differ
diff --git a/resources/images/plugins/plugin_new_valid.png b/resources/images/plugins/plugin_new_valid.png
new file mode 100644
index 0000000000..0dfea2852e
Binary files /dev/null and b/resources/images/plugins/plugin_new_valid.png differ
diff --git a/resources/images/plugins/plugin_updater.png b/resources/images/plugins/plugin_updater.png
new file mode 100644
index 0000000000..bbd541dc03
Binary files /dev/null and b/resources/images/plugins/plugin_updater.png differ
diff --git a/resources/images/plugins/plugin_updater_updates.png b/resources/images/plugins/plugin_updater_updates.png
new file mode 100644
index 0000000000..f9cb8456ce
Binary files /dev/null and b/resources/images/plugins/plugin_updater_updates.png differ
diff --git a/resources/images/plugins/plugin_upgrade_invalid.png b/resources/images/plugins/plugin_upgrade_invalid.png
new file mode 100644
index 0000000000..e19963ce62
Binary files /dev/null and b/resources/images/plugins/plugin_upgrade_invalid.png differ
diff --git a/resources/images/plugins/plugin_upgrade_ok.png b/resources/images/plugins/plugin_upgrade_ok.png
new file mode 100644
index 0000000000..7cd2e89127
Binary files /dev/null and b/resources/images/plugins/plugin_upgrade_ok.png differ
diff --git a/resources/images/plugins/plugin_upgrade_valid.png b/resources/images/plugins/plugin_upgrade_valid.png
new file mode 100644
index 0000000000..5a0b79b11a
Binary files /dev/null and b/resources/images/plugins/plugin_upgrade_valid.png differ
diff --git a/setup/installer/windows/portable.c b/setup/installer/windows/portable.c
index 2a557e9174..05763bc303 100644
--- a/setup/installer/windows/portable.c
+++ b/setup/installer/windows/portable.c
@@ -95,6 +95,11 @@ void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) {
ExitProcess(1);
}
+ if (! SetEnvironmentVariable(TEXT("CALIBRE_PORTABLE_BUILD"), exe)) {
+ show_last_error(TEXT("Failed to set environment variables"));
+ ExitProcess(1);
+ }
+
dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP;
_sntprintf_s(cmdline, BUFSIZE, _TRUNCATE, TEXT(" \"--with-library=%s\""), library_dir);
diff --git a/src/calibre/constants.py b/src/calibre/constants.py
index f6b0a8d802..627f1b751a 100644
--- a/src/calibre/constants.py
+++ b/src/calibre/constants.py
@@ -32,6 +32,7 @@ isbsd = isfreebsd or isnetbsd
islinux = not(iswindows or isosx or isbsd)
isfrozen = hasattr(sys, 'frozen')
isunix = isosx or islinux
+isportable = os.environ.get('CALIBRE_PORTABLE_BUILD', None) is not None
try:
preferred_encoding = locale.getpreferredencoding()
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index ec0f28273f..d1c5b6ccd5 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -594,7 +594,7 @@ from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
-from calibre.devices.nook.driver import NOOK, NOOK_COLOR, NOOK_TSR
+from calibre.devices.nook.driver import NOOK, NOOK_COLOR
from calibre.devices.prs505.driver import PRS505
from calibre.devices.user_defined.driver import USER_DEFINED
from calibre.devices.android.driver import ANDROID, S60
@@ -603,10 +603,11 @@ from calibre.devices.eslick.driver import ESLICK, EBK52
from calibre.devices.nuut2.driver import NUUT2
from calibre.devices.iriver.driver import IRIVER_STORY
from calibre.devices.binatone.driver import README
-from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
+from calibre.devices.hanvon.driver import (N516, EB511, ALEX, AZBOOKA, THEBOOK,
+ LIBREAIR)
from calibre.devices.edge.driver import EDGE
-from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
- SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER
+from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS,
+ SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER)
from calibre.devices.sne.driver import SNE
from calibre.devices.misc import (PALMPRE, AVANT, SWEEX, PDNOVEL,
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR,
@@ -693,7 +694,7 @@ plugins += [
KINDLE,
KINDLE2,
KINDLE_DX,
- NOOK, NOOK_COLOR, NOOK_TSR,
+ NOOK, NOOK_COLOR,
PRS505,
ANDROID,
S60,
@@ -716,7 +717,7 @@ plugins += [
EB600,
README,
N516,
- THEBOOK,
+ THEBOOK, LIBREAIR,
EB511,
ELONEX,
TECLAST_K3,
@@ -866,13 +867,20 @@ class ActionStore(InterfaceActionBase):
from calibre.gui2.store.config.store import save_settings as save
save(config_widget)
+class ActionPluginUpdater(InterfaceActionBase):
+ name = 'Plugin Updater'
+ author = 'Grant Drake'
+ description = 'Queries the MobileRead forums for updates to plugins to install'
+ actual_plugin = 'calibre.gui2.actions.plugin_updates:PluginUpdaterAction'
+
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
- ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore]
+ ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,
+ ActionPluginUpdater]
# }}}
diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py
index 24b720ec3e..e259400ec9 100644
--- a/src/calibre/customize/ui.py
+++ b/src/calibre/customize/ui.py
@@ -493,6 +493,8 @@ def initialize_plugin(plugin, path_to_zip_file):
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
%tb) + '\n'+tb)
+def has_external_plugins():
+ return bool(config['plugins'])
def initialize_plugins():
global _initialized_plugins
diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py
index e1133ab929..dea5844028 100644
--- a/src/calibre/devices/apple/driver.py
+++ b/src/calibre/devices/apple/driver.py
@@ -135,7 +135,8 @@ class ITUNES(DriverBase):
'''
Calling sequences:
Initialization:
- can_handle() or can_handle_windows()
+ can_handle() | can_handle_windows()
+ _launch_iTunes()
reset()
open()
card_prefix()
diff --git a/src/calibre/devices/hanvon/driver.py b/src/calibre/devices/hanvon/driver.py
index f9dec178c6..3798257c2d 100644
--- a/src/calibre/devices/hanvon/driver.py
+++ b/src/calibre/devices/hanvon/driver.py
@@ -52,6 +52,18 @@ class THEBOOK(N516):
EBOOK_DIR_MAIN = 'My books'
WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE'
+class LIBREAIR(N516):
+ name = 'Libre Air Driver'
+ gui_name = 'Libre Air'
+ description = _('Communicate with the Libre Air reader.')
+ author = 'Kovid Goyal'
+ FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'rtf', 'txt', 'pdf']
+
+ BCD = [0x399]
+ VENDOR_NAME = 'ALURATEK'
+ WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGET'
+ EBOOK_DIR_MAIN = 'Books'
+
class ALEX(N516):
name = 'Alex driver'
diff --git a/src/calibre/devices/nook/driver.py b/src/calibre/devices/nook/driver.py
index e09fb7eaf9..9c8f882f3d 100644
--- a/src/calibre/devices/nook/driver.py
+++ b/src/calibre/devices/nook/driver.py
@@ -81,55 +81,28 @@ class NOOK(USBMS):
return [x.replace('#', '_') for x in components]
class NOOK_COLOR(NOOK):
- gui_name = _('Nook Color')
- description = _('Communicate with the Nook Color eBook reader.')
+ description = _('Communicate with the Nook Color and TSR eBook readers.')
- PRODUCT_ID = [0x002]
+ PRODUCT_ID = [0x002, 0x003]
BCD = [0x216]
- WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK'
+ WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK'
EBOOK_DIR_MAIN = 'My Files'
+ NEWS_IN_FOLDER = False
+
+ def upload_cover(self, path, filename, metadata, filepath):
+ pass
+
+ def get_carda_ebook_dir(self, for_upload=False):
+ if for_upload:
+ return self.EBOOK_DIR_MAIN
+ return ''
def create_upload_path(self, path, mdata, fname, create_dirs=True):
- filepath = NOOK.create_upload_path(self, path, mdata, fname,
- create_dirs=False)
- edm = self.EBOOK_DIR_MAIN
- subdir = 'Books'
- if mdata.tags:
- if _('News') in mdata.tags:
- subdir = 'Magazines'
- filepath = filepath.replace(os.sep+edm+os.sep,
- os.sep+edm+os.sep+subdir+os.sep)
- filedir = os.path.dirname(filepath)
- if create_dirs and not os.path.exists(filedir):
- os.makedirs(filedir)
-
- return filepath
-
- def upload_cover(self, path, filename, metadata, filepath):
- pass
-
- def get_carda_ebook_dir(self, for_upload=False):
- if for_upload:
- return 'My Files/Books'
- return ''
-
-class NOOK_TSR(NOOK):
- gui_name = _('Nook Simple')
- description = _('Communicate with the Nook TSR eBook reader.')
-
- PRODUCT_ID = [0x003]
- BCD = [0x216]
-
- EBOOK_DIR_MAIN = 'My Files/Books'
- WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK'
-
- def upload_cover(self, path, filename, metadata, filepath):
- pass
-
- def get_carda_ebook_dir(self, for_upload=False):
- if for_upload:
- return 'My Files/Books'
- return ''
+ is_news = mdata.tags and _('News') in mdata.tags
+ subdir = 'Magazines' if is_news else 'Books'
+ path = os.path.join(path, subdir)
+ return USBMS.create_upload_path(self, path, mdata, fname,
+ create_dirs=create_dirs)
diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py
index f0090989d9..442f3701c4 100644
--- a/src/calibre/devices/usbms/device.py
+++ b/src/calibre/devices/usbms/device.py
@@ -101,6 +101,9 @@ class Device(DeviceConfig, DevicePlugin):
#: The maximum length of paths created on the device
MAX_PATH_LEN = 250
+ #: Put news in its own folder
+ NEWS_IN_FOLDER = True
+
def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None):
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None
@@ -946,7 +949,8 @@ class Device(DeviceConfig, DevicePlugin):
extra_components = []
tag = special_tag
if tag.startswith(_('News')):
- extra_components.append('News')
+ if self.NEWS_IN_FOLDER:
+ extra_components.append('News')
else:
for c in tag.split('/'):
c = sanitize(c)
diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py
index bea90eeba8..bb515f95a4 100644
--- a/src/calibre/ebooks/epub/output.py
+++ b/src/calibre/ebooks/epub/output.py
@@ -394,6 +394,13 @@ class EPUBOutput(OutputFormatPlugin):
for tag in XPath('//h:img[@src]')(root):
tag.set('src', tag.get('src', '').replace('&', ''))
+ # ADE whimpers in fright when it encounters a
outside a
+ #
+ in_table = XPath('ancestor::h:table')
+ for tag in XPath('//h:td|//h:tr|//h:th')(root):
+ if not in_table(tag):
+ tag.tag = XHTML('div')
+
special_chars = re.compile(u'[\u200b\u00ad]')
for elem in root.iterdescendants():
if getattr(elem, 'text', False):
@@ -413,7 +420,7 @@ class EPUBOutput(OutputFormatPlugin):
rule.style.removeProperty('margin-left')
# padding-left breaks rendering in webkit and gecko
rule.style.removeProperty('padding-left')
- # Change whitespace:pre to pre-line to accommodate readers that
+ # Change whitespace:pre to pre-wrap to accommodate readers that
# cannot scroll horizontally
for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
style = rule.style
diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py
index 3d5f6c00ef..ce6c46c6cf 100644
--- a/src/calibre/ebooks/html/input.py
+++ b/src/calibre/ebooks/html/input.py
@@ -455,13 +455,16 @@ class HTMLInput(InputFormatPlugin):
bhref = os.path.basename(link)
id, href = self.oeb.manifest.generate(id='added',
href=bhref)
+ guessed = self.guess_type(href)[0]
+ media_type = guessed or self.BINARY_MIME
+ if 'text' in media_type:
+ self.log.warn('Ignoring link to text file %r'%link_)
+ return None
+
self.oeb.log.debug('Added', link)
self.oeb.container = self.DirContainer(os.path.dirname(link),
self.oeb.log, ignore_opf=True)
# Load into memory
- guessed = self.guess_type(href)[0]
- media_type = guessed or self.BINARY_MIME
-
item = self.oeb.manifest.add(id, href, media_type)
item.html_input_href = bhref
if guessed in self.OEB_STYLES:
diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py
index 303bb2db6e..849dbd1555 100644
--- a/src/calibre/ebooks/metadata/sources/identify.py
+++ b/src/calibre/ebooks/metadata/sources/identify.py
@@ -85,7 +85,11 @@ class ISBNMerge(object):
isbns, min_year = xisbn.get_isbn_pool(isbn)
if not isbns:
isbns = frozenset([isbn])
- self.pools[isbns] = pool = (min_year, [])
+ if isbns in self.pools:
+ # xISBN had a brain fart
+ pool = self.pools[isbns]
+ else:
+ self.pools[isbns] = pool = (min_year, [])
if not self.pool_has_result_from_same_source(pool, result):
pool[1].append(result)
diff --git a/src/calibre/ebooks/metadata/xisbn.py b/src/calibre/ebooks/metadata/xisbn.py
index 56156c034e..ff21dfd89d 100644
--- a/src/calibre/ebooks/metadata/xisbn.py
+++ b/src/calibre/ebooks/metadata/xisbn.py
@@ -45,6 +45,11 @@ class xISBN(object):
ans.append(rec)
return ans
+ def isbns_in_data(self, data):
+ for rec in data:
+ for i in rec.get('isbn', []):
+ yield i
+
def get_data(self, isbn):
isbn = self.purify(isbn)
with self.lock:
@@ -57,9 +62,8 @@ class xISBN(object):
data = []
id_ = len(self._data)
self._data.append(data)
- for rec in data:
- for i in rec.get('isbn', []):
- self._map[i] = id_
+ for i in self.isbns_in_data(data):
+ self._map[i] = id_
self._map[isbn] = id_
return self._data[self._map[isbn]]
diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py
index 2275552c15..d108742f3c 100644
--- a/src/calibre/ebooks/mobi/mobiml.py
+++ b/src/calibre/ebooks/mobi/mobiml.py
@@ -442,9 +442,16 @@ class MobiMLizer(object):
if tag in TABLE_TAGS and self.ignore_tables:
tag = 'span' if tag == 'td' else 'div'
- # GR: Added 'width', 'border' and 'scope'
+ if tag == 'table':
+ col = style.backgroundColor
+ if col:
+ elem.set('bgcolor', col)
+ css = style.cssdict()
+ if 'border' in css or 'border-width' in css:
+ elem.set('border', '1')
if tag in TABLE_TAGS:
- for attr in ('rowspan', 'colspan','width','border','scope'):
+ for attr in ('rowspan', 'colspan', 'width', 'border', 'scope',
+ 'bgcolor'):
if attr in elem.attrib:
istate.attrib[attr] = elem.attrib[attr]
if tag == 'q':
diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py
index 2ca62f0dea..bf9de37513 100644
--- a/src/calibre/ebooks/mobi/writer.py
+++ b/src/calibre/ebooks/mobi/writer.py
@@ -241,6 +241,7 @@ class Serializer(object):
if self.write_page_breaks_after_item:
buffer.write('')
buffer.write('')
+ self.anchor_offset = None
def serialize_elem(self, elem, item, nsrmap=NSRMAP):
buffer = self.buffer
diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py
index fc7a27b5cd..f6ff594701 100644
--- a/src/calibre/ebooks/oeb/stylizer.py
+++ b/src/calibre/ebooks/oeb/stylizer.py
@@ -11,7 +11,6 @@ __copyright__ = '2008, Marshall T. Vandegrift '
import os, itertools, re, logging, copy, unicodedata
from weakref import WeakKeyDictionary
from xml.dom import SyntaxErr as CSSSyntaxError
-import cssutils
from cssutils.css import (CSSStyleRule, CSSPageRule, CSSStyleDeclaration,
CSSFontFaceRule, cssproperties)
try:
@@ -20,7 +19,8 @@ try:
except ImportError:
# cssutils >= 0.9.8
from cssutils.css import PropertyValue as CSSValueList
-from cssutils import profile as cssprofiles
+from cssutils import (profile as cssprofiles, parseString, parseStyle, log as
+ cssutils_log, CSSParser, profiles)
from lxml import etree
from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError
from calibre import force_unicode
@@ -28,7 +28,7 @@ from calibre.ebooks import unit_convert
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
-cssutils.log.setLevel(logging.WARN)
+cssutils_log.setLevel(logging.WARN)
_html_css_stylesheet = None
@@ -36,7 +36,7 @@ def html_css_stylesheet():
global _html_css_stylesheet
if _html_css_stylesheet is None:
html_css = open(P('templates/html.css'), 'rb').read()
- _html_css_stylesheet = cssutils.parseString(html_css)
+ _html_css_stylesheet = parseString(html_css)
_html_css_stylesheet.namespaces['h'] = XHTML_NS
return _html_css_stylesheet
@@ -157,11 +157,11 @@ class Stylizer(object):
# Add cssutils parsing profiles from output_profile
for profile in self.opts.output_profile.extra_css_modules:
- cssutils.profile.addProfile(profile['name'],
+ cssprofiles.addProfile(profile['name'],
profile['props'],
profile['macros'])
- parser = cssutils.CSSParser(fetcher=self._fetch_css_file,
+ parser = CSSParser(fetcher=self._fetch_css_file,
log=logging.getLogger('calibre.css'))
self.font_face_rules = []
for elem in head:
@@ -473,6 +473,7 @@ class Style(object):
self._width = None
self._height = None
self._lineHeight = None
+ self._bgcolor = None
stylizer._styles[element] = self
def set(self, prop, val):
@@ -533,6 +534,48 @@ class Style(object):
def pt_to_px(self, value):
return (self._profile.dpi / 72.0) * value
+ @property
+ def backgroundColor(self):
+ '''
+ Return the background color by parsing both the background-color and
+ background shortcut properties. Note that inheritance/default values
+ are not used. None is returned if no background color is set.
+ '''
+
+ def validate_color(col):
+ return cssprofiles.validateWithProfile('color',
+ col,
+ profiles=[profiles.Profiles.CSS_LEVEL_2])[1]
+
+ if self._bgcolor is None:
+ col = None
+ val = self._style.get('background-color', None)
+ if val and validate_color(val):
+ col = val
+ else:
+ val = self._style.get('background', None)
+ if val is not None:
+ try:
+ style = parseStyle('background: '+val)
+ val = style.getProperty('background').cssValue
+ try:
+ val = list(val)
+ except:
+ # val is CSSPrimitiveValue
+ val = [val]
+ for c in val:
+ c = c.cssText
+ if validate_color(c):
+ col = c
+ break
+ except:
+ pass
+ if col is None:
+ self._bgcolor = False
+ else:
+ self._bgcolor = col
+ return self._bgcolor if self._bgcolor else None
+
@property
def fontSize(self):
def normalize_fontsize(value, base):
diff --git a/src/calibre/gui2/actions/plugin_updates.py b/src/calibre/gui2/actions/plugin_updates.py
new file mode 100644
index 0000000000..f8adbbe98f
--- /dev/null
+++ b/src/calibre/gui2/actions/plugin_updates.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Grant Drake '
+__docformat__ = 'restructuredtext en'
+
+from PyQt4.Qt import QApplication, Qt, QIcon
+from calibre.gui2.actions import InterfaceAction
+from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
+ FILTER_ALL, FILTER_UPDATE_AVAILABLE)
+
+class PluginUpdaterAction(InterfaceAction):
+
+ name = 'Plugin Updater'
+ action_spec = (_('Plugin Updater'), None, None, None)
+ action_type = 'current'
+
+ def genesis(self):
+ self.qaction.setIcon(QIcon(I('plugins/plugin_updater.png')))
+ self.qaction.triggered.connect(self.check_for_plugin_updates)
+
+ def check_for_plugin_updates(self):
+ # Get the user to choose a plugin to install
+ initial_filter = FILTER_UPDATE_AVAILABLE
+ mods = QApplication.keyboardModifiers()
+ if mods & Qt.ControlModifier or mods & Qt.ShiftModifier:
+ initial_filter = FILTER_ALL
+
+ d = PluginUpdaterDialog(self.gui, initial_filter=initial_filter)
+ d.exec_()
diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py
index 1ebd4ea6ba..c3bf9bbe8b 100644
--- a/src/calibre/gui2/actions/preferences.py
+++ b/src/calibre/gui2/actions/preferences.py
@@ -24,6 +24,8 @@ class PreferencesAction(InterfaceAction):
pm.addAction(QIcon(I('config.png')), _('Change calibre behavior'), self.do_config)
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
self.gui.run_wizard)
+ pm.addAction(QIcon(I('plugins/plugin_updater.png')),
+ _('Get plugins to enhance calibre'), self.get_plugins)
if not DEBUG:
pm.addSeparator()
ac = pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'),
@@ -36,6 +38,12 @@ class PreferencesAction(InterfaceAction):
for x in (self.gui.preferences_action, self.qaction):
x.triggered.connect(self.do_config)
+ def get_plugins(self):
+ from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
+ FILTER_NOT_INSTALLED)
+ d = PluginUpdaterDialog(self.gui,
+ initial_filter=FILTER_NOT_INSTALLED)
+ d.exec_()
def do_config(self, checked=False, initial_plugin=None,
close_after_initial=False):
diff --git a/src/calibre/gui2/dialogs/plugin_updater.py b/src/calibre/gui2/dialogs/plugin_updater.py
new file mode 100644
index 0000000000..e7d641b43e
--- /dev/null
+++ b/src/calibre/gui2/dialogs/plugin_updater.py
@@ -0,0 +1,869 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Grant Drake '
+__docformat__ = 'restructuredtext en'
+
+import re, datetime, traceback
+from lxml import html
+from PyQt4.Qt import (Qt, QUrl, QFrame, QVBoxLayout, QLabel, QBrush, QTextEdit,
+ QComboBox, QAbstractItemView, QHBoxLayout, QDialogButtonBox,
+ QAbstractTableModel, QVariant, QTableView, QModelIndex,
+ QSortFilterProxyModel, QAction, QIcon, QDialog,
+ QFont, QPixmap, QSize)
+from calibre import browser, prints
+from calibre.constants import numeric_version, iswindows, isosx, DEBUG
+from calibre.customize.ui import (initialized_plugins, is_disabled, remove_plugin,
+ add_plugin, enable_plugin, disable_plugin,
+ NameConflict, has_external_plugins)
+from calibre.gui2 import error_dialog, question_dialog, info_dialog, NONE, open_url, gprefs
+from calibre.gui2.preferences.plugins import ConfigWidget
+from calibre.utils.date import UNDEFINED_DATE, format_date
+
+
+MR_URL = 'http://www.mobileread.com/forums/'
+MR_INDEX_URL = MR_URL + 'showpost.php?p=1362767&postcount=1'
+
+FILTER_ALL = 0
+FILTER_INSTALLED = 1
+FILTER_UPDATE_AVAILABLE = 2
+FILTER_NOT_INSTALLED = 3
+
+def get_plugin_updates_available():
+ '''
+ API exposed to read whether there are updates available for any
+ of the installed user plugins.
+ Returns None if no updates found
+ Returns list(DisplayPlugin) of plugins installed that have a new version
+ '''
+ if not has_external_plugins():
+ return None
+ display_plugins = read_available_plugins()
+ if display_plugins:
+ update_plugins = filter(filter_upgradeable_plugins, display_plugins)
+ if len(update_plugins) > 0:
+ return update_plugins
+ return None
+
+def filter_upgradeable_plugins(display_plugin):
+ return display_plugin.is_upgrade_available()
+
+def filter_not_installed_plugins(display_plugin):
+ return not display_plugin.is_installed()
+
+def read_available_plugins():
+ display_plugins = []
+ br = browser()
+ br.set_handle_gzip(True)
+ try:
+ raw = br.open_novisit(MR_INDEX_URL).read()
+ if not raw:
+ return
+ except:
+ traceback.print_exc()
+ return
+ raw = raw.decode('utf-8', errors='replace')
+ root = html.fromstring(raw)
+ list_nodes = root.xpath('//div[@id="post_message_1362767"]/ul/li')
+ # Add our deprecated plugins which are nested in a grey span
+ list_nodes.extend(root.xpath('//div[@id="post_message_1362767"]/span/ul/li'))
+ for list_node in list_nodes:
+ try:
+ display_plugin = DisplayPlugin(list_node)
+ get_installed_plugin_status(display_plugin)
+ display_plugins.append(display_plugin)
+ except:
+ if DEBUG:
+ prints('======= MobileRead Parse Error =======')
+ traceback.print_exc()
+ prints(html.tostring(list_node))
+ display_plugins = sorted(display_plugins, key=lambda k: k.name)
+ return display_plugins
+
+def get_installed_plugin_status(display_plugin):
+ display_plugin.installed_version = None
+ display_plugin.plugin = None
+ for plugin in initialized_plugins():
+ if plugin.name == display_plugin.name:
+ display_plugin.plugin = plugin
+ display_plugin.installed_version = plugin.version
+ break
+ if display_plugin.uninstall_plugins:
+ # Plugin requires a specific plugin name to be uninstalled first
+ # This could occur when a plugin is renamed (Kindle Collections)
+ # or multiple plugins deprecated into a newly named one.
+ # Check whether user has the previous version(s) installed
+ plugins_to_remove = list(display_plugin.uninstall_plugins)
+ for plugin_to_uninstall in plugins_to_remove:
+ found = False
+ for plugin in initialized_plugins():
+ if plugin.name == plugin_to_uninstall:
+ found = True
+ break
+ if not found:
+ display_plugin.uninstall_plugins.remove(plugin_to_uninstall)
+
+
+class ImageTitleLayout(QHBoxLayout):
+ '''
+ A reusable layout widget displaying an image followed by a title
+ '''
+ def __init__(self, parent, icon_name, title):
+ QHBoxLayout.__init__(self)
+ title_font = QFont()
+ title_font.setPointSize(16)
+ title_image_label = QLabel(parent)
+ pixmap = QPixmap()
+ pixmap.load(I(icon_name))
+ if pixmap is None:
+ error_dialog(parent, _('Restart required'),
+ _('You must restart Calibre before using this plugin!'), show=True)
+ else:
+ title_image_label.setPixmap(pixmap)
+ title_image_label.setMaximumSize(32, 32)
+ title_image_label.setScaledContents(True)
+ self.addWidget(title_image_label)
+ shelf_label = QLabel(title, parent)
+ shelf_label.setFont(title_font)
+ self.addWidget(shelf_label)
+ self.insertStretch(-1)
+
+
+class SizePersistedDialog(QDialog):
+ '''
+ This dialog is a base class for any dialogs that want their size/position
+ restored when they are next opened.
+ '''
+
+ initial_extra_size = QSize(0, 0)
+
+ def __init__(self, parent, unique_pref_name):
+ QDialog.__init__(self, parent)
+ self.unique_pref_name = unique_pref_name
+ self.geom = gprefs.get(unique_pref_name, None)
+ self.finished.connect(self.dialog_closing)
+
+ def resize_dialog(self):
+ if self.geom is None:
+ self.resize(self.sizeHint()+self.initial_extra_size)
+ else:
+ self.restoreGeometry(self.geom)
+
+ def dialog_closing(self, result):
+ geom = bytearray(self.saveGeometry())
+ gprefs[self.unique_pref_name] = geom
+
+
+class VersionHistoryDialog(SizePersistedDialog):
+
+ def __init__(self, parent, plugin_name, html):
+ SizePersistedDialog.__init__(self, parent, 'Plugin Updater plugin:version history dialog')
+ self.setWindowTitle(_('Version History for %s')%plugin_name)
+
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ self.notes = QTextEdit(html, self)
+ self.notes.setReadOnly(True)
+ layout.addWidget(self.notes)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ # Cause our dialog size to be restored from prefs or created on first usage
+ self.resize_dialog()
+
+
+class PluginFilterComboBox(QComboBox):
+ def __init__(self, parent):
+ QComboBox.__init__(self, parent)
+ items = [_('All'), _('Installed'), _('Update available'), _('Not installed')]
+ self.addItems(items)
+
+
+class DisplayPlugin(object):
+
+ def __init__(self, list_node):
+ # The html from the index web page looks like this:
+ '''
+Book Sync
+Add books to a list to be automatically sent to your device the next time it is connected.
+Version: 1.1; Released: 02-22-2011; Calibre: 0.7.42; Author: kiwidude;
+Platforms: Windows, OSX, Linux; History: Yes;
+ '''
+ self.name = list_node.xpath('a')[0].text_content().strip()
+ self.forum_link = list_node.xpath('a/@href')[0].strip()
+ self.installed_version = None
+
+ description_text = list_node.xpath('i')[0].text_content()
+ description_parts = description_text.partition('Version:')
+ self.description = description_parts[0].strip()
+
+ details_text = description_parts[1] + description_parts[2].replace('\r\n','')
+ details_pairs = details_text.split(';')
+ details = {}
+ for details_pair in details_pairs:
+ pair = details_pair.split(':')
+ if len(pair) == 2:
+ key = pair[0].strip().lower()
+ value = pair[1].strip()
+ details[key] = value
+
+ donation_node = list_node.xpath('i/span/a/@href')
+ self.donation_link = donation_node[0] if donation_node else None
+
+ self.available_version = self._version_text_to_tuple(details.get('version', None))
+
+ release_date = details.get('released', '01-01-0101').split('-')
+ date_parts = [int(re.search(r'(\d+)', x).group(1)) for x in release_date]
+ self.release_date = datetime.date(date_parts[2], date_parts[0], date_parts[1])
+
+ self.calibre_required_version = self._version_text_to_tuple(details.get('calibre', None))
+ self.author = details.get('author', '')
+ self.platforms = [p.strip().lower() for p in details.get('platforms', '').split(',')]
+ # Optional pairing just for plugins which require checking for uninstall first
+ self.uninstall_plugins = []
+ uninstall = details.get('uninstall', None)
+ if uninstall:
+ self.uninstall_plugins = [i.strip() for i in uninstall.split(',')]
+ self.has_changelog = details.get('history', 'No').lower() in ['yes', 'true']
+ self.is_deprecated = details.get('deprecated', 'No').lower() in ['yes', 'true']
+
+ def _version_text_to_tuple(self, version_text):
+ if version_text:
+ ver = version_text.split('.')
+ while len(ver) < 3:
+ ver.append('0')
+ ver = [int(re.search(r'(\d+)', x).group(1)) for x in ver]
+ return tuple(ver)
+ else:
+ return None
+
+ def is_disabled(self):
+ if self.plugin is None:
+ return False
+ return is_disabled(self.plugin)
+
+ def is_installed(self):
+ return self.installed_version is not None
+
+ def is_upgrade_available(self):
+ return self.is_installed() and (self.installed_version < self.available_version \
+ or self.is_deprecated)
+
+ def is_valid_platform(self):
+ if iswindows:
+ return 'windows' in self.platforms
+ if isosx:
+ return 'osx' in self.platforms
+ return 'linux' in self.platforms
+
+ def is_valid_calibre(self):
+ return numeric_version >= self.calibre_required_version
+
+ def is_valid_to_install(self):
+ return self.is_valid_platform() and self.is_valid_calibre() and not self.is_deprecated
+
+
+class DisplayPluginSortFilterModel(QSortFilterProxyModel):
+
+ def __init__(self, parent):
+ QSortFilterProxyModel.__init__(self, parent)
+ self.setSortRole(Qt.UserRole)
+ self.filter_criteria = FILTER_ALL
+
+ def filterAcceptsRow(self, sourceRow, sourceParent):
+ index = self.sourceModel().index(sourceRow, 0, sourceParent)
+ display_plugin = self.sourceModel().display_plugins[index.row()]
+ if self.filter_criteria == FILTER_ALL:
+ return not (display_plugin.is_deprecated and not display_plugin.is_installed())
+ if self.filter_criteria == FILTER_INSTALLED:
+ return display_plugin.is_installed()
+ if self.filter_criteria == FILTER_UPDATE_AVAILABLE:
+ return display_plugin.is_upgrade_available()
+ if self.filter_criteria == FILTER_NOT_INSTALLED:
+ return not display_plugin.is_installed() and not display_plugin.is_deprecated
+ return False
+
+ def set_filter_criteria(self, filter_value):
+ self.filter_criteria = filter_value
+ self.invalidateFilter()
+
+
+class DisplayPluginModel(QAbstractTableModel):
+
+ def __init__(self, display_plugins):
+ QAbstractTableModel.__init__(self)
+ self.display_plugins = display_plugins
+ self.headers = map(QVariant, [_('Plugin Name'), _('Donate'), _('Status'), _('Installed'),
+ _('Available'), _('Released'), _('Calibre'), _('Author')])
+
+ def rowCount(self, *args):
+ return len(self.display_plugins)
+
+ def columnCount(self, *args):
+ return len(self.headers)
+
+ def headerData(self, section, orientation, role):
+ if role == Qt.DisplayRole and orientation == Qt.Horizontal:
+ return self.headers[section]
+ return NONE
+
+ def data(self, index, role):
+ if not index.isValid():
+ return NONE;
+ row, col = index.row(), index.column()
+ if row < 0 or row >= self.rowCount():
+ return NONE
+ display_plugin = self.display_plugins[row]
+ if role in [Qt.DisplayRole, Qt.UserRole]:
+ if col == 0:
+ return QVariant(display_plugin.name)
+ if col == 1:
+ if display_plugin.donation_link:
+ return QVariant(_('PayPal'))
+ if col == 2:
+ return self._get_status(display_plugin)
+ if col == 3:
+ return QVariant(self._get_display_version(display_plugin.installed_version))
+ if col == 4:
+ return QVariant(self._get_display_version(display_plugin.available_version))
+ if col == 5:
+ if role == Qt.UserRole:
+ return self._get_display_release_date(display_plugin.release_date, 'yyyyMMdd')
+ else:
+ return self._get_display_release_date(display_plugin.release_date)
+ if col == 6:
+ return QVariant(self._get_display_version(display_plugin.calibre_required_version))
+ if col == 7:
+ return QVariant(display_plugin.author)
+ elif role == Qt.DecorationRole:
+ if col == 0:
+ return self._get_status_icon(display_plugin)
+ if col == 1:
+ if display_plugin.donation_link:
+ return QIcon(I('donate.png'))
+ elif role == Qt.ToolTipRole:
+ if col == 1 and display_plugin.donation_link:
+ return QVariant(_('This plugin is FREE but you can reward the developer for their effort\n'
+ 'by donating to them via PayPal.\n\n'
+ 'Right-click and choose Donate to reward: ')+display_plugin.author)
+ else:
+ return self._get_status_tooltip(display_plugin)
+ elif role == Qt.ForegroundRole:
+ if col != 1: # Never change colour of the donation column
+ if display_plugin.is_deprecated:
+ return QVariant(QBrush(Qt.blue))
+ if display_plugin.is_disabled():
+ return QVariant(QBrush(Qt.gray))
+ return NONE
+
+ def plugin_to_index(self, display_plugin):
+ for i, p in enumerate(self.display_plugins):
+ if display_plugin == p:
+ return self.index(i, 0, QModelIndex())
+ return QModelIndex()
+
+ def refresh_plugin(self, display_plugin):
+ idx = self.plugin_to_index(display_plugin)
+ self.dataChanged.emit(idx, idx)
+
+ def _get_display_release_date(self, date_value, format='dd MMM yyyy'):
+ if date_value and date_value != UNDEFINED_DATE:
+ return QVariant(format_date(date_value, format))
+ return NONE
+
+ def _get_display_version(self, version):
+ if version is None:
+ return ''
+ return '.'.join([str(v) for v in list(version)])
+
+ def _get_status(self, display_plugin):
+ if not display_plugin.is_valid_platform():
+ return _('Platform unavailable')
+ if not display_plugin.is_valid_calibre():
+ return _('Calibre upgrade required')
+ if display_plugin.is_installed():
+ if display_plugin.is_deprecated:
+ return _('Plugin deprecated')
+ elif display_plugin.is_upgrade_available():
+ return _('New version available')
+ else:
+ return _('Latest version installed')
+ return _('Not installed')
+
+ def _get_status_icon(self, display_plugin):
+ if display_plugin.is_deprecated:
+ icon_name = 'plugin_deprecated.png'
+ elif display_plugin.is_disabled():
+ if display_plugin.is_upgrade_available():
+ if display_plugin.is_valid_to_install():
+ icon_name = 'plugin_disabled_valid.png'
+ else:
+ icon_name = 'plugin_disabled_invalid.png'
+ else:
+ icon_name = 'plugin_disabled_ok.png'
+ elif display_plugin.is_installed():
+ if display_plugin.is_upgrade_available():
+ if display_plugin.is_valid_to_install():
+ icon_name = 'plugin_upgrade_valid.png'
+ else:
+ icon_name = 'plugin_upgrade_invalid.png'
+ else:
+ icon_name = 'plugin_upgrade_ok.png'
+ else: # A plugin available not currently installed
+ if display_plugin.is_valid_to_install():
+ icon_name = 'plugin_new_valid.png'
+ else:
+ icon_name = 'plugin_new_invalid.png'
+ return QIcon(I('plugins/' + icon_name))
+
+ def _get_status_tooltip(self, display_plugin):
+ if display_plugin.is_deprecated:
+ return QVariant(_('This plugin has been deprecated and should be uninstalled')+'\n\n'+
+ _('Right-click to see more options'))
+ if not display_plugin.is_valid_platform():
+ return QVariant(_('This plugin can only be installed on: %s') % \
+ ', '.join(display_plugin.platforms)+'\n\n'+
+ _('Right-click to see more options'))
+ if numeric_version < display_plugin.calibre_required_version:
+ return QVariant(_('You must upgrade to at least Calibre %s before installing this plugin') % \
+ self._get_display_version(display_plugin.calibre_required_version)+'\n\n'+
+ _('Right-click to see more options'))
+ if display_plugin.installed_version < display_plugin.available_version:
+ if display_plugin.installed_version is None:
+ return QVariant(_('You can install this plugin')+'\n\n'+
+ _('Right-click to see more options'))
+ else:
+ return QVariant(_('A new version of this plugin is available')+'\n\n'+
+ _('Right-click to see more options'))
+ return QVariant(_('This plugin is installed and up-to-date')+'\n\n'+
+ _('Right-click to see more options'))
+
+
+class PluginUpdaterDialog(SizePersistedDialog):
+
+ initial_extra_size = QSize(350, 100)
+
+ def __init__(self, gui, initial_filter=FILTER_UPDATE_AVAILABLE):
+ SizePersistedDialog.__init__(self, gui, 'Plugin Updater plugin:plugin updater dialog')
+ self.gui = gui
+ self.forum_link = None
+ self.model = None
+ self._initialize_controls()
+ self._create_context_menu()
+
+ display_plugins = read_available_plugins()
+
+ if display_plugins:
+ self.model = DisplayPluginModel(display_plugins)
+ self.proxy_model = DisplayPluginSortFilterModel(self)
+ self.proxy_model.setSourceModel(self.model)
+ self.plugin_view.setModel(self.proxy_model)
+ self.plugin_view.resizeColumnsToContents()
+ self.plugin_view.selectionModel().currentRowChanged.connect(self._plugin_current_changed)
+ self.plugin_view.doubleClicked.connect(self.install_button.click)
+ self.filter_combo.setCurrentIndex(initial_filter)
+ self._select_and_focus_view()
+ else:
+ error_dialog(self.gui, _('Update Check Failed'),
+ _('Unable to reach the MobileRead plugins forum index page.'),
+ det_msg=MR_INDEX_URL, show=True)
+ self.filter_combo.setEnabled(False)
+ # Cause our dialog size to be restored from prefs or created on first usage
+ self.resize_dialog()
+
+ def _initialize_controls(self):
+ self.setWindowTitle(_('User plugins'))
+ self.setWindowIcon(QIcon(I('plugins/plugin_updater.png')))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+ title_layout = ImageTitleLayout(self, 'plugins/plugin_updater.png',
+ _('User Plugins'))
+ layout.addLayout(title_layout)
+
+ header_layout = QHBoxLayout()
+ layout.addLayout(header_layout)
+ self.filter_combo = PluginFilterComboBox(self)
+ self.filter_combo.setMinimumContentsLength(20)
+ self.filter_combo.currentIndexChanged[int].connect(self._filter_combo_changed)
+ header_layout.addWidget(QLabel(_('Filter list of plugins')+':', self))
+ header_layout.addWidget(self.filter_combo)
+ header_layout.addStretch(10)
+
+ self.plugin_view = QTableView(self)
+ self.plugin_view.horizontalHeader().setStretchLastSection(True)
+ self.plugin_view.setSelectionBehavior(QAbstractItemView.SelectRows)
+ self.plugin_view.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.plugin_view.setAlternatingRowColors(True)
+ self.plugin_view.setSortingEnabled(True)
+ self.plugin_view.setIconSize(QSize(28, 28))
+ layout.addWidget(self.plugin_view)
+
+ details_layout = QHBoxLayout()
+ layout.addLayout(details_layout)
+ forum_label = QLabel('Plugin Forum Thread', self)
+ forum_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
+ forum_label.linkActivated.connect(self._forum_label_activated)
+ details_layout.addWidget(QLabel(_('Description')+':', self), 0, Qt.AlignLeft)
+ details_layout.addWidget(forum_label, 1, Qt.AlignRight)
+
+ self.description = QLabel(self)
+ self.description.setFrameStyle(QFrame.Panel | QFrame.Sunken)
+ self.description.setAlignment(Qt.AlignTop | Qt.AlignLeft)
+ self.description.setMinimumHeight(40)
+ layout.addWidget(self.description)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
+ self.button_box.rejected.connect(self._close_clicked)
+ self.install_button = self.button_box.addButton(_('&Install'), QDialogButtonBox.AcceptRole)
+ self.install_button.setToolTip(_('Install the selected plugin'))
+ self.install_button.clicked.connect(self._install_clicked)
+ self.install_button.setEnabled(False)
+ self.configure_button = self.button_box.addButton(' '+_('&Customize plugin ')+' ', QDialogButtonBox.ResetRole)
+ self.configure_button.setToolTip(_('Customize the options for this plugin'))
+ self.configure_button.clicked.connect(self._configure_clicked)
+ self.configure_button.setEnabled(False)
+ layout.addWidget(self.button_box)
+
+ def _create_context_menu(self):
+ self.plugin_view.setContextMenuPolicy(Qt.ActionsContextMenu)
+ self.install_action = QAction(QIcon(I('plugins/plugin_upgrade_ok.png')), _('&Install'), self)
+ self.install_action.setToolTip(_('Install the selected plugin'))
+ self.install_action.triggered.connect(self._install_clicked)
+ self.install_action.setEnabled(False)
+ self.plugin_view.addAction(self.install_action)
+ self.history_action = QAction(QIcon(I('chapters.png')), _('Version &History'), self)
+ self.history_action.setToolTip(_('Show history of changes to this plugin'))
+ self.history_action.triggered.connect(self._history_clicked)
+ self.history_action.setEnabled(False)
+ self.plugin_view.addAction(self.history_action)
+ self.forum_action = QAction(QIcon(I('plugins/mobileread.png')), _('Plugin &Forum Thread'), self)
+ self.forum_action.triggered.connect(self._forum_label_activated)
+ self.forum_action.setEnabled(False)
+ self.plugin_view.addAction(self.forum_action)
+
+ sep1 = QAction(self)
+ sep1.setSeparator(True)
+ self.plugin_view.addAction(sep1)
+
+ self.toggle_enabled_action = QAction(_('Enable/&Disable plugin'), self)
+ self.toggle_enabled_action.setToolTip(_('Enable or disable this plugin'))
+ self.toggle_enabled_action.triggered.connect(self._toggle_enabled_clicked)
+ self.toggle_enabled_action.setEnabled(False)
+ self.plugin_view.addAction(self.toggle_enabled_action)
+ self.uninstall_action = QAction(_('&Remove plugin'), self)
+ self.uninstall_action.setToolTip(_('Uninstall the selected plugin'))
+ self.uninstall_action.triggered.connect(self._uninstall_clicked)
+ self.uninstall_action.setEnabled(False)
+ self.plugin_view.addAction(self.uninstall_action)
+
+ sep2 = QAction(self)
+ sep2.setSeparator(True)
+ self.plugin_view.addAction(sep2)
+
+ self.donate_enabled_action = QAction(QIcon(I('donate.png')), _('Donate to developer'), self)
+ self.donate_enabled_action.setToolTip(_('Donate to the developer of this plugin'))
+ self.donate_enabled_action.triggered.connect(self._donate_clicked)
+ self.donate_enabled_action.setEnabled(False)
+ self.plugin_view.addAction(self.donate_enabled_action)
+
+ sep3 = QAction(self)
+ sep3.setSeparator(True)
+ self.plugin_view.addAction(sep3)
+
+ self.configure_action = QAction(QIcon(I('config.png')), _('&Customize plugin'), self)
+ self.configure_action.setToolTip(_('Customize the options for this plugin'))
+ self.configure_action.triggered.connect(self._configure_clicked)
+ self.configure_action.setEnabled(False)
+ self.plugin_view.addAction(self.configure_action)
+
+ def _close_clicked(self):
+ # Force our toolbar/action to be updated based on uninstalled updates
+ if self.model:
+ update_plugins = filter(filter_upgradeable_plugins, self.model.display_plugins)
+ self.gui.recalc_update_label(len(update_plugins))
+ self.reject()
+
+ def _plugin_current_changed(self, current, previous):
+ if current.isValid():
+ actual_idx = self.proxy_model.mapToSource(current)
+ display_plugin = self.model.display_plugins[actual_idx.row()]
+ self.description.setText(display_plugin.description)
+ self.forum_link = display_plugin.forum_link
+ self.forum_action.setEnabled(bool(self.forum_link))
+ self.install_button.setEnabled(display_plugin.is_valid_to_install())
+ self.install_action.setEnabled(self.install_button.isEnabled())
+ self.uninstall_action.setEnabled(display_plugin.is_installed())
+ self.history_action.setEnabled(display_plugin.has_changelog)
+ self.configure_button.setEnabled(display_plugin.is_installed())
+ self.configure_action.setEnabled(self.configure_button.isEnabled())
+ self.toggle_enabled_action.setEnabled(display_plugin.is_installed())
+ self.donate_enabled_action.setEnabled(bool(display_plugin.donation_link))
+ else:
+ self.description.setText('')
+ self.forum_link = None
+ self.forum_action.setEnabled(False)
+ self.install_button.setEnabled(False)
+ self.install_action.setEnabled(False)
+ self.uninstall_action.setEnabled(False)
+ self.history_action.setEnabled(False)
+ self.configure_button.setEnabled(False)
+ self.configure_action.setEnabled(False)
+ self.toggle_enabled_action.setEnabled(False)
+ self.donate_enabled_action.setEnabled(False)
+
+ def _donate_clicked(self):
+ plugin = self._selected_display_plugin()
+ if plugin and plugin.donation_link:
+ open_url(QUrl(plugin.donation_link))
+
+ def _select_and_focus_view(self, change_selection=True):
+ if change_selection and self.plugin_view.model().rowCount() > 0:
+ self.plugin_view.selectRow(0)
+ else:
+ idx = self.plugin_view.selectionModel().currentIndex()
+ self._plugin_current_changed(idx, 0)
+ self.plugin_view.setFocus()
+
+ def _filter_combo_changed(self, idx):
+ self.proxy_model.set_filter_criteria(idx)
+ if idx == FILTER_NOT_INSTALLED:
+ self.plugin_view.sortByColumn(5, Qt.DescendingOrder)
+ else:
+ self.plugin_view.sortByColumn(0, Qt.AscendingOrder)
+ self._select_and_focus_view()
+
+ def _forum_label_activated(self):
+ if self.forum_link:
+ open_url(QUrl(self.forum_link))
+
+ def _selected_display_plugin(self):
+ idx = self.plugin_view.selectionModel().currentIndex()
+ actual_idx = self.proxy_model.mapToSource(idx)
+ return self.model.display_plugins[actual_idx.row()]
+
+ def _uninstall_plugin(self, name_to_remove):
+ if DEBUG:
+ prints('Removing plugin: ', name_to_remove)
+ remove_plugin(name_to_remove)
+ # Make sure that any other plugins that required this plugin
+ # to be uninstalled first have the requirement removed
+ for display_plugin in self.model.display_plugins:
+ # Make sure we update the status and display of the
+ # plugin we just uninstalled
+ if name_to_remove in display_plugin.uninstall_plugins:
+ if DEBUG:
+ prints('Removing uninstall dependency for: ', display_plugin.name)
+ display_plugin.uninstall_plugins.remove(name_to_remove)
+ if display_plugin.name == name_to_remove:
+ if DEBUG:
+ prints('Resetting plugin to uninstalled status: ', display_plugin.name)
+ display_plugin.installed_version = None
+ display_plugin.plugin = None
+ display_plugin.uninstall_plugins = []
+ if self.proxy_model.filter_criteria not in [FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE]:
+ self.model.refresh_plugin(display_plugin)
+
+ def _uninstall_clicked(self):
+ display_plugin = self._selected_display_plugin()
+ if not question_dialog(self, _('Are you sure?'), ''+
+ _('Are you sure you want to uninstall the %s plugin?')%display_plugin.name,
+ show_copy_button=False):
+ return
+ self._uninstall_plugin(display_plugin.name)
+ if self.proxy_model.filter_criteria in [FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE]:
+ self.model.reset()
+ self._select_and_focus_view()
+ else:
+ self._select_and_focus_view(change_selection=False)
+
+ def _install_clicked(self):
+ display_plugin = self._selected_display_plugin()
+ if not question_dialog(self, _('Install %s')%display_plugin.name, ' ' + \
+ _('Installing plugins is a security risk. '
+ 'Plugins can contain a virus/malware. '
+ 'Only install it if you got it from a trusted source.'
+ ' Are you sure you want to proceed?'),
+ show_copy_button=False):
+ return
+
+ if display_plugin.uninstall_plugins:
+ uninstall_names = list(display_plugin.uninstall_plugins)
+ if DEBUG:
+ prints('Uninstalling plugin: ', ', '.join(uninstall_names))
+ for name_to_remove in uninstall_names:
+ self._uninstall_plugin(name_to_remove)
+
+ if DEBUG:
+ prints('Locating zip file for %s: %s'% (display_plugin.name, display_plugin.forum_link))
+ self.gui.status_bar.showMessage(_('Locating zip file for %s: %s') % (display_plugin.name, display_plugin.forum_link))
+ plugin_zip_url = self._read_zip_attachment_url(display_plugin.forum_link)
+ if not plugin_zip_url:
+ return error_dialog(self.gui, _('Install Plugin Failed'),
+ _('Unable to locate a plugin zip file for %s') % display_plugin.name,
+ det_msg=display_plugin.forum_link, show=True)
+
+ if DEBUG:
+ prints('Downloading plugin zip attachment: ', plugin_zip_url)
+ self.gui.status_bar.showMessage(_('Downloading plugin zip attachment: %s') % plugin_zip_url)
+ zip_path = self._download_zip(plugin_zip_url)
+
+ if DEBUG:
+ prints('Installing plugin: ', zip_path)
+ self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path)
+
+ try:
+ try:
+ plugin = add_plugin(zip_path)
+ except NameConflict as e:
+ return error_dialog(self.gui, _('Already exists'),
+ unicode(e), show=True)
+ # Check for any toolbars to add to.
+ widget = ConfigWidget(self.gui)
+ widget.gui = self.gui
+ widget.check_for_add_to_toolbars(plugin)
+ self.gui.status_bar.showMessage(_('Plugin installed: %s') % display_plugin.name)
+ info_dialog(self.gui, _('Success'),
+ _('Plugin {0} successfully installed under '
+ ' {1} plugins. You may have to restart calibre '
+ 'for the plugin to take effect.').format(plugin.name, plugin.type),
+ show=True, show_copy_button=False)
+
+ display_plugin.plugin = plugin
+ # We cannot read the 'actual' version information as the plugin will not be loaded yet
+ display_plugin.installed_version = display_plugin.available_version
+ except:
+ if DEBUG:
+ prints('ERROR occurred while installing plugin: %s'%display_plugin.name)
+ traceback.print_exc()
+ error_dialog(self.gui, _('Install Plugin Failed'),
+ _('A problem occurred while installing this plugin.'
+ ' This plugin will now be uninstalled.'
+ ' Please post the error message in details below into'
+ ' the forum thread for this plugin and restart Calibre.'),
+ det_msg=traceback.format_exc(), show=True)
+ if DEBUG:
+ prints('Due to error now uninstalling plugin: %s'%display_plugin.name)
+ remove_plugin(display_plugin.name)
+ display_plugin.plugin = None
+
+ display_plugin.uninstall_plugins = []
+ if self.proxy_model.filter_criteria in [FILTER_NOT_INSTALLED, FILTER_UPDATE_AVAILABLE]:
+ self.model.reset()
+ self._select_and_focus_view()
+ else:
+ self.model.refresh_plugin(display_plugin)
+ self._select_and_focus_view(change_selection=False)
+
+ def _history_clicked(self):
+ display_plugin = self._selected_display_plugin()
+ text = self._read_version_history_html(display_plugin.forum_link)
+ if text:
+ dlg = VersionHistoryDialog(self, display_plugin.name, text)
+ dlg.exec_()
+ else:
+ return error_dialog(self, _('Version history missing'),
+ _('Unable to find the version history for %s')%display_plugin.name,
+ show=True)
+
+ def _configure_clicked(self):
+ display_plugin = self._selected_display_plugin()
+ plugin = display_plugin.plugin
+ if not plugin.is_customizable():
+ return info_dialog(self, _('Plugin not customizable'),
+ _('Plugin: %s does not need customization')%plugin.name, show=True)
+ from calibre.customize import InterfaceActionBase
+ if isinstance(plugin, InterfaceActionBase) and not getattr(plugin,
+ 'actual_iaction_plugin_loaded', False):
+ return error_dialog(self, _('Must restart'),
+ _('You must restart calibre before you can'
+ ' configure the %s plugin')%plugin.name, show=True)
+ plugin.do_user_config(self.parent())
+
+ def _toggle_enabled_clicked(self):
+ display_plugin = self._selected_display_plugin()
+ plugin = display_plugin.plugin
+ if not plugin.can_be_disabled:
+ return error_dialog(self,_('Plugin cannot be disabled'),
+ _('The plugin: %s cannot be disabled')%plugin.name, show=True)
+ if is_disabled(plugin):
+ enable_plugin(plugin)
+ else:
+ disable_plugin(plugin)
+ self.model.refresh_plugin(display_plugin)
+
+ def _read_version_history_html(self, forum_link):
+ br = browser()
+ br.set_handle_gzip(True)
+ try:
+ raw = br.open_novisit(forum_link).read()
+ if not raw:
+ return None
+ except:
+ traceback.print_exc()
+ return None
+ raw = raw.decode('utf-8', errors='replace')
+ root = html.fromstring(raw)
+ spoiler_nodes = root.xpath('//div[@class="smallfont" and strong="Spoiler"]')
+ for spoiler_node in spoiler_nodes:
+ try:
+ if spoiler_node.getprevious() is None:
+ # This is a spoiler node that has been indented using [INDENT]
+ # Need to go up to parent div, then previous node to get header
+ heading_node = spoiler_node.getparent().getprevious()
+ else:
+ # This is a spoiler node after a BR tag from the heading
+ heading_node = spoiler_node.getprevious().getprevious()
+ if heading_node is None:
+ continue
+ if heading_node.text_content().lower().find('version history') != -1:
+ div_node = spoiler_node.xpath('div')[0]
+ text = html.tostring(div_node, method='html', encoding=unicode)
+ return re.sub(' ', ' ', text)
+ except:
+ if DEBUG:
+ prints('======= MobileRead Parse Error =======')
+ traceback.print_exc()
+ prints(html.tostring(spoiler_node))
+ return None
+
+ def _read_zip_attachment_url(self, forum_link):
+ br = browser()
+ br.set_handle_gzip(True)
+ try:
+ raw = br.open_novisit(forum_link).read()
+ if not raw:
+ return None
+ except:
+ traceback.print_exc()
+ return None
+ raw = raw.decode('utf-8', errors='replace')
+ root = html.fromstring(raw)
+ attachment_nodes = root.xpath('//fieldset/table/tr/td/a')
+ for attachment_node in attachment_nodes:
+ try:
+ filename = attachment_node.text_content().lower()
+ if filename.find('.zip') != -1:
+ full_url = MR_URL + attachment_node.attrib['href']
+ return full_url
+ except:
+ if DEBUG:
+ prints('======= MobileRead Parse Error =======')
+ traceback.print_exc()
+ prints(html.tostring(attachment_node))
+ return None
+
+ def _download_zip(self, plugin_zip_url):
+ from calibre.ptempfile import PersistentTemporaryFile
+ br = browser()
+ br.set_handle_gzip(True)
+ raw = br.open_novisit(plugin_zip_url).read()
+ pt = PersistentTemporaryFile('.zip')
+ pt.write(raw)
+ pt.close()
+ return pt.name
diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py
index a75ff01b21..874be6832f 100644
--- a/src/calibre/gui2/init.py
+++ b/src/calibre/gui2/init.py
@@ -27,7 +27,6 @@ def partial(*args, **kwargs):
_keep_refs.append(ans)
return ans
-
class LibraryViewMixin(object): # {{{
def __init__(self, db):
@@ -145,6 +144,7 @@ class UpdateLabel(QLabel): # {{{
def __init__(self, *args, **kwargs):
QLabel.__init__(self, *args, **kwargs)
+ self.setCursor(Qt.PointingHandCursor)
def contextMenuEvent(self, e):
pass
@@ -182,14 +182,6 @@ class StatusBar(QStatusBar): # {{{
self.defmsg.setText(self.default_message)
self.clearMessage()
- def new_version_available(self, ver, url):
- msg = (u' %s: %s') % (
- _('Update found'), ver, ver)
- self.update_label.setText(msg)
- self.update_label.setCursor(Qt.PointingHandCursor)
- self.update_label.setVisible(True)
-
def get_version(self):
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
v = __version__
@@ -257,12 +249,6 @@ class LayoutMixin(object): # {{{
self.setStatusBar(self.status_bar)
self.status_bar.update_label.linkActivated.connect(self.update_link_clicked)
- def update_link_clicked(self, url):
- url = unicode(url)
- if url.startswith('update:'):
- version = url.partition(':')[-1]
- self.update_found(version, force=True)
-
def finalize_layout(self):
self.status_bar.initialize(self.system_tray_icon)
self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index b62aa28a68..d818f2db2a 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -388,6 +388,10 @@ class MetadataSingleDialogBase(ResizableDialog):
def apply_changes(self):
self.changed.add(self.book_id)
+ if self.db is None:
+ # break_cycles has already been called, don't know why this should
+ # happen but a user reported it
+ return True
for widget in self.basic_metadata_widgets:
try:
if not widget.commit(self.db, self.book_id):
diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py
index 013ab42684..ff4b9feac8 100644
--- a/src/calibre/gui2/metadata/single_download.py
+++ b/src/calibre/gui2/metadata/single_download.py
@@ -236,6 +236,11 @@ class ResultsView(QTableView): # {{{
self.resizeRowsToContents()
self.resizeColumnsToContents()
self.setFocus(Qt.OtherFocusReason)
+ idx = self.model().index(0, 0)
+ if idx.isValid() and self.model().rowCount() > 0:
+ self.show_details(idx)
+ sm = self.selectionModel()
+ sm.select(idx, sm.ClearAndSelect|sm.Rows)
def currentChanged(self, current, previous):
ret = QTableView.currentChanged(self, current, previous)
@@ -480,12 +485,6 @@ class IdentifyWidget(QWidget): # {{{
return
self.results_view.show_results(self.worker.results)
-
- self.comments_view.show_data('''
- Found %d results
- To see details, click on any result ''' %
- len(self.worker.results))
-
self.results_found.emit()
diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py
index b3c7873b45..3b581be701 100644
--- a/src/calibre/gui2/preferences/coloring.py
+++ b/src/calibre/gui2/preferences/coloring.py
@@ -22,11 +22,7 @@ from calibre.library.coloring import (Rule, conditionable_columns,
class ConditionEditor(QWidget): # {{{
- def __init__(self, fm, parent=None):
- QWidget.__init__(self, parent)
- self.fm = fm
-
- self.action_map = {
+ ACTION_MAP = {
'bool' : (
(_('is true'), 'is true',),
(_('is false'), 'is false'),
@@ -61,10 +57,17 @@ class ConditionEditor(QWidget): # {{{
(_('is set'), 'is set'),
(_('is not set'), 'is not set'),
),
- }
+ }
- for x in ('float', 'rating', 'datetime'):
- self.action_map[x] = self.action_map['int']
+ for x in ('float', 'rating', 'datetime'):
+ ACTION_MAP[x] = ACTION_MAP['int']
+
+
+ def __init__(self, fm, parent=None):
+ QWidget.__init__(self, parent)
+ self.fm = fm
+
+ self.action_map = self.ACTION_MAP
self.l = l = QGridLayout(self)
self.setLayout(l)
@@ -446,9 +449,15 @@ class RulesModel(QAbstractListModel): # {{{
def condition_to_html(self, condition):
c, a, v = condition
+ action_name = a
+ for actions in ConditionEditor.ACTION_MAP.itervalues():
+ for trans, ac in actions:
+ if ac == a:
+ action_name = trans
+
return (
_(' If the %s column %s value: %s') %
- (c, a, prepare_string_for_xml(v)))
+ (c, action_name, prepare_string_for_xml(v)))
# }}}
diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py
index 4f88e5aa1d..246df79d8f 100644
--- a/src/calibre/gui2/preferences/plugins.py
+++ b/src/calibre/gui2/preferences/plugins.py
@@ -8,16 +8,16 @@ __docformat__ = 'restructuredtext en'
import textwrap, os
from collections import OrderedDict
-from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \
- QBrush
+from PyQt4.Qt import (Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon,
+ QBrush)
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.plugins_ui import Ui_Form
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
disable_plugin, plugin_customization, add_plugin,
remove_plugin, NameConflict)
-from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \
- question_dialog, gprefs
+from calibre.gui2 import (NONE, error_dialog, info_dialog, choose_files,
+ question_dialog, gprefs)
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.icu import lower
@@ -217,6 +217,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.customize_plugin_button.clicked.connect(self.customize_plugin)
self.remove_plugin_button.clicked.connect(self.remove_plugin)
self.button_plugin_add.clicked.connect(self.add_plugin)
+ self.button_plugin_updates.clicked.connect(self.update_plugins)
+ self.button_plugin_new.clicked.connect(self.get_plugins)
self.search.initialize('plugin_search_history',
help_text=_('Search for plugin'))
self.search.search.connect(self.find)
@@ -353,6 +355,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
plugin.name + _(' cannot be removed. It is a '
'builtin plugin. Try disabling it instead.')).exec_()
+ def get_plugins(self):
+ self.update_plugins(not_installed=True)
+
+ def update_plugins(self, not_installed=False):
+ from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
+ FILTER_UPDATE_AVAILABLE, FILTER_NOT_INSTALLED)
+ mode = FILTER_NOT_INSTALLED if not_installed else FILTER_UPDATE_AVAILABLE
+ d = PluginUpdaterDialog(self.gui, initial_filter=mode)
+ d.exec_()
+ self._plugin_model.populate()
+ self._plugin_model.reset()
+ self.changed_signal.emit()
+
def reload_store_plugins(self):
self.gui.load_store_plugins()
if self.gui.iactions.has_key('Store'):
diff --git a/src/calibre/gui2/preferences/plugins.ui b/src/calibre/gui2/preferences/plugins.ui
index ebf422dfe3..b4dfd4426e 100644
--- a/src/calibre/gui2/preferences/plugins.ui
+++ b/src/calibre/gui2/preferences/plugins.ui
@@ -113,16 +113,49 @@
-
-
-
- &Add a new plugin
-
-
-
- :/images/plugins.png:/images/plugins.png
+
+
+ QFrame::HLine
+ -
+
+
-
+
+
+ Get &new plugins
+
+
+
+ :/images/plugins/plugin_new.png:/images/plugins/plugin_new.png
+
+
+
+ -
+
+
+ Check for &updated plugins
+
+
+
+ :/images/plugins/plugin_updater.png:/images/plugins/plugin_updater.png
+
+
+
+ -
+
+
+ &Load plugin from file
+
+
+
+ :/images/document_open.png:/images/document_open.png
+
+
+
+
+
diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.py b/src/calibre/gui2/store/config/chooser/chooser_widget.py
index a9399028f8..5e7eca8c66 100644
--- a/src/calibre/gui2/store/config/chooser/chooser_widget.py
+++ b/src/calibre/gui2/store/config/chooser/chooser_widget.py
@@ -6,7 +6,7 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember '
__docformat__ = 'restructuredtext en'
-from PyQt4.Qt import (QWidget, QIcon, QDialog)
+from PyQt4.Qt import (QWidget, QIcon, QDialog, QComboBox)
from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog
from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form
@@ -18,6 +18,8 @@ class StoreChooserWidget(QWidget, Ui_Form):
self.setupUi(self)
self.query.initialize('store_config_chooser_query')
+ self.query.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
+ self.query.setMinimumContentsLength(25)
self.adv_search_builder.setIcon(QIcon(I('search.png')))
diff --git a/src/calibre/gui2/store/mobileread/store_dialog.py b/src/calibre/gui2/store/mobileread/store_dialog.py
index 8908c9bb68..749f96a614 100644
--- a/src/calibre/gui2/store/mobileread/store_dialog.py
+++ b/src/calibre/gui2/store/mobileread/store_dialog.py
@@ -7,7 +7,7 @@ __copyright__ = '2011, John Schember '
__docformat__ = 'restructuredtext en'
-from PyQt4.Qt import (Qt, QDialog, QIcon)
+from PyQt4.Qt import (Qt, QDialog, QIcon, QComboBox)
from calibre.gui2.store.mobileread.adv_search_builder import AdvSearchBuilderDialog
from calibre.gui2.store.mobileread.models import BooksModel
@@ -21,6 +21,8 @@ class MobileReadStoreDialog(QDialog, Ui_Dialog):
self.plugin = plugin
self.search_query.initialize('store_mobileread_search')
+ self.search_query.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
+ self.search_query.setMinimumContentsLength(25)
self.adv_search_button.setIcon(QIcon(I('search.png')))
diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py
index f406b4923e..7db4d1bbaf 100644
--- a/src/calibre/gui2/store/search/search.py
+++ b/src/calibre/gui2/store/search/search.py
@@ -10,7 +10,8 @@ import re
from random import shuffle
from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel,
- QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout)
+ QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout,
+ QComboBox)
from calibre.gui2 import JSONConfig, info_dialog
from calibre.gui2.progress_indicator import ProgressIndicator
@@ -57,6 +58,8 @@ class SearchDialog(QDialog, Ui_Dialog):
# Set the search query
self.search_edit.setText(query)
+ self.search_edit.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
+ self.search_edit.setMinimumContentsLength(25)
# Create and add the progress indicator
self.pi = ProgressIndicator(self, 24)
diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py
index 9aae245d98..aacf30fe10 100644
--- a/src/calibre/gui2/update.py
+++ b/src/calibre/gui2/update.py
@@ -3,16 +3,30 @@ __copyright__ = '2008, Kovid Goyal '
import traceback
-from PyQt4.Qt import QThread, pyqtSignal, Qt, QUrl, QDialog, QGridLayout, \
- QLabel, QCheckBox, QDialogButtonBox, QIcon, QPixmap
+from PyQt4.Qt import (QThread, pyqtSignal, Qt, QUrl, QDialog, QGridLayout,
+ QLabel, QCheckBox, QDialogButtonBox, QIcon, QPixmap)
import mechanize
-from calibre.constants import __appname__, __version__, iswindows, isosx
+from calibre.constants import (__appname__, __version__, iswindows, isosx,
+ isportable)
from calibre import browser
from calibre.utils.config import prefs
from calibre.gui2 import config, dynamic, open_url
+from calibre.gui2.dialogs.plugin_updater import get_plugin_updates_available
URL = 'http://status.calibre-ebook.com/latest'
+NO_CALIBRE_UPDATE = '-0.0.0'
+VSEP = '|'
+
+def get_newest_version():
+ br = browser()
+ req = mechanize.Request(URL)
+ req.add_header('CALIBRE_VERSION', __version__)
+ req.add_header('CALIBRE_OS',
+ 'win' if iswindows else 'osx' if isosx else 'oth')
+ req.add_header('CALIBRE_INSTALL_UUID', prefs['installation_uuid'])
+ version = br.open(req).read().strip()
+ return version
class CheckForUpdates(QThread):
@@ -24,23 +38,29 @@ class CheckForUpdates(QThread):
def run(self):
while True:
+ calibre_update_version = NO_CALIBRE_UPDATE
+ plugins_update_found = 0
try:
- br = browser()
- req = mechanize.Request(URL)
- req.add_header('CALIBRE_VERSION', __version__)
- req.add_header('CALIBRE_OS',
- 'win' if iswindows else 'osx' if isosx else 'oth')
- req.add_header('CALIBRE_INSTALL_UUID', prefs['installation_uuid'])
- version = br.open(req).read().strip()
+ version = get_newest_version()
if version and version != __version__ and len(version) < 10:
- self.update_found.emit(version)
+ calibre_update_version = version
except:
traceback.print_exc()
+ try:
+ update_plugins = get_plugin_updates_available()
+ if update_plugins is not None:
+ plugins_update_found = len(update_plugins)
+ except:
+ traceback.print_exc()
+ if (calibre_update_version != NO_CALIBRE_UPDATE or
+ plugins_update_found > 0):
+ self.update_found.emit('%s%s%d'%(calibre_update_version,
+ VSEP, plugins_update_found))
self.sleep(self.INTERVAL)
class UpdateNotification(QDialog):
- def __init__(self, version, parent=None):
+ def __init__(self, calibre_version, plugin_updates, parent=None):
QDialog.__init__(self, parent)
self.resize(400, 250)
self.l = QGridLayout()
@@ -54,7 +74,8 @@ class UpdateNotification(QDialog):
'See the new features.') + ''+_('Update only if one of the '
'new features or bug fixes is important to you. '
- 'If the current version works well for you, do not update.'))%(__appname__, version))
+ 'If the current version works well for you, do not update.'))%(
+ __appname__, calibre_version))
self.label.setOpenExternalLinks(True)
self.label.setWordWrap(True)
self.setWindowTitle(_('Update available!'))
@@ -70,18 +91,30 @@ class UpdateNotification(QDialog):
b = self.bb.addButton(_('&Get update'), self.bb.AcceptRole)
b.setDefault(True)
b.setIcon(QIcon(I('arrow-down.png')))
+ if plugin_updates > 0:
+ b = self.bb.addButton(_('Update &plugins'), self.bb.ActionRole)
+ b.setIcon(QIcon(I('plugins/plugin_updater.png')))
+ b.clicked.connect(self.get_plugins, type=Qt.QueuedConnection)
self.bb.addButton(self.bb.Cancel)
self.l.addWidget(self.bb, 2, 0, 1, -1)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
- dynamic.set('update to version %s'%version, False)
+ dynamic.set('update to version %s'%calibre_version, False)
+
+ def get_plugins(self):
+ from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
+ FILTER_UPDATE_AVAILABLE)
+ d = PluginUpdaterDialog(self.parent(),
+ initial_filter=FILTER_UPDATE_AVAILABLE)
+ d.exec_()
def show_future(self, *args):
config.set('new_version_notification', bool(self.cb.isChecked()))
def accept(self):
- url = 'http://calibre-ebook.com/download_'+\
- ('windows' if iswindows else 'osx' if isosx else 'linux')
+ url = ('http://calibre-ebook.com/download_' +
+ ('portable' if isportable else 'windows' if iswindows
+ else 'osx' if isosx else 'linux'))
open_url(QUrl(url))
QDialog.accept(self)
@@ -89,21 +122,79 @@ class UpdateNotification(QDialog):
class UpdateMixin(object):
def __init__(self, opts):
+ self.last_newest_calibre_version = NO_CALIBRE_UPDATE
if not opts.no_update_check:
self.update_checker = CheckForUpdates(self)
self.update_checker.update_found.connect(self.update_found,
type=Qt.QueuedConnection)
self.update_checker.start()
- def update_found(self, version, force=False):
- os = 'windows' if iswindows else 'osx' if isosx else 'linux'
- url = 'http://calibre-ebook.com/download_%s'%os
- self.status_bar.new_version_available(version, url)
+ def recalc_update_label(self, number_of_plugin_updates):
+ self.update_found('%s%s%d'%(self.last_newest_calibre_version, VSEP,
+ number_of_plugin_updates), no_show_popup=True)
- if force or (config.get('new_version_notification') and \
- dynamic.get('update to version %s'%version, True)):
- self._update_notification__ = UpdateNotification(version,
- parent=self)
- self._update_notification__.show()
+ def update_found(self, version, force=False, no_show_popup=False):
+ try:
+ calibre_version, plugin_updates = version.split(VSEP)
+ plugin_updates = int(plugin_updates)
+ except:
+ traceback.print_exc()
+ return
+ self.last_newest_calibre_version = calibre_version
+ has_calibre_update = calibre_version and calibre_version != NO_CALIBRE_UPDATE
+ has_plugin_updates = plugin_updates > 0
+ self.plugin_update_found(plugin_updates)
+
+ if not has_calibre_update and not has_plugin_updates:
+ self.status_bar.update_label.setVisible(False)
+ return
+ if has_calibre_update:
+ plt = u''
+ if has_plugin_updates:
+ plt = _(' (%d plugin updates)')%plugin_updates
+ msg = (u'%s: '
+ u'%s%s') % (
+ _('Update found'), version, calibre_version, plt)
+ else:
+ msg = (u'%d %s')%(version, plugin_updates,
+ _('updated plugins'))
+ self.status_bar.update_label.setText(msg)
+ self.status_bar.update_label.setVisible(True)
+ if has_calibre_update:
+ if (force or (config.get('new_version_notification') and
+ dynamic.get('update to version %s'%calibre_version, True))):
+ if not no_show_popup:
+ self._update_notification__ = UpdateNotification(calibre_version,
+ plugin_updates, parent=self)
+ self._update_notification__.show()
+ elif has_plugin_updates:
+ if force:
+ from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
+ FILTER_UPDATE_AVAILABLE)
+ d = PluginUpdaterDialog(self,
+ initial_filter=FILTER_UPDATE_AVAILABLE)
+ d.exec_()
+
+ def plugin_update_found(self, number_of_updates):
+ # Change the plugin icon to indicate there are updates available
+ plugin = self.iactions.get('Plugin Updates', None)
+ if not plugin:
+ return
+ if number_of_updates:
+ plugin.qaction.setText(_('Plugin Updates')+'*')
+ plugin.qaction.setIcon(QIcon(I('plugins/plugin_updater_updates.png')))
+ plugin.qaction.setToolTip(
+ _('There are %d plugin updates available')%number_of_updates)
+ else:
+ plugin.qaction.setText(_('Plugin Updates'))
+ plugin.qaction.setIcon(QIcon(I('plugins/plugin_updater.png')))
+ plugin.qaction.setToolTip(_('Install and configure user plugins'))
+
+ def update_link_clicked(self, url):
+ url = unicode(url)
+ if url.startswith('update:'):
+ version = url[len('update:'):]
+ self.update_found(version, force=True)
+
diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py
index dd8d876005..96fbafe4d5 100644
--- a/src/calibre/gui2/widgets.py
+++ b/src/calibre/gui2/widgets.py
@@ -5,15 +5,15 @@ Miscellaneous widgets used in the GUI
'''
import re, traceback
-from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
- QListWidgetItem, QTextCharFormat, QApplication, \
- QSyntaxHighlighter, QCursor, QColor, QWidget, \
- QPixmap, QSplitterHandle, QToolButton, \
- QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
- QRegExp, QSettings, QSize, QSplitter, \
- QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, \
- QMenu, QStringListModel, QCompleter, QStringList, \
- QTimer, QRect, QFontDatabase, QGraphicsView
+from PyQt4.Qt import (QIcon, QFont, QLabel, QListWidget, QAction,
+ QListWidgetItem, QTextCharFormat, QApplication,
+ QSyntaxHighlighter, QCursor, QColor, QWidget,
+ QPixmap, QSplitterHandle, QToolButton,
+ QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal,
+ QRegExp, QSettings, QSize, QSplitter,
+ QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene,
+ QMenu, QStringListModel, QCompleter, QStringList,
+ QTimer, QRect, QFontDatabase, QGraphicsView)
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
from calibre.gui2.filename_pattern_ui import Ui_Form
@@ -21,12 +21,12 @@ from calibre import fit_image
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.utils.config import prefs, XMLConfig, tweaks
from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator
-from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \
- IMAGE_EXTENSIONS, dnd_has_extension, DownloadDialog
+from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files,
+ IMAGE_EXTENSIONS, dnd_has_extension, DownloadDialog)
history = XMLConfig('history')
-class ProgressIndicator(QWidget):
+class ProgressIndicator(QWidget): # {{{
def __init__(self, *args):
QWidget.__init__(self, *args)
@@ -57,8 +57,9 @@ class ProgressIndicator(QWidget):
def stop(self):
self.pi.stopAnimation()
self.setVisible(False)
+# }}}
-class FilenamePattern(QWidget, Ui_Form):
+class FilenamePattern(QWidget, Ui_Form): # {{{
changed_signal = pyqtSignal()
@@ -148,8 +149,9 @@ class FilenamePattern(QWidget, Ui_Form):
return pat
+# }}}
-class FormatList(QListWidget):
+class FormatList(QListWidget): # {{{
DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS
formats_dropped = pyqtSignal(object, object)
delete_format = pyqtSignal()
@@ -188,6 +190,8 @@ class FormatList(QListWidget):
else:
return QListWidget.keyPressEvent(self, event)
+# }}}
+
class ImageDropMixin(object): # {{{
'''
Adds support for dropping images onto widgets and a context menu for
@@ -262,7 +266,7 @@ class ImageDropMixin(object): # {{{
pixmap_to_data(pmap))
# }}}
-class ImageView(QWidget, ImageDropMixin):
+class ImageView(QWidget, ImageDropMixin): # {{{
BORDER_WIDTH = 1
cover_changed = pyqtSignal(object)
@@ -314,8 +318,9 @@ class ImageView(QWidget, ImageDropMixin):
p.drawRect(target)
#p.drawRect(self.rect())
p.end()
+# }}}
-class CoverView(QGraphicsView, ImageDropMixin):
+class CoverView(QGraphicsView, ImageDropMixin): # {{{
cover_changed = pyqtSignal(object)
@@ -333,7 +338,9 @@ class CoverView(QGraphicsView, ImageDropMixin):
self.scene.addPixmap(pmap)
self.setScene(self.scene)
-class FontFamilyModel(QAbstractListModel):
+# }}}
+
+class FontFamilyModel(QAbstractListModel): # {{{
def __init__(self, *args):
QAbstractListModel.__init__(self, *args)
@@ -371,7 +378,9 @@ class FontFamilyModel(QAbstractListModel):
def index_of(self, family):
return self.families.index(family.strip())
+# }}}
+# BasicList {{{
class BasicListItem(QListWidgetItem):
def __init__(self, text, user_data=None):
@@ -404,9 +413,9 @@ class BasicList(QListWidget):
def items(self):
for i in range(self.count()):
yield self.item(i)
+# }}}
-
-class LineEditECM(object):
+class LineEditECM(object): # {{{
'''
Extend the context menu of a QLineEdit to include more actions.
@@ -449,8 +458,9 @@ class LineEditECM(object):
from calibre.utils.icu import capitalize
self.setText(capitalize(unicode(self.text())))
+# }}}
-class EnLineEdit(LineEditECM, QLineEdit):
+class EnLineEdit(LineEditECM, QLineEdit): # {{{
'''
Enhanced QLineEdit.
@@ -459,9 +469,9 @@ class EnLineEdit(LineEditECM, QLineEdit):
'''
pass
+# }}}
-
-class ItemsCompleter(QCompleter):
+class ItemsCompleter(QCompleter): # {{{
'''
A completer object that completes a list of tags. It is used in conjunction
@@ -486,8 +496,9 @@ class ItemsCompleter(QCompleter):
model = QStringListModel(items, self)
self.setModel(model)
+# }}}
-class CompleteLineEdit(EnLineEdit):
+class CompleteLineEdit(EnLineEdit): # {{{
'''
A QLineEdit that can complete parts of text separated by separator.
@@ -550,8 +561,9 @@ class CompleteLineEdit(EnLineEdit):
self.setText(complete_text_pat % (before_text[:cursor_pos - prefix_len], text, self.separator, after_text))
self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra)
+# }}}
-class EnComboBox(QComboBox):
+class EnComboBox(QComboBox): # {{{
'''
Enhanced QComboBox.
@@ -575,7 +587,9 @@ class EnComboBox(QComboBox):
idx = 0
self.setCurrentIndex(idx)
-class CompleteComboBox(EnComboBox):
+# }}}
+
+class CompleteComboBox(EnComboBox): # {{{
def __init__(self, *args):
EnComboBox.__init__(self, *args)
@@ -590,8 +604,9 @@ class CompleteComboBox(EnComboBox):
def set_space_before_sep(self, space_before):
self.lineEdit().set_space_before_sep(space_before)
+# }}}
-class HistoryLineEdit(QComboBox):
+class HistoryLineEdit(QComboBox): # {{{
lost_focus = pyqtSignal()
@@ -638,7 +653,9 @@ class HistoryLineEdit(QComboBox):
QComboBox.focusOutEvent(self, e)
self.lost_focus.emit()
-class ComboBoxWithHelp(QComboBox):
+# }}}
+
+class ComboBoxWithHelp(QComboBox): # {{{
'''
A combobox where item 0 is help text. CurrentText will return '' for item 0.
Be sure to always fetch the text with currentText. Don't use the signals
@@ -685,8 +702,9 @@ class ComboBoxWithHelp(QComboBox):
QComboBox.hidePopup(self)
self.set_state()
+# }}}
-class EncodingComboBox(QComboBox):
+class EncodingComboBox(QComboBox): # {{{
'''
A combobox that holds text encodings support
by Python. This is only populated with the most
@@ -709,8 +727,9 @@ class EncodingComboBox(QComboBox):
for item in self.ENCODINGS:
self.addItem(item)
+# }}}
-class PythonHighlighter(QSyntaxHighlighter):
+class PythonHighlighter(QSyntaxHighlighter): # {{{
Rules = []
Formats = {}
@@ -948,6 +967,9 @@ class PythonHighlighter(QSyntaxHighlighter):
QSyntaxHighlighter.rehighlight(self)
QApplication.restoreOverrideCursor()
+# }}}
+
+# Splitter {{{
class SplitterHandle(QSplitterHandle):
double_clicked = pyqtSignal(object)
@@ -1179,3 +1201,5 @@ class Splitter(QSplitter):
# }}}
+# }}}
+
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index a19534191b..97454c90e2 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -149,6 +149,15 @@ class CSV_XML(CatalogPlugin): # {{{
elif field == 'comments':
item = item.replace(u'\r\n',u' ')
item = item.replace(u'\n',u' ')
+
+ # Convert HTML to markdown text
+ if type(item) is unicode:
+ opening_tag = re.search('<(\w+)(\x20|>)',item)
+ if opening_tag:
+ closing_tag = re.search('<\/%s>$' % opening_tag.group(1), item)
+ if closing_tag:
+ item = html2text(item)
+
outstr.append(u'"%s"' % unicode(item).replace('"','""'))
outfile.write(u','.join(outstr) + u'\n')
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 274bf03eb3..4b527e169c 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -515,7 +515,7 @@ Downloading from the internet can sometimes result in a corrupted download. If t
* Try rebooting your computer and running a registry cleaner like `Wise registry cleaner `_.
* Try downloading the installer with an alternate browser. For example if you are using Internet Explorer, try using Firefox or Chrome instead.
-Best place to ask for more help is in the `forums `_.
+If you still cannot get the installer to work and you are on windows, you can use the `calibre portable install `_, which does not need an installer (it is just a zip file).
My antivirus program claims |app| is a virus/trojan?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/src/calibre/web/feeds/recipes/collection.py b/src/calibre/web/feeds/recipes/collection.py
index 1eb504d282..87a65dd9e6 100644
--- a/src/calibre/web/feeds/recipes/collection.py
+++ b/src/calibre/web/feeds/recipes/collection.py
@@ -21,8 +21,8 @@ NS = 'http://calibre-ebook.com/recipe_collection'
E = ElementMaker(namespace=NS, nsmap={None:NS})
def iterate_over_builtin_recipe_files():
- exclude = ['craigslist', 'iht', 'outlook_india', 'toronto_sun',
- 'indian_express', 'india_today', 'livemint']
+ exclude = ['craigslist', 'iht', 'toronto_sun',
+ 'india_today', 'livemint']
d = os.path.dirname
base = os.path.join(d(d(d(d(d(d(os.path.abspath(__file__))))))), 'recipes')
for f in os.listdir(base):
@@ -101,6 +101,7 @@ def get_custom_recipe_collection(*args):
if recipe_class is not None:
rmap['custom:%s'%id_] = recipe_class
except:
+ print 'Failed to load recipe from: %r'%fname
import traceback
traceback.print_exc()
continue
|