This commit is contained in:
GRiker 2010-07-18 04:50:20 -06:00
commit 174c09022a
54 changed files with 27091 additions and 20936 deletions

View File

@ -4,6 +4,72 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.9
date: 2010-07-17
new features:
- title: "New unified toolbar"
type: major
description: >
"A new unified toolbar combines the old toolbar and device display, to save space. Now when a device is connected, buttons
are created in the unified toolbar for the device and its storage cards. Click the arrow next to the button to eject the device."
- title: "Device drivers: Add option to allow calibre to automatically manage metadata on the device in Preferences->Add/Save->Sending to device"
- title: "BibTeX output for catalogs. The list of books in calibre can now also be output as a .bib file"
- title: "A new toolbar button to choose/create different calibre libraries. Be careful using it if you also use custom columns."
- title: "Support for the MiBuk"
bug fixes:
- title: "MOBI metadata: Replace HTML entities in the title read from the MOBI file"
- title: "Conversion pipeline: Handle elements with percentage sizes that are children of zero size parents correctly."
tickets: [6155]
- title: "Fix regression that made LRF conversion less robust"
tickets: [6180]
- title: "FB2 Input: Handle embedded images correctly, so that EPUB generated from FB2 works with Adobe Digital Editions."
tickets: [6183]
- title: "Fix regression that prevented old news from being deleted in the calibre library if calibre is never kept running for more than an hour"
- title: "RTF Input: Fix handling of text align and superscript/subscripts"
tickets: [3644,5060]
- title: "Fix long series or publisher names causing convert dialog to become too wide"
- title: "SONY driver: Fix handling of invalid XML databases with null bytes"
tickets: [6165]
- title: "iTunes driver: Better series_index sorting"
- title: "Improved editing of dates for custom columns"
- title: "Linux USB scanner: Don't fail to start calibre if SYFS is not present. Instead simply fail to detect devices"
tickets: [6156]
- title: "Android driver: Show books on device if Aldiko is being used"
tickets: [6100]
- title: "Upgrade to Qt 4.6.3 in all binary builds to ensure proper rendering of the new toolbar icons"
- title: "Fix handling of entities in epub files by the epub-fix command"
tickets: [6136]
new recipes:
- title: "EL Pain Impresso"
author: Darko Miletic
- title: "MIT Technology Review, Alternet, Waco Tribune Herald and Orlando Sentinel"
author: rty
improved recipes:
- Google Reader
- version: 0.7.8
date: 2010-07-09

View File

@ -0,0 +1,38 @@
from calibre.ptempfile import PersistentTemporaryFile
from calibre.web.feeds.news import BasicNewsRecipe
class Alternet(BasicNewsRecipe):
title = u'Alternet'
__author__= 'rty'
oldest_article = 7
max_articles_per_feed = 100
publisher = 'alternet.org'
category = 'News, Magazine'
description = 'News magazine and online community'
feeds = [
(u'Front Page', u'http://feeds.feedblitz.com/alternet'),
(u'Breaking News', u'http://feeds.feedblitz.com/alternet_breaking_news'),
(u'Top Ten Campaigns', u'http://feeds.feedblitz.com/alternet_top_10_campaigns'),
(u'Special Coverage Areas', u'http://feeds.feedblitz.com/alternet_coverage')
]
remove_attributes = ['width', 'align','cellspacing']
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'en'
encoding = 'UTF-8'
temp_files = []
articles_are_obfuscated = True
def get_article_url(self, article):
return article.get('link', None)
def get_obfuscated_article(self, url):
br = self.get_browser()
br.open(url)
response = br.follow_link(url_regex = r'/printversion/[0-9]+', nr = 0)
html = response.read()
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
self.temp_files[-1].write(html)
self.temp_files[-1].close()
return self.temp_files[-1].name

View File

@ -0,0 +1,44 @@
import string
from calibre.web.feeds.news import BasicNewsRecipe
class TechnologyReview(BasicNewsRecipe):
title = u'Technology Review'
__author__ = 'rty'
description = 'MIT Technology Magazine'
publisher = 'Technology Review Inc.'
category = 'Technology, Innovation, R&D'
oldest_article = 14
max_articles_per_feed = 100
No_stylesheets = True
extra_css = """
.ArticleBody {font: normal; text-align: justify}
.headline {font: bold x-large}
.subheadline {font: italic large}
"""
feeds = [
(u'Computing', u'http://feeds.technologyreview.com/technology_review_Computing'),
(u'Web', u'http://feeds.technologyreview.com/technology_review_Web'),
(u'Communications', u'http://feeds.technologyreview.com/technology_review_Communications'),
(u'Energy', u'http://feeds.technologyreview.com/technology_review_Energy'),
(u'Materials', u'http://feeds.technologyreview.com/technology_review_Materials'),
(u'Biomedicine', u'http://feeds.technologyreview.com/technology_review_Biotech'),
(u'Business', u'http://feeds.technologyreview.com/technology_review_Biztech')
]
remove_attributes = ['width', 'align','cellspacing']
remove_tags = [
dict(name='div', attrs={'id':['CloseLink','footerAdDiv','copyright']}),
]
remove_tags_after = [dict(name='div', attrs={'id':'copyright'})]
def get_article_url(self, article):
return article.get('guid', article.get('id', None))
def print_version(self, url):
baseurl='http://www.technologyreview.com/printer_friendly_article.aspx?id='
split1 = string.split(url,"/")
xxx=split1 [4]
split2= string.split(xxx,"/")
s = baseurl + split2[0]
return s

View File

@ -265,9 +265,20 @@
<xsl:value-of select="@line-height"/>
<xsl:text>pt;</xsl:text>
</xsl:if>
<xsl:if test="(@align = 'just')">
<xsl:text>text-align: justify;</xsl:text>
</xsl:if>
<xsl:if test="(@align = 'cent')">
<xsl:text>text-align: center;</xsl:text>
</xsl:if>
<xsl:if test="(@align = 'left')">
<xsl:text>text-align: left;</xsl:text>
</xsl:if>
<xsl:if test="(@align = 'right')">
<xsl:text>text-align: right;</xsl:text>
</xsl:if>
</xsl:template>
<xsl:template match="rtf:inline">
<xsl:variable name="num-attrs" select="count(@*)"/>
<xsl:choose>

View File

@ -440,6 +440,9 @@ xml_entity_to_unicode = partial(entity_to_unicode, result_exceptions = {
'>' : '&gt;',
'&' : '&amp;'})
def replace_entities(raw):
return _ent_pat.sub(entity_to_unicode, raw)
def prepare_string_for_xml(raw, attribute=False):
raw = _ent_pat.sub(entity_to_unicode, raw)
raw = raw.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.8'
__version__ = '0.7.9'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -60,6 +60,9 @@ class FB2Input(InputFormatPlugin):
transform = etree.XSLT(styledoc)
result = transform(doc)
for img in result.xpath('//img[@src]'):
src = img.get('src')
img.set('src', self.binary_map.get(src, src))
open('index.xhtml', 'wb').write(transform.tostring(result))
stream.seek(0)
mi = get_metadata(stream, 'fb2')
@ -83,9 +86,15 @@ class FB2Input(InputFormatPlugin):
return os.path.join(os.getcwd(), 'metadata.opf')
def extract_embedded_content(self, doc):
self.binary_map = {}
for elem in doc.xpath('./*'):
if 'binary' in elem.tag and elem.attrib.has_key('id'):
ct = elem.get('content-type', '')
fname = elem.attrib['id']
ext = ct.rpartition('/')[-1].lower()
if ext in ('png', 'jpeg', 'jpg'):
fname += '.' + ext
self.binary_map[elem.get('id')] = fname
data = b64decode(elem.text.strip())
open(fname, 'wb').write(data)

