Merge from trunk

This commit is contained in:
Charles Haley 2010-10-03 09:34:57 +01:00
commit e2038a71dd
18 changed files with 257 additions and 51 deletions

View File

@ -29,6 +29,10 @@ class WashingtonPost(BasicNewsRecipe):
('Technology', 'http://www.washingtonpost.com/wp-dyn/rss/technology/index.xml'),
('Health', 'http://www.washingtonpost.com/wp-dyn/rss/health/index.xml'),
('Education', 'http://www.washingtonpost.com/wp-dyn/rss/education/index.xml'),
('Style',
'http://www.washingtonpost.com/wp-dyn/rss/print/style/index.xml'),
('Sports',
'http://feeds.washingtonpost.com/wp-dyn/rss/linkset/2010/08/19/LI2010081904067_xml'),
('Editorials', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/05/30/LI2005053000331.xml'),
]

View File

@ -455,6 +455,24 @@ def prepare_string_for_xml(raw, attribute=False):
def isbytestring(obj):
return isinstance(obj, (str, bytes))
def force_unicode(obj, enc=preferred_encoding):
if isbytestring(obj):
try:
obj = obj.decode(enc)
except:
try:
obj = obj.decode(filesystem_encoding if enc ==
preferred_encoding else preferred_encoding)
except:
try:
obj = obj.decode('utf-8')
except:
obj = repr(obj)
if isbytestring(obj):
obj = obj.decode('utf-8')
return obj
def human_readable(size):
""" Convert a size in bytes into a human readable form """
divisor, suffix = 1, "B"

View File

