E-book viewer: Make the list of bookmarks a dockable window that can be kept open while reading the book

This commit is contained in:
Kovid Goyal 2014-08-05 21:59:34 +05:30
parent df48faedfe
commit 78287334b0
4 changed files with 110 additions and 206 deletions

View File

@ -6,44 +6,88 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import cPickle, os import cPickle
from PyQt4.Qt import ( from PyQt4.Qt import (
Qt, QDialog, QListWidgetItem, QFileDialog, QItemSelectionModel) Qt, QListWidget, QListWidgetItem, QItemSelectionModel,
QGridLayout, QPushButton, QIcon, QWidget, pyqtSignal, QLabel)
from calibre.gui2.viewer.bookmarkmanager_ui import Ui_BookmarkManager from calibre.gui2 import choose_save_file, choose_files
class BookmarkManager(QDialog, Ui_BookmarkManager): class BookmarksList(QListWidget):
def __init__(self, parent, bookmarks):
QDialog.__init__(self, parent)
self.setupUi(self) changed = pyqtSignal()
self.original_bookmarks = bookmarks def __init__(self, parent=None):
self.set_bookmarks() QListWidget.__init__(self, parent)
self.setDragEnabled(True)
self.setDragDropMode(self.InternalMove)
self.setDefaultDropAction(Qt.MoveAction)
self.setAlternatingRowColors(True)
self.setStyleSheet('QListView::item { padding: 0.5ex }')
self.viewport().setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.button_revert.clicked.connect(lambda :self.set_bookmarks()) def dropEvent(self, ev):
self.button_delete.clicked.connect(self.delete_bookmark) QListWidget.dropEvent(self, ev)
self.button_edit.clicked.connect(self.edit_bookmark) if ev.isAccepted():
self.button_export.clicked.connect(self.export_bookmarks) self.changed.emit()
self.button_import.clicked.connect(self.import_bookmarks)
self.bookmarks_list.setStyleSheet('QListView::item { padding: 0.5ex }')
self.bookmarks_list.viewport().setAcceptDrops(True)
self.bookmarks_list.setDropIndicatorShown(True)
self.bookmarks_list.itemChanged.connect(self.item_changed)
self.resize(600, 500)
self.bookmarks_list.setFocus(Qt.OtherFocusReason)
def set_bookmarks(self, bookmarks=None):
if bookmarks is None: class BookmarkManager(QWidget):
bookmarks = self.original_bookmarks
edited = pyqtSignal(object)
activated = pyqtSignal(object)
create_requested = pyqtSignal()
def __init__(self, parent):
QWidget.__init__(self, parent)
self.l = l = QGridLayout(self)
l.setContentsMargins(0, 0, 0, 0)
self.setLayout(l)
self.bookmarks_list = bl = BookmarksList(self)
bl.itemChanged.connect(self.item_changed)
l.addWidget(bl, 0, 0, 1, -1)
bl.itemClicked.connect(self.item_activated)
bl.changed.connect(lambda : self.edited.emit(self.get_bookmarks()))
self.la = la = QLabel(_(
'Double click to edit and drag-and-drop to re-order the bookmarks'))
la.setWordWrap(True)
l.addWidget(la, l.rowCount(), 0, 1, -1)
self.button_new = b = QPushButton(QIcon(I('bookmarks.png')), _('&New'), self)
b.clicked.connect(self.create_requested)
b.setToolTip(_('Create a new bookmark at the current location'))
l.addWidget(b)
self.button_delete = b = QPushButton(QIcon(I('trash.png')), _('&Remove'), self)
b.setToolTip(_('Remove the currently selected bookmark'))
b.clicked.connect(self.delete_bookmark)
l.addWidget(b, l.rowCount() - 1, 1)
self.button_export = b = QPushButton(QIcon(I('back.png')), _('E&xport'), self)
b.clicked.connect(self.export_bookmarks)
l.addWidget(b)
self.button_import = b = QPushButton(QIcon(I('forward.png')), _('&Import'), self)
b.clicked.connect(self.import_bookmarks)
l.addWidget(b, l.rowCount() - 1, 1)
def item_activated(self, item):
bm = self.item_to_bm(item)
self.activated.emit(bm)
def set_bookmarks(self, bookmarks=()):
self.bookmarks_list.clear() self.bookmarks_list.clear()
for bm in bookmarks: for bm in bookmarks:
i = QListWidgetItem(bm['title']) if bm['title'] != 'calibre_current_page_bookmark':
i.setData(Qt.UserRole, self.bm_to_item(bm)) i = QListWidgetItem(bm['title'])
i.setFlags(i.flags() | Qt.ItemIsEditable) i.setData(Qt.UserRole, self.bm_to_item(bm))
self.bookmarks_list.addItem(i) i.setFlags(i.flags() | Qt.ItemIsEditable)
if len(bookmarks) > 0: self.bookmarks_list.addItem(i)
if self.bookmarks_list.count() > 0:
self.bookmarks_list.setCurrentItem(self.bookmarks_list.item(0), QItemSelectionModel.ClearAndSelect) self.bookmarks_list.setCurrentItem(self.bookmarks_list.item(0), QItemSelectionModel.ClearAndSelect)
def item_changed(self, item): def item_changed(self, item):
@ -56,16 +100,19 @@ class BookmarkManager(QDialog, Ui_BookmarkManager):
bm['title'] = title bm['title'] = title
item.setData(Qt.UserRole, self.bm_to_item(bm)) item.setData(Qt.UserRole, self.bm_to_item(bm))
self.bookmarks_list.blockSignals(False) self.bookmarks_list.blockSignals(False)
self.edited.emit(self.get_bookmarks())
def delete_bookmark(self): def delete_bookmark(self):
row = self.bookmarks_list.currentRow() row = self.bookmarks_list.currentRow()
if row > -1: if row > -1:
self.bookmarks_list.takeItem(row) self.bookmarks_list.takeItem(row)
self.edited.emit(self.get_bookmarks())
def edit_bookmark(self): def edit_bookmark(self):
item = self.bookmarks_list.currentItem() item = self.bookmarks_list.currentItem()
if item is not None: if item is not None:
self.bookmarks_list.editItem(item) self.bookmarks_list.editItem(item)
self.edited.emit(self.get_bookmarks())
def bm_to_item(self, bm): def bm_to_item(self, bm):
return bytearray(cPickle.dumps(bm, -1)) return bytearray(cPickle.dumps(bm, -1))
@ -78,22 +125,22 @@ class BookmarkManager(QDialog, Ui_BookmarkManager):
return [self.item_to_bm(l.item(i)) for i in xrange(l.count())] return [self.item_to_bm(l.item(i)) for i in xrange(l.count())]
def export_bookmarks(self): def export_bookmarks(self):
filename = QFileDialog.getSaveFileName(self, _("Export Bookmarks"), filename = choose_save_file(
'%s%suntitled.pickle' % (os.getcwdu(), os.sep), self, 'export-viewer-bookmarks', _('Export Bookmarks'),
_("Saved Bookmarks (*.pickle)")) filters=[(_('Saved Bookmarks'), ['pickle'])], all_files=False, initial_filename='bookmarks.pickle')
if not filename: if filename:
return with open(filename, 'wb') as fileobj:
cPickle.dump(self.get_bookmarks(), fileobj, -1)
with open(filename, 'w') as fileobj:
cPickle.dump(self.get_bookmarks(), fileobj)
def import_bookmarks(self): def import_bookmarks(self):
filename = QFileDialog.getOpenFileName(self, _("Import Bookmarks"), '%s' % os.getcwdu(), _("Pickled Bookmarks (*.pickle)")) files = choose_files(self, 'export-viewer-bookmarks', _('Import Bookmarks'),
if not filename: filters=[(_('Saved Bookmarks'), ['pickle'])], all_files=False, select_only_single_file=True)
if not files:
return return
filename = files[0]
imported = None imported = None
with open(filename, 'r') as fileobj: with open(filename, 'rb') as fileobj:
imported = cPickle.load(fileobj) imported = cPickle.load(fileobj)
if imported is not None: if imported is not None:
@ -103,7 +150,7 @@ class BookmarkManager(QDialog, Ui_BookmarkManager):
if 'title' not in bm: if 'title' not in bm:
bad = True bad = True
break break
except: except Exception:
pass pass
if not bad: if not bad:
@ -112,13 +159,5 @@ class BookmarkManager(QDialog, Ui_BookmarkManager):
if bm not in bookmarks: if bm not in bookmarks:
bookmarks.append(bm) bookmarks.append(bm)
self.set_bookmarks([bm for bm in bookmarks if bm['title'] != 'calibre_current_page_bookmark']) self.set_bookmarks([bm for bm in bookmarks if bm['title'] != 'calibre_current_page_bookmark'])
self.edited.emit(self.get_bookmarks())
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
d = BookmarkManager(None, [{'title':'Bookmark #%d' % i, 'data':b'xxxxx'} for i in range(1, 5)])
d.exec_()
import pprint
pprint.pprint(d.get_bookmarks())

