Merge from trunk

This commit is contained in:
Charles Haley 2010-07-17 10:00:21 +01:00
commit ac159887ed
23 changed files with 3822 additions and 567 deletions

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_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

@ -265,9 +265,20 @@
<xsl:value-of select="@line-height"/> <xsl:value-of select="@line-height"/>
<xsl:text>pt;</xsl:text> <xsl:text>pt;</xsl:text>
</xsl:if> </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>
<xsl:template match="rtf:inline"> <xsl:template match="rtf:inline">
<xsl:variable name="num-attrs" select="count(@*)"/> <xsl:variable name="num-attrs" select="count(@*)"/>
<xsl:choose> <xsl:choose>

View File

@ -361,6 +361,8 @@ def strftime(fmt, t=None):
before 1900 ''' before 1900 '''
if t is None: if t is None:
t = time.localtime() t = time.localtime()
if hasattr(t, 'timetuple'):
t = t.timetuple()
early_year = t[0] < 1900 early_year = t[0] < 1900
if early_year: if early_year:
replacement = 1900 if t[0]%4 == 0 else 1901 replacement = 1900 if t[0]%4 == 0 else 1901

View File

@ -446,7 +446,7 @@ from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
BOOQ, ELONEX, POCKETBOOK301, MENTOR BOOQ, ELONEX, POCKETBOOK301, MENTOR
from calibre.devices.iliad.driver import ILIAD from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800 from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK from calibre.devices.jetbook.driver import JETBOOK, MIBUK
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
from calibre.devices.nook.driver import NOOK from calibre.devices.nook.driver import NOOK
from calibre.devices.prs505.driver import PRS505 from calibre.devices.prs505.driver import PRS505
@ -467,12 +467,12 @@ from calibre.devices.kobo.driver import KOBO
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \ from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing LibraryThing
from calibre.ebooks.metadata.douban import DoubanBooks from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.library.catalog import CSV_XML, EPUB_MOBI from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested from calibre.ebooks.epub.fix.unmanifested import Unmanifested
from calibre.ebooks.epub.fix.epubcheck import Epubcheck from calibre.ebooks.epub.fix.epubcheck import Epubcheck
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon, plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, Unmanifested, Epubcheck] LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested, Epubcheck]
plugins += [ plugins += [
ComicInput, ComicInput,
EPUBInput, EPUBInput,
@ -517,6 +517,7 @@ plugins += [
IREXDR1000, IREXDR1000,
IREXDR800, IREXDR800,
JETBOOK, JETBOOK,
MIBUK,
SHINEBOOK, SHINEBOOK,
POCKETBOOK360, POCKETBOOK360,
POCKETBOOK301, POCKETBOOK301,

View File

@ -80,3 +80,21 @@ class JETBOOK(USBMS):
return mi return mi
class MIBUK(USBMS):
name = 'MiBuk Wolder Device Interface'
description = _('Communicate with the MiBuk Wolder reader.')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'txt', 'rtf', 'pdf']
VENDOR_ID = [0x0525]
PRODUCT_ID = [0xa4a5]
BCD = [0x314]
SUPPORTS_SUB_DIRS = True
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = 'WOLDERMIBUK'

View File

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

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys import os, sys
from threading import RLock 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, \ QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
@ -33,10 +33,6 @@ def _config():
help=_('Send file to storage card instead of main memory by default')) help=_('Send file to storage card instead of main memory by default'))
c.add_opt('confirm_delete', default=False, c.add_opt('confirm_delete', default=False,
help=_('Confirm before deleting')) 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, c.add_opt('main_window_geometry', default=None,
help=_('Main window geometry')) # value QVariant.toByteArray help=_('Main window geometry')) # value QVariant.toByteArray
c.add_opt('new_version_notification', default=True, c.add_opt('new_version_notification', default=True,

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.gui2 import gprefs
from calibre.gui2.catalog.catalog_bibtex_ui import Ui_Form
from PyQt4.Qt import QWidget, QListWidgetItem
class PluginWidget(QWidget, Ui_Form):
TITLE = _('BibTeX Options')
HELP = _('Options specific to')+' BibTeX '+_('output')
OPTION_FIELDS = [('bib_cit','{authors}{id}'),
('bib_entry', 0), #mixed
('bibfile_enc', 0), #utf-8
('bibfile_enctag', 0), #strict
('impcit', True) ]
sync_enabled = False
formats = set(['bib'])
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setupUi(self)
from calibre.library.catalog import FIELDS
self.all_fields = []
for x in FIELDS :
if x != 'all':
self.all_fields.append(x)
QListWidgetItem(x, self.db_fields)
def initialize(self, name): #not working properly to update
self.name = name
fields = gprefs.get(name+'_db_fields', self.all_fields)
# Restore the activated db_fields from last use
for x in xrange(self.db_fields.count()):
item = self.db_fields.item(x)
item.setSelected(unicode(item.text()) in fields)
# Update dialog fields from stored options
for opt in self.OPTION_FIELDS:
opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
getattr(self, opt[0]).setCurrentIndex(opt_value)
elif opt[0] == 'impcit' :
getattr(self, opt[0]).setChecked(opt_value)
else:
getattr(self, opt[0]).setText(opt_value)
def options(self):
# Save the currently activated fields
fields = []
for x in xrange(self.db_fields.count()):
item = self.db_fields.item(x)
if item.isSelected():
fields.append(unicode(item.text()))
gprefs.set(self.name+'_db_fields', fields)
# Dictionary currently activated fields
if len(self.db_fields.selectedItems()):
opts_dict = {'fields':[unicode(item.text()) for item in self.db_fields.selectedItems()]}
else:
opts_dict = {'fields':['all']}
# Save/return the current options
# bib_cit stores as text
# 'bibfile_enc','bibfile_enctag' stores as int (Indexes)
for opt in self.OPTION_FIELDS:
if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
opt_value = getattr(self,opt[0]).currentIndex()
elif opt[0] == 'impcit' :
opt_value = getattr(self, opt[0]).isChecked()
else :
opt_value = unicode(getattr(self, opt[0]).text())
gprefs.set(self.name + '_' + opt[0], opt_value)
opts_dict[opt[0]] = opt_value
return opts_dict

View File

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>579</width>
<height>411</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Bib file encoding:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Fields to include in output:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QComboBox" name="bibfile_enc">
<item>
<property name="text">
<string notr="true">utf-8</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">cp1252</string>
</property>
</item>
<item>
<property name="text">
<string>ascii/LaTeX</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1" rowspan="12">
<widget class="QListWidget" name="db_fields">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string extracomment="Select all fields to be exported"/>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Encoding configuration (change if you have errors) :</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QComboBox" name="bibfile_enctag">
<item>
<property name="text">
<string>strict</string>
</property>
</item>
<item>
<property name="text">
<string>replace</string>
</property>
</item>
<item>
<property name="text">
<string>ignore</string>
</property>
</item>
<item>
<property name="text">
<string>backslashreplace</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>60</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>BibTeX entry type:</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QComboBox" name="bib_entry">
<item>
<property name="text">
<string>mixed</string>
</property>
</item>
<item>
<property name="text">
<string>misc</string>
</property>
</item>
<item>
<property name="text">
<string>book</string>
</property>
</item>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="impcit">
<property name="text">
<string>Create a citation tag?</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Expression to form the BibTeX citation tag:</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLineEdit" name="bib_cit"/>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Some explanation about this template:
-The fields availables are 'author_sort', 'authors', 'id',
'isbn', 'pubdate', 'publisher', 'series_index', 'series',
'tags', 'timestamp', 'title', 'uuid'
-For list types ie authors and tags, only the first element
wil be selected.
-For time field, only the date will be used. </string>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -20,6 +20,30 @@
<string>Book Cover</string> <string>Book Cover</string>
</property> </property>
<layout class="QGridLayout" name="_2"> <layout class="QGridLayout" name="_2">
<item row="0" column="0">
<layout class="QHBoxLayout" name="_3">
<item>
<widget class="ImageView" name="cover" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_prefer_metadata_cover">
<property name="text">
<string>Use cover from &amp;source file</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0"> <item row="1" column="0">
<layout class="QVBoxLayout" name="_4"> <layout class="QVBoxLayout" name="_4">
<property name="spacing"> <property name="spacing">
@ -71,30 +95,6 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_prefer_metadata_cover">
<property name="text">
<string>Use cover from &amp;source file</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="_3">
<item>
<widget class="ImageView" name="cover" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
<zorder>opt_prefer_metadata_cover</zorder> <zorder>opt_prefer_metadata_cover</zorder>
<zorder></zorder> <zorder></zorder>
@ -232,9 +232,6 @@
<property name="insertPolicy"> <property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum> <enum>QComboBox::InsertAlphabetically</enum>
</property> </property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="6" column="1">

View File

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

View File

@ -0,0 +1,82 @@
#!/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
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
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'):
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.perform_action(action, loc)

View File

@ -0,0 +1,171 @@
<?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="3">
<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="2" column="1">
<widget class="QLineEdit" name="location">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<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="2">
<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="2">
<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="1">
<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="1">
<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="2">
<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>
</layout>
</widget>
<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.constants import iswindows, isosx
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn 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, \ open_url, open_local_file, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \ ALL_COLUMNS, NONE, info_dialog, choose_files, \
warning_dialog, ResizableDialog, question_dialog warning_dialog, ResizableDialog, question_dialog
@ -343,9 +343,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.model = library_view.model() self.model = library_view.model()
self.db = self.model.db self.db = self.model.db
self.server = server 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) self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
input_map = prefs['input_format_order'] input_map = prefs['input_format_order']
@ -808,12 +805,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
d = CheckIntegrity(self.db, self) d = CheckIntegrity(self.db, self)
d.exec_() 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): def accept(self):
mcs = unicode(self.max_cover_size.text()).strip() mcs = unicode(self.max_cover_size.text()).strip()
if not re.match(r'\d+x\d+', mcs): 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['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
config['new_version_notification'] = bool(self.new_version_notification.isChecked()) config['new_version_notification'] = bool(self.new_version_notification.isChecked())
prefs['network_timeout'] = int(self.timeout.value()) 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())] 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 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() val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString()
config['gui_layout'] = unicode(val) 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: if must_restart:
warning_dialog(self, _('Must restart'), warning_dialog(self, _('Must restart'),
_('The changes you made require that Calibre be ' _('The changes you made require that Calibre be '

View File

@ -113,50 +113,6 @@
</property> </property>
<widget class="QWidget" name="page_3"> <widget class="QWidget" name="page_3">
<layout class="QVBoxLayout" name="verticalLayout"> <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> <item>
<widget class="QCheckBox" name="new_version_notification"> <widget class="QCheckBox" name="new_version_notification">
<property name="text"> <property name="text">

View File

@ -277,12 +277,6 @@
</property> </property>
<item> <item>
<widget class="EnComboBox" name="series"> <widget class="EnComboBox" name="series">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip"> <property name="toolTip">
<string>List of known series. You can add new series.</string> <string>List of known series. You can add new series.</string>
</property> </property>
@ -295,9 +289,6 @@
<property name="insertPolicy"> <property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum> <enum>QComboBox::InsertAlphabetically</enum>
</property> </property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget> </widget>
</item> </item>
<item> <item>

View File

@ -7,14 +7,13 @@ __docformat__ = 'restructuredtext en'
import functools, sys, os import functools, sys, os
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QIcon, QStackedWidget, \ from PyQt4.Qt import QMenu, Qt, QStackedWidget, \
QSize, QSizePolicy, QStatusBar, QUrl, QLabel, QFont QSize, QSizePolicy, QStatusBar, QLabel, QFont
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import isosx, __appname__, preferred_encoding, \ from calibre.constants import isosx, __appname__, preferred_encoding, \
__version__ __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.library.views import BooksView, DeviceBooksView
from calibre.gui2.widgets import Splitter from calibre.gui2.widgets import Splitter
from calibre.gui2.tag_view import TagBrowserWidget from calibre.gui2.tag_view import TagBrowserWidget
@ -28,157 +27,6 @@ def partial(*args, **kwargs):
_keep_refs.append(ans) _keep_refs.append(ans)
return 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): # {{{ class LibraryViewMixin(object): # {{{
def __init__(self, db): def __init__(self, db):

View File

@ -6,108 +6,105 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from operator import attrgetter from operator import attrgetter
from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, QVariant, \ from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \
QAbstractListModel, QFont, QApplication, QPalette, pyqtSignal, QToolButton, \ pyqtSignal, QToolButton, \
QModelIndex, QListView, QAbstractButton, QPainter, QPixmap, QColor, \ QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \
QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout 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.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton 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.gui2.widgets import ComboBoxWithHelp
from calibre import human_readable from calibre import human_readable
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
ICON_SIZE = 48 ICON_SIZE = 48
# Location View {{{ class SaveMenu(QMenu): # {{{
class LocationModel(QAbstractListModel): # {{{ save_fmt = pyqtSignal(object)
devicesChanged = pyqtSignal()
def __init__(self, parent): def __init__(self, parent):
QAbstractListModel.__init__(self, parent) QMenu.__init__(self, _('Save single format to disk...'), parent)
self.icons = [QVariant(QIcon(I('library.png'))), for ext in sorted(BOOK_EXTENSIONS):
QVariant(QIcon(I('reader.svg'))), action = self.addAction(ext.upper())
QVariant(QIcon(I('sd.svg'))), setattr(self, 'do_'+ext, partial(self.do, ext))
QVariant(QIcon(I('sd.svg')))] action.triggered.connect(
self.text = [_('Library\n%d books'), getattr(self, 'do_'+ext))
_('Reader\n%s'),
_('Card A\n%s'), def do(self, ext, *args):
_('Card B\n%s')] 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.free = [-1, -1, -1]
self.count = 0 self.count = 0
self.highlight_row = 0 self.location_actions = QActionGroup(self)
self.library_tooltip = _('Click to see the books available on your computer') self.location_actions.setExclusive(True)
self.tooltips = [ self.current_location = 'library'
self.library_tooltip, self._mem = []
_('Click to see the books in the main memory of your reader'), self.tooltips = {}
_('Click to see the books on storage card A in your reader'),
_('Click to see the books on storage card B in your reader')
]
def database_changed(self, db): def ac(name, text, icon, tooltip):
lp = db.library_path icon = QIcon(I(icon))
if not isinstance(lp, unicode): ac = self.location_actions.addAction(icon, text)
lp = lp.decode(filesystem_encoding, 'replace') setattr(self, 'location_'+name, ac)
self.tooltips[0] = self.library_tooltip + '\n\n' + \ ac.setAutoRepeat(False)
_('Books located at') + ' ' + lp ac.setCheckable(True)
self.dataChanged.emit(self.index(0), self.index(0)) 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 ac
return 1 + len([i for i in self.free if i >= 0])
def get_device_row(self, row): ac('library', _('Library'), 'lt.png',
if row == 2 and self.free[1] == -1 and self.free[2] > -1: _('Show books in calibre library'))
row = 3 ac('main', _('Main'), 'reader.svg',
return row _('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): def _location_selected(self, location, *args):
ans = self.tooltips[row] if location != self.current_location and hasattr(self,
if row > 0: 'location_'+location):
fs = self.free[drow-1] self.current_location = location
if fs > -1: self.location_selected.emit(location)
ans += '\n\n%s '%(human_readable(fs)) + _('free') getattr(self, 'location_'+location).setChecked(True)
return ans
def data(self, index, role): def _eject_requested(self, *args):
row = index.row() self.unmount_device.emit()
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))
return data def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None):
if icon is None:
def device_connected(self, dev): icon = I('reader.svg')
self.icons[1] = QIcon(dev.icon) self.location_main.setIcon(QIcon(icon))
self.dataChanged.emit(self.index(1), self.index(1)) had_device = self.has_device
def headerData(self, section, orientation, role):
return NONE
def update_devices(self, cp=(None, None), fs=[-1, -1, -1]):
if cp is None: if cp is None:
cp = (None, None) cp = (None, None)
if isinstance(cp, (str, unicode)): if isinstance(cp, (str, unicode)):
@ -120,137 +117,34 @@ class LocationModel(QAbstractListModel): # {{{
cpa, cpb = cp cpa, cpb = cp
self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1 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.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1
self.reset() self.update_tooltips()
self.devicesChanged.emit() if self.has_device != had_device:
self.locations_changed.emit()
if not self.has_device:
self.location_library.trigger()
def location_changed(self, row): def update_tooltips(self):
self.highlight_row = row for i, loc in enumerate(('main', 'carda', 'cardb')):
self.dataChanged.emit( t = self.tooltips[loc]
self.index(0), self.index(self.rowCount(QModelIndex())-1)) 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 @property
def book_count(self): def has_device(self):
return self.model().count return max(self.free) > -1
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)
@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 +220,7 @@ class SearchBar(QWidget): # {{{
class ToolBar(QToolBar): # {{{ 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) QToolBar.__init__(self, parent)
self.setContextMenuPolicy(Qt.PreventContextMenu) self.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False) self.setMovable(False)
@ -335,11 +229,12 @@ class ToolBar(QToolBar): # {{{
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea) self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
self.showing_device = False
self.all_actions = actions self.all_actions = actions
self.donate = donate 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 = QWidget()
self.d_widget.setLayout(QVBoxLayout()) self.d_widget.setLayout(QVBoxLayout())
self.d_widget.layout().addWidget(donate) self.d_widget.layout().addWidget(donate)
@ -350,40 +245,45 @@ class ToolBar(QToolBar): # {{{
def contextMenuEvent(self, *args): def contextMenuEvent(self, *args):
pass pass
def device_status_changed(self, connected):
self.showing_device = connected
self.build_bar()
def build_bar(self): 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') o = attrgetter(order_field+'_order')
sepvals = [2] if self.showing_device else [1] sepvals = [2] if showing_device else [1]
sepvals += [3] sepvals += [3]
actions = [x for x in self.all_actions if o(x) > -1] actions = [x for x in self.all_actions if o(x) > -1]
actions.sort(cmp=lambda x,y : cmp(o(x), o(y))) actions.sort(cmp=lambda x,y : cmp(o(x), o(y)))
self.clear() 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.setCursor(Qt.PointingHandCursor)
ch.setAutoRaise(True) ch.setAutoRaise(True)
if ac.menu() is not None:
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'):
ch.setPopupMode(ch.MenuButtonPopup) 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: for x in actions:
if x.separator_before in sepvals: if x.separator_before in sepvals:
self.insertSeparator(x) self.insertSeparator(x)
self.choose_action.setVisible(not showing_device)
self.location_action.setVisible(self.showing_device)
self.choose_action.setVisible(not self.showing_device)
def count_changed(self, new_count): def count_changed(self, new_count):
text = _('%d books')%new_count text = _('%d books')%new_count
@ -397,6 +297,9 @@ class ToolBar(QToolBar): # {{{
self.setToolButtonStyle(style) self.setToolButtonStyle(style)
QToolBar.resizeEvent(self, ev) QToolBar.resizeEvent(self, ev)
def database_changed(self, db):
pass
# }}} # }}}
class Action(QAction): class Action(QAction):
@ -405,6 +308,7 @@ class Action(QAction):
class MainWindowMixin(object): class MainWindowMixin(object):
def __init__(self): def __init__(self):
self.device_connected = None
self.setObjectName('MainWindow') self.setObjectName('MainWindow')
self.setWindowIcon(QIcon(I('library.png'))) self.setWindowIcon(QIcon(I('library.png')))
self.setWindowTitle(__appname__) self.setWindowTitle(__appname__)
@ -417,9 +321,30 @@ class MainWindowMixin(object):
self.resize(1012, 740) self.resize(1012, 740)
self.donate_button = ThrobbingButton(self.centralwidget) self.donate_button = ThrobbingButton(self.centralwidget)
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE) self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
self.location_manager = LocationManager(self)
# Actions {{{ 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 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 = [] all_actions = []
def ac(normal_order, device_order, separator_before, def ac(normal_order, device_order, separator_before,
@ -467,17 +392,135 @@ class MainWindowMixin(object):
ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'), ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
'tags.svg') 'tags.svg')
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) def show_help(self, *args):
self.search_bar = SearchBar(self) open_url(QUrl('http://calibre-ebook.com/user_manual'))
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 read_toolbar_settings(self):
pass

