From 7f2a1f99daf02d61d17f10dc8efc1622c799b85d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 19 Oct 2013 11:15:30 +0530 Subject: [PATCH] Drag 'n drop re-ordering of spine items --- src/calibre/ebooks/oeb/polish/container.py | 22 ++++++++++++++++++++++ src/calibre/gui2/tweak_book/boss.py | 11 +++++++++++ src/calibre/gui2/tweak_book/file_list.py | 18 ++++++++++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index b8362b73c9..72814f4564 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -355,6 +355,28 @@ class Container(object): # {{{ for name in nixed: self.remove_item(name) + def set_spine(self, spine_items): + ''' Set the spine to be spine_items where spine_items is an iterable of + the form (name, linear). Will raise an error if one of the names is not + present in the manifest. ''' + imap = self.manifest_id_map + imap = {name:item_id for item_id, name in imap.iteritems()} + items = [item for item, name, linear in self.spine_iter] + tail, last_tail = (items[0].tail, items[-1].tail) if items else ('\n ', '\n ') + map(self.remove_from_xml, items) + spine = self.opf_xpath('//opf:spine')[0] + spine.text = tail + for name, linear in spine_items: + i = spine.makeelement('{%s}itemref' % OPF_NAMESPACES['opf'], nsmap={'opf':OPF_NAMESPACES['opf']}) + i.tail = tail + i.set('idref', imap[name]) + spine.append(i) + if not linear: + i.set('linear', 'no') + if len(spine) > 0: + spine[-1].tail = last_tail + self.dirty(self.opf_name) + def remove_item(self, name): ''' Remove the item identified by name from this container. This removes all diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 0dda3b59d1..87f59a8d4c 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -35,6 +35,7 @@ class Boss(QObject): def __call__(self, gui): self.gui = gui gui.file_list.delete_requested.connect(self.delete_requested) + gui.file_list.reorder_spine.connect(self.reorder_spine) def mkdtemp(self): self.container_count += 1 @@ -130,6 +131,16 @@ class Boss(QObject): self.gui.file_list.delete_done(spine_items, other_items) # TODO: Update other GUI elements + def reorder_spine(self, items): + # TODO: If content.opf is dirty in an editor, abort, calling + # file_list.build(current_container) to undo drag and drop + self.add_savepoint(_('Re-order text')) + c = current_container() + c.set_spine(items) + self.gui.action_save.setEnabled(True) + self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items + # TODO: If content.opf is open in an editor, reload it + def save_book(self): self.gui.action_save.setEnabled(False) tdir = tempfile.mkdtemp(prefix='save-%05d-' % self.container_count, dir=self.tdir) diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index abe0724843..66d1aff212 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -22,6 +22,7 @@ from calibre.utils.icu import sort_key TOP_ICON_SIZE = 24 NAME_ROLE = Qt.UserRole CATEGORY_ROLE = NAME_ROLE + 1 +LINEAR_ROLE = CATEGORY_ROLE + 1 NBSP = '\xa0' class ItemDelegate(QStyledItemDelegate): # {{{ @@ -56,6 +57,7 @@ class ItemDelegate(QStyledItemDelegate): # {{{ class FileList(QTreeWidget): delete_requested = pyqtSignal(object, object) + reorder_spine = pyqtSignal(object) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) @@ -211,6 +213,7 @@ class FileList(QTreeWidget): item.setStatusTip(0, _('Full path: ') + name) item.setData(0, NAME_ROLE, name) item.setData(0, CATEGORY_ROLE, category) + item.setData(0, LINEAR_ROLE, bool(linear)) set_display_name(name, item) # TODO: Add appropriate tooltips based on the emblems emblems = [] @@ -294,11 +297,22 @@ class FileList(QTreeWidget): super(FileList, self).dropEvent(event) current_order = {text.child(i):i for i in xrange(text.childCount())} if current_order != pre_drop_order: - pass # TODO: Implement this + order = [] + for child in (text.child(i) for i in xrange(text.childCount())): + name = unicode(child.data(0, NAME_ROLE).toString()) + linear = child.data(0, LINEAR_ROLE).toBool() + 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) class FileListWidget(QWidget): delete_requested = pyqtSignal(object, object) + reorder_spine = pyqtSignal(object) def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -306,7 +320,7 @@ class FileListWidget(QWidget): self.file_list = FileList(self) self.layout().addWidget(self.file_list) self.layout().setContentsMargins(0, 0, 0, 0) - for x in ('delete_requested',): + for x in ('delete_requested', 'reorder_spine'): getattr(self.file_list, x).connect(getattr(self, x)) for x in ('delete_done',): setattr(self, x, getattr(self.file_list, x))