View File

@ -1,143 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BookmarkManager</class>
<widget class="QDialog" name="BookmarkManager">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>451</width>
<height>363</height>
</rect>
</property>
<property name="windowTitle">
<string>Bookmark Manager</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Actions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="button_edit">
<property name="text">
<string>Edit</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/edit_input.png</normaloff>:/images/edit_input.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_delete">
<property name="text">
<string>Delete</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_revert">
<property name="text">
<string>Reset</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/edit-undo.png</normaloff>:/images/edit-undo.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_export">
<property name="text">
<string>Export</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/back.png</normaloff>:/images/back.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_import">
<property name="text">
<string>Import</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QListWidget" name="bookmarks_list">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>BookmarkManager</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>225</x>
<y>337</y>
</hint>
<hint type="destinationlabel">
<x>225</x>
<y>181</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>BookmarkManager</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>225</x>
<y>337</y>
</hint>
<hint type="destinationlabel">
<x>225</x>
<y>181</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -11,7 +11,6 @@ from PyQt4.Qt import (
from calibre.gui2.viewer.ui import Main as MainWindow from calibre.gui2.viewer.ui import Main as MainWindow
from calibre.gui2.viewer.printing import Printing from calibre.gui2.viewer.printing import Printing
from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
from calibre.gui2.viewer.toc import TOC from calibre.gui2.viewer.toc import TOC
from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files, from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files,
@ -109,6 +108,9 @@ class EbookViewer(MainWindow):
self.search.focus_to_library.connect(lambda: self.view.setFocus(Qt.OtherFocusReason)) self.search.focus_to_library.connect(lambda: self.view.setFocus(Qt.OtherFocusReason))
self.toc.pressed[QModelIndex].connect(self.toc_clicked) self.toc.pressed[QModelIndex].connect(self.toc_clicked)
self.reference.goto.connect(self.goto) self.reference.goto.connect(self.goto)
self.bookmarks.edited.connect(self.bookmarks_edited)
self.bookmarks.activated.connect(self.goto_bookmark)
self.bookmarks.create_requested.connect(self.bookmark)
self.set_bookmarks([]) self.set_bookmarks([])
self.load_theme_menu() self.load_theme_menu()
@ -762,11 +764,16 @@ class EbookViewer(MainWindow):
self.iterator.add_bookmark(bm) self.iterator.add_bookmark(bm)
self.set_bookmarks(self.iterator.bookmarks) self.set_bookmarks(self.iterator.bookmarks)
def set_bookmarks(self, bookmarks): def bookmarks_edited(self, bookmarks):
self.build_bookmarks_menu(bookmarks)
self.iterator.set_bookmarks(bookmarks)
self.iterator.save_bookmarks()
def build_bookmarks_menu(self, bookmarks):
self.bookmarks_menu.clear() self.bookmarks_menu.clear()
sc = _(' or ').join(self.view.shortcuts.get_shortcuts('Bookmark')) sc = _(' or ').join(self.view.shortcuts.get_shortcuts('Bookmark'))
self.bookmarks_menu.addAction(_("Bookmark this location [%s]") % sc, self.bookmark) self.bookmarks_menu.addAction(_("Bookmark this location [%s]") % sc, self.bookmark)
self.bookmarks_menu.addAction(_("Manage Bookmarks"), self.manage_bookmarks) self.bookmarks_menu.addAction(_("Show/hide Bookmarks"), self.bookmarks_dock.toggleViewAction().trigger)
self.bookmarks_menu.addSeparator() self.bookmarks_menu.addSeparator()
current_page = None current_page = None
self.existing_bookmarks = [] self.existing_bookmarks = []
@ -779,17 +786,9 @@ class EbookViewer(MainWindow):
self.bookmarks_menu.addAction(bm['title'], partial(self.goto_bookmark, bm)) self.bookmarks_menu.addAction(bm['title'], partial(self.goto_bookmark, bm))
return current_page return current_page
def manage_bookmarks(self): def set_bookmarks(self, bookmarks):
bmm = BookmarkManager(self, self.iterator.bookmarks) self.bookmarks.set_bookmarks(bookmarks)
if bmm.exec_() != BookmarkManager.Accepted: return self.build_bookmarks_menu(bookmarks)
return
bookmarks = bmm.get_bookmarks()
if bookmarks != self.iterator.bookmarks:
self.iterator.set_bookmarks(bookmarks)
self.iterator.save_bookmarks()
self.set_bookmarks(bookmarks)
def save_current_position(self): def save_current_position(self):
if not self.get_remember_current_page_opt(): if not self.get_remember_current_page_opt():

View File

@ -19,6 +19,7 @@ from calibre.gui2 import rating_font
from calibre.gui2.main_window import MainWindow from calibre.gui2.main_window import MainWindow
from calibre.gui2.search_box import SearchBox2 from calibre.gui2.search_box import SearchBox2
from calibre.gui2.viewer.documentview import DocumentView from calibre.gui2.viewer.documentview import DocumentView
from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
from calibre.gui2.viewer.toc import TOCView from calibre.gui2.viewer.toc import TOCView
class DoubleSpinBox(QDoubleSpinBox): # {{{ class DoubleSpinBox(QDoubleSpinBox): # {{{
@ -215,6 +216,14 @@ class Main(MainWindow):
self.addDockWidget(Qt.LeftDockWidgetArea, d) self.addDockWidget(Qt.LeftDockWidgetArea, d)
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.bookmarks_dock = d = QDockWidget(_('Bookmarks'), self)
self.bookmarks = BookmarkManager(self)
d.setObjectName('bookmarks-dock')
d.setWidget(self.bookmarks)
d.close() # starts out hidden
self.addDockWidget(Qt.RightDockWidgetArea, d)
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.create_actions() self.create_actions()
self.metadata = Metadata(self.centralwidget) self.metadata = Metadata(self.centralwidget)