Enhancement 2120768: up/down arrows for setting metadata edit order

I changed all the row move buttons in Preferences to use the Shift (move 5), Ctrl (move 10), and Ctrl-Shirt (move to end). This involved a fair bit of refactoring. It is possible that I missed the buttons somewhere.
This commit is contained in:
Charles Haley 2025-08-17 16:30:23 +01:00
parent 4d0bee8870
commit bdaa04430f
10 changed files with 100 additions and 103 deletions

View File

@ -5,6 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from functools import partial
import textwrap
from qt.core import (
@ -480,8 +481,8 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False,
class ListViewWithMoveByKeyPress(QListView):
def set_movement_functions(self, up_function, down_function):
self.up_function = up_function
self.down_function = down_function
self.up_function = partial(up_function, use_kbd_modifiers=False)
self.down_function = partial(down_function, use_kbd_modifiers=False)
def event(self, event):
if (event.type() == QEvent.KeyPress and
@ -497,8 +498,8 @@ class ListViewWithMoveByKeyPress(QListView):
class ListWidgetWithMoveByKeyPress(QListWidget):
def set_movement_functions(self, up_function, down_function):
self.up_function = up_function
self.down_function = down_function
self.up_function = partial(up_function, use_kbd_modifiers=False)
self.down_function = partial(down_function, use_kbd_modifiers=False)
def event(self, event):
if (event.type() == QEvent.KeyPress and
@ -514,8 +515,8 @@ class ListWidgetWithMoveByKeyPress(QListWidget):
class TableWidgetWithMoveByKeyPress(QTableWidget):
def set_movement_functions(self, up_function, down_function):
self.up_function = up_function
self.down_function = down_function
self.up_function = partial(up_function, use_kbd_modifiers=False)
self.down_function = partial(down_function, use_kbd_modifiers=False)
def event(self, event):
if (event.type() == QEvent.KeyPress and
@ -528,6 +529,19 @@ class TableWidgetWithMoveByKeyPress(QTableWidget):
return QTableWidget.event(self, event)
def get_move_count(row_count):
mods = QApplication.keyboardModifiers()
if mods == Qt.KeyboardModifier.ShiftModifier:
count = 5
elif mods == Qt.KeyboardModifier.ControlModifier:
count = 10
elif mods == (Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier):
count = row_count
else:
count = 1
return count
# Testing {{{
def test_widget(category, name, gui=None, callback=None):

View File

@ -16,7 +16,7 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported
from calibre.gui2 import config, dynamic, gprefs, info_dialog
from calibre.gui2.actions.choose_library import get_change_library_action_plugin
from calibre.gui2.preferences import ConfigWidgetBase, Setting, test_widget
from calibre.gui2.preferences import ConfigWidgetBase, get_move_count, Setting, test_widget
from calibre.gui2.preferences.behavior_ui import Ui_Form
from calibre.utils.config import prefs
from calibre.utils.icu import sort_key
@ -67,9 +67,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('virtual_lib_on_startup', db.prefs, choices=choices)
self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs)
self.input_up_button.clicked.connect(self.up_input)
self.input_down_button.clicked.connect(self.down_input)
self.opt_input_order.set_movement_functions(self.up_input, self.down_input)
self.input_up_button.clicked.connect(partial(self.up_input, use_kbd_modifiers=True))
self.input_down_button.clicked.connect(partial(self.down_input, use_kbd_modifiers=True))
self.opt_input_order.set_movement_functions(partial(self.up_input, use_kbd_modifiers=False),
partial(self.down_input, use_kbd_modifiers=False))
self.opt_input_order.dropEvent = partial(input_order_drop_event, self)
for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'):
signal = getattr(self.opt_internally_viewed_formats, 'item'+signal)
@ -158,19 +159,23 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
item.setData(Qt.ItemDataRole.UserRole, (format))
item.setFlags(Qt.ItemFlag.ItemIsEnabled|Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsDragEnabled)
def up_input(self, *args):
idx = self.opt_input_order.currentRow()
if idx > 0:
self.opt_input_order.insertItem(idx-1, self.opt_input_order.takeItem(idx))
self.opt_input_order.setCurrentRow(idx-1)
self.changed_signal.emit()
def up_input(self, use_kbd_modifiers, *args):
count = get_move_count(self.opt_input_order.count()) if use_kbd_modifiers else 1
for _ in range(count):
idx = self.opt_input_order.currentRow()
if idx > 0:
self.opt_input_order.insertItem(idx-1, self.opt_input_order.takeItem(idx))
self.opt_input_order.setCurrentRow(idx-1)
self.changed_signal.emit()
def down_input(self, *args):
idx = self.opt_input_order.currentRow()
if idx < self.opt_input_order.count()-1:
self.opt_input_order.insertItem(idx+1, self.opt_input_order.takeItem(idx))
self.opt_input_order.setCurrentRow(idx+1)
self.changed_signal.emit()
def down_input(self, use_kbd_modifiers, *args):
count = get_move_count(self.opt_input_order.count()) if use_kbd_modifiers else 1
for _ in range(count):
idx = self.opt_input_order.currentRow()
if idx < self.opt_input_order.count()-1:
self.opt_input_order.insertItem(idx+1, self.opt_input_order.takeItem(idx))
self.opt_input_order.setCurrentRow(idx+1)
self.changed_signal.emit()
# }}}

View File

@ -51,7 +51,7 @@ from calibre.constants import config_dir
from calibre.gui2 import choose_files, choose_save_file, error_dialog, gprefs, info_dialog, open_local_file, pixmap_to_data, question_dialog
from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.metadata.single_download import RichTextDelegate
from calibre.gui2.preferences import ListViewWithMoveByKeyPress
from calibre.gui2.preferences import get_move_count, ListViewWithMoveByKeyPress
from calibre.gui2.widgets2 import ColorButton, FlowLayout, Separator
from calibre.library.coloring import Rule, color_row_key, conditionable_columns, displayable_columns, rule_from_template
from calibre.utils.icu import lower, sort_key
@ -1275,27 +1275,29 @@ class EditRules(QWidget): # {{{
self.model.remove_rule(row)
self.changed.emit()
def move_rows(self, moving_up=True):
sm = self.rules_view.selectionModel()
rows = sorted(sm.selectedRows(), reverse=not moving_up)
if rows:
if rows[0].row() == (0 if moving_up else self.model.rowCount() - 1):
return
sm.clear()
indices_to_select = []
for idx in rows:
if idx.isValid():
idx = self.model.move(idx, -1 if moving_up else 1)
if idx is not None:
indices_to_select.append(idx)
if indices_to_select:
new_selections = QItemSelection()
for idx in indices_to_select:
new_selections.merge(QItemSelection(idx, idx),
QItemSelectionModel.SelectionFlag.Select)
sm.select(new_selections, QItemSelectionModel.SelectionFlag.Select)
self.rules_view.scrollTo(indices_to_select[0])
self.changed.emit()
def move_rows(self, moving_up=True, use_kbd_modifiers=True):
count = get_move_count(self.rules_view.model().rowCount()) if use_kbd_modifiers else 1
for _ in range(count):
sm = self.rules_view.selectionModel()
rows = sorted(sm.selectedRows(), reverse=not moving_up)
if rows:
if rows[0].row() == (0 if moving_up else self.model.rowCount() - 1):
return
sm.clear()
indices_to_select = []
for idx in rows:
if idx.isValid():
idx = self.model.move(idx, -1 if moving_up else 1)
if idx is not None:
indices_to_select.append(idx)
if indices_to_select:
new_selections = QItemSelection()
for idx in indices_to_select:
new_selections.merge(QItemSelection(idx, idx),
QItemSelectionModel.SelectionFlag.Select)
sm.select(new_selections, QItemSelectionModel.SelectionFlag.Select)
self.rules_view.scrollTo(indices_to_select[0])
self.changed.emit()
def clear(self):
self.model.clear()

View File

@ -7,12 +7,13 @@ __docformat__ = 'restructuredtext en'
import copy
import sys
from functools import partial
from contextlib import suppress
from qt.core import QAbstractItemView, QApplication, QIcon, Qt, QTableWidgetItem
from qt.core import QAbstractItemView, QIcon, Qt, QTableWidgetItem
from calibre.gui2 import Application, error_dialog, gprefs, question_dialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences import ConfigWidgetBase, get_move_count, test_widget
from calibre.gui2.preferences.columns_ui import Ui_Form
from calibre.gui2.preferences.create_custom_column import CreateCustomColumn
@ -42,8 +43,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
default=0) + 1
self.created_count = self.initial_created_count
self.column_up.clicked.connect(self.up_column)
self.column_down.clicked.connect(self.down_column)
self.column_up.clicked.connect(partial(self.up_column, use_kbd_modifiers=True))
self.column_down.clicked.connect(partial(self.down_column, use_kbd_modifiers=True))
self.opt_columns.setSelectionMode(QAbstractItemView.SingleSelection)
self.opt_columns.set_movement_functions(self.up_column, self.down_column)
self.del_custcol_button.clicked.connect(self.del_custcol)
@ -278,20 +279,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
key = self.opt_columns.item(row, self.KEY_COLUMN).text()
self.setup_row(row, key, row, force_checked_to=checked)
def get_move_count(self):
mods = QApplication.keyboardModifiers()
if mods == Qt.KeyboardModifier.ShiftModifier:
count = 5
elif mods == Qt.KeyboardModifier.ControlModifier:
count = 10
elif mods == (Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier):
count = self.opt_columns.rowCount()
else:
count = 1
return count
def up_column(self):
count = self.get_move_count()
def up_column(self, use_kbd_modifiers):
count = get_move_count(self.opt_columns.rowCount()) if use_kbd_modifiers else 1
for _ in range(count):
row = self.opt_columns.currentRow()
if row > 0:
@ -307,8 +296,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit()
self.opt_columns.setSortingEnabled(True)
def down_column(self):
count = self.get_move_count()
def down_column(self, use_kbd_modifiers):
count = get_move_count(self.opt_columns.rowCount()) if use_kbd_modifiers else 1
for _ in range(count):
row = self.opt_columns.currentRow()
if row < self.opt_columns.rowCount()-1:

View File

@ -29,7 +29,7 @@ from calibre.ebooks.metadata.search_internet import qquote
from calibre.gui2 import choose_files, choose_save_file, error_dialog
from calibre.gui2.book_details import get_field_list
from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.preferences import LazyConfigWidgetBase
from calibre.gui2.preferences import get_move_count, LazyConfigWidgetBase
from calibre.gui2.preferences.coloring import EditRules
from calibre.gui2.ui import get_gui
from calibre.utils.formatter import EvalFormatter
@ -204,6 +204,11 @@ class DisplayedFields(QAbstractListModel):
self.db.new_api.set_pref(self.pref_name, self.fields)
def move(self, idx, delta):
row = idx.row()
if delta > 0:
delta = delta if row + delta < self.rowCount() else self.rowCount() - row - 1
else:
delta = -row if row + delta < 0 else delta
row = idx.row() + delta
if row >= 0 and row < len(self.fields):
t = self.fields[row]
@ -294,20 +299,22 @@ def reset_layout(in_widget, model=None):
in_widget.changed_signal.emit()
def move_field_up(widget, model):
def move_field_up(widget, model, *args, use_kbd_modifiers=True):
count = get_move_count(model.rowCount()) if use_kbd_modifiers else 1
idx = widget.currentIndex()
if idx.isValid():
idx = model.move(idx, -1)
idx = model.move(idx, -count)
if idx is not None:
sm = widget.selectionModel()
sm.select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect)
widget.setCurrentIndex(idx)
def move_field_down(widget, model):
def move_field_down(widget, model, *args, use_kbd_modifiers=True):
count = get_move_count(model.rowCount()) if use_kbd_modifiers else 1
idx = widget.currentIndex()
if idx.isValid():
idx = model.move(idx, 1)
idx = model.move(idx, count)
if idx is not None:
sm = widget.selectionModel()
sm.select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect)