View File

@ -368,7 +368,15 @@ class LRFInput(InputFormatPlugin):
if options.verbose > 2:
open('lrs.xml', 'wb').write(xml.encode('utf-8'))
parser = etree.XMLParser(no_network=True, huge_tree=True)
try:
doc = etree.fromstring(xml, parser=parser)
except:
self.log.warn('Failed to parse XML. Trying to recover')
parser = etree.XMLParser(no_network=True, huge_tree=True,
recover=True)
doc = etree.fromstring(xml, parser=parser)
char_button_map = {}
for x in doc.xpath('//CharButton[@refobj]'):
ro = x.get('refobj')

View File

@ -14,7 +14,8 @@ except ImportError:
from lxml import html, etree
from calibre import xml_entity_to_unicode, CurrentDir, entity_to_unicode
from calibre import xml_entity_to_unicode, CurrentDir, entity_to_unicode, \
replace_entities
from calibre.utils.filenames import ascii_filename
from calibre.utils.date import parse_date
from calibre.ptempfile import TemporaryDirectory
@ -70,7 +71,7 @@ class EXTHHeader(object):
#else:
# print 'unknown record', id, repr(content)
if title:
self.mi.title = title
self.mi.title = replace_entities(title)
def process_metadata(self, id, content, codec):
if id == 100:

View File

@ -475,7 +475,8 @@ class Style(object):
value = float(m.group(1))
unit = m.group(2)
if unit == '%':
base = base or self.width
if base is None:
base = self.width
result = (value / 100.0) * base
elif unit == 'px':
result = value * 72.0 / self._profile.dpi

View File

