mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Merge branch 'rbreu:main' into easy_drag_and_drop
This commit is contained in:
commit
3b00e730f6
44 changed files with 5734 additions and 1287 deletions
25
.github/workflows/build_appimage.yml
vendored
Normal file
25
.github/workflows/build_appimage.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
name: build_appimage
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
|
||||
build_appimage:
|
||||
name: build_appimage
|
||||
|
||||
runs-on: 'ubuntu-20.04'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Build appimage
|
||||
run: |
|
||||
python3 tools/build_appimage.py --version=${{ github.ref_name }} --jsonfile=tools/linux_libs.json
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: BeeRef*.appimage
|
||||
retention-days: 5
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -28,6 +28,8 @@ share/python-wheels/
|
|||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
*.appimage
|
||||
squashfs-root/
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ Added
|
|||
* Added panning via scrollwheel:
|
||||
* Scroll wheel + Shift + Ctrl: pan vertically
|
||||
* Scroll wheel + Shift: pan horizontally
|
||||
* Make mouse and mouse wheel controls configurable
|
||||
(Settings -> Keyboard & Mouse)
|
||||
|
||||
|
||||
Fixed
|
||||
|
|
@ -24,6 +26,11 @@ Fixed
|
|||
correctly depending on the selected images.
|
||||
* Removed black line under marching ants outline of crop mode, which
|
||||
would scale with the image and get potentially very thick.
|
||||
* Fixed a crash when importing images with unsupported exif orientation info
|
||||
* Fixed threading issue when importing images (causing potential
|
||||
hangs/weird behaviour)
|
||||
* Fixed an intermittent crash when invoking New Scene
|
||||
* Fixed bee files hanging on to disk space of deleted images (issue #99)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ BeeRef is written in Python and PyQt6.
|
|||
Developing
|
||||
----------
|
||||
|
||||
Optional step: Use pyenv to create a virtual environment:
|
||||
Optional step: Use pyenv to create a virtual environment::
|
||||
|
||||
pyenv install -v 3.11
|
||||
pyenv virtualenv 3.11 beeref
|
||||
|
||||
Once the vitrual environment is set up, you can enter it with:
|
||||
Once the vitrual environment is set up, you can enter it with::
|
||||
|
||||
pyenv activate beeref
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ def main():
|
|||
logger.info(f'Starting {constants.APPNAME} version {constants.VERSION}')
|
||||
logger.debug('System: %s', ' '.join(platform.uname()))
|
||||
logger.debug('Python: %s', platform.python_version())
|
||||
logger.debug('LD_LIBRARY_PATH: %s', os.environ.get('LD_LIBRARY_PATH'))
|
||||
settings = BeeSettings()
|
||||
logger.info(f'Using settings: {settings.fileName()}')
|
||||
logger.info(f'Logging to: {logfile_name()}')
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import cached_property
|
||||
import logging
|
||||
|
||||
|
|
@ -21,22 +20,39 @@ from PyQt6 import QtGui
|
|||
|
||||
from beeref.actions.menu_structure import menu_structure
|
||||
from beeref.config import KeyboardSettings, settings_events
|
||||
from beeref.utils import ActionList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Action(dict):
|
||||
class Action:
|
||||
SETTINGS_GROUP = 'Actions'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, id, text, callback=None, shortcuts=None,
|
||||
checkable=False, checked=False, group=None, settings=None,
|
||||
enabled=True, menu_item=None, menu_id=None):
|
||||
self.id = id
|
||||
self.text = text
|
||||
self.callback = callback
|
||||
self.shortcuts = shortcuts or []
|
||||
self.checkable = checkable
|
||||
self.checked = checked
|
||||
self.group = group
|
||||
self.settings = settings
|
||||
self.enabled = enabled
|
||||
self.menu_item = menu_item
|
||||
self.menu_id = menu_id
|
||||
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']
|
||||
return self.id == other.id
|
||||
|
||||
def __str__(self):
|
||||
return self.id
|
||||
|
||||
def on_restore_defaults(self):
|
||||
if self.qaction:
|
||||
|
|
@ -50,7 +66,7 @@ class Action(dict):
|
|||
if isinstance(menu_item['items'], list):
|
||||
# This is a normal menu
|
||||
for item in menu_item['items']:
|
||||
if item == self['id']:
|
||||
if item == self.id:
|
||||
path.append(menu_item['menu'])
|
||||
return True
|
||||
if isinstance(item, dict):
|
||||
|
|
@ -58,7 +74,7 @@ class Action(dict):
|
|||
if _get_path(item):
|
||||
path.append(menu_item['menu'])
|
||||
return True
|
||||
elif menu_item['items'] == self.get('menu_id'):
|
||||
elif menu_item['items'] == self.menu_id:
|
||||
# This is a dynamic submenu (e.g. Recent Files)
|
||||
path.append(menu_item['menu'])
|
||||
return True
|
||||
|
|
@ -69,13 +85,13 @@ class Action(dict):
|
|||
return path[::-1]
|
||||
|
||||
def get_shortcuts(self):
|
||||
return self.kb_settings.get_shortcuts(
|
||||
'Actions', self['id'], self.get('shortcuts'))
|
||||
return self.kb_settings.get_list(
|
||||
self.SETTINGS_GROUP, self.id, self.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'))
|
||||
logger.debug(f'Setting shortcut "{self.id}" to: {value}')
|
||||
self.kb_settings.set_list(
|
||||
self.SETTINGS_GROUP, self.id, value, self.shortcuts)
|
||||
if self.qaction:
|
||||
self.qaction.setShortcuts(value)
|
||||
|
||||
|
|
@ -88,348 +104,335 @@ class Action(dict):
|
|||
|
||||
def shortcuts_changed(self):
|
||||
"""Whether shortcuts have changed from their defaults."""
|
||||
return self.get_shortcuts() != self.get('shortcuts', [])
|
||||
return self.get_shortcuts() != self.shortcuts
|
||||
|
||||
def get_default_shortcut(self, index):
|
||||
try:
|
||||
return self.get('shortcuts', [])[index]
|
||||
return self.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': 'show_color_gamut',
|
||||
'text': 'Show &Color Gamut',
|
||||
'callback': 'on_action_show_color_gamut',
|
||||
'group': 'active_when_single_image',
|
||||
}),
|
||||
Action({
|
||||
'id': 'sample_color',
|
||||
'text': 'Sample Color',
|
||||
'shortcuts': ['S'],
|
||||
'callback': 'on_action_sample_color',
|
||||
'group': 'active_when_items_in_scene',
|
||||
}),
|
||||
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',
|
||||
'callback': 'on_action_open_settings_dir',
|
||||
}),
|
||||
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='show_color_gamut',
|
||||
text='Show &Color Gamut',
|
||||
callback='on_action_show_color_gamut',
|
||||
group='active_when_single_image',
|
||||
),
|
||||
Action(
|
||||
id='sample_color',
|
||||
text='Sample Color',
|
||||
shortcuts=['S'],
|
||||
callback='on_action_sample_color',
|
||||
group='active_when_items_in_scene',
|
||||
),
|
||||
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 && Mouse',
|
||||
callback='on_action_keyboard_settings',
|
||||
),
|
||||
Action(
|
||||
id='open_settings_dir',
|
||||
text='&Open Settings Folder',
|
||||
callback='on_action_open_settings_dir',
|
||||
),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -55,10 +55,10 @@ class ActionsMixin:
|
|||
|
||||
def _init_action_checkable(self, actiondef, qaction):
|
||||
qaction.setCheckable(True)
|
||||
callback = getattr(self, actiondef['callback'])
|
||||
callback = getattr(self, actiondef.callback)
|
||||
qaction.toggled.connect(callback)
|
||||
settings_key = actiondef.get('settings')
|
||||
checked = actiondef.get('checked', False)
|
||||
settings_key = actiondef.settings
|
||||
checked = actiondef.checked
|
||||
qaction.setChecked(checked)
|
||||
if settings_key:
|
||||
val = self.settings.value(settings_key, checked, type=bool)
|
||||
|
|
@ -69,18 +69,18 @@ class ActionsMixin:
|
|||
|
||||
def _create_actions(self):
|
||||
for action in actions.values():
|
||||
qaction = QtGui.QAction(action['text'], self)
|
||||
qaction = QtGui.QAction(action.text, self)
|
||||
shortcuts = action.get_shortcuts()
|
||||
if shortcuts:
|
||||
qaction.setShortcuts(shortcuts)
|
||||
if action.get('checkable', False):
|
||||
if action.checkable:
|
||||
self._init_action_checkable(action, qaction)
|
||||
else:
|
||||
qaction.triggered.connect(getattr(self, action['callback']))
|
||||
qaction.triggered.connect(getattr(self, action.callback))
|
||||
self.addAction(qaction)
|
||||
qaction.setEnabled(action.get('enabled', True))
|
||||
if 'group' in action:
|
||||
self.bee_actiongroups[action['group']].append(qaction)
|
||||
qaction.setEnabled(action.enabled)
|
||||
if action.group:
|
||||
self.bee_actiongroups[action.group].append(qaction)
|
||||
qaction.setEnabled(False)
|
||||
action.qaction = qaction
|
||||
|
||||
|
|
@ -112,10 +112,10 @@ class ActionsMixin:
|
|||
for i in range(10):
|
||||
action_id = f'recent_files_{i}'
|
||||
key = 0 if i == 9 else i + 1
|
||||
action = Action({'id': action_id,
|
||||
'menu_id': '_build_recent_files',
|
||||
'text': f'File {i + 1}',
|
||||
'shortcuts': [f'Ctrl+{key}']})
|
||||
action = Action(id=action_id,
|
||||
menu_id='_build_recent_files',
|
||||
text=f'File {i + 1}',
|
||||
shortcuts=[f'Ctrl+{key}'])
|
||||
actions[action_id] = action
|
||||
|
||||
if i < len(files):
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ class InsertItems(QtGui.QUndoCommand):
|
|||
self.ignore_first_redo = ignore_first_redo
|
||||
|
||||
def redo(self):
|
||||
self.scene.deselect_all_items()
|
||||
if self.ignore_first_redo:
|
||||
self.ignore_first_redo = False
|
||||
return
|
||||
|
||||
self.scene.deselect_all_items()
|
||||
if self.position:
|
||||
self.old_positions = []
|
||||
rect = self.scene.itemsBoundingRect(items=self.items)
|
||||
|
|
|
|||
91
beeref/config/__init__.py
Normal file
91
beeref/config/__init__.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# This file is part of BeeRef.
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BeeRef is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Handling of command line args and Qt settings."""
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
import os.path
|
||||
|
||||
from PyQt6 import QtCore
|
||||
|
||||
from beeref import constants
|
||||
from beeref.config.controls import KeyboardSettings # noqa F401
|
||||
from beeref.config.settings import ( # noqa F401
|
||||
BeeSettings,
|
||||
CommandlineArgs,
|
||||
settings_events,
|
||||
)
|
||||
from beeref.logging import qt_message_handler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def logfile_name():
|
||||
return os.path.join(
|
||||
os.path.dirname(BeeSettings().fileName()), f'{constants.APPNAME}.log')
|
||||
|
||||
|
||||
logging_conf = {
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': ('{asctime} {name} {process:d} {thread:d} {message}'),
|
||||
'style': '{',
|
||||
},
|
||||
'simple': {
|
||||
'format': '{levelname} {name}: {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'simple',
|
||||
'level': CommandlineArgs().loglevel,
|
||||
},
|
||||
'file': {
|
||||
'class': 'beeref.logging.BeeRotatingFileHandler',
|
||||
'formatter': 'verbose',
|
||||
'filename': logfile_name(),
|
||||
'maxBytes': 1024 * 1000, # 1MB
|
||||
'backupCount': 1,
|
||||
'level': 'DEBUG',
|
||||
'delay': True,
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'beeref': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'TRACE',
|
||||
'propagate': False,
|
||||
},
|
||||
'Qt': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
}
|
||||
|
||||
logging.config.dictConfig(logging_conf)
|
||||
|
||||
# Redirect Qt logging to Python logger:
|
||||
QtCore.qInstallMessageHandler(qt_message_handler)
|
||||
336
beeref/config/controls.py
Normal file
336
beeref/config/controls.py
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
# This file is part of BeeRef.
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BeeRef is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Handling of keyboard shortcuts and mouse controls."""
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import cached_property
|
||||
import logging
|
||||
import logging.config
|
||||
import os.path
|
||||
|
||||
from beeref.config.settings import BeeSettings, settings_events
|
||||
from beeref.utils import ActionList
|
||||
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MouseConfigBase:
|
||||
MODIFIER_MAP = OrderedDict((
|
||||
('No Modifier', Qt.KeyboardModifier.NoModifier),
|
||||
('Shift', Qt.KeyboardModifier.ShiftModifier),
|
||||
('Ctrl', Qt.KeyboardModifier.ControlModifier),
|
||||
('Alt', Qt.KeyboardModifier.AltModifier),
|
||||
('Meta', Qt.KeyboardModifier.MetaModifier),
|
||||
('Keypad', Qt.KeyboardModifier.KeypadModifier),
|
||||
))
|
||||
|
||||
BUTTON_MAP = OrderedDict((
|
||||
('Not Configured', Qt.MouseButton.NoButton),
|
||||
('Left', Qt.MouseButton.LeftButton),
|
||||
('Middle', Qt.MouseButton.MiddleButton),
|
||||
))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.id == other.id
|
||||
|
||||
def __str__(self):
|
||||
return self.id
|
||||
|
||||
@cached_property
|
||||
def kb_settings(self):
|
||||
return KeyboardSettings()
|
||||
|
||||
def get_modifiers(self):
|
||||
return self.kb_settings.get_list(
|
||||
self.SETTINGS_GROUP, f'{self.id}_modifiers', self.modifiers)
|
||||
|
||||
def set_modifiers(self, value):
|
||||
logger.debug(
|
||||
f'Setting {self.SETTINGS_GROUP} modifiers '
|
||||
f'for "{self.id}" to: {value}')
|
||||
self.kb_settings.set_list(
|
||||
self.SETTINGS_GROUP, f'{self.id}_modifiers', value, self.modifiers)
|
||||
|
||||
def get_inverted(self):
|
||||
return self.kb_settings.get_value(
|
||||
self.SETTINGS_GROUP, f'{self.id}_inverted', self.inverted)
|
||||
|
||||
def set_inverted(self, value):
|
||||
logger.debug(
|
||||
f'Setting {self.SETTINGS_GROUP} inverted '
|
||||
f'for "{self.id}" to: {value}')
|
||||
self.kb_settings.set_value(
|
||||
self.SETTINGS_GROUP, f'{self.id}_inverted', value, self.inverted)
|
||||
|
||||
@classmethod
|
||||
def modifiers_to_qt(cls, modifiers):
|
||||
combined = cls.MODIFIER_MAP[modifiers[0]]
|
||||
for mod in modifiers[1:]:
|
||||
combined = combined | cls.MODIFIER_MAP[mod]
|
||||
return combined
|
||||
|
||||
|
||||
class MouseWheelConfig(MouseConfigBase):
|
||||
|
||||
SETTINGS_GROUP = 'MouseWheel'
|
||||
|
||||
def __init__(self, id, group, text, modifiers, invertible):
|
||||
self.id = id
|
||||
self.group = group
|
||||
self.text = text
|
||||
self.modifiers = modifiers
|
||||
self.invertible = invertible
|
||||
self.inverted = False
|
||||
|
||||
def controls_changed(self):
|
||||
"""Whether controls have changed from their defaults."""
|
||||
return (set(self.get_modifiers()) != set(self.modifiers)
|
||||
or self.get_inverted() != self.inverted)
|
||||
|
||||
def is_configured(self):
|
||||
"""Whether controls have been configured for this action."""
|
||||
return bool(self.get_modifiers())
|
||||
|
||||
def remove_controls(self):
|
||||
self.set_modifiers([])
|
||||
self.set_inverted(False)
|
||||
|
||||
def conflicts_with(self, other):
|
||||
"""Whether controls conflict with `other`.
|
||||
|
||||
For unconfigured controls, always return False."""
|
||||
return (self.is_configured()
|
||||
and other.is_configured()
|
||||
and set(self.get_modifiers()) == set(other.get_modifiers()))
|
||||
|
||||
def matches_event(self, event):
|
||||
if not self.is_configured():
|
||||
return False
|
||||
modifiers = self.get_modifiers()
|
||||
return self.modifiers_to_qt(modifiers) == event.modifiers()
|
||||
|
||||
|
||||
class MouseConfig(MouseConfigBase):
|
||||
|
||||
SETTINGS_GROUP = 'Mouse'
|
||||
|
||||
def __init__(self, id, group, text, button, modifiers, invertible):
|
||||
self.id = id
|
||||
self.group = group
|
||||
self.text = text
|
||||
self.button = button
|
||||
self.modifiers = modifiers
|
||||
self.invertible = invertible
|
||||
self.inverted = False
|
||||
|
||||
def get_button(self):
|
||||
return self.kb_settings.get_value(
|
||||
self.SETTINGS_GROUP, f'{self.id}_button', self.button)
|
||||
|
||||
def set_button(self, value):
|
||||
logger.debug(
|
||||
f'Setting {self.SETTINGS_GROUP} button '
|
||||
f'for "{self.id}" to: {value}')
|
||||
self.kb_settings.set_value(
|
||||
self.SETTINGS_GROUP, f'{self.id}_button', value, self.button)
|
||||
|
||||
def conflicts_with(self, other):
|
||||
"""Whether controls conflict with `other`.
|
||||
|
||||
For unconfigured controls, always return False.
|
||||
"""
|
||||
return (self.is_configured()
|
||||
and other.is_configured()
|
||||
and self.get_button() == other.get_button()
|
||||
and set(self.get_modifiers()) == set(other.get_modifiers()))
|
||||
|
||||
def controls_changed(self):
|
||||
"""Whether controls have changed from their defaults."""
|
||||
return (self.get_button() != self.button
|
||||
or set(self.get_modifiers()) != set(self.modifiers)
|
||||
or self.get_inverted() != self.inverted)
|
||||
|
||||
def is_configured(self):
|
||||
"""Whether controls have been configured for this action."""
|
||||
return self.get_button() != 'Not Configured'
|
||||
|
||||
def remove_controls(self):
|
||||
self.set_button('Not Configured')
|
||||
self.set_modifiers([])
|
||||
self.set_inverted(False)
|
||||
|
||||
def matches_event(self, event):
|
||||
if not self.is_configured():
|
||||
return False
|
||||
modifiers = self.get_modifiers()
|
||||
return (self.modifiers_to_qt(modifiers) == event.modifiers()
|
||||
and self.BUTTON_MAP[self.get_button()] == event.button())
|
||||
|
||||
|
||||
class KeyboardSettings(QtCore.QSettings):
|
||||
|
||||
MOUSEWHEEL_ACTIONS = ActionList([
|
||||
MouseWheelConfig(
|
||||
id='zoom1',
|
||||
group='zoom',
|
||||
text='Zoom',
|
||||
modifiers=('No Modifier',),
|
||||
invertible=True,
|
||||
),
|
||||
MouseWheelConfig(
|
||||
id='zoom2',
|
||||
group='zoom',
|
||||
text='Zoom (alternative)',
|
||||
modifiers=(),
|
||||
invertible=True,
|
||||
),
|
||||
MouseWheelConfig(
|
||||
id='pan_horizontal1',
|
||||
group='pan_horizontal',
|
||||
text='Pan horizontally',
|
||||
modifiers=('Shift',),
|
||||
invertible=True,
|
||||
),
|
||||
MouseWheelConfig(
|
||||
id='pan_horizontal2',
|
||||
group='pan_horizontal',
|
||||
text='Pan horizontally (alternative)',
|
||||
modifiers=(),
|
||||
invertible=True,
|
||||
),
|
||||
MouseWheelConfig(
|
||||
id='pan_vertical1',
|
||||
group='pan_vertical',
|
||||
text='Pan vertically',
|
||||
modifiers=('Shift', 'Ctrl'),
|
||||
invertible=True,
|
||||
),
|
||||
MouseWheelConfig(
|
||||
id='pan_vertical2',
|
||||
group='pan_vertical',
|
||||
text='Pan vertically (alternative)',
|
||||
modifiers=(),
|
||||
invertible=True,
|
||||
),
|
||||
])
|
||||
|
||||
MOUSE_ACTIONS = ActionList([
|
||||
MouseConfig(
|
||||
id='zoom1',
|
||||
group='zoom',
|
||||
text='Zoom',
|
||||
button='Middle',
|
||||
modifiers=('Ctrl',),
|
||||
invertible=True,
|
||||
),
|
||||
MouseConfig(
|
||||
id='zoom2',
|
||||
group='zoom',
|
||||
text='Zoom (alternative)',
|
||||
button='Not Configured',
|
||||
modifiers=(),
|
||||
invertible=True,
|
||||
),
|
||||
MouseConfig(
|
||||
id='pan1',
|
||||
group='pan',
|
||||
text='Pan',
|
||||
button='Middle',
|
||||
modifiers=('No Modifier',),
|
||||
invertible=False,
|
||||
),
|
||||
MouseConfig(
|
||||
id='pan2',
|
||||
group='pan',
|
||||
text='Pan (alternative)',
|
||||
button='Left',
|
||||
modifiers=('Alt',),
|
||||
invertible=False,
|
||||
),
|
||||
MouseConfig(
|
||||
id='movewindow1',
|
||||
group='movewindow',
|
||||
text='Move Window',
|
||||
button='Left',
|
||||
modifiers=('Ctrl', 'Alt'),
|
||||
invertible=False,
|
||||
),
|
||||
MouseConfig(
|
||||
id='movewindow2',
|
||||
group='movewindow (alternative)',
|
||||
text='Move Window',
|
||||
button='Not Configured',
|
||||
modifiers=(),
|
||||
invertible=False,
|
||||
),
|
||||
])
|
||||
|
||||
def __init__(self):
|
||||
settings_format = QtCore.QSettings.Format.IniFormat
|
||||
filename = os.path.join(
|
||||
os.path.dirname(BeeSettings().fileName()),
|
||||
'KeyboardSettings.ini')
|
||||
super().__init__(filename, settings_format)
|
||||
|
||||
def set_list(self, group, key, values, default=None):
|
||||
if values == default:
|
||||
self.remove(f'{group}/{key}')
|
||||
else:
|
||||
self.setValue(f'{group}/{key}', ', '.join(values))
|
||||
|
||||
def get_list(self, group, key, default=None):
|
||||
values = self.value(f'{group}/{key}')
|
||||
if values is not None:
|
||||
values = list(filter(lambda x: x, values.split(', ')))
|
||||
return values
|
||||
|
||||
return list(default or []) # Always return new instance of default
|
||||
|
||||
def get_value(self, group, key, default=None):
|
||||
value = self.value(f'{group}/{key}')
|
||||
return default if value is None else value
|
||||
|
||||
def set_value(self, group, key, value, default=None):
|
||||
if value == default:
|
||||
self.remove(f'{group}/{key}')
|
||||
else:
|
||||
self.setValue(f'{group}/{key}', value)
|
||||
|
||||
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 and mouse controls to defaults')
|
||||
for key in self.allKeys():
|
||||
self.remove(key)
|
||||
settings_events.restore_keyboard_defaults.emit()
|
||||
|
||||
def mousewheel_action_for_event(self, event):
|
||||
for action in self.MOUSEWHEEL_ACTIONS.values():
|
||||
if action.matches_event(event):
|
||||
return action.group, action.get_inverted()
|
||||
return None, None
|
||||
|
||||
def mouse_action_for_event(self, event):
|
||||
for action in self.MOUSE_ACTIONS.values():
|
||||
if action.matches_event(event):
|
||||
return action.group, action.get_inverted()
|
||||
return None, None
|
||||
|
|
@ -13,17 +13,13 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Handling of command line args and Qt settings."""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import logging.config
|
||||
import os.path
|
||||
|
||||
from PyQt6 import QtCore
|
||||
|
||||
from beeref import constants
|
||||
from beeref.logging import qt_message_handler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -142,7 +138,6 @@ class BeeSettings(QtCore.QSettings):
|
|||
'validate' are specified in the FIELDS entry for the given
|
||||
key. The default value will be returned if validation or type
|
||||
casting fails.
|
||||
|
||||
"""
|
||||
|
||||
val = self.value(key)
|
||||
|
|
@ -205,94 +200,3 @@ class BeeSettings(QtCore.QSettings):
|
|||
if existing_only:
|
||||
values = [f for f in values if os.path.exists(f)]
|
||||
return values
|
||||
|
||||
|
||||
class KeyboardSettings(QtCore.QSettings):
|
||||
|
||||
def __init__(self):
|
||||
settings_format = QtCore.QSettings.Format.IniFormat
|
||||
filename = os.path.join(
|
||||
os.path.dirname(BeeSettings().fileName()),
|
||||
'KeyboardSettings.ini')
|
||||
super().__init__(filename, settings_format)
|
||||
|
||||
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(', ')))
|
||||
return values
|
||||
|
||||
return list(default or []) # Always return new instance of default
|
||||
|
||||
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():
|
||||
return os.path.join(
|
||||
os.path.dirname(BeeSettings().fileName()), f'{constants.APPNAME}.log')
|
||||
|
||||
|
||||
logging_conf = {
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': ('{asctime} {name} {process:d} {thread:d} {message}'),
|
||||
'style': '{',
|
||||
},
|
||||
'simple': {
|
||||
'format': '{levelname} {name}: {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'simple',
|
||||
'level': CommandlineArgs().loglevel,
|
||||
},
|
||||
'file': {
|
||||
'class': 'beeref.logging.BeeRotatingFileHandler',
|
||||
'formatter': 'verbose',
|
||||
'filename': logfile_name(),
|
||||
'maxBytes': 1024 * 1000, # 1MB
|
||||
'backupCount': 1,
|
||||
'level': 'DEBUG',
|
||||
'delay': True,
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'beeref': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'TRACE',
|
||||
'propagate': False,
|
||||
},
|
||||
'Qt': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
}
|
||||
|
||||
logging.config.dictConfig(logging_conf)
|
||||
|
||||
# Redirect Qt logging to Python logger:
|
||||
QtCore.qInstallMessageHandler(qt_message_handler)
|
||||
|
|
@ -19,6 +19,8 @@ VERSION = '0.3.3-dev'
|
|||
WEBSITE = 'https://github.com/rbreu/beeref'
|
||||
COPYRIGHT = 'Copyright © 2021-2023 Rebecca Breu'
|
||||
|
||||
CHANGED_SYMBOL = '✎'
|
||||
|
||||
COLORS = {
|
||||
# Qt:
|
||||
'Active:Base': (60, 60, 60),
|
||||
|
|
@ -31,6 +33,10 @@ COLORS = {
|
|||
'Active:ButtonText': (200, 200, 200),
|
||||
'Active:Highlight': (83, 167, 165),
|
||||
'Active:Link': (90, 181, 179),
|
||||
|
||||
'Disabled:Base': (40, 40, 40),
|
||||
'Disabled:Window': (40, 40, 40, 50),
|
||||
'Disabled:WindowText': (120, 120, 120),
|
||||
'Disabled:Light': (0, 0, 0, 0),
|
||||
'Disabled:Text': (140, 140, 140),
|
||||
|
||||
|
|
|
|||
|
|
@ -45,9 +45,13 @@ def exif_rotated_image(path=None):
|
|||
logger.exception(f'Exif parser failed on image: {path}')
|
||||
return img
|
||||
|
||||
if 'orientation' in exifimg.list_all():
|
||||
orientation = exifimg.orientation
|
||||
else:
|
||||
try:
|
||||
if 'orientation' in exifimg.list_all():
|
||||
orientation = exifimg.orientation
|
||||
else:
|
||||
return img
|
||||
except NotImplementedError:
|
||||
logger.exception(f'Exif failed reading orientation of image: {path}')
|
||||
return img
|
||||
|
||||
transform = QtGui.QTransform()
|
||||
|
|
|
|||
|
|
@ -261,6 +261,7 @@ class SQLiteIO:
|
|||
if self.worker.canceled:
|
||||
break
|
||||
self.delete_items(to_delete)
|
||||
self.ex('VACUUM')
|
||||
self.connection.commit()
|
||||
if self.worker:
|
||||
self.worker.finished.emit(self.filename, [])
|
||||
|
|
|
|||
|
|
@ -114,9 +114,10 @@ class MainControlsMixin:
|
|||
self.exit_movewin_mode()
|
||||
event.accept()
|
||||
return True
|
||||
if (event.button() == Qt.MouseButton.LeftButton
|
||||
and event.modifiers() == (Qt.KeyboardModifier.ControlModifier
|
||||
| Qt.KeyboardModifier.AltModifier)):
|
||||
|
||||
action, inverted =\
|
||||
self.control_target.keyboard_settings.mouse_action_for_event(event)
|
||||
if action == 'movewindow':
|
||||
self.enter_movewin_mode()
|
||||
event.accept()
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -45,20 +45,22 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.max_z = 0
|
||||
self.min_z = 0
|
||||
self.Z_STEP = 0.001
|
||||
self.multi_select_item = MultiSelectItem()
|
||||
self.rubberband_item = RubberbandItem()
|
||||
self.selectionChanged.connect(self.on_selection_change)
|
||||
self.changed.connect(self.on_change)
|
||||
self.items_to_add = Queue()
|
||||
self.internal_clipboard = []
|
||||
self.edit_item = None
|
||||
self.crop_item = None
|
||||
self.settings = BeeSettings()
|
||||
self.clear()
|
||||
self._clear_ongoing = False
|
||||
|
||||
def clear(self):
|
||||
self._clear_ongoing = True
|
||||
super().clear()
|
||||
self.internal_clipboard = []
|
||||
self.rubberband_item = RubberbandItem()
|
||||
self.multi_select_item = MultiSelectItem()
|
||||
self._clear_ongoing = False
|
||||
|
||||
def addItem(self, item):
|
||||
logger.debug(f'Adding item {item}')
|
||||
|
|
@ -452,6 +454,10 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
return (rect.topLeft() + rect.bottomRight()) / 2
|
||||
|
||||
def on_selection_change(self):
|
||||
if self._clear_ongoing:
|
||||
# Ignore events while clearing the scene since the
|
||||
# multiselect item will get cleared, too
|
||||
return
|
||||
if self.has_multi_selection():
|
||||
self.multi_select_item.fit_selection_area(
|
||||
self.itemsBoundingRect(selection_only=True))
|
||||
|
|
@ -462,6 +468,10 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.removeItem(self.multi_select_item)
|
||||
|
||||
def on_change(self, region):
|
||||
if self._clear_ongoing:
|
||||
# Ignore events while clearing the scene since the
|
||||
# multiselect item will get cleared, too
|
||||
return
|
||||
if (self.multi_select_item.scene()
|
||||
and self.multi_select_item.active_mode is None):
|
||||
self.multi_select_item.fit_selection_area(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from collections import OrderedDict
|
||||
import re
|
||||
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
|
@ -93,3 +94,16 @@ def qcolor_to_hex(color):
|
|||
rgb = color.name()
|
||||
alpha = hex(color.alpha()).removeprefix('0x')
|
||||
return f'{rgb}{alpha}'
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from PyQt6.QtCore import Qt
|
|||
|
||||
from beeref.actions import ActionsMixin, actions
|
||||
from beeref import commands
|
||||
from beeref.config import CommandlineArgs, BeeSettings
|
||||
from beeref.config import CommandlineArgs, BeeSettings, KeyboardSettings
|
||||
from beeref import constants
|
||||
from beeref import fileio
|
||||
from beeref.fileio.export import exporter_registry
|
||||
|
|
@ -51,6 +51,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.app = app
|
||||
self.parent = parent
|
||||
self.settings = BeeSettings()
|
||||
self.keyboard_settings = KeyboardSettings()
|
||||
self.welcome_overlay = widgets.welcome_overlay.WelcomeOverlay(self)
|
||||
|
||||
self.setBackgroundBrush(
|
||||
|
|
@ -493,7 +494,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
widgets.settings.SettingsDialog(self)
|
||||
|
||||
def on_action_keyboard_settings(self):
|
||||
widgets.settings.KeyboardSettingsDialog(self)
|
||||
widgets.controls.ControlsDialog(self)
|
||||
|
||||
def on_action_help(self):
|
||||
widgets.HelpDialog(self)
|
||||
|
|
@ -540,6 +541,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
def do_insert_images(self, filenames, pos=None):
|
||||
if not pos:
|
||||
pos = self.get_view_center()
|
||||
self.scene.deselect_all_items()
|
||||
self.undo_stack.beginMacro('Insert Images')
|
||||
self.worker = fileio.ThreadedIO(
|
||||
fileio.load_images,
|
||||
|
|
@ -748,17 +750,23 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.reset_previous_transform()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
if event.modifiers() == Qt.KeyboardModifier.NoModifier:
|
||||
self.zoom(event.angleDelta().y(), event.position())
|
||||
action, inverted\
|
||||
= self.keyboard_settings.mousewheel_action_for_event(event)
|
||||
|
||||
delta = event.angleDelta().y()
|
||||
if inverted:
|
||||
delta = delta * -1
|
||||
|
||||
if action == 'zoom':
|
||||
self.zoom(delta, event.position())
|
||||
event.accept()
|
||||
return
|
||||
if event.modifiers() == (Qt.KeyboardModifier.ShiftModifier
|
||||
| Qt.KeyboardModifier.ControlModifier):
|
||||
self.pan(QtCore.QPointF(0, 0.5 * event.angleDelta().y()))
|
||||
if action == 'pan_horizontal':
|
||||
self.pan(QtCore.QPointF(0, 0.5 * delta))
|
||||
event.accept()
|
||||
return
|
||||
if event.modifiers() == Qt.KeyboardModifier.ShiftModifier:
|
||||
self.pan(QtCore.QPointF(0.5 * event.angleDelta().y(), 0))
|
||||
if action == 'pan_vertical':
|
||||
self.pan(QtCore.QPointF(0.5 * delta, 0))
|
||||
event.accept()
|
||||
return
|
||||
|
||||
|
|
@ -784,17 +792,17 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
event.accept()
|
||||
return
|
||||
|
||||
if (event.button() == Qt.MouseButton.MiddleButton
|
||||
and event.modifiers() == Qt.KeyboardModifier.ControlModifier):
|
||||
action, inverted = self.keyboard_settings.mouse_action_for_event(event)
|
||||
|
||||
if action == 'zoom':
|
||||
self.active_mode = self.ZOOM_MODE
|
||||
self.event_start = event.position()
|
||||
self.event_anchor = event.position()
|
||||
self.event_inverted = inverted
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if (event.button() == Qt.MouseButton.MiddleButton
|
||||
or (event.button() == Qt.MouseButton.LeftButton
|
||||
and event.modifiers() == Qt.KeyboardModifier.AltModifier)):
|
||||
if action == 'pan':
|
||||
logger.trace('Begin pan')
|
||||
self.active_mode = self.PAN_MODE
|
||||
self.event_start = event.position()
|
||||
|
|
@ -820,6 +828,8 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.reset_previous_transform()
|
||||
pos = event.position()
|
||||
delta = (self.event_start - pos).y()
|
||||
if self.event_inverted:
|
||||
delta *= -1
|
||||
self.event_start = pos
|
||||
self.zoom(delta * 20, self.event_anchor)
|
||||
event.accept()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ from PyQt6.QtCore import Qt
|
|||
|
||||
from beeref import constants, commands
|
||||
from beeref.config import logfile_name
|
||||
from beeref.widgets import settings, welcome_overlay, color_gamut # noqa: F401
|
||||
from beeref.widgets import ( # noqa: F401
|
||||
controls,
|
||||
settings,
|
||||
welcome_overlay,
|
||||
color_gamut,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -69,12 +74,12 @@ class HelpDialog(QtWidgets.QDialog):
|
|||
# Controls
|
||||
with open(os.path.join(docdir, 'controls.html')) as f:
|
||||
controls_txt = f.read()
|
||||
controls = QtWidgets.QLabel(controls_txt)
|
||||
controls.setTextInteractionFlags(
|
||||
controls_label = QtWidgets.QLabel(controls_txt)
|
||||
controls_label.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
scroll = QtWidgets.QScrollArea(self)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(controls)
|
||||
scroll.setWidget(controls_label)
|
||||
tabs.addTab(scroll, '&Controls')
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
|
|
|||
96
beeref/widgets/controls/__init__.py
Normal file
96
beeref/widgets/controls/__init__.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# This file is part of BeeRef.
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BeeRef is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from beeref.config import KeyboardSettings
|
||||
from beeref.widgets.controls.keyboard import KeyboardShortcutsView
|
||||
from beeref.widgets.controls.mouse import MouseView
|
||||
from beeref.widgets.controls.mousewheel import MouseWheelView
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ControlsDialog(QtWidgets.QDialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle('Keyboard & Mouse Controls')
|
||||
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(table.model().setFilterFixedString)
|
||||
kb_layout.addWidget(search_input)
|
||||
kb_layout.addWidget(table)
|
||||
tabs.addTab(keyboard, '&Keyboard Shortcuts')
|
||||
|
||||
# Mouse controls
|
||||
mouse = QtWidgets.QWidget(parent)
|
||||
mouse_layout = QtWidgets.QVBoxLayout()
|
||||
mouse.setLayout(mouse_layout)
|
||||
table = MouseView(mouse)
|
||||
search_input = QtWidgets.QLineEdit()
|
||||
search_input.setPlaceholderText('Search...')
|
||||
search_input.textChanged.connect(table.model().setFilterFixedString)
|
||||
mouse_layout.addWidget(search_input)
|
||||
mouse_layout.addWidget(table)
|
||||
tabs.addTab(mouse, '&Mouse')
|
||||
|
||||
# Mouse wheel controls
|
||||
mousewheel = QtWidgets.QWidget(parent)
|
||||
wheel_layout = QtWidgets.QVBoxLayout()
|
||||
mousewheel.setLayout(wheel_layout)
|
||||
table = MouseWheelView(mousewheel)
|
||||
search_input = QtWidgets.QLineEdit()
|
||||
search_input.setPlaceholderText('Search...')
|
||||
search_input.textChanged.connect(table.model().setFilterFixedString)
|
||||
wheel_layout.addWidget(search_input)
|
||||
wheel_layout.addWidget(table)
|
||||
tabs.addTab(mousewheel, 'Mouse &Wheel')
|
||||
|
||||
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 keyboard and mouse settings '
|
||||
'to their default values?')
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
KeyboardSettings().restore_defaults()
|
||||
262
beeref/widgets/controls/common.py
Normal file
262
beeref/widgets/controls/common.py
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# This file is part of BeeRef.
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BeeRef is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import partial
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref.config import KeyboardSettings
|
||||
from beeref import constants
|
||||
|
||||
|
||||
class MouseControlsEditorBase(QtWidgets.QDialog):
|
||||
"""Common code for MouseWheel and Mouse control editors."""
|
||||
|
||||
saved = QtCore.pyqtSignal()
|
||||
|
||||
def init_dialog(self, parent, index, actions, title):
|
||||
super().__init__(parent)
|
||||
self.actions = actions
|
||||
self.action = self.actions[index.row()]
|
||||
self.setWindowTitle(f'title {self.action.text}')
|
||||
self.old_modifiers = self.action.get_modifiers()
|
||||
self.remove_from_other = None
|
||||
self.ignore_on_changed = False
|
||||
self.setAutoFillBackground(True)
|
||||
self.layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.setModal(True)
|
||||
|
||||
def init_modifiers_input(self):
|
||||
group = QtWidgets.QGroupBox('Modifiers')
|
||||
group_layout = QtWidgets.QVBoxLayout()
|
||||
group.setLayout(group_layout)
|
||||
self.layout.addWidget(group)
|
||||
self.checkboxes = {}
|
||||
for mod in self.action.MODIFIER_MAP.keys():
|
||||
checkbox = QtWidgets.QCheckBox(mod)
|
||||
checkbox.setChecked(mod in self.old_modifiers)
|
||||
checkbox.stateChanged.connect(
|
||||
partial(self.on_modifiers_changed, mod))
|
||||
self.checkboxes[mod] = checkbox
|
||||
group_layout.addWidget(checkbox)
|
||||
|
||||
def init_button_row(self):
|
||||
buttons = QtWidgets.QDialogButtonBox(
|
||||
QtWidgets.QDialogButtonBox.StandardButton.Cancel
|
||||
| QtWidgets.QDialogButtonBox.StandardButton.Ok)
|
||||
buttons.accepted.connect(self.on_save)
|
||||
buttons.rejected.connect(self.reject)
|
||||
self.layout.addWidget(buttons)
|
||||
|
||||
def set_modifiers_no_modifier(self):
|
||||
"""Check 'No Modifiers', uncheck everything else."""
|
||||
for key, checkbox in self.checkboxes.items():
|
||||
checkbox.setChecked(key == 'No Modifier')
|
||||
|
||||
def on_modifiers_changed(self, modifier, value):
|
||||
"""Ensure that when 'No Modifiers' is checked, nothing else is
|
||||
checked at the same time.
|
||||
|
||||
If everything is unchecked, set 'No Modifiers' automatically.
|
||||
"""
|
||||
if self.ignore_on_changed:
|
||||
return
|
||||
|
||||
checked = value == Qt.CheckState.Checked.value
|
||||
self.ignore_on_changed = True
|
||||
|
||||
if checked and modifier == 'No Modifier':
|
||||
self.set_modifiers_no_modifier()
|
||||
|
||||
if checked and modifier != 'No Modifier':
|
||||
self.checkboxes['No Modifier'].setChecked(False)
|
||||
|
||||
if not checked and not self.get_modifiers(cleaned=False):
|
||||
self.set_modifiers_no_modifier()
|
||||
|
||||
self.ignore_on_changed = False
|
||||
|
||||
def get_modifiers(self, cleaned=True):
|
||||
modifiers = [key for key, checkbox in self.checkboxes.items()
|
||||
if checkbox.isChecked()]
|
||||
if cleaned and 'No Modifier' in modifiers:
|
||||
# In this case the list already should only have the one
|
||||
# entry, but just to make sure...
|
||||
return ['No Modifier']
|
||||
return modifiers
|
||||
|
||||
def set_modifiers(self, modifiers):
|
||||
for key, checkbox in self.checkboxes.items():
|
||||
checkbox.setChecked(key in modifiers)
|
||||
|
||||
def get_temp_action(self):
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
def reset_inputs(self):
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
def on_save(self):
|
||||
"""Don't let users save the same controls on different actions."""
|
||||
|
||||
temp = self.get_temp_action()
|
||||
self.remove_from_other = None
|
||||
for action in self.actions.values():
|
||||
if action == self.action:
|
||||
continue
|
||||
if action.conflicts_with(temp):
|
||||
msg = ('<p>These controls are already used for:</p>'
|
||||
f'<p>{action.text}</p>'
|
||||
'<p>Do you want to remove the other controls'
|
||||
' to save these ones?</p>')
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self, 'Save Controls?', msg)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
self.remove_from_other = action
|
||||
self.accept()
|
||||
self.saved.emit()
|
||||
else:
|
||||
self.reset_inputs()
|
||||
return
|
||||
self.accept()
|
||||
self.saved.emit()
|
||||
|
||||
|
||||
class MouseControlsModelBase(QtCore.QAbstractTableModel):
|
||||
COLUMNS = None
|
||||
COL_ACTION = 1
|
||||
COL_CHANGED = 2
|
||||
COL_BUTTON = 3
|
||||
COL_MODIFIERS = 4
|
||||
COL_INVERTED = 5
|
||||
|
||||
HEADERS = {
|
||||
COL_ACTION: 'Action',
|
||||
COL_CHANGED: constants.CHANGED_SYMBOL,
|
||||
COL_BUTTON: 'Button',
|
||||
COL_MODIFIERS: 'Modifiers',
|
||||
COL_INVERTED: 'Inverted',
|
||||
}
|
||||
|
||||
def __init__(self, actions):
|
||||
super().__init__()
|
||||
self.settings = KeyboardSettings()
|
||||
self.actions = actions
|
||||
|
||||
def rowCount(self, parent):
|
||||
return len(self.actions)
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(self.COLUMNS)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if (role == QtCore.Qt.ItemDataRole.DisplayRole
|
||||
and orientation == QtCore.Qt.Orientation.Horizontal):
|
||||
key = self.COLUMNS[section]
|
||||
return self.HEADERS[key]
|
||||
|
||||
def flags(self, index):
|
||||
key = self.COLUMNS[index.column()]
|
||||
base = (QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemNeverHasChildren)
|
||||
|
||||
if key in (self.COL_ACTION, self.COL_CHANGED):
|
||||
return base
|
||||
elif key in (self.COL_BUTTON, self.COL_MODIFIERS):
|
||||
return (base | QtCore.Qt.ItemFlag.ItemIsEditable)
|
||||
elif key == self.COL_INVERTED:
|
||||
action = self.actions[index.row()]
|
||||
if action.invertible and action.is_configured():
|
||||
return (base
|
||||
| QtCore.Qt.ItemFlag.ItemIsEditable
|
||||
| QtCore.Qt.ItemFlag.ItemIsUserCheckable)
|
||||
else:
|
||||
return base
|
||||
|
||||
def data(self, index, role):
|
||||
key = self.COLUMNS[index.column()]
|
||||
action = self.actions[index.row()]
|
||||
|
||||
if role in (QtCore.Qt.ItemDataRole.DisplayRole,
|
||||
QtCore.Qt.ItemDataRole.EditRole):
|
||||
if key == self.COL_ACTION:
|
||||
return action.text
|
||||
if key == self.COL_CHANGED and action.controls_changed():
|
||||
return constants.CHANGED_SYMBOL
|
||||
if key == self.COL_BUTTON:
|
||||
return action.get_button()
|
||||
if key == self.COL_MODIFIERS:
|
||||
return ' + '.join(action.get_modifiers())
|
||||
if key == self.COL_INVERTED:
|
||||
if not action.is_configured() or not action.invertible:
|
||||
return None
|
||||
return 'Yes' if action.get_inverted() else 'No'
|
||||
|
||||
if role == QtCore.Qt.ItemDataRole.ToolTipRole:
|
||||
changed = action.controls_changed()
|
||||
if not changed:
|
||||
return
|
||||
if key == self.COL_CHANGED:
|
||||
return 'Changed from default'
|
||||
if key == self.COL_BUTTON:
|
||||
if action.button == 'Not Configured':
|
||||
default = 'Not configured'
|
||||
else:
|
||||
default = action.button
|
||||
return f'Default: {default}'
|
||||
if key == self.COL_MODIFIERS:
|
||||
if not action.modifiers:
|
||||
default = 'Not configured'
|
||||
else:
|
||||
default = ' + '.join(action.modifiers)
|
||||
return f'Default: {default}'
|
||||
if key == self.COL_INVERTED and action.invertible:
|
||||
default = 'Yes' if action.inverted else 'No'
|
||||
return f'Default: {default}'
|
||||
|
||||
if role == QtCore.Qt.ItemDataRole.CheckStateRole:
|
||||
if (key == self.COL_INVERTED
|
||||
and action.is_configured()
|
||||
and action.invertible):
|
||||
return (Qt.CheckState.Checked if action.get_inverted()
|
||||
else Qt.CheckState.Unchecked)
|
||||
|
||||
def set_data_on_action(self, action, value):
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
def setData(self, index, value, role, remove_from_other=None):
|
||||
key = self.COLUMNS[index.column()]
|
||||
action = self.actions[index.row()]
|
||||
if key == self.COL_INVERTED:
|
||||
action.set_inverted(
|
||||
True if value == Qt.CheckState.Checked.value else False)
|
||||
else:
|
||||
self.set_data_on_action(action, value)
|
||||
if remove_from_other:
|
||||
# These controls has conflicts with another action and the
|
||||
# user chose to remove the other controls
|
||||
remove_from_other.remove_controls()
|
||||
row = list(self.actions.keys()).index(remove_from_other.id)
|
||||
self.dataChanged.emit(
|
||||
self.index(row, 0),
|
||||
self.index(row, self.columnCount(None) - 1))
|
||||
|
||||
# Whole row might be affected, so excpliclity emit dataChanged
|
||||
self.dataChanged.emit(
|
||||
self.index(index.row(), 0),
|
||||
self.index(index.row(), self.columnCount(None) - 1))
|
||||
|
||||
return True
|
||||
201
beeref/widgets/controls/keyboard.py
Normal file
201
beeref/widgets/controls/keyboard.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# This file is part of BeeRef.
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BeeRef is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
|
||||
from beeref import constants
|
||||
from beeref.actions.actions import actions
|
||||
from beeref.config import KeyboardSettings, settings_events
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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):
|
||||
"""Don't let users save the same shortcuts on different actions."""
|
||||
|
||||
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', constants.CHANGED_SYMBOL, '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 constants.CHANGED_SYMBOL
|
||||
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 'Not set'
|
||||
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 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)
|
||||
self.setAlternatingRowColors(True)
|
||||
settings_events.restore_defaults.connect(
|
||||
self.on_restore_defaults)
|
||||
|
||||
def on_restore_defaults(self):
|
||||
self.viewport().update()
|
||||
170
beeref/widgets/controls/mouse.py
Normal file
170
beeref/widgets/controls/mouse.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# This file is part of BeeRef.
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BeeRef is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
|
||||
from beeref.config import KeyboardSettings, settings_events
|
||||
from beeref.config.controls import MouseConfig
|
||||
from beeref.widgets.controls.common import (
|
||||
MouseControlsEditorBase,
|
||||
MouseControlsModelBase,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MouseControlsEditor(MouseControlsEditorBase):
|
||||
|
||||
def __init__(self, parent, index):
|
||||
self.init_dialog(parent, index, KeyboardSettings.MOUSE_ACTIONS,
|
||||
'Mouse Controls for:')
|
||||
self.old_button = self.action.get_button()
|
||||
|
||||
self.layout.addWidget(QtWidgets.QLabel('Mouse Button:'))
|
||||
self.button_input = QtWidgets.QComboBox(parent=parent)
|
||||
self.button_input.insertItems(0, self.action.BUTTON_MAP.keys())
|
||||
values = list(self.action.BUTTON_MAP.keys())
|
||||
self.button_input.setCurrentIndex(values.index(self.old_button))
|
||||
self.layout.addWidget(self.button_input)
|
||||
|
||||
self.init_modifiers_input()
|
||||
self.init_button_row()
|
||||
self.on_button_changed()
|
||||
self.button_input.currentIndexChanged.connect(self.on_button_changed)
|
||||
self.show()
|
||||
|
||||
def on_button_changed(self):
|
||||
"""Disable modifier inputs when no button configured; enable
|
||||
otherwise.
|
||||
"""
|
||||
self.ignore_on_changed = True
|
||||
if self.get_button() == 'Not Configured':
|
||||
for key, checkbox in self.checkboxes.items():
|
||||
checkbox.setChecked(False)
|
||||
self.set_modifiers_enabled(False)
|
||||
else:
|
||||
if not self.get_modifiers(cleaned=False):
|
||||
self.set_modifiers_no_modifier()
|
||||
self.set_modifiers_enabled(True)
|
||||
|
||||
self.ignore_on_changed = False
|
||||
|
||||
def set_modifiers_enabled(self, enabled):
|
||||
for key, checkbox in self.checkboxes.items():
|
||||
checkbox.setEnabled(enabled)
|
||||
|
||||
def get_button(self):
|
||||
values = list(self.action.BUTTON_MAP.keys())
|
||||
return values[self.button_input.currentIndex()]
|
||||
|
||||
def set_button(self, value):
|
||||
values = list(self.action.BUTTON_MAP.keys())
|
||||
self.button_input.setCurrentIndex(values.index(value))
|
||||
|
||||
def get_modifiers(self, cleaned=True):
|
||||
if cleaned and self.get_button() == 'Not Configured':
|
||||
# In this case the list should already be empty but just
|
||||
# to make sure...
|
||||
return []
|
||||
return super().get_modifiers(cleaned=True)
|
||||
|
||||
def get_temp_action(self):
|
||||
return MouseConfig(button=self.get_button(),
|
||||
modifiers=self.get_modifiers(),
|
||||
group=None, text=None, invertible=None, id=None)
|
||||
|
||||
def reset_inputs(self):
|
||||
self.set_button(self.old_button)
|
||||
self.set_modifiers(self.old_modifiers)
|
||||
|
||||
|
||||
class MouseDelegate(QtWidgets.QStyledItemDelegate):
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
widget = QtWidgets.QWidget(parent)
|
||||
widget.editor = MouseControlsEditor(widget, index)
|
||||
widget.editor.saved.connect(
|
||||
partial(self.setModelData, widget, index.model(), index))
|
||||
return widget
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
editor = editor.editor
|
||||
if editor.result() == QtWidgets.QDialog.DialogCode.Accepted:
|
||||
model.setData(
|
||||
index,
|
||||
{'button': editor.get_button(),
|
||||
'modifiers': editor.get_modifiers()},
|
||||
QtCore.Qt.ItemDataRole.EditRole,
|
||||
remove_from_other=editor.remove_from_other)
|
||||
|
||||
|
||||
class MouseModel(MouseControlsModelBase):
|
||||
"""An entry in the keyboard shortcuts table."""
|
||||
|
||||
COLUMNS = (MouseControlsModelBase.COL_ACTION,
|
||||
MouseControlsModelBase.COL_CHANGED,
|
||||
MouseControlsModelBase.COL_BUTTON,
|
||||
MouseControlsModelBase.COL_MODIFIERS,
|
||||
MouseControlsModelBase.COL_INVERTED)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(KeyboardSettings.MOUSE_ACTIONS)
|
||||
|
||||
def set_data_on_action(self, action, value):
|
||||
action.set_button(value['button'])
|
||||
action.set_modifiers(value['modifiers'])
|
||||
|
||||
|
||||
class MouseProxy(QtCore.QSortFilterProxyModel):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setSourceModel(MouseModel())
|
||||
self.setFilterCaseSensitivity(
|
||||
QtCore.Qt.CaseSensitivity.CaseInsensitive)
|
||||
|
||||
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 MouseView(QtWidgets.QTableView):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setMinimumSize(QtCore.QSize(400, 200))
|
||||
self.setItemDelegate(MouseDelegate())
|
||||
self.setShowGrid(False)
|
||||
self.setModel(MouseProxy())
|
||||
self.horizontalHeader().setSectionResizeMode(
|
||||
0, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
||||
self.horizontalHeader().setSectionResizeMode(
|
||||
1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.setSelectionMode(
|
||||
QtWidgets.QHeaderView.SelectionMode.SingleSelection)
|
||||
self.setAlternatingRowColors(True)
|
||||
settings_events.restore_defaults.connect(
|
||||
self.on_restore_defaults)
|
||||
|
||||
def on_restore_defaults(self):
|
||||
self.viewport().update()
|
||||
120
beeref/widgets/controls/mousewheel.py
Normal file
120
beeref/widgets/controls/mousewheel.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# This file is part of BeeRef.
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BeeRef is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
|
||||
from beeref.config import KeyboardSettings, settings_events
|
||||
from beeref.config.controls import MouseWheelConfig
|
||||
from beeref.widgets.controls.common import (
|
||||
MouseControlsEditorBase,
|
||||
MouseControlsModelBase,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MouseWheelModifiersEditor(MouseControlsEditorBase):
|
||||
|
||||
def __init__(self, parent, index):
|
||||
self.init_dialog(parent, index, KeyboardSettings.MOUSEWHEEL_ACTIONS,
|
||||
'MouseWheel Controls for:')
|
||||
self.init_modifiers_input()
|
||||
self.init_button_row()
|
||||
self.show()
|
||||
|
||||
def get_temp_action(self):
|
||||
return MouseWheelConfig(
|
||||
modifiers=self.get_modifiers(),
|
||||
group=None, text=None, invertible=None, id=None)
|
||||
|
||||
def reset_inputs(self):
|
||||
self.set_modifiers(self.old_modifiers)
|
||||
|
||||
|
||||
class MouseWheelDelegate(QtWidgets.QStyledItemDelegate):
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
widget = QtWidgets.QWidget(parent)
|
||||
widget.editor = MouseWheelModifiersEditor(widget, index)
|
||||
widget.editor.saved.connect(
|
||||
partial(self.setModelData, widget, index.model(), index))
|
||||
return widget
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
editor = editor.editor
|
||||
if editor.result() == QtWidgets.QDialog.DialogCode.Accepted:
|
||||
model.setData(
|
||||
index,
|
||||
editor.get_modifiers(),
|
||||
QtCore.Qt.ItemDataRole.EditRole,
|
||||
remove_from_other=editor.remove_from_other)
|
||||
|
||||
|
||||
class MouseWheelModel(MouseControlsModelBase):
|
||||
"""An entry in the keyboard shortcuts table."""
|
||||
|
||||
COLUMNS = (MouseControlsModelBase.COL_ACTION,
|
||||
MouseControlsModelBase.COL_CHANGED,
|
||||
MouseControlsModelBase.COL_MODIFIERS,
|
||||
MouseControlsModelBase.COL_INVERTED)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(KeyboardSettings.MOUSEWHEEL_ACTIONS)
|
||||
|
||||
def set_data_on_action(self, action, value):
|
||||
action.set_modifiers(value)
|
||||
|
||||
|
||||
class MouseWheelProxy(QtCore.QSortFilterProxyModel):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setSourceModel(MouseWheelModel())
|
||||
self.setFilterCaseSensitivity(
|
||||
QtCore.Qt.CaseSensitivity.CaseInsensitive)
|
||||
|
||||
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 MouseWheelView(QtWidgets.QTableView):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setMinimumSize(QtCore.QSize(400, 200))
|
||||
self.setItemDelegate(MouseWheelDelegate())
|
||||
self.setShowGrid(False)
|
||||
self.setModel(MouseWheelProxy())
|
||||
self.horizontalHeader().setSectionResizeMode(
|
||||
0, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
||||
self.horizontalHeader().setSectionResizeMode(
|
||||
1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.setSelectionMode(
|
||||
QtWidgets.QHeaderView.SelectionMode.SingleSelection)
|
||||
self.setAlternatingRowColors(True)
|
||||
settings_events.restore_defaults.connect(
|
||||
self.on_restore_defaults)
|
||||
|
||||
def on_restore_defaults(self):
|
||||
self.viewport().update()
|
||||
|
|
@ -16,19 +16,15 @@
|
|||
from functools import partial
|
||||
import logging
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from beeref import constants
|
||||
from beeref.actions.actions import actions
|
||||
from beeref.config import BeeSettings, KeyboardSettings, settings_events
|
||||
from beeref.config import BeeSettings, settings_events
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CHANGED_SYMBOL = '✎'
|
||||
|
||||
|
||||
class GroupBase(QtWidgets.QGroupBox):
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -47,7 +43,7 @@ class GroupBase(QtWidgets.QGroupBox):
|
|||
def update_title(self):
|
||||
title = [self.TITLE]
|
||||
if self.settings.value_changed(self.KEY):
|
||||
title.append(CHANGED_SYMBOL)
|
||||
title.append(constants.CHANGED_SYMBOL)
|
||||
self.setTitle(' '.join(title))
|
||||
|
||||
def on_value_changed(self, value):
|
||||
|
|
@ -178,222 +174,3 @@ 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 CHANGED_SYMBOL
|
||||
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 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)
|
||||
self.setAlternatingRowColors(True)
|
||||
settings_events.restore_keyboard_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()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
[flake8]
|
||||
exclude = squashfs-root
|
||||
|
||||
[coverage:run]
|
||||
source = beeref
|
||||
|
||||
[tool:pytest]
|
||||
norecursedirs = squashfs-root
|
||||
addopts = --cov-report html --cov-config=setup.cfg
|
||||
2
setup.py
2
setup.py
|
|
@ -19,9 +19,11 @@ setup(
|
|||
'beeref',
|
||||
'beeref.actions',
|
||||
'beeref.assets',
|
||||
'beeref.config',
|
||||
'beeref.documentation',
|
||||
'beeref.fileio',
|
||||
'beeref.widgets',
|
||||
'beeref.widgets.controls',
|
||||
],
|
||||
entry_points={
|
||||
'gui_scripts': [
|
||||
|
|
|
|||
|
|
@ -2,30 +2,35 @@ from unittest.mock import patch
|
|||
|
||||
from PyQt6 import QtGui
|
||||
|
||||
from beeref.actions.actions import Action, ActionList
|
||||
from beeref.actions.actions import Action
|
||||
|
||||
|
||||
def test_action_str():
|
||||
action = Action(id='foo', text='Foo', shortcuts=['Ctrl+R'])
|
||||
assert str(action) == 'foo'
|
||||
|
||||
|
||||
def test_action_equals_true():
|
||||
action1 = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+R']})
|
||||
action2 = Action({'id': 'foo', 'text': 'Bar', 'shortcuts': ['Ctrl+F']})
|
||||
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']})
|
||||
action1 = Action(id='foo', text='Foo', shortcuts=['Ctrl+R'])
|
||||
action2 = Action(id='bar', text='Bar', shortcuts=['Ctrl+R'])
|
||||
assert not action1 == action2
|
||||
|
||||
|
||||
def test_action_on_restore_defaults(kbsettings, view):
|
||||
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+R']})
|
||||
action = Action(id='foo', text='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 = Action(id='foo', text='Foo')
|
||||
action.qaction = QtGui.QAction('foo', view)
|
||||
action.qaction.setShortcuts(['Ctrl+R'])
|
||||
action.on_restore_defaults()
|
||||
|
|
@ -33,105 +38,105 @@ def test_action_on_restore_defaults_when_no_defaults(kbsettings, view):
|
|||
|
||||
|
||||
def test_action_get_overwritten_shortcuts(kbsettings):
|
||||
kbsettings.set_shortcuts('Actions', 'foo', ['Alt+O'])
|
||||
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
kbsettings.set_list('Actions', 'foo', ['Alt+O'])
|
||||
action = Action(id='foo', text='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']})
|
||||
action = Action(id='foo', text='Foo', shortcuts=['Ctrl+F'])
|
||||
assert action.get_shortcuts() == ['Ctrl+F']
|
||||
|
||||
|
||||
def test_action_set_shortcuts_when_no_qaction(kbsettings):
|
||||
action = Action({'id': 'foo'})
|
||||
action = Action(id='foo', text='Foo')
|
||||
action.qaction = None
|
||||
action.set_shortcuts(['Ctrl+F'])
|
||||
assert kbsettings.get_shortcuts('Actions', 'foo') == ['Ctrl+F']
|
||||
assert kbsettings.get_list('Actions', 'foo') == ['Ctrl+F']
|
||||
|
||||
|
||||
def test_action_set_shortcuts_when_qaction(kbsettings, view):
|
||||
action = Action({'id': 'foo'})
|
||||
action = Action(id='foo', text='Foo')
|
||||
action.qaction = QtGui.QAction('foo', view)
|
||||
action.set_shortcuts(['Ctrl+F'])
|
||||
assert kbsettings.get_shortcuts('Actions', 'foo') == ['Ctrl+F']
|
||||
assert kbsettings.get_list('Actions', 'foo') == ['Ctrl+F']
|
||||
assert action.qaction.shortcuts() == ['Ctrl+F']
|
||||
|
||||
|
||||
def test_action_get_qkeysequence_first(kbsettings):
|
||||
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
action = Action(id='foo', text='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']})
|
||||
action = Action(id='foo', text='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'})
|
||||
action = Action(id='foo', text='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']})
|
||||
action = Action(id='foo', text='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']})
|
||||
action = Action(id='foo', text='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'])
|
||||
action = Action(id='foo', text='Foo', shortcuts=['Ctrl+F'])
|
||||
kbsettings.set_list('Actions', 'foo', ['Ctrl+B'])
|
||||
assert action.shortcuts_changed() is True
|
||||
|
||||
|
||||
def test_action_shortcuts_changed_when_empty(kbsettings):
|
||||
action = Action({'id': 'foo'})
|
||||
action = Action(id='foo', text='Foo')
|
||||
assert action.shortcuts_changed() is False
|
||||
|
||||
|
||||
def test_action_get_default_shortcut_first():
|
||||
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
action = Action(id='foo', text='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']})
|
||||
action = Action(id='foo', text='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'})
|
||||
action = Action(id='foo', text='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']})
|
||||
action = Action(id='foo', text='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'})
|
||||
action = Action(id='baz', text='Foo')
|
||||
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'})
|
||||
action = Action(id='baz', text='Foo')
|
||||
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'})
|
||||
action = Action(id='baz', text='Foo', menu_id='_build_recent_files')
|
||||
assert action.menu_path == ['Foo']
|
||||
|
||||
|
||||
|
|
@ -139,21 +144,5 @@ def test_action_menu_path_recent_files():
|
|||
[{'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'})
|
||||
action = Action(id='baz', text='Foo', 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
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ 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.actions import Action
|
||||
from beeref.actions.menu_structure import MENU_SEPARATOR
|
||||
from beeref.utils import ActionList
|
||||
|
||||
|
||||
class FooWidget(QtWidgets.QWidget, ActionsMixin):
|
||||
|
|
@ -28,13 +29,13 @@ class FooWidget(QtWidgets.QWidget, ActionsMixin):
|
|||
@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')
|
||||
ActionList([Action(
|
||||
id='foo',
|
||||
text='&Foo',
|
||||
shortcuts=['Ctrl+F'],
|
||||
callback='on_foo',
|
||||
)]))
|
||||
@patch('beeref.config.KeyboardSettings.get_list')
|
||||
def test_create_actions(kb_mock, toggle_mock, trigger_mock, qapp):
|
||||
kb_mock.side_effect = lambda group, key, default: default
|
||||
widget = FooWidget()
|
||||
|
|
@ -56,15 +57,15 @@ def test_create_actions(kb_mock, toggle_mock, trigger_mock, qapp):
|
|||
[{'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'})])):
|
||||
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'])
|
||||
kbsettings.set_list('Actions', 'foo', ['Alt+O'])
|
||||
widget = FooWidget()
|
||||
widget.build_menu_and_actions()
|
||||
qaction = widget.actions()[0]
|
||||
|
|
@ -76,12 +77,12 @@ def test_create_actions_with_shortcut_from_settings(qapp, kbsettings):
|
|||
@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',
|
||||
})]))
|
||||
ActionList([Action(
|
||||
id='foo',
|
||||
text='&Foo',
|
||||
checkable=True,
|
||||
callback='on_foo',
|
||||
)]))
|
||||
def test_create_actions_checkable(toggle_mock, trigger_mock, qapp):
|
||||
widget = FooWidget()
|
||||
widget.build_menu_and_actions()
|
||||
|
|
@ -100,13 +101,13 @@ def test_create_actions_checkable(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,
|
||||
'checked': True,
|
||||
'callback': 'on_foo',
|
||||
})]))
|
||||
ActionList([Action(
|
||||
id='foo',
|
||||
text='&Foo',
|
||||
checkable=True,
|
||||
checked=True,
|
||||
callback='on_foo',
|
||||
)]))
|
||||
def test_create_actions_checkable_checked_true(
|
||||
toggle_mock, trigger_mock, qapp):
|
||||
widget = FooWidget()
|
||||
|
|
@ -127,13 +128,13 @@ def test_create_actions_checkable_checked_true(
|
|||
@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',
|
||||
})]))
|
||||
ActionList([Action(
|
||||
id='foo',
|
||||
text='&Foo',
|
||||
checkable=True,
|
||||
settings='foo/bar',
|
||||
callback='on_foo',
|
||||
)]))
|
||||
def test_create_actions_checkable_with_settings(
|
||||
toggle_mock, settings_mock, callback_mock, qapp):
|
||||
widget = FooWidget()
|
||||
|
|
@ -150,12 +151,12 @@ def test_create_actions_checkable_with_settings(
|
|||
@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',
|
||||
})]))
|
||||
ActionList([Action(
|
||||
id='foo',
|
||||
text='&Foo',
|
||||
callback='on_foo',
|
||||
group='bar',
|
||||
)]))
|
||||
def test_create_actions_with_group(qapp):
|
||||
widget = FooWidget()
|
||||
widget.build_menu_and_actions()
|
||||
|
|
@ -167,11 +168,11 @@ def test_create_actions_with_group(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',
|
||||
})]))
|
||||
ActionList([Action(
|
||||
id='foo',
|
||||
text='&Foo',
|
||||
callback='on_foo',
|
||||
)]))
|
||||
def test_build_menu_and_actions_with_actions(qapp):
|
||||
widget = FooWidget()
|
||||
with patch('PyQt6.QtWidgets.QMenu.addAction') as add_mock:
|
||||
|
|
@ -195,11 +196,11 @@ def test_build_menu_and_actions_with_separator(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',
|
||||
})]))
|
||||
ActionList([Action(
|
||||
id='foo',
|
||||
text='&Foo',
|
||||
callback='on_foo',
|
||||
)]))
|
||||
def test_build_menu_and_actions_with_submenu(qapp):
|
||||
widget = FooWidget()
|
||||
with patch('PyQt6.QtWidgets.QMenu.addAction') as add_mock:
|
||||
|
|
@ -216,18 +217,18 @@ def test_build_menu_and_actions_with_submenu(qapp):
|
|||
[{'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',
|
||||
}),
|
||||
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()
|
||||
|
|
@ -241,12 +242,12 @@ def test_actiongroup_set_enabled(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',
|
||||
})]))
|
||||
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
|
||||
|
|
@ -256,7 +257,7 @@ def test_build_menu_and_actions_disables_actiongroups(qapp):
|
|||
|
||||
|
||||
@patch('PyQt6.QtGui.QAction.triggered')
|
||||
@patch('beeref.config.KeyboardSettings.get_shortcuts')
|
||||
@patch('beeref.config.KeyboardSettings.get_list')
|
||||
@patch('beeref.actions.mixin.menu_structure',
|
||||
[{'menu': 'Foo', 'items': '_build_recent_files'}])
|
||||
@patch('beeref.actions.mixin.actions', ActionList([]))
|
||||
|
|
@ -294,7 +295,7 @@ def test_create_recent_files_more_than_10_files(
|
|||
@patch('beeref.actions.mixin.menu_structure',
|
||||
[{'menu': 'Foo', 'items': '_build_recent_files'}])
|
||||
@patch('beeref.actions.mixin.actions', ActionList([]))
|
||||
@patch('beeref.config.KeyboardSettings.get_shortcuts')
|
||||
@patch('beeref.config.KeyboardSettings.get_list')
|
||||
def test_create_recent_files_fewer_files_than_10_files(
|
||||
kb_mock, triggered_mock, qapp):
|
||||
kb_mock.side_effect = lambda group, key, default: default
|
||||
|
|
@ -329,7 +330,7 @@ def test_create_recent_files_fewer_files_than_10_files(
|
|||
@patch('beeref.actions.mixin.menu_structure',
|
||||
[{'menu': 'Foo', 'items': '_build_recent_files'}])
|
||||
@patch('beeref.actions.mixin.actions', ActionList([]))
|
||||
@patch('beeref.config.KeyboardSettings.get_shortcuts')
|
||||
@patch('beeref.config.KeyboardSettings.get_list')
|
||||
def test_create_recent_files_when_no_files(kb_mock, qapp):
|
||||
kb_mock.side_effect = lambda group, key, default: default
|
||||
widget = FooWidget()
|
||||
|
|
|
|||
0
tests/config/__init__.py
Normal file
0
tests/config/__init__.py
Normal file
510
tests/config/test_controls.py
Normal file
510
tests/config/test_controls.py
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref.config.controls import (
|
||||
KeyboardSettings,
|
||||
MouseConfig,
|
||||
MouseWheelConfig,
|
||||
)
|
||||
|
||||
|
||||
def test_mousewheelconfig_eq():
|
||||
action1 = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=False)
|
||||
action2 = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Baz', modifiers=[], invertible=False)
|
||||
assert action1 == action2
|
||||
|
||||
|
||||
def test_mousewheelconfig_str():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=False)
|
||||
assert str(action) == 'foo'
|
||||
|
||||
|
||||
def test_mousewheelconfig_kb_settings():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=False)
|
||||
assert isinstance(action.kb_settings, KeyboardSettings)
|
||||
|
||||
|
||||
def test_mousewheelconfig_get_modifiers(kbsettings):
|
||||
kbsettings.set_list('MouseWheel', 'foo_modifiers', ['Ctrl', 'Shift'])
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=False)
|
||||
assert action.get_modifiers() == ['Ctrl', 'Shift']
|
||||
|
||||
|
||||
def test_mousewheelconfig_get_modifiers_default(kbsettings):
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Shift'],
|
||||
invertible=False)
|
||||
assert action.get_modifiers() == ['Shift']
|
||||
|
||||
|
||||
def test_mousewheelconfig_set_modifiers(kbsettings):
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=False)
|
||||
action.set_modifiers(['Shift'])
|
||||
assert kbsettings.get_list('MouseWheel', 'foo_modifiers') == ['Shift']
|
||||
|
||||
|
||||
def test_mousewheelconfig_set_modifiers_default(kbsettings):
|
||||
kbsettings.set_list('MouseWheel', 'foo_modifiers', ['Alt', 'Ctrl'])
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo',
|
||||
modifiers=['Shift'], invertible=False)
|
||||
action.set_modifiers(['Shift'])
|
||||
assert kbsettings.value('MouseWheel/foo_modifiers') is None
|
||||
|
||||
|
||||
def test_mousewheelconfig_get_inverted(kbsettings):
|
||||
kbsettings.set_value('MouseWheel', 'foo_inverted', True)
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=True)
|
||||
assert action.get_inverted() is True
|
||||
|
||||
|
||||
def test_mousewheelconfig_get_inverted_default():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=True)
|
||||
assert action.get_inverted() is False
|
||||
|
||||
|
||||
def test_mousewheelconfig_set_inverted(kbsettings):
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=True)
|
||||
action.set_inverted(True)
|
||||
assert kbsettings.get_value('MouseWheel', 'foo_inverted') is True
|
||||
|
||||
|
||||
def test_mousewheelconfig_set_inverted_default(kbsettings):
|
||||
kbsettings.set_value('MouseWheel', 'foo_inverted', True)
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=True)
|
||||
action.set_inverted(False)
|
||||
assert kbsettings.value('MouseWheel/foo_inverted') is None
|
||||
|
||||
|
||||
def test_mousewheelconfig_modifiers_to_qt_multiple():
|
||||
assert MouseWheelConfig.modifiers_to_qt(['Shift', 'Ctrl']) == (
|
||||
Qt.KeyboardModifier.ShiftModifier
|
||||
| Qt.KeyboardModifier.ControlModifier)
|
||||
|
||||
|
||||
def test_mousewheelconfig_modifiers_to_single():
|
||||
assert MouseWheelConfig.modifiers_to_qt(['Alt']) ==\
|
||||
Qt.KeyboardModifier.AltModifier
|
||||
|
||||
|
||||
def test_mousewheelconfig_controls_changed_not_changed():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Shift'],
|
||||
invertible=True)
|
||||
assert action.controls_changed() is False
|
||||
|
||||
|
||||
def test_mousewheelconfig_controls_changed_modifiers_changed():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Shift'],
|
||||
invertible=True)
|
||||
action.set_modifiers(['Alt'])
|
||||
assert action.controls_changed() is True
|
||||
|
||||
|
||||
def test_mousewheelconfig_controls_changed_inverted_changed():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Shift'],
|
||||
invertible=True)
|
||||
action.set_inverted(True)
|
||||
assert action.controls_changed() is True
|
||||
|
||||
|
||||
def test_mousewheelconfig_controls_is_configured_true():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['No Modifiers'],
|
||||
invertible=True)
|
||||
assert action.is_configured() is True
|
||||
|
||||
|
||||
def test_mousewheelconfig_controls_is_configured_false():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[], invertible=True)
|
||||
assert action.is_configured() is False
|
||||
|
||||
|
||||
def test_mousewheelconfig_controls_remove_controls():
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Shift'],
|
||||
invertible=True)
|
||||
action.set_inverted(True)
|
||||
action.remove_controls()
|
||||
assert action.get_modifiers() == []
|
||||
assert action.get_inverted() is False
|
||||
|
||||
|
||||
def test_mousewheelconfig_conflicts_with_true():
|
||||
action1 = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Shift'],
|
||||
invertible=True)
|
||||
action2 = MouseWheelConfig(
|
||||
id='bar', group='foobar', text='Bar', modifiers=['Shift'],
|
||||
invertible=True)
|
||||
assert action1.conflicts_with(action2) is True
|
||||
|
||||
|
||||
def test_mousewheelconfig_conflicts_with_false():
|
||||
action1 = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Shift'],
|
||||
invertible=True)
|
||||
action2 = MouseWheelConfig(
|
||||
id='bar', group='foobar', text='Bar', modifiers=['Shift', 'Ctrl'],
|
||||
invertible=True)
|
||||
assert action1.conflicts_with(action2) is False
|
||||
|
||||
|
||||
def test_mousewheelconfig_conflicts_false_when_both_not_configured():
|
||||
action1 = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[],
|
||||
invertible=True)
|
||||
action2 = MouseWheelConfig(
|
||||
id='bar', group='foobar', text='Bar', modifiers=[],
|
||||
invertible=True)
|
||||
assert action1.conflicts_with(action2) is False
|
||||
|
||||
|
||||
def test_mousewheelconfig_matches_event_true():
|
||||
event = MagicMock(
|
||||
modifiers=MagicMock(return_value=Qt.KeyboardModifier.AltModifier))
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Alt'],
|
||||
invertible=True)
|
||||
assert action.matches_event(event) is True
|
||||
|
||||
|
||||
def test_mousewheelconfig_matches_event_false():
|
||||
event = MagicMock(
|
||||
modifiers=MagicMock(return_value=Qt.KeyboardModifier.AltModifier))
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
assert action.matches_event(event) is False
|
||||
|
||||
|
||||
def test_mousewheelconfig_matches_event_false_when_not_configured():
|
||||
event = MagicMock(
|
||||
modifiers=MagicMock(return_value=Qt.KeyboardModifier.AltModifier))
|
||||
action = MouseWheelConfig(
|
||||
id='foo', group='foobar', text='Foo', modifiers=[],
|
||||
invertible=True)
|
||||
assert action.matches_event(event) is False
|
||||
|
||||
|
||||
def test_mouseconfig_get_button(kbsettings):
|
||||
kbsettings.set_value('Mouse', 'foo_button', 'Left')
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle', modifiers=[],
|
||||
invertible=False)
|
||||
assert action.get_button() == 'Left'
|
||||
|
||||
|
||||
def test_mouseconfig_get_button_default():
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Left', modifiers=[],
|
||||
invertible=False)
|
||||
assert action.get_button() == 'Left'
|
||||
|
||||
|
||||
def test_mouseconfig_set_button(kbsettings):
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Left', modifiers=[],
|
||||
invertible=False)
|
||||
action.set_button('Middle')
|
||||
assert kbsettings.get_value('Mouse', 'foo_button') == 'Middle'
|
||||
|
||||
|
||||
def test_mouseconfig_set_button_default(kbsettings):
|
||||
kbsettings.set_value('Mouse', 'foo_button', 'Middle')
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle', modifiers=[],
|
||||
invertible=False)
|
||||
action.set_button('Middle')
|
||||
assert kbsettings.value('Mouse/foo_button') is None
|
||||
|
||||
|
||||
def test_mouseconfig_conflicts_with_true():
|
||||
action1 = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Left',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
action2 = MouseConfig(
|
||||
id='bar', group='foobar', text='Bar', button='Left',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
assert action1.conflicts_with(action2) is True
|
||||
|
||||
|
||||
def test_mouseconfig_conflicts_with_false_when_diff_buttons():
|
||||
action1 = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Left',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
action2 = MouseConfig(
|
||||
id='bar', group='foobar', text='Bar', button='Middle',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
assert action1.conflicts_with(action2) is False
|
||||
|
||||
|
||||
def test_mouseconfig_conflicts_with_false_when_diff_modifiers():
|
||||
action1 = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
action2 = MouseConfig(
|
||||
id='bar', group='foobar', text='Bar', button='Middle',
|
||||
modifiers=['Shift', 'Ctrl'], invertible=True)
|
||||
assert action1.conflicts_with(action2) is False
|
||||
|
||||
|
||||
def test_mouseconfig_conflicts_false_when_both_not_configured():
|
||||
action1 = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Not Configured',
|
||||
modifiers=[], invertible=True)
|
||||
action2 = MouseConfig(
|
||||
id='bar', group='foobar', text='Bar', button='Not Configured',
|
||||
modifiers=[], invertible=True)
|
||||
assert action1.conflicts_with(action2) is False
|
||||
|
||||
|
||||
def test_mouseconfig_is_configured_false():
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle',
|
||||
modifiers=[], invertible=True)
|
||||
action.set_button('Not Configured')
|
||||
assert action.conflicts_with(action) is False
|
||||
|
||||
|
||||
def test_mouseconfig_is_configured_true():
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Not Configured',
|
||||
modifiers=[], invertible=True)
|
||||
action.set_button('Left')
|
||||
assert action.conflicts_with(action) is True
|
||||
|
||||
|
||||
def test_mouseconfig_controls_changed_false():
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
action.controls_changed() is False
|
||||
|
||||
|
||||
def test_mouseconfig_controls_changed_true_when_button_changed():
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
action.set_button('Left')
|
||||
action.controls_changed() is True
|
||||
|
||||
|
||||
def test_mouseconfig_controls_changed_true_when_modifiers_changed():
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
action.set_modifiers(['Shift', 'Ctrl'])
|
||||
action.controls_changed() is True
|
||||
|
||||
|
||||
def test_mouseconfig_controls_changed_true_when_inverted_changed():
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
action.set_inverted(True)
|
||||
action.controls_changed() is True
|
||||
|
||||
|
||||
def test_mouseconfig_remove_controls():
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle',
|
||||
modifiers=['Shift'], invertible=True)
|
||||
action.set_inverted(True)
|
||||
|
||||
action.remove_controls()
|
||||
assert action.get_button() == 'Not Configured'
|
||||
assert action.get_modifiers() == []
|
||||
assert action.get_inverted() is False
|
||||
|
||||
|
||||
def test_mouseconfig_matches_event_true():
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.LeftButton),
|
||||
modifiers=MagicMock(return_value=Qt.KeyboardModifier.AltModifier))
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Left',
|
||||
modifiers=['Alt'], invertible=True)
|
||||
assert action.matches_event(event) is True
|
||||
|
||||
|
||||
def test_mouseconfig_matches_event_false_when_diff_button():
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.LeftButton),
|
||||
modifiers=MagicMock(return_value=Qt.KeyboardModifier.AltModifier))
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Middle',
|
||||
modifiers=['Alt'], invertible=True)
|
||||
assert action.matches_event(event) is False
|
||||
|
||||
|
||||
def test_mouseconfig_matches_event_false_when_diff_modifiers():
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.LeftButton),
|
||||
modifiers=MagicMock(return_value=Qt.KeyboardModifier.AltModifier))
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Left',
|
||||
modifiers=['Ctrl'], invertible=True)
|
||||
assert action.matches_event(event) is False
|
||||
|
||||
|
||||
def test_mouseconfig_matches_event_false_when_not_configured():
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.LeftButton),
|
||||
modifiers=MagicMock(return_value=Qt.KeyboardModifier.AltModifier))
|
||||
action = MouseConfig(
|
||||
id='foo', group='foobar', text='Foo', button='Not Configured',
|
||||
modifiers=[], invertible=True)
|
||||
assert action.matches_event(event) is False
|
||||
|
||||
|
||||
def test_keyboardsettings_set_value(kbsettings):
|
||||
kbsettings.set_value('mygroup', 'foo', 'bar')
|
||||
assert kbsettings.value('mygroup/foo', 'bar')
|
||||
|
||||
|
||||
def test_keyboardsettings_set_value_default_value(kbsettings):
|
||||
kbsettings.setValue('mygroup/foo', 'bar')
|
||||
kbsettings.set_value('mygroup', 'foo', 'baz', 'baz')
|
||||
assert kbsettings.value('mygroup/foo') is None
|
||||
|
||||
|
||||
def test_keyboardsettings_get_value_existing(kbsettings):
|
||||
kbsettings.set_value('mygroup', 'bar', 'foo')
|
||||
value = kbsettings.get_value('mygroup', 'bar', 'baz')
|
||||
assert value == 'foo'
|
||||
|
||||
|
||||
def test_keyboardsettings_get_value_default(kbsettings):
|
||||
assert kbsettings.get_value('mygroup', 'bar', 'baz') == 'baz'
|
||||
|
||||
|
||||
def test_keyboardsettings_set_list(kbsettings):
|
||||
kbsettings.set_list('mygroup', 'foo', ['Ctrl+F'])
|
||||
assert kbsettings.get_list('mygroup', 'foo') == ['Ctrl+F']
|
||||
|
||||
|
||||
def test_keyboardsettings_set_list_multiple(kbsettings):
|
||||
kbsettings.set_list('mygroup', 'foo', ['Ctrl+F', 'Alt+O'])
|
||||
assert kbsettings.get_list('mygroup', 'foo') == ['Ctrl+F', 'Alt+O']
|
||||
|
||||
|
||||
def test_keyboardsettings_set_list_default_value(kbsettings):
|
||||
kbsettings.setValue('mygroup/foo', 'Ctrl+F')
|
||||
kbsettings.set_list('mygroup', 'foo', 'Ctrl+B', 'Ctrl+B')
|
||||
assert kbsettings.value('mygroup/foo') is None
|
||||
|
||||
|
||||
def test_keyboardsettings_get_list_existing(kbsettings):
|
||||
kbsettings.set_list('mygroup', 'bar', ['Ctrl+R'])
|
||||
shortcuts = kbsettings.get_list('mygroup', 'bar', ['Ctrl+B'])
|
||||
assert shortcuts == ['Ctrl+R']
|
||||
|
||||
|
||||
def test_keyboardsettings_get_list_existing_empty_list(kbsettings):
|
||||
kbsettings.set_list('mygroup', 'bar', [])
|
||||
shortcuts = kbsettings.get_list('mygroup', 'bar', ['Ctrl+B'])
|
||||
assert shortcuts == []
|
||||
|
||||
|
||||
def test_keyboardsettings_get_list_default(kbsettings):
|
||||
shortcuts = kbsettings.get_list('mygroup', 'bar', ['Ctrl+B'])
|
||||
assert shortcuts == ['Ctrl+B']
|
||||
|
||||
|
||||
@patch('beeref.config.KeyboardSettings.setValue')
|
||||
@patch('beeref.config.KeyboardSettings.remove')
|
||||
def test_keyboardsettings_set_list_other_than_default_saves(
|
||||
remove_mock, set_mock, kbsettings):
|
||||
kbsettings.set_list('mygroup', 'bar', ['Ctrl+R'], ['Ctrl+Z'])
|
||||
set_mock.assert_called_once_with('mygroup/bar', 'Ctrl+R')
|
||||
remove_mock.assert_not_called()
|
||||
|
||||
|
||||
@patch('beeref.config.KeyboardSettings.setValue')
|
||||
@patch('beeref.config.KeyboardSettings.remove')
|
||||
def test_keyboardsettings_set_list_with_than_default_doesnt_save(
|
||||
remove_mock, set_mock, kbsettings):
|
||||
kbsettings.set_list('mygroup', 'bar', ['Ctrl+R'], ['Ctrl+R'])
|
||||
set_mock.assert_not_called()
|
||||
remove_mock.assert_called_once_with('mygroup/bar')
|
||||
|
||||
|
||||
@patch('PyQt6.QtGui.QAction.setShortcuts')
|
||||
def test_keyboardsettings_restore_defaults_restores(shortcut_mock, kbsettings):
|
||||
kbsettings.setValue('Actions/bar', 'Ctrl+R')
|
||||
kbsettings.restore_defaults()
|
||||
assert kbsettings.contains('Actions/bar') is False
|
||||
|
||||
|
||||
def test_keyboardsettings_mousewheel_action_for_event_finds(kbsettings):
|
||||
action = kbsettings.MOUSEWHEEL_ACTIONS['zoom1']
|
||||
action.set_modifiers(['Shift', 'Ctrl', 'Alt'])
|
||||
action.set_inverted(True)
|
||||
|
||||
event = MagicMock(
|
||||
modifiers=MagicMock(
|
||||
return_value=(Qt.KeyboardModifier.AltModifier
|
||||
| Qt.KeyboardModifier.ShiftModifier
|
||||
| Qt.KeyboardModifier.ControlModifier)))
|
||||
|
||||
group, inverted = kbsettings.mousewheel_action_for_event(event)
|
||||
assert group == 'zoom'
|
||||
assert inverted is True
|
||||
|
||||
|
||||
def test_keyboardsettings_mousewheel_action_for_event_empty(kbsettings):
|
||||
event = MagicMock(
|
||||
modifiers=MagicMock(
|
||||
return_value=(Qt.KeyboardModifier.AltModifier
|
||||
| Qt.KeyboardModifier.ShiftModifier
|
||||
| Qt.KeyboardModifier.ControlModifier)))
|
||||
|
||||
group, inverted = kbsettings.mousewheel_action_for_event(event)
|
||||
assert group is None
|
||||
assert inverted is None
|
||||
|
||||
|
||||
def test_keyboardsettings_mouse_action_for_event_finds(kbsettings):
|
||||
action = kbsettings.MOUSE_ACTIONS['zoom1']
|
||||
action.set_button('Middle')
|
||||
action.set_modifiers(['Shift', 'Ctrl', 'Alt'])
|
||||
action.set_inverted(True)
|
||||
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.MiddleButton),
|
||||
modifiers=MagicMock(
|
||||
return_value=(Qt.KeyboardModifier.AltModifier
|
||||
| Qt.KeyboardModifier.ShiftModifier
|
||||
| Qt.KeyboardModifier.ControlModifier)))
|
||||
|
||||
group, inverted = kbsettings.mouse_action_for_event(event)
|
||||
assert group == 'zoom'
|
||||
assert inverted is True
|
||||
|
||||
|
||||
def test_keyboardsettings_mouse_action_for_event_empty(kbsettings):
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.MiddleButton),
|
||||
modifiers=MagicMock(
|
||||
return_value=(Qt.KeyboardModifier.AltModifier
|
||||
| Qt.KeyboardModifier.ShiftModifier
|
||||
| Qt.KeyboardModifier.ControlModifier)))
|
||||
|
||||
group, inverted = kbsettings.mouse_action_for_event(event)
|
||||
assert group is None
|
||||
assert inverted is None
|
||||
|
|
@ -4,7 +4,7 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
|
||||
from beeref.config import CommandlineArgs
|
||||
from beeref.config.settings import CommandlineArgs
|
||||
|
||||
|
||||
def test_command_line_args_singleton():
|
||||
|
|
@ -13,7 +13,7 @@ def test_command_line_args_singleton():
|
|||
CommandlineArgs._instance = None
|
||||
|
||||
|
||||
@patch('beeref.config.parser.parse_args')
|
||||
@patch('beeref.config.settings.parser.parse_args')
|
||||
def test_command_line_args_with_check_forces_new_parsing(parse_mock):
|
||||
args1 = CommandlineArgs()
|
||||
args2 = CommandlineArgs(with_check=True)
|
||||
|
|
@ -117,48 +117,3 @@ def test_settings_recent_files_update_respects_max_num(settings):
|
|||
assert len(recent) == 10
|
||||
assert recent[0] == os.path.abspath('14.bee')
|
||||
assert recent[-1] == os.path.abspath('5.bee')
|
||||
|
||||
|
||||
def test_keyboardsettings_set_shortcuts(kbsettings):
|
||||
kbsettings.set_shortcuts('Actions', 'foo', ['Ctrl+F'])
|
||||
assert kbsettings.get_shortcuts('Actions', 'foo') == ['Ctrl+F']
|
||||
|
||||
|
||||
def test_keyboardsettings_set_shortcuts_multiple(kbsettings):
|
||||
kbsettings.set_shortcuts('Actions', 'foo', ['Ctrl+F', 'Alt+O'])
|
||||
assert kbsettings.get_shortcuts('Actions', 'foo') == ['Ctrl+F', 'Alt+O']
|
||||
|
||||
|
||||
def test_keyboardsettings_get_shortcuts_existing(kbsettings):
|
||||
kbsettings.set_shortcuts('Actions', 'bar', ['Ctrl+R'])
|
||||
shortcuts = kbsettings.get_shortcuts('Actions', 'bar', ['Ctrl+B'])
|
||||
assert shortcuts == ['Ctrl+R']
|
||||
|
||||
|
||||
def test_keyboardsettings_get_shortcuts_default(kbsettings):
|
||||
shortcuts = kbsettings.get_shortcuts('Actions', 'bar', ['Ctrl+B'])
|
||||
assert shortcuts == ['Ctrl+B']
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
|
||||
@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
|
||||
|
|
@ -29,6 +29,13 @@ def test_exif_rotated_image_exif_unpack_error(qapp, imgfilename3x3):
|
|||
assert img.isNull() is False
|
||||
|
||||
|
||||
def test_exif_rotated_image_exif_notimplementederror(qapp, imgfilename3x3):
|
||||
with patch('beeref.fileio.image.exif.Image.list_all',
|
||||
side_effect=NotImplementedError()):
|
||||
img = exif_rotated_image(imgfilename3x3)
|
||||
assert img.isNull() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path,expected',
|
||||
[('test3x3.png', 'test3x3.png'),
|
||||
('test3x3_orientation1.jpg', 'test3x3.jpg'),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
||||
from beeref.actions.actions import Action
|
||||
from beeref import utils
|
||||
|
||||
|
||||
|
|
@ -78,3 +79,19 @@ def test_get_file_extension_from_format(formatstr, expected):
|
|||
((255, 0, 0, 100), '#ff000064')])
|
||||
def test_qcolor_to_hex(rgba, expected):
|
||||
assert utils.qcolor_to_hex(QtGui.QColor(*rgba)) == expected
|
||||
|
||||
|
||||
def test_actionlist_inits_dict():
|
||||
action1 = Action(id='foo', text='Foo')
|
||||
action2 = Action(id='bar', text='Bar')
|
||||
actionlist = utils.ActionList([action1, action2])
|
||||
actionlist['foo'] == action1
|
||||
actionlist['bar'] == action2
|
||||
|
||||
|
||||
def test_actionlist_acts_as_list():
|
||||
action1 = Action(id='foo', text='Foo')
|
||||
action2 = Action(id='bar', text='Bar')
|
||||
actionlist = utils.ActionList([action1, action2])
|
||||
actionlist[0] == action1
|
||||
actionlist[1] == action2
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ def test_on_action_settings(show_mock, view):
|
|||
show_mock.assert_called_once()
|
||||
|
||||
|
||||
@patch('beeref.widgets.settings.KeyboardSettingsDialog.show')
|
||||
@patch('beeref.widgets.controls.ControlsDialog.show')
|
||||
def test_on_action_keyboard_settings(show_mock, view):
|
||||
view.on_action_keyboard_settings()
|
||||
show_mock.assert_called_once()
|
||||
|
|
@ -940,6 +940,19 @@ def test_wheel_event_zoom(zoom_mock, view):
|
|||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.zoom')
|
||||
def test_wheel_event_zoom_custom_inverted(zoom_mock, view, kbsettings):
|
||||
kbsettings.MOUSEWHEEL_ACTIONS['zoom2'].set_modifiers(['Alt'])
|
||||
kbsettings.MOUSEWHEEL_ACTIONS['zoom2'].set_inverted(True)
|
||||
event = MagicMock()
|
||||
event.angleDelta.return_value = QtCore.QPointF(0.0, 40.0)
|
||||
event.position.return_value = QtCore.QPointF(10.0, 20.0)
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.AltModifier
|
||||
view.wheelEvent(event)
|
||||
zoom_mock.assert_called_once_with(-40, QtCore.QPointF(10.0, 20.0))
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.pan')
|
||||
def test_wheel_event_pan_vertically(pan_mock, view):
|
||||
event = MagicMock()
|
||||
|
|
@ -948,7 +961,21 @@ def test_wheel_event_pan_vertically(pan_mock, view):
|
|||
event.modifiers.return_value = (Qt.KeyboardModifier.ShiftModifier
|
||||
| Qt.KeyboardModifier.ControlModifier)
|
||||
view.wheelEvent(event)
|
||||
pan_mock.assert_called_once_with(QtCore.QPointF(0, 20))
|
||||
pan_mock.assert_called_once_with(QtCore.QPointF(20, 0))
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.pan')
|
||||
def test_wheel_event_pan_vertically_custom_inverted(
|
||||
pan_mock, view, kbsettings):
|
||||
kbsettings.MOUSEWHEEL_ACTIONS['pan_vertical2'].set_modifiers(['Alt'])
|
||||
kbsettings.MOUSEWHEEL_ACTIONS['pan_vertical2'].set_inverted(True)
|
||||
event = MagicMock()
|
||||
event.angleDelta.return_value = QtCore.QPointF(0.0, 40.0)
|
||||
event.position.return_value = QtCore.QPointF(10.0, 20.0)
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.AltModifier
|
||||
view.wheelEvent(event)
|
||||
pan_mock.assert_called_once_with(QtCore.QPointF(-20, 0))
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
|
|
@ -959,7 +986,21 @@ def test_wheel_event_pan_horizontally(pan_mock, view):
|
|||
event.position.return_value = QtCore.QPointF(10.0, 20.0)
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.ShiftModifier
|
||||
view.wheelEvent(event)
|
||||
pan_mock.assert_called_once_with(QtCore.QPointF(20, 0))
|
||||
pan_mock.assert_called_once_with(QtCore.QPointF(0, 20))
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.pan')
|
||||
def test_wheel_event_pan_horizontally_custom_inverted(
|
||||
pan_mock, view, kbsettings):
|
||||
kbsettings.MOUSEWHEEL_ACTIONS['pan_horizontal2'].set_modifiers(['Alt'])
|
||||
kbsettings.MOUSEWHEEL_ACTIONS['pan_horizontal2'].set_inverted(True)
|
||||
event = MagicMock()
|
||||
event.angleDelta.return_value = QtCore.QPointF(0.0, 40.0)
|
||||
event.position.return_value = QtCore.QPointF(10.0, 20.0)
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.AltModifier
|
||||
view.wheelEvent(event)
|
||||
pan_mock.assert_called_once_with(QtCore.QPointF(0, -20))
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
|
|
@ -973,6 +1014,26 @@ def test_mouse_press_zoom(mouse_event_mock, view):
|
|||
assert view.active_mode == view.ZOOM_MODE
|
||||
assert view.event_start == QtCore.QPointF(10.0, 20.0)
|
||||
assert view.event_anchor == QtCore.QPointF(10.0, 20.0)
|
||||
assert view.event_inverted is False
|
||||
mouse_event_mock.assert_not_called()
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mousePressEvent')
|
||||
def test_mouse_press_zoom_custom_inverted(mouse_event_mock, view, kbsettings):
|
||||
kbsettings.MOUSE_ACTIONS['zoom1'].set_button('Left')
|
||||
kbsettings.MOUSE_ACTIONS['zoom1'].set_modifiers(['Alt', 'Shift'])
|
||||
kbsettings.MOUSE_ACTIONS['zoom1'].set_inverted(True)
|
||||
event = MagicMock()
|
||||
event.position.return_value = QtCore.QPointF(10.0, 20.0)
|
||||
event.button.return_value = Qt.MouseButton.LeftButton
|
||||
event.modifiers.return_value = (
|
||||
Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier)
|
||||
view.mousePressEvent(event)
|
||||
assert view.active_mode == view.ZOOM_MODE
|
||||
assert view.event_start == QtCore.QPointF(10.0, 20.0)
|
||||
assert view.event_anchor == QtCore.QPointF(10.0, 20.0)
|
||||
assert view.event_inverted is True
|
||||
mouse_event_mock.assert_not_called()
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
|
@ -982,7 +1043,7 @@ def test_mouse_press_pan_middle_drag(mouse_event_mock, view):
|
|||
event = MagicMock()
|
||||
event.position.return_value = QtCore.QPointF(10.0, 20.0)
|
||||
event.button.return_value = Qt.MouseButton.MiddleButton
|
||||
event.modifiers.return_value = None
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.NoModifier
|
||||
view.mousePressEvent(event)
|
||||
assert view.active_mode == view.PAN_MODE
|
||||
assert view.event_start == QtCore.QPointF(10.0, 20.0)
|
||||
|
|
@ -1136,6 +1197,7 @@ def test_mouse_move_zoom(zoom_mock, mouse_event_mock, view):
|
|||
view.active_mode = view.ZOOM_MODE
|
||||
view.event_anchor = QtCore.QPointF(55.0, 66.0)
|
||||
view.event_start = QtCore.QPointF(10.0, 20.0)
|
||||
view.event_inverted = False
|
||||
event = MagicMock()
|
||||
event.position.return_value = QtCore.QPointF(10.0, 18.0)
|
||||
view.mouseMoveEvent(event)
|
||||
|
|
@ -1144,6 +1206,21 @@ def test_mouse_move_zoom(zoom_mock, mouse_event_mock, view):
|
|||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mouseMoveEvent')
|
||||
@patch('beeref.view.BeeGraphicsView.zoom')
|
||||
def test_mouse_move_zoom_inverted(zoom_mock, mouse_event_mock, view):
|
||||
view.active_mode = view.ZOOM_MODE
|
||||
view.event_anchor = QtCore.QPointF(55.0, 66.0)
|
||||
view.event_start = QtCore.QPointF(10.0, 20.0)
|
||||
view.event_inverted = True
|
||||
event = MagicMock()
|
||||
event.position.return_value = QtCore.QPointF(10.0, 18.0)
|
||||
view.mouseMoveEvent(event)
|
||||
zoom_mock.assert_called_once_with(-40, QtCore.QPointF(55.0, 66.0))
|
||||
mouse_event_mock.assert_not_called()
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mouseMoveEvent')
|
||||
def test_mouse_move_sample_color(mouse_event_mock, view):
|
||||
view.active_mode = view.SAMPLE_COLOR_MODE
|
||||
|
|
|
|||
0
tests/widgets/__init__.py
Normal file
0
tests/widgets/__init__.py
Normal file
16
tests/widgets/controls/test_controls.py
Normal file
16
tests/widgets/controls/test_controls.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from beeref.widgets.controls import ControlsDialog
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.Yes)
|
||||
@patch('beeref.config.KeyboardSettings.restore_defaults')
|
||||
def test_controls_dialog_on_restore_defaults(
|
||||
restore_mock, msg_mock, kbsettings, view):
|
||||
dialog = ControlsDialog(view)
|
||||
dialog.on_restore_defaults()
|
||||
msg_mock.assert_called_once()
|
||||
restore_mock.assert_called()
|
||||
422
tests/widgets/controls/test_keyboard.py
Normal file
422
tests/widgets/controls/test_keyboard.py
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from beeref.actions.actions import Action
|
||||
from beeref.widgets.controls.keyboard import (
|
||||
KeyboardShortcutsDelegate,
|
||||
KeyboardShortcutsEditor,
|
||||
KeyboardShortcutsModel,
|
||||
KeyboardShortcutsProxy,
|
||||
)
|
||||
from beeref.utils import ActionList
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_editor_on_save_no_conflicts(view):
|
||||
a1 = Action(id='foo', text='Foo', shortcuts=['Ctrl+F'])
|
||||
a2 = Action(id='bar', text='Bar', shortcuts=['Ctrl+B'])
|
||||
with patch('beeref.widgets.controls.keyboard.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_on_save_reenter_existing_shortcut(view):
|
||||
a1 = Action(id='foo', text='Foo', shortcuts=['Ctrl+F'])
|
||||
a2 = Action(id='bar', text='bar', shortcuts=['Ctrl+B'])
|
||||
with patch('beeref.widgets.controls.keyboard.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_on_save_conflicts_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.controls.keyboard.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_on_save_conflicts_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.controls.keyboard.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_on_save_conflicts_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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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.controls.keyboard.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: Not set'
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_setdata_saves():
|
||||
action = Action(id='foo', text='Foo', shortcuts=['Ctrl+F'])
|
||||
with patch('beeref.widgets.controls.keyboard.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', text='Foo', shortcuts=['Ctrl+F'])
|
||||
with patch('beeref.widgets.controls.keyboard.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', text='Foo')
|
||||
with patch('beeref.widgets.controls.keyboard.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', text='Foo', shortcuts=['Ctrl+B'])
|
||||
with patch('beeref.widgets.controls.keyboard.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', text='Foo', shortcuts=['Ctrl+F'])
|
||||
a2 = Action(id='bar', text='Bar', shortcuts=['Ctrl+B'])
|
||||
with patch('beeref.widgets.controls.keyboard.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.controls.keyboard.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()
|
||||
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.controls.keyboard.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')
|
||||
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.controls.keyboard.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', text='Foo', shortcuts=['Ctrl+F'])
|
||||
a2 = Action(id='bar', text='Bar', shortcuts=['Ctrl+B'])
|
||||
with patch('beeref.widgets.controls.keyboard.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() == []
|
||||
1257
tests/widgets/controls/test_mouse.py
Normal file
1257
tests/widgets/controls/test_mouse.py
Normal file
File diff suppressed because it is too large
Load diff
966
tests/widgets/controls/test_mousewheel.py
Normal file
966
tests/widgets/controls/test_mousewheel.py
Normal file
|
|
@ -0,0 +1,966 @@
|
|||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref.config.controls import MouseWheelConfig
|
||||
from beeref.widgets.controls.mousewheel import (
|
||||
MouseWheelDelegate,
|
||||
MouseWheelModifiersEditor,
|
||||
MouseWheelModel,
|
||||
MouseWheelProxy,
|
||||
)
|
||||
from beeref.utils import ActionList
|
||||
|
||||
|
||||
def test_mousewheel_editor_inits_modifiers_when_not_configured(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=[],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
assert len(editor.checkboxes) == 6
|
||||
for checkbox in editor.checkboxes.values():
|
||||
assert checkbox.isChecked() is False
|
||||
|
||||
|
||||
def test_mousewheel_editor_inits_modifiers_when_configured(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt', 'Ctrl'],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
assert len(editor.checkboxes) == 6
|
||||
for key, checkbox in editor.checkboxes.items():
|
||||
if key in ('Alt', 'Ctrl'):
|
||||
assert checkbox.isChecked() is True
|
||||
else:
|
||||
assert checkbox.isChecked() is False
|
||||
|
||||
|
||||
def test_mousewheel_editor_set_modifiers_no_modifier(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt', 'Ctrl'],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.set_modifiers_no_modifier()
|
||||
for key, checkbox in editor.checkboxes.items():
|
||||
if key == 'No Modifier':
|
||||
assert checkbox.isChecked() is True
|
||||
else:
|
||||
assert checkbox.isChecked() is False
|
||||
|
||||
|
||||
def test_mousewheel_editor_on_modifiers_changed_no_modifiers_checked(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt', 'Ctrl'],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.on_modifiers_changed('No Modifier', Qt.CheckState.Checked.value)
|
||||
for key, checkbox in editor.checkboxes.items():
|
||||
if key == 'No Modifier':
|
||||
assert checkbox.isChecked() is True
|
||||
else:
|
||||
assert checkbox.isChecked() is False
|
||||
|
||||
|
||||
def test_mousewheel_editor_on_modifiers_changed_when_a_modifier_checked(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['No Modifier'],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.on_modifiers_changed('Alt', Qt.CheckState.Checked.value)
|
||||
for key, checkbox in editor.checkboxes.items():
|
||||
if key == 'No Modifier':
|
||||
assert checkbox.isChecked() is False
|
||||
|
||||
|
||||
def test_mousewheel_editor_on_modifiers_changed_everything_unchecked(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=[],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.on_modifiers_changed('Alt', Qt.CheckState.Unchecked.value)
|
||||
for key, checkbox in editor.checkboxes.items():
|
||||
if key == 'No Modifier':
|
||||
assert checkbox.isChecked() is True
|
||||
else:
|
||||
assert checkbox.isChecked() is False
|
||||
|
||||
|
||||
def test_mousewheel_editor_get_modifiers(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=[],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.checkboxes['Alt'].setChecked(True)
|
||||
editor.get_modifiers() == ['Alt']
|
||||
|
||||
|
||||
def test_mousewheel_editor_get_modifiers_when_no_modifiers(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=[],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.ignore_on_changed = True
|
||||
editor.checkboxes['No Modifier'].setChecked(True)
|
||||
editor.checkboxes['Alt'].setChecked(True)
|
||||
editor.get_modifiers() == ['No Modifier']
|
||||
|
||||
|
||||
def test_mousewheel_editor_get_modifiers_when_no_modifiers_cleaned_false(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=[],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.ignore_on_changed = True
|
||||
editor.checkboxes['Shift'].setChecked(True)
|
||||
editor.checkboxes['Alt'].setChecked(True)
|
||||
editor.ignore_on_changed = False
|
||||
|
||||
assert editor.get_modifiers(cleaned=False) == ['Shift', 'Alt']
|
||||
|
||||
|
||||
def test_mousewheel_editor_set_modifiers(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=[],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.set_modifiers(['Alt', 'Shift'])
|
||||
for key, checkbox in editor.checkboxes.items():
|
||||
if key in ['Alt', 'Shift']:
|
||||
assert checkbox.isChecked() is True
|
||||
else:
|
||||
assert checkbox.isChecked() is False
|
||||
|
||||
|
||||
def test_mousewheel_editor_get_temp_action(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
tmp = editor.get_temp_action()
|
||||
assert tmp.get_modifiers() == ['Alt']
|
||||
|
||||
|
||||
def test_mousewheel_editor_reset_inputs(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.set_modifiers(['Ctrl'])
|
||||
editor.reset_inputs()
|
||||
assert editor.get_modifiers() == ['Alt']
|
||||
|
||||
|
||||
@patch('beeref.widgets.controls.mousewheel.MouseWheelModifiersEditor.accept')
|
||||
def test_mousewheel_editor_on_save_no_conflicts(accept_mock, view):
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a1, a2])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.set_modifiers(['Shift'])
|
||||
editor.on_save()
|
||||
assert editor.get_modifiers() == ['Shift']
|
||||
assert editor.remove_from_other is None
|
||||
accept_mock.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.widgets.controls.mousewheel.MouseWheelModifiersEditor.accept')
|
||||
def test_mousewheel_editor_on_save_reenter_existing_shortcut(
|
||||
accept_mock, view):
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a1, a2])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.on_save()
|
||||
assert editor.get_modifiers() == ['Alt']
|
||||
assert editor.remove_from_other is None
|
||||
accept_mock.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.No)
|
||||
@patch('beeref.widgets.controls.mousewheel.MouseWheelModifiersEditor.accept')
|
||||
def test_mousewheel_editor_on_save_conflicts_cancel(
|
||||
accept_mock, msg_mock, view):
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a1, a2])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.set_modifiers(['Ctrl'])
|
||||
editor.on_save()
|
||||
assert editor.get_modifiers() == ['Alt']
|
||||
assert editor.remove_from_other is None
|
||||
accept_mock.assert_not_called()
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.Yes)
|
||||
@patch('beeref.widgets.controls.mousewheel.MouseWheelModifiersEditor.accept')
|
||||
def test_mousewheel_editor_on_save_conflicts_confirm(
|
||||
accept_mock, msg_mock, view):
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a1, a2])):
|
||||
editor = MouseWheelModifiersEditor(
|
||||
view, index=MagicMock(row=MagicMock(return_value=0)))
|
||||
|
||||
editor.set_modifiers(['Ctrl'])
|
||||
editor.on_save()
|
||||
assert editor.get_modifiers() == ['Ctrl']
|
||||
assert editor.remove_from_other == a2
|
||||
accept_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_mousewheel_delegate_create_editor(view):
|
||||
delegate = MouseWheelDelegate()
|
||||
model = MouseWheelModel()
|
||||
widget = delegate.createEditor(
|
||||
view, QtWidgets.QStyleOptionViewItem(), index=model.index(0, 3))
|
||||
assert isinstance(widget.editor, MouseWheelModifiersEditor)
|
||||
|
||||
|
||||
def test_mousewheel_delegate_setmodeldata(view):
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
delegate = MouseWheelDelegate()
|
||||
model = MouseWheelModel()
|
||||
widget = delegate.createEditor(
|
||||
view, QtWidgets.QStyleOptionViewItem(), index=model.index(0, 2))
|
||||
widget.editor.set_modifiers(['Ctrl', 'Shift'])
|
||||
|
||||
with patch.object(widget.editor, 'result',
|
||||
return_value=QtWidgets.QDialog.DialogCode.Accepted):
|
||||
delegate.setModelData(widget, model, index=model.index(0, 2))
|
||||
assert action.get_modifiers() == ['Shift', 'Ctrl']
|
||||
|
||||
|
||||
def test_mousewheel_model_columncount():
|
||||
model = MouseWheelModel()
|
||||
model.columnCount(None) == 3
|
||||
|
||||
|
||||
def test_mousewheel_model_rowcount():
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a1, a2])):
|
||||
model = MouseWheelModel()
|
||||
model.rowCount(None) == 2
|
||||
|
||||
|
||||
def test_mousewheel_model_headerdata():
|
||||
model = MouseWheelModel()
|
||||
header = model.headerData(
|
||||
0,
|
||||
QtCore.Qt.Orientation.Horizontal,
|
||||
QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert header == 'Action'
|
||||
|
||||
|
||||
def test_flags_first_column():
|
||||
model = MouseWheelModel()
|
||||
flags = model.flags(model.index(0, 0))
|
||||
assert flags == (QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemNeverHasChildren)
|
||||
|
||||
|
||||
def test_flags_modifiers_column():
|
||||
model = MouseWheelModel()
|
||||
flags = model.flags(model.index(0, 2))
|
||||
assert flags == (QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemNeverHasChildren
|
||||
| QtCore.Qt.ItemFlag.ItemIsEditable)
|
||||
|
||||
|
||||
def test_flags_inverted_column_when_invertible():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
flags = model.flags(model.index(0, 4))
|
||||
assert flags == (QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemNeverHasChildren
|
||||
| QtCore.Qt.ItemFlag.ItemIsEditable
|
||||
| QtCore.Qt.ItemFlag.ItemIsUserCheckable)
|
||||
|
||||
|
||||
def test_flags_inverted_column_when_not_invertible():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
flags = model.flags(model.index(0, 4))
|
||||
assert flags == (QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemNeverHasChildren)
|
||||
|
||||
|
||||
def test_mousewheel_model_data_gets_text():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=0),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value == 'Foo'
|
||||
|
||||
|
||||
def test_mousewheel_model_data_gets_changed_when_not_changed():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
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_mousewheel_model_data_gets_changed_when_changed():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
action.set_modifiers(['Shift'])
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=1),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value == '✎'
|
||||
|
||||
|
||||
def test_mousewheel_model_data_gets_modifiers():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Ctrl', 'Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value == 'Ctrl + Alt'
|
||||
|
||||
|
||||
def test_mousewheel_model_data_gets_inverted_when_invertible():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Ctrl', 'Alt'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value == 'No'
|
||||
|
||||
|
||||
def test_mousewheel_model_data_gets_inverted_when_not_invertible():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Ctrl', 'Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value is None
|
||||
|
||||
|
||||
def test_mousewheel_model_data_tooltip_changed_when_not_changed():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Ctrl', 'Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
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_mousewheel_model_data_tooltip_changed_when_changed():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
action.set_modifiers(['Shift'])
|
||||
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_mousewheel_model_data_tooltip_modifiers_changed_from_not_configured():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=[],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
action.set_modifiers(['Shift'])
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value == 'Default: Not configured'
|
||||
|
||||
|
||||
def test_mousewheel_model_data_tooltip_modifiers_when_changed():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Ctrl', 'Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
action.set_modifiers(['Shift'])
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value == 'Default: Ctrl + Alt'
|
||||
|
||||
|
||||
def test_mousewheel_model_data_tooltip_inverted_when_changed_and_invertible():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
action.set_inverted(True)
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value == 'Default: No'
|
||||
|
||||
|
||||
def test_mousewheel_model_data_tooltip_inverted_changed_and_not_invertible():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=False)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
action.set_modifiers(['Shift'])
|
||||
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_mousewheel_model_data_checkstaterole_invertible_invertcol_inverted():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
action.set_inverted(True)
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.CheckStateRole)
|
||||
assert value == Qt.CheckState.Checked
|
||||
|
||||
|
||||
def test_mousewheel_model_data_checkstaterole_invertible_invcol_not_inverted():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.CheckStateRole)
|
||||
assert value == Qt.CheckState.Unchecked
|
||||
|
||||
|
||||
def test_mousewheel_model_data_checkstaterole_other_column():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.CheckStateRole)
|
||||
assert value is None
|
||||
|
||||
|
||||
def test_mousewheel_model_setdate_saves_inverted():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
model.setData(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
value=Qt.CheckState.Checked.value,
|
||||
role=None)
|
||||
assert action.get_inverted() is True
|
||||
|
||||
|
||||
def test_mousewheel_model_setdata_saves_controls():
|
||||
action = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([action])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
model.setData(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
value=['Ctrl'],
|
||||
role=None)
|
||||
|
||||
assert action.get_modifiers() == ['Ctrl']
|
||||
|
||||
|
||||
def test_mousewheel_model_setdata_saves_controls_and_removes_from_other():
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a1, a2])):
|
||||
model = MouseWheelModel()
|
||||
|
||||
model.setData(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
value=['Ctrl'],
|
||||
role=None,
|
||||
remove_from_other=a2)
|
||||
|
||||
assert a1.get_modifiers() == ['Ctrl']
|
||||
assert a2.get_modifiers() == []
|
||||
|
||||
|
||||
def test_mousewheel_proxy_data_unfiltered():
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
a3 = MouseWheelConfig(
|
||||
id='baz1',
|
||||
group='baz',
|
||||
text='Baz',
|
||||
modifiers=['Shift'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a2, a1, a3])):
|
||||
proxy = MouseWheelProxy()
|
||||
|
||||
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'
|
||||
|
||||
|
||||
def test_mousewheel_proxy_data_filtered():
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
a3 = MouseWheelConfig(
|
||||
id='baz1',
|
||||
group='baz',
|
||||
text='Baz',
|
||||
modifiers=['Shift'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a2, a1, a3])):
|
||||
proxy = MouseWheelProxy()
|
||||
|
||||
proxy.setFilterFixedString('b')
|
||||
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_mousewheel_proxy_setdata_saves_correct_filtered_index():
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
a3 = MouseWheelConfig(
|
||||
id='baz1',
|
||||
group='baz',
|
||||
text='Baz',
|
||||
modifiers=['Shift'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a2, a1, a3])):
|
||||
proxy = MouseWheelProxy()
|
||||
|
||||
proxy.setFilterFixedString('b')
|
||||
proxy.setData(
|
||||
index=proxy.index(1, 3),
|
||||
value={'modifiers': ['Ctrl', 'Alt']},
|
||||
role=None)
|
||||
|
||||
a3.get_modifiers() == ['Ctrl', 'Alt']
|
||||
|
||||
|
||||
def test_mousewheel_proxy_setdata_remove_from_other():
|
||||
a1 = MouseWheelConfig(
|
||||
id='foo1',
|
||||
group='foo',
|
||||
text='Foo',
|
||||
modifiers=['Alt'],
|
||||
invertible=True)
|
||||
a2 = MouseWheelConfig(
|
||||
id='bar1',
|
||||
group='bar',
|
||||
text='Bar',
|
||||
modifiers=['Ctrl'],
|
||||
invertible=True)
|
||||
|
||||
with patch('beeref.config.controls.KeyboardSettings.MOUSEWHEEL_ACTIONS',
|
||||
ActionList([a2, a1])):
|
||||
proxy = MouseWheelProxy()
|
||||
|
||||
proxy.setData(
|
||||
index=proxy.index(0, 3),
|
||||
value={'modifiers': ['Ctrl', 'Alt']},
|
||||
role=None,
|
||||
remove_from_other=a2)
|
||||
|
||||
a1.get_modifiers() == ['Ctrl', 'Alt']
|
||||
a2.get_modifiers() == []
|
||||
|
|
@ -1,16 +1,10 @@
|
|||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore, QtGui
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from beeref.actions.actions import Action, ActionList
|
||||
from beeref.widgets.settings import (
|
||||
ArrangeGapWidget,
|
||||
ImageStorageFormatWidget,
|
||||
KeyboardSettingsDialog,
|
||||
KeyboardShortcutsDelegate,
|
||||
KeyboardShortcutsEditor,
|
||||
KeyboardShortcutsModel,
|
||||
KeyboardShortcutsProxy,
|
||||
SettingsDialog,
|
||||
)
|
||||
|
||||
|
|
@ -100,401 +94,3 @@ 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()
|
||||
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')
|
||||
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()
|
||||
|
|
|
|||
172
tools/build_appimage.py
Executable file
172
tools/build_appimage.py
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Build the BeeRef appimage. Run from the git root directory.
|
||||
# On github actions:
|
||||
# ./tools/build_appimage --version=${{ github.ref_name }}\
|
||||
# --jsonfile=tools/linux_libs.json
|
||||
# Locally:
|
||||
# ./tools/build_appimage --version=0.3.3-dev --jsonfile=tools/linux_libs.json
|
||||
# --skip-apt
|
||||
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from urllib.request import urlretrieve
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=('Create an appimage for BeeRef. '
|
||||
'Run from the git root directory.'))
|
||||
parser.add_argument(
|
||||
'-v', '--version',
|
||||
required=True,
|
||||
help='BeeRef version number/tag for output file')
|
||||
parser.add_argument(
|
||||
'-j', '--jsonfile',
|
||||
required=True,
|
||||
help='Json with lib files and packages as generated by find_linux_libs')
|
||||
parser.add_argument(
|
||||
'--redownload',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Re-use downloaded files if present')
|
||||
parser.add_argument(
|
||||
'--skip-apt',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Skip apt install step')
|
||||
parser.add_argument(
|
||||
'-l', '--loglevel',
|
||||
default='INFO',
|
||||
choices=list(logging._nameToLevel.keys()),
|
||||
help='log level for console output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
BEEVERSION = args.version.removeprefix('v')
|
||||
APPIMAGE = 'python3.11.9-cp311-cp311-manylinux2014_x86_64.AppImage'
|
||||
# ^ Siehe:
|
||||
# https://python-appimage.readthedocs.io/en/latest/#alternative-site-packages-location
|
||||
PYVER = '3.11'
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=getattr(logging, args.loglevel))
|
||||
|
||||
|
||||
def run_command(*args, capture_output=False):
|
||||
logger.info(f'Running command: {args}')
|
||||
result = subprocess.run(args, capture_output=capture_output)
|
||||
assert result.returncode == 0, f'Failed with exit code {result.returncode}'
|
||||
|
||||
|
||||
def download_file(url, filename):
|
||||
if not args.redownload and os.path.exists(filename):
|
||||
logger.info(f'Found file: {filename}')
|
||||
else:
|
||||
logger.info(f'Downloading: {url}')
|
||||
logger.info(f'Saving as: {filename}')
|
||||
urlretrieve(url, filename=filename)
|
||||
os.chmod(filename, 0o755)
|
||||
|
||||
|
||||
url = ('https://github.com/niess/python-appimage/releases/download/'
|
||||
f'python{PYVER}/{APPIMAGE}')
|
||||
download_file(url, filename='python.appimage')
|
||||
|
||||
|
||||
try:
|
||||
shutil.rmtree('squashfs-root')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
run_command('./python.appimage', '--appimage-extract',
|
||||
capture_output=True)
|
||||
|
||||
run_command('squashfs-root/usr/bin/pip',
|
||||
'install',
|
||||
'.',
|
||||
f'--target=squashfs-root/opt/python{PYVER}/lib/python{PYVER}/')
|
||||
|
||||
logger.info(f'Reading from: {args.jsonfile}')
|
||||
with open(args.jsonfile, 'r') as f:
|
||||
data = json.loads(f.read())
|
||||
libs = data['libs']
|
||||
packages = data['packages']
|
||||
excludes = data['excludes']
|
||||
paths = set()
|
||||
|
||||
if not args.skip_apt:
|
||||
run_command('sudo', 'apt', 'install', *packages)
|
||||
|
||||
logger.info('Copying .so files to appimage...')
|
||||
|
||||
existing_files = []
|
||||
for root, subdirs, files in os.walk('squashfs-root'):
|
||||
existing_files.extend(files)
|
||||
|
||||
for lib in libs:
|
||||
if os.path.basename(lib) in existing_files:
|
||||
logger.debug(f'Skipping {lib} (already in appimage)')
|
||||
continue
|
||||
if os.path.basename(lib) in excludes:
|
||||
logger.debug(f'Skipping {lib} (excluded)')
|
||||
continue
|
||||
paths.add(os.path.dirname(lib))
|
||||
if os.path.exists(lib):
|
||||
filename = lib
|
||||
else:
|
||||
filename, _ = os.path.splitext(lib)
|
||||
dest = f'squashfs-root{filename}'
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
logger.debug(f'Copying {filename} to {dest}')
|
||||
shutil.copyfile(filename, f'squashfs-root{filename}')
|
||||
|
||||
|
||||
logger.info('Writing run script...')
|
||||
# Adapted from usr/bin/python3.x in the python appimage
|
||||
os.remove('squashfs-root/AppRun')
|
||||
# ^ This is only a symlink to usr/bin/python3.x
|
||||
|
||||
paths = [
|
||||
'/usr/lib', # The libs that come with the python appimage ar in /usr/lib
|
||||
] + list(paths)
|
||||
ld_paths = ['${APPDIR}' + p for p in paths] + ['${LD_LIBRARY_PATH}']
|
||||
ld_paths = ':'.join(ld_paths)
|
||||
logger.debug(f'LD_LIBRARY_PATH: {ld_paths}')
|
||||
|
||||
content = """#! /bin/bash
|
||||
|
||||
# If running from an extracted image, then export ARGV0 and APPDIR
|
||||
if [ -z "${APPIMAGE}" ]; then
|
||||
export ARGV0="$0"
|
||||
|
||||
self=$(readlink -f -- "$0") # Protect spaces (issue 55)
|
||||
here="${self%/*}"
|
||||
tmp="${here%/*}"
|
||||
export APPDIR="${tmp%/*}"
|
||||
fi
|
||||
|
||||
# Resolve the calling command (preserving symbolic links).
|
||||
export APPIMAGE_COMMAND=$(command -v -- "$ARGV0")
|
||||
|
||||
# Export SSL certificate
|
||||
export SSL_CERT_FILE="${APPDIR}/opt/_internal/certs.pem"
|
||||
"""
|
||||
content += f'export LD_LIBRARY_PATH="{ld_paths}"\n'
|
||||
content += f'"$APPDIR/opt/python{PYVER}/bin/python{PYVER}" -I -m beeref "$@"\n'
|
||||
|
||||
with open('squashfs-root/AppRun', 'w') as f:
|
||||
f.write(content)
|
||||
os.chmod('squashfs-root/AppRun', 0o755)
|
||||
|
||||
url = ('https://github.com/AppImage/AppImageKit/releases/download/'
|
||||
'continuous/appimagetool-x86_64.AppImage')
|
||||
|
||||
download_file(url, filename='appimagetool.appimage')
|
||||
run_command('./appimagetool.appimage',
|
||||
'squashfs-root',
|
||||
f'BeeRef-{BEEVERSION}.appimage',
|
||||
'--no-appstream')
|
||||
171
tools/find_linux_libs.py
Executable file
171
tools/find_linux_libs.py
Executable file
|
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Create JSON with Linux libs needed for BeeRef appimage
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib import request
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=('Create JSON with Linux libs needed for BeeRef appimage'))
|
||||
parser.add_argument(
|
||||
'pid',
|
||||
nargs=1,
|
||||
default=None,
|
||||
help='PID of running BeeRef process')
|
||||
parser.add_argument(
|
||||
'-l', '--loglevel',
|
||||
default='INFO',
|
||||
choices=list(logging._nameToLevel.keys()),
|
||||
help='log level for console output')
|
||||
parser.add_argument(
|
||||
'--jsonfile',
|
||||
default='linux_libs.json',
|
||||
help='JSON input/output file')
|
||||
parser.add_argument(
|
||||
'--check-appimage',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Check a running appimage process for missing libraries')
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def strip_minor_versions(path):
|
||||
# foo2.so.2.1.1 -> foo2.so.2
|
||||
return re.sub('(.so.[0-9]*)[.0-9]*$', r'\1', path)
|
||||
|
||||
|
||||
def what_links_to(path):
|
||||
links = set()
|
||||
dirname = os.path.dirname(path)
|
||||
for filename in os.listdir(dirname):
|
||||
filename = os.path.join(dirname, filename)
|
||||
if (os.path.islink(filename)
|
||||
and str(pathlib.Path(filename).resolve()) == path):
|
||||
links.add(filename)
|
||||
return sorted(links, key=len)
|
||||
|
||||
|
||||
def is_lib(path):
|
||||
return ('.so' in path
|
||||
and os.path.expanduser('~') not in path
|
||||
and 'python3' not in path
|
||||
and 'mesa-diverted' not in path)
|
||||
|
||||
|
||||
def iter_lsofoutput(output):
|
||||
for line in output.splitlines():
|
||||
line = line.split()
|
||||
if line[3] == 'mem':
|
||||
path = line[-1]
|
||||
if is_lib(path):
|
||||
yield path
|
||||
|
||||
|
||||
PID = args.pid[0]
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=getattr(logging, args.loglevel))
|
||||
|
||||
|
||||
result = subprocess.run(('lsof', '-p', PID), capture_output=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
output = result.stdout.decode('utf-8')
|
||||
|
||||
|
||||
if args.check_appimage:
|
||||
logger.info('Checking appimage...')
|
||||
errors = False
|
||||
for lib in iter_lsofoutput(output):
|
||||
if 'mount_BeeRef' not in lib:
|
||||
print(f'Not in appimage: {lib}')
|
||||
errors = True
|
||||
if not errors:
|
||||
print('No missing libs found.')
|
||||
sys.exit()
|
||||
|
||||
|
||||
libs = []
|
||||
|
||||
if os.path.exists(args.jsonfile):
|
||||
logger.info(f'Reading from: {args.jsonfile}')
|
||||
with open(args.jsonfile, 'r') as f:
|
||||
data = json.loads(f.read())
|
||||
known_libs = data['libs']
|
||||
packages = set(data['packages'])
|
||||
else:
|
||||
logger.info(f'No file {args.jsonfile}; starting from scratch')
|
||||
known_libs = []
|
||||
packages = set()
|
||||
|
||||
|
||||
for lib in iter_lsofoutput(output):
|
||||
links = what_links_to(lib)
|
||||
if len(links) == 1:
|
||||
lib = links[0]
|
||||
else:
|
||||
logger.warning(f'Double check: {lib} {links}')
|
||||
lib = links[0]
|
||||
if lib in known_libs:
|
||||
logger.debug(f'Found known lib: {lib}')
|
||||
else:
|
||||
logger.debug(f'Found unknown lib: {lib}')
|
||||
libs.append(lib)
|
||||
|
||||
|
||||
for lib in libs:
|
||||
result = subprocess.run(('apt-file', 'search', lib), capture_output=True)
|
||||
if result.returncode != 0:
|
||||
logger.warning(f'Fix manually: {lib}')
|
||||
continue
|
||||
output = result.stdout.decode('utf-8')
|
||||
pkgs = set()
|
||||
for line in output.splitlines():
|
||||
pkg = line.split(': ')[0]
|
||||
if not (pkg.endswith('-dev') or pkg.endswith('-dbg')):
|
||||
pkgs.add(pkg)
|
||||
if len(pkgs) == 1:
|
||||
pkg = pkgs.pop()
|
||||
logger.debug(f'Found package: {pkg}')
|
||||
packages.add(pkg)
|
||||
else:
|
||||
logger.warning(f'Fix manually: {lib}')
|
||||
|
||||
|
||||
# Find the libs we shouldn't include in the appimage
|
||||
with request.urlopen(
|
||||
'https://raw.githubusercontent.com/AppImageCommunity/pkg2appimage/'
|
||||
'master/excludelist') as f:
|
||||
response = f.read().decode()
|
||||
|
||||
|
||||
exclude_masterlist = set()
|
||||
for line in response.splitlines():
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
line = line.split()[0]
|
||||
line = strip_minor_versions(line)
|
||||
exclude_masterlist.add(line)
|
||||
|
||||
excludes = []
|
||||
for ex in exclude_masterlist:
|
||||
for lib in (libs + known_libs):
|
||||
if lib.endswith(ex):
|
||||
excludes.append(ex)
|
||||
continue
|
||||
|
||||
|
||||
logger.info(f'Writing to: {args.jsonfile}')
|
||||
with open(args.jsonfile, 'w') as f:
|
||||
data = {'libs': sorted(libs + known_libs),
|
||||
'packages': sorted(packages),
|
||||
'excludes': sorted(excludes)}
|
||||
f.write(json.dumps(data, indent=4))
|
||||
238
tools/linux_libs.json
Normal file
238
tools/linux_libs.json
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
{
|
||||
"libs": [
|
||||
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
|
||||
"/lib/x86_64-linux-gnu/libbz2.so.1",
|
||||
"/lib/x86_64-linux-gnu/libc.so.6",
|
||||
"/lib/x86_64-linux-gnu/libcom_err.so.2",
|
||||
"/lib/x86_64-linux-gnu/libdbus-1.so.3",
|
||||
"/lib/x86_64-linux-gnu/libdl.so.2",
|
||||
"/lib/x86_64-linux-gnu/libexpat.so.1",
|
||||
"/lib/x86_64-linux-gnu/libgcc_s.so.1",
|
||||
"/lib/x86_64-linux-gnu/libgpg-error.so.0",
|
||||
"/lib/x86_64-linux-gnu/libkeyutils.so.1",
|
||||
"/lib/x86_64-linux-gnu/liblzma.so.5",
|
||||
"/lib/x86_64-linux-gnu/libm.so.6",
|
||||
"/lib/x86_64-linux-gnu/libpcre.so.3",
|
||||
"/lib/x86_64-linux-gnu/libpthread.so.0",
|
||||
"/lib/x86_64-linux-gnu/libresolv.so.2",
|
||||
"/lib/x86_64-linux-gnu/librt.so.1",
|
||||
"/lib/x86_64-linux-gnu/libselinux.so.1",
|
||||
"/lib/x86_64-linux-gnu/libutil.so.1",
|
||||
"/lib/x86_64-linux-gnu/libz.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/gio/modules/libgvfsdbus.so",
|
||||
"/usr/lib/x86_64-linux-gnu/gtk-3.0/modules/libcanberra-gtk-module.so",
|
||||
"/usr/lib/x86_64-linux-gnu/gvfs/libgvfscommon.so",
|
||||
"/usr/lib/x86_64-linux-gnu/libGLX.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libGLdispatch.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libX11-xcb.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libX11.so.6",
|
||||
"/usr/lib/x86_64-linux-gnu/libXau.so.6",
|
||||
"/usr/lib/x86_64-linux-gnu/libXcomposite.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libXcursor.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libXdamage.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libXdmcp.so.6",
|
||||
"/usr/lib/x86_64-linux-gnu/libXext.so.6",
|
||||
"/usr/lib/x86_64-linux-gnu/libXfixes.so.3",
|
||||
"/usr/lib/x86_64-linux-gnu/libXi.so.6",
|
||||
"/usr/lib/x86_64-linux-gnu/libXinerama.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libXrandr.so.2",
|
||||
"/usr/lib/x86_64-linux-gnu/libXrender.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libatk-1.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libatk-bridge-2.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libatspi.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libblkid.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libbrotlicommon.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libbrotlidec.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libbsd.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libcairo-gobject.so.2",
|
||||
"/usr/lib/x86_64-linux-gnu/libcairo.so.2",
|
||||
"/usr/lib/x86_64-linux-gnu/libcanberra-gtk3.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libcanberra.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libcrypto.so",
|
||||
"/usr/lib/x86_64-linux-gnu/libdatrie.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libepoxy.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libffi.so.7",
|
||||
"/usr/lib/x86_64-linux-gnu/libfontconfig.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libfreetype.so.6",
|
||||
"/usr/lib/x86_64-linux-gnu/libfribidi.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libgcrypt.so.20",
|
||||
"/usr/lib/x86_64-linux-gnu/libgdk-3.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libgdk_pixbuf-2.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libgio-2.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libglib-2.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libgraphite2.so.3",
|
||||
"/usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2",
|
||||
"/usr/lib/x86_64-linux-gnu/libgthread-2.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libgtk-3.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libharfbuzz.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libk5crypto.so.3",
|
||||
"/usr/lib/x86_64-linux-gnu/libkrb5.so.3",
|
||||
"/usr/lib/x86_64-linux-gnu/libkrb5support.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libltdl.so.7",
|
||||
"/usr/lib/x86_64-linux-gnu/liblz4.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libmd.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libmount.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libogg.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libpango-1.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libpangocairo-1.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libpangoft2-1.0.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libpixman-1.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libpng16.so.16",
|
||||
"/usr/lib/x86_64-linux-gnu/libsqlite3.so",
|
||||
"/usr/lib/x86_64-linux-gnu/libssl.so",
|
||||
"/usr/lib/x86_64-linux-gnu/libstdc++.so.6",
|
||||
"/usr/lib/x86_64-linux-gnu/libsystemd.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libtdb.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libthai.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libuuid.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libvorbis.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libvorbisfile.so.3",
|
||||
"/usr/lib/x86_64-linux-gnu/libwayland-client.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libwayland-cursor.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libwayland-egl.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-cursor.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-icccm.so.4",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-image.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-keysyms.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-randr.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-render-util.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-render.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-shape.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-shm.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-sync.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-util.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-xfixes.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb-xkb.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libxcb.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libxkbcommon-x11.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libxkbcommon.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libzstd.so.1"
|
||||
],
|
||||
"packages": [
|
||||
"gvfs",
|
||||
"gvfs-libs",
|
||||
"libatk-bridge2.0-0",
|
||||
"libatk1.0-0",
|
||||
"libatspi2.0-0",
|
||||
"libblkid1",
|
||||
"libbrotli1",
|
||||
"libbsd0",
|
||||
"libbz2-1.0",
|
||||
"libc6",
|
||||
"libcairo-gobject2",
|
||||
"libcairo2",
|
||||
"libcanberra-gtk3-0",
|
||||
"libcanberra-gtk3-module",
|
||||
"libcanberra0",
|
||||
"libcom-err2",
|
||||
"libdatrie1",
|
||||
"libdbus-1-3",
|
||||
"libepoxy0",
|
||||
"libexpat1",
|
||||
"libffi7",
|
||||
"libfontconfig1",
|
||||
"libfreetype6",
|
||||
"libfribidi0",
|
||||
"libgcc-s1",
|
||||
"libgcrypt20",
|
||||
"libgdk-pixbuf2.0-0",
|
||||
"libglib2.0-0",
|
||||
"libglvnd0",
|
||||
"libglx0",
|
||||
"libgpg-error0",
|
||||
"libgraphite2-3",
|
||||
"libgssapi-krb5-2",
|
||||
"libgtk-3-0",
|
||||
"libharfbuzz0b",
|
||||
"libk5crypto3",
|
||||
"libkeyutils1",
|
||||
"libkrb5-3",
|
||||
"libkrb5support0",
|
||||
"libltdl7",
|
||||
"liblz4-1",
|
||||
"liblzma5",
|
||||
"libmd0",
|
||||
"libmount1",
|
||||
"libogg0",
|
||||
"libpango-1.0-0",
|
||||
"libpangocairo-1.0-0",
|
||||
"libpangoft2-1.0-0",
|
||||
"libpcre2-8-0",
|
||||
"libpcre3",
|
||||
"libpixman-1-0",
|
||||
"libpng16-16",
|
||||
"libselinux1",
|
||||
"libsqlite3-0",
|
||||
"libssl1.1",
|
||||
"libstdc++6",
|
||||
"libsystemd0",
|
||||
"libtdb1",
|
||||
"libthai0",
|
||||
"libuuid1",
|
||||
"libvorbis0a",
|
||||
"libvorbisfile3",
|
||||
"libwayland-client0",
|
||||
"libwayland-cursor0",
|
||||
"libwayland-egl1",
|
||||
"libx11-6",
|
||||
"libx11-xcb1",
|
||||
"libxau6",
|
||||
"libxcb-cursor0",
|
||||
"libxcb-icccm4",
|
||||
"libxcb-image0",
|
||||
"libxcb-keysyms1",
|
||||
"libxcb-randr0",
|
||||
"libxcb-render-util0",
|
||||
"libxcb-render0",
|
||||
"libxcb-shape0",
|
||||
"libxcb-shm0",
|
||||
"libxcb-sync1",
|
||||
"libxcb-util1",
|
||||
"libxcb-xfixes0",
|
||||
"libxcb-xkb1",
|
||||
"libxcb1",
|
||||
"libxcomposite1",
|
||||
"libxcursor1",
|
||||
"libxdamage1",
|
||||
"libxdmcp6",
|
||||
"libxext6",
|
||||
"libxfixes3",
|
||||
"libxi6",
|
||||
"libxinerama1",
|
||||
"libxkbcommon-x11-0",
|
||||
"libxkbcommon0",
|
||||
"libxrandr2",
|
||||
"libxrender1",
|
||||
"libzstd1",
|
||||
"zlib1g"
|
||||
],
|
||||
"excludes": [
|
||||
"ld-linux-x86-64.so.2",
|
||||
"libGLX.so.0",
|
||||
"libGLdispatch.so.0",
|
||||
"libX11-xcb.so.1",
|
||||
"libX11.so.6",
|
||||
"libc.so.6",
|
||||
"libcom_err.so.2",
|
||||
"libdl.so.2",
|
||||
"libexpat.so.1",
|
||||
"libfontconfig.so.1",
|
||||
"libfreetype.so.6",
|
||||
"libfribidi.so.0",
|
||||
"libgcc_s.so.1",
|
||||
"libgpg-error.so.0",
|
||||
"libharfbuzz.so.0",
|
||||
"libm.so.6",
|
||||
"libpthread.so.0",
|
||||
"libresolv.so.2",
|
||||
"librt.so.1",
|
||||
"libstdc++.so.6",
|
||||
"libthai.so.0",
|
||||
"libutil.so.1",
|
||||
"libxcb.so.1",
|
||||
"libz.so.1"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue