Add dialog for changing keyboard settings

This commit is contained in:
Rebecca Breu 2024-01-01 20:03:18 +01:00
parent 1eab4712d3
commit fb3bc234c7
17 changed files with 1275 additions and 373 deletions

View file

@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
python-version: ['3.9', '3.11']
pyqt-version: ['6.4.0', '6.6.1']
pyqt-version: ['6.5.0', '6.6.1']
steps:
- uses: actions/checkout@v4
- name: Set up Python

View file

@ -9,6 +9,10 @@ Added
* The opacity of images can be changed (Images -> Change Opacity).
* Images can be set to display as grayscale (Images -> Grayscale).
* The scene can now also be exported as SVG
* Keyboard shortcuts can now be edited from within BeeRef (Settings ->
Keyboard Shortcuts). The KeyboardSettings.ini file will now only
store values which are changed from the default, since it's no longer
needed as a reference.
Fixed

View file

@ -13,308 +13,409 @@
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
actions = [
{
from collections import OrderedDict
from functools import cached_property
import logging
from PyQt6 import QtGui
from beeref.actions.menu_structure import menu_structure
from beeref.config import KeyboardSettings, settings_events
logger = logging.getLogger(__name__)
class Action(dict):
def __init__(self, *args, **kwargs):
self.qaction = None
self.kb_settings = KeyboardSettings()
super().__init__(*args, **kwargs)
settings_events.restore_keyboard_defaults.connect(
self.on_restore_defaults)
def __eq__(self, other):
return self['id'] == other['id']
def on_restore_defaults(self):
if self.qaction:
self.qaction.setShortcuts(self.get_shortcuts())
@cached_property
def menu_path(self):
path = []
def _get_path(menu_item):
if isinstance(menu_item['items'], list):
# This is a normal menu
for item in menu_item['items']:
if item == self['id']:
path.append(menu_item['menu'])
return True
if isinstance(item, dict):
# This is a submenu
if _get_path(item):
path.append(menu_item['menu'])
return True
elif menu_item['items'] == self.get('menu_id'):
# This is a dynamic submenu (e.g. Recent Files)
path.append(menu_item['menu'])
return True
for menu_item in menu_structure:
_get_path(menu_item)
return path[::-1]
def get_shortcuts(self):
return self.kb_settings.get_shortcuts(
'Actions', self['id'], self.get('shortcuts'))
def set_shortcuts(self, value):
logger.debug(f'Setting shortcut "{self["id"]}" to: {value}')
self.kb_settings.set_shortcuts(
'Actions', self['id'], value, self.get('shortcuts'))
if self.qaction:
self.qaction.setShortcuts(value)
def get_qkeysequence(self, index):
"""Current shortcuts as QKeySequence"""
try:
return QtGui.QKeySequence(self.get_shortcuts()[index])
except IndexError:
return QtGui.QKeySequence()
def shortcuts_changed(self):
return self.get_shortcuts() != self.get('shortcuts', [])
def get_default_shortcut(self, index):
try:
return self.get('shortcuts', [])[index]
except IndexError:
return None
class ActionList(OrderedDict):
def __init__(self, actions):
super().__init__()
for action in actions:
self[action['id']] = action
def __getitem__(self, key):
if isinstance(key, int):
key = list(self.keys())[key]
return super().__getitem__(key)
actions = ActionList([
Action({
'id': 'open',
'text': '&Open',
'shortcuts': ['Ctrl+O'],
'callback': 'on_action_open',
},
{
}),
Action({
'id': 'save',
'text': '&Save',
'shortcuts': ['Ctrl+S'],
'callback': 'on_action_save',
'group': 'active_when_items_in_scene',
},
{
}),
Action({
'id': 'save_as',
'text': 'Save &As...',
'shortcuts': ['Ctrl+Shift+S'],
'callback': 'on_action_save_as',
'group': 'active_when_items_in_scene',
},
{
}),
Action({
'id': 'export_scene',
'text': 'E&xport Scene...',
'shortcuts': ['Ctrl+Shift+E'],
'callback': 'on_action_export_scene',
'group': 'active_when_items_in_scene',
},
{
}),
Action({
'id': 'quit',
'text': '&Quit',
'shortcuts': ['Ctrl+Q'],
'callback': 'on_action_quit',
},
{
}),
Action({
'id': 'insert_images',
'text': '&Images...',
'shortcuts': ['Ctrl+I'],
'callback': 'on_action_insert_images',
},
{
}),
Action({
'id': 'insert_text',
'text': '&Text',
'shortcuts': ['Ctrl+T'],
'callback': 'on_action_insert_text',
},
{
}),
Action({
'id': 'undo',
'text': '&Undo',
'shortcuts': ['Ctrl+Z'],
'callback': 'on_action_undo',
'group': 'active_when_can_undo',
},
{
}),
Action({
'id': 'redo',
'text': '&Redo',
'shortcuts': ['Ctrl+Shift+Z'],
'callback': 'on_action_redo',
'group': 'active_when_can_redo',
},
{
}),
Action({
'id': 'copy',
'text': '&Copy',
'shortcuts': ['Ctrl+C'],
'callback': 'on_action_copy',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'cut',
'text': 'Cu&t',
'shortcuts': ['Ctrl+X'],
'callback': 'on_action_cut',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'paste',
'text': '&Paste',
'shortcuts': ['Ctrl+V'],
'callback': 'on_action_paste',
},
{
}),
Action({
'id': 'delete',
'text': '&Delete',
'shortcuts': ['Del'],
'callback': 'on_action_delete_items',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'raise_to_top',
'text': '&Raise to Top',
'shortcuts': ['PgUp'],
'callback': 'on_action_raise_to_top',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'lower_to_bottom',
'text': 'Lower to Bottom',
'shortcuts': ['PgDown'],
'callback': 'on_action_lower_to_bottom',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'normalize_height',
'text': '&Height',
'shortcuts': ['Shift+H'],
'callback': 'on_action_normalize_height',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'normalize_width',
'text': '&Width',
'shortcuts': ['Shift+W'],
'callback': 'on_action_normalize_width',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'normalize_size',
'text': '&Size',
'shortcuts': ['Shift+S'],
'callback': 'on_action_normalize_size',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'arrange_optimal',
'text': '&Optimal',
'shortcuts': ['Shift+O'],
'callback': 'on_action_arrange_optimal',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'arrange_horizontal',
'text': '&Horizontal',
'callback': 'on_action_arrange_horizontal',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'arrange_vertical',
'text': '&Vertical',
'callback': 'on_action_arrange_vertical',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'change_opacity',
'text': 'Change &Opacity...',
'callback': 'on_action_change_opacity',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'grayscale',
'text': '&Grayscale',
'shortcuts': ['G'],
'checkable': True,
'callback': 'on_action_grayscale',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'crop',
'text': '&Crop',
'shortcuts': ['Shift+C'],
'callback': 'on_action_crop',
'group': 'active_when_single_image',
},
{
}),
Action({
'id': 'flip_horizontally',
'text': 'Flip &Horizontally',
'shortcuts': ['H'],
'callback': 'on_action_flip_horizontally',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'flip_vertically',
'text': 'Flip &Vertically',
'shortcuts': ['V'],
'callback': 'on_action_flip_vertically',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'new_scene',
'text': '&New Scene',
'shortcuts': ['Ctrl+N'],
'callback': 'clear_scene',
},
{
}),
Action({
'id': 'fit_scene',
'text': '&Fit Scene',
'shortcuts': ['1'],
'callback': 'on_action_fit_scene',
},
{
}),
Action({
'id': 'fit_selection',
'text': 'Fit &Selection',
'shortcuts': ['2'],
'callback': 'on_action_fit_selection',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'reset_scale',
'text': 'Reset &Scale',
'callback': 'on_action_reset_scale',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'reset_rotation',
'text': 'Reset &Rotation',
'callback': 'on_action_reset_rotation',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'reset_flip',
'text': 'Reset &Flip',
'callback': 'on_action_reset_flip',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'reset_crop',
'text': 'Reset Cro&p',
'callback': 'on_action_reset_crop',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'reset_transforms',
'text': 'Reset &All',
'shortcuts': ['R'],
'callback': 'on_action_reset_transforms',
'group': 'active_when_selection',
},
{
}),
Action({
'id': 'select_all',
'text': '&Select All',
'shortcuts': ['Ctrl+A'],
'callback': 'on_action_select_all',
},
{
}),
Action({
'id': 'deselect_all',
'text': 'Deselect &All',
'shortcuts': ['Ctrl+Shift+A'],
'callback': 'on_action_deselect_all',
},
{
}),
Action({
'id': 'help',
'text': '&Help',
'shortcuts': ['F1', 'Ctrl+H'],
'callback': 'on_action_help',
},
{
}),
Action({
'id': 'about',
'text': '&About',
'callback': 'on_action_about',
},
{
}),
Action({
'id': 'debuglog',
'text': 'Show &Debug Log',
'callback': 'on_action_debuglog',
},
{
}),
Action({
'id': 'show_scrollbars',
'text': 'Show &Scrollbars',
'checkable': True,
'settings': 'View/show_scrollbars',
'callback': 'on_action_show_scrollbars',
},
{
}),
Action({
'id': 'show_menubar',
'text': 'Show &Menu Bar',
'checkable': True,
'settings': 'View/show_menubar',
'callback': 'on_action_show_menubar',
},
{
}),
Action({
'id': 'show_titlebar',
'text': 'Show &Title Bar',
'checkable': True,
'checked': True,
'callback': 'on_action_show_titlebar',
},
{
}),
Action({
'id': 'move_window',
'text': 'Move &Window',
'shortcuts': ['Ctrl+M'],
'callback': 'on_action_move_window',
},
{
}),
Action({
'id': 'fullscreen',
'text': '&Fullscreen',
'shortcuts': ['F11'],
'checkable': True,
'callback': 'on_action_fullscreen',
},
{
}),
Action({
'id': 'always_on_top',
'text': '&Always On Top',
'checkable': True,
'callback': 'on_action_always_on_top',
},
{
}),
Action({
'id': 'settings',
'text': '&Settings',
'callback': 'on_action_settings',
},
{
}),
Action({
'id': 'keyboard_settings',
'text': '&Keyboard Shortcuts',
'callback': 'on_action_keyboard_settings',
}),
Action({
'id': 'open_settings_dir',
'text': 'Open Settings Folder',
'text': '&Open Settings Folder',
'callback': 'on_action_open_settings_dir',
},
]
}),
])