View File

@ -12,13 +12,13 @@ __docformat__ = 'restructuredtext en'
import collections, os, sys, textwrap, time import collections, os, sys, textwrap, time
from Queue import Queue, Empty from Queue import Queue, Empty
from threading import Thread from threading import Thread
from PyQt4.Qt import Qt, SIGNAL, QObject, QTimer, \ from PyQt4.Qt import Qt, SIGNAL, QTimer, \
QPixmap, QMenu, QIcon, pyqtSignal, \ QPixmap, QMenu, QIcon, pyqtSignal, \
QDialog, \ QDialog, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \
QMessageBox, QHelpEvent QMessageBox, QHelpEvent
from calibre import prints, patheq from calibre import prints
from calibre.constants import __appname__, isosx from calibre.constants import __appname__, isosx
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
@ -27,7 +27,6 @@ from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
gprefs, max_available_height, config, info_dialog gprefs, max_available_height, config, info_dialog
from calibre.gui2.cover_flow import CoverFlowMixin from calibre.gui2.cover_flow import CoverFlowMixin
from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2.wizard import move_library
from calibre.gui2.dialogs.scheduler import Scheduler from calibre.gui2.dialogs.scheduler import Scheduler
from calibre.gui2.update import UpdateMixin from calibre.gui2.update import UpdateMixin
from calibre.gui2.main_window import MainWindow from calibre.gui2.main_window import MainWindow
@ -38,7 +37,7 @@ from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.book_info import BookInfo from calibre.gui2.dialogs.book_info import BookInfo
from calibre.library.database2 import LibraryDatabase2 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_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_view import TagBrowserMixin from calibre.gui2.tag_view import TagBrowserMixin
@ -91,7 +90,7 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
# }}} # }}}
class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
AnnotationsAction, AddAction, DeleteAction, AnnotationsAction, AddAction, DeleteAction,
@ -192,21 +191,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
####################### Start spare job server ######################## ####################### Start spare job server ########################
QTimer.singleShot(1000, self.add_spare_server) QTimer.singleShot(1000, self.add_spare_server)
####################### Location View ######################## ####################### Location Manager ########################
QObject.connect(self.location_view, self.location_manager.location_selected.connect(self.location_selected)
SIGNAL('location_selected(PyQt_PyObject)'), self.location_manager.unmount_device.connect(self.device_manager.umount_device)
self.location_selected)
QObject.connect(self.location_view,
SIGNAL('umount_device()'),
self.device_manager.umount_device)
self.eject_action.triggered.connect(self.device_manager.umount_device) self.eject_action.triggered.connect(self.device_manager.umount_device)
#################### Update notification ################### #################### Update notification ###################
UpdateMixin.__init__(self, opts) UpdateMixin.__init__(self, opts)
####################### Setup Toolbar #####################
ToolbarMixin.__init__(self)
####################### Search boxes ######################## ####################### Search boxes ########################
SavedSearchBoxMixin.__init__(self) SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__init__(self) SearchBoxMixin.__init__(self)
@ -218,7 +210,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
if self.system_tray_icon.isVisible() and opts.start_in_tray: if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows() 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 \ self.library_view.model().count_changed_signal.connect \
(t.count_changed) (t.count_changed)
if not gprefs.get('quick_start_guide_added', False): if not gprefs.get('quick_start_guide_added', False):
@ -235,8 +227,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.db_images.reset() self.db_images.reset()
self.library_view.model().count_changed() self.library_view.model().count_changed()
self.location_view.model().database_changed(self.library_view.model().db) self.tool_bar.database_changed(self.library_view.model().db)
self.library_view.model().database_changed.connect(self.location_view.model().database_changed, self.library_view.model().database_changed.connect(self.tool_bar.database_changed,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
########################### Tags Browser ############################## ########################### Tags Browser ##############################
@ -396,10 +388,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.tags_view.recount() self.tags_view.recount()
self.create_device_menu() self.create_device_menu()
self.set_device_menu_items_state(bool(self.device_connected)) 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): def library_moved(self, newloc):
if newloc is None: return if newloc is None: return

View File

@ -490,6 +490,7 @@ class EnComboBox(QComboBox):
QComboBox.__init__(self, *args) QComboBox.__init__(self, *args)
self.setLineEdit(EnLineEdit(self)) self.setLineEdit(EnLineEdit(self))
self.setAutoCompletionCaseSensitivity(Qt.CaseSensitive) self.setAutoCompletionCaseSensitivity(Qt.CaseSensitive)
self.setMinimumContentsLength(20)
def text(self): def text(self):
return unicode(self.currentText()) return unicode(self.currentText())

View File

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Greg Riker <griker at hotmail.com>' __copyright__ = '2010, Greg Riker <griker at hotmail.com>'
import datetime, htmlentitydefs, os, re, shutil import datetime, htmlentitydefs, os, re, shutil, codecs
from collections import namedtuple from collections import namedtuple
from copy import deepcopy from copy import deepcopy
@ -9,6 +11,7 @@ from copy import deepcopy
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
from calibre import filesystem_encoding, prints, prepare_string_for_xml, strftime from calibre import filesystem_encoding, prints, prepare_string_for_xml, strftime
from calibre.constants import preferred_encoding
from calibre.customize import CatalogPlugin from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
@ -21,6 +24,10 @@ FIELDS = ['all', 'author_sort', 'authors', 'comments',
'series_index', 'series', 'size', 'tags', 'timestamp', 'title', 'series_index', 'series', 'size', 'tags', 'timestamp', 'title',
'uuid'] 'uuid']
#Allowed fields for template
TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate',
'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid' ]
class CSV_XML(CatalogPlugin): class CSV_XML(CatalogPlugin):
'CSV/XML catalog generator' 'CSV/XML catalog generator'
@ -89,17 +96,20 @@ class CSV_XML(CatalogPlugin):
fields = self.get_output_fields(opts) fields = self.get_output_fields(opts)
if self.fmt == 'csv': if self.fmt == 'csv':
outfile = open(path_to_output, 'w') outfile = codecs.open(path_to_output, 'w', 'utf8')
# Output the field headers # Output the field headers
outfile.write(u'%s\n' % u','.join(fields)) outfile.write(u'%s\n' % u','.join(fields))
# Output the entry fields # Output the entry fields
for entry in data: for entry in data:
outstr = '' outstr = []
for (x, field) in enumerate(fields): for field in fields:
item = entry[field] item = entry[field]
if field == 'formats': if item is None:
outstr.append('""')
continue
elif field == 'formats':
fmt_list = [] fmt_list = []
for format in item: for format in item:
fmt_list.append(format.rpartition('.')[2].lower()) fmt_list.append(format.rpartition('.')[2].lower())
@ -111,18 +121,13 @@ class CSV_XML(CatalogPlugin):
item = u'%s' % re.sub(r'[\D]', '', item) item = u'%s' % re.sub(r'[\D]', '', item)
elif field in ['pubdate', 'timestamp']: elif field in ['pubdate', 'timestamp']:
item = isoformat(item) item = isoformat(item)
elif field == 'comments':
item = item.replace(u'\r\n',u' ')
item = item.replace(u'\n',u' ')
if x < len(fields) - 1: outstr.append(u'"%s"' % unicode(item).replace('"','""'))
if item is not None:
outstr += u'"%s",' % unicode(item).replace('"','""') outfile.write(u','.join(outstr) + u'\n')
else:
outstr += '"",'
else:
if item is not None:
outstr += u'"%s"\n' % unicode(item).replace('"','""')
else:
outstr += '""\n'
outfile.write(outstr.encode('utf-8'))
outfile.close() outfile.close()
elif self.fmt == 'xml': elif self.fmt == 'xml':
@ -181,6 +186,329 @@ class CSV_XML(CatalogPlugin):
f.write(etree.tostring(root, encoding='utf-8', f.write(etree.tostring(root, encoding='utf-8',
xml_declaration=True, pretty_print=True)) xml_declaration=True, pretty_print=True))
class BIBTEX(CatalogPlugin):
'BIBTEX catalog generator'
Option = namedtuple('Option', 'option, default, dest, action, help')
name = 'Catalog_BIBTEX'
description = 'BIBTEX catalog generator'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Sengian'
version = (1, 0, 0)
file_types = set(['bib'])
cli_options = [
Option('--fields',
default = 'all',
dest = 'fields',
action = None,
help = _('The fields to output when cataloging books in the '
'database. Should be a comma-separated list of fields.\n'
'Available fields: %s.\n'
"Default: '%%default'\n"
"Applies to: BIBTEX output format")%', '.join(FIELDS)),
Option('--sort-by',
default = 'id',
dest = 'sort_by',
action = None,
help = _('Output field to sort on.\n'
'Available fields: author_sort, id, rating, size, timestamp, title.\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--create-citation',
default = 'True',
dest = 'impcit',
action = None,
help = _('Create a citation for BibTeX entries.\n'
'Boolean value: True, False\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--citation-template',
default = '{authors}{id}',
dest = 'bib_cit',
action = None,
help = _('The template for citation creation from database fields.\n'
' Should be a template with {} enclosed fields.\n'
'Available fields: %s.\n'
"Default: '%%default'\n"
"Applies to: BIBTEX output format")%', '.join(TEMPLATE_ALLOWED_FIELDS)),
Option('--choose-encoding',
default = 'utf8',
dest = 'bibfile_enc',
action = None,
help = _('BibTeX file encoding output.\n'
'Available types: utf8, cp1252, ascii.\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--choose-encoding-configuration',
default = 'strict',
dest = 'bibfile_enctag',
action = None,
help = _('BibTeX file encoding flag.\n'
'Available types: strict, replace, ignore, backslashreplace.\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--entry-type',
default = 'book',
dest = 'bib_entry',
action = None,
help = _('Entry type for BibTeX catalog.\n'
'Available types: book, misc, mixed.\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format"))]
def run(self, path_to_output, opts, db, notification=DummyReporter()):
from types import StringType, UnicodeType
from calibre.library.save_to_disk import preprocess_template
#Bibtex functions
from calibre.utils.bibtex import bibtex_author_format, utf8ToBibtex, ValidateCitationKey
def create_bibtex_entry(entry, fields, mode, template_citation,
asccii_bibtex = True, citation_bibtex = True):
#Bibtex doesn't like UTF-8 but keep unicode until writing
#Define starting chain or if book valid strict and not book return a Fail string
bibtex_entry = []
if mode != "misc" and check_entry_book_valid(entry) :
bibtex_entry.append(u'@book{')
elif mode != "book" :
bibtex_entry.append(u'@misc{')
else :
#case strict book
return ''
if citation_bibtex :
# Citation tag
bibtex_entry.append(make_bibtex_citation(entry, template_citation, asccii_bibtex))
bibtex_entry = [u' '.join(bibtex_entry)]
for field in fields:
item = entry[field]
#check if the field should be included (none or empty)
if item is None:
continue
try:
if len(item) == 0 :
continue
except TypeError:
pass
if field == 'authors' :
bibtex_entry.append(u'author = "%s"' % bibtex_author_format(item))
elif field in ['title', 'publisher', 'cover', 'uuid',
'author_sort', 'series'] :
bibtex_entry.append(u'%s = "%s"' % (field, utf8ToBibtex(item, asccii_bibtex)))
elif field == 'id' :
bibtex_entry.append(u'calibreid = "%s"' % int(item))
elif field == 'rating' :
bibtex_entry.append(u'rating = "%s"' % int(item))
elif field == 'size' :
bibtex_entry.append(u'%s = "%s octets"' % (field, int(item)))
elif field == 'tags' :
#A list to flatten
bibtex_entry.append(u'tags = "%s"' % utf8ToBibtex(u', '.join(item), asccii_bibtex))
elif field == 'comments' :
#\n removal
item = item.replace(u'\r\n',u' ')
item = item.replace(u'\n',u' ')
bibtex_entry.append(u'note = "%s"' % utf8ToBibtex(item, asccii_bibtex))
elif field == 'isbn' :
# Could be 9, 10 or 13 digits
bibtex_entry.append(u'isbn = "%s"' % re.sub(u'[\D]', u'', item))
elif field == 'formats' :
item = u', '.join([format.rpartition('.')[2].lower() for format in item])
bibtex_entry.append(u'formats = "%s"' % item)
elif field == 'series_index' :
bibtex_entry.append(u'volume = "%s"' % int(item))
elif field == 'timestamp' :
bibtex_entry.append(u'timestamp = "%s"' % isoformat(item).partition('T')[0])
elif field == 'pubdate' :
bibtex_entry.append(u'year = "%s"' % item.year)
bibtex_entry.append(u'month = "%s"' % utf8ToBibtex(strftime("%b", item),
asccii_bibtex))
bibtex_entry = u',\n '.join(bibtex_entry)
bibtex_entry += u' }\n\n'
return bibtex_entry
def check_entry_book_valid(entry):
#Check that the required fields are ok for a book entry
for field in ['title', 'authors', 'publisher'] :
if entry[field] is None or len(entry[field]) == 0 :
return False
if entry['pubdate'] is None :
return False
else :
return True
def make_bibtex_citation(entry, template_citation, asccii_bibtex):
#define a function to replace the template entry by its value
def tpl_replace(objtplname) :
tpl_field = re.sub(u'[\{\}]', u'', objtplname.group())
if tpl_field in TEMPLATE_ALLOWED_FIELDS :
if tpl_field in ['pubdate', 'timestamp'] :
tpl_field = isoformat(entry[tpl_field]).partition('T')[0]
elif tpl_field in ['tags', 'authors'] :
tpl_field =entry[tpl_field][0]
elif tpl_field in ['id', 'series_index'] :
tpl_field = str(entry[tpl_field])
else :
tpl_field = entry[tpl_field]
return tpl_field
else:
return u''
if len(template_citation) >0 :
tpl_citation = utf8ToBibtex(ValidateCitationKey(re.sub(u'\{[^{}]*\}',
tpl_replace, template_citation)), asccii_bibtex)
if len(tpl_citation) >0 :
return tpl_citation
if len(entry["isbn"]) > 0 :
template_citation = u'%s' % re.sub(u'[\D]',u'', entry["isbn"])
else :
template_citation = u'%s' % str(entry["id"])
if asccii_bibtex :
return ValidateCitationKey(template_citation.encode('ascii', 'replace'))
else :
return ValidateCitationKey(template_citation)
self.fmt = path_to_output.rpartition('.')[2]
self.notification = notification
# Combobox options
bibfile_enc = ['utf8', 'cp1252', 'ascii']
bibfile_enctag = ['strict', 'replace', 'ignore', 'backslashreplace']
bib_entry = ['mixed', 'misc', 'book']
# Needed beacause CLI return str vs int by widget
try:
bibfile_enc = bibfile_enc[opts.bibfile_enc]
bibfile_enctag = bibfile_enctag[opts.bibfile_enctag]
bib_entry = bib_entry[opts.bib_entry]
except:
if opts.bibfile_enc in bibfile_enc :
bibfile_enc = opts.bibfile_enc
else :
log(" WARNING: incorrect --choose-encoding flag, revert to default")
bibfile_enc = bibfile_enc[0]
if opts.bibfile_enctag in bibfile_enctag :
bibfile_enctag = opts.bibfile_enctag
else :
log(" WARNING: incorrect --choose-encoding-configuration flag, revert to default")
bibfile_enctag = bibfile_enctag[0]
if opts.bib_entry in bib_entry :
bib_entry = opts.bib_entry
else :
log(" WARNING: incorrect --entry-type flag, revert to default")
bib_entry = bib_entry[0]
if opts.verbose:
opts_dict = vars(opts)
log("%s(): Generating %s" % (self.name,self.fmt))
if opts_dict['search_text']:
log(" --search='%s'" % opts_dict['search_text'])
if opts_dict['ids']:
log(" Book count: %d" % len(opts_dict['ids']))
if opts_dict['search_text']:
log(" (--search ignored when a subset of the database is specified)")
if opts_dict['fields']:
if opts_dict['fields'] == 'all':
log(" Fields: %s" % ', '.join(FIELDS[1:]))
else:
log(" Fields: %s" % opts_dict['fields'])
log(" Output file will be encoded in %s with %s flag" % (bibfile_enc, bibfile_enctag))
log(" BibTeX entry type is %s with a citation like '%s' flag" % (bib_entry, opts_dict['bib_cit']))
# If a list of ids are provided, don't use search_text
if opts.ids:
opts.search_text = None
data = self.search_sort_db(db, opts)
if not len(data):
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
# Get the requested output fields as a list
fields = self.get_output_fields(opts)
if not len(data):
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
#Entries writing after Bibtex formating (or not)
if bibfile_enc != 'ascii' :
asccii_bibtex = False
else :
asccii_bibtex = True
#Check and go to default in case of bad CLI
if isinstance(opts.impcit, (StringType, UnicodeType)) :
if opts.impcit == 'False' :
citation_bibtex= False
elif opts.impcit == 'True' :
citation_bibtex= True
else :
log(" WARNING: incorrect --create-citation, revert to default")
citation_bibtex= True
else :
citation_bibtex= opts.impcit
template_citation = preprocess_template(opts.bib_cit)
#Open output and write entries
outfile = codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag)
#File header
nb_entries = len(data)
#check in book strict if all is ok else throw a warning into log
if bib_entry == 'book' :
nb_books = len(filter(check_entry_book_valid, data))
if nb_books < nb_entries :
log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries))
nb_entries = nb_books
outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries))
outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n'
% (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding)))
for entry in data:
outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation,
asccii_bibtex, citation_bibtex))
outfile.close()
class EPUB_MOBI(CatalogPlugin): class EPUB_MOBI(CatalogPlugin):
'ePub catalog generator' 'ePub catalog generator'

View File

@ -116,6 +116,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# missing functions # missing functions
self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter') 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): def __init__(self, library_path, row_factory=False):
self.field_metadata = FieldMetadata() self.field_metadata = FieldMetadata()
if not os.path.exists(library_path): if not os.path.exists(library_path):

2539
src/calibre/utils/bibtex.py Normal file

File diff suppressed because it is too large Load Diff