mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-09 22:55:02 -05:00
1283 lines
54 KiB
Python
1283 lines
54 KiB
Python
#!/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 tempfile, shutil, sys, os
|
|
from collections import OrderedDict
|
|
from functools import partial, wraps
|
|
|
|
from PyQt4.Qt import (
|
|
QObject, QApplication, QDialog, QGridLayout, QLabel, QSize, Qt, QCursor,
|
|
QDialogButtonBox, QIcon, QTimer, QPixmap, QTextBrowser, QVBoxLayout, QInputDialog)
|
|
|
|
from calibre import prints, prepare_string_for_xml, isbytestring
|
|
from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory
|
|
from calibre.ebooks.oeb.base import urlnormalize
|
|
from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish
|
|
from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_container, guess_type, OEB_FONTS
|
|
from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage
|
|
from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all
|
|
from calibre.ebooks.oeb.polish.replace import rename_files, replace_file, get_recommended_folders, rationalize_folders
|
|
from calibre.ebooks.oeb.polish.split import split, merge, AbortError, multisplit
|
|
from calibre.ebooks.oeb.polish.toc import remove_names_from_toc, find_existing_toc, create_inline_toc
|
|
from calibre.ebooks.oeb.polish.utils import link_stylesheets, setup_cssutils_serialization as scs
|
|
from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog, choose_save_file
|
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
|
from calibre.gui2.dialogs.message_box import MessageBox
|
|
from calibre.gui2.tweak_book import set_current_container, current_container, tprefs, actions, editors
|
|
from calibre.gui2.tweak_book.undo import GlobalUndoHistory
|
|
from calibre.gui2.tweak_book.file_list import NewFileDialog
|
|
from calibre.gui2.tweak_book.save import SaveManager, save_container, find_first_existing_ancestor
|
|
from calibre.gui2.tweak_book.preview import parse_worker, font_cache
|
|
from calibre.gui2.tweak_book.toc import TOCEditor
|
|
from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
|
|
from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook
|
|
from calibre.gui2.tweak_book.preferences import Preferences
|
|
from calibre.gui2.tweak_book.widgets import RationalizeFolders, MultiSplit
|
|
|
|
_diff_dialogs = []
|
|
|
|
def get_container(*args, **kwargs):
|
|
kwargs['tweak_mode'] = True
|
|
container = _gc(*args, **kwargs)
|
|
# We preload the embedded fonts from this book, so that the preview panel
|
|
# works
|
|
font_cache.remove_fonts()
|
|
for name, mt in container.mime_map.iteritems():
|
|
if mt in OEB_FONTS and container.exists(name):
|
|
with container.open(name, 'rb') as f:
|
|
raw = f.read()
|
|
font_cache.add_font(raw)
|
|
return container
|
|
|
|
def setup_cssutils_serialization():
|
|
scs(tprefs['editor_tab_stop_width'])
|
|
|
|
class BusyCursor(object):
|
|
|
|
def __enter__(self):
|
|
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
|
|
|
|
def __exit__(self, *args):
|
|
QApplication.restoreOverrideCursor()
|
|
|
|
def in_thread_job(func):
|
|
@wraps(func)
|
|
def ans(*args, **kwargs):
|
|
with BusyCursor():
|
|
return func(*args, **kwargs)
|
|
return ans
|
|
|
|
class Boss(QObject):
|
|
|
|
def __init__(self, parent, notify=None):
|
|
QObject.__init__(self, parent)
|
|
self.global_undo = GlobalUndoHistory()
|
|
self.container_count = 0
|
|
self.tdir = None
|
|
self.save_manager = SaveManager(parent, notify)
|
|
self.save_manager.report_error.connect(self.report_save_error)
|
|
self.doing_terminal_save = False
|
|
self.ignore_preview_to_editor_sync = False
|
|
setup_cssutils_serialization()
|
|
|
|
def __call__(self, gui):
|
|
self.gui = gui
|
|
fl = gui.file_list
|
|
fl.delete_requested.connect(self.delete_requested)
|
|
fl.reorder_spine.connect(self.reorder_spine)
|
|
fl.rename_requested.connect(self.rename_requested)
|
|
fl.bulk_rename_requested.connect(self.bulk_rename_requested)
|
|
fl.edit_file.connect(self.edit_file_requested)
|
|
fl.merge_requested.connect(self.merge_requested)
|
|
fl.mark_requested.connect(self.mark_requested)
|
|
fl.export_requested.connect(self.export_requested)
|
|
fl.replace_requested.connect(self.replace_requested)
|
|
fl.link_stylesheets_requested.connect(self.link_stylesheets_requested)
|
|
self.gui.central.current_editor_changed.connect(self.apply_current_editor_state)
|
|
self.gui.central.close_requested.connect(self.editor_close_requested)
|
|
self.gui.central.search_panel.search_triggered.connect(self.search)
|
|
self.gui.preview.sync_requested.connect(self.sync_editor_to_preview)
|
|
self.gui.preview.split_start_requested.connect(self.split_start_requested)
|
|
self.gui.preview.split_requested.connect(self.split_requested)
|
|
self.gui.preview.link_clicked.connect(self.link_clicked)
|
|
self.gui.check_book.item_activated.connect(self.check_item_activated)
|
|
self.gui.check_book.check_requested.connect(self.check_requested)
|
|
self.gui.check_book.fix_requested.connect(self.fix_requested)
|
|
self.gui.toc_view.navigate_requested.connect(self.link_clicked)
|
|
self.gui.image_browser.image_activated.connect(self.image_activated)
|
|
self.gui.checkpoints.revert_requested.connect(self.revert_requested)
|
|
self.gui.checkpoints.compare_requested.connect(self.compare_requested)
|
|
|
|
def preferences(self):
|
|
p = Preferences(self.gui)
|
|
if p.exec_() == p.Accepted:
|
|
for ed in editors.itervalues():
|
|
ed.apply_settings()
|
|
setup_cssutils_serialization()
|
|
self.gui.apply_settings()
|
|
|
|
def mark_requested(self, name, action):
|
|
self.commit_dirty_opf()
|
|
c = current_container()
|
|
if action == 'cover':
|
|
mark_as_cover(current_container(), name)
|
|
elif action.startswith('titlepage:'):
|
|
action, move_to_start = action.partition(':')[0::2]
|
|
move_to_start = move_to_start == 'True'
|
|
mark_as_titlepage(current_container(), name, move_to_start=move_to_start)
|
|
|
|
if c.opf_name in editors:
|
|
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
|
|
self.gui.file_list.build(c)
|
|
self.set_modified()
|
|
|
|
def mkdtemp(self, prefix=''):
|
|
self.container_count += 1
|
|
return tempfile.mkdtemp(prefix='%s%05d-' % (prefix, self.container_count), dir=self.tdir)
|
|
|
|
def _check_before_open(self):
|
|
if self.gui.action_save.isEnabled():
|
|
if not question_dialog(self.gui, _('Unsaved changes'), _(
|
|
'The current book has unsaved changes. If you open a new book, they will be lost'
|
|
' are you sure you want to proceed?')):
|
|
return
|
|
if self.save_manager.has_tasks:
|
|
return info_dialog(self.gui, _('Cannot open'),
|
|
_('The current book is being saved, you cannot open a new book until'
|
|
' the saving is completed'), show=True)
|
|
return True
|
|
|
|
def new_book(self):
|
|
if not self._check_before_open():
|
|
return
|
|
d = NewBook(self.gui)
|
|
if d.exec_() == d.Accepted:
|
|
fmt = d.fmt.lower()
|
|
path = choose_save_file(self.gui, 'edit-book-new-book', _('Choose file location'),
|
|
filters=[(fmt.upper(), (fmt,))], all_files=False)
|
|
if path is not None:
|
|
if not path.lower().endswith('.' + fmt):
|
|
path = path + '.' + fmt
|
|
from calibre.ebooks.oeb.polish.create import create_book
|
|
create_book(d.mi, path, fmt=fmt)
|
|
self.open_book(path=path)
|
|
|
|
def open_book(self, path=None, edit_file=None, clear_notify_data=True):
|
|
if not self._check_before_open():
|
|
return
|
|
if not hasattr(path, 'rpartition'):
|
|
path = choose_files(self.gui, 'open-book-for-tweaking', _('Choose book'),
|
|
[(_('Books'), [x.lower() for x in SUPPORTED])], all_files=False, select_only_single_file=True)
|
|
if not path:
|
|
return
|
|
path = path[0]
|
|
|
|
ext = path.rpartition('.')[-1].upper()
|
|
if ext not in SUPPORTED:
|
|
return error_dialog(self.gui, _('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)
|
|
if not os.path.exists(path):
|
|
return error_dialog(self.gui, _('File not found'), _(
|
|
'The file %s does not exist.') % path, show=True)
|
|
|
|
for name in tuple(editors):
|
|
self.close_editor(name)
|
|
self.gui.preview.clear()
|
|
self.container_count = -1
|
|
if self.tdir:
|
|
shutil.rmtree(self.tdir, ignore_errors=True)
|
|
self.tdir = PersistentTemporaryDirectory()
|
|
self._edit_file_on_open = edit_file
|
|
self._clear_notify_data = clear_notify_data
|
|
self.gui.blocking_job('open_book', _('Opening book, please wait...'), self.book_opened, get_container, path, tdir=self.mkdtemp())
|
|
|
|
def book_opened(self, job):
|
|
ef = getattr(self, '_edit_file_on_open', None)
|
|
cn = getattr(self, '_clear_notify_data', True)
|
|
self._edit_file_on_open = None
|
|
|
|
if job.traceback is not None:
|
|
if 'DRMError:' in job.traceback:
|
|
from calibre.gui2.dialogs.drm_error import DRMErrorMessage
|
|
return DRMErrorMessage(self.gui).exec_()
|
|
return error_dialog(self.gui, _('Failed to open book'),
|
|
_('Failed to open book, click Show details for more information.'),
|
|
det_msg=job.traceback, show=True)
|
|
if cn:
|
|
self.save_manager.clear_notify_data()
|
|
parse_worker.clear()
|
|
container = job.result
|
|
set_current_container(container)
|
|
with BusyCursor():
|
|
self.current_metadata = self.gui.current_metadata = container.mi
|
|
self.global_undo.open_book(container)
|
|
self.gui.update_window_title()
|
|
self.gui.file_list.current_edited_name = None
|
|
self.gui.file_list.build(container, preserve_state=False)
|
|
self.gui.action_save.setEnabled(False)
|
|
self.update_global_history_actions()
|
|
recent_books = list(tprefs.get('recent-books', []))
|
|
path = container.path_to_ebook
|
|
if path in recent_books:
|
|
recent_books.remove(path)
|
|
recent_books.insert(0, path)
|
|
tprefs['recent-books'] = recent_books[:10]
|
|
self.gui.update_recent_books()
|
|
if ef:
|
|
self.gui.file_list.request_edit(ef)
|
|
self.gui.toc_view.update_if_visible()
|
|
self.add_savepoint(_('Start of editing session'))
|
|
|
|
def update_editors_from_container(self, container=None, names=None):
|
|
c = container or current_container()
|
|
for name, ed in tuple(editors.iteritems()):
|
|
if c.has_name(name):
|
|
if names is None or name in names:
|
|
ed.replace_data(c.raw_data(name))
|
|
ed.is_synced_to_container = True
|
|
else:
|
|
self.close_editor(name)
|
|
|
|
def refresh_file_list(self):
|
|
container = current_container()
|
|
self.gui.file_list.build(container)
|
|
|
|
def apply_container_update_to_gui(self):
|
|
self.refresh_file_list()
|
|
self.update_global_history_actions()
|
|
self.update_editors_from_container()
|
|
self.set_modified()
|
|
self.gui.toc_view.update_if_visible()
|
|
|
|
@in_thread_job
|
|
def delete_requested(self, spine_items, other_items):
|
|
self.add_savepoint(_('Before: Delete files'))
|
|
c = current_container()
|
|
c.remove_from_spine(spine_items)
|
|
for name in other_items:
|
|
c.remove_item(name)
|
|
self.set_modified()
|
|
self.gui.file_list.delete_done(spine_items, other_items)
|
|
spine_names = [x for x, remove in spine_items if remove]
|
|
for name in spine_names + list(other_items):
|
|
if name in editors:
|
|
self.close_editor(name)
|
|
if not editors:
|
|
self.gui.preview.clear()
|
|
if remove_names_from_toc(current_container(), spine_names + list(other_items)):
|
|
self.gui.toc_view.update_if_visible()
|
|
toc = find_existing_toc(current_container())
|
|
if toc and toc in editors:
|
|
editors[toc].replace_data(c.raw_data(toc))
|
|
|
|
def commit_dirty_opf(self):
|
|
c = current_container()
|
|
if c.opf_name in editors and not editors[c.opf_name].is_synced_to_container:
|
|
self.commit_editor_to_container(c.opf_name)
|
|
|
|
def reorder_spine(self, items):
|
|
self.add_savepoint(_('Before: Re-order text'))
|
|
c = current_container()
|
|
c.set_spine(items)
|
|
self.set_modified()
|
|
self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items
|
|
if c.opf_name in editors:
|
|
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
|
|
|
|
def add_file(self):
|
|
if current_container() is None:
|
|
return error_dialog(self.gui, _('No open book'), _(
|
|
'You must first open a book to tweak, before trying to create new files'
|
|
' in it.'), show=True)
|
|
|
|
self.commit_dirty_opf()
|
|
d = NewFileDialog(self.gui)
|
|
if d.exec_() != d.Accepted:
|
|
return
|
|
self.add_savepoint(_('Before: Add file %s') % self.gui.elided_text(d.file_name))
|
|
c = current_container()
|
|
data = d.file_data
|
|
if d.using_template:
|
|
data = data.replace(b'%CURSOR%', b'')
|
|
try:
|
|
c.add_file(d.file_name, data)
|
|
except:
|
|
self.rewind_savepoint()
|
|
raise
|
|
self.gui.file_list.build(c)
|
|
self.gui.file_list.select_name(d.file_name)
|
|
if c.opf_name in editors:
|
|
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
|
|
mt = c.mime_map[d.file_name]
|
|
syntax = syntax_from_mime(d.file_name, mt)
|
|
if syntax:
|
|
if d.using_template:
|
|
self.edit_file(d.file_name, syntax, use_template=d.file_data.decode('utf-8'))
|
|
else:
|
|
self.edit_file(d.file_name, syntax)
|
|
self.set_modified()
|
|
|
|
def add_files(self):
|
|
if current_container() is None:
|
|
return error_dialog(self.gui, _('No open book'), _(
|
|
'You must first open a book to tweak, before trying to create new files'
|
|
' in it.'), show=True)
|
|
|
|
files = choose_files(self.gui, 'tweak-book-bulk-import-files', _('Choose files'))
|
|
if files:
|
|
folder_map = get_recommended_folders(current_container(), files)
|
|
files = {x:('/'.join((folder, os.path.basename(x))) if folder else os.path.basename(x))
|
|
for x, folder in folder_map.iteritems()}
|
|
self.add_savepoint(_('Before Add files'))
|
|
c = current_container()
|
|
for path, name in files.iteritems():
|
|
i = 0
|
|
while c.exists(name):
|
|
i += 1
|
|
name, ext = name.rpartition('.')[0::2]
|
|
name = '%s_%d.%s' % (name, i, ext)
|
|
try:
|
|
with open(path, 'rb') as f:
|
|
c.add_file(name, f.read())
|
|
except:
|
|
self.rewind_savepoint()
|
|
raise
|
|
self.gui.file_list.build(c)
|
|
if c.opf_name in editors:
|
|
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
|
|
self.set_modified()
|
|
|
|
def edit_toc(self):
|
|
self.add_savepoint(_('Before: Edit Table of Contents'))
|
|
d = TOCEditor(title=self.current_metadata.title, parent=self.gui)
|
|
if d.exec_() != d.Accepted:
|
|
self.rewind_savepoint()
|
|
return
|
|
with BusyCursor():
|
|
self.set_modified()
|
|
self.update_editors_from_container()
|
|
self.gui.toc_view.update_if_visible()
|
|
|
|
def insert_inline_toc(self):
|
|
self.commit_all_editors_to_container()
|
|
self.add_savepoint(_('Before: Insert inline Table of Contents'))
|
|
name = create_inline_toc(current_container())
|
|
if name is None:
|
|
self.rewind_savepoint()
|
|
return error_dialog(self.gui, _('No Table of Contents'), _(
|
|
'Cannot create an inline Table of Contents as this book has no existing'
|
|
' Table of Contents. You must first create a Table of Contents using the'
|
|
' Edit Table of Contents tool.'), show=True)
|
|
self.apply_container_update_to_gui()
|
|
self.edit_file(name, 'html')
|
|
|
|
def polish(self, action, name):
|
|
with BusyCursor():
|
|
self.add_savepoint(_('Before: %s') % name)
|
|
try:
|
|
report, changed = tweak_polish(current_container(), {action:True})
|
|
except:
|
|
self.rewind_savepoint()
|
|
raise
|
|
self.apply_container_update_to_gui()
|
|
from calibre.ebooks.markdown import markdown
|
|
report = markdown('# %s\n\n'%self.current_metadata.title + '\n\n'.join(report), output_format='html4')
|
|
if not changed:
|
|
self.rewind_savepoint()
|
|
d = QDialog(self.gui)
|
|
d.l = QVBoxLayout()
|
|
d.setLayout(d.l)
|
|
d.e = QTextBrowser(d)
|
|
d.l.addWidget(d.e)
|
|
d.e.setHtml(report)
|
|
d.bb = QDialogButtonBox(QDialogButtonBox.Close)
|
|
if changed:
|
|
b = d.b = d.bb.addButton(_('See what &changed'), d.bb.AcceptRole)
|
|
b.setIcon(QIcon(I('diff.png'))), b.setAutoDefault(False)
|
|
b.clicked.connect(partial(self.show_current_diff, allow_revert=True))
|
|
d.bb.button(d.bb.Close).setDefault(True)
|
|
d.l.addWidget(d.bb)
|
|
d.bb.rejected.connect(d.reject)
|
|
d.bb.accepted.connect(d.accept)
|
|
d.resize(600, 400)
|
|
d.exec_()
|
|
|
|
# Renaming {{{
|
|
|
|
def rationalize_folders(self):
|
|
c = current_container()
|
|
if not c.SUPPORTS_FILENAMES:
|
|
return error_dialog(self.gui, _('Not supported'),
|
|
_('The %s format does not support file and folder names internally, therefore'
|
|
' arranging files into folders is not allowed.') % c.book_type.upper(), show=True)
|
|
d = RationalizeFolders(self.gui)
|
|
if d.exec_() != d.Accepted:
|
|
return
|
|
self.commit_all_editors_to_container()
|
|
name_map = rationalize_folders(c, d.folder_map)
|
|
if not name_map:
|
|
return info_dialog(self.gui, _('Nothing to do'), _(
|
|
'The files in this book are already arranged into folders'), show=True)
|
|
self.add_savepoint(_('Before: Arrange into folders'))
|
|
self.gui.blocking_job(
|
|
'rationalize_folders', _('Renaming and updating links...'), partial(self.rename_done, name_map),
|
|
rename_files, current_container(), name_map)
|
|
|
|
def rename_requested(self, oldname, newname):
|
|
self.commit_all_editors_to_container()
|
|
if guess_type(oldname) != guess_type(newname):
|
|
args = os.path.splitext(oldname) + os.path.splitext(newname)
|
|
if not confirm(
|
|
_('You are changing the file type of {0}<b>{1}</b> to {2}<b>{3}</b>.'
|
|
' Doing so can cause problems, are you sure?').format(*args),
|
|
'confirm-filetype-change', parent=self.gui, title=_('Are you sure?'),
|
|
config_set=tprefs):
|
|
return
|
|
if urlnormalize(newname) != newname:
|
|
if not confirm(
|
|
_('The name you have chosen {0} contains special characters, internally'
|
|
' it will look like: {1}Try to use only the English alphabet [a-z], numbers [0-9],'
|
|
' hyphens and underscores for file names. Other characters can cause problems for '
|
|
' different ebook viewers. Are you sure you want to proceed?').format(
|
|
'<pre>%s</pre>'%newname, '<pre>%s</pre>' % urlnormalize(newname)),
|
|
'confirm-urlunsafe-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs):
|
|
return
|
|
self.add_savepoint(_('Before: Rename %s') % oldname)
|
|
name_map = {oldname:newname}
|
|
self.gui.blocking_job(
|
|
'rename_file', _('Renaming and updating links...'), partial(self.rename_done, name_map),
|
|
rename_files, current_container(), name_map)
|
|
|
|
def bulk_rename_requested(self, name_map):
|
|
self.add_savepoint(_('Before: Bulk rename'))
|
|
self.gui.blocking_job(
|
|
'bulk_rename_files', _('Renaming and updating links...'), partial(self.rename_done, name_map),
|
|
rename_files, current_container(), name_map)
|
|
|
|
def rename_done(self, name_map, job):
|
|
if job.traceback is not None:
|
|
return error_dialog(self.gui, _('Failed to rename files'),
|
|
_('Failed to rename files, click Show details for more information.'),
|
|
det_msg=job.traceback, show=True)
|
|
self.gui.file_list.build(current_container())
|
|
self.set_modified()
|
|
for oldname, newname in name_map.iteritems():
|
|
if oldname in editors:
|
|
editors[newname] = editors.pop(oldname)
|
|
self.gui.central.rename_editor(editors[newname], newname)
|
|
if self.gui.preview.current_name == oldname:
|
|
self.gui.preview.current_name = newname
|
|
self.apply_container_update_to_gui()
|
|
# }}}
|
|
|
|
# Global history {{{
|
|
def do_global_undo(self):
|
|
container = self.global_undo.undo()
|
|
if container is not None:
|
|
set_current_container(container)
|
|
self.apply_container_update_to_gui()
|
|
|
|
def do_global_redo(self):
|
|
container = self.global_undo.redo()
|
|
if container is not None:
|
|
set_current_container(container)
|
|
self.apply_container_update_to_gui()
|
|
|
|
def update_global_history_actions(self):
|
|
gu = self.global_undo
|
|
for x, text in (('undo', _('&Revert to')), ('redo', '&Revert to')):
|
|
ac = getattr(self.gui, 'action_global_%s' % x)
|
|
ac.setEnabled(getattr(gu, 'can_' + x))
|
|
ac.setText(text + ' "%s"'%(getattr(gu, x + '_msg') or '...'))
|
|
|
|
def add_savepoint(self, msg):
|
|
self.commit_all_editors_to_container()
|
|
nc = clone_container(current_container(), self.mkdtemp())
|
|
self.global_undo.add_savepoint(nc, msg)
|
|
set_current_container(nc)
|
|
self.update_global_history_actions()
|
|
|
|
def rewind_savepoint(self):
|
|
container = self.global_undo.rewind_savepoint()
|
|
if container is not None:
|
|
set_current_container(container)
|
|
self.update_global_history_actions()
|
|
|
|
def create_diff_dialog(self, revert_msg=_('&Revert changes'), show_open_in_editor=True):
|
|
global _diff_dialogs
|
|
from calibre.gui2.tweak_book.diff.main import Diff
|
|
def line_activated(name, lnum, right):
|
|
if right:
|
|
self.edit_file_requested(name, None, guess_type(name))
|
|
if name in editors:
|
|
editor = editors[name]
|
|
editor.go_to_line(lnum)
|
|
editor.setFocus(Qt.OtherFocusReason)
|
|
self.gui.raise_()
|
|
d = Diff(revert_button_msg=revert_msg, show_open_in_editor=show_open_in_editor)
|
|
[x.break_cycles() for x in _diff_dialogs if not x.isVisible()]
|
|
_diff_dialogs = [x for x in _diff_dialogs if x.isVisible()] + [d]
|
|
d.show(), d.raise_(), d.setFocus(Qt.OtherFocusReason), d.setWindowModality(Qt.NonModal)
|
|
if show_open_in_editor:
|
|
d.line_activated.connect(line_activated)
|
|
return d
|
|
|
|
def show_current_diff(self, allow_revert=True, to_container=None):
|
|
self.commit_all_editors_to_container()
|
|
d = self.create_diff_dialog()
|
|
d.revert_requested.connect(partial(self.revert_requested, self.global_undo.previous_container))
|
|
other = to_container or self.global_undo.previous_container
|
|
d.container_diff(other, self.global_undo.current_container,
|
|
names=(self.global_undo.label_for_container(other), self.global_undo.label_for_container(self.global_undo.current_container)))
|
|
|
|
def compare_book(self):
|
|
self.commit_all_editors_to_container()
|
|
c = current_container()
|
|
path = choose_files(self.gui, 'select-book-for-comparison', _('Choose book'), filters=[
|
|
(_('%s books') % c.book_type.upper(), (c.book_type,))], select_only_single_file=True, all_files=False)
|
|
if path and path[0]:
|
|
with TemporaryDirectory('_compare') as tdir:
|
|
other = _gc(path[0], tdir=tdir, tweak_mode=True)
|
|
d = self.create_diff_dialog(revert_msg=None)
|
|
d.container_diff(other, c,
|
|
names=(_('Other book'), _('Current book')))
|
|
|
|
def revert_requested(self, container):
|
|
self.commit_all_editors_to_container()
|
|
nc = self.global_undo.revert_to(container)
|
|
set_current_container(nc)
|
|
self.apply_container_update_to_gui()
|
|
|
|
def compare_requested(self, container):
|
|
self.show_current_diff(to_container=container)
|
|
|
|
# }}}
|
|
|
|
def set_modified(self):
|
|
self.gui.action_save.setEnabled(True)
|
|
|
|
def fix_html(self, current):
|
|
if current:
|
|
ed = self.gui.central.current_editor
|
|
if hasattr(ed, 'fix_html'):
|
|
ed.fix_html()
|
|
else:
|
|
with BusyCursor():
|
|
self.add_savepoint(_('Before: Fix HTML'))
|
|
fix_all_html(current_container())
|
|
self.update_editors_from_container()
|
|
self.set_modified()
|
|
|
|
def pretty_print(self, current):
|
|
if current:
|
|
ed = self.gui.central.current_editor
|
|
for name, x in editors.iteritems():
|
|
if x is ed:
|
|
break
|
|
ed.pretty_print(name)
|
|
else:
|
|
with BusyCursor():
|
|
self.add_savepoint(_('Before: Beautify files'))
|
|
pretty_all(current_container())
|
|
self.update_editors_from_container()
|
|
self.set_modified()
|
|
|
|
def mark_selected_text(self):
|
|
ed = self.gui.central.current_editor
|
|
if ed is not None:
|
|
ed.mark_selected_text()
|
|
if ed.has_marked_text:
|
|
self.gui.central.search_panel.set_where('selected-text')
|
|
|
|
def editor_action(self, action):
|
|
ed = self.gui.central.current_editor
|
|
for n, x in editors.iteritems():
|
|
if x is ed:
|
|
edname = n
|
|
break
|
|
if hasattr(ed, 'action_triggered'):
|
|
if action and action[0] == 'insert_resource':
|
|
rtype = action[1]
|
|
if rtype == 'image' and ed.syntax not in {'css', 'html'}:
|
|
return error_dialog(self.gui, _('Not supported'), _(
|
|
'Inserting images is only supported for HTML and CSS files.'), show=True)
|
|
rdata = get_resource_data(rtype, self.gui)
|
|
if rdata is None:
|
|
return
|
|
if rtype == 'image':
|
|
chosen_name, chosen_image_is_external = rdata
|
|
if chosen_image_is_external:
|
|
with open(chosen_image_is_external[1], 'rb') as f:
|
|
current_container().add_file(chosen_image_is_external[0], f.read())
|
|
self.refresh_file_list()
|
|
chosen_name = chosen_image_is_external[0]
|
|
href = current_container().name_to_href(chosen_name, edname)
|
|
ed.insert_image(href)
|
|
else:
|
|
ed.action_triggered(action)
|
|
|
|
def show_find(self):
|
|
self.gui.central.show_find()
|
|
ed = self.gui.central.current_editor
|
|
if ed is not None and hasattr(ed, 'selected_text'):
|
|
text = ed.selected_text
|
|
if text and text.strip():
|
|
self.gui.central.pre_fill_search(text)
|
|
|
|
def search(self, action, overrides=None):
|
|
' Run a search/replace '
|
|
sp = self.gui.central.search_panel
|
|
# Ensure the search panel is visible
|
|
sp.setVisible(True)
|
|
ed = self.gui.central.current_editor
|
|
name = editor = None
|
|
for n, x in editors.iteritems():
|
|
if x is ed:
|
|
name = n
|
|
break
|
|
state = sp.state
|
|
if overrides:
|
|
state.update(overrides)
|
|
searchable_names = self.gui.file_list.searchable_names
|
|
where = state['where']
|
|
err = None
|
|
if name is None and where in {'current', 'selected-text'}:
|
|
err = _('No file is being edited.')
|
|
elif where == 'selected' and not searchable_names['selected']:
|
|
err = _('No files are selected in the Files Browser')
|
|
elif where == 'selected-text' and not ed.has_marked_text:
|
|
err = _('No text is marked. First select some text, and then use'
|
|
' The "Mark selected text" action in the Search menu to mark it.')
|
|
if not err and not state['find']:
|
|
err = _('No search query specified')
|
|
if err:
|
|
return error_dialog(self.gui, _('Cannot search'), err, show=True)
|
|
del err
|
|
|
|
files = OrderedDict()
|
|
do_all = state['wrap'] or action in {'replace-all', 'count'}
|
|
marked = False
|
|
if where == 'current':
|
|
editor = ed
|
|
elif where in {'styles', 'text', 'selected'}:
|
|
files = searchable_names[where]
|
|
if name in files:
|
|
# Start searching in the current editor
|
|
editor = ed
|
|
# Re-order the list of other files so that we search in the same
|
|
# order every time. Depending on direction, search the files
|
|
# that come after the current file, or before the current file,
|
|
# first.
|
|
lfiles = list(files)
|
|
idx = lfiles.index(name)
|
|
before, after = lfiles[:idx], lfiles[idx+1:]
|
|
if state['direction'] == 'up':
|
|
lfiles = list(reversed(before))
|
|
if do_all:
|
|
lfiles += list(reversed(after)) + [name]
|
|
else:
|
|
lfiles = after
|
|
if do_all:
|
|
lfiles += before + [name]
|
|
files = OrderedDict((m, files[m]) for m in lfiles)
|
|
else:
|
|
editor = ed
|
|
marked = True
|
|
|
|
def no_match():
|
|
QApplication.restoreOverrideCursor()
|
|
msg = '<p>' + _('No matches were found for %s') % ('<pre style="font-style:italic">' + prepare_string_for_xml(state['find']) + '</pre>')
|
|
if not state['wrap']:
|
|
msg += '<p>' + _('You have turned off search wrapping, so all text might not have been searched.'
|
|
' Try the search again, with wrapping enabled. Wrapping is enabled via the'
|
|
' "Wrap" checkbox at the bottom of the search panel.')
|
|
return error_dialog(
|
|
self.gui, _('Not found'), msg, show=True)
|
|
|
|
pat = sp.get_regex(state)
|
|
|
|
def do_find():
|
|
if editor is not None:
|
|
if editor.find(pat, marked=marked, save_match='gui'):
|
|
return
|
|
if not files:
|
|
if not state['wrap']:
|
|
return no_match()
|
|
return editor.find(pat, wrap=True, marked=marked, save_match='gui') or no_match()
|
|
for fname, syntax in files.iteritems():
|
|
if fname in editors:
|
|
if not editors[fname].find(pat, complete=True, save_match='gui'):
|
|
continue
|
|
return self.show_editor(fname)
|
|
raw = current_container().raw_data(fname)
|
|
if pat.search(raw) is not None:
|
|
self.edit_file(fname, syntax)
|
|
if editors[fname].find(pat, complete=True, save_match='gui'):
|
|
return
|
|
return no_match()
|
|
|
|
def no_replace(prefix=''):
|
|
QApplication.restoreOverrideCursor()
|
|
if prefix:
|
|
prefix += ' '
|
|
error_dialog(
|
|
self.gui, _('Cannot replace'), prefix + _(
|
|
'You must first click Find, before trying to replace'), show=True)
|
|
return False
|
|
|
|
def do_replace():
|
|
if editor is None:
|
|
return no_replace()
|
|
if not editor.replace(pat, state['replace'], saved_match='gui'):
|
|
return no_replace(_(
|
|
'Currently selected text does not match the search query.'))
|
|
return True
|
|
|
|
def count_message(action, count, show_diff=False):
|
|
msg = _('%(action)s %(num)s occurrences of %(query)s' % dict(num=count, query=state['find'], action=action))
|
|
if show_diff and count > 0:
|
|
d = MessageBox(MessageBox.INFO, _('Searching done'), prepare_string_for_xml(msg), parent=self.gui, show_copy_button=False)
|
|
d.diffb = b = d.bb.addButton(_('See what &changed'), d.bb.ActionRole)
|
|
b.setIcon(QIcon(I('diff.png'))), d.set_details(None), b.clicked.connect(d.accept)
|
|
b.clicked.connect(partial(self.show_current_diff, allow_revert=True))
|
|
d.exec_()
|
|
else:
|
|
info_dialog(self.gui, _('Searching done'), prepare_string_for_xml(msg), show=True)
|
|
|
|
def do_all(replace=True):
|
|
count = 0
|
|
if not files and editor is None:
|
|
return 0
|
|
lfiles = files or {name:editor.syntax}
|
|
|
|
for n, syntax in lfiles.iteritems():
|
|
if n in editors:
|
|
raw = editors[n].get_raw_data()
|
|
else:
|
|
raw = current_container().raw_data(n)
|
|
if replace:
|
|
raw, num = pat.subn(state['replace'], raw)
|
|
else:
|
|
num = len(pat.findall(raw))
|
|
count += num
|
|
if replace and num > 0:
|
|
if n in editors:
|
|
editors[n].replace_data(raw)
|
|
else:
|
|
with current_container().open(n, 'wb') as f:
|
|
f.write(raw.encode('utf-8'))
|
|
QApplication.restoreOverrideCursor()
|
|
count_message(_('Replaced') if replace else _('Found'), count, show_diff=replace)
|
|
return count
|
|
|
|
with BusyCursor():
|
|
if action == 'find':
|
|
return do_find()
|
|
if action == 'replace':
|
|
return do_replace()
|
|
if action == 'replace-find' and do_replace():
|
|
return do_find()
|
|
if action == 'replace-all':
|
|
if marked:
|
|
return count_message(_('Replaced'), editor.all_in_marked(pat, state['replace']))
|
|
self.add_savepoint(_('Before: Replace all'))
|
|
count = do_all()
|
|
if count == 0:
|
|
self.rewind_savepoint()
|
|
return
|
|
if action == 'count':
|
|
if marked:
|
|
return count_message(_('Found'), editor.all_in_marked(pat))
|
|
return do_all(replace=False)
|
|
|
|
def create_checkpoint(self):
|
|
text, ok = QInputDialog.getText(self.gui, _('Choose name'), _(
|
|
'Choose a name for the checkpoint.\nYou can later restore the book'
|
|
' to this checkpoint via the\n"Revert to..." entries in the Edit menu.'))
|
|
if ok:
|
|
self.add_savepoint(text)
|
|
|
|
def commit_editor_to_container(self, name, container=None):
|
|
container = container or current_container()
|
|
ed = editors[name]
|
|
with container.open(name, 'wb') as f:
|
|
f.write(ed.data)
|
|
if name == container.opf_name:
|
|
container.refresh_mime_map()
|
|
if container is current_container():
|
|
ed.is_synced_to_container = True
|
|
if name == container.opf_name:
|
|
self.gui.file_list.build(container)
|
|
|
|
def commit_all_editors_to_container(self):
|
|
with BusyCursor():
|
|
for name, ed in editors.iteritems():
|
|
if not ed.is_synced_to_container:
|
|
self.commit_editor_to_container(name)
|
|
ed.is_synced_to_container = True
|
|
|
|
def save_book(self):
|
|
c = current_container()
|
|
for name, ed in editors.iteritems():
|
|
if ed.is_modified or not ed.is_synced_to_container:
|
|
self.commit_editor_to_container(name, c)
|
|
ed.is_modified = False
|
|
destdir = os.path.dirname(c.path_to_ebook)
|
|
if not os.path.exists(destdir):
|
|
info_dialog(self.gui, _('Path does not exist'), _(
|
|
'The file you are editing (%s) no longer exists. You have to choose a new save location.') % c.path_to_ebook,
|
|
show_copy_button=False, show=True)
|
|
fmt = c.path_to_ebook.rpartition('.')[-1].lower()
|
|
start_dir = find_first_existing_ancestor(c.path_to_ebook)
|
|
path = choose_save_file(self.gui, 'choose-new-save-location', _('Choose file location'), initial_dir=start_dir,
|
|
filters=[(fmt.upper(), (fmt,))], all_files=False)
|
|
if path is not None:
|
|
if not path.lower().endswith('.' + fmt):
|
|
path = path + '.' + fmt
|
|
c.path_to_ebook = path
|
|
self.global_undo.update_path_to_ebook(path)
|
|
else:
|
|
return
|
|
self.gui.action_save.setEnabled(False)
|
|
tdir = self.mkdtemp(prefix='save-')
|
|
container = clone_container(c, tdir)
|
|
self.save_manager.schedule(tdir, container)
|
|
|
|
def save_copy(self):
|
|
c = current_container()
|
|
ext = c.path_to_ebook.rpartition('.')[-1]
|
|
path = choose_save_file(self.gui, 'tweak_book_save_copy', _(
|
|
'Choose path'), filters=[(_('Book (%s)') % ext.upper(), [ext.lower()])], all_files=False)
|
|
if not path:
|
|
return
|
|
tdir = self.mkdtemp(prefix='save-copy-')
|
|
container = clone_container(c, tdir)
|
|
for name, ed in editors.iteritems():
|
|
if ed.is_modified or not ed.is_synced_to_container:
|
|
self.commit_editor_to_container(name, container)
|
|
|
|
def do_save(c, path, tdir):
|
|
save_container(c, path)
|
|
shutil.rmtree(tdir, ignore_errors=True)
|
|
return path
|
|
|
|
self.gui.blocking_job('save_copy', _('Saving copy, please wait...'), self.copy_saved, do_save, container, path, tdir)
|
|
|
|
def copy_saved(self, job):
|
|
if job.traceback is not None:
|
|
return error_dialog(self.gui, _('Failed to save copy'),
|
|
_('Failed to save copy, click Show details for more information.'), det_msg=job.traceback, show=True)
|
|
msg = _('Copy saved to %s') % job.result
|
|
info_dialog(self.gui, _('Copy saved'), msg, show=True)
|
|
self.gui.show_status_message(msg, 5)
|
|
|
|
def report_save_error(self, tb):
|
|
if self.doing_terminal_save:
|
|
prints(tb, file=sys.stderr)
|
|
return
|
|
error_dialog(self.gui, _('Could not save'),
|
|
_('Saving of the book failed. Click "Show Details"'
|
|
' for more information. You can try to save a copy'
|
|
' to a different location, via File->Save a Copy'), det_msg=tb, show=True)
|
|
|
|
def go_to_line_number(self):
|
|
ed = self.gui.central.current_editor
|
|
if ed is None or not ed.has_line_numbers:
|
|
return
|
|
num, ok = QInputDialog.getInt(self.gui, _('Enter line number'), ('Line number:'), ed.current_line, 1, max(100000, ed.number_of_lines))
|
|
if ok:
|
|
ed.current_line = num
|
|
|
|
def split_start_requested(self):
|
|
self.commit_all_editors_to_container()
|
|
self.gui.preview.do_start_split()
|
|
|
|
@in_thread_job
|
|
def split_requested(self, name, loc):
|
|
self.add_savepoint(_('Before: Split %s') % self.gui.elided_text(name))
|
|
try:
|
|
bottom_name = split(current_container(), name, loc)
|
|
except AbortError:
|
|
self.rewind_savepoint()
|
|
raise
|
|
self.apply_container_update_to_gui()
|
|
self.edit_file(bottom_name, 'html')
|
|
|
|
def multisplit(self):
|
|
ed = self.gui.central.current_editor
|
|
if ed.syntax != 'html':
|
|
return
|
|
name = None
|
|
for n, x in editors.iteritems():
|
|
if ed is x:
|
|
name = n
|
|
break
|
|
if name is None:
|
|
return
|
|
d = MultiSplit(self.gui)
|
|
if d.exec_() == d.Accepted:
|
|
with BusyCursor():
|
|
self.add_savepoint(_('Before: Split %s') % self.gui.elided_text(name))
|
|
try:
|
|
multisplit(current_container(), name, d.xpath)
|
|
except AbortError:
|
|
self.rewind_savepoint()
|
|
raise
|
|
self.apply_container_update_to_gui()
|
|
|
|
@in_thread_job
|
|
def link_clicked(self, name, anchor):
|
|
if not name:
|
|
return
|
|
if name in editors:
|
|
editor = editors[name]
|
|
self.gui.central.show_editor(editor)
|
|
else:
|
|
try:
|
|
mt = current_container().mime_map[name]
|
|
except KeyError:
|
|
return error_dialog(self.gui, _('Does not exist'), _(
|
|
'The file %s does not exist. If you were trying to click an item in'
|
|
' the Table of Contents, you may'
|
|
' need to refresh it by right-clicking and choosing "Refresh".') % name, show=True)
|
|
syntax = syntax_from_mime(name, mt)
|
|
editor = self.edit_file(name, syntax)
|
|
if anchor:
|
|
editor.go_to_anchor(anchor)
|
|
|
|
@in_thread_job
|
|
def check_item_activated(self, item):
|
|
is_mult = item.has_multiple_locations and getattr(item, 'current_location_index', None) is not None
|
|
name = item.all_locations[item.current_location_index][0] if is_mult else item.name
|
|
if name in editors:
|
|
editor = editors[name]
|
|
self.gui.central.show_editor(editor)
|
|
else:
|
|
editor = self.edit_file_requested(name, None, current_container().mime_map[name])
|
|
if getattr(editor, 'has_line_numbers', False):
|
|
if is_mult:
|
|
editor.go_to_line(*(item.all_locations[item.current_location_index][1:3]))
|
|
else:
|
|
editor.go_to_line(item.line, item.col)
|
|
editor.set_focus()
|
|
|
|
@in_thread_job
|
|
def check_requested(self, *args):
|
|
if current_container() is None:
|
|
return
|
|
self.commit_all_editors_to_container()
|
|
c = self.gui.check_book
|
|
c.parent().show()
|
|
c.parent().raise_()
|
|
c.run_checks(current_container())
|
|
|
|
@in_thread_job
|
|
def fix_requested(self, errors):
|
|
self.add_savepoint(_('Before: Auto-fix errors'))
|
|
c = self.gui.check_book
|
|
c.parent().show()
|
|
c.parent().raise_()
|
|
changed = c.fix_errors(current_container(), errors)
|
|
if changed:
|
|
self.apply_container_update_to_gui()
|
|
self.set_modified()
|
|
else:
|
|
self.rewind_savepoint()
|
|
|
|
@in_thread_job
|
|
def merge_requested(self, category, names, master):
|
|
self.add_savepoint(_('Before: Merge files into %s') % self.gui.elided_text(master))
|
|
try:
|
|
merge(current_container(), category, names, master)
|
|
except AbortError:
|
|
self.rewind_savepoint()
|
|
raise
|
|
self.apply_container_update_to_gui()
|
|
if master in editors:
|
|
self.show_editor(master)
|
|
|
|
@in_thread_job
|
|
def link_stylesheets_requested(self, names, sheets, remove):
|
|
self.add_savepoint(_('Before: Link stylesheets'))
|
|
changed_names = link_stylesheets(current_container(), names, sheets, remove)
|
|
if changed_names:
|
|
self.update_editors_from_container(names=changed_names)
|
|
self.set_modified()
|
|
|
|
@in_thread_job
|
|
def export_requested(self, name, path):
|
|
if name in editors and not editors[name].is_synced_to_container:
|
|
self.commit_editor_to_container(name)
|
|
with current_container().open(name, 'rb') as src, open(path, 'wb') as dest:
|
|
shutil.copyfileobj(src, dest)
|
|
|
|
@in_thread_job
|
|
def replace_requested(self, name, path, basename, force_mt):
|
|
self.add_savepoint(_('Before: Replace %s') % name)
|
|
replace_file(current_container(), name, path, basename, force_mt)
|
|
self.apply_container_update_to_gui()
|
|
|
|
def browse_images(self):
|
|
self.gui.image_browser.refresh()
|
|
self.gui.image_browser.show()
|
|
self.gui.image_browser.raise_()
|
|
|
|
def image_activated(self, name):
|
|
mt = current_container().mime_map.get(name, guess_type(name))
|
|
self.edit_file_requested(name, None, mt)
|
|
|
|
def sync_editor_to_preview(self, name, lnum):
|
|
editor = self.edit_file(name, 'html')
|
|
self.ignore_preview_to_editor_sync = True
|
|
try:
|
|
editor.current_line = lnum
|
|
finally:
|
|
self.ignore_preview_to_editor_sync = False
|
|
|
|
def sync_preview_to_editor(self):
|
|
if self.ignore_preview_to_editor_sync:
|
|
return
|
|
ed = self.gui.central.current_editor
|
|
if ed is not None:
|
|
name = None
|
|
for n, x in editors.iteritems():
|
|
if ed is x:
|
|
name = n
|
|
break
|
|
if name is not None and getattr(ed, 'syntax', None) == 'html':
|
|
self.gui.preview.sync_to_editor(name, ed.current_line)
|
|
|
|
def init_editor(self, name, editor, data=None, use_template=False):
|
|
editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed)
|
|
editor.data_changed.connect(self.editor_data_changed)
|
|
editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed)
|
|
editor.cursor_position_changed.connect(self.sync_preview_to_editor)
|
|
editor.cursor_position_changed.connect(self.update_cursor_position)
|
|
if data is not None:
|
|
if use_template:
|
|
editor.init_from_template(data)
|
|
else:
|
|
editor.data = data
|
|
editor.is_synced_to_container = True
|
|
editor.modification_state_changed.connect(self.editor_modification_state_changed)
|
|
self.gui.central.add_editor(name, editor)
|
|
|
|
def edit_file(self, name, syntax, use_template=None):
|
|
editor = editors.get(name, None)
|
|
if editor is None:
|
|
if use_template is None:
|
|
data = current_container().raw_data(name)
|
|
if isbytestring(data) and syntax in {'html', 'css', 'text', 'xml'}:
|
|
try:
|
|
data = data.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
return error_dialog(self.gui, _('Cannot decode'), _(
|
|
'Cannot edit %s as it appears to be in an unknown character encoding') % name, show=True)
|
|
else:
|
|
data = use_template
|
|
editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs)
|
|
self.init_editor(name, editor, data, use_template=bool(use_template))
|
|
self.show_editor(name)
|
|
return editor
|
|
|
|
def show_editor(self, name):
|
|
self.gui.central.show_editor(editors[name])
|
|
editors[name].set_focus()
|
|
|
|
def edit_file_requested(self, name, syntax, mime):
|
|
if name in editors:
|
|
self.gui.central.show_editor(editors[name])
|
|
return
|
|
syntax = syntax or syntax_from_mime(name, mime)
|
|
if not syntax:
|
|
return error_dialog(
|
|
self.gui, _('Unsupported file format'),
|
|
_('Editing files of type %s is not supported' % mime), show=True)
|
|
return self.edit_file(name, syntax)
|
|
|
|
# Editor basic controls {{{
|
|
def do_editor_undo(self):
|
|
ed = self.gui.central.current_editor
|
|
if ed is not None:
|
|
ed.undo()
|
|
|
|
def do_editor_redo(self):
|
|
ed = self.gui.central.current_editor
|
|
if ed is not None:
|
|
ed.redo()
|
|
|
|
def do_editor_copy(self):
|
|
ed = self.gui.central.current_editor
|
|
if ed is not None:
|
|
ed.copy()
|
|
|
|
def do_editor_cut(self):
|
|
ed = self.gui.central.current_editor
|
|
if ed is not None:
|
|
ed.cut()
|
|
|
|
def do_editor_paste(self):
|
|
ed = self.gui.central.current_editor
|
|
if ed is not None:
|
|
ed.paste()
|
|
|
|
def editor_data_changed(self, editor):
|
|
self.gui.preview.start_refresh_timer()
|
|
|
|
def editor_undo_redo_state_changed(self, *args):
|
|
self.apply_current_editor_state()
|
|
|
|
def editor_copy_available_state_changed(self, *args):
|
|
self.apply_current_editor_state()
|
|
|
|
def editor_modification_state_changed(self, is_modified):
|
|
self.apply_current_editor_state()
|
|
if is_modified:
|
|
self.set_modified()
|
|
# }}}
|
|
|
|
def apply_current_editor_state(self):
|
|
ed = self.gui.central.current_editor
|
|
self.gui.cursor_position_widget.update_position()
|
|
if ed is not None:
|
|
actions['editor-undo'].setEnabled(ed.undo_available)
|
|
actions['editor-redo'].setEnabled(ed.redo_available)
|
|
actions['editor-copy'].setEnabled(ed.copy_available)
|
|
actions['editor-cut'].setEnabled(ed.cut_available)
|
|
actions['go-to-line-number'].setEnabled(ed.has_line_numbers)
|
|
actions['fix-html-current'].setEnabled(ed.syntax == 'html')
|
|
name = None
|
|
for n, x in editors.iteritems():
|
|
if ed is x:
|
|
name = n
|
|
break
|
|
if name is not None and getattr(ed, 'syntax', None) == 'html':
|
|
if self.gui.preview.show(name):
|
|
# The file being displayed by the preview has changed.
|
|
# Set the preview's position to the current cursor
|
|
# position in the editor, in case the editors' cursor
|
|
# position has not changed, since the last time it was
|
|
# focused. This is not inefficient since multiple requests
|
|
# to sync are de-bounced with a 100 msec wait.
|
|
self.sync_preview_to_editor()
|
|
if name is not None:
|
|
self.gui.file_list.mark_name_as_current(name)
|
|
if ed.has_line_numbers:
|
|
self.gui.cursor_position_widget.update_position(*ed.cursor_position)
|
|
else:
|
|
actions['go-to-line-number'].setEnabled(False)
|
|
self.gui.file_list.clear_currently_edited_name()
|
|
|
|
def update_cursor_position(self):
|
|
ed = self.gui.central.current_editor
|
|
if getattr(ed, 'has_line_numbers', False):
|
|
self.gui.cursor_position_widget.update_position(*ed.cursor_position)
|
|
else:
|
|
self.gui.cursor_position_widget.update_position()
|
|
|
|
def editor_close_requested(self, editor):
|
|
name = None
|
|
for n, ed in editors.iteritems():
|
|
if ed is editor:
|
|
name = n
|
|
if not name:
|
|
return
|
|
if not editor.is_synced_to_container:
|
|
self.commit_editor_to_container(name)
|
|
self.close_editor(name)
|
|
|
|
def close_editor(self, name):
|
|
editor = editors.pop(name)
|
|
self.gui.central.close_editor(editor)
|
|
editor.break_cycles()
|
|
if not editors or getattr(self.gui.central.current_editor, 'syntax', None) != 'html':
|
|
self.gui.preview.clear()
|
|
|
|
def insert_character(self):
|
|
self.gui.insert_char.show()
|
|
|
|
# Shutdown {{{
|
|
def quit(self):
|
|
if not self.confirm_quit():
|
|
return
|
|
self.save_state()
|
|
self.shutdown()
|
|
QApplication.instance().quit()
|
|
|
|
def confirm_quit(self):
|
|
if self.doing_terminal_save:
|
|
return False
|
|
if self.save_manager.has_tasks:
|
|
if not question_dialog(
|
|
self.gui, _('Are you sure?'), _(
|
|
'The current book is being saved in the background, quitting will abort'
|
|
' the save process, are you sure?'), default_yes=False):
|
|
return False
|
|
|
|
if self.gui.action_save.isEnabled():
|
|
d = QDialog(self.gui)
|
|
d.l = QGridLayout(d)
|
|
d.setLayout(d.l)
|
|
d.setWindowTitle(_('Unsaved changes'))
|
|
d.i = QLabel('')
|
|
d.i.setPixmap(QPixmap(I('save.png')).scaledToHeight(64, Qt.SmoothTransformation))
|
|
d.i.setMaximumSize(QSize(d.i.pixmap().width(), 64))
|
|
d.i.setScaledContents(True)
|
|
d.l.addWidget(d.i, 0, 0)
|
|
d.m = QLabel(_('There are unsaved changes, if you quit without saving, you will lose them.'))
|
|
d.m.setWordWrap(True)
|
|
d.l.addWidget(d.m, 0, 1)
|
|
d.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
|
|
d.bb.rejected.connect(d.reject)
|
|
d.bb.accepted.connect(d.accept)
|
|
d.l.addWidget(d.bb, 1, 0, 1, 2)
|
|
d.do_save = None
|
|
def endit(x):
|
|
d.do_save = x
|
|
d.accept()
|
|
b = d.bb.addButton(_('&Save and Quit'), QDialogButtonBox.ActionRole)
|
|
b.setIcon(QIcon(I('save.png')))
|
|
b.clicked.connect(lambda *args: endit(True))
|
|
b = d.bb.addButton(_('&Quit without saving'), QDialogButtonBox.ActionRole)
|
|
b.clicked.connect(lambda *args: endit(False))
|
|
d.resize(d.sizeHint())
|
|
if d.exec_() != d.Accepted or d.do_save is None:
|
|
return False
|
|
if d.do_save:
|
|
self.gui.action_save.trigger()
|
|
self.gui.blocking_job.set_msg(_('Saving, please wait...'))
|
|
self.gui.blocking_job.start()
|
|
self.doing_terminal_save = True
|
|
QTimer.singleShot(50, self.check_terminal_save)
|
|
return False
|
|
|
|
return True
|
|
|
|
def check_terminal_save(self):
|
|
if self.save_manager.has_tasks:
|
|
return QTimer.singleShot(50, self.check_terminal_save)
|
|
self.shutdown()
|
|
QApplication.instance().quit()
|
|
|
|
def shutdown(self):
|
|
self.gui.preview.stop_refresh_timer()
|
|
self.save_state()
|
|
[x.reject() for x in _diff_dialogs]
|
|
del _diff_dialogs[:]
|
|
self.save_manager.shutdown()
|
|
parse_worker.shutdown()
|
|
self.save_manager.wait(0.1)
|
|
|
|
def save_state(self):
|
|
with tprefs:
|
|
self.gui.save_state()
|
|
# }}}
|
|
|