View file

@ -114,6 +114,7 @@ menu_structure = [
'menu': '&Settings',
'items': [
'settings',
'keyboard_settings',
'open_settings_dir',
],
},

View file

@ -19,11 +19,9 @@ import os.path
from PyQt6 import QtGui, QtWidgets
from .actions import actions
from .actions import Action, actions
from .menu_structure import menu_structure, MENU_SEPARATOR
from beeref.config import KeyboardSettings
class ActionsMixin:
@ -35,11 +33,10 @@ class ActionsMixin:
"""Creates a new menu or rebuilds the given menu."""
self.context_menu = QtWidgets.QMenu(self)
self.toplevel_menus = []
self.bee_actions = {}
self.bee_actiongroups = defaultdict(list)
self._post_create_functions = []
self._create_actions()
self._create_menu(self.bee_actions, self.context_menu, menu_structure)
self._create_menu(self.context_menu, menu_structure)
for func, arg in self._post_create_functions:
func(arg)
del self._post_create_functions
@ -71,10 +68,9 @@ class ActionsMixin:
partial(self._store_checkable_setting, settings_key))
def _create_actions(self):
for action in actions:
for action in actions.values():
qaction = QtGui.QAction(action['text'], self)
shortcuts = KeyboardSettings().get_shortcuts(
'Actions', action['id'], action.get('shortcuts'))
shortcuts = action.get_shortcuts()
if shortcuts:
qaction.setShortcuts(shortcuts)
if action.get('checkable', False):
@ -83,25 +79,25 @@ class ActionsMixin:
qaction.triggered.connect(getattr(self, action['callback']))
self.addAction(qaction)
qaction.setEnabled(action.get('enabled', True))
self.bee_actions[action['id']] = qaction
if 'group' in action:
self.bee_actiongroups[action['group']].append(qaction)
qaction.setEnabled(False)
action.qaction = qaction
def _create_menu(self, actions, menu, items):
def _create_menu(self, menu, items):
if isinstance(items, str):
getattr(self, items)(menu)
return menu
for item in items:
if isinstance(item, str):
menu.addAction(actions[item])
menu.addAction(actions[item].qaction)
if item == MENU_SEPARATOR:
menu.addSeparator()
if isinstance(item, dict):
submenu = menu.addMenu(item['menu'])
if menu == self.context_menu:
self.toplevel_menus.append(submenu)
self._create_menu(actions, submenu, item['items'])
self._create_menu(submenu, item['items'])
return menu
@ -112,31 +108,31 @@ class ActionsMixin:
files = self.settings.get_recent_files(existing_only=True)
items = []
i = -1
for i, filename in enumerate(files):
qaction = QtGui.QAction(os.path.basename(filename), self)
for i in range(10):
action_id = f'recent_files_{i}'
key = 0 if i == 9 else i + 1
if key < 10:
shortcuts = KeyboardSettings().get_shortcuts(
'Actions', action_id, [f'Ctrl+{key}'])
qaction.setShortcuts(shortcuts)
qaction.triggered.connect(partial(self.open_from_file, filename))
self.addAction(qaction)
self._recent_files_submenu.addAction(qaction)
self.bee_actions[action_id] = qaction
items.append(action_id)
action = Action({'id': action_id,
'menu_id': '_build_recent_files',
'text': f'File {i + 1}',
'shortcuts': [f'Ctrl+{key}']})
actions[action_id] = action
# Set shortcuts in settings file for remaining slots:
for j in range(i + 1, 10):
key = 0 if j == 9 else j + 1
KeyboardSettings().get_shortcuts(
'Actions', f'recent_files_{j}', [f'Ctrl+{key}'])
if i < len(files):
filename = files[i]
qaction = QtGui.QAction(os.path.basename(filename), self)
qaction.setShortcuts(action.get_shortcuts())
qaction.triggered.connect(
partial(self.open_from_file, filename))
self.addAction(qaction)
action.qaction = qaction
self._recent_files_submenu.addAction(qaction)
items.append(action_id)
def _clear_recent_files(self):
for action in self._recent_files_submenu.actions():
self.removeAction(action)
self._recent_files_submenu.clear()
for key in list(self.bee_actions.keys()):
for key in list(actions.keys()):
if key.startswith('recent_files_'):
self.bee_actions.pop(key)
actions[key].qaction = None

View file

@ -95,6 +95,7 @@ class CommandlineArgs:
class BeeSettingsEvents(QtCore.QObject):
restore_defaults = QtCore.pyqtSignal()
restore_keyboard_defaults = QtCore.pyqtSignal()
# We want to send and receive settings events globally, not per
@ -203,8 +204,6 @@ class BeeSettings(QtCore.QSettings):
class KeyboardSettings(QtCore.QSettings):
save_unknown_shortcuts = True
def __init__(self):
settings_format = QtCore.QSettings.Format.IniFormat
filename = os.path.join(
@ -212,21 +211,29 @@ class KeyboardSettings(QtCore.QSettings):
'KeyboardSettings.ini')
super().__init__(filename, settings_format)
def set_shortcuts(self, group, key, values):
self.setValue(f'{group}/{key}', ', '.join(values))
def set_shortcuts(self, group, key, values, default=None):
if values == default:
self.remove(f'{group}/{key}')
else:
self.setValue(f'{group}/{key}', ', '.join(values))
def get_shortcuts(self, group, key, default=None):
values = self.value(f'{group}/{key}')
if values is not None:
values = list(filter(lambda x: x, values.split(', ')))
logger.debug(f'Found custom shortcuts for {group}/{key}: {values}')
return values
values = default or []
if self.save_unknown_shortcuts:
self.set_shortcuts(group, key, values)
return list(default or []) # Always return new instance of default
return values
def restore_defaults(self):
"""Restore all the values specified in FILEDS to their default values
by removing them from the settings file.
"""
logger.debug('Restoring keyboard shortcuts to defaults')
for key in self.allKeys():
self.remove(key)
settings_events.restore_keyboard_defaults.emit()
def logfile_name():