@ -192,12 +192,18 @@ class RTFInput(InputFormatPlugin):
from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException
self.log = log
self.log('Converting RTF to XML...')
#Name of the preprocesssed RTF file
fname = self.preprocess(stream.name)
try:
xml = self.generate_xml(fname)
except RtfInvalidCodeException, e:
raise ValueError(_('This RTF file has a feature calibre does not '
'support. Convert it to HTML first and then try it.\n%s')%e)
'''dataxml = open('dataxml.xml', 'w')
dataxml.write(xml)
dataxml.close'''
d = glob.glob(os.path.join('*_rtf_pict_dir', 'picts.rtf'))
if d:
imap = {}
@ -205,6 +211,7 @@ class RTFInput(InputFormatPlugin):
imap = self.extract_images(d[0])
except:
self.log.exception('Failed to extract images...')
self.log('Parsing XML...')
parser = etree.XMLParser(recover=True, no_network=True)
doc = etree.fromstring(xml, parser=parser)
@ -214,10 +221,10 @@ class RTFInput(InputFormatPlugin):
name = imap.get(num, None)
if name is not None:
pict.set('num', name)
self.log('Converting XML to HTML...')
inline_class = InlineClass(self.log)
styledoc = etree.fromstring(P('templates/rtf.xsl', data=True))
extensions = { ('calibre', 'inline-class') : inline_class }
transform = etree.XSLT(styledoc, extensions=extensions)
result = transform(doc)

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys
from threading import RLock
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
@ -33,10 +33,6 @@ def _config():
help=_('Send file to storage card instead of main memory by default'))
c.add_opt('confirm_delete', default=False,
help=_('Confirm before deleting'))
c.add_opt('toolbar_icon_size', default=QSize(48, 48),
help=_('Toolbar icon size')) # value QVariant.toSize
c.add_opt('show_text_in_toolbar', default=True,
help=_('Show button labels in the toolbar'))
c.add_opt('main_window_geometry', default=None,
help=_('Main window geometry')) # value QVariant.toByteArray
c.add_opt('new_version_notification', default=True,

View File

@ -58,7 +58,7 @@
<item row="9" column="0" colspan="2">
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
</item>
<item row="10" column="0">
<item row="10" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@ -43,12 +43,6 @@
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>16777215</height>
</size>
</property>
<property name="minimumContentsLength">
<number>30</number>
</property>

View File

@ -638,7 +638,6 @@ class DeviceMixin(object): # {{{
self.device_error_dialog = error_dialog(self, _('Error'),
_('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal)
self.device_connected = None
self.emailer = Emailer()
self.emailer.start()
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
@ -755,17 +754,14 @@ class DeviceMixin(object): # {{{
self.device_manager.device.__class__.get_gui_name()+\
_(' detected.'), 3000)
self.device_connected = device_kind
self.location_view.model().device_connected(self.device_manager.device)
self.refresh_ondevice_info (device_connected = True, reset_only = True)
else:
self.device_connected = None
self.status_bar.device_disconnected()
self.location_view.model().update_devices()
if self.current_view() != self.library_view:
self.book_details.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0))
self.refresh_ondevice_info (device_connected = False)
self.tool_bar.device_status_changed(bool(connected))
self.location_manager.update_devices()
self.refresh_ondevice_info(device_connected=False)
def info_read(self, job):
'''
@ -774,7 +770,8 @@ class DeviceMixin(object): # {{{
if job.failed:
return self.device_job_exception(job)
info, cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
self.location_manager.update_devices(cp, fs,
self.device_manager.device.icon)
self.status_bar.device_connected(info[0])
self.device_manager.books(Dispatcher(self.metadata_downloaded))
@ -1076,9 +1073,9 @@ class DeviceMixin(object): # {{{
dynamic.set('catalogs_to_be_synced', set([]))
if files:
remove = []
space = { self.location_view.model().free[0] : None,
self.location_view.model().free[1] : 'carda',
self.location_view.model().free[2] : 'cardb' }
space = { self.location_manager.free[0] : None,
self.location_manager.free[1] : 'carda',
self.location_manager.free[2] : 'cardb' }
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
self.upload_books(files, names, metadata,
on_card=on_card,
@ -1140,9 +1137,9 @@ class DeviceMixin(object): # {{{
dynamic.set('news_to_be_synced', set([]))
if config['upload_news_to_device'] and files:
remove = ids if del_on_upload else []
space = { self.location_view.model().free[0] : None,
self.location_view.model().free[1] : 'carda',
self.location_view.model().free[2] : 'cardb' }
space = { self.location_manager.free[0] : None,
self.location_manager.free[1] : 'carda',
self.location_manager.free[2] : 'cardb' }
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
self.upload_books(files, names, metadata,
on_card=on_card,
@ -1263,7 +1260,8 @@ class DeviceMixin(object): # {{{
self.device_job_exception(job)
return
cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
self.location_manager.update_devices(cp, fs,
self.device_manager.device.icon)
# reset the views so that up-to-date info is shown. These need to be
# here because the sony driver updates collections in sync_booklists
self.memory_view.reset()

View File

@ -0,0 +1,90 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from PyQt4.Qt import QDialog
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
from calibre.gui2 import error_dialog, choose_dir, warning_dialog
from calibre.constants import filesystem_encoding
from calibre import isbytestring, patheq
from calibre.utils.config import prefs
from calibre.gui2.wizard import move_library
class ChooseLibrary(QDialog, Ui_Dialog):
def __init__(self, db, callback, parent):
QDialog.__init__(self, parent)
self.setupUi(self)
self.db = db
self.new_db = None
self.callback = callback
self.location.initialize('choose_library_dialog')
lp = db.library_path
if isbytestring(lp):
lp = lp.decode(filesystem_encoding)
loc = unicode(self.old_location.text()).format(lp)
self.old_location.setText(loc)
self.browse_button.clicked.connect(self.choose_loc)
def choose_loc(self, *args):
loc = choose_dir(self, 'choose library location',
_('Choose location for calibre library'))
if loc is not None:
self.location.setText(loc)
def check_action(self, ac, loc):
exists = self.db.exists_at(loc)
if patheq(loc, self.db.library_path):
error_dialog(self, _('Same as current'),
_('The location %s contains the current calibre'
' library')%loc, show=True)
return False
empty = not os.listdir(loc)
if ac == 'existing' and not exists:
error_dialog(self, _('No existing library found'),
_('There is no existing calibre library at %s')%loc,
show=True)
return False
if ac in ('new', 'move') and not empty:
error_dialog(self, _('Not empty'),
_('The folder %s is not empty. Please choose an empty'
' folder')%loc,
show=True)
return False
return True
def perform_action(self, ac, loc):
if ac in ('new', 'existing'):
warning_dialog(self.parent(), _('Custom columns'),
_('If you use custom columns and they differ between '
'libraries, you will have various problems. Best '
'to ensure you have the same custom columns in each '
'library.'), show=True)
if ac in ('new', 'existing'):
prefs['library_path'] = loc
self.callback(loc)
else:
move_library(self.db.library_path, loc, self.parent(),
self.callback)
def accept(self):
action = 'move'
if self.existing_library.isChecked():
action = 'existing'
elif self.empty_library.isChecked():
action = 'new'
loc = os.path.abspath(unicode(self.location.text()).strip())
if not loc or not os.path.exists(loc) or not self.check_action(action,
loc):
return
QDialog.accept(self)
self.location.save_history()
self.perform_action(action, loc)

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>602</width>
<height>245</height>
</rect>
</property>
<property name="windowTitle">
<string>Choose your calibre library</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="old_location">
<property name="text">
<string>Your calibre library is currently located at {0}</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>New &amp;Location:</string>
</property>
<property name="buddy">
<cstring>location</cstring>
</property>
</widget>
</item>
<item row="4" column="0" colspan="4">
<widget class="QRadioButton" name="existing_library">
<property name="text">
<string>Use &amp;existing library at the new location</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0" colspan="3">
<widget class="QRadioButton" name="empty_library">
<property name="text">
<string>&amp;Create an empty library at the new location</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="3">
<widget class="QRadioButton" name="move_library">
<property name="text">
<string>&amp;Move current library to new location</string>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="7" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="browse_button">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="HistoryLineEdit" name="location"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>HistoryLineEdit</class>
<extends>QComboBox</extends>
<header>calibre/gui2/widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
from calibre.constants import iswindows, isosx
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
from calibre.gui2 import error_dialog, config, gprefs, \
open_url, open_local_file, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \
warning_dialog, ResizableDialog, question_dialog
@ -343,9 +343,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.model = library_view.model()
self.db = self.model.db
self.server = server
path = prefs['library_path']
self.location.setText(path if path else '')
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
input_map = prefs['input_format_order']
@ -808,12 +805,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
d = CheckIntegrity(self.db, self)
d.exec_()
def browse(self):
dir = choose_dir(self, 'database location dialog',
_('Select location for books'))
if dir:
self.location.setText(dir)
def accept(self):
mcs = unicode(self.max_cover_size.text()).strip()
if not re.match(r'\d+x\d+', mcs):
@ -834,7 +825,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
prefs['network_timeout'] = int(self.timeout.value())
path = unicode(self.location.text())
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
prefs['input_format_order'] = input_cols
@ -875,17 +865,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString()
config['gui_layout'] = unicode(val)
if not path or not os.path.exists(path) or not os.path.isdir(path):
d = error_dialog(self, _('Invalid database location'),
_('Invalid database location ')+path+
_('<br>Must be a directory.'))
d.exec_()
elif not os.access(path, os.W_OK):
d = error_dialog(self, _('Invalid database location'),
_('Invalid database location.<br>Cannot write to ')+path)
d.exec_()
else:
self.database_location = os.path.abspath(path)
if must_restart:
warning_dialog(self, _('Must restart'),
_('The changes you made require that Calibre be '

View File

@ -113,50 +113,6 @@
</property>
<widget class="QWidget" name="page_3">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="_2">
<item>
<widget class="QLabel" name="label">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>70</height>
</size>
</property>
<property name="text">
<string>&amp;Location of ebooks (The ebooks are stored in folders sorted by author and metadata is stored in the file metadata.db)</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>location</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="_3">
<item>
<widget class="QLineEdit" name="location"/>
</item>
<item>
<widget class="QToolButton" name="browse_button">
<property name="toolTip">
<string>Browse for the new database location</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../resources/images.qrc">
<normaloff>:/images/mimetypes/dir.svg</normaloff>:/images/mimetypes/dir.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="new_version_notification">
<property name="text">

View File

@ -60,7 +60,7 @@
<item>
<widget class="QPushButton" name="stop_all_jobs_button">
<property name="text">
<string>Stop &amp;all jobs</string>
<string>Stop &amp;all non device jobs</string>
</property>
</widget>
</item>

View File

@ -10,7 +10,7 @@ Scheduler for automated recipe downloads
from datetime import timedelta
from PyQt4.Qt import QDialog, SIGNAL, Qt, QTime, QObject, QMenu, \
QAction, QIcon, QMutex, QTimer
QAction, QIcon, QMutex, QTimer, pyqtSignal
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
from calibre.gui2.search_box import SearchBox2
@ -203,6 +203,9 @@ class Scheduler(QObject):
INTERVAL = 1 # minutes
delete_old_news = pyqtSignal(object)
start_recipe_fetch = pyqtSignal(object)
def __init__(self, parent, db):
QObject.__init__(self, parent)
self.internet_connection_failed = False
@ -225,20 +228,23 @@ class Scheduler(QObject):
self.download_all_scheduled)
self.timer = QTimer(self)
self.timer.start(int(self.INTERVAL * 60000))
self.timer.start(int(self.INTERVAL * 60 * 1000))
self.oldest_timer = QTimer()
self.connect(self.oldest_timer, SIGNAL('timeout()'), self.oldest_check)
self.connect(self.timer, SIGNAL('timeout()'), self.check)
self.oldest = gconf['oldest_news']
self.oldest_timer.start(int(60 * 60000))
self.oldest_check()
self.oldest_timer.start(int(60 * 60 * 1000))
QTimer.singleShot(5 * 1000, self.oldest_check)
self.database_changed = self.recipe_model.database_changed
def oldest_check(self):
if self.oldest > 0:
delta = timedelta(days=self.oldest)
ids = self.recipe_model.db.tags_older_than(_('News'), delta)
if ids:
self.emit(SIGNAL('delete_old_news(PyQt_PyObject)'), ids)
ids = list(ids)
if ids:
self.delete_old_news.emit(ids)
def show_dialog(self, *args):
self.lock.lock()
@ -282,7 +288,7 @@ class Scheduler(QObject):
'urn':urn,
}
self.download_queue.add(urn)
self.emit(SIGNAL('start_recipe_fetch(PyQt_PyObject)'), arg)
self.start_recipe_fetch.emit(arg)
finally:
self.lock.unlock()

View File

@ -7,14 +7,13 @@ __docformat__ = 'restructuredtext en'
import functools, sys, os
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QIcon, QStackedWidget, \
QSize, QSizePolicy, QStatusBar, QUrl, QLabel, QFont
from PyQt4.Qt import QMenu, Qt, QStackedWidget, \
QSize, QSizePolicy, QStatusBar, QLabel, QFont
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import isosx, __appname__, preferred_encoding, \
__version__
from calibre.gui2 import config, is_widescreen, open_url
from calibre.gui2 import config, is_widescreen
from calibre.gui2.library.views import BooksView, DeviceBooksView
from calibre.gui2.widgets import Splitter
from calibre.gui2.tag_view import TagBrowserWidget
@ -28,157 +27,6 @@ def partial(*args, **kwargs):
_keep_refs.append(ans)
return ans
class SaveMenu(QMenu): # {{{
save_fmt = pyqtSignal(object)
def __init__(self, parent):
QMenu.__init__(self, _('Save single format to disk...'), parent)
for ext in sorted(BOOK_EXTENSIONS):
action = self.addAction(ext.upper())
setattr(self, 'do_'+ext, partial(self.do, ext))
action.triggered.connect(
getattr(self, 'do_'+ext))
def do(self, ext, *args):
self.save_fmt.emit(ext)
# }}}
class ToolbarMixin(object): # {{{
def __init__(self):
self.action_help.triggered.connect(self.show_help)
md = QMenu()
md.addAction(_('Edit metadata individually'),
partial(self.edit_metadata, False, bulk=False))
md.addSeparator()
md.addAction(_('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True))
md.addSeparator()
md.addAction(_('Download metadata and covers'),
partial(self.download_metadata, False, covers=True),
Qt.ControlModifier+Qt.Key_D)
md.addAction(_('Download only metadata'),
partial(self.download_metadata, False, covers=False))
md.addAction(_('Download only covers'),
partial(self.download_metadata, False, covers=True,
set_metadata=False, set_social_metadata=False))
md.addAction(_('Download only social metadata'),
partial(self.download_metadata, False, covers=False,
set_metadata=False, set_social_metadata=True))
self.metadata_menu = md
mb = QMenu()
mb.addAction(_('Merge into first selected book - delete others'),
self.merge_books)
mb.addSeparator()
mb.addAction(_('Merge into first selected book - keep others'),
partial(self.merge_books, safe_merge=True))
self.merge_menu = mb
self.action_merge.setMenu(mb)
md.addSeparator()
md.addAction(self.action_merge)
self.add_menu = QMenu()
self.add_menu.addAction(_('Add books from a single directory'),
self.add_books)
self.add_menu.addAction(_('Add books from directories, including '
'sub-directories (One book per directory, assumes every ebook '
'file is the same book in a different format)'),
self.add_recursive_single)
self.add_menu.addAction(_('Add books from directories, including '
'sub directories (Multiple books per directory, assumes every '
'ebook file is a different book)'), self.add_recursive_multiple)
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
'formats)'), self.add_empty)
self.action_add.setMenu(self.add_menu)
self.action_add.triggered.connect(self.add_books)
self.action_del.triggered.connect(self.delete_books)
self.action_edit.triggered.connect(self.edit_metadata)
self.action_merge.triggered.connect(self.merge_books)
self.action_save.triggered.connect(self.save_to_disk)
self.save_menu = QMenu()
self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk,
False))
self.save_menu.addAction(_('Save to disk in a single directory'),
partial(self.save_to_single_dir, False))
self.save_menu.addAction(_('Save only %s format to disk')%
prefs['output_format'].upper(),
partial(self.save_single_format_to_disk, False))
self.save_menu.addAction(
_('Save only %s format to disk in a single directory')%
prefs['output_format'].upper(),
partial(self.save_single_fmt_to_single_dir, False))
self.save_sub_menu = SaveMenu(self)
self.save_menu.addMenu(self.save_sub_menu)
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
self.action_view.triggered.connect(self.view_book)
self.view_menu = QMenu()
self.view_menu.addAction(_('View'), partial(self.view_book, False))
ac = self.view_menu.addAction(_('View specific format'))
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
self.action_view.setMenu(self.view_menu)
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
self.delete_menu = QMenu()
self.delete_menu.addAction(_('Remove selected books'), self.delete_books)
self.delete_menu.addAction(
_('Remove files of a specific format from selected books..'),
self.delete_selected_formats)
self.delete_menu.addAction(
_('Remove all formats from selected books, except...'),
self.delete_all_but_selected_formats)
self.delete_menu.addAction(
_('Remove covers from selected books'), self.delete_covers)
self.delete_menu.addSeparator()
self.delete_menu.addAction(
_('Remove matching books from device'),
self.remove_matching_books_from_device)
self.action_del.setMenu(self.delete_menu)
self.action_open_containing_folder.setShortcut(Qt.Key_O)
self.addAction(self.action_open_containing_folder)
self.action_open_containing_folder.triggered.connect(self.view_folder)
self.action_sync.setShortcut(Qt.Key_D)
self.action_sync.setEnabled(True)
self.create_device_menu()
self.action_sync.triggered.connect(
self._sync_action_triggered)
self.action_edit.setMenu(md)
self.action_save.setMenu(self.save_menu)
cm = QMenu()
cm.addAction(_('Convert individually'), partial(self.convert_ebook,
False, bulk=False))
cm.addAction(_('Bulk convert'),
partial(self.convert_ebook, False, bulk=True))
cm.addSeparator()
ac = cm.addAction(
_('Create catalog of books in your calibre library'))
ac.triggered.connect(self.generate_catalog)
self.action_convert.setMenu(cm)
self.action_convert.triggered.connect(self.convert_ebook)
self.convert_menu = cm
pm = QMenu()
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
self.run_wizard)
self.action_preferences.setMenu(pm)
self.preferences_menu = pm
for x in (self.preferences_action, self.action_preferences):
x.triggered.connect(self.do_config)
def show_help(self, *args):
open_url(QUrl('http://calibre-ebook.com/user_manual'))
# }}}
class LibraryViewMixin(object): # {{{
def __init__(self, db):

View File

@ -6,108 +6,106 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from operator import attrgetter
from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, QVariant, \
QAbstractListModel, QFont, QApplication, QPalette, pyqtSignal, QToolButton, \
QModelIndex, QListView, QAbstractButton, QPainter, QPixmap, QColor, \
QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \
pyqtSignal, QToolButton, \
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \
QMenu, QUrl
from calibre.constants import __appname__, filesystem_encoding
from calibre.constants import __appname__, isosx
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2 import NONE, config
from calibre.gui2 import config, open_url
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre import human_readable
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2.dialogs.scheduler import Scheduler
ICON_SIZE = 48
# Location View {{{
class SaveMenu(QMenu): # {{{
class LocationModel(QAbstractListModel): # {{{
devicesChanged = pyqtSignal()
save_fmt = pyqtSignal(object)
def __init__(self, parent):
QAbstractListModel.__init__(self, parent)
self.icons = [QVariant(QIcon(I('library.png'))),
QVariant(QIcon(I('reader.svg'))),
QVariant(QIcon(I('sd.svg'))),
QVariant(QIcon(I('sd.svg')))]
self.text = [_('Library\n%d books'),
_('Reader\n%s'),
_('Card A\n%s'),
_('Card B\n%s')]
QMenu.__init__(self, _('Save single format to disk...'), parent)
for ext in sorted(BOOK_EXTENSIONS):
action = self.addAction(ext.upper())
setattr(self, 'do_'+ext, partial(self.do, ext))
action.triggered.connect(
getattr(self, 'do_'+ext))
def do(self, ext, *args):
self.save_fmt.emit(ext)
# }}}
class LocationManager(QObject): # {{{
locations_changed = pyqtSignal()
unmount_device = pyqtSignal()
location_selected = pyqtSignal(object)
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.free = [-1, -1, -1]
self.count = 0
self.highlight_row = 0
self.library_tooltip = _('Click to see the books available on your computer')
self.tooltips = [
self.library_tooltip,
_('Click to see the books in the main memory of your reader'),
_('Click to see the books on storage card A in your reader'),
_('Click to see the books on storage card B in your reader')
]
self.location_actions = QActionGroup(self)
self.location_actions.setExclusive(True)
self.current_location = 'library'
self._mem = []
self.tooltips = {}
def database_changed(self, db):
lp = db.library_path
if not isinstance(lp, unicode):
lp = lp.decode(filesystem_encoding, 'replace')
self.tooltips[0] = self.library_tooltip + '\n\n' + \
_('Books located at') + ' ' + lp
self.dataChanged.emit(self.index(0), self.index(0))
def ac(name, text, icon, tooltip):
icon = QIcon(I(icon))
ac = self.location_actions.addAction(icon, text)
setattr(self, 'location_'+name, ac)
ac.setAutoRepeat(False)
ac.setCheckable(True)
receiver = partial(self._location_selected, name)
ac.triggered.connect(receiver)
self.tooltips[name] = tooltip
if name != 'library':
m = QMenu(parent)
self._mem.append(m)
a = m.addAction(icon, tooltip)
a.triggered.connect(receiver)
self._mem.append(a)
a = m.addAction(QIcon(I('eject.svg')), _('Eject this device'))
a.triggered.connect(self._eject_requested)
ac.setMenu(m)
self._mem.append(a)
else:
ac.setToolTip(tooltip)
def rowCount(self, *args):
return 1 + len([i for i in self.free if i >= 0])
return ac
def get_device_row(self, row):
if row == 2 and self.free[1] == -1 and self.free[2] > -1:
row = 3
return row
ac('library', _('Library'), 'lt.png',
_('Show books in calibre library'))
ac('main', _('Reader'), 'reader.svg',
_('Show books in the main memory of the device'))
ac('carda', _('Card A'), 'sd.svg',
_('Show books in storage card A'))
ac('cardb', _('Card B'), 'sd.svg',
_('Show books in storage card B'))
def get_tooltip(self, row, drow):
ans = self.tooltips[row]
if row > 0:
fs = self.free[drow-1]
if fs > -1:
ans += '\n\n%s '%(human_readable(fs)) + _('free')
return ans
def _location_selected(self, location, *args):
if location != self.current_location and hasattr(self,
'location_'+location):
self.current_location = location
self.location_selected.emit(location)
getattr(self, 'location_'+location).setChecked(True)
def data(self, index, role):
row = index.row()
drow = self.get_device_row(row)
data = NONE
if role == Qt.DisplayRole:
text = self.text[drow]%(human_readable(self.free[drow-1])) if row > 0 \
else self.text[drow]%self.count
data = QVariant(text)
elif role == Qt.DecorationRole:
data = self.icons[drow]
elif role in (Qt.ToolTipRole, Qt.StatusTipRole):
ans = self.get_tooltip(row, drow)
data = QVariant(ans)
elif role == Qt.SizeHintRole:
data = QVariant(QSize(155, 90))
elif role == Qt.FontRole:
font = QFont('monospace')
font.setBold(row == self.highlight_row)
data = QVariant(font)
elif role == Qt.ForegroundRole and row == self.highlight_row:
return QVariant(QApplication.palette().brush(
QPalette.HighlightedText))
elif role == Qt.BackgroundRole and row == self.highlight_row:
return QVariant(QApplication.palette().brush(
QPalette.Highlight))
def _eject_requested(self, *args):
self.unmount_device.emit()
return data
def device_connected(self, dev):
self.icons[1] = QIcon(dev.icon)
self.dataChanged.emit(self.index(1), self.index(1))
def headerData(self, section, orientation, role):
return NONE
def update_devices(self, cp=(None, None), fs=[-1, -1, -1]):
def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None):
if icon is None:
icon = I('reader.svg')
self.location_main.setIcon(QIcon(icon))
had_device = self.has_device
if cp is None:
cp = (None, None)
if isinstance(cp, (str, unicode)):
@ -120,137 +118,34 @@ class LocationModel(QAbstractListModel): # {{{
cpa, cpb = cp
self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1
self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1
self.reset()
self.devicesChanged.emit()
self.update_tooltips()
if self.has_device != had_device:
self.locations_changed.emit()
if not self.has_device:
self.location_library.trigger()
def location_changed(self, row):
self.highlight_row = row
self.dataChanged.emit(
self.index(0), self.index(self.rowCount(QModelIndex())-1))
def update_tooltips(self):
for i, loc in enumerate(('main', 'carda', 'cardb')):
t = self.tooltips[loc]
if self.free[i] > -1:
t += u'\n\n%s '%human_readable(self.free[i]) + _('available')
ac = getattr(self, 'location_'+loc)
ac.setToolTip(t)
ac.setWhatsThis(t)
ac.setStatusTip(t)
def location_for_row(self, row):
if row == 0: return 'library'
if row == 1: return 'main'
if row == 3: return 'cardb'
return 'carda' if self.free[1] > -1 else 'cardb'
# }}}
class LocationView(QListView):
umount_device = pyqtSignal()
location_selected = pyqtSignal(object)
def __init__(self, parent):
QListView.__init__(self, parent)
self.setModel(LocationModel(self))
self.reset()
self.currentChanged = self.current_changed
self.eject_button = EjectButton(self)
self.eject_button.hide()
self.entered.connect(self.item_entered)
self.viewportEntered.connect(self.viewport_entered)
self.eject_button.clicked.connect(self.eject_clicked)
self.model().devicesChanged.connect(self.eject_button.hide)
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding))
self.setMouseTracking(True)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setEditTriggers(self.NoEditTriggers)
self.setTabKeyNavigation(True)
self.setProperty("showDropIndicator", True)
self.setSelectionMode(self.SingleSelection)
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setMovement(self.Static)
self.setFlow(self.LeftToRight)
self.setGridSize(QSize(175, ICON_SIZE))
self.setViewMode(self.ListMode)
self.setWordWrap(True)
self.setObjectName("location_view")
self.setMaximumSize(QSize(600, ICON_SIZE+16))
self.setMinimumWidth(400)
def eject_clicked(self, *args):
self.umount_device.emit()
def count_changed(self, new_count):
self.model().count = new_count
self.model().reset()
@property
def book_count(self):
return self.model().count
def current_changed(self, current, previous):
if current.isValid():
i = current.row()
location = self.model().location_for_row(i)
self.location_selected.emit(location)
self.model().location_changed(i)
def location_changed(self, row):
if 0 <= row and row <= 3:
self.model().location_changed(row)
def leaveEvent(self, event):
self.unsetCursor()
self.eject_button.hide()
def item_entered(self, location):
self.setCursor(Qt.PointingHandCursor)
self.eject_button.hide()
if location.row() == 1:
rect = self.visualRect(location)
self.eject_button.resize(rect.height()/2, rect.height()/2)
x, y = rect.left(), rect.top()
x = x + (rect.width() - self.eject_button.width() - 2)
y += 6
self.eject_button.move(x, y)
self.eject_button.show()
def viewport_entered(self):
self.unsetCursor()
self.eject_button.hide()
class EjectButton(QAbstractButton):
def __init__(self, parent):
QAbstractButton.__init__(self, parent)
self.mouse_over = False
self.setMouseTracking(True)
def enterEvent(self, event):
self.mouse_over = True
QAbstractButton.enterEvent(self, event)
def leaveEvent(self, event):
self.mouse_over = False
QAbstractButton.leaveEvent(self, event)
def paintEvent(self, event):
painter = QPainter(self)
painter.setClipRect(event.rect())
image = QPixmap(I('eject')).scaledToHeight(event.rect().height(),
Qt.SmoothTransformation)
if not self.mouse_over:
alpha_mask = QPixmap(image.width(), image.height())
color = QColor(128, 128, 128)
alpha_mask.fill(color)
image.setAlphaChannel(alpha_mask)
painter.drawPixmap(0, 0, image)
def has_device(self):
return max(self.free) > -1
@property
def available_actions(self):
ans = [self.location_library]
for i, loc in enumerate(('main', 'carda', 'cardb')):
if self.free[i] > -1:
ans.append(getattr(self, 'location_'+loc))
return ans
# }}}
@ -326,7 +221,7 @@ class SearchBar(QWidget): # {{{
class ToolBar(QToolBar): # {{{
def __init__(self, actions, donate, location_view, parent=None):
def __init__(self, actions, donate, location_manager, parent=None):
QToolBar.__init__(self, parent)
self.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False)
@ -335,11 +230,12 @@ class ToolBar(QToolBar): # {{{
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
self.showing_device = False
self.all_actions = actions
self.donate = donate
self.location_view = location_view
self.location_manager = location_manager
self.location_manager.locations_changed.connect(self.build_bar)
self.d_widget = QWidget()
self.d_widget.setLayout(QVBoxLayout())
self.d_widget.layout().addWidget(donate)
@ -350,40 +246,45 @@ class ToolBar(QToolBar): # {{{
def contextMenuEvent(self, *args):
pass
def device_status_changed(self, connected):
self.showing_device = connected
self.build_bar()
def build_bar(self):
order_field = 'device' if self.showing_device else 'normal'
showing_device = self.location_manager.has_device
order_field = 'device' if showing_device else 'normal'
o = attrgetter(order_field+'_order')
sepvals = [2] if self.showing_device else [1]
sepvals = [2] if showing_device else [1]
sepvals += [3]
actions = [x for x in self.all_actions if o(x) > -1]
actions.sort(cmp=lambda x,y : cmp(o(x), o(y)))
self.clear()
for x in actions:
self.addAction(x)
ch = self.widgetForAction(x)
def setup_tool_button(ac):
ch = self.widgetForAction(ac)
ch.setCursor(Qt.PointingHandCursor)
ch.setAutoRaise(True)
if x.action_name == 'choose_library':
self.location_action = self.addWidget(self.location_view)
self.choose_action = x
if config['show_donate_button']:
self.addWidget(self.d_widget)
if x.action_name not in ('choose_library', 'help'):
if ac.menu() is not None:
ch.setPopupMode(ch.MenuButtonPopup)
for x in actions:
self.addAction(x)
setup_tool_button(x)
if x.action_name == 'choose_library':
self.choose_action = x
if showing_device:
self.addSeparator()
for ac in self.location_manager.available_actions:
self.addAction(ac)
setup_tool_button(ac)
self.addSeparator()
self.location_manager.location_library.trigger()
elif config['show_donate_button']:
self.addWidget(self.d_widget)
for x in actions:
if x.separator_before in sepvals:
self.insertSeparator(x)
self.location_action.setVisible(self.showing_device)
self.choose_action.setVisible(not self.showing_device)
self.choose_action.setVisible(not showing_device)
def count_changed(self, new_count):
text = _('%d books')%new_count
@ -397,6 +298,9 @@ class ToolBar(QToolBar): # {{{
self.setToolButtonStyle(style)
QToolBar.resizeEvent(self, ev)
def database_changed(self, db):
pass
# }}}
class Action(QAction):
@ -404,7 +308,8 @@ class Action(QAction):
class MainWindowMixin(object):
def __init__(self):
def __init__(self, db):
self.device_connected = None
self.setObjectName('MainWindow')
self.setWindowIcon(QIcon(I('library.png')))
self.setWindowTitle(__appname__)
@ -417,9 +322,36 @@ class MainWindowMixin(object):
self.resize(1012, 740)
self.donate_button = ThrobbingButton(self.centralwidget)
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
self.location_manager = LocationManager(self)
# Actions {{{
self.init_scheduler(db)
all_actions = self.setup_actions()
self.search_bar = SearchBar(self)
self.tool_bar = ToolBar(all_actions, self.donate_button,
self.location_manager, self)
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
self.tool_bar.choose_action.triggered.connect(self.choose_library)
l = self.centralwidget.layout()
l.addWidget(self.search_bar)
def init_scheduler(self, db):
self.scheduler = Scheduler(self, db)
self.scheduler.start_recipe_fetch.connect(
self.download_scheduled_recipe, type=Qt.QueuedConnection)
def read_toolbar_settings(self):
pass
def choose_library(self, *args):
from calibre.gui2.dialogs.choose_library import ChooseLibrary
db = self.library_view.model().db
c = ChooseLibrary(db, self.library_moved, self)
c.exec_()
def setup_actions(self): # {{{
all_actions = []
def ac(normal_order, device_order, separator_before,
@ -467,17 +399,139 @@ class MainWindowMixin(object):
ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
'tags.svg')
self.action_news.setMenu(self.scheduler.news_menu)
self.action_news.triggered.connect(
self.scheduler.show_dialog)
self.action_help.triggered.connect(self.show_help)
md = QMenu()
md.addAction(_('Edit metadata individually'),
partial(self.edit_metadata, False, bulk=False))
md.addSeparator()
md.addAction(_('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True))
md.addSeparator()
md.addAction(_('Download metadata and covers'),
partial(self.download_metadata, False, covers=True),
Qt.ControlModifier+Qt.Key_D)
md.addAction(_('Download only metadata'),
partial(self.download_metadata, False, covers=False))
md.addAction(_('Download only covers'),
partial(self.download_metadata, False, covers=True,
set_metadata=False, set_social_metadata=False))
md.addAction(_('Download only social metadata'),
partial(self.download_metadata, False, covers=False,
set_metadata=False, set_social_metadata=True))
self.metadata_menu = md
mb = QMenu()
mb.addAction(_('Merge into first selected book - delete others'),
self.merge_books)
mb.addSeparator()
mb.addAction(_('Merge into first selected book - keep others'),
partial(self.merge_books, safe_merge=True))
self.merge_menu = mb
self.action_merge.setMenu(mb)
md.addSeparator()
md.addAction(self.action_merge)
self.add_menu = QMenu()
self.add_menu.addAction(_('Add books from a single directory'),
self.add_books)
self.add_menu.addAction(_('Add books from directories, including '
'sub-directories (One book per directory, assumes every ebook '
'file is the same book in a different format)'),
self.add_recursive_single)
self.add_menu.addAction(_('Add books from directories, including '
'sub directories (Multiple books per directory, assumes every '
'ebook file is a different book)'), self.add_recursive_multiple)
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
'formats)'), self.add_empty)
self.action_add.setMenu(self.add_menu)
self.action_add.triggered.connect(self.add_books)
self.action_del.triggered.connect(self.delete_books)
self.action_edit.triggered.connect(self.edit_metadata)
self.action_merge.triggered.connect(self.merge_books)
self.action_save.triggered.connect(self.save_to_disk)
self.save_menu = QMenu()
self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk,
False))
self.save_menu.addAction(_('Save to disk in a single directory'),
partial(self.save_to_single_dir, False))
self.save_menu.addAction(_('Save only %s format to disk')%
prefs['output_format'].upper(),
partial(self.save_single_format_to_disk, False))
self.save_menu.addAction(
_('Save only %s format to disk in a single directory')%
prefs['output_format'].upper(),
partial(self.save_single_fmt_to_single_dir, False))
self.save_sub_menu = SaveMenu(self)
self.save_menu.addMenu(self.save_sub_menu)
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
self.action_view.triggered.connect(self.view_book)
self.view_menu = QMenu()
self.view_menu.addAction(_('View'), partial(self.view_book, False))
ac = self.view_menu.addAction(_('View specific format'))
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
self.action_view.setMenu(self.view_menu)
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
self.delete_menu = QMenu()
self.delete_menu.addAction(_('Remove selected books'), self.delete_books)
self.delete_menu.addAction(
_('Remove files of a specific format from selected books..'),
self.delete_selected_formats)
self.delete_menu.addAction(
_('Remove all formats from selected books, except...'),
self.delete_all_but_selected_formats)
self.delete_menu.addAction(
_('Remove covers from selected books'), self.delete_covers)
self.delete_menu.addSeparator()
self.delete_menu.addAction(
_('Remove matching books from device'),
self.remove_matching_books_from_device)
self.action_del.setMenu(self.delete_menu)
self.action_open_containing_folder.setShortcut(Qt.Key_O)
self.addAction(self.action_open_containing_folder)
self.action_open_containing_folder.triggered.connect(self.view_folder)
self.action_sync.setShortcut(Qt.Key_D)
self.action_sync.setEnabled(True)
self.create_device_menu()
self.action_sync.triggered.connect(
self._sync_action_triggered)
self.action_edit.setMenu(md)
self.action_save.setMenu(self.save_menu)
cm = QMenu()
cm.addAction(_('Convert individually'), partial(self.convert_ebook,
False, bulk=False))
cm.addAction(_('Bulk convert'),
partial(self.convert_ebook, False, bulk=True))
cm.addSeparator()
ac = cm.addAction(
_('Create catalog of books in your calibre library'))
ac.triggered.connect(self.generate_catalog)
self.action_convert.setMenu(cm)
self.action_convert.triggered.connect(self.convert_ebook)
self.convert_menu = cm
pm = QMenu()
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
self.run_wizard)
self.action_preferences.setMenu(pm)
self.preferences_menu = pm
for x in (self.preferences_action, self.action_preferences):
x.triggered.connect(self.do_config)
return all_actions
# }}}
self.location_view = LocationView(self.centralwidget)
self.search_bar = SearchBar(self)
self.tool_bar = ToolBar(all_actions, self.donate_button, self.location_view, self)
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
l = self.centralwidget.layout()
l.addWidget(self.search_bar)
def show_help(self, *args):
open_url(QUrl('http://calibre-ebook.com/user_manual'))
def read_toolbar_settings(self):
pass

View File

@ -12,13 +12,13 @@ __docformat__ = 'restructuredtext en'
import collections, os, sys, textwrap, time
from Queue import Queue, Empty
from threading import Thread
from PyQt4.Qt import Qt, SIGNAL, QObject, QTimer, \
from PyQt4.Qt import Qt, SIGNAL, QTimer, \
QPixmap, QMenu, QIcon, pyqtSignal, \
QDialog, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
QMessageBox, QHelpEvent
from calibre import prints, patheq
from calibre import prints
from calibre.constants import __appname__, isosx
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic
@ -27,8 +27,6 @@ from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
gprefs, max_available_height, config, info_dialog
from calibre.gui2.cover_flow import CoverFlowMixin
from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2.wizard import move_library
from calibre.gui2.dialogs.scheduler import Scheduler
from calibre.gui2.update import UpdateMixin
from calibre.gui2.main_window import MainWindow
from calibre.gui2.layout import MainWindowMixin
@ -38,7 +36,7 @@ from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.book_info import BookInfo
from calibre.library.database2 import LibraryDatabase2
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_view import TagBrowserMixin
@ -91,7 +89,7 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
# }}}
class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
AnnotationsAction, AddAction, DeleteAction,
@ -120,7 +118,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.another_instance_wants_to_talk)
self.check_messages_timer.start(1000)
MainWindowMixin.__init__(self)
MainWindowMixin.__init__(self, db)
# Jobs Button {{{
self.job_manager = JobManager()
@ -192,21 +190,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
####################### Start spare job server ########################
QTimer.singleShot(1000, self.add_spare_server)
####################### Location View ########################
QObject.connect(self.location_view,
SIGNAL('location_selected(PyQt_PyObject)'),
self.location_selected)
QObject.connect(self.location_view,
SIGNAL('umount_device()'),
self.device_manager.umount_device)
####################### Location Manager ########################
self.location_manager.location_selected.connect(self.location_selected)
self.location_manager.unmount_device.connect(self.device_manager.umount_device)
self.eject_action.triggered.connect(self.device_manager.umount_device)
#################### Update notification ###################
UpdateMixin.__init__(self, opts)
####################### Setup Toolbar #####################
ToolbarMixin.__init__(self)
####################### Search boxes ########################
SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__init__(self)
@ -218,7 +209,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows()
for t in (self.location_view, self.tool_bar):
for t in (self.tool_bar, ):
self.library_view.model().count_changed_signal.connect \
(t.count_changed)
if not gprefs.get('quick_start_guide_added', False):
@ -235,8 +226,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.db_images.reset()
self.library_view.model().count_changed()
self.location_view.model().database_changed(self.library_view.model().db)
self.library_view.model().database_changed.connect(self.location_view.model().database_changed,
self.tool_bar.database_changed(self.library_view.model().db)
self.library_view.model().database_changed.connect(self.tool_bar.database_changed,
type=Qt.QueuedConnection)
########################### Tags Browser ##############################
@ -261,18 +252,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
db, server_config().parse())
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
self.scheduler = Scheduler(self, self.library_view.model().db)
self.action_news.setMenu(self.scheduler.news_menu)
self.connect(self.action_news, SIGNAL('triggered(bool)'),
self.scheduler.show_dialog)
self.connect(self.scheduler, SIGNAL('delete_old_news(PyQt_PyObject)'),
self.library_view.model().delete_books_by_id,
Qt.QueuedConnection)
self.connect(self.scheduler,
SIGNAL('start_recipe_fetch(PyQt_PyObject)'),
self.download_scheduled_recipe, Qt.QueuedConnection)
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
AddAction.__init__(self)
@ -280,6 +259,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.finalize_layout()
self.donate_button.start_animation()
self.scheduler.delete_old_news.connect(
self.library_view.model().delete_books_by_id,
type=Qt.QueuedConnection)
def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev)
self.search.setMaximumWidth(self.width()-150)
@ -396,10 +380,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.tags_view.recount()
self.create_device_menu()
self.set_device_menu_items_state(bool(self.device_connected))
if not patheq(self.library_path, d.database_location):
newloc = d.database_location
move_library(self.library_path, newloc, self,
self.library_moved)
def library_moved(self, newloc):
if newloc is None: return
@ -414,6 +394,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.search.clear_to_help()
self.book_details.reset_info()
self.library_view.model().count_changed()
self.scheduler.database_changed(db)
prefs['library_path'] = self.library_path
def show_book_info(self, *args):

View File

@ -116,6 +116,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# missing functions
self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter')
@classmethod
def exists_at(cls, path):
return path and os.path.exists(os.path.join(path, 'metadata.db'))
def __init__(self, library_path, row_factory=False):
self.field_metadata = FieldMetadata()
if not os.path.exists(library_path):

View File

@ -104,30 +104,46 @@ will appear in the next release of |app|.
How does |app| manage collections on my SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When |app| connects with the device, it retrieves all collections for the books on the device. The collections
When |app| connects with the reader, it retrieves all collections for the books on the reader. The collections
of which books are members are shown on the device view.
When you send a book to the device, |app| will add the book to collections based on the metadata for that book. By
When you send a book to the reader, |app| will add the book to collections based on the metadata for that book. By
default, collections are created from tags and series. You can control what metadata is used by going to
Preferences->Plugins->Device Interface plugins and customizing the SONY device interface plugin. If you remove all
values, |app| will not add the book to any collection.
Collection management is largely controlled by 'Preserve device collections' found at Preferences->Add/Save->Sending
to device. If checked (the default), managing collections is left to the user; |app| will not delete already
existing collections for a book on your device when you resend the book to the device, but |app| will add the book to
collections if necessary. To ensure that the collections for a book are based only on current |app| metadata, first
delete the books from the device, then resend the books. You can edit collections directly on the device view by
double-clicking or right-clicking in the collections column.
Collection management is largely controlled by the 'Metadata management' option found at
Preferences->Add/Save->Sending to device. If set to 'Manual' (the default), managing collections is left to
the user; |app| will not delete already existing collections for a book on your reader when you resend the
book to the reader, but |app| will add the book to collections if necessary. To ensure that the collections
for a book are based only on current |app| metadata, first delete the books from the reader, then resend the
books. You can edit collections directly on the device view by double-clicking or right-clicking in the
collections column.
If 'Preserve device collections' is not checked, then |app| will manage collections. Collections will be built using
|app| metadata exclusively. Sending a book to the device will correct the collections for that book so its
collections exactly match the book's metadata. Collections are added and deleted as necessary. Editing collections on
the device pane is not permitted, because collections not in the metadata will be removed automatically.
If 'Metadata management' is set to 'Only on send', then |app| will manage collections more aggressively.
Collections will be built using |app| metadata exclusively. Sending a book to the reader will correct the
collections for that book so its collections exactly match the book's metadata, adding and deleting
collections as necessary. Editing collections on the device view is not permitted, because collections not in
the metadata will be removed automatically.
In summary, check 'Preserve device collections' if you want to manage collections yourself. Collections for a book
will never be removed by |app|, but can be removed by you by editing on the device view. Uncheck 'Preserve device
collections' if you want |app| to manage the collections, adding books to and removing books from collections as
needed.
If 'Metadata management' is set to 'Automatic management', then |app| will update metadata and collections
both when the reader is connected and when books are sent. When calibre detects the reader and generates the
list of books on the reader, it will send metadata from the library to the reader for all books on the reader
that are in the library (On device is True), adding and removing books from collections as indicated by the
metadata and device customization. When a book is sent, |app| corrects the metadata for that book, adding and
deleting collections. Manual editing of metadata on the device view is not allowed. Note that this option
specifies sending metadata, not books. The book files on the reader are not changed.
In summary, choose 'manual management' if you want to manage collections yourself. Collections for a book
will never be removed by |app|, but can be removed by you by editing on the device view. Choose 'Only on
send' if you want |app| to manage collections when you send a book, adding books to and removing books from
collections as needed. Choose 'Automatic management' if you want |app| to keep collections up to date
whenever the reader is connected.
If you use multiple installations of calibre to manage your reader, then option 'Automatic management' may not
be what you want. Connecting the reader to one library will reset the metadata to what is in that library.
Connecting to the other library will reset the metadata to what is in that other library. Metadata in books
found in both libraries will be flopped back and forth.
Can I use both |app| and the SONY software to manage my reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -65,7 +65,7 @@
__docformat__ = 'reStructuredText'
__author__ = 'sengian <sengian1 at gmail.com>'
import os, re, string
import re, string
utf8enc2latex_mapping = {
# This is a mapping of Unicode characters to LaTeX equivalents.

View File

@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
import os, copy
from PyQt4.Qt import QAbstractItemModel, QVariant, Qt, QColor, QFont, QIcon, \
QModelIndex, SIGNAL
QModelIndex, SIGNAL, QMetaObject, pyqtSlot
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.gui2 import NONE
@ -131,6 +131,16 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
self.scheduler_config = SchedulerConfig()
self.do_refresh()
@pyqtSlot()
def do_database_change(self):
self.db = self.newdb
self.newdb = None
self.do_refresh()
def database_changed(self, db):
self.newdb = db
QMetaObject.invokeMethod(self, 'do_database_change', Qt.QueuedConnection)
def get_builtin_recipe(self, urn, download=True):
if download:
try: