Edit book: Allow merging HTML files by drag and drop of the files onto another HTML file

This commit is contained in:
Kovid Goyal 2021-11-14 13:56:34 +05:30
parent 852e5ca1dd
commit dc21508a39
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 70 additions and 28 deletions

View File

@ -127,6 +127,9 @@ links and references to the merged files. Note that merging files can sometimes
cause text styling to change, since the individual files could have used
different stylesheets.
You can also select text files and then drag and drop the text files onto
another text file to merge the dropped text files into the target text file.
Changing text file order
^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -134,7 +137,8 @@ You can re-arrange the order in which text (HTML) files are opened when reading
the book by simply dragging and dropping them in the Files browser. For the
technically inclined, this is called re-ordering the book spine. Note that you
have to drop the items *between* other items, not on top of them, this can be a
little fiddly until you get used to it.
little fiddly until you get used to it. Dropping on top of another file will
cause the files to be merged.
Marking the cover
^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -8,14 +8,14 @@ import posixpath
import sys
import textwrap
from collections import Counter, OrderedDict, defaultdict
from gettext import pgettext
from functools import partial
from gettext import pgettext
from qt.core import (
QApplication, QCheckBox, QDialog, QDialogButtonBox, QFont, QFormLayout, QItemSelectionModel,
QGridLayout, QIcon, QInputDialog, QLabel, QLineEdit, QListWidget, QAbstractItemView,
QListWidgetItem, QMenu, QPainter, QPixmap, QRadioButton, QScrollArea, QSize,
QSpinBox, QStyle, QStyledItemDelegate, Qt, QTimer, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget, pyqtSignal, sip
QAbstractItemView, QApplication, QCheckBox, QDialog, QDialogButtonBox, QFont,
QFormLayout, QGridLayout, QIcon, QInputDialog, QItemSelectionModel, QLabel,
QLineEdit, QListWidget, QListWidgetItem, QMenu, QPainter, QPixmap, QRadioButton,
QScrollArea, QSize, QSpinBox, QStyle, QStyledItemDelegate, Qt, QTimer, QTreeView,
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal, sip
)
from calibre import human_readable, sanitize_file_name
@ -43,7 +43,6 @@ from calibre_extensions.progress_indicator import set_no_activate_on_click
from polyglot.binary import as_hex_unicode
from polyglot.builtins import iteritems
FILE_COPY_MIME = 'application/calibre-edit-book-files'
TOP_ICON_SIZE = 24
NAME_ROLE = Qt.ItemDataRole.UserRole
@ -293,6 +292,63 @@ class FileList(QTreeWidget, OpenWithHandler):
ans.setData(CONTAINER_DND_MIMETYPE, '\n'.join(filter(None, names)).encode('utf-8'))
return ans
def dropMimeData(self, parent, index, data, action):
if not parent or not data.hasFormat(CONTAINER_DND_MIMETYPE):
return False
names = bytes(data.data(CONTAINER_DND_MIMETYPE)).decode('utf-8').splitlines()
if not names:
return False
category = parent.data(0, CATEGORY_ROLE)
if category is None:
self.handle_reorder_drop(parent, index, names)
elif category == 'text':
self.handle_merge_drop(parent, names)
return False # we have to return false to prevent Qt's internal machinery from re-ordering nodes
def handle_merge_drop(self, target_node, names):
category_node = target_node.parent()
current_order = {category_node.child(i).data(0, NAME_ROLE):i for i in range(category_node.childCount())}
names = sorted(names, key=lambda x: current_order.get(x, -1))
target_name = target_node.data(0, NAME_ROLE)
if len(names) == 1:
msg = _('Merge the file {0} into the file {1}?').format(elided_text(names[0]), elided_text(target_name))
else:
msg = _('Merge the {0} selected files into the file {1}?').format(len(names), elided_text(target_name))
if question_dialog(self, _('Merge files'), msg, skip_dialog_name='edit-book-merge-on-drop'):
names.append(target_name)
names = sorted(names, key=lambda x: current_order.get(x, -1))
self.merge_requested.emit(target_node.data(0, CATEGORY_ROLE), names, target_name)
def handle_reorder_drop(self, category_node, idx, names):
current_order = tuple(category_node.child(i).data(0, NAME_ROLE) for i in range(category_node.childCount()))
linear_map = {category_node.child(i).data(0, NAME_ROLE):category_node.child(i).data(0, LINEAR_ROLE) for i in range(category_node.childCount())}
order_map = {name: i for i, name in enumerate(current_order)}
try:
insert_before = current_order[idx]
except IndexError:
insert_before = None
names = sorted(names, key=lambda x: order_map.get(x, -1))
moved_names = frozenset(names)
new_names = [n for n in current_order if n not in moved_names]
try:
insertion_point = len(new_names) if insert_before is None else new_names.index(insert_before)
except ValueError:
return
new_names = new_names[:insertion_point] + names + new_names[insertion_point:]
order = [[name, linear_map[name]] for name in new_names]
# Ensure that all non-linear items are at the end, by making any non-linear
# items not at the end, linear
for i, (name, linear) in tuple(enumerate(order)):
if not linear and i < len(order) - 1 and order[i+1][1]:
order[i][1] = True
self.reorder_spine.emit(order)
def dropEvent(self, event):
# the dropEvent() implementation of QTreeWidget handles InternalMoves
# internally and is not suitable for us. QTreeView::dropEvent calls
# dropMimeData() where we handle the drop
QTreeView.dropEvent(self, event)
@property
def current_name(self):
ci = self.currentItem()
@ -470,7 +526,7 @@ class FileList(QTreeWidget, OpenWithHandler):
item = QTreeWidgetItem(self.categories['text' if linear is not None else category], 1)
flags = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
if category == 'text':
flags |= Qt.ItemFlag.ItemIsDragEnabled
flags |= Qt.ItemFlag.ItemIsDragEnabled | Qt.ItemFlag.ItemIsDropEnabled
if name not in cannot_be_renamed:
flags |= Qt.ItemFlag.ItemIsEditable
item.setFlags(flags)
@ -479,6 +535,7 @@ class FileList(QTreeWidget, OpenWithHandler):
item.setData(0, CATEGORY_ROLE, category)
item.setData(0, LINEAR_ROLE, bool(linear))
item.setData(0, MIME_ROLE, imt)
set_display_name(name, item)
tooltips = []
emblems = []
@ -815,25 +872,6 @@ class FileList(QTreeWidget, OpenWithHandler):
ans = list(sorted(ans, key=lambda idx:idx.row()))
return ans
def dropEvent(self, event):
with self:
text = self.categories['text']
pre_drop_order = {text.child(i).data(0, NAME_ROLE):i for i in range(text.childCount())}
super().dropEvent(event)
current_order = {text.child(i).data(0, NAME_ROLE):i for i in range(text.childCount())}
if current_order != pre_drop_order:
order = []
for child in (text.child(i) for i in range(text.childCount())):
name = str(child.data(0, NAME_ROLE) or '')
linear = bool(child.data(0, LINEAR_ROLE))
order.append([name, linear])
# Ensure that all non-linear items are at the end, any non-linear
# items not at the end will be made linear
for i, (name, linear) in tuple(enumerate(order)):
if not linear and i < len(order) - 1 and order[i+1][1]:
order[i][1] = True
self.reorder_spine.emit(order)
def item_double_clicked(self, item, column):
category = str(item.data(0, CATEGORY_ROLE) or '')
if category: