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())
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))
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)
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)