Start work on new Tweak Book tool

This commit is contained in:
Kovid Goyal 2013-10-08 21:51:16 +05:30
parent e34477da93
commit efb83eb6fc
7 changed files with 426 additions and 0 deletions

View File

@ -495,6 +495,12 @@ class Container(object): # {{{
with open(dest, 'wb') as f: with open(dest, 'wb') as f:
f.write(data) f.write(data)
def filesize(self, name):
if name in self.dirtied:
self.commit_item(name)
path = self.name_to_abspath(name)
return os.path.getsize(path)
def open(self, name, mode='rb'): def open(self, name, mode='rb'):
''' Open the file pointed to by name for direct read/write. Note that ''' Open the file pointed to by name for direct read/write. Note that
this will commit the file if it is dirtied and remove it from the parse this will commit the file if it is dirtied and remove it from the parse

View File

@ -0,0 +1,17 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
_current_container = None
def current_container():
return _current_container
def set_current_container(container):
global _current_container
_current_container = container

View File

@ -0,0 +1,163 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.Qt import (
QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon,
QStyledItemDelegate, QStyle)
from calibre import guess_type, human_readable
from calibre.ebooks.oeb.base import OEB_STYLES
from calibre.gui2.tweak_book import current_container
ICON_SIZE = 24
NAME_ROLE = Qt.UserRole
class ItemDelegate(QStyledItemDelegate):
def sizeHint(self, option, index):
ans = QStyledItemDelegate.sizeHint(self, option, index)
top_level = not index.parent().isValid()
ans += QSize(0, 20 if top_level else 10)
return ans
def paint(self, painter, option, index):
top_level = not index.parent().isValid()
hover = option.state & QStyle.State_MouseOver
if hover:
if top_level:
suffix = '\xa0(%d)' % index.model().rowCount(index)
else:
suffix = '\xa0' + human_readable(current_container().filesize(unicode(index.data(NAME_ROLE).toString())))
br = painter.boundingRect(option.rect, Qt.AlignRight|Qt.AlignVCenter, suffix)
if top_level and index.row() > 0:
option.rect.adjust(0, 5, 0, 0)
painter.drawLine(option.rect.topLeft(), option.rect.topRight())
option.rect.adjust(0, 1, 0, 0)
if hover:
option.rect.adjust(0, 0, -br.width(), 0)
QStyledItemDelegate.paint(self, painter, option, index)
if hover:
option.rect.adjust(0, 0, br.width(), 0)
painter.drawText(option.rect, Qt.AlignRight|Qt.AlignVCenter, suffix)
class FileList(QTreeWidget):
def __init__(self, parent=None):
QTreeWidget.__init__(self, parent)
self.delegate = ItemDelegate(self)
self.setTextElideMode(Qt.ElideMiddle)
self.setItemDelegate(self.delegate)
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.header().close()
self.setDragEnabled(True)
self.setSelectionMode(self.ExtendedSelection)
self.viewport().setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(self.InternalMove)
self.setAutoScroll(True)
self.setAutoScrollMargin(ICON_SIZE*2)
self.setDefaultDropAction(Qt.MoveAction)
self.setAutoExpandDelay(1000)
self.setAnimated(True)
self.setMouseTracking(True)
self.in_drop_event = False
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
self.root = self.invisibleRootItem()
def build(self, container):
self.clear()
self.root = self.invisibleRootItem()
self.root.setFlags(Qt.ItemIsDragEnabled)
self.categories = {}
for category, text, icon in (
('text', _('Text'), 'keyboard-prefs.png'),
('styles', _('Styles'), 'lookfeel.png'),
('images', _('Images'), 'view-image.png'),
('fonts', _('Fonts'), 'font.png'),
('misc', _('Miscellaneous'), 'mimetypes/dir.png'),
):
self.categories[category] = i = QTreeWidgetItem(self.root, 0)
i.setText(0, text)
i.setIcon(0, QIcon(I(icon)))
f = i.font(0)
f.setBold(True)
i.setFont(0, f)
i.setData(0, NAME_ROLE, category)
flags = Qt.ItemIsEnabled
if category == 'text':
flags |= Qt.ItemIsDropEnabled
i.setFlags(flags)
processed, seen = set(), {}
def get_display_name(name, item):
parts = name.split('/')
text = parts[-1]
while text in seen and parts:
text = parts.pop() + '/' + text
seen[text] = item
return text
for name, linear in container.spine_names:
processed.add(name)
i = QTreeWidgetItem(self.categories['text'], 1)
prefix = '' if linear else '[nl] '
if not linear:
i.setIcon(self.non_linear_icon)
i.setText(0, prefix + get_display_name(name, i))
i.setStatusTip(0, _('Full path: ') + name)
i.setFlags(Qt.ItemIsEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsSelectable)
i.setData(0, NAME_ROLE, name)
font_types = {guess_type('a.'+x)[0] for x in ('ttf', 'otf', 'woff')}
def get_category(mt):
category = 'misc'
if mt.startswith('image/'):
category = 'images'
elif mt in font_types:
category = 'fonts'
elif mt in OEB_STYLES:
category = 'styles'
return category
all_files = list(container.manifest_type_map.iteritems())
all_files.append((guess_type('a.opf')[0], [container.opf_name]))
for name in container.name_path_map:
if name in processed:
continue
processed.add(name)
imt = container.mime_map.get(name, guess_type(name)[0])
icat = get_category(imt)
i = QTreeWidgetItem(self.categories[icat], 1)
i.setText(0, get_display_name(name, i))
i.setStatusTip(0, _('Full path: ') + name)
i.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
i.setData(0, NAME_ROLE, name)
for c in self.categories.itervalues():
self.expandItem(c)
def show_context_menu(self, point):
pass
class FileListWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setLayout(QGridLayout(self))
self.file_list = FileList(self)
self.layout().addWidget(self.file_list)
self.layout().setContentsMargins(0, 0, 0, 0)
def build(self, container):
self.file_list.build(container)

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import time
from threading import Thread
from functools import partial
from PyQt4.Qt import (QWidget, QVBoxLayout, QLabel, Qt, QPainter, QBrush, QColor)
from calibre.gui2 import Dispatcher
from calibre.gui2.progress_indicator import ProgressIndicator
class LongJob(Thread):
daemon = True
def __init__(self, name, user_text, callback, function, *args, **kwargs):
Thread.__init__(self, name=name)
self.user_text = user_text
self.function = function
self.args, self.kwargs = args, kwargs
self.result = self.traceback = None
self.time_taken = None
self.callback = callback
def run(self):
st = time.time()
try:
self.result = self.function(*self.args, **self.kwargs)
except:
import traceback
self.traceback = traceback.format_exc()
self.time_taken = time.time() - st
try:
self.callback(self)
finally:
pass
class BlockingJob(QWidget):
def __init__(self, parent):
QWidget.__init__(self, parent)
l = QVBoxLayout()
self.setLayout(l)
l.addStretch(10)
self.pi = ProgressIndicator(self, 128)
l.addWidget(self.pi, alignment=Qt.AlignHCenter)
self.msg = QLabel('')
l.addSpacing(10)
l.addWidget(self.msg, alignment=Qt.AlignHCenter)
l.addStretch(10)
self.setVisible(False)
def start(self):
self.setGeometry(0, 0, self.parent().width(), self.parent().height())
self.setVisible(True)
self.raise_()
self.pi.startAnimation()
def stop(self):
self.pi.stopAnimation()
self.setVisible(False)
def job_done(self, callback, job):
del job.callback
self.stop()
callback(job)
def paintEvent(self, ev):
p = QPainter(self)
p.fillRect(ev.region().boundingRect(), QBrush(QColor(200, 200, 200, 160), Qt.SolidPattern))
p.end()
QWidget.paintEvent(self, ev)
def __call__(self, name, user_text, callback, function, *args, **kwargs):
self.msg.setText('<h2>%s</h2>' % user_text)
job = LongJob(name, user_text, Dispatcher(partial(self.job_done, callback)), function, *args, **kwargs)
job.start()
self.start()

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os
from PyQt4.Qt import QIcon
from calibre.constants import islinux
from calibre.gui2 import Application, ORG_NAME, APP_UID
from calibre.ptempfile import reset_base_dir
from calibre.utils.config import OptionParser
from calibre.gui2.tweak_book.ui import Main
def option_parser():
return OptionParser('''\
%prog [opts] [path_to_ebook]
Launch the calibre tweak book tool.
''')
def main(args=sys.argv):
# Ensure we can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
reset_base_dir()
parser = option_parser()
opts, args = parser.parse_args(args)
override = 'calibre-tweak-book' if islinux else None
app = Application(args, override_program_name=override)
app.load_builtin_fonts()
app.setWindowIcon(QIcon(I('tweak.png')))
Application.setOrganizationName(ORG_NAME)
Application.setApplicationName(APP_UID)
main = Main(opts)
sys.excepthook = main.unhandled_exception
main.show()
if len(args) > 1:
main.open_book(args[1])
app.exec_()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,85 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import shutil, tempfile
from PyQt4.Qt import QDockWidget, Qt, QLabel, QIcon
from calibre.ebooks.oeb.polish.container import get_container
from calibre.ebooks.oeb.polish.main import SUPPORTED
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.gui2 import error_dialog
from calibre.gui2.main_window import MainWindow
from calibre.gui2.tweak_book import set_current_container, current_container
from calibre.gui2.tweak_book.file_list import FileListWidget
from calibre.gui2.tweak_book.job import BlockingJob
from calibre.gui2.tweak_book.undo import GlobalUndoHistory
def load_book(path_to_ebook, base_tdir):
tdir = tempfile.mkdtemp(dir=base_tdir)
return get_container(path_to_ebook, tdir=tdir)
class Main(MainWindow):
APP_NAME = _('Tweak Book')
def __init__(self, opts):
MainWindow.__init__(self, opts, disable_automatic_gc=True)
self.setWindowTitle(self.APP_NAME)
self.setWindowIcon(QIcon(I('tweak.png')))
self.opts = opts
self.tdir = None
self.path_to_ebook = None
self.container = None
self.global_undo = GlobalUndoHistory()
self.blocking_job = BlockingJob(self)
self.file_list_dock = d = QDockWidget(_('&Files Browser'), self)
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.file_list = FileListWidget(d)
d.setWidget(self.file_list)
self.addDockWidget(Qt.LeftDockWidgetArea, d)
self.status_bar = self.statusBar()
self.l = QLabel('Placeholder')
self.setCentralWidget(self.l)
def resizeEvent(self, ev):
self.blocking_job.resize(ev.size())
return super(Main, self).resizeEvent(ev)
def open_book(self, path):
ext = path.rpartition('.')[-1].upper()
if ext not in SUPPORTED:
return error_dialog(self, _('Unsupported format'),
_('Tweaking is only supported for books in the %s formats.'
' Convert your book to one of these formats first.') % _(' and ').join(sorted(SUPPORTED)),
show=True)
# TODO: Handle already open, dirtied book
if self.tdir:
shutil.rmtree(self.tdir, ignore_errors=True)
self.tdir = PersistentTemporaryDirectory()
self.blocking_job('open_book', _('Opening book, please wait...'), self.book_opened, load_book, path, self.tdir)
def book_opened(self, job):
if job.traceback is not None:
return error_dialog(self, _('Failed to open book'),
_('Failed to open book, click Show details for more information.'),
det_msg=job.traceback, show=True)
container = job.result
set_current_container(container)
self.current_metadata = container.mi
self.global_undo.open_book(container)
self.update_window_title()
self.file_list.build(container)
def update_window_title(self):
self.setWindowTitle(self.current_metadata.title + ' [%s] - %s' %(current_container().book_type.upper(), self.APP_NAME))

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
class State(object):
def __init__(self, container):
self.container = container
self.operation = None
class GlobalUndoHistory(object):
def __init__(self):
self.states = []
def open_book(self, container):
self.states = [State(container)]