View File

@ -208,8 +208,8 @@ class BookDetailsTab(LazyConfigWidgetBase, Ui_Form):
self.field_display_order.setModel(self.display_model)
mu = partial(move_field_up, self.field_display_order, self.display_model)
md = partial(move_field_down, self.field_display_order, self.display_model)
self.df_up_button.clicked.connect(mu)
self.df_down_button.clicked.connect(md)
self.df_up_button.clicked.connect(partial(mu, use_kbd_modifiers=True))
self.df_down_button.clicked.connect(partial(md, use_kbd_modifiers=True))
self.field_display_order.set_movement_functions(mu, md)
self.opt_book_details_css.textChanged.connect(self.changed_signal)

View File

@ -57,8 +57,8 @@ class EditMetadataTab(LazyConfigWidgetBase, Ui_Form):
mu = partial(move_field_up, self.em_display_order, self.em_display_model)
md = partial(move_field_down, self.em_display_order, self.em_display_model)
self.em_display_order.set_movement_functions(mu, md)
self.em_up_button.clicked.connect(mu)
self.em_down_button.clicked.connect(md)
self.em_up_button.clicked.connect(partial(mu, use_kbd_modifiers=True))
self.em_down_button.clicked.connect(partial(md, use_kbd_modifiers=True))
self.em_export_layout_button.clicked.connect(partial(export_layout, self, model=self.em_display_model))
self.em_import_layout_button.clicked.connect(partial(import_layout, self, model=self.em_display_model))
self.em_reset_layout_button.clicked.connect(partial(reset_layout, model=self.em_display_model))

