Example #1
0
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Set to undefined') + '\t' + QKeySequence(Qt.Key.Key_Space).toString(QKeySequence.SequenceFormat.NativeText),
                 self.clear_to_undefined)
     m.addSeparator()
     populate_standard_spinbox_context_menu(self, m)
     m.popup(ev.globalPos())
Example #2
0
 def show_context_menu(self, point):
     item = self.folders.itemAt(point)
     if item is None:
         return
     m = QMenu(self)
     m.addAction(QIcon(I('mimetypes/dir.png')), _('Create new folder'), partial(self.create_folder, item))
     m.popup(self.folders.mapToGlobal(point))
Example #3
0
class Quickview(QDialog, Ui_Quickview):

    reopen_after_dock_change = pyqtSignal()
    tab_pressed_signal = pyqtSignal(object, object)
    quickview_closed = pyqtSignal()

    def __init__(self, gui, row, toggle_shortcut):
        self.is_pane = gprefs.get('quickview_is_pane', False)

        if not self.is_pane:
            QDialog.__init__(self, gui, flags=Qt.WindowType.Widget)
        else:
            QDialog.__init__(self, gui)
        Ui_Quickview.__init__(self)
        self.setupUi(self)
        self.isClosed = False
        self.current_book = None
        self.closed_by_button = False

        if self.is_pane:
            self.main_grid_layout.setContentsMargins(0, 0, 0, 0)
        else:
            self.setWindowIcon(self.windowIcon())

        self.books_table_column_widths = None
        try:
            self.books_table_column_widths = \
                        gprefs.get('quickview_dialog_books_table_widths', None)
            if not self.is_pane:
                geom = gprefs.get('quickview_dialog_geometry', None)
                if geom:
                    QApplication.instance().safe_restore_geometry(
                        self, QByteArray(geom))
        except:
            pass

        self.view = gui.library_view
        self.db = self.view.model().db
        self.gui = gui
        self.is_closed = False
        self.current_book_id = None  # the db id of the book used to fill the lh pane
        self.current_column = None  # current logical column in books list
        self.current_key = None  # current lookup key in books list
        self.last_search = None
        self.no_valid_items = False
        self.follow_library_view = True

        self.apply_vls.setCheckState(
            Qt.CheckState.Checked if gprefs['qv_respects_vls'] else Qt.
            CheckState.Unchecked)
        self.apply_vls.stateChanged.connect(self.vl_box_changed)

        self.fm = self.db.field_metadata

        self.items.setSelectionMode(
            QAbstractItemView.SelectionMode.SingleSelection)
        self.items.currentTextChanged.connect(self.item_selected)
        self.items.setProperty('highlight_current_item', 150)
        self.items.itemDoubleClicked.connect(self.item_doubleclicked)
        self.items.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.items.customContextMenuRequested.connect(
            self.show_item_context_menu)

        focus_filter = WidgetFocusFilter(self.items)
        focus_filter.focus_entered_signal.connect(self.focus_entered)
        self.items.installEventFilter(focus_filter)

        self.tab_pressed_signal.connect(self.tab_pressed)
        return_filter = BooksTableFilter(self.books_table)
        return_filter.return_pressed_signal.connect(self.return_pressed)
        self.books_table.installEventFilter(return_filter)

        focus_filter = WidgetFocusFilter(self.books_table)
        focus_filter.focus_entered_signal.connect(self.focus_entered)
        self.books_table.installEventFilter(focus_filter)

        self.close_button.clicked.connect(self.close_button_clicked)
        self.refresh_button.clicked.connect(self.refill)

        self.tab_order_widgets = [
            self.items, self.books_table, self.lock_qv, self.dock_button,
            self.refresh_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.SelectionBehavior.SelectRows)
        self.books_table.setSelectionMode(
            QAbstractItemView.SelectionMode.SingleSelection)
        self.books_table.setProperty('highlight_current_item', 150)

        # Set up the books table columns
        self.add_columns_to_widget()

        self.books_table_header_height = self.books_table.height()
        self.books_table.cellDoubleClicked.connect(self.book_doubleclicked)
        self.books_table.currentCellChanged.connect(
            self.books_table_cell_changed)
        self.books_table.cellClicked.connect(
            self.books_table_set_search_string)
        self.books_table.cellActivated.connect(
            self.books_table_set_search_string)
        self.books_table.sortByColumn(0, Qt.SortOrder.AscendingOrder)

        # get the standard table row height. Do this here because calling
        # resizeRowsToContents can word wrap long cell contents, creating
        # double-high rows
        self.books_table.setRowCount(1)
        self.books_table.setItem(0, 0, TableItem())
        self.books_table.resizeRowsToContents()
        self.books_table_row_height = self.books_table.rowHeight(0)
        self.books_table.setRowCount(0)

        # Add the data
        self.refresh(row)

        self.slave_timers = [QTimer(self), QTimer(self), QTimer(self)]
        self.view.clicked.connect(
            partial(self.delayed_slave, func=self.slave, dex=0))
        self.view.selectionModel().currentColumnChanged.connect(
            partial(self.delayed_slave, func=self.column_slave, dex=1))
        QCoreApplication.instance().aboutToQuit.connect(self.save_state)
        self.view.model().new_bookdisplay_data.connect(
            partial(self.delayed_slave, func=self.book_was_changed, dex=2))

        self.close_button.setDefault(False)
        self.close_button_tooltip = _(
            'The Quickview shortcut ({0}) shows/hides the Quickview panel')
        self.refresh_button.setIcon(QIcon.ic('view-refresh.png'))
        self.close_button.setIcon(self.style().standardIcon(
            QStyle.StandardPixmap.SP_DialogCloseButton))
        if self.is_pane:
            self.dock_button.setText(_('Undock'))
            self.dock_button.setToolTip(
                _('Show the Quickview panel in its own floating window'))
            self.dock_button.setIcon(QIcon(I('arrow-up.png')))
            # Remove the ampersands from the buttons because shortcuts exist.
            self.lock_qv.setText(_('Lock Quickview contents'))
            self.refresh_button.setText(_('Refresh'))
            self.gui.quickview_splitter.add_quickview_dialog(self)
            self.close_button.setVisible(False)
        else:
            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)
        self.view.model().search_done.connect(self.check_for_no_items)

        # Enable the refresh button only when QV is locked
        self.refresh_button.setEnabled(False)
        self.lock_qv.stateChanged.connect(self.lock_qv_changed)

        self.view_icon = QIcon(I('view.png'))
        self.view_plugin = self.gui.iactions['View']
        self.edit_metadata_icon = QIcon(I('edit_input.png'))
        self.quickview_icon = QIcon(I('quickview.png'))
        self.select_book_icon = QIcon(I('library.png'))
        self.search_icon = QIcon(I('search.png'))
        self.books_table.setContextMenuPolicy(
            Qt.ContextMenuPolicy.CustomContextMenu)
        self.books_table.customContextMenuRequested.connect(
            self.show_context_menu)

        # Add the quickview toggle as a shortcut for the close button
        # Don't add it if it identical to the current &X shortcut because that
        # breaks &X
        if (not self.is_pane and toggle_shortcut
                and self.close_button.shortcut() != toggle_shortcut):
            toggle_sc = QShortcut(toggle_shortcut, self.close_button)
            toggle_sc.activated.connect(lambda: self.close_button_clicked())
            toggle_sc.setEnabled(True)
            self.close_button.setToolTip(
                _('Alternate shortcut: ') + toggle_shortcut.toString())

    def delayed_slave(self, current, func=None, dex=None):
        self.slave_timers[dex].stop()
        t = self.slave_timers[dex] = QTimer(self)
        t.timeout.connect(partial(func, current))
        t.setSingleShot(True)
        t.setInterval(200)
        t.start()

    def item_doubleclicked(self, item):
        tb = self.gui.stack.tb_widget
        tb.set_focus_to_find_box()
        tb.item_search.lineEdit().setText(self.current_key + ':=' +
                                          item.text())
        tb.do_find()

    def show_item_context_menu(self, point):
        item = self.items.currentItem()
        self.context_menu = QMenu(self)
        self.context_menu.addAction(self.search_icon,
                                    _('Find item in the Tag browser'),
                                    partial(self.item_doubleclicked, item))
        self.context_menu.addAction(
            self.search_icon, _('Find item in the library'),
            partial(self.do_search, follow_library_view=False))
        self.context_menu.popup(self.items.mapToGlobal(point))
        self.context_menu = QMenu(self)

    def show_context_menu(self, point):
        index = self.books_table.indexAt(point)
        row = index.row()
        column = index.column()
        item = self.books_table.item(index.row(), 0)
        if item is None:
            return False
        book_id = int(item.data(Qt.ItemDataRole.UserRole))
        book_displayed = self.book_displayed_in_library_view(book_id)
        m = self.context_menu = QMenu(self)
        a = m.addAction(self.select_book_icon,
                        _('Select this book in the library'),
                        partial(self.select_book, book_id))
        a.setEnabled(book_displayed)
        m.addAction(self.search_icon, _('Find item in the library'),
                    partial(self.do_search, follow_library_view=False))
        a = m.addAction(
            self.edit_metadata_icon, _('Edit metadata'),
            partial(self.edit_metadata, book_id, follow_library_view=False))
        a.setEnabled(book_displayed)
        a = m.addAction(self.quickview_icon, _('Quickview this cell'),
                        partial(self.quickview_item, row, column))
        a.setEnabled(
            self.is_category(self.column_order[column]) and book_displayed
            and not self.lock_qv.isChecked())
        m.addSeparator()
        m.addAction(self.view_icon, _('Open book in the E-book viewer'),
                    partial(self.view_plugin._view_calibre_books, [book_id]))
        self.context_menu.popup(self.books_table.mapToGlobal(point))
        return True

    def lock_qv_changed(self, state):
        self.refresh_button.setEnabled(state)

    def add_columns_to_widget(self):
        '''
        Get the list of columns from the preferences. Clear the current table
        and add the current column set
        '''
        self.column_order = [x[0] for x in get_qv_field_list(self.fm) if x[1]]
        self.books_table.clear()
        self.books_table.setRowCount(0)
        self.books_table.setColumnCount(len(self.column_order))
        for idx, col in enumerate(self.column_order):
            t = QTableWidgetItem(self.fm[col]['name'])
            self.books_table.setHorizontalHeaderItem(idx, t)

    def refill(self):
        '''
            Refill the table in case the columns displayed changes
        '''
        self.add_columns_to_widget()
        self.refresh(self.view.currentIndex(), ignore_lock=True)

    def set_search_text(self, txt):
        self.last_search = txt

    def focus_entered(self, obj):
        if obj == self.books_table:
            self.books_table_set_search_string(
                self.books_table.currentRow(),
                self.books_table.currentColumn())
        elif obj.currentItem():
            self.item_selected(obj.currentItem().text())

    def books_table_cell_changed(self, cur_row, cur_col, prev_row, prev_col):
        self.books_table_set_search_string(cur_row, cur_col)

    def books_table_set_search_string(self, current_row, current_col):
        '''
        Given the contents of a cell, compute a search string that will find
        that book and any others with identical contents in the cell.
        '''
        current = self.books_table.item(current_row, current_col)
        if current is None:
            return
        book_id = current.data(Qt.ItemDataRole.UserRole)

        if current is None:
            return
        col = self.column_order[current.column()]
        if col == 'title':
            self.set_search_text('title:="' +
                                 current.text().replace('"', '\\"') + '"')
        elif col == 'authors':
            authors = []
            for aut in [t.strip() for t in current.text().split('&')]:
                authors.append('authors:="' + aut.replace('"', '\\"') + '"')
            self.set_search_text(' and '.join(authors))
        elif self.fm[col]['datatype'] == 'series':
            mi = self.db.get_metadata(book_id,
                                      index_is_id=True,
                                      get_user_categories=False)
            t = mi.get(col)
            if t:
                self.set_search_text(col + ':="' + t + '"')
            else:
                self.set_search_text(None)
        else:
            if self.fm[col]['is_multiple']:
                items = [(col + ':"=' + v.strip() + '"')
                         for v in current.text().split(
                             self.fm[col]['is_multiple']['ui_to_list'])]
                self.set_search_text(' and '.join(items))
            else:
                self.set_search_text(col + ':"=' + current.text() + '"')

    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.FocusReason.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_after_dock_change.emit()

    # search button
    def do_search(self, follow_library_view=True):
        if self.no_valid_items:
            return
        if self.last_search is not None:
            try:
                self.follow_library_view = follow_library_view
                self.gui.search.set_search_string(self.last_search)
            finally:
                self.follow_library_view = True

    def book_was_changed(self, mi):
        '''
        Called when book information is changed in the library view. Make that
        book info current. This means that prev and next in edit metadata will move
        the current book and change quickview
        '''
        if self.is_closed or self.current_column is None or not self.follow_library_view:
            return
        # There is an ordering problem when libraries are changed. The library
        # view is changed, triggering a book_was_changed signal. Unfortunately
        # this happens before the library_changed actions are run, meaning we
        # still have the old database. To avoid the problem we just ignore the
        # operation if we get an exception. The "close" will come
        # eventually.
        try:
            self.refresh(self.view.model().index(self.db.row(mi.id),
                                                 self.current_column))
        except:
            pass

    # clicks on the items listWidget
    def item_selected(self, txt):
        if self.no_valid_items:
            return
        self.fill_in_books_box(str(txt))
        self.set_search_text(self.current_key + ':"=' +
                             txt.replace('"', '\\"') + '"')

    def vl_box_changed(self):
        gprefs['qv_respects_vls'] = self.apply_vls.isChecked()
        self._refresh(self.current_book_id, self.current_key)

    def refresh(self, idx, ignore_lock=False):
        '''
        Given a cell in the library view, display the information. This method
        converts the index into the lookup key
        '''
        if (not ignore_lock and self.lock_qv.isChecked()):
            return
        if not idx.isValid():
            from calibre.constants import DEBUG
            if DEBUG:
                from calibre import prints
                prints('QuickView: current index is not valid')
            return

        try:
            self.current_column = (self.view.column_map.index('authors') if
                                   (self.current_column is None
                                    and self.view.column_map[idx.column()]
                                    == 'title') else idx.column())
            key = self.view.column_map[self.current_column]
            book_id = self.view.model().id(idx.row())
            if self.current_book_id == book_id and self.current_key == key:
                return
            self._refresh(book_id, key)
        except:
            traceback.print_exc()
            self.indicate_no_items()

    def is_category(self, key):
        return key is not None and (
            self.fm[key]['is_category'] or
            (self.fm[key]['datatype'] == 'composite'
             and self.fm[key]['display'].get('make_category', False)))

    def _refresh(self, book_id, key):
        '''
        Actually fill in the left-hand panel from the information in the
        selected column of the selected book
        '''
        # Only show items for categories
        if not self.is_category(key):
            if self.current_key is None:
                self.indicate_no_items()
                return
            key = self.current_key
        label_text = _('&Item: {0} ({1})')
        if self.is_pane:
            label_text = label_text.replace('&', '')

        self.items.blockSignals(True)
        self.items.clear()
        self.books_table.setRowCount(0)

        mi = self.db.new_api.get_proxy_metadata(book_id)
        vals = mi.get(key, None)
        if self.fm[key]['datatype'] == 'composite' and self.fm[key][
                'is_multiple']:
            sep = self.fm[key]['is_multiple'].get('cache_to_list', ',')
            vals = [v.strip() for v in vals.split(sep) if v.strip()]
        try:
            # Check if we are in the GridView and there are no values for the
            # selected column. In this case switch the column to 'authors'
            # because there isn't an easy way to switch columns in GridView
            # when the QV box is empty.
            if not vals:
                is_grid_view = (
                    self.gui.current_view().alternate_views.current_view !=
                    self.gui.current_view().alternate_views.main_view)
                if is_grid_view:
                    key = 'authors'
                    vals = mi.get(key, None)
        except:
            traceback.print_exc()

        self.current_book_id = book_id
        self.current_key = key
        self.items_label.setText(label_text.format(self.fm[key]['name'], key))

        if vals:
            self.no_valid_items = False
            if self.fm[key]['datatype'] == 'rating':
                if self.fm[key]['display'].get('allow_half_stars', False):
                    vals = str(vals / 2.0)
                else:
                    vals = str(vals // 2)
            if not isinstance(vals, list):
                vals = [vals]
            vals.sort(key=sort_key)

            for v in vals:
                a = QListWidgetItem(v)
                a.setToolTip(
                    '<p>' +
                    _('Click to show only books with this item. '
                      'Double click to search for this item in the Tag browser'
                      ) + '</p>')
                self.items.addItem(a)
            self.items.setCurrentRow(0)

            self.fill_in_books_box(vals[0])
        else:
            self.indicate_no_items()
        self.items.blockSignals(False)

    def check_for_no_items(self):
        if not self.is_closed and self.view.model().count() == 0:
            self.indicate_no_items()

    def indicate_no_items(self):
        self.no_valid_items = True
        self.items.clear()
        self.add_columns_to_widget()
        self.items.addItem(QListWidgetItem(_('**No items found**')))
        self.books_label.setText(
            _('Click in a column  in the library view '
              'to see the information for that book'))

    def fill_in_books_box(self, selected_item):
        '''
        Given the selected row in the left-hand box, fill in the grid with
        the books that contain that data.
        '''
        # Do a bit of fix-up on the items so that the search works.
        if selected_item.startswith('.'):
            sv = '.' + selected_item
        else:
            sv = selected_item
        sv = self.current_key + ':"=' + sv.replace('"', r'\"') + '"'
        if self.apply_vls.isChecked():
            books = self.db.search(sv, return_matches=True, sort_results=False)
        else:
            books = self.db.new_api.search(sv)

        self.books_table.setRowCount(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)
        self.books_table.blockSignals(True)
        tt = ('<p>' + _(
            'Double click on a book to change the selection in the library view or '
            'change the column shown in the left-hand panel. '
            'Shift- or Ctrl- double click to edit the metadata of a book, '
            'which also changes the selected book.') + '</p>')
        for row, b in enumerate(books):
            for col in self.column_order:
                a = TableItem(partial(self.get_item_data, b, col))
                if col == 'title':
                    if b == self.current_book_id:
                        select_item = a
                # The data is supplied on demand when the item is displayed
                a.setData(Qt.ItemDataRole.UserRole, b)
                a.setToolTip(tt)
                self.books_table.setItem(row,
                                         self.key_to_table_widget_column(col),
                                         a)
                self.books_table.setRowHeight(row, self.books_table_row_height)
        self.books_table.blockSignals(False)
        self.books_table.setSortingEnabled(True)
        if select_item is not None:
            self.books_table.setCurrentItem(select_item)
            self.books_table.scrollToItem(
                select_item, QAbstractItemView.ScrollHint.PositionAtCenter)
        self.set_search_text(sv)

    def get_item_data(self, book_id, col):
        mi = self.db.new_api.get_proxy_metadata(book_id)
        try:
            if col == 'title':
                return (mi.title, mi.title_sort, 0)
            elif col == 'authors':
                return (' & '.join(mi.authors), mi.author_sort, 0)
            elif col == 'series':
                series = mi.format_field('series')[1]
                if series is None:
                    return ('', None, 0)
                else:
                    return (series, mi.series, mi.series_index)
            elif col == 'size':
                v = mi.get('book_size')
                if v is not None:
                    return (f'{v:n}', v, 0)
                else:
                    return ('', None, 0)
            elif self.fm[col]['datatype'] == 'series':
                v = mi.format_field(col)[1]
                return (v, mi.get(col), mi.get(col + '_index'))
            elif self.fm[col]['datatype'] == 'datetime':
                v = mi.format_field(col)[1]
                d = mi.get(col)
                if d is None:
                    d = UNDEFINED_DATE
                return (v, timestampfromdt(d), 0)
            elif self.fm[col]['datatype'] in ('float', 'int'):
                v = mi.format_field(col)[1]
                sort_val = mi.get(col)
                return (v, sort_val, 0)
            else:
                v = mi.format_field(col)[1]
                return (v, v, 0)
        except:
            traceback.print_exc()
            return (_('Something went wrong while filling in the table'), '',
                    0)

    # Deal with sizing the table columns. Done here because the numbers are not
    # 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)
        else:
            # the vertical scroll bar might not be rendered, so might not yet
            # have a width. Assume 25. Not a problem because user-changed column
            # widths will be remembered
            w = self.books_table.width(
            ) - 25 - self.books_table.verticalHeader().width()
            w //= self.books_table.columnCount()
            for c in range(0, self.books_table.columnCount()):
                self.books_table.setColumnWidth(c, w)
        self.save_state()

    def key_to_table_widget_column(self, key):
        return self.column_order.index(key)

    def return_pressed(self):
        row = self.books_table.currentRow()
        if gprefs['qv_retkey_changes_column']:
            self.select_book_and_qv(row, self.books_table.currentColumn())
        else:
            self.select_book_and_qv(
                row, self.key_to_table_widget_column(self.current_key))

    def book_not_in_view_error(self):
        from calibre.gui2 import error_dialog
        error_dialog(self,
                     _('Quickview: Book not in library view'),
                     _('The book you selected is not currently displayed in '
                       'the library view, perhaps because of a search or a '
                       'Virtual library, so Quickview cannot select it.'),
                     show=True,
                     show_copy_button=False)

    def book_displayed_in_library_view(self, book_id):
        try:
            self.db.data.index(book_id)
            return True
        except:
            return False

    def quickview_item(self, row, column):
        self.select_book_and_qv(row, column)

    def book_doubleclicked(self, row, column):
        if self.no_valid_items:
            return
        try:
            if gprefs['qv_dclick_changes_column']:
                self.quickview_item(row, column)
            else:
                self.quickview_item(
                    row, self.key_to_table_widget_column(self.current_key))
        except:
            self.book_not_in_view_error()

    def edit_metadata(self, book_id, follow_library_view=True):
        try:
            self.follow_library_view = follow_library_view
            self.view.select_rows([book_id])
            em = find_plugin('Edit Metadata')
            if em and em.actual_plugin_:
                em.actual_plugin_.edit_metadata(None)
        finally:
            self.follow_library_view = True

    def select_book(self, book_id):
        '''
        Select a book in the library view without changing the QV lists
        '''
        try:
            self.follow_library_view = False
            self.view.select_cell(self.db.data.id_to_index(book_id),
                                  self.current_column)
        finally:
            self.follow_library_view = True

    def select_book_and_qv(self, row, column):
        '''
        row and column both refer the qv table. In particular, column is not
        the logical column in the book list.
        '''
        item = self.books_table.item(row, column)
        if item is None:
            return
        book_id = int(
            self.books_table.item(row, column).data(Qt.ItemDataRole.UserRole))
        if not self.book_displayed_in_library_view(book_id):
            self.book_not_in_view_error()
            return
        key = self.column_order[column]
        if QApplication.keyboardModifiers() in (
                Qt.KeyboardModifier.ControlModifier,
                Qt.KeyboardModifier.ShiftModifier):
            self.edit_metadata(book_id)
        else:
            self.view.select_cell(self.db.data.id_to_index(book_id),
                                  self.view.column_map.index(key))

    def set_focus(self):
        self.activateWindow()
        self.books_table.setFocus()

    def column_slave(self, current):
        '''
        called when the column is changed on the booklist
        '''
        if self.follow_library_view and gprefs['qv_follows_column']:
            self.slave(current)

    def slave(self, current):
        '''
        called when a book is clicked on the library view
        '''
        if self.is_closed or not self.follow_library_view:
            return
        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
        self.books_table_column_widths = []
        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
        if not self.is_pane:
            gprefs['quickview_dialog_geometry'] = bytearray(
                self.saveGeometry())

    def _close(self):
        self.save_state()
        # clean up to prevent memory leaks
        self.db = self.view = self.gui = None
        self.is_closed = True

    def close_button_clicked(self):
        self.closed_by_button = True
        self.quickview_closed.emit()

    def reject(self):
        if not self.closed_by_button:
            self.close_button_clicked()
        else:
            self._reject()

    def _reject(self):
        if self.is_pane:
            self.gui.quickview_splitter.hide_quickview_widget()
        self.gui.library_view.setFocus(Qt.FocusReason.ActiveWindowFocusReason)
        self._close()
        QDialog.reject(self)
Example #4
0
class ConfigWidget(ConfigWidgetBase):
    def setupUi(self, x):
        self.l = l = QVBoxLayout(self)
        self.la1 = la = QLabel(
            _("Values for the tweaks are shown below. Edit them to change the behavior of calibre."
              " Your changes will only take effect <b>after a restart</b> of calibre."
              ))
        l.addWidget(la), la.setWordWrap(True)
        self.splitter = s = QSplitter(self)
        s.setChildrenCollapsible(False)
        l.addWidget(s, 10)

        self.lv = lv = QWidget(self)
        lv.l = l2 = QVBoxLayout(lv)
        l2.setContentsMargins(0, 0, 0, 0)
        self.tweaks_view = tv = TweaksView(self)
        l2.addWidget(tv)
        self.plugin_tweaks_button = b = QPushButton(self)
        b.setToolTip(
            _("Edit tweaks for any custom plugins you have installed"))
        b.setText(_("&Plugin tweaks"))
        l2.addWidget(b)
        s.addWidget(lv)

        self.lv1 = lv = QWidget(self)
        s.addWidget(lv)
        lv.g = g = QGridLayout(lv)
        g.setContentsMargins(0, 0, 0, 0)

        self.search = sb = SearchBox2(self)
        sb.sizePolicy().setHorizontalStretch(10)
        sb.setSizeAdjustPolicy(
            QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLength)
        sb.setMinimumContentsLength(10)
        g.setColumnStretch(0, 100)
        g.addWidget(self.search, 0, 0, 1, 1)
        self.next_button = b = QPushButton(self)
        b.setIcon(QIcon(I("arrow-down.png")))
        b.setText(_("&Next"))
        g.addWidget(self.next_button, 0, 1, 1, 1)
        self.previous_button = b = QPushButton(self)
        b.setIcon(QIcon(I("arrow-up.png")))
        b.setText(_("&Previous"))
        g.addWidget(self.previous_button, 0, 2, 1, 1)

        self.hb = hb = QGroupBox(self)
        hb.setTitle(_("Help"))
        hb.l = l2 = QVBoxLayout(hb)
        self.help = h = QPlainTextEdit(self)
        l2.addWidget(h)
        h.setReadOnly(True)
        g.addWidget(hb, 1, 0, 1, 3)

        self.eb = eb = QGroupBox(self)
        g.addWidget(eb, 2, 0, 1, 3)
        eb.setTitle(_("Edit tweak"))
        eb.g = ebg = QGridLayout(eb)
        self.edit_tweak = et = QPlainTextEdit(self)
        et.setMinimumWidth(400)
        et.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
        ebg.addWidget(et, 0, 0, 1, 2)
        self.restore_default_button = b = QPushButton(self)
        b.setToolTip(_("Restore this tweak to its default value"))
        b.setText(_("&Reset this tweak"))
        ebg.addWidget(b, 1, 0, 1, 1)
        self.apply_button = ab = QPushButton(self)
        ab.setToolTip(_("Apply any changes you made to this tweak"))
        ab.setText(_("&Apply changes to this tweak"))
        ebg.addWidget(ab, 1, 1, 1, 1)

    def genesis(self, gui):
        self.gui = gui
        self.delegate = Delegate(self.tweaks_view)
        self.tweaks_view.setItemDelegate(self.delegate)
        self.tweaks_view.current_changed.connect(self.current_changed)
        self.view = self.tweaks_view
        self.highlighter = PythonHighlighter(self.edit_tweak.document())
        self.restore_default_button.clicked.connect(self.restore_to_default)
        self.apply_button.clicked.connect(self.apply_tweak)
        self.plugin_tweaks_button.clicked.connect(self.plugin_tweaks)
        self.splitter.setStretchFactor(0, 1)
        self.splitter.setStretchFactor(1, 100)
        self.next_button.clicked.connect(self.find_next)
        self.previous_button.clicked.connect(self.find_previous)
        self.search.initialize('tweaks_search_history',
                               help_text=_('Search for tweak'))
        self.search.search.connect(self.find)
        self.view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.view.customContextMenuRequested.connect(self.show_context_menu)
        self.copy_icon = QIcon(I('edit-copy.png'))

    def show_context_menu(self, point):
        idx = self.tweaks_view.currentIndex()
        if not idx.isValid():
            return True
        tweak = self.tweaks.data(idx, Qt.ItemDataRole.UserRole)
        self.context_menu = QMenu(self)
        self.context_menu.addAction(
            self.copy_icon, _('Copy to clipboard'),
            partial(self.copy_item_to_clipboard,
                    val="%s (%s: %s)" %
                    (tweak.name, _('ID'), tweak.var_names[0])))
        self.context_menu.popup(self.mapToGlobal(point))
        return True

    def copy_item_to_clipboard(self, val):
        cb = QApplication.clipboard()
        cb.clear()
        cb.setText(val)

    def plugin_tweaks(self):
        raw = self.tweaks.plugin_tweaks_string
        d = PluginTweaks(raw, self)
        if d.exec_() == QDialog.DialogCode.Accepted:
            g, l = {}, {}
            try:
                exec(unicode_type(d.edit.toPlainText()), g, l)
            except:
                import traceback
                return error_dialog(
                    self,
                    _('Failed'),
                    _('There was a syntax error in your tweak. Click '
                      'the "Show details" button for details.'),
                    show=True,
                    det_msg=traceback.format_exc())
            self.tweaks.set_plugin_tweaks(l)
            self.changed()

    def current_changed(self, current, previous):
        self.tweaks_view.scrollTo(current)
        tweak = self.tweaks.data(current, Qt.ItemDataRole.UserRole)
        self.help.setPlainText(tweak.doc)
        self.edit_tweak.setPlainText(tweak.edit_text)

    def changed(self, *args):
        self.changed_signal.emit()

    def initialize(self):
        self.tweaks = self._model = Tweaks()
        self.tweaks_view.setModel(self.tweaks)

    def restore_to_default(self, *args):
        idx = self.tweaks_view.currentIndex()
        if idx.isValid():
            self.tweaks.restore_to_default(idx)
            tweak = self.tweaks.data(idx, Qt.ItemDataRole.UserRole)
            self.edit_tweak.setPlainText(tweak.edit_text)
            self.changed()

    def restore_defaults(self):
        ConfigWidgetBase.restore_defaults(self)
        self.tweaks.restore_to_defaults()
        self.changed()

    def apply_tweak(self):
        idx = self.tweaks_view.currentIndex()
        if idx.isValid():
            l, g = {}, {}
            try:
                exec(unicode_type(self.edit_tweak.toPlainText()), g, l)
            except:
                import traceback
                error_dialog(self.gui,
                             _('Failed'),
                             _('There was a syntax error in your tweak. Click '
                               'the "Show details" button for details.'),
                             det_msg=traceback.format_exc(),
                             show=True)
                return
            self.tweaks.update_tweak(idx, l)
            self.changed()

    def commit(self):
        raw = self.tweaks.to_string()
        if not isinstance(raw, bytes):
            raw = raw.encode('utf-8')
        try:
            custom_tweaks = exec_tweaks(raw)
        except:
            import traceback
            error_dialog(
                self,
                _('Invalid tweaks'),
                _('The tweaks you entered are invalid, try resetting the'
                  ' tweaks to default and changing them one by one until'
                  ' you find the invalid setting.'),
                det_msg=traceback.format_exc(),
                show=True)
            raise AbortCommit('abort')
        write_custom_tweaks(custom_tweaks)
        ConfigWidgetBase.commit(self)
        return True

    def find(self, query):
        if not query:
            return
        try:
            idx = self._model.find(query)
        except ParseException:
            self.search.search_done(False)
            return
        self.search.search_done(True)
        if not idx.isValid():
            info_dialog(self,
                        _('No matches'),
                        _('Could not find any shortcuts matching %s') % query,
                        show=True,
                        show_copy_button=False)
            return
        self.highlight_index(idx)

    def highlight_index(self, idx):
        if not idx.isValid():
            return
        self.view.scrollTo(idx)
        self.view.selectionModel().select(
            idx, QItemSelectionModel.SelectionFlag.ClearAndSelect)
        self.view.setCurrentIndex(idx)

    def find_next(self, *args):
        idx = self.view.currentIndex()
        if not idx.isValid():
            idx = self._model.index(0)
        idx = self._model.find_next(idx,
                                    unicode_type(self.search.currentText()))
        self.highlight_index(idx)

    def find_previous(self, *args):
        idx = self.view.currentIndex()
        if not idx.isValid():
            idx = self._model.index(0)
        idx = self._model.find_next(idx,
                                    unicode_type(self.search.currentText()),
                                    backwards=True)
        self.highlight_index(idx)