diff --git a/src/calibre/gui2/actions/show_quickview.py b/src/calibre/gui2/actions/show_quickview.py index bc2dc82b0c..90de2d2d93 100644 --- a/src/calibre/gui2/actions/show_quickview.py +++ b/src/calibre/gui2/actions/show_quickview.py @@ -6,14 +6,16 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +from PyQt5.Qt import QAction + from calibre.gui2.actions import InterfaceAction from calibre.gui2.dialogs.quickview import Quickview from calibre.gui2 import error_dialog class ShowQuickviewAction(InterfaceAction): - name = 'Show quickview' - action_spec = (_('Show quickview'), 'search.png', None, _('Q')) + name = 'Show Quickview' + action_spec = (_('Show Quickview'), 'search.png', None, _('Q')) dont_add_to = frozenset(['context-menu-device']) action_type = 'current' @@ -22,9 +24,19 @@ class ShowQuickviewAction(InterfaceAction): def genesis(self): self.qaction.triggered.connect(self.show_quickview) + self.focus_action = QAction(self.gui) + self.gui.addAction(self.focus_action) + self.gui.keyboard.register_shortcut('Focus To Quickview', _('Focus To Quickview'), + description=_('Move the focus to the Quickview pane/window'), + default_keys=('Shift+Q',), action=self.focus_action, + group=self.action_spec[0]) + self.focus_action.triggered.connect(self.focus_quickview) + def show_quickview(self, *args): if self.current_instance: if not self.current_instance.is_closed: + self.current_instance.reject() + self.current_instance = None return self.current_instance = None if self.gui.current_view() is not self.gui.library_view: @@ -34,10 +46,16 @@ class ShowQuickviewAction(InterfaceAction): return index = self.gui.library_view.currentIndex() if index.isValid(): - self.current_instance = \ - Quickview(self.gui, self.gui.library_view, index) + self.current_instance = Quickview(self.gui, index) + self.current_instance.reopen_quickview.connect(self.reopen_quickview) self.current_instance.show() + def reopen_quickview(self): + if self.current_instance and not self.current_instance.is_closed: + self.current_instance.reject() + self.current_instance = None + self.show_quickview() + def change_quickview_column(self, idx): self.show_quickview() if self.current_instance: @@ -48,3 +66,8 @@ class ShowQuickviewAction(InterfaceAction): def library_changed(self, db): if self.current_instance and not self.current_instance.is_closed: self.current_instance.reject() + + def focus_quickview(self): + if not (self.current_instance and not self.current_instance.is_closed): + self.show_quickview() + self.current_instance.set_focus() diff --git a/src/calibre/gui2/dialogs/quickview.py b/src/calibre/gui2/dialogs/quickview.py index 9df7200fbe..e496e346f6 100644 --- a/src/calibre/gui2/dialogs/quickview.py +++ b/src/calibre/gui2/dialogs/quickview.py @@ -4,9 +4,10 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' -from PyQt5.Qt import (Qt, QDialog, QAbstractItemView, QTableWidgetItem, - QListWidgetItem, QByteArray, QCoreApplication, - QApplication, pyqtSignal, QDialogButtonBox) +from PyQt5.Qt import ( + Qt, QDialog, QAbstractItemView, QTableWidgetItem, QIcon, QListWidgetItem, + QCoreApplication, QEvent, QObject, QApplication, pyqtSignal, + QDialogButtonBox, QByteArray) from calibre.customize.ui import find_plugin from calibre.gui2 import gprefs @@ -14,6 +15,7 @@ from calibre.gui2.dialogs.quickview_ui import Ui_Quickview from calibre.utils.icu import sort_key class TableItem(QTableWidgetItem): + ''' A QTableWidgetItem that sorts on a separate string and uses ICU rules ''' @@ -42,12 +44,53 @@ class TableItem(QTableWidgetItem): return self.sort_idx < other.sort_idx return 0 +IN_WIDGET_ITEMS = 0 +IN_WIDGET_BOOKS = 1 +IN_WIDGET_LOCK = 2 +IN_WIDGET_DOCK = 3 +IN_WIDGET_SEARCH = 4 +IN_WIDGET_CLOSE = 5 + +class BooksKeyPressFilter(QObject): + + return_pressed_signal = pyqtSignal() + + def eventFilter(self, obj, event): + if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Return: + self.return_pressed_signal.emit() + return True + return False + +class WidgetTabFilter(QObject): + + def __init__(self, attach_to_Class, which_widget, tab_signal): + QObject.__init__(self, attach_to_Class) + self.tab_signal = tab_signal + self.which_widget = which_widget + + def eventFilter(self, obj, event): + if event.type() == QEvent.KeyPress: + if event.key() == Qt.Key_Tab: + self.tab_signal.emit(self.which_widget, True) + return True + if event.key() == Qt.Key_Backtab: + self.tab_signal.emit(self.which_widget, False) + return True + return False + class Quickview(QDialog, Ui_Quickview): - change_quickview_column = pyqtSignal(object) + change_quickview_column = pyqtSignal(object) + reopen_quickview = pyqtSignal() + tab_pressed_signal = pyqtSignal(object, object) - def __init__(self, gui, view, row): - QDialog.__init__(self, gui, flags=Qt.Window) + def __init__(self, gui, row): + self.is_pane = gprefs.get('quickview_is_pane', False) + + if not self.is_pane: + QDialog.__init__(self, gui, flags=Qt.Window) + else: + QDialog.__init__(self, gui) Ui_Quickview.__init__(self) self.setupUi(self) self.isClosed = False @@ -56,19 +99,21 @@ class Quickview(QDialog, Ui_Quickview): try: self.books_table_column_widths = \ gprefs.get('quickview_dialog_books_table_widths', None) - geom = gprefs.get('quickview_dialog_geometry', bytearray('')) - self.restoreGeometry(QByteArray(geom)) + if not self.is_pane: + geom = gprefs.get('quickview_dialog_geometry', bytearray('')) + self.restoreGeometry(QByteArray(geom)) except: pass - # Remove the help button from the window title bar - icon = self.windowIcon() - self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) - self.setWindowFlags(self.windowFlags()|Qt.WindowStaysOnTopHint) - self.setWindowIcon(icon) + if not self.is_pane: + # Remove the help button from the window title bar + icon = self.windowIcon() + self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) + self.setWindowFlags(self.windowFlags()|Qt.WindowStaysOnTopHint) + self.setWindowIcon(icon) - self.db = view.model().db - self.view = view + self.view = gui.library_view + self.db = self.view.model().db self.gui = gui self.is_closed = False self.current_book_id = None @@ -88,7 +133,19 @@ class Quickview(QDialog, Ui_Quickview): self.items.setSelectionMode(QAbstractItemView.SingleSelection) self.items.currentTextChanged.connect(self.item_selected) + self.tab_pressed_signal.connect(self.tab_pressed) # Set up the books table columns + return_filter = BooksKeyPressFilter(self.books_table) + return_filter.return_pressed_signal.connect(self.return_pressed) + self.books_table.installEventFilter(return_filter) + + self.close_button = self.buttonBox.button(QDialogButtonBox.Close) + + self.tab_order_widgets = [self.items, self.books_table, self.lock_qv, + self.dock_button, self.search_button, self.close_button] + for idx,widget in enumerate(self.tab_order_widgets): + widget.installEventFilter(WidgetTabFilter(widget, idx, self.tab_pressed_signal)) + self.books_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.books_table.setSelectionMode(QAbstractItemView.SingleSelection) self.books_table.setColumnCount(3) @@ -118,10 +175,45 @@ class Quickview(QDialog, Ui_Quickview): self.change_quickview_column.connect(self.slave) QCoreApplication.instance().aboutToQuit.connect(self.save_state) self.search_button.clicked.connect(self.do_search) - view.model().new_bookdisplay_data.connect(self.book_was_changed) + self.view.model().new_bookdisplay_data.connect(self.book_was_changed) - close_button = self.buttonBox.button(QDialogButtonBox.Close) - close_button.setAutoDefault(False) + self.close_button.setDefault(False) + if self.is_pane: + self.dock_button.setText(_('Undock')) + self.dock_button.setToolTip(_('Pop up the quickview panel into its own floating window')) + self.dock_button.setIcon(QIcon(I('arrow-up.png'))) + self.lock_qv.setText(_('Lock Quickview contents')) + self.search_button.setText(_('Search')) + self.gui.quickview_splitter.add_quickview_dialog(self) + else: + self.lock_qv.setText(_('&Dock')) + self.close_button.setText(_('&Close')) + self.dock_button.setToolTip(_('Embed the quickview panel into the main calibre window')) + self.dock_button.setIcon(QIcon(I('arrow-down.png'))) + self.set_focus() + + self.books_table.horizontalHeader().sectionResized.connect(self.section_resized) + self.dock_button.clicked.connect(self.show_as_pane_changed) + + def tab_pressed(self, in_widget, isForward): + if isForward: + in_widget += 1 + if in_widget >= len(self.tab_order_widgets): + in_widget = 0 + else: + in_widget -= 1 + if in_widget < 0: + in_widget = len(self.tab_order_widgets) - 1 + self.tab_order_widgets[in_widget].setFocus(Qt.TabFocusReason) + + def show(self): + QDialog.show(self) + if self.is_pane: + self.gui.quickview_splitter.show_quickview_widget() + + def show_as_pane_changed(self): + gprefs['quickview_is_pane'] = not gprefs.get('quickview_is_pane', False) + self.reopen_quickview.emit() # search button def do_search(self): @@ -172,8 +264,10 @@ class Quickview(QDialog, Ui_Quickview): self.indicate_no_items() return key = self.current_key - self.items_label.setText(_('&Item: {0} ({1})').format( - self.db.field_metadata[key]['name'], key)) + label_text = _('&Item: {0} ({1})') + if self.is_pane: + label_text = label_text.replace('&', '') + self.items_label.setText(label_text.format(self.db.field_metadata[key]['name'], key)) self.items.blockSignals(True) self.items.clear() @@ -224,8 +318,10 @@ class Quickview(QDialog, Ui_Quickview): sort_results=False) self.books_table.setRowCount(len(books)) - self.books_label.setText(_('&Books with selected item "{0}": {1}'). - format(selected_item, len(books))) + label_text = _('&Books with selected item "{0}": {1}') + if self.is_pane: + label_text = label_text.replace('&', '') + self.books_label.setText(label_text.format(selected_item, len(books))) select_item = None self.books_table.setSortingEnabled(False) @@ -261,6 +357,11 @@ class Quickview(QDialog, Ui_Quickview): # correct until the first paint. def resizeEvent(self, *args): QDialog.resizeEvent(self, *args) + + # Do this if we are resizing for the first time to reset state. + if self.is_pane and self.height() == 0: + self.gui.quickview_splitter.set_sizes() + if self.books_table_column_widths is not None: for c,w in enumerate(self.books_table_column_widths): self.books_table.setColumnWidth(c, w) @@ -274,9 +375,15 @@ class Quickview(QDialog, Ui_Quickview): self.books_table.setColumnWidth(c, w) self.save_state() + def return_pressed(self): + self.select_book(self.books_table.currentRow()) + def book_doubleclicked(self, row, column): if self.no_valid_items: return + self.select_book(row) + + def select_book(self, row): book_id = int(self.books_table.item(row, self.title_column).data(Qt.UserRole)) self.view.select_rows([book_id]) modifiers = int(QApplication.keyboardModifiers()) @@ -285,6 +392,9 @@ class Quickview(QDialog, Ui_Quickview): if em is not None: em.actual_plugin_.edit_metadata(None) + def set_focus(self): + self.books_table.setFocus(Qt.ActiveWindowFocusReason) + # called when a book is clicked on the library view def slave(self, current): if self.is_closed: @@ -292,6 +402,9 @@ class Quickview(QDialog, Ui_Quickview): self.refresh(current) self.view.activateWindow() + def section_resized(self, logicalIndex, oldSize, newSize): + self.save_state() + def save_state(self): if self.is_closed: return @@ -299,7 +412,8 @@ class Quickview(QDialog, Ui_Quickview): for c in range(0, self.books_table.columnCount()): self.books_table_column_widths.append(self.books_table.columnWidth(c)) gprefs['quickview_dialog_books_table_widths'] = self.books_table_column_widths - gprefs['quickview_dialog_geometry'] = bytearray(self.saveGeometry()) + if not self.is_pane: + gprefs['quickview_dialog_geometry'] = bytearray(self.saveGeometry()) def _close(self): self.save_state() @@ -307,12 +421,10 @@ class Quickview(QDialog, Ui_Quickview): self.db = self.view = self.gui = None self.is_closed = True - # called by the window system - def closeEvent(self, *args): - self._close() - QDialog.closeEvent(self, *args) - # called by the close button def reject(self): + if self.is_pane: + self.gui.quickview_splitter.hide_quickview_widget() + self.gui.library_view.setFocus(Qt.ActiveWindowFocusReason) self._close() QDialog.reject(self) diff --git a/src/calibre/gui2/dialogs/quickview.ui b/src/calibre/gui2/dialogs/quickview.ui index 3bd909288f..934744c338 100644 --- a/src/calibre/gui2/dialogs/quickview.ui +++ b/src/calibre/gui2/dialogs/quickview.ui @@ -11,7 +11,7 @@ - + 0 0 @@ -68,8 +68,31 @@ &Lock Quickview contents - Select to prevent Quickview from changing content when the - selection on the library view is changed + <p>Select to prevent Quickview from changing content when the + selection on the library view is changed</p> + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + &Dock + + + false diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 840c4c5ab8..f84df9d587 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -8,7 +8,8 @@ __docformat__ = 'restructuredtext en' import functools from PyQt5.Qt import (Qt, QApplication, QStackedWidget, QMenu, QTimer, - QSize, QSizePolicy, QStatusBar, QLabel, QFont, QAction, QTabBar) + QSize, QSizePolicy, QStatusBar, QLabel, QFont, QAction, QTabBar, + QVBoxLayout, QWidget, QSplitter) from calibre.utils.config import prefs from calibre.utils.icu import sort_key @@ -99,6 +100,37 @@ class LibraryViewMixin(object): # {{{ # }}} +class QuickviewSplitter(QSplitter): # {{{ + + def __init__(self, parent=None, orientation=Qt.Vertical, qv_widget=None): + QSplitter.__init__(self, parent=parent, orientation=orientation) + self.splitterMoved.connect(self.splitter_moved) + self.setChildrenCollapsible(False) + self.qv_widget = qv_widget + + def splitter_moved(self): + gprefs['quickview_dialog_heights'] = self.sizes() + + def resizeEvent(self, *args): + QSplitter.resizeEvent(self, *args) + if self.sizes()[1] != 0: + gprefs['quickview_dialog_heights'] = self.sizes() + + def set_sizes(self): + sizes = gprefs.get('quickview_dialog_heights', []) + if len(sizes) == 2: + self.setSizes(sizes) + + def add_quickview_dialog(self, qv_dialog): + self.qv_widget.layout().addWidget(qv_dialog) + + def show_quickview_widget(self): + self.qv_widget.show() + + def hide_quickview_widget(self): + self.qv_widget.hide() +# }}} + class LibraryWidget(Splitter): # {{{ def __init__(self, parent): @@ -113,6 +145,10 @@ class LibraryWidget(Splitter): # {{{ connect_button=not config['separate_cover_flow'], side_index=idx, initial_side_size=size, initial_show=False, shortcut='Shift+Alt+B') + + quickview_widget = QWidget() + parent.quickview_splitter = QuickviewSplitter( + parent=self, orientation=Qt.Vertical, qv_widget=quickview_widget) parent.library_view = BooksView(parent) parent.library_view.setObjectName('library_view') stack = QStackedWidget(self) @@ -121,7 +157,13 @@ class LibraryWidget(Splitter): # {{{ parent.grid_view = GridView(parent) parent.grid_view.setObjectName('grid_view') av.add_view('grid', parent.grid_view) - self.addWidget(stack) + parent.quickview_splitter.addWidget(stack) + + quickview_widget.setLayout(QVBoxLayout()) + parent.quickview_splitter.addWidget(quickview_widget) + parent.quickview_splitter.hide_quickview_widget() + + self.addWidget(parent.quickview_splitter) # }}} class Stack(QStackedWidget): # {{{