'
from PyQt4.Qt import QMainWindow, Qt, QApplication, pyqtSignal
from calibre import xml_replace_entities
+from calibre.gui2 import error_dialog
from calibre.gui2.tweak_book import actions
from calibre.gui2.tweak_book.editor.text import TextEdit
@@ -16,6 +17,7 @@ class Editor(QMainWindow):
modification_state_changed = pyqtSignal(object)
undo_redo_state_changed = pyqtSignal(object, object)
+ copy_available_state_changed = pyqtSignal(object)
data_changed = pyqtSignal(object)
def __init__(self, syntax, parent=None):
@@ -29,20 +31,11 @@ class Editor(QMainWindow):
self.create_toolbars()
self.undo_available = False
self.redo_available = False
+ self.copy_available = self.cut_available = False
self.editor.undoAvailable.connect(self._undo_available)
self.editor.redoAvailable.connect(self._redo_available)
self.editor.textChanged.connect(self._data_changed)
-
- def _data_changed(self):
- self.data_changed.emit(self)
-
- def _undo_available(self, available):
- self.undo_available = available
- self.undo_redo_state_changed.emit(self.undo_available, self.redo_available)
-
- def _redo_available(self, available):
- self.redo_available = available
- self.undo_redo_state_changed.emit(self.undo_available, self.redo_available)
+ self.editor.copyAvailable.connect(self._copy_available)
@dynamic_property
def data(self):
@@ -58,6 +51,13 @@ class Editor(QMainWindow):
def get_raw_data(self):
return unicode(self.editor.toPlainText())
+ def replace_data(self, raw, only_if_different=True):
+ if isinstance(raw, bytes):
+ raw = raw.decode('utf-8')
+ current = self.get_raw_data() if only_if_different else False
+ if current != raw:
+ self.editor.replace_text(raw)
+
def undo(self):
self.editor.undo()
@@ -73,22 +73,62 @@ class Editor(QMainWindow):
return property(fget=fget, fset=fset)
def create_toolbars(self):
- self.action_bar = b = self.addToolBar(_('Edit actions tool bar'))
+ self.action_bar = b = self.addToolBar(_('File actions tool bar'))
b.setObjectName('action_bar') # Needed for saveState
- b.addAction(actions['editor-save'])
- b.addAction(actions['editor-undo'])
- b.addAction(actions['editor-redo'])
+ for x in ('save', 'undo', 'redo'):
+ try:
+ b.addAction(actions['editor-%s' % x])
+ except KeyError:
+ pass
+ self.edit_bar = b = self.addToolBar(_('Edit actions tool bar'))
+ for x in ('cut', 'copy', 'paste'):
+ try:
+ b.addAction(actions['editor-%s' % x])
+ except KeyError:
+ pass
def break_cycles(self):
self.modification_state_changed.disconnect()
self.undo_redo_state_changed.disconnect()
+ self.copy_available_state_changed.disconnect()
self.data_changed.disconnect()
self.editor.undoAvailable.disconnect()
self.editor.redoAvailable.disconnect()
self.editor.modificationChanged.disconnect()
self.editor.textChanged.disconnect()
+ self.editor.copyAvailable.disconnect()
self.editor.setPlainText('')
+ def _data_changed(self):
+ self.data_changed.emit(self)
+
+ def _undo_available(self, available):
+ self.undo_available = available
+ self.undo_redo_state_changed.emit(self.undo_available, self.redo_available)
+
+ def _redo_available(self, available):
+ self.redo_available = available
+ self.undo_redo_state_changed.emit(self.undo_available, self.redo_available)
+
+ def _copy_available(self, available):
+ self.copy_available = self.cut_available = available
+ self.copy_available_state_changed.emit(available)
+
+ def cut(self):
+ self.editor.cut()
+
+ def copy(self):
+ self.editor.copy()
+
+ def paste(self):
+ if not self.editor.canPaste():
+ return error_dialog(self, _('No text'), _(
+ 'There is no suitable text in the clipboard to paste.'), show=True)
+ self.editor.paste()
+
+ def contextMenuEvent(self, ev):
+ ev.ignore()
+
def launch_editor(path_to_edit, path_is_raw=False, syntax='html'):
if path_is_raw:
raw = path_to_edit
@@ -102,7 +142,7 @@ def launch_editor(path_to_edit, path_is_raw=False, syntax='html'):
syntax = 'css'
app = QApplication([])
t = Editor(syntax)
- t.load_text(raw, syntax=syntax)
+ t.data = raw
t.show()
app.exec_()
diff --git a/src/calibre/gui2/tweak_book/preview.py b/src/calibre/gui2/tweak_book/preview.py
index 59efe2fc70..eabf1e22c9 100644
--- a/src/calibre/gui2/tweak_book/preview.py
+++ b/src/calibre/gui2/tweak_book/preview.py
@@ -11,16 +11,16 @@ from threading import Thread
from Queue import Queue, Empty
from PyQt4.Qt import (
- QWidget, QVBoxLayout, QApplication, QSize, QNetworkAccessManager,
- QNetworkReply, QTimer, QNetworkRequest, QUrl, Qt, QNetworkDiskCache)
-from PyQt4.QtWebKit import QWebView
+ QWidget, QVBoxLayout, QApplication, QSize, QNetworkAccessManager, QMenu, QIcon,
+ QNetworkReply, QTimer, QNetworkRequest, QUrl, Qt, QNetworkDiskCache, QToolBar)
+from PyQt4.QtWebKit import QWebView, QWebInspector
from calibre import prints
from calibre.constants import iswindows
from calibre.ebooks.oeb.polish.parsing import parse
from calibre.ebooks.oeb.base import serialize, OEB_DOCS
from calibre.ptempfile import PersistentTemporaryDirectory
-from calibre.gui2.tweak_book import current_container, editors
+from calibre.gui2.tweak_book import current_container, editors, tprefs, actions
from calibre.gui2.viewer.documentview import apply_settings
from calibre.gui2.viewer.config import config
from calibre.utils.ipc.simple_worker import offload_worker
@@ -117,6 +117,9 @@ class ParseWorker(Thread):
def get_data(self, name):
return getattr(self.parse_items.get(name, None), 'parsed_data', None)
+ def clear(self):
+ self.parse_items.clear()
+
parse_worker = ParseWorker()
# }}}
@@ -218,6 +221,7 @@ class WebView(QWebView):
def __init__(self, parent=None):
QWebView.__init__(self, parent)
+ self.inspector = QWebInspector(self)
w = QApplication.instance().desktop().availableGeometry(self).width()
self._size_hint = QSize(int(w/3), int(w/2))
settings = self.page().settings()
@@ -232,10 +236,11 @@ class WebView(QWebView):
settings.setAttribute(settings.DeveloperExtrasEnabled, True)
settings.setDefaultTextEncoding('utf-8')
- self.setHtml('')
self.page().setNetworkAccessManager(NetworkAccessManager(self))
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
+ self.clear()
+
def sizeHint(self):
return self._size_hint
@@ -253,6 +258,20 @@ class WebView(QWebView):
mf.setScrollBarValue(Qt.Vertical, val[1])
return property(fget=fget, fset=fset)
+ def clear(self):
+ self.setHtml('
')
+
+ def inspect(self):
+ self.inspector.parent().show()
+ self.inspector.parent().raise_()
+ self.pageAction(self.page().InspectElement).trigger()
+
+ def contextMenuEvent(self, ev):
+ menu = QMenu(self)
+ menu.addAction(actions['reload-preview'])
+ menu.addAction(QIcon(I('debug.png')), _('Inspect element'), self.inspect)
+ menu.exec_(ev.globalPos())
+
class Preview(QWidget):
def __init__(self, parent=None):
@@ -261,7 +280,24 @@ class Preview(QWidget):
self.setLayout(l)
l.setContentsMargins(0, 0, 0, 0)
self.view = WebView(self)
+ self.inspector = self.view.inspector
+ self.inspector.setPage(self.view.page())
l.addWidget(self.view)
+ self.bar = QToolBar(self)
+ l.addWidget(self.bar)
+
+ ac = actions['auto-reload-preview']
+ ac.setCheckable(True)
+ ac.setChecked(True)
+ ac.toggled.connect(self.auto_reload_toggled)
+ self.auto_reload_toggled(ac.isChecked())
+ self.bar.addAction(ac)
+
+ ac = actions['reload-preview']
+ ac.triggered.connect(self.refresh)
+ self.bar.addAction(ac)
+
+ actions['preview-dock'].toggled.connect(self.visibility_changed)
self.current_name = None
self.last_sync_request = None
@@ -278,9 +314,38 @@ class Preview(QWidget):
def refresh(self):
if self.current_name:
+ self.refresh_timer.stop()
# This will check if the current html has changed in its editor,
# and re-parse it if so
parse_worker.add_request(self.current_name)
# Tell webkit to reload all html and associated resources
- self.view.refresh()
+ current_url = QUrl.fromLocalFile(current_container().name_to_abspath(self.current_name))
+ if current_url != self.view.url():
+ # The container was changed
+ self.view.setUrl(current_url)
+ else:
+ self.view.refresh()
+
+ def clear(self):
+ self.view.clear()
+
+ @property
+ def is_visible(self):
+ return actions['preview-dock'].isChecked()
+
+ def start_refresh_timer(self):
+ if self.is_visible and actions['auto-reload-preview'].isChecked():
+ self.refresh_timer.start(tprefs['preview_refresh_time'] * 1000)
+
+ def stop_refresh_timer(self):
+ self.refresh_timer.stop()
+
+ def auto_reload_toggled(self, checked):
+ actions['auto-reload-preview'].setToolTip(_(
+ 'Auto reload preview when text changes in editor') if not checked else _(
+ 'Disable auto reload of preview'))
+
+ def visibility_changed(self, is_visible):
+ if is_visible:
+ self.refresh()
diff --git a/src/calibre/gui2/tweak_book/toc.py b/src/calibre/gui2/tweak_book/toc.py
new file mode 100644
index 0000000000..fa828a4f0f
--- /dev/null
+++ b/src/calibre/gui2/tweak_book/toc.py
@@ -0,0 +1,96 @@
+#!/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 '
+
+from PyQt4.Qt import (QDialog, pyqtSignal, QIcon, QVBoxLayout, QDialogButtonBox, QStackedWidget)
+
+from calibre.ebooks.oeb.polish.toc import commit_toc
+from calibre.gui2 import gprefs, error_dialog
+from calibre.gui2.toc.main import TOCView, ItemEdit
+from calibre.gui2.tweak_book import current_container
+
+class TOCEditor(QDialog):
+
+ explode_done = pyqtSignal(object)
+ writing_done = pyqtSignal(object)
+
+ def __init__(self, title=None, parent=None):
+ QDialog.__init__(self, parent)
+
+ t = title or current_container().mi.title
+ self.book_title = t
+ self.setWindowTitle(_('Edit the ToC in %s')%t)
+ self.setWindowIcon(QIcon(I('toc.png')))
+
+ l = self.l = QVBoxLayout()
+ self.setLayout(l)
+
+ self.stacks = s = QStackedWidget(self)
+ l.addWidget(s)
+ self.toc_view = TOCView(self)
+ self.toc_view.add_new_item.connect(self.add_new_item)
+ s.addWidget(self.toc_view)
+ self.item_edit = ItemEdit(self)
+ s.addWidget(self.item_edit)
+
+ bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
+ l.addWidget(bb)
+ bb.accepted.connect(self.accept)
+ bb.rejected.connect(self.reject)
+
+ self.read_toc()
+
+ self.resize(950, 630)
+ geom = gprefs.get('toc_editor_window_geom', None)
+ if geom is not None:
+ self.restoreGeometry(bytes(geom))
+
+ def add_new_item(self, item, where):
+ self.item_edit(item, where)
+ self.stacks.setCurrentIndex(1)
+
+ def accept(self):
+ if self.stacks.currentIndex() == 1:
+ self.toc_view.update_item(*self.item_edit.result)
+ gprefs['toc_edit_splitter_state'] = bytearray(self.item_edit.splitter.saveState())
+ self.stacks.setCurrentIndex(0)
+ elif self.stacks.currentIndex() == 0:
+ self.write_toc()
+ super(TOCEditor, self).accept()
+
+ def really_accept(self, tb):
+ gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry())
+ if tb:
+ error_dialog(self, _('Failed to write book'),
+ _('Could not write %s. Click "Show details" for'
+ ' more information.')%self.book_title, det_msg=tb, show=True)
+ gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry())
+ super(TOCEditor, self).reject()
+ return
+
+ super(TOCEditor, self).accept()
+
+ def reject(self):
+ if not self.bb.isEnabled():
+ return
+ if self.stacks.currentIndex() == 1:
+ gprefs['toc_edit_splitter_state'] = bytearray(self.item_edit.splitter.saveState())
+ self.stacks.setCurrentIndex(0)
+ else:
+ gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry())
+ super(TOCEditor, self).reject()
+
+ def read_toc(self):
+ self.toc_view(current_container())
+ self.item_edit.load(current_container())
+ self.stacks.setCurrentIndex(0)
+
+ def write_toc(self):
+ toc = self.toc_view.create_toc()
+ commit_toc(current_container(), toc, lang=self.toc_view.toc_lang,
+ uid=self.toc_view.toc_uid)
+
diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py
index 46d223e61a..d69fbab793 100644
--- a/src/calibre/gui2/tweak_book/ui.py
+++ b/src/calibre/gui2/tweak_book/ui.py
@@ -6,6 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal '
+from functools import partial
+
from PyQt4.Qt import (
QDockWidget, Qt, QLabel, QIcon, QAction, QApplication, QWidget,
QVBoxLayout, QStackedWidget, QTabWidget, QImage, QPixmap, pyqtSignal)
@@ -20,6 +22,7 @@ from calibre.gui2.tweak_book.keyboard import KeyboardManager
from calibre.gui2.tweak_book.preview import Preview
class Central(QStackedWidget):
+
' The central widget, hosts the editors '
current_editor_changed = pyqtSignal()
@@ -106,9 +109,9 @@ class Main(MainWindow):
self.keyboard = KeyboardManager()
self.create_actions()
- self.create_menubar()
- self.create_toolbar()
+ self.create_toolbars()
self.create_docks()
+ self.create_menubar()
self.status_bar = self.statusBar()
self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
@@ -138,7 +141,8 @@ class Main(MainWindow):
def reg(icon, text, target, sid, keys, description):
ac = actions[sid] = QAction(QIcon(I(icon)), text, self)
ac.setObjectName('action-' + sid)
- ac.triggered.connect(target)
+ if target is not None:
+ ac.triggered.connect(target)
if isinstance(keys, type('')):
keys = (keys,)
self.keyboard.register_shortcut(
@@ -156,12 +160,44 @@ class Main(MainWindow):
self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit'))
# Editor actions
+ group = _('Editor actions')
self.action_editor_undo = reg('edit-undo.png', _('&Undo'), self.boss.do_editor_undo, 'editor-undo', 'Ctrl+Z',
_('Undo typing'))
self.action_editor_redo = reg('edit-redo.png', _('&Redo'), self.boss.do_editor_redo, 'editor-redo', 'Ctrl+Y',
_('Redo typing'))
self.action_editor_save = reg('save.png', _('&Save'), self.boss.do_editor_save, 'editor-save', 'Ctrl+S',
_('Save changes to the current file'))
+ self.action_editor_cut = reg('edit-cut.png', _('C&ut text'), self.boss.do_editor_cut, 'editor-cut', ('Ctrl+X', 'Shift+Delete', ),
+ _('Cut text'))
+ self.action_editor_copy = reg('edit-copy.png', _('&Copy text'), self.boss.do_editor_copy, 'editor-copy', ('Ctrl+C', 'Ctrl+Insert'),
+ _('Copy text'))
+ self.action_editor_paste = reg('edit-paste.png', _('&Paste text'), self.boss.do_editor_paste, 'editor-paste', ('Ctrl+V', 'Shift+Insert', ),
+ _('Paste text'))
+ self.action_editor_cut.setEnabled(False)
+ self.action_editor_copy.setEnabled(False)
+ self.action_editor_undo.setEnabled(False)
+ self.action_editor_redo.setEnabled(False)
+
+ # Tool actions
+ group = _('Tools')
+ self.action_toc = reg('toc.png', _('&Edit Table of Contents'), self.boss.edit_toc, 'edit-toc', (), _('Edit Table of Contents'))
+
+ # Polish actions
+ group = _('Polish')
+ self.action_subset_fonts = reg(
+ 'subset-fonts.png', _('&Subset embedded fonts'), partial(
+ self.boss.polish, 'subset', _('Subset fonts')), 'subset-fonts', (), _('Subset embedded fonts'))
+ self.action_embed_fonts = reg(
+ 'embed-fonts.png', _('&Embed referenced fonts'), partial(
+ self.boss.polish, 'embed', _('Embed fonts')), 'embed-fonts', (), _('Embed referenced fonts'))
+ self.action_smarten_punctuation = reg(
+ 'smarten-punctuation.png', _('&Smarten punctuation'), partial(
+ self.boss.polish, 'smarten_punctuation', _('Smarten punctuation')), 'smarten-punctuation', (), _('Smarten punctuation'))
+
+ # Preview actions
+ group = _('Preview')
+ self.action_auto_reload_preview = reg('auto-reload.png', _('Auto reload preview'), None, 'auto-reload-preview', (), _('Auto reload preview'))
+ self.action_reload_preview = reg('view-refresh.png', _('Refresh preview'), None, 'reload-preview', ('F5', 'Ctrl+R'), _('Refresh preview'))
def create_menubar(self):
b = self.menuBar()
@@ -174,30 +210,78 @@ class Main(MainWindow):
e = b.addMenu(_('&Edit'))
e.addAction(self.action_global_undo)
e.addAction(self.action_global_redo)
+ e.addSeparator()
+ e.addAction(self.action_editor_undo)
+ e.addAction(self.action_editor_redo)
+ e.addSeparator()
+ e.addAction(self.action_editor_cut)
+ e.addAction(self.action_editor_copy)
+ e.addAction(self.action_editor_paste)
- def create_toolbar(self):
- self.global_bar = b = self.addToolBar(_('Global tool bar'))
- b.setObjectName('global_bar') # Needed for saveState
- b.addAction(self.action_open_book)
- b.addAction(self.action_global_undo)
- b.addAction(self.action_global_redo)
- b.addAction(self.action_save)
+ e = b.addMenu(_('&Tools'))
+ e.addAction(self.action_toc)
+ e.addAction(self.action_embed_fonts)
+ e.addAction(self.action_subset_fonts)
+ e.addAction(self.action_smarten_punctuation)
+
+ e = b.addMenu(_('&View'))
+ t = e.addMenu(_('Tool&bars'))
+ e.addSeparator()
+ for name, ac in actions.iteritems():
+ if name.endswith('-dock'):
+ e.addAction(ac)
+ elif name.endswith('-bar'):
+ t.addAction(ac)
+
+ def create_toolbars(self):
+ def create(text, name):
+ name += '-bar'
+ b = self.addToolBar(text)
+ b.setObjectName(name) # Needed for saveState
+ setattr(self, name.replace('-', '_'), b)
+ actions[name] = b.toggleViewAction()
+ return b
+
+ a = create(_('Book tool bar'), 'global').addAction
+ for x in ('open_book', 'global_undo', 'global_redo', 'save', 'toc'):
+ a(getattr(self, 'action_' + x))
+
+ a = create(_('Polish book tool bar'), 'polish').addAction
+ for x in ('embed_fonts', 'subset_fonts', 'smarten_punctuation'):
+ a(getattr(self, 'action_' + x))
def create_docks(self):
- self.file_list_dock = d = QDockWidget(_('&Files Browser'), self)
- d.setObjectName('file_list_dock') # Needed for saveState
+
+ def create(name, oname):
+ oname += '-dock'
+ d = QDockWidget(name, self)
+ d.setObjectName(oname) # Needed for saveState
+ ac = d.toggleViewAction()
+ desc = _('Toggle %s') % name.replace('&', '')
+ self.keyboard.register_shortcut(
+ oname, desc, description=desc, action=ac, group=_('Windows'))
+ actions[oname] = ac
+ setattr(self, oname.replace('-', '_'), d)
+ return d
+
+ d = create(_('&Files Browser'), 'files-browser')
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.file_list = FileListWidget(d)
d.setWidget(self.file_list)
self.addDockWidget(Qt.LeftDockWidgetArea, d)
- self.preview_dock = d = QDockWidget(_('&Book preview'), self)
- d.setObjectName('file_list_dock') # Needed for saveState
+ d = create(_('File &Preview'), 'preview')
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.preview = Preview(d)
d.setWidget(self.preview)
self.addDockWidget(Qt.RightDockWidgetArea, d)
+ d = create(_('&Inspector'), 'inspector')
+ d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
+ d.setWidget(self.preview.inspector)
+ self.preview.inspector.setParent(d)
+ self.addDockWidget(Qt.BottomDockWidgetArea, d)
+
def resizeEvent(self, ev):
self.blocking_job.resize(ev.size())
return super(Main, self).resizeEvent(ev)
@@ -227,3 +311,8 @@ class Main(MainWindow):
state = tprefs.get('main_window_state', None)
if state is not None:
self.restoreState(state, self.STATE_VERSION)
+ # We never want to start with the inspector showing
+ self.inspector_dock.close()
+
+ def contextMenuEvent(self, ev):
+ ev.ignore()
diff --git a/src/calibre/gui2/tweak_book/undo.py b/src/calibre/gui2/tweak_book/undo.py
index f9bd9ba810..ae388c56ce 100644
--- a/src/calibre/gui2/tweak_book/undo.py
+++ b/src/calibre/gui2/tweak_book/undo.py
@@ -57,7 +57,7 @@ class GlobalUndoHistory(object):
revert to state before creating savepoint. '''
if self.pos > 0 and self.pos == len(self.states) - 1:
self.pos -= 1
- cleanup(self.states.pop())
+ cleanup([self.states.pop().container])
ans = self.current_container
ans.message = None
return ans