@ -707,7 +707,7 @@ OptionRecommendation(name='timestamp',
if mi.cover.startswith('http:') or mi.cover.startswith('https:'):
mi.cover = self.download_cover(mi.cover)
ext = mi.cover.rpartition('.')[-1].lower().strip()
if ext not in ('png', 'jpg', 'jpeg'):
if ext not in ('png', 'jpg', 'jpeg', 'gif'):
ext = 'jpg'
mi.cover_data = (ext, open(mi.cover, 'rb').read())
mi.cover = None

View File

@ -654,8 +654,6 @@ class Metadata(object):
if predicate(x):
l.remove(x)
def __getitem__(self, key):
return self.items[key]

View File

@ -132,17 +132,23 @@ class OEBReader(object):
if not mi.language:
mi.language = get_lang().replace('_', '-')
self.oeb.metadata.add('language', mi.language)
if not mi.title:
mi.title = self.oeb.translate(__('Unknown'))
if not mi.authors:
mi.authors = [self.oeb.translate(__('Unknown'))]
if not mi.book_producer:
mi.book_producer = '%(a)s (%(v)s) [http://%(a)s.kovidgoyal.net]'%\
mi.book_producer = '%(a)s (%(v)s) [http://%(a)s-ebook.com]'%\
dict(a=__appname__, v=__version__)
meta_info_to_oeb_metadata(mi, self.oeb.metadata, self.logger)
self.oeb.metadata.add('identifier', str(uuid.uuid4()), id='uuid_id',
scheme='uuid')
m = self.oeb.metadata
m.add('identifier', str(uuid.uuid4()), id='uuid_id', scheme='uuid')
self.oeb.uid = self.oeb.metadata.identifier[-1]
if not m.title:
m.add('title', self.oeb.translate(__('Unknown')))
has_aut = False
for x in m.creator:
if getattr(x, 'role', '').lower() in ('', 'aut'):
has_aut = True
break
if not has_aut:
m.add('creator', self.oeb.translate(__('Unknown')), role='aut')
def _manifest_prune_invalid(self):
'''

View File

@ -3,6 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """
import os, sys, Queue, threading
from threading import RLock
from urllib import unquote
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
QByteArray, QTranslator, QCoreApplication, QThread, \
@ -505,6 +506,11 @@ class FileDialog(QObject):
fs = QFileDialog.getOpenFileNames(parent, title, initial_dir, ftext, "")
for f in fs:
f = unicode(f)
if not f: continue
if not os.path.exists(f):
# QFileDialog for some reason quotes spaces
# on linux if there is more than one space in a row
f = unquote(f)
if f and os.path.exists(f):
self.selected_files.append(f)
else:

View File

@ -234,13 +234,14 @@ class AddAction(InterfaceAction):
self.gui.set_books_in_library(booklists=[model.db], reset=True)
self.gui.refresh_ondevice()
def add_books_from_device(self, view):
def add_books_from_device(self, view, paths=None):
if paths is None:
rows = view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
d.exec_()
return
paths = [p for p in view._model.paths(rows) if p is not None]
paths = [p for p in view.model().paths(rows) if p is not None]
ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
def ext(x):
ans = os.path.splitext(x)[1]
@ -261,7 +262,7 @@ class AddAction(InterfaceAction):
return
from calibre.gui2.add import Adder
self.__adder_func = partial(self._add_from_device_adder, on_card=None,
model=view._model)
model=view.model())
self._adder = Adder(self.gui, self.gui.library_view.model().db,
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
self._adder.add(paths)

View File

@ -21,7 +21,10 @@ from calibre.gui2.convert import Widget
def create_opf_file(db, book_id):
mi = db.get_metadata(book_id, index_is_id=True)
mi.application_id = uuid.uuid4()
old_cover = mi.cover
mi.cover = None
raw = metadata_to_opf(mi)
mi.cover = old_cover
opf_file = PersistentTemporaryFile('.opf')
opf_file.write(raw)
opf_file.close()

View File

@ -23,7 +23,7 @@ from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
warning_dialog, \
question_dialog, info_dialog, choose_dir
from calibre.ebooks.metadata import authors_to_string
from calibre import preferred_encoding, prints
from calibre import preferred_encoding, prints, force_unicode
from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
@ -964,12 +964,12 @@ class DeviceMixin(object): # {{{
for jobname, exception, tb in results:
title = jobname.partition(':')[-1]
if exception is not None:
errors.append([title, exception, tb])
errors.append(list(map(force_unicode, [title, exception, tb])))
else:
good.append(title)
if errors:
errors = '\n'.join([
'%s\n\n%s\n%s\n' %
errors = u'\n'.join([
u'%s\n\n%s\n%s\n' %
(title, e, tb) for \
title, e, tb in errors
])

View File

@ -375,7 +375,7 @@ p, li { white-space: pre-wrap; }
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>For help with writing advanced news recipes, please visit &lt;a href=&quot;http://__appname__.kovidgoyal.net/user_manual/news.html&quot;&gt;User Recipes&lt;/a&gt;</string>
<string>For help with writing advanced news recipes, please visit &lt;a href=&quot;http://__appname__-ebook.com/user_manual/news.html&quot;&gt;User Recipes&lt;/a&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>

View File

@ -56,6 +56,7 @@ class LocationManager(QObject): # {{{
self._mem.append(a)
else:
ac.setToolTip(tooltip)
ac.calibre_name = name
return ac
@ -112,7 +113,6 @@ class LocationManager(QObject): # {{{
ac.setWhatsThis(t)
ac.setStatusTip(t)
@property
def has_device(self):
return max(self.free) > -1
@ -228,6 +228,7 @@ class ToolBar(QToolBar): # {{{
self.added_actions = []
self.build_bar()
self.preferred_width = self.sizeHint().width()
self.setAcceptDrops(True)
def apply_settings(self):
sz = gprefs['toolbar_icon_size']
@ -317,6 +318,59 @@ class ToolBar(QToolBar): # {{{
def database_changed(self, db):
pass
#support drag&drop from/to library from/to reader/card
def dragEnterEvent(self, event):
md = event.mimeData()
if md.hasFormat("application/calibre+from_library") or \
md.hasFormat("application/calibre+from_device"):
event.setDropAction(Qt.CopyAction)
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
allowed = False
md = event.mimeData()
#Drop is only allowed in the location manager widget's different from the selected one
for ac in self.location_manager.available_actions:
w = self.widgetForAction(ac)
if w is not None:
if ( md.hasFormat("application/calibre+from_library") or \
md.hasFormat("application/calibre+from_device") ) and \
w.geometry().contains(event.pos()) and \
isinstance(w, QToolButton) and not w.isChecked():
allowed = True
break
if allowed:
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
data = event.mimeData()
mime = 'application/calibre+from_library'
if data.hasFormat(mime):
ids = list(map(int, str(data.data(mime)).split()))
tgt = None
for ac in self.location_manager.available_actions:
w = self.widgetForAction(ac)
if w is not None and w.geometry().contains(event.pos()):
tgt = ac.calibre_name
if tgt is not None:
if tgt == 'main':
tgt = None
self.gui.sync_to_device(tgt, False, send_ids=ids)
event.accept()
mime = 'application/calibre+from_device'
if data.hasFormat(mime):
paths = [unicode(u.toLocalFile()) for u in data.urls()]
if paths:
self.gui.iactions['Add Books'].add_books_from_device(
self.gui.current_view(), paths=paths)
event.accept()
# }}}
class MainWindowMixin(object): # {{{

View File

@ -1081,12 +1081,11 @@ class DeviceBooksModel(BooksModel): # {{{
self.db = db
self.map = list(range(0, len(db)))
def current_changed(self, current, previous):
data = {}
item = self.db[self.map[current.row()]]
def cover(self, row):
item = self.db[self.map[row]]
cdata = item.thumbnail
if cdata is not None:
img = QImage()
if cdata is not None:
if hasattr(cdata, 'image_path'):
img.load(cdata.image_path)
elif cdata:
@ -1096,7 +1095,14 @@ class DeviceBooksModel(BooksModel): # {{{
img.loadFromData(cdata)
if img.isNull():
img = self.default_image
data['cover'] = img
return img
def current_changed(self, current, previous):
data = {}
item = self.db[self.map[current.row()]]
cover = self.cover(current.row())
if cover is not self.default_image:
data['cover'] = cover
type = _('Unknown')
ext = os.path.splitext(item.path)[1]
if ext:

View File

@ -9,7 +9,8 @@ import os
from functools import partial
from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
QModelIndex, QIcon, QItemSelection
QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, \
QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
@ -18,7 +19,8 @@ from calibre.gui2.library.models import BooksModel, DeviceBooksModel
from calibre.utils.config import tweaks, prefs
from calibre.gui2 import error_dialog, gprefs
from calibre.gui2.library import DEFAULT_SORT
from calibre.constants import filesystem_encoding
from calibre import force_unicode
class BooksView(QTableView): # {{{
@ -31,6 +33,7 @@ class BooksView(QTableView): # {{{
self.setDragEnabled(True)
self.setDragDropOverwriteMode(False)
self.setDragDropMode(self.DragDrop)
self.drag_start_pos = None
self.setAlternatingRowColors(True)
self.setSelectionBehavior(self.SelectRows)
self.setShowGrid(False)
@ -426,6 +429,69 @@ class BooksView(QTableView): # {{{
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
def drag_icon(self, cover, multiple):
cover = cover.scaledToHeight(120, Qt.SmoothTransformation)
if multiple:
base_width = cover.width()
base_height = cover.height()
base = QImage(base_width+21, base_height+21,
QImage.Format_ARGB32_Premultiplied)
base.fill(QColor(255, 255, 255, 0).rgba())
p = QPainter(base)
rect = QRect(20, 0, base_width, base_height)
p.fillRect(rect, QColor('white'))
p.drawRect(rect)
rect.moveLeft(10)
rect.moveTop(10)
p.fillRect(rect, QColor('white'))
p.drawRect(rect)
rect.moveLeft(0)
rect.moveTop(20)
p.fillRect(rect, QColor('white'))
p.save()
p.setCompositionMode(p.CompositionMode_SourceAtop)
p.drawImage(rect.topLeft(), cover)
p.restore()
p.drawRect(rect)
p.end()
cover = base
return QPixmap.fromImage(cover)
def drag_data(self):
m = self.model()
db = m.db
rows = self.selectionModel().selectedRows()
selected = map(m.id, rows)
ids = ' '.join(map(str, selected))
md = QMimeData()
md.setData('application/calibre+from_library', ids)
md.setUrls([QUrl.fromLocalFile(db.abspath(i, index_is_id=True))
for i in selected])
drag = QDrag(self)
drag.setMimeData(md)
cover = self.drag_icon(m.cover(self.currentIndex().row()),
len(selected) > 1)
drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3))
drag.setPixmap(cover)
return drag
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.drag_start_pos = event.pos()
return QTableView.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
if not (event.buttons() & Qt.LeftButton) or self.drag_start_pos is None:
return
if (event.pos() - self.drag_start_pos).manhattanLength() \
< QApplication.startDragDistance():
return
index = self.indexAt(event.pos())
if not index.isValid():
return
drag = self.drag_data()
drag.exec_(Qt.CopyAction)
def dragEnterEvent(self, event):
if int(event.possibleActions() & Qt.CopyAction) + \
int(event.possibleActions() & Qt.MoveAction) == 0:
@ -547,6 +613,21 @@ class DeviceBooksView(BooksView): # {{{
self.setDragDropMode(self.NoDragDrop)
self.setAcceptDrops(False)
def drag_data(self):
m = self.model()
rows = self.selectionModel().selectedRows()
paths = [force_unicode(p, enc=filesystem_encoding) for p in m.paths(rows) if p]
md = QMimeData()
md.setData('application/calibre+from_device', 'dummy')
md.setUrls([QUrl.fromLocalFile(p) for p in paths])
drag = QDrag(self)
drag.setMimeData(md)
cover = self.drag_icon(m.cover(self.currentIndex().row()), len(paths) >
1)
drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3))
drag.setPixmap(cover)
return drag
def contextMenuEvent(self, event):
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
self._model.db.supports_collections() and \

View File

@ -302,7 +302,7 @@ def do_add_empty(db, title, authors, isbn):
if isbn:
mi.isbn = isbn
db.import_book(mi, [])
write_dirtied()
write_dirtied(db)
send_message()
def command_add(args, dbpath):
@ -456,7 +456,7 @@ def do_set_metadata(db, id, stream):
db.set_metadata(id, mi)
db.clean()
do_show_metadata(db, id, False)
write_dirtied()
write_dirtied(db)
send_message()
def set_metadata_option_parser():

View File

@ -19,7 +19,7 @@ from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import preferred_encoding, filesystem_encoding
from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata import title_sort
from calibre import strftime
from calibre import strftime, prints
plugboard_any_device_value = 'any device'
plugboard_any_format_value = 'any format'

View File

@ -18,7 +18,7 @@ Editing the metadata of one book at a time
Click the book you want to edit and then click the :guilabel:`Edit metadata` button or press the ``E`` key. A dialog opens that allows you to edit all aspects of the metadata. It has various features to make editing faster and more efficient. A list of the commonly used tips:
* You can click the button in between title and authors to swap them automatically. Or
* You can click the button in between title and authors to swap them automatically.
* You can click the button next to author sort to automatically to have |app| automatically fill it from the author name.
* You can click the button next to tags to use the Tag Editor to manage the tags associated with the book.
* The ISBN box will have a red background if you enter an invalid ISBN. It will be green for valid ISBNs

View File

@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
Perform various initialization tasks.
'''
import locale, sys, os
import locale, sys, os, re
# Default translation is NOOP
import __builtin__
@ -114,6 +114,34 @@ if not _run_once:
r, w, a, rb, wb, ab, r+, w+, a+, r+b, w+b, a+b
'''
if iswindows:
class fwrapper(object):
def __init__(self, name, fobject):
object.__setattr__(self, 'fobject', fobject)
object.__setattr__(self, 'name', name)
def __getattribute__(self, attr):
if attr == 'name':
return object.__getattribute__(self, attr)
fobject = object.__getattribute__(self, 'fobject')
return getattr(fobject, attr)
def __setattr__(self, attr, val):
fobject = object.__getattribute__(self, 'fobject')
return setattr(fobject, attr, val)
def __repr__(self):
fobject = object.__getattribute__(self, 'fobject')
name = object.__getattribute__(self, 'name')
return re.sub(r'''['"]<fdopen>['"]''', repr(name),
repr(fobject))
def __str__(self):
return repr(self)
def __unicode__(self):
return repr(self).decode('utf-8')
m = mode[0]
random = len(mode) > 1 and mode[1] == '+'
binary = mode[-1] == 'b'
@ -139,6 +167,7 @@ if not _run_once:
flags |= os.O_NOINHERIT
fd = os.open(name, flags)
ans = os.fdopen(fd, mode, bufsize)
ans = fwrapper(name, ans)
else:
import fcntl
try:

View File

@ -5,8 +5,8 @@
msgid ""
msgstr ""
"Project-Id-Version: calibre 0.7.21\n"
"POT-Creation-Date: 2010-10-01 14:42+MDT\n"
"PO-Revision-Date: 2010-10-01 14:42+MDT\n"
"POT-Creation-Date: 2010-10-02 11:26+MDT\n"
"PO-Revision-Date: 2010-10-02 11:26+MDT\n"
"Last-Translator: Automatically generated\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
@ -10057,7 +10057,7 @@ msgid ""
" files in each directory of the calibre library. This is\n"
" useful if your metadata.db file has been corrupted.\n"
"\n"
" WARNING: This completely regenrates your datbase. You will\n"
" WARNING: This completely regenerates your datbase. You will\n"
" lose stored per-book conversion settings and custom recipes.\n"
" "
msgstr ""