View File

@ -50,8 +50,8 @@ class QuickviewTab(LazyConfigWidgetBase, Ui_Form):
mu = partial(move_field_up, self.qv_display_order, self.qv_display_model)
md = partial(move_field_down, self.qv_display_order, self.qv_display_model)
self.qv_display_order.set_movement_functions(mu, md)
self.qv_up_button.clicked.connect(mu)
self.qv_down_button.clicked.connect(md)
self.qv_up_button.clicked.connect(partial(mu, use_kbd_modifiers=True))
self.qv_down_button.clicked.connect(partial(md, use_kbd_modifiers=True))
def lazy_initialize(self):
self.qv_display_model.initialize()

View File

@ -71,37 +71,17 @@ class TbDisplayTab(LazyConfigWidgetBase, Ui_Form):
self.tb_reset_layout_button.clicked.connect(partial(reset_layout, self, model=self.tb_display_model))
self.tb_export_layout_button.clicked.connect(partial(export_layout, self, model=self.tb_display_model))
self.tb_import_layout_button.clicked.connect(partial(import_layout, self, model=self.tb_display_model))
self.tb_up_button.clicked.connect(self.tb_up_button_clicked)
self.tb_down_button.clicked.connect(self.tb_down_button_clicked)
self.tb_display_order.set_movement_functions(self.tb_up_button_clicked, self.tb_down_button_clicked)
mu = partial(move_field_up, self.tb_display_order, self.tb_display_model)
md = partial(move_field_down, self.tb_display_order, self.tb_display_model)
self.tb_up_button.clicked.connect(partial(mu, use_kbd_modifiers=True))
self.tb_down_button.clicked.connect(partial(md, use_kbd_modifiers=True))
self.tb_display_order.set_movement_functions(mu, md)
def lazy_initialize(self):
self.tb_display_model.initialize()
self.tb_focus_label.setVisible(self.opt_tag_browser_allow_keyboard_focus.isChecked())
def tb_down_button_clicked(self):
idx = self.tb_display_order.currentIndex()
if idx.isValid():
row = idx.row()
model = self.tb_display_model
fields = model.fields
key = fields[row][0]
if not is_standard_category(key):
return
if row < len(fields) and is_standard_category(fields[row+1][0]):
move_field_down(self.tb_display_order, model)
def tb_up_button_clicked(self):
idx = self.tb_display_order.currentIndex()
if idx.isValid():
row = idx.row()
model = self.tb_display_model
fields = model.fields
key = fields[row][0]
if not is_standard_category(key):
return
move_field_up(self.tb_display_order, model)
def restore_defaults(self):
LazyConfigWidgetBase.restore_defaults(self)
self.tb_display_model.restore_defaults()

View File

@ -100,7 +100,7 @@ class TbHierarchyTab(LazyConfigWidgetBase, Ui_Form):
self.tb_search_order.addItem(item)
node = v
def move_tb_search_up(self):
def move_tb_search_up(self, *args, **kwargs):
idx = self.tb_search_order.currentRow()
if idx <= 0:
return
@ -109,7 +109,7 @@ class TbHierarchyTab(LazyConfigWidgetBase, Ui_Form):
self.tb_search_order.setCurrentRow(idx-1)
self.changed_signal.emit()
def move_tb_search_down(self):
def move_tb_search_down(self, *args, **kwargs):
idx = self.tb_search_order.currentRow()
if idx < 0 or idx == 3:
return