View file

@ -36,5 +36,7 @@ COLORS = {
# BeeRef specific:
'Scene:Selection': (116, 234, 231),
'Scene:Canvas': (60, 60, 60),
'Scene:Text': (200, 200, 200)
'Scene:Text': (200, 200, 200),
'Table:AlternativeRow': (70, 70, 70),
}

View file

@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
class ExporterRegistry(dict):
DEFAULT_TYPE = 'default exporter'
DEFAULT_TYPE = 0
def __getitem__(self, key):
key = key.removeprefix('.')

View file

@ -458,6 +458,9 @@ class BeeGraphicsView(MainControlsMixin,
def on_action_settings(self):
widgets.settings.SettingsDialog(self)
def on_action_keyboard_settings(self):
widgets.settings.KeyboardSettingsDialog(self)
def on_action_help(self):
widgets.HelpDialog(self)

View file

@ -16,10 +16,11 @@
from functools import partial
import logging
from PyQt6 import QtWidgets
from PyQt6 import QtWidgets, QtCore, QtGui
from beeref import constants
from beeref.config import BeeSettings, settings_events
from beeref.actions.actions import actions
from beeref.config import BeeSettings, KeyboardSettings, settings_events
logger = logging.getLogger(__name__)
@ -36,7 +37,8 @@ class RadioGroup(QtWidgets.QGroupBox):
self.settings = BeeSettings()
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
settings_events.restore_defaults.connect(self.on_restore_defaults)
settings_events.restore_keyboard_defaults.connect(
self.on_restore_defaults)
if self.HELPTEXT:
helptxt = QtWidgets.QLabel(self.HELPTEXT)
@ -177,3 +179,227 @@ class SettingsDialog(QtWidgets.QDialog):
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
BeeSettings().restore_defaults()
class KeyboardShortcutsEditor(QtWidgets.QKeySequenceEdit):
def __init__(self, parent, index):
super().__init__(parent)
self.action = actions[index.row()]
try:
self.old_value = self.action.get_shortcuts()[index.column() - 2]
except IndexError:
self.old_value = ''
self.setClearButtonEnabled(True)
self.setMaximumSequenceLength(1)
self.editingFinished.connect(self.on_editing_finished)
self.finished_last_called_with = None
self.remove_from_other = None
def on_editing_finished(self):
shortcut = self.keySequence().toString()
if self.finished_last_called_with == shortcut:
# Workaround for bug
# https://bugreports.qt.io/browse/QTBUG-40
# editingFinished signal is emitted twice because of
# the QMessageBox below
return
self.remove_from_other = None
self.finished_last_called_with = shortcut
for action in actions.values():
if action == self.action:
continue
if shortcut in action.get_shortcuts():
txt = ': '.join(action.menu_path + [action['text']])
txt = txt.replace('&', '').removesuffix('...')
msg = ('<p>This shortcut is already used for:</p>'
f'<p>{txt}</p>'
'<p>Do you want to remove the other shortcut'
' to save this one?</p>')
reply = QtWidgets.QMessageBox.question(
self, 'Save Shortcut?', msg)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
self.remove_from_other = action
else:
self.setKeySequence(self.old_value)
class KeyboardShortcutsDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
return KeyboardShortcutsEditor(parent, index)
def setModelData(self, editor, model, index):
model.setData(
index,
editor.keySequence(),
QtCore.Qt.ItemDataRole.EditRole,
remove_from_other=editor.remove_from_other)
class KeyboardShortcutsModel(QtCore.QAbstractTableModel):
"""An entry in the keyboard shortcuts table."""
HEADER = ('Action', '', 'Shortcut', 'Alternative')
def __init__(self):
super().__init__()
self.settings = KeyboardSettings()
def rowCount(self, parent):
return len(actions)
def columnCount(self, parent):
return len(self.HEADER)
def data(self, index, role):
if role in (QtCore.Qt.ItemDataRole.DisplayRole,
QtCore.Qt.ItemDataRole.EditRole):
action = actions[index.row()]
txt = ': '.join(action.menu_path + [action['text']])
if index.column() == 0:
return txt.replace('&', '').removesuffix('...')
if index.column() == 1 and action.shortcuts_changed():
return ''
if index.column() > 1:
return action.get_qkeysequence(index.column() - 2)
if role == QtCore.Qt.ItemDataRole.ToolTipRole:
action = actions[index.row()]
changed = action.shortcuts_changed()
if changed and index.column() == 1:
return 'Changed from default'
if changed and index.column() > 1:
default = action.get_default_shortcut(index.column() - 2)
default = default or '-'
return f'Default: {default}'
def setData(self, index, value, role, remove_from_other=None):
action = actions[index.row()]
shortcuts = action.get_shortcuts() + [None, None]
shortcuts[index.column() - 2] = value.toString()
shortcuts = list(filter(bool, shortcuts))
if len(shortcuts) != len(set(shortcuts)):
# We got the same shortcut twice
shortcuts = set(shortcuts)
action.set_shortcuts(shortcuts)
# Whole row might be affected, so excpliclity emit dataChanged
self.dataChanged.emit(self.index(index.row(), 1),
self.index(index.row(), 3))
if remove_from_other:
# This shortcut has conflicts with another action and the
# user chose to remove the other shortcut
shortcuts = remove_from_other.get_shortcuts()
shortcuts.remove(value.toString())
remove_from_other.set_shortcuts(shortcuts)
row = list(actions.keys()).index(remove_from_other['id'])
self.dataChanged.emit(self.index(row, 1),
self.index(row, 3))
return True
def headerData(self, section, orientation, role):
if (role == QtCore.Qt.ItemDataRole.DisplayRole
and orientation == QtCore.Qt.Orientation.Horizontal):
return self.HEADER[section]
def flags(self, index):
base = (QtCore.Qt.ItemFlag.ItemIsEnabled
| QtCore.Qt.ItemFlag.ItemNeverHasChildren)
if index.column() <= 1:
return base
else:
return (base | QtCore.Qt.ItemFlag.ItemIsEditable)
class KeyboardShortcutsProxy(QtCore.QSortFilterProxyModel):
def __init__(self):
super().__init__()
self.setSourceModel(KeyboardShortcutsModel())
self.setFilterCaseSensitivity(
QtCore.Qt.CaseSensitivity.CaseInsensitive)
def data(self, index, role):
if (role == QtCore.Qt.ItemDataRole.BackgroundRole
and index.row() % 2):
return QtGui.QColor(*constants.COLORS['Table:AlternativeRow'])
else:
return super().data(index, role)
def setData(self, index, value, role, remove_from_other=None):
result = self.sourceModel().setData(
self.mapToSource(index),
value,
role,
remove_from_other=remove_from_other)
return result
class KeyboardShortcutsView(QtWidgets.QTableView):
def __init__(self, parent):
super().__init__(parent)
self.setMinimumSize(QtCore.QSize(400, 200))
self.setItemDelegate(KeyboardShortcutsDelegate())
self.setShowGrid(False)
self.setModel(KeyboardShortcutsProxy())
self.horizontalHeader().setSectionResizeMode(
0, QtWidgets.QHeaderView.ResizeMode.Stretch)
self.horizontalHeader().setSectionResizeMode(
1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
self.setSelectionMode(
QtWidgets.QHeaderView.SelectionMode.SingleSelection)
settings_events.restore_defaults.connect(self.on_restore_defaults)
def on_restore_defaults(self):
self.viewport().update()
class KeyboardSettingsDialog(QtWidgets.QDialog):
def __init__(self, parent):
super().__init__(parent)
self.setWindowTitle('Keyboard Shortcuts')
tabs = QtWidgets.QTabWidget()
# Keyboard shortcuts
keyboard = QtWidgets.QWidget(parent)
kb_layout = QtWidgets.QVBoxLayout()
keyboard.setLayout(kb_layout)
table = KeyboardShortcutsView(keyboard)
search_input = QtWidgets.QLineEdit()
search_input.setPlaceholderText('Search...')
search_input.textChanged.connect(
lambda value: table.model().setFilterFixedString(value))
kb_layout.addWidget(search_input)
kb_layout.addWidget(table)
tabs.addTab(keyboard, '&Keyboard Shortcuts')
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
layout.addWidget(tabs)
# Bottom row of buttons
buttons = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Close)
buttons.rejected.connect(self.reject)
reset_btn = QtWidgets.QPushButton('&Restore Defaults')
reset_btn.setAutoDefault(False)
reset_btn.clicked.connect(self.on_restore_defaults)
buttons.addButton(reset_btn,
QtWidgets.QDialogButtonBox.ButtonRole.ActionRole)
layout.addWidget(buttons)
self.show()
def on_restore_defaults(self, *args, **kwargs):
reply = QtWidgets.QMessageBox.question(
self,
'Restore defaults?',
'Do you want to restore all settings to their default values?')
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
KeyboardSettings().restore_defaults()

View file

@ -9,8 +9,8 @@ setup(
license='LICENSE',
description='A simple reference image viewer',
install_requires=[
'pyQt6>=6.4.0,<=6.6.1',
'pyQt6-Qt6>=6.4.0,<=6.6.1',
'pyQt6>=6.5.0,<=6.6.1',
'pyQt6-Qt6>=6.5.0,<=6.6.1',
'rectangle-packer>=2.0.1,<=2.0.2',
'exif>=1.3.5,<=1.6.0',
],

View file

@ -0,0 +1,159 @@
from unittest.mock import patch
from PyQt6 import QtGui
from beeref.actions.actions import Action, ActionList
def test_action_equals_true():
action1 = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+R']})
action2 = Action({'id': 'foo', 'text': 'Bar', 'shortcuts': ['Ctrl+F']})
assert action1 == action2
def test_action_equals_false():
action1 = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+R']})
action2 = Action({'id': 'Bar', 'text': 'Foo', 'shortcuts': ['Ctrl+R']})
assert not action1 == action2
def test_action_on_restore_defaults(kbsettings, view):
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+R']})
action.qaction = QtGui.QAction('foo', view)
action.on_restore_defaults()
assert action.qaction.shortcuts() == ['Ctrl+R']
def test_action_on_restore_defaults_when_no_defaults(kbsettings, view):
action = Action({'id': 'foo'})
action.qaction = QtGui.QAction('foo', view)
action.qaction.setShortcuts(['Ctrl+R'])
action.on_restore_defaults()
assert action.qaction.shortcuts() == []
def test_action_get_overwritten_shortcuts(kbsettings):
kbsettings.set_shortcuts('Actions', 'foo', ['Alt+O'])
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
assert action.get_shortcuts() == ['Alt+O']
def test_action_get_shortcuts_gets_default(kbsettings):
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
assert action.get_shortcuts() == ['Ctrl+F']
def test_action_set_shortcuts_when_no_qaction(kbsettings):
action = Action({'id': 'foo'})
action.qaction = None
action.set_shortcuts(['Ctrl+F'])
assert kbsettings.get_shortcuts('Actions', 'foo') == ['Ctrl+F']
def test_action_set_shortcuts_when_qaction(kbsettings, view):
action = Action({'id': 'foo'})
action.qaction = QtGui.QAction('foo', view)
action.set_shortcuts(['Ctrl+F'])
assert kbsettings.get_shortcuts('Actions', 'foo') == ['Ctrl+F']
assert action.qaction.shortcuts() == ['Ctrl+F']
def test_action_get_qkeysequence_first(kbsettings):
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
assert action.get_qkeysequence(0) == QtGui.QKeySequence('Ctrl+F')
def test_action_get_qkeysequence_second(kbsettings):
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F', 'Ctrl+B']})
assert action.get_qkeysequence(1) == QtGui.QKeySequence('Ctrl+B')
def test_action_get_qkeysequence_first_when_not_set(kbsettings):
action = Action({'id': 'foo'})
assert action.get_qkeysequence(0) == QtGui.QKeySequence()
def test_action_get_qkeysequence_second_when_not_set(kbsettings):
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
assert action.get_qkeysequence(1) == QtGui.QKeySequence()
def test_action_shortcuts_changed_when_not_changed(kbsettings):
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
assert action.shortcuts_changed() is False
def test_action_shortcuts_changed_when_changed(kbsettings):
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
kbsettings.set_shortcuts('Actions', 'foo', ['Ctrl+B'])
assert action.shortcuts_changed() is True
def test_action_shortcuts_changed_when_empty(kbsettings):
action = Action({'id': 'foo'})
assert action.shortcuts_changed() is False
def test_action_get_default_shortcut_first():
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
assert action.get_default_shortcut(0) == 'Ctrl+F'
def test_action_get_default_shortcut_second():
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F', 'Ctrl+B']})
assert action.get_default_shortcut(1) == 'Ctrl+B'
def test_action_get_default_shortcut_first_when_none_set():
action = Action({'id': 'foo'})
assert action.get_default_shortcut(0) is None
def test_action_get_default_shortcut_second_when_none_set():
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
assert action.get_default_shortcut(1) is None
@patch('beeref.actions.actions.menu_structure',
[{'menu': 'Foo', 'items': ['bar', 'baz']}])
def test_action_menu_path():
action = Action({'id': 'baz'})
assert action.menu_path == ['Foo']
@patch('beeref.actions.actions.menu_structure',
[{'menu': 'Foo', 'items': [{'menu': 'Bar', 'items': ['baz']}]}])
def test_action_menu_path_with_submenus():
action = Action({'id': 'baz'})
assert action.menu_path == ['Foo', 'Bar']
@patch('beeref.actions.actions.menu_structure',
[{'menu': 'Foo', 'items': '_build_recent_files'}])
def test_action_menu_path_recent_files():
action = Action({'id': 'baz', 'menu_id': '_build_recent_files'})
assert action.menu_path == ['Foo']
@patch('beeref.actions.actions.menu_structure',
[{'menu': 'Foo', 'items': [
{'menu': 'Bar', 'items': '_build_recent_files'}]}])
def test_action_menu_path_recent_files_in_submenu():
action = Action({'id': 'baz', 'menu_id': '_build_recent_files'})
assert action.menu_path == ['Foo', 'Bar']
def test_actionlist_inits_dict():
action1 = Action({'id': 'foo'})
action2 = Action({'id': 'bar'})
actionlist = ActionList([action1, action2])
actionlist['foo'] == action1
actionlist['bar'] == action2
def test_actionlist_acts_as_list():
action1 = Action({'id': 'foo'})
action2 = Action({'id': 'bar'})
actionlist = ActionList([action1, action2])
actionlist[0] == action1
actionlist[1] == action2

View file

@ -4,6 +4,7 @@ from unittest.mock import patch, MagicMock, call
from PyQt6 import QtWidgets
from beeref.actions import ActionsMixin
from beeref.actions.actions import Action, ActionList
from beeref.actions.menu_structure import MENU_SEPARATOR
@ -24,21 +25,19 @@ class FooWidget(QtWidgets.QWidget, ActionsMixin):
@patch('PyQt6.QtGui.QAction.triggered')
@patch('PyQt6.QtGui.QAction.toggled')
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
@patch('beeref.actions.mixin.KeyboardSettings.get_shortcuts')
def test_create_actions(
kb_mock, actions_mock, menu_mock, toggle_mock, trigger_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
@patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'shortcuts': ['Ctrl+F'],
'callback': 'on_foo',
})]))
@patch('beeref.config.KeyboardSettings.get_shortcuts')
def test_create_actions(kb_mock, toggle_mock, trigger_mock, qapp):
kb_mock.side_effect = lambda group, key, default: default
widget = FooWidget()
actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'shortcuts': ['Ctrl+F'],
'callback': 'on_foo',
}]
menu_mock.__iter__.return_value = ['foo']
widget.build_menu_and_actions()
trigger_mock.connect.assert_called_once_with(widget.on_foo)
toggle_mock.connect.assert_not_called()
@ -48,44 +47,43 @@ def test_create_actions(
assert qaction.text() == '&Foo'
assert qaction.shortcut() == 'Ctrl+F'
assert qaction.isEnabled() is True
assert widget.bee_actions['foo'] == qaction
from beeref.actions.mixin import actions
assert actions['foo'].qaction == qaction
kb_mock.assert_called_once_with('Actions', 'foo', ['Ctrl+F'])
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_create_actions_with_shortcut_from_settings(
actions_mock, menu_mock, qapp, kbsettings):
kbsettings.set_shortcuts('Actions', 'foo', ['Alt+O'])
widget = FooWidget()
actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'shortcuts': ['Ctrl+F'],
'callback': 'on_foo',
}]
menu_mock.__iter__.return_value = ['foo']
widget.build_menu_and_actions()
qaction = widget.actions()[0]
assert qaction.shortcut() == 'Alt+O'
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
def test_create_actions_with_shortcut_from_settings(qapp, kbsettings):
with patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'shortcuts': ['Ctrl+F'],
'callback': 'on_foo'})])):
# Create Action inside the test function so that its
# kbsettings get created after the kbsettings fixture changes
# the file path
kbsettings.set_shortcuts('Actions', 'foo', ['Alt+O'])
widget = FooWidget()
widget.build_menu_and_actions()
qaction = widget.actions()[0]
assert qaction.shortcuts() == ['Alt+O']
@patch('PyQt6.QtGui.QAction.triggered')
@patch('PyQt6.QtGui.QAction.toggled')
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_create_actions_checkable(
actions_mock, menu_mock, toggle_mock, trigger_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
@patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'checkable': True,
'callback': 'on_foo',
})]))
def test_create_actions_checkable(toggle_mock, trigger_mock, qapp):
widget = FooWidget()
actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'checkable': True,
'callback': 'on_foo',
}]
menu_mock.__iter__.return_value = ['foo']
widget.build_menu_and_actions()
trigger_mock.connect.assert_not_called()
toggle_mock.connect.assert_called_once_with(widget.on_foo)
@ -95,25 +93,23 @@ def test_create_actions_checkable(
assert qaction.text() == '&Foo'
assert qaction.isEnabled() is True
assert qaction.isChecked() is False
assert widget.bee_actions['foo'] == qaction
@patch('PyQt6.QtGui.QAction.triggered')
@patch('PyQt6.QtGui.QAction.toggled')
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
@patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'checkable': True,
'checked': True,
'callback': 'on_foo',
})]))
def test_create_actions_checkable_checked_true(
actions_mock, menu_mock, toggle_mock, trigger_mock, qapp):
toggle_mock, trigger_mock, qapp):
widget = FooWidget()
actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'checkable': True,
'checked': True,
'callback': 'on_foo',
}]
menu_mock.__iter__.return_value = ['foo']
widget.build_menu_and_actions()
trigger_mock.connect.assert_not_called()
toggle_mock.connect.assert_called_once_with(widget.on_foo)
@ -123,27 +119,24 @@ def test_create_actions_checkable_checked_true(
assert qaction.text() == '&Foo'
assert qaction.isEnabled() is True
assert qaction.isChecked() is True
assert widget.bee_actions['foo'] == qaction
@patch.object(FooWidget, 'on_foo')
@patch.object(FooWidget, 'settings')
@patch('PyQt6.QtGui.QAction.toggled')
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
@patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'checkable': True,
'settings': 'foo/bar',
'callback': 'on_foo',
})]))
def test_create_actions_checkable_with_settings(
actions_mock, menu_mock, toggle_mock, settings_mock, callback_mock,
qapp):
toggle_mock, settings_mock, callback_mock, qapp):
widget = FooWidget()
actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'checkable': True,
'callback': 'on_foo',
'settings': 'foo/bar',
}]
menu_mock.__iter__.return_value = ['foo']
settings_mock.value.return_value = True
widget.build_menu_and_actions()
settings_mock.value.assert_called_once_with(
@ -154,152 +147,141 @@ def test_create_actions_checkable_with_settings(
callback_mock.assert_called_once_with(True)
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_create_actions_with_group(actions_mock, menu_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
@patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'bar',
})]))
def test_create_actions_with_group(qapp):
widget = FooWidget()
actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'bar',
}]
menu_mock.__iter__.return_value = ['foo']
widget.build_menu_and_actions()
assert len(widget.actions()) == 1
qaction = widget.actions()[0]
assert widget.bee_actiongroups['bar'] == [qaction]
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_build_menu_and_actions_with_actions(actions_mock, menu_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
@patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
})]))
def test_build_menu_and_actions_with_actions(qapp):
widget = FooWidget()
actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'bar',
}]
menu_mock.__iter__.return_value = ['foo']
with patch('PyQt6.QtWidgets.QMenu.addAction') as add_mock:
widget.build_menu_and_actions()
assert isinstance(widget.context_menu, QtWidgets.QMenu)
add_mock.assert_called_once_with(widget.bee_actions['foo'])
from beeref.actions.mixin import actions
add_mock.assert_called_once_with(actions['foo'].qaction)
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_build_menu_and_actions_with_separator(actions_mock, menu_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': [MENU_SEPARATOR]}])
@patch('beeref.actions.mixin.actions', ActionList([]))
def test_build_menu_and_actions_with_separator(qapp):
widget = FooWidget()
menu_mock.__iter__.return_value = [MENU_SEPARATOR]
with patch('PyQt6.QtWidgets.QMenu.addSeparator') as sep_mock:
widget.build_menu_and_actions()
assert isinstance(widget.context_menu, QtWidgets.QMenu)
sep_mock.assert_called_once_with()
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_build_menu_and_actions_with_submenu(actions_mock, menu_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': [{'menu': 'Bar', 'items': ['foo']}]}])
@patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
})]))
def test_build_menu_and_actions_with_submenu(qapp):
widget = FooWidget()
actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'bar',
}]
menu_mock.__iter__.return_value = [
{'menu': '&Bar', 'items': ['foo']}]
with patch('PyQt6.QtWidgets.QMenu.addAction') as add_mock:
with patch('PyQt6.QtWidgets.QMenu.addMenu') as addmenu_mock:
addmenu_mock.return_value = QtWidgets.QMenu()
widget.build_menu_and_actions()
assert isinstance(widget.context_menu, QtWidgets.QMenu)
addmenu_mock.assert_called_once_with('&Bar')
add_mock.assert_called_once_with(widget.bee_actions['foo'])
addmenu_mock.assert_called_with('Bar')
from beeref.actions.mixin import actions
add_mock.assert_called_once_with(actions['foo'].qaction)
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_actiongroup_set_enabled(actions_mock, menu_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
@patch('beeref.actions.mixin.actions',
ActionList([
Action({
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'g1',
}),
Action({
'id': 'bar',
'text': '&Bar',
'callback': 'on_foo',
'group': 'g2',
}),
]))
def test_actiongroup_set_enabled(qapp):
widget = FooWidget()
actions_mock.__iter__.return_value = [
{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'g1',
},
{
'id': 'bar',
'text': '&Bar',
'callback': 'on_foo',
'group': 'g2',
},
]
menu_mock.__iter__.return_value = ['foo']
widget.build_menu_and_actions()
widget.actiongroup_set_enabled('g1', True)
assert widget.bee_actions['foo'].isEnabled() is True
assert widget.bee_actions['bar'].isEnabled() is False
from beeref.actions.mixin import actions
assert actions['foo'].qaction.isEnabled() is True
assert actions['bar'].qaction.isEnabled() is False
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_build_menu_and_actions_disables_actiongroups(
actions_mock, menu_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': ['foo']}])
@patch('beeref.actions.mixin.actions',
ActionList([Action({
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'active_when_selection',
})]))
def test_build_menu_and_actions_disables_actiongroups(qapp):
widget = FooWidget()
widget.scene.has_selection.return_value = False
actions_mock.__iter__.return_value = [
{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'active_when_selection',
},
]
menu_mock.__iter__.return_value = ['foo']
widget.build_menu_and_actions()
qaction = widget.actions()[0]
assert qaction.isEnabled() is False
@patch('PyQt6.QtGui.QAction.triggered')
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
@patch('beeref.actions.mixin.KeyboardSettings.get_shortcuts')
def test_create_recent_files_more_files_than_shortcuts(
kb_mock, actions_mock, menu_mock, triggered_mock, qapp):
@patch('beeref.config.KeyboardSettings.get_shortcuts')
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': '_build_recent_files'}])
@patch('beeref.actions.mixin.actions', ActionList([]))
def test_create_recent_files_more_than_10_files(
kb_mock, triggered_mock, qapp):
kb_mock.side_effect = lambda group, key, default: default
widget = FooWidget()
widget.settings.get_recent_files.return_value = [
os.path.abspath(f'{i}.bee') for i in range(15)]
menu_mock.__iter__.return_value = [{
'menu': 'Open &Recent',
'items': '_build_recent_files',
}]
widget.build_menu_and_actions()
triggered_mock.connect.assert_called()
assert len(widget.actions()) == 15
assert len(widget.actions()) == 10
from beeref.actions.mixin import actions
qaction1 = widget.actions()[0]
assert qaction1.text() == '0.bee'
assert qaction1.shortcut() == 'Ctrl+1'
assert qaction1.isEnabled() is True
assert widget.bee_actions['recent_files_0'] == qaction1
assert actions['recent_files_0'].qaction == qaction1
qaction10 = widget.actions()[9]
assert qaction10.text() == '9.bee'
assert qaction10.shortcut() == 'Ctrl+0'
assert qaction10.isEnabled() is True
assert widget.bee_actions['recent_files_9'] == qaction10
qaction15 = widget.actions()[-1]
assert qaction15.text() == '14.bee'
assert qaction15.shortcut() == ''
assert qaction15.isEnabled() is True
assert widget.bee_actions['recent_files_14'] == qaction15
assert actions['recent_files_9'].qaction == qaction10
assert kb_mock.call_count == 10
kb_mock.assert_has_calls(
@ -309,73 +291,62 @@ def test_create_recent_files_more_files_than_shortcuts(
@patch('PyQt6.QtGui.QAction.triggered')
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
@patch('beeref.actions.mixin.KeyboardSettings.get_shortcuts')
def test_create_recent_files_fewer_files_than_shortcuts(
kb_mock, actions_mock, menu_mock, triggered_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': '_build_recent_files'}])
@patch('beeref.actions.mixin.actions', ActionList([]))
@patch('beeref.config.KeyboardSettings.get_shortcuts')
def test_create_recent_files_fewer_files_than_10_files(
kb_mock, triggered_mock, qapp):
kb_mock.side_effect = lambda group, key, default: default
widget = FooWidget()
widget.settings.get_recent_files.return_value = [
os.path.abspath(f'{i}.bee') for i in range(5)]
menu_mock.__iter__.return_value = [{
'menu': 'Open &Recent',
'items': '_build_recent_files',
}]
widget.build_menu_and_actions()
triggered_mock.connect.assert_called()
assert len(widget.actions()) == 5
from beeref.actions.mixin import actions
qaction1 = widget.actions()[0]
assert qaction1.text() == '0.bee'
assert qaction1.shortcut() == 'Ctrl+1'
assert qaction1.isEnabled() is True
assert widget.bee_actions['recent_files_0'] == qaction1
assert actions['recent_files_0'].qaction == qaction1
qaction5 = widget.actions()[4]
assert qaction5.text() == '4.bee'
assert qaction5.shortcut() == 'Ctrl+5'
assert qaction5.isEnabled() is True
assert widget.bee_actions['recent_files_4'] == qaction5
assert actions['recent_files_4'].qaction == qaction5
assert actions['recent_files_5'].qaction is None
assert kb_mock.call_count == 10
assert kb_mock.call_count == 5
kb_mock.assert_has_calls(
[call('Actions', 'recent_files_0', ['Ctrl+1']),
call('Actions', 'recent_files_9', ['Ctrl+0'])],
call('Actions', 'recent_files_4', ['Ctrl+5'])],
any_order=True)
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
@patch('beeref.actions.mixin.KeyboardSettings.get_shortcuts')
def test_create_recent_files_when_no_files(
kb_mock, actions_mock, menu_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': '_build_recent_files'}])
@patch('beeref.actions.mixin.actions', ActionList([]))
@patch('beeref.config.KeyboardSettings.get_shortcuts')
def test_create_recent_files_when_no_files(kb_mock, qapp):
kb_mock.side_effect = lambda group, key, default: default
widget = FooWidget()
widget.settings.get_recent_files.return_value = []
menu_mock.__iter__.return_value = [{
'menu': 'Open &Recent',
'items': '_build_recent_files',
}]
widget.build_menu_and_actions()
assert len(widget.actions()) == 0
assert kb_mock.call_count == 10
kb_mock.assert_has_calls(
[call('Actions', 'recent_files_0', ['Ctrl+1']),
call('Actions', 'recent_files_9', ['Ctrl+0'])],
any_order=True)
kb_mock.assert_not_called()
@patch('PyQt6.QtGui.QAction.triggered')
@patch('beeref.actions.mixin.menu_structure')
@patch('beeref.actions.mixin.actions')
def test_update_recent_files(actions_mock, menu_mock, triggered_mock, qapp):
@patch('beeref.actions.mixin.menu_structure',
[{'menu': 'Foo', 'items': '_build_recent_files'}])
@patch('beeref.actions.mixin.actions', ActionList([]))
def test_update_recent_files(triggered_mock, qapp):
widget = FooWidget()
widget.settings.get_recent_files.return_value = [
os.path.abspath('foo.bee')]
menu_mock.__iter__.return_value = [{
'menu': 'Open &Recent',
'items': '_build_recent_files',
}]
widget.build_menu_and_actions()
triggered_mock.connect.reset_mock()

View file

@ -17,9 +17,13 @@ def pytest_configure(config):
import logging.config
logging.config.dictConfig = MagicMock
# Disable creation of KeyboardSettings.ini to speed tests up
from beeref.config import KeyboardSettings
KeyboardSettings.save_unknown_shortcuts = False
@pytest.fixture(autouse=True)
def reset_beeref_actions():
from beeref.actions.actions import actions
for key in list(actions.keys()):
if key.startswith('recent_files_'):
actions.pop(key)
@pytest.fixture(autouse=True)

View file

@ -59,13 +59,13 @@ def test_settings_value_or_default_gets_default_when_cast_error(settings):
assert settings.valueOrDefault('Items/arrange_gap') == 0
def test_restore_defaults_restores(settings):
def test_settings_restore_defaults_restores(settings):
settings.setValue('Items/image_storage_format', 'png')
settings.restore_defaults()
assert settings.contains('Items/image_storage_format') is False
def test_restore_defaults_leaves_other_settings(settings):
def test_settings_restore_defaults_leaves_other_settings(settings):
settings.setValue('foo/bar', 'baz')
settings.restore_defaults()
assert settings.contains('foo/bar') is True
@ -122,34 +122,34 @@ def test_keyboardsettings_set_shortcuts_multiple(kbsettings):
def test_keyboardsettings_get_shortcuts_existing(kbsettings):
kbsettings.set_shortcuts('Actions', 'bar', ['Ctrl+R'])
with patch.object(kbsettings, 'set_shortcuts') as set_mock:
with patch.object(kbsettings, 'save_unknown_shortcuts', True):
shortcuts = kbsettings.get_shortcuts('Actions', 'bar', ['Ctrl+B'])
assert shortcuts == ['Ctrl+R']
set_mock.assert_not_called()
shortcuts = kbsettings.get_shortcuts('Actions', 'bar', ['Ctrl+B'])
assert shortcuts == ['Ctrl+R']
def test_keyboardsettings_get_shortcuts_default(kbsettings):
with patch.object(kbsettings, 'set_shortcuts') as set_mock:
with patch.object(kbsettings, 'save_unknown_shortcuts', True):
shortcuts = kbsettings.get_shortcuts('Actions', 'bar', ['Ctrl+B'])
assert shortcuts == ['Ctrl+B']
set_mock.assert_called_once_with('Actions', 'bar', ['Ctrl+B'])
shortcuts = kbsettings.get_shortcuts('Actions', 'bar', ['Ctrl+B'])
assert shortcuts == ['Ctrl+B']
def test_keyboardsettings_get_shortcuts_default_doesnt_override_empty(
kbsettings):
kbsettings.set_shortcuts('Actions', 'bar', [])
with patch.object(kbsettings, 'set_shortcuts') as set_mock:
with patch.object(kbsettings, 'save_unknown_shortcuts', True):
shortcuts = kbsettings.get_shortcuts('Actions', 'bar', ['Ctrl+B'])
assert shortcuts == []
set_mock.assert_not_called()
@patch('beeref.config.KeyboardSettings.setValue')
@patch('beeref.config.KeyboardSettings.remove')
def test_keyboardsettings_set_shortcuts_other_than_default_saves(
remove_mock, set_mock, kbsettings):
kbsettings.set_shortcuts('Actions', 'bar', ['Ctrl+R'], ['Ctrl+Z'])
set_mock.assert_called_once_with('Actions/bar', 'Ctrl+R')
remove_mock.assert_not_called()
def test_keyboardsettings_get_shortcuts_not_set_no_defaults(kbsettings):
with patch('beeref.config.KeyboardSettings.set_shortcuts') as set_mock:
with patch.object(kbsettings, 'save_unknown_shortcuts', True):
shortcuts = kbsettings.get_shortcuts('Actions', 'baz')
assert shortcuts == []
set_mock.assert_called_once_with('Actions', 'baz', [])
@patch('beeref.config.KeyboardSettings.setValue')
@patch('beeref.config.KeyboardSettings.remove')
def test_keyboardsettings_set_shortcuts_with_than_default_doesnt_save(
remove_mock, set_mock, kbsettings):
kbsettings.set_shortcuts('Actions', 'bar', ['Ctrl+R'], ['Ctrl+R'])
set_mock.assert_not_called()
remove_mock.assert_called_once_with('Actions/bar')
def test_keyboardsettings_restore_defaults_restores(kbsettings):
kbsettings.setValue('Actions/bar', 'Ctrl+R')
kbsettings.restore_defaults()
assert kbsettings.contains('Actions/bar') is False

View file

@ -11,12 +11,12 @@ from beeref.items import BeePixmapItem, BeeTextItem
from beeref.view import BeeGraphicsView
def test_inits_menu(view, qapp):
def test_inits_menu(qapp):
parent = QtWidgets.QMainWindow()
view = BeeGraphicsView(qapp, parent)
assert isinstance(view.context_menu, QtWidgets.QMenu)
assert len(view.actions()) > 0
assert view.bee_actions
assert view.actions()
assert view.bee_actiongroups
@ -31,7 +31,7 @@ def test_init_without_filename(open_file_mock, qapp, commandline_args):
@patch('beeref.view.BeeGraphicsView.open_from_file')
def test_init_with_filename(open_file_mock, view, qapp, commandline_args):
def test_init_with_filename(open_file_mock, qapp, commandline_args):
commandline_args.filename = 'test.bee'
parent = QtWidgets.QMainWindow()
view = BeeGraphicsView(qapp, parent)
@ -374,6 +374,12 @@ def test_on_action_settings(show_mock, view):
show_mock.assert_called_once()
@patch('beeref.widgets.settings.KeyboardSettingsDialog.show')
def test_on_action_keyboard_settings(show_mock, view):
view.on_action_keyboard_settings()
show_mock.assert_called_once()
@patch('beeref.widgets.HelpDialog.show')
def test_on_action_help(show_mock, view):
view.on_action_help()

View file

@ -1,9 +1,16 @@
from unittest.mock import patch
from unittest.mock import patch, MagicMock
from PyQt6 import QtWidgets
from PyQt6 import QtWidgets, QtCore, QtGui
from beeref.actions.actions import Action, ActionList
from beeref.widgets.settings import (
ArrangeGapWidget,
ImageStorageFormatWidget,
KeyboardSettingsDialog,
KeyboardShortcutsDelegate,
KeyboardShortcutsEditor,
KeyboardShortcutsModel,
KeyboardShortcutsProxy,
SettingsDialog,
)
@ -67,3 +74,418 @@ def test_settings_dialog_on_restore_defaults(msg_mock, settings, view):
msg_mock.assert_called_once()
assert settings.valueOrDefault('Items/image_storage_format') == 'best'
assert settings.valueOrDefault('Items/arrange_gap') == 0
def test_keyboard_shortcuts_editor_no_conflicts(view):
a1 = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
a2 = Action({'id': 'bar', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
editor = KeyboardShortcutsEditor(
view,
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)))
editor.setKeySequence('Ctrl+A')
editor.on_editing_finished()
assert editor.keySequence().toString() == 'Ctrl+A'
assert editor.remove_from_other is None
def test_keyboard_shortcuts_editor_reenter_existing_shortcut(view):
a1 = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
a2 = Action({'id': 'bar', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
editor = KeyboardShortcutsEditor(
view,
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)))
editor.setKeySequence('Ctrl+F')
editor.on_editing_finished()
assert editor.keySequence().toString() == 'Ctrl+F'
assert editor.remove_from_other is None
@patch('PyQt6.QtWidgets.QMessageBox.question',
return_value=QtWidgets.QMessageBox.StandardButton.No)
def test_keyboard_shortcuts_editor_conflicts_choose_to_cancel(
question_mock, view):
a1 = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
a2 = Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
editor = KeyboardShortcutsEditor(
view,
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)))
editor.setKeySequence('Ctrl+B')
editor.on_editing_finished()
assert editor.keySequence().toString() == 'Ctrl+F'
assert editor.remove_from_other is None
@patch('PyQt6.QtWidgets.QMessageBox.question',
return_value=QtWidgets.QMessageBox.StandardButton.Yes)
def test_keyboard_shortcuts_editor_conflicts_choose_to_confirm(
question_mock, view):
a1 = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
a2 = Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
editor = KeyboardShortcutsEditor(
view,
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)))
editor.setKeySequence('Ctrl+B')
editor.on_editing_finished()
assert editor.keySequence().toString() == 'Ctrl+B'
assert editor.remove_from_other == a2
@patch('PyQt6.QtWidgets.QMessageBox.question',
return_value=QtWidgets.QMessageBox.StandardButton.No)
def test_keyboard_shortcuts_editor_conflicts_choose_to_cancel_when_no_shortcut(
question_mock, view):
a1 = Action({'id': 'foo', 'text': 'Foo'})
a2 = Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
editor = KeyboardShortcutsEditor(
view,
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)))
editor.setKeySequence('Ctrl+B')
editor.on_editing_finished()
assert editor.keySequence().toString() == ''
assert a2.get_shortcuts() == ['Ctrl+B']
def test_keyboard_shortcuts_delegate_setmodeldata(view):
a1 = Action({'id': 'foo', 'text': 'Foo'})
a2 = Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
model = KeyboardShortcutsModel()
delegate = KeyboardShortcutsDelegate()
editor = delegate.createEditor(view, None, index=model.index(0, 2))
editor.setKeySequence('Ctrl+F')
delegate.setModelData(
editor, model, index=model.index(0, 2))
assert a1.get_shortcuts() == ['Ctrl+F']
def test_keyboard_shortcuts_model_columncount():
model = KeyboardShortcutsModel()
model.columnCount(None) == 4
@patch('beeref.widgets.settings.actions',
ActionList([
Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']}),
Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
]))
def test_keyboard_shortcuts_model_rowcount():
model = KeyboardShortcutsModel()
model.rowCount(None) == 2
def test_keyboard_shortcuts_model_data_gets_text():
action = Action({'id': 'foo', 'text': '&Foo', 'shortcuts': ['Ctrl+F']})
action.menu_path = ['&Bar', 'Ba&z']
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=0),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.DisplayRole)
assert value == 'Bar: Baz: Foo'
def test_keyboard_shortcuts_model_data_gets_changed_when_not_changed():
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=1),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.DisplayRole)
assert value is None
def test_keyboard_shortcuts_model_data_gets_changed_when_changed(kbsettings):
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
action.set_shortcuts(['Ctrl+B'])
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=1),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.DisplayRole)
assert value == ''
def test_keyboard_shortcuts_model_data_gets_shortcut():
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.DisplayRole)
assert value == 'Ctrl+F'
def test_keyboard_shortcuts_model_data_tooltip_changed_when_not_changed():
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=1),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.ToolTipRole)
assert value is None
def test_keyboard_shortcuts_model_data_tooltip_changed_when_changed():
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
action.set_shortcuts(['Ctrl+B'])
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=1),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.ToolTipRole)
assert value == 'Changed from default'
def test_keyboard_shortcuts_model_data_tooltip_shortcut_when_not_changed():
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.ToolTipRole)
assert value is None
def test_keyboard_shortcuts_model_data_tooltip_shortcut_not_changed_not_set():
action = Action({'id': 'foo', 'text': 'Foo'})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=3),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.ToolTipRole)
assert value is None
def test_keyboard_shortcuts_model_data_tooltip_shortcut_when_changed():
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
action.set_shortcuts(['Ctrl+B'])
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.ToolTipRole)
assert value == 'Default: Ctrl+F'
def test_keyboard_shortcuts_model_data_tooltip_shortcut_changed_from_none():
action = Action({'id': 'foo', 'text': 'Foo'})
with patch('beeref.widgets.settings.actions', ActionList([action])):
action.set_shortcuts(['Ctrl+B'])
model = KeyboardShortcutsModel()
value = model.data(
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)),
role=QtCore.Qt.ItemDataRole.ToolTipRole)
assert value == 'Default: -'
def test_keyboard_shortcuts_model_setdata_saves():
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
model.setData(
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)),
value=QtGui.QKeySequence('Ctrl+B'),
role=None)
assert action.get_shortcuts() == ['Ctrl+B']
def test_keyboard_shortcuts_model_setdata_saves_second_shortcut():
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
model.setData(
index=MagicMock(
column=MagicMock(return_value=3),
row=MagicMock(return_value=0)),
value=QtGui.QKeySequence('Ctrl+B'),
role=None)
assert action.get_shortcuts() == ['Ctrl+F', 'Ctrl+B']
def test_keyboard_shortcuts_model_setdata_saves_second_shortcut_no_first():
action = Action({'id': 'foo'})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
model.setData(
index=MagicMock(
column=MagicMock(return_value=3),
row=MagicMock(return_value=0)),
value=QtGui.QKeySequence('Ctrl+B'),
role=None)
assert action.get_shortcuts() == ['Ctrl+B']
def test_keyboard_shortcuts_model_setdata_removes_duplicate():
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([action])):
model = KeyboardShortcutsModel()
model.setData(
index=MagicMock(
column=MagicMock(return_value=3),
row=MagicMock(return_value=0)),
value=QtGui.QKeySequence('Ctrl+B'),
role=None)
assert action.get_shortcuts() == ['Ctrl+B']
def test_keyboard_shortcuts_model_setdata_remove_from_other():
a1 = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
a2 = Action({'id': 'bar', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
model = KeyboardShortcutsModel()
model.setData(
index=MagicMock(
column=MagicMock(return_value=2),
row=MagicMock(return_value=0)),
value=QtGui.QKeySequence('Ctrl+B'),
role=None,
remove_from_other=a2)
assert a1.get_shortcuts() == ['Ctrl+B']
assert a2.get_shortcuts() == []
def test_keyboard_shortcuts_model_headerdata():
model = KeyboardShortcutsModel()
header = model.headerData(
0,
QtCore.Qt.Orientation.Horizontal,
QtCore.Qt.ItemDataRole.DisplayRole)
assert header == 'Action'
def test_flags_first_column():
model = KeyboardShortcutsModel()
flags = model.flags(model.index(0, 0))
assert flags == (QtCore.Qt.ItemFlag.ItemIsEnabled
| QtCore.Qt.ItemFlag.ItemNeverHasChildren)
def test_flags_shortcut_column():
model = KeyboardShortcutsModel()
flags = model.flags(model.index(0, 2))
assert flags == (QtCore.Qt.ItemFlag.ItemIsEnabled
| QtCore.Qt.ItemFlag.ItemNeverHasChildren
| QtCore.Qt.ItemFlag.ItemIsEditable)
@patch('beeref.widgets.settings.actions',
ActionList([Action({'id': 'bar', 'text': 'Bar'}),
Action({'id': 'foo', 'text': 'Foo'}),
Action({'id': 'baz', 'text': 'Baz'})]))
def test_keyboard_shortcuts_proxy_data_unfiltered():
proxy = KeyboardShortcutsProxy()
color1 = proxy.data(
proxy.index(0, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
color2 = proxy.data(
proxy.index(1, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
color3 = proxy.data(
proxy.index(2, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
assert color1 == color3
assert color1 != color2
assert proxy.data(
proxy.index(0, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Bar'
assert proxy.data(
proxy.index(1, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Foo'
assert proxy.data(
proxy.index(2, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Baz'
@patch('beeref.widgets.settings.actions',
ActionList([Action({'id': 'bar', 'text': 'Bar'}),
Action({'id': 'foo', 'text': 'Foo'}),
Action({'id': 'baz', 'text': 'Baz'})]))
def test_keyboard_shortcuts_proxy_data_filtered():
proxy = KeyboardShortcutsProxy()
proxy.setFilterFixedString('b')
color1 = proxy.data(
proxy.index(0, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
color2 = proxy.data(
proxy.index(1, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
assert color1 != color2
assert proxy.data(
proxy.index(0, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Bar'
assert proxy.data(
proxy.index(1, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Baz'
def test_keyboard_shortcuts_proxy_setdata_saves_correct_filtered_index():
a1 = Action({'id': 'bar', 'text': 'Bar'})
a2 = Action({'id': 'foo', 'text': 'Foo'})
a3 = Action({'id': 'baz', 'text': 'Baz'})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2, a3])):
proxy = KeyboardShortcutsProxy()
proxy.setFilterFixedString('b')
proxy.setData(
index=proxy.index(1, 2),
value=QtGui.QKeySequence('Ctrl+B'),
role=None)
assert a1.get_shortcuts() == []
assert a2.get_shortcuts() == []
assert a3.get_shortcuts() == ['Ctrl+B']
def test_keyboard_shortcuts_proxy_setdata_remove_from_other():
a1 = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
a2 = Action({'id': 'bar', 'shortcuts': ['Ctrl+B']})
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
proxy = KeyboardShortcutsProxy()
proxy.setData(
index=proxy.index(0, 2),
value=QtGui.QKeySequence('Ctrl+B'),
role=None,
remove_from_other=a2)
assert a1.get_shortcuts() == ['Ctrl+B']
assert a2.get_shortcuts() == []
@patch('PyQt6.QtWidgets.QMessageBox.question',
return_value=QtWidgets.QMessageBox.StandardButton.Yes)
@patch('beeref.config.KeyboardSettings.restore_defaults')
def test_keyboard_settings_dialog_on_restore_defaults(
restore_mock, msg_mock, kbsettings, view):
dialog = KeyboardSettingsDialog(view)
dialog.on_restore_defaults()
msg_mock.assert_called_once()
restore_mock.assert_called()