mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Compare commits
60 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caed5c3801 | ||
|
|
960337887f | ||
|
|
e804766dc4 | ||
|
|
ef8189ec72 | ||
|
|
46f0ae54ae | ||
|
|
c545ad6ddc | ||
|
|
6e3bc35f74 | ||
|
|
95b024ee42 | ||
|
|
2b9240693e | ||
|
|
f2fc116bb3 | ||
|
|
b22056cefa | ||
|
|
aac2d0edfc | ||
|
|
a89027f0ba | ||
|
|
bdc3653cd6 | ||
|
|
b513af015c | ||
|
|
479665b9f1 | ||
|
|
38aaaf2553 | ||
|
|
c5e23fdf4f | ||
|
|
7ca33cfc0b | ||
|
|
13192987c6 | ||
|
|
65942c08cf | ||
|
|
5b86ad95d9 | ||
|
|
8a19a916a0 | ||
|
|
4648fa9c91 | ||
|
|
656f20f1ca | ||
|
|
3bd65b644f | ||
|
|
0891df8a20 | ||
|
|
62272b7a88 | ||
|
|
1be96ca4c3 | ||
|
|
b3bb70ce8a | ||
|
|
f53abad9c8 | ||
|
|
4aa6c2eb30 | ||
|
|
9356f53b40 | ||
|
|
6ddccd875d | ||
|
|
5f4c5088f0 | ||
|
|
2af6a75cc3 | ||
|
|
fb8515e8ae | ||
|
|
3151dc4993 | ||
|
|
1119004fe1 | ||
|
|
82787d08aa | ||
|
|
c64385b2c0 | ||
|
|
a532d1d667 | ||
|
|
b025204dfb | ||
|
|
b2c8b867f2 | ||
|
|
f9590e9f2e | ||
|
|
53c2ced83e | ||
|
|
f8b3270e0c | ||
|
|
113ba39488 | ||
|
|
7aab1ed38f | ||
|
|
b1aec4a156 | ||
|
|
14549c1a67 | ||
|
|
5c308c57b7 | ||
|
|
69dc401d0b | ||
|
|
2a9487a5e9 | ||
|
|
052bfb9fa5 | ||
|
|
a7ac9e8640 | ||
|
|
51b9bafbeb | ||
|
|
0e3fdf5c72 | ||
|
|
09984faedf | ||
|
|
40e9c27ca7 |
86 changed files with 9489 additions and 2264 deletions
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -10,10 +10,15 @@ assignees: ''
|
|||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**System info**
|
||||
|
||||
BeeRef version number:
|
||||
Your operating system:
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1.
|
||||
2.
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Expected behavior**
|
||||
|
|
|
|||
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the feature**
|
||||
A clear and concise description of what feature you want.
|
||||
|
||||
ONLY ONE FEATURE REQUEST PER REPORT! Even if they are related! You may open as many feature requests as you want.
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
|
@ -10,17 +10,15 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: ['ubuntu-20.04',
|
||||
'ubuntu-22.04',
|
||||
'macos-11',
|
||||
'macos-12',
|
||||
os: ['macos-12',
|
||||
'macos-13',
|
||||
'macos-14',
|
||||
'windows-latest']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install dependencies
|
||||
|
|
@ -32,7 +30,7 @@ jobs:
|
|||
run: |
|
||||
pyinstaller BeeRef.spec
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-${{ matrix.os }}
|
||||
path: dist/*
|
||||
|
|
|
|||
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
|
||||
8
.github/workflows/flake8.yml
vendored
8
.github/workflows/flake8.yml
vendored
|
|
@ -3,16 +3,16 @@ name: flake8
|
|||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
flake8:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.12
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
|
|||
15
.github/workflows/pytest.yml
vendored
15
.github/workflows/pytest.yml
vendored
|
|
@ -3,17 +3,16 @@ name: pytest
|
|||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.9', '3.11']
|
||||
pyqt-version: ['6.5.0', '6.6.1']
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
pyqt-version: ["6.7.0"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
|
|
@ -34,4 +33,8 @@ jobs:
|
|||
run: |
|
||||
xvfb-run --auto-servernum --server-num=1 --server-args="-screen 1 1920x1200x24 -ac +extension GLX" pytest --cov --cov-report=xml
|
||||
- name: Upload Coverage report to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
verbose: true
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
|||
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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,87 @@
|
|||
0.3.4-dev (unreleased)
|
||||
======================
|
||||
|
||||
Added
|
||||
-----
|
||||
|
||||
* Added a setting to change the default memory limit for individual
|
||||
images. If a big image won't load, increase this limit. This
|
||||
setting can be overridden by Qt's default environment variable
|
||||
QT_IMAGEIO_MAXALLOC
|
||||
* Display error messages when images can't be loaded from bee files
|
||||
* Added option to export all images from scene (File -> Export Images)
|
||||
* Added a confirmation dialog when attempting to close unsaved files.
|
||||
The confirmation dialog can be disalbed in:
|
||||
Settings -> Miscellaneous -> Confirm when closing an unsaved file
|
||||
* Add option to arrange by filename (Arrange -> Square (by filename))
|
||||
* Added a setting to choose the default arrange method on importing
|
||||
images in batch.
|
||||
(Settings -> Settings -> Images & Items -> Default Arrange Method).
|
||||
* Added the ability to open image files from command line. If the
|
||||
first command line arg is a bee file, it will be opened and all
|
||||
further files will be ignored, as previously. If the first argument
|
||||
isn't a bee file, all files will be treated as images and inserted
|
||||
as if opened with "Insert -> Images".
|
||||
|
||||
Fixed
|
||||
-----
|
||||
|
||||
* Fixed a case where adding/importing an image would hang when the image
|
||||
contained unsupported exif data (#111)
|
||||
* Fixed a hang when saving an open bee file that had been removed
|
||||
since being opened
|
||||
* Shortcuts now only trigger once when holding down the key
|
||||
combination to avoid inconstistend program states and potential
|
||||
crashes (by DarkDefender)
|
||||
* Fixed a crash when pressing the crop shortcut while dragging an image
|
||||
(by DarkDefender)
|
||||
|
||||
|
||||
Changed
|
||||
-------
|
||||
|
||||
* Arrange Horiszontal/Vertical now also sort by filename instead of
|
||||
the previous seemingly random behaviour
|
||||
|
||||
|
||||
0.3.3 - 2024-05-05
|
||||
==================
|
||||
|
||||
Added
|
||||
-----
|
||||
|
||||
* Moving the window from within BeeRef now changes to a diffent cursor from
|
||||
the default arrow cursor.
|
||||
* Added a color sampler which can copy colors from images to the
|
||||
clipboard in hex format (Images -> Sample Color)
|
||||
* Added notification when attempting to paste from an empty or
|
||||
unusable clipboard
|
||||
* 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
|
||||
-----
|
||||
|
||||
* Fixed a crash when pressing the keyboard shortcut for New Scene
|
||||
while in the process of doing a rubberband selection.
|
||||
* The checkmark of the menu entry Images -> Grayscale is now updating
|
||||
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)
|
||||
* Fixed Drag @ Drop from pinterest feed (by Randommist)
|
||||
* Fixed pasted items being inserted behind existing items
|
||||
|
||||
|
||||
|
||||
0.3.2 - 2024-01-21
|
||||
==================
|
||||
|
||||
|
|
@ -23,7 +107,7 @@ Fixed
|
|||
* Scene Export: Fix output image size and margins when scene had been
|
||||
scaled or moved.
|
||||
* Scene Export: Selecting filename without file extension now
|
||||
automatically appends the extension from the selected filter istead
|
||||
automatically appends the extension from the selected filter instead
|
||||
of resulting in a confusing error message.
|
||||
* The exemption from antialias/smoothing for images displayed at large
|
||||
zoom now also works on images that are flipped horizontally
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
[Desktop Entry]
|
||||
Name=BeeRef
|
||||
Comment=BeeRef 0.3.2
|
||||
GenericName=Image Viewer
|
||||
Comment=A simple reference image viewer
|
||||
Terminal=false
|
||||
Exec=/home/yourname/path/to/BeeRef-0.3.2-linux
|
||||
Exec=/home/yourname/path/to/BeeRef-0.3.4-dev.appimage
|
||||
Type=Application
|
||||
Icon=/home/yourname/path/to/logo.png
|
||||
Name[en_US]=BeeRef
|
||||
|
||||
MimeType=application/x-beeref;
|
||||
Categories=Qt;KDE;Graphics;
|
||||
|
|
|
|||
|
|
@ -103,10 +103,13 @@ 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()}')
|
||||
CommandlineArgs(with_check=True) # Force checking
|
||||
settings.on_startup()
|
||||
args = CommandlineArgs(with_check=True) # Force checking
|
||||
assert not args.debug_raise_error, args.debug_raise_error
|
||||
|
||||
os.environ["QT_DEBUG_PLUGINS"] = "1"
|
||||
app = BeeRefApplication(sys.argv)
|
||||
|
|
|
|||
|
|
@ -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,341 +104,347 @@ 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': '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='export_images',
|
||||
text='Export &Images...',
|
||||
callback='on_action_export_images',
|
||||
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 (by filename)',
|
||||
callback='on_action_arrange_horizontal',
|
||||
group='active_when_selection',
|
||||
),
|
||||
Action(
|
||||
id='arrange_vertical',
|
||||
text='&Vertical (by filename)',
|
||||
callback='on_action_arrange_vertical',
|
||||
group='active_when_selection',
|
||||
),
|
||||
Action(
|
||||
id='arrange_square',
|
||||
text='&Square (by filename)',
|
||||
callback='on_action_arrange_square',
|
||||
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='on_action_new_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',
|
||||
),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ menu_structure = [
|
|||
'save',
|
||||
'save_as',
|
||||
'export_scene',
|
||||
'export_images',
|
||||
MENU_SEPARATOR,
|
||||
'quit',
|
||||
],
|
||||
|
|
@ -101,6 +102,7 @@ menu_structure = [
|
|||
'arrange_optimal',
|
||||
'arrange_horizontal',
|
||||
'arrange_vertical',
|
||||
'arrange_square',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -110,6 +112,7 @@ menu_structure = [
|
|||
'grayscale',
|
||||
MENU_SEPARATOR,
|
||||
'show_color_gamut',
|
||||
'sample_color',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,19 @@ class ActionsMixin:
|
|||
|
||||
def _create_actions(self):
|
||||
for action in actions.values():
|
||||
qaction = QtGui.QAction(action['text'], self)
|
||||
qaction = QtGui.QAction(action.text, self)
|
||||
qaction.setAutoRepeat(False)
|
||||
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 +113,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):
|
||||
|
|
@ -123,7 +124,7 @@ class ActionsMixin:
|
|||
qaction = QtGui.QAction(os.path.basename(filename), self)
|
||||
qaction.setShortcuts(action.get_shortcuts())
|
||||
qaction.triggered.connect(
|
||||
partial(self.open_from_file, filename))
|
||||
partial(self.on_action_open_recent_file, filename))
|
||||
self.addAction(qaction)
|
||||
action.qaction = qaction
|
||||
self._recent_files_submenu.addAction(qaction)
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from importlib.resources import files as rsc_files
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from PyQt6 import QtGui, QtWidgets
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class BeeAssets:
|
||||
_instance = None
|
||||
PATH = os.path.dirname(__file__)
|
||||
PATH = rsc_files('beeref.assets')
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not cls._instance:
|
||||
|
|
@ -37,7 +37,8 @@ class BeeAssets:
|
|||
def on_new(self):
|
||||
logger.debug(f'Assets path: {self.PATH}')
|
||||
|
||||
self.logo = QtGui.QIcon(os.path.join(self.PATH, 'logo.png'))
|
||||
self.logo = QtGui.QIcon(str(self.PATH.joinpath('logo.png')))
|
||||
assert self.logo.isNull() is False
|
||||
self.cursor_rotate = self.cursor_from_image(
|
||||
'cursor_rotate.png', (20, 20))
|
||||
self.cursor_flip_h = self.cursor_from_image(
|
||||
|
|
@ -48,7 +49,8 @@ class BeeAssets:
|
|||
def cursor_from_image(self, filename, hotspot):
|
||||
app = QtWidgets.QApplication.instance()
|
||||
scaling = app.primaryScreen().devicePixelRatio()
|
||||
img = QtGui.QImage(os.path.join(self.PATH, filename))
|
||||
img = QtGui.QImage(str(self.PATH.joinpath(filename)))
|
||||
assert img.isNull() is False
|
||||
pixmap = QtGui.QPixmap.fromImage(img)
|
||||
pixmap.setDevicePixelRatio(scaling)
|
||||
return QtGui.QCursor(
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 48 KiB |
|
|
@ -13,11 +13,11 @@
|
|||
viewBox="0 0 50.536563 50.536564"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/home/rbreu/code/python/beeref/git/beeref/assets/logo.png"
|
||||
inkscape:export-xdpi="201.03999"
|
||||
inkscape:export-ydpi="201.03999">
|
||||
inkscape:export-xdpi="128.67"
|
||||
inkscape:export-ydpi="128.67">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
|
|
@ -496,10 +496,10 @@
|
|||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1311"
|
||||
inkscape:window-width="1280"
|
||||
inkscape:window-height="656"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="55"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
|
@ -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)
|
||||
|
|
@ -39,6 +40,7 @@ class InsertItems(QtGui.QUndoCommand):
|
|||
for item in self.items:
|
||||
self.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
item.bring_to_front()
|
||||
|
||||
def undo(self):
|
||||
self.scene.deselect_all_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,14 @@
|
|||
# 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
|
||||
import os.path
|
||||
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
||||
from beeref import constants
|
||||
from beeref.logging import qt_message_handler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -32,10 +29,14 @@ logger = logging.getLogger(__name__)
|
|||
parser = argparse.ArgumentParser(
|
||||
description=f'{constants.APPNAME_FULL} {constants.VERSION}')
|
||||
parser.add_argument(
|
||||
'filename',
|
||||
nargs='?',
|
||||
'filenames',
|
||||
nargs='*',
|
||||
default=None,
|
||||
help='Bee file to open')
|
||||
help=('Bee file or images to open. '
|
||||
'If the first file is a bee file, it will be opened and all '
|
||||
'further files will be ignored. If the first argument isn\'t a '
|
||||
'bee file, all files will be treated as images and inserted as '
|
||||
'if opened with "Insert -> Images".'))
|
||||
parser.add_argument(
|
||||
'--settings-dir',
|
||||
help='settings directory to use instead of default location')
|
||||
|
|
@ -59,6 +60,10 @@ parser.add_argument(
|
|||
default=False,
|
||||
action='store_true',
|
||||
help='draw item\'s transform handle areas for debugging')
|
||||
parser.add_argument(
|
||||
'--debug-raise-error',
|
||||
default='',
|
||||
help='immediately exit with given error message')
|
||||
|
||||
|
||||
class CommandlineArgs:
|
||||
|
|
@ -108,6 +113,10 @@ settings_events = BeeSettingsEvents()
|
|||
class BeeSettings(QtCore.QSettings):
|
||||
|
||||
FIELDS = {
|
||||
'Save/confirm_close_unsaved': {
|
||||
'default': True,
|
||||
'cast': bool,
|
||||
},
|
||||
'Items/image_storage_format': {
|
||||
'default': 'best',
|
||||
'validate': lambda x: x in ('png', 'jpg', 'best'),
|
||||
|
|
@ -117,6 +126,17 @@ class BeeSettings(QtCore.QSettings):
|
|||
'cast': int,
|
||||
'validate': lambda x: 0 <= x <= 200,
|
||||
},
|
||||
'Items/arrange_default': {
|
||||
'default': 'optimal',
|
||||
'validate': lambda x: x in (
|
||||
'optimal', 'horizontal', 'vertical', 'square'),
|
||||
},
|
||||
'Items/image_allocation_limit': {
|
||||
'default': 256,
|
||||
'cast': int,
|
||||
'validate': lambda x: x >= 0,
|
||||
'post_save_callback': QtGui.QImageReader.setAllocationLimit,
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -132,6 +152,26 @@ class BeeSettings(QtCore.QSettings):
|
|||
constants.APPNAME,
|
||||
constants.APPNAME)
|
||||
|
||||
def on_startup(self):
|
||||
"""Settings to be applied on application startup."""
|
||||
|
||||
if os.environ.get('QT_IMAGEIO_MAXALLOC'):
|
||||
alloc = int(os.environ['QT_IMAGEIO_MAXALLOC'])
|
||||
else:
|
||||
alloc = self.valueOrDefault('Items/image_allocation_limit')
|
||||
QtGui.QImageReader.setAllocationLimit(alloc)
|
||||
|
||||
def setValue(self, key, value):
|
||||
super().setValue(key, value)
|
||||
if key in self.FIELDS and 'post_save_callback' in self.FIELDS[key]:
|
||||
self.FIELDS[key]['post_save_callback'](value)
|
||||
|
||||
def remove(self, key):
|
||||
super().remove(key)
|
||||
if key in self.FIELDS and 'post_save_callback' in self.FIELDS[key]:
|
||||
value = self.valueOrDefault(key)
|
||||
self.FIELDS[key]['post_save_callback'](value)
|
||||
|
||||
def valueOrDefault(self, key):
|
||||
"""Get the value for key, or the default value specified in FIELDS.
|
||||
|
||||
|
|
@ -142,7 +182,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 +244,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)
|
||||
|
|
@ -15,13 +15,16 @@
|
|||
|
||||
APPNAME = 'BeeRef'
|
||||
APPNAME_FULL = f'{APPNAME} Reference Image Viewer'
|
||||
VERSION = '0.3.2'
|
||||
VERSION = '0.3.4-dev'
|
||||
WEBSITE = 'https://github.com/rbreu/beeref'
|
||||
COPYRIGHT = 'Copyright © 2021-2023 Rebecca Breu'
|
||||
COPYRIGHT = 'Copyright © 2021-2024 Rebecca Breu'
|
||||
|
||||
CHANGED_SYMBOL = '✎'
|
||||
|
||||
COLORS = {
|
||||
# Qt:
|
||||
'Active:Base': (60, 60, 60),
|
||||
'Active:AlternateBase': (70, 70, 70),
|
||||
'Active:Window': (40, 40, 40),
|
||||
'Active:Button': (40, 40, 40),
|
||||
'Active:Text': (200, 200, 200),
|
||||
|
|
@ -30,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),
|
||||
|
||||
|
|
@ -37,6 +44,4 @@ COLORS = {
|
|||
'Scene:Selection': (116, 234, 231),
|
||||
'Scene:Canvas': (60, 60, 60),
|
||||
'Scene:Text': (200, 200, 200),
|
||||
'Table:AlternativeRow': (70, 70, 70),
|
||||
|
||||
}
|
||||
|
|
|
|||
3
beeref/documentation/__init__.py
Normal file
3
beeref/documentation/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reading files in this directory with importlib.resources.files with
|
||||
# Python 3.9 requires an __init__.py file here. Later version are fine
|
||||
# without.
|
||||
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
<p>
|
||||
Middle Click + Drag<br/>
|
||||
Alt + Left Click + Drag
|
||||
Alt + Left Click + Drag<br/>
|
||||
Shift + Ctrl + Mouse Wheel (pan vertically)<br/>
|
||||
Shift + Mouse wheel (pan horizontally)
|
||||
</p>
|
||||
|
||||
<h4>Zoom Canvas</h4>
|
||||
|
||||
<p>
|
||||
Mouse wheel up/down<br/>
|
||||
Ctrl + Mouse Wheel<br/>
|
||||
Ctrl + Middle Click + Drag
|
||||
</p>
|
||||
|
||||
|
|
@ -27,7 +29,7 @@
|
|||
Rubberband Selection: Left Click + Drag
|
||||
</p>
|
||||
|
||||
<h4>Move Selection</h4>
|
||||
<h4>Move Selected Items</h4>
|
||||
|
||||
<p>
|
||||
Left Click + Drag
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ def save_bee(filename, scene, create_new=False, worker=None):
|
|||
logger.debug(f'Create new: {create_new}')
|
||||
io = SQLiteIO(filename, scene, create_new, worker=worker)
|
||||
io.write()
|
||||
logger.info('Saved!')
|
||||
logger.info('End save')
|
||||
|
||||
|
||||
def load_images(filenames, pos, scene, worker):
|
||||
|
|
@ -87,6 +87,7 @@ class ThreadedIO(QtCore.QThread):
|
|||
progress = QtCore.pyqtSignal(int)
|
||||
finished = QtCore.pyqtSignal(str, list)
|
||||
begin_processing = QtCore.pyqtSignal(int)
|
||||
user_input_required = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
super().__init__()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
IMG_LOADING_ERROR_MSG = (
|
||||
'Unknown format or too big?\n'
|
||||
'Check Settings -> Images & Items -> Maximum Image Size')
|
||||
|
||||
|
||||
class BeeFileIOError(Exception):
|
||||
def __init__(self, msg, filename):
|
||||
self.msg = msg
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@
|
|||
|
||||
import base64
|
||||
import logging
|
||||
import pathlib
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
||||
from .errors import BeeFileIOError
|
||||
from beeref import constants, widgets
|
||||
from beeref.items import BeePixmapItem
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -46,11 +48,41 @@ def register_exporter(cls):
|
|||
|
||||
|
||||
class ExporterBase:
|
||||
|
||||
def emit_begin_processing(self, worker, start):
|
||||
if worker:
|
||||
worker.begin_processing.emit(start)
|
||||
|
||||
def emit_progress(self, worker, progress):
|
||||
if worker:
|
||||
worker.progress.emit(progress)
|
||||
|
||||
def emit_finished(self, worker, filename, errors):
|
||||
filename = str(filename)
|
||||
if worker:
|
||||
worker.finished.emit(filename, errors)
|
||||
|
||||
def emit_user_input_required(self, worker, msg):
|
||||
if worker:
|
||||
worker.user_input_required.emit(msg)
|
||||
|
||||
def handle_export_error(self, filename, error, worker):
|
||||
filename = str(filename)
|
||||
logger.debug(f'Export failed: {error}')
|
||||
if worker:
|
||||
worker.finished.emit(filename, [str(error)])
|
||||
return
|
||||
else:
|
||||
e = error if isinstance(error, Exception) else None
|
||||
raise BeeFileIOError(msg=str(error), filename=filename) from e
|
||||
|
||||
|
||||
class SceneExporterBase(ExporterBase):
|
||||
"""For exporting the scene to a single image."""
|
||||
|
||||
def __init__(self, scene):
|
||||
self.scene = scene
|
||||
self.scene.cancel_crop_mode()
|
||||
self.scene.cancel_active_modes()
|
||||
self.scene.deselect_all_items()
|
||||
# Selection outlines/handles will be rendered to the exported
|
||||
# image, so deselect first. (Alternatively, pass an attribute
|
||||
|
|
@ -67,7 +99,7 @@ class ExporterBase:
|
|||
|
||||
|
||||
@register_exporter
|
||||
class SceneToPixmapExporter(ExporterBase):
|
||||
class SceneToPixmapExporter(SceneExporterBase):
|
||||
|
||||
TYPE = ExporterRegistry.DEFAULT_TYPE
|
||||
|
||||
|
|
@ -108,33 +140,25 @@ class SceneToPixmapExporter(ExporterBase):
|
|||
|
||||
def export(self, filename, worker=None):
|
||||
logger.debug(f'Exporting scene to {filename}')
|
||||
if worker:
|
||||
worker.begin_processing.emit(1)
|
||||
|
||||
self.emit_begin_processing(worker, 1)
|
||||
image = self.render_to_image()
|
||||
|
||||
if worker and worker.canceled:
|
||||
logger.debug('Export canceled')
|
||||
worker.finished.emit(filename, [])
|
||||
self.emit_finished(worker, filename, [])
|
||||
return
|
||||
|
||||
if not image.save(filename, quality=90):
|
||||
msg = 'Error writing file'
|
||||
logger.debug(f'Export failed: {msg}')
|
||||
if worker:
|
||||
worker.finished.emit(filename, [msg])
|
||||
return
|
||||
else:
|
||||
raise BeeFileIOError(msg, filename=filename)
|
||||
self.handle_export_error(filename, 'Error writing file', worker)
|
||||
return
|
||||
|
||||
logger.debug('Export finished')
|
||||
if worker:
|
||||
worker.progress.emit(1)
|
||||
worker.finished.emit(filename, [])
|
||||
self.emit_progress(worker, 1)
|
||||
self.emit_finished(worker, filename, [])
|
||||
|
||||
|
||||
@register_exporter
|
||||
class SceneToSVGExporter(ExporterBase):
|
||||
class SceneToSVGExporter(SceneExporterBase):
|
||||
|
||||
TYPE = 'svg'
|
||||
|
||||
|
|
@ -222,18 +246,15 @@ class SceneToSVGExporter(ExporterBase):
|
|||
element.set('opacity', str(item.opacity()))
|
||||
|
||||
svg.append(element)
|
||||
if worker:
|
||||
worker.progress.emit(i)
|
||||
if worker.canceled:
|
||||
return
|
||||
self.emit_progress(worker, i)
|
||||
if worker and worker.canceled:
|
||||
return
|
||||
|
||||
return svg
|
||||
|
||||
def export(self, filename, worker=None):
|
||||
logger.debug(f'Exporting scene to {filename}')
|
||||
if worker:
|
||||
worker.begin_processing.emit(len(self.scene.items()))
|
||||
|
||||
self.emit_begin_processing(worker, len(self.scene.items()))
|
||||
svg = self.render_to_svg(worker)
|
||||
|
||||
if worker and worker.canceled:
|
||||
|
|
@ -248,13 +269,89 @@ class SceneToSVGExporter(ExporterBase):
|
|||
with open(filename, 'w') as f:
|
||||
tree.write(f, encoding='unicode', xml_declaration=True)
|
||||
except OSError as e:
|
||||
logger.debug(f'Export failed: {e}')
|
||||
if worker:
|
||||
worker.finished.emit(filename, [str(e)])
|
||||
return
|
||||
else:
|
||||
raise BeeFileIOError(msg=str(e), filename=filename) from e
|
||||
self.handle_export_error(filename, e, worker)
|
||||
return
|
||||
|
||||
logger.debug('Export finished')
|
||||
if worker:
|
||||
worker.finished.emit(filename, [])
|
||||
self.emit_finished(worker, filename, [])
|
||||
|
||||
|
||||
class ImagesToDirectoryExporter(ExporterBase):
|
||||
"""Export all images to a folder.
|
||||
|
||||
Not registered in the registry as it is accessed via its own menu entry,
|
||||
not auto-detected by file extension.
|
||||
"""
|
||||
|
||||
def __init__(self, scene, dirname):
|
||||
self.scene = scene
|
||||
self.dirname = dirname
|
||||
self.items = list(self.scene.items_by_type(BeePixmapItem.TYPE))
|
||||
self.max_save_id = 0
|
||||
for item in self.items:
|
||||
if item.save_id:
|
||||
self.max_save_id = max(self.max_save_id, item.save_id)
|
||||
self.num_total = len(self.items)
|
||||
self.start_from = 0
|
||||
self.handle_existing = None
|
||||
|
||||
def export(self, worker=None):
|
||||
logger.debug(f'Exporting images to {self.dirname}')
|
||||
logger.debug(f'Starting at {self.start_from}')
|
||||
|
||||
self.emit_begin_processing(worker, self.num_total)
|
||||
self.emit_progress(worker, self.start_from)
|
||||
|
||||
for i, item in enumerate(
|
||||
self.items[self.start_from:], start=self.start_from):
|
||||
if worker and worker.canceled:
|
||||
logger.debug('Export canceled')
|
||||
worker.finished.emit(self.dirname, [])
|
||||
return
|
||||
|
||||
pixmap, imgformat = item.pixmap_to_bytes()
|
||||
|
||||
if item.save_id:
|
||||
filename = item.get_filename_for_export(imgformat)
|
||||
else:
|
||||
self.max_save_id += 1
|
||||
save_id = self.max_save_id
|
||||
filename = item.get_filename_for_export(imgformat, save_id)
|
||||
|
||||
try:
|
||||
path = pathlib.Path(self.dirname) / filename
|
||||
path_exists = path.exists()
|
||||
except OSError as e:
|
||||
self.handle_export_error(self.dirname, e, worker)
|
||||
return
|
||||
|
||||
if path_exists:
|
||||
logger.debug(f'File already exists: {path}')
|
||||
if self.handle_existing is None:
|
||||
self.start_from = i
|
||||
self.emit_user_input_required(worker, str(path))
|
||||
return
|
||||
else:
|
||||
if self.handle_existing == 'skip':
|
||||
self.handle_existing = None
|
||||
logger.debug('Skipping file')
|
||||
continue
|
||||
elif self.handle_existing == 'skip_all':
|
||||
logger.debug('Skipping file')
|
||||
continue
|
||||
elif self.handle_existing == 'overwrite':
|
||||
self.handle_existing = None
|
||||
logger.debug('Overwrite file')
|
||||
elif self.handle_existing == 'overwrite_all':
|
||||
logger.debug('Overwrite file')
|
||||
|
||||
logger.debug(f'Writing file: {path}')
|
||||
try:
|
||||
path.write_bytes(pixmap)
|
||||
except OSError as e:
|
||||
self.handle_export_error(path, e, worker)
|
||||
return
|
||||
|
||||
self.emit_progress(worker, i)
|
||||
|
||||
self.emit_finished(worker, self.dirname, [])
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ import logging
|
|||
import os.path
|
||||
import tempfile
|
||||
from urllib.error import URLError
|
||||
from urllib import request
|
||||
from urllib import parse, request
|
||||
|
||||
from PyQt6 import QtGui
|
||||
|
||||
import exif
|
||||
from lxml import etree
|
||||
import plum
|
||||
|
||||
|
||||
|
|
@ -44,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, ValueError):
|
||||
logger.exception(f'Exif failed reading orientation of image: {path}')
|
||||
return img
|
||||
|
||||
transform = QtGui.QTransform()
|
||||
|
|
@ -85,7 +90,15 @@ def load_image(path):
|
|||
return (exif_rotated_image(path), path)
|
||||
|
||||
url = bytes(path.toEncoded()).decode()
|
||||
domain = '.'.join(parse.urlparse(url).netloc.split(".")[-2:])
|
||||
img = exif_rotated_image()
|
||||
if domain == 'pinterest.com':
|
||||
try:
|
||||
page_data = request.urlopen(url).read()
|
||||
root = etree.HTML(page_data)
|
||||
url = root.xpath("//img")[0].get('src')
|
||||
except Exception as e:
|
||||
logger.debug(f'Pinterest image download failed: {e}')
|
||||
try:
|
||||
imgdata = request.urlopen(url).read()
|
||||
except URLError as e:
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ import tempfile
|
|||
from PyQt6 import QtGui
|
||||
|
||||
from beeref import constants
|
||||
from beeref.items import BeePixmapItem
|
||||
from .errors import BeeFileIOError
|
||||
from beeref.items import BeePixmapItem, BeeErrorItem
|
||||
from .errors import BeeFileIOError, IMG_LOADING_ERROR_MSG
|
||||
from .schema import SCHEMA, USER_VERSION, MIGRATIONS, APPLICATION_ID
|
||||
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ def handle_sqlite_errors(func):
|
|||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
func(self, *args, **kwargs)
|
||||
except sqlite3.Error as e:
|
||||
except Exception as e:
|
||||
logger.exception(f'Error while reading/writing {self.filename}')
|
||||
try:
|
||||
# Try to roll back transaction if there is any
|
||||
|
|
@ -80,6 +80,7 @@ class SQLiteIO:
|
|||
self.filename = filename
|
||||
self.readonly = readonly
|
||||
self.worker = worker
|
||||
self.retry = False
|
||||
|
||||
def __del__(self):
|
||||
self._close_connection()
|
||||
|
|
@ -109,14 +110,20 @@ class SQLiteIO:
|
|||
self._connection = sqlite3.connect(uri, uri=True)
|
||||
self._cursor = self.connection.cursor()
|
||||
if not self.create_new:
|
||||
self._migrate()
|
||||
try:
|
||||
self._migrate()
|
||||
except Exception:
|
||||
# Updating a file failed; try creating it from scratch instead
|
||||
logger.exception('Error migrating bee file')
|
||||
self.create_new = True
|
||||
self._establish_connection()
|
||||
|
||||
def _migrate(self):
|
||||
"""Migrate database if necessary."""
|
||||
|
||||
version = self.fetchone('PRAGMA user_version')[0]
|
||||
logger.debug(f'Found bee file version: {version}')
|
||||
if version == USER_VERSION:
|
||||
if version >= USER_VERSION:
|
||||
logger.debug('Version ok; no migrations necessary')
|
||||
return
|
||||
|
||||
|
|
@ -210,8 +217,14 @@ class SQLiteIO:
|
|||
}
|
||||
|
||||
if data['type'] == 'pixmap':
|
||||
data['item'] = BeePixmapItem(QtGui.QImage())
|
||||
data['item'].pixmap_from_bytes(row[9])
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
item.pixmap_from_bytes(row[9])
|
||||
if item.pixmap().isNull():
|
||||
item = data['data']['text'] = (
|
||||
f'Image could not be loaded: {item.filename}\n'
|
||||
+ IMG_LOADING_ERROR_MSG)
|
||||
data['type'] = BeeErrorItem.TYPE
|
||||
data['item'] = item
|
||||
|
||||
self.scene.add_item_later(data)
|
||||
|
||||
|
|
@ -230,22 +243,31 @@ class SQLiteIO:
|
|||
def write(self):
|
||||
if self.readonly:
|
||||
raise sqlite3.OperationalError(
|
||||
'attempt to write a readonly database')
|
||||
'Attempt to write to a readonly database')
|
||||
try:
|
||||
self.create_schema_on_new()
|
||||
self.write_data()
|
||||
except sqlite3.Error:
|
||||
if self.create_new:
|
||||
# If writing to a new file fails, we can't recover
|
||||
except Exception:
|
||||
if self.retry:
|
||||
# Trying to recover failed
|
||||
raise
|
||||
else:
|
||||
# Updating a file failed; try creating it from scratch instead
|
||||
self.retry = True
|
||||
# Try creating file from scratch and save again
|
||||
logger.exception(
|
||||
f'Updating to existing file {self.filename} failed')
|
||||
self.create_new = True
|
||||
self._close_connection()
|
||||
self.write()
|
||||
|
||||
def write_data(self):
|
||||
to_delete = self.fetchall('SELECT id from ITEMS')
|
||||
to_delete = {row[0] for row in self.fetchall('SELECT id from ITEMS')}
|
||||
# We don't want to touch existing items that are displayed as errors:
|
||||
keep = {item.original_save_id
|
||||
for item in self.scene.items_by_type(BeeErrorItem.TYPE)}
|
||||
logger.debug(f'Not saving error items: {keep}')
|
||||
to_delete = to_delete - keep
|
||||
|
||||
to_save = list(self.scene.items_for_save())
|
||||
if self.worker:
|
||||
self.worker.begin_processing.emit(len(to_save))
|
||||
|
|
@ -253,7 +275,7 @@ class SQLiteIO:
|
|||
logger.debug(f'Saving {item} with id {item.save_id}')
|
||||
if item.save_id:
|
||||
self.update_item(item)
|
||||
to_delete.remove((item.save_id,))
|
||||
to_delete.remove(item.save_id)
|
||||
else:
|
||||
self.insert_item(item)
|
||||
if self.worker:
|
||||
|
|
@ -261,11 +283,13 @@ 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, [])
|
||||
|
||||
def delete_items(self, to_delete):
|
||||
to_delete = [(pk,) for pk in to_delete]
|
||||
self.exmany('DELETE FROM items WHERE id=?', to_delete)
|
||||
self.exmany('DELETE FROM sqlar WHERE item_id=?', to_delete)
|
||||
self.connection.commit()
|
||||
|
|
@ -282,13 +306,7 @@ class SQLiteIO:
|
|||
|
||||
if hasattr(item, 'pixmap_to_bytes'):
|
||||
pixmap, imgformat = item.pixmap_to_bytes()
|
||||
|
||||
if item.filename:
|
||||
basename = os.path.splitext(os.path.basename(item.filename))[0]
|
||||
name = f'{item.save_id:04}-{basename}.{imgformat}'
|
||||
else:
|
||||
name = f'{item.save_id:04}.{imgformat}'
|
||||
|
||||
name = item.get_filename_for_export(imgformat)
|
||||
self.ex(
|
||||
'INSERT INTO sqlar (item_id, name, mode, sz, data) '
|
||||
'VALUES (?, ?, ?, ?, ?)',
|
||||
|
|
|
|||
174
beeref/items.py
174
beeref/items.py
|
|
@ -20,6 +20,7 @@ text).
|
|||
from collections import defaultdict
|
||||
from functools import cached_property
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
from PyQt6.QtCore import Qt
|
||||
|
|
@ -40,6 +41,32 @@ def register_item(cls):
|
|||
return cls
|
||||
|
||||
|
||||
def sort_by_filename(items):
|
||||
"""Order items by filename.
|
||||
|
||||
Items with a filename (ordered by filename) first, then items
|
||||
without a filename but with a save_id follow (ordered by
|
||||
save_id), then remaining items in the order that they have
|
||||
been inserted into the scene.
|
||||
"""
|
||||
|
||||
items_by_filename = []
|
||||
items_by_save_id = []
|
||||
items_remaining = []
|
||||
|
||||
for item in items:
|
||||
if getattr(item, 'filename', None):
|
||||
items_by_filename.append(item)
|
||||
elif getattr(item, 'save_id', None):
|
||||
items_by_save_id.append(item)
|
||||
else:
|
||||
items_remaining.append(item)
|
||||
|
||||
items_by_filename.sort(key=lambda x: x.filename)
|
||||
items_by_save_id.sort(key=lambda x: x.save_id)
|
||||
return items_by_filename + items_by_save_id + items_remaining
|
||||
|
||||
|
||||
class BeeItemMixin(SelectableMixin):
|
||||
"""Base for all items added by the user."""
|
||||
|
||||
|
|
@ -64,7 +91,7 @@ class BeeItemMixin(SelectableMixin):
|
|||
def on_selected_change(self, value):
|
||||
if (value and self.scene()
|
||||
and not self.scene().has_selection()
|
||||
and not self.scene().rubberband_active):
|
||||
and not self.scene().active_mode is None):
|
||||
self.bring_to_front()
|
||||
|
||||
def update_from_data(self, **kwargs):
|
||||
|
|
@ -85,7 +112,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
TYPE = 'pixmap'
|
||||
CROP_HANDLE_SIZE = 15
|
||||
|
||||
def __init__(self, image, filename=None):
|
||||
def __init__(self, image, filename=None, **kwargs):
|
||||
super().__init__(QtGui.QPixmap.fromImage(image))
|
||||
self.save_id = None
|
||||
self.filename = filename
|
||||
|
|
@ -132,14 +159,56 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
logger.debug('Setting grayscale for {self} to {value}')
|
||||
self._grayscale = value
|
||||
if value is True:
|
||||
img = self.pixmap().toImage()
|
||||
img = img.convertToFormat(QtGui.QImage.Format.Format_Grayscale8)
|
||||
# Using the grayscale image format to convert to grayscale
|
||||
# loses an image's tranparency. So the straightworward
|
||||
# following method gives us an ugly black replacement:
|
||||
# img = img.convertToFormat(QtGui.QImage.Format.Format_Grayscale8)
|
||||
|
||||
# Instead, we will fill the background with the current
|
||||
# canvas colour, so the issue is only visible if the image
|
||||
# overlaps other images. The way we do it here only works
|
||||
# as long as the canvas colour is itself grayscale,
|
||||
# though.
|
||||
img = QtGui.QImage(
|
||||
self.pixmap().size(), QtGui.QImage.Format.Format_Grayscale8)
|
||||
img.fill(QtGui.QColor(*COLORS['Scene:Canvas']))
|
||||
painter = QtGui.QPainter(img)
|
||||
painter.drawPixmap(0, 0, self.pixmap())
|
||||
painter.end()
|
||||
self._grayscale_pixmap = QtGui.QPixmap.fromImage(img)
|
||||
|
||||
# Alternative methods that have their own issues:
|
||||
#
|
||||
# 1. Use setAlphaChannel of the resulting grayscale
|
||||
# image. How do we get the original alpha channel? Using
|
||||
# the whole original image also takes color values into
|
||||
# account, not just their alpha values.
|
||||
#
|
||||
# 2. QtWidgets.QGraphicsColorizeEffect() with black colour
|
||||
# on the GraphicsItem. This applys to everything the paint
|
||||
# method does, so the selection outline/handles will also
|
||||
# be gray. setGraphicsEffect is only available on some
|
||||
# widgets, so we can't apply it selectively.
|
||||
#
|
||||
# 3. Going through every pixel and doing it manually — bad
|
||||
# performance.
|
||||
else:
|
||||
self._grayscale_pixmap = None
|
||||
|
||||
self.update()
|
||||
|
||||
def sample_color_at(self, pos):
|
||||
ipos = self.mapFromScene(pos)
|
||||
if self.grayscale:
|
||||
pm = self._grayscale_pixmap
|
||||
else:
|
||||
pm = self.pixmap()
|
||||
img = pm.toImage()
|
||||
|
||||
color = img.pixelColor(int(ipos.x()), int(ipos.y()))
|
||||
if color.alpha():
|
||||
return color
|
||||
|
||||
def bounding_rect_unselected(self):
|
||||
if self.crop_mode:
|
||||
return QtWidgets.QGraphicsPixmapItem.boundingRect(self)
|
||||
|
|
@ -155,6 +224,16 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
self.crop.width(),
|
||||
self.crop.height()]}
|
||||
|
||||
def get_filename_for_export(self, imgformat, save_id_default=None):
|
||||
save_id = self.save_id or save_id_default
|
||||
assert save_id is not None
|
||||
|
||||
if self.filename:
|
||||
basename = os.path.splitext(os.path.basename(self.filename))[0]
|
||||
return f'{save_id:04}-{basename}.{imgformat}'
|
||||
else:
|
||||
return f'{save_id:04}.{imgformat}'
|
||||
|
||||
def get_imgformat(self, img):
|
||||
"""Determines the format for storing this image."""
|
||||
|
||||
|
|
@ -384,7 +463,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
color = QtGui.QColor(0, 0, 0)
|
||||
color.setAlpha(100)
|
||||
painter.setBrush(QtGui.QBrush(color))
|
||||
painter.setPen(QtGui.QPen())
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawPath(path)
|
||||
painter.setBrush(QtGui.QBrush())
|
||||
|
||||
|
|
@ -435,13 +514,13 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
|
||||
for handle in self.crop_handles():
|
||||
if handle().contains(event.pos()):
|
||||
self.setCursor(self.get_crop_handle_cursor(handle))
|
||||
self.set_cursor(self.get_crop_handle_cursor(handle))
|
||||
return
|
||||
for edge in self.crop_edges():
|
||||
if edge().contains(event.pos()):
|
||||
self.setCursor(self.get_crop_edge_cursor(edge))
|
||||
self.set_cursor(self.get_crop_edge_cursor(edge))
|
||||
return
|
||||
self.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.unset_cursor()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if not self.crop_mode:
|
||||
|
|
@ -505,7 +584,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
return point
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.crop_mode:
|
||||
if self.crop_mode and self.crop_mode_event_start:
|
||||
diff = event.pos() - self.crop_mode_event_start
|
||||
if self.crop_mode_move == self.crop_handle_topleft:
|
||||
new = self.ensure_point_within_crop_bounds(
|
||||
|
|
@ -560,7 +639,7 @@ class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem):
|
|||
|
||||
TYPE = 'text'
|
||||
|
||||
def __init__(self, text=None):
|
||||
def __init__(self, text=None, **kwargs):
|
||||
super().__init__(text or "Text")
|
||||
self.save_id = None
|
||||
logger.debug(f'Initialized {self}')
|
||||
|
|
@ -650,3 +729,78 @@ class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem):
|
|||
|
||||
def copy_to_clipboard(self, clipboard):
|
||||
clipboard.setText(self.toPlainText())
|
||||
|
||||
|
||||
@register_item
|
||||
class BeeErrorItem(BeeItemMixin, QtWidgets.QGraphicsTextItem):
|
||||
"""Class for displaying error messages when an item can't be loaded
|
||||
from a bee file.
|
||||
|
||||
This item will be displayed instead of the original item. It won't
|
||||
save to bee files. The original item will be preserved in the bee
|
||||
file, unless this item gets deleted by the user, or a new bee file
|
||||
is saved.
|
||||
"""
|
||||
|
||||
TYPE = 'error'
|
||||
|
||||
def __init__(self, text=None, **kwargs):
|
||||
super().__init__(text or "Text")
|
||||
self.original_save_id = None
|
||||
logger.debug(f'Initialized {self}')
|
||||
self.is_image = False
|
||||
self.init_selectable()
|
||||
self.is_editable = False
|
||||
self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text']))
|
||||
|
||||
@classmethod
|
||||
def create_from_data(cls, **kwargs):
|
||||
data = kwargs.get('data', {})
|
||||
item = cls(**data)
|
||||
return item
|
||||
|
||||
def __str__(self):
|
||||
txt = self.toPlainText()[:40]
|
||||
return (f'Error "{txt}"')
|
||||
|
||||
def contains(self, point):
|
||||
return self.boundingRect().contains(point)
|
||||
|
||||
def paint(self, painter, option, widget):
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
color = QtGui.QColor(200, 0, 0)
|
||||
brush = QtGui.QBrush(color)
|
||||
painter.setBrush(brush)
|
||||
painter.drawRect(QtWidgets.QGraphicsTextItem.boundingRect(self))
|
||||
option.state = QtWidgets.QStyle.StateFlag.State_Enabled
|
||||
super().paint(painter, option, widget)
|
||||
self.paint_selectable(painter, option, widget)
|
||||
|
||||
def update_from_data(self, **kwargs):
|
||||
self.original_save_id = kwargs.get('save_id', self.original_save_id)
|
||||
self.setPos(kwargs.get('x', self.pos().x()),
|
||||
kwargs.get('y', self.pos().y()))
|
||||
self.setZValue(kwargs.get('z', self.zValue()))
|
||||
self.setScale(kwargs.get('scale', self.scale()))
|
||||
self.setRotation(kwargs.get('rotation', self.rotation()))
|
||||
|
||||
def create_copy(self):
|
||||
item = BeeErrorItem(self.toPlainText())
|
||||
item.setPos(self.pos())
|
||||
item.setZValue(self.zValue())
|
||||
item.setScale(self.scale())
|
||||
item.setRotation(self.rotation())
|
||||
return item
|
||||
|
||||
def flip(self, *args, **kwargs):
|
||||
"""Returns the flip value (1 or -1)"""
|
||||
# Never display error messages flipped
|
||||
return 1
|
||||
|
||||
def do_flip(self, *args, **kwargs):
|
||||
"""Flips the item."""
|
||||
# Never flip error messages
|
||||
pass
|
||||
|
||||
def copy_to_clipboard(self, clipboard):
|
||||
clipboard.setText(self.toPlainText())
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import logging
|
|||
from PyQt6 import QtCore, QtGui
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref import commands
|
||||
from beeref import commands, widgets
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref import fileio
|
||||
|
||||
|
|
@ -49,10 +49,17 @@ class MainControlsMixin:
|
|||
else:
|
||||
self.enter_movewin_mode()
|
||||
|
||||
@property
|
||||
def viewport_or_self(self):
|
||||
if hasattr(self, 'viewport'):
|
||||
return self.viewport()
|
||||
return self
|
||||
|
||||
def enter_movewin_mode(self):
|
||||
logger.debug('Entering movewin mode')
|
||||
self.setMouseTracking(True)
|
||||
self.movewin_active = True
|
||||
self.viewport_or_self.setCursor(Qt.CursorShape.SizeAllCursor)
|
||||
self.event_start = QtCore.QPointF(self.cursor().pos())
|
||||
if hasattr(self, 'disable_mouse_events'):
|
||||
self.disable_mouse_events()
|
||||
|
|
@ -61,6 +68,7 @@ class MainControlsMixin:
|
|||
logger.debug('Exiting movewin mode')
|
||||
self.setMouseTracking(False)
|
||||
self.movewin_active = False
|
||||
self.viewport_or_self.unsetCursor()
|
||||
if hasattr(self, 'enable_mouse_events'):
|
||||
self.enable_mouse_events()
|
||||
|
||||
|
|
@ -72,7 +80,9 @@ class MainControlsMixin:
|
|||
elif mimedata.hasImage():
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
logger.info('Attempted drop not an image')
|
||||
msg = 'Attempted drop not an image or image too big'
|
||||
logger.info(msg)
|
||||
widgets.BeeNotification(self.control_target, msg)
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
event.acceptProposedAction()
|
||||
|
|
@ -106,9 +116,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
|
||||
|
|
|
|||
152
beeref/scene.py
152
beeref/scene.py
|
|
@ -13,9 +13,10 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from queue import Queue
|
||||
from functools import partial
|
||||
import logging
|
||||
import math
|
||||
from queue import Queue
|
||||
|
||||
from PyQt6 import QtCore, QtWidgets, QtGui
|
||||
from PyQt6.QtCore import Qt
|
||||
|
|
@ -24,7 +25,7 @@ import rpack
|
|||
|
||||
from beeref import commands
|
||||
from beeref.config import BeeSettings
|
||||
from beeref.items import item_registry
|
||||
from beeref.items import item_registry, BeeErrorItem, sort_by_filename
|
||||
from beeref.selection import MultiSelectItem, RubberbandItem
|
||||
|
||||
|
||||
|
|
@ -32,28 +33,35 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
||||
cursor_changed = QtCore.pyqtSignal(QtGui.QCursor)
|
||||
cursor_cleared = QtCore.pyqtSignal()
|
||||
|
||||
MOVE_MODE = 1
|
||||
RUBBERBAND_MODE = 2
|
||||
|
||||
def __init__(self, undo_stack):
|
||||
super().__init__()
|
||||
self.move_active = False
|
||||
self.rubberband_active = False
|
||||
self.active_mode = None
|
||||
self.undo_stack = undo_stack
|
||||
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}')
|
||||
|
|
@ -63,6 +71,19 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
logger.debug(f'Removing item {item}')
|
||||
super().removeItem(item)
|
||||
|
||||
def cancel_active_modes(self):
|
||||
"""Cancels ongoing crop modes, rubberband modes etc, if there are
|
||||
any.
|
||||
"""
|
||||
self.cancel_crop_mode()
|
||||
self.end_rubberband_mode()
|
||||
|
||||
def end_rubberband_mode(self):
|
||||
if self.rubberband_item.scene():
|
||||
logger.debug('Ending rubberband selection')
|
||||
self.removeItem(self.rubberband_item)
|
||||
self.active_mode = None
|
||||
|
||||
def cancel_crop_mode(self):
|
||||
"""Cancels an ongoing crop mode, if there is any."""
|
||||
if self.crop_item:
|
||||
|
|
@ -81,7 +102,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.undo_stack.push(commands.InsertItems(self, copies, position))
|
||||
|
||||
def raise_to_top(self):
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
items = self.selectedItems(user_only=True)
|
||||
z_values = map(lambda i: i.zValue(), items)
|
||||
delta = self.max_z + self.Z_STEP - min(z_values)
|
||||
|
|
@ -90,7 +111,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
item.setZValue(item.zValue() + delta)
|
||||
|
||||
def lower_to_bottom(self):
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
items = self.selectedItems(user_only=True)
|
||||
z_values = map(lambda i: i.zValue(), items)
|
||||
delta = self.min_z - self.Z_STEP - max(z_values)
|
||||
|
|
@ -106,7 +127,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
:param mode: "width" or "height".
|
||||
"""
|
||||
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
values = []
|
||||
items = self.selectedItems(user_only=True)
|
||||
for item in items:
|
||||
|
|
@ -138,7 +159,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
Size meaning the area = widh * height.
|
||||
"""
|
||||
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
sizes = []
|
||||
items = self.selectedItems(user_only=True)
|
||||
for item in items:
|
||||
|
|
@ -158,12 +179,23 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.undo_stack.push(
|
||||
commands.NormalizeItems(items, scale_factors))
|
||||
|
||||
def arrange_default(self):
|
||||
default = self.settings.valueOrDefault('Items/arrange_default')
|
||||
MAPPING = {
|
||||
'optimal': self.arrange_optimal,
|
||||
'horizontal': self.arrange,
|
||||
'vertical': partial(self.arrange, vertical=True),
|
||||
'square': self.arrange_square,
|
||||
}
|
||||
|
||||
MAPPING[default]()
|
||||
|
||||
def arrange(self, vertical=False):
|
||||
"""Arrange items in a line (horizontally or vertically)."""
|
||||
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
|
||||
items = self.selectedItems(user_only=True)
|
||||
items = sort_by_filename(self.selectedItems(user_only=True))
|
||||
if len(items) < 2:
|
||||
return
|
||||
|
||||
|
|
@ -202,14 +234,13 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
positions))
|
||||
|
||||
def arrange_optimal(self):
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
|
||||
items = self.selectedItems(user_only=True)
|
||||
if len(items) < 2:
|
||||
return
|
||||
|
||||
gap = self.settings.valueOrDefault('Items/arrange_gap')
|
||||
center = self.get_selection_center()
|
||||
|
||||
sizes = []
|
||||
for item in items:
|
||||
|
|
@ -232,15 +263,53 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
|
||||
# We want the items to center around the selection's center,
|
||||
# not (0, 0)
|
||||
center = self.get_selection_center()
|
||||
bounds = rpack.bbox_size(sizes, positions)
|
||||
diff = center - QtCore.QPointF(bounds[0]/2, bounds[1]/2)
|
||||
positions = [QtCore.QPointF(*pos) + diff for pos in positions]
|
||||
|
||||
self.undo_stack.push(commands.ArrangeItems(self, items, positions))
|
||||
|
||||
def arrange_square(self):
|
||||
self.cancel_active_modes()
|
||||
max_width = 0
|
||||
max_height = 0
|
||||
gap = self.settings.valueOrDefault('Items/arrange_gap')
|
||||
items = sort_by_filename(self.selectedItems(user_only=True))
|
||||
|
||||
if len(items) < 2:
|
||||
return
|
||||
|
||||
for item in items:
|
||||
rect = self.itemsBoundingRect(items=[item])
|
||||
max_width = max(max_width, rect.width() + gap)
|
||||
max_height = max(max_height, rect.height() + gap)
|
||||
|
||||
# We want the items to center around the selection's center,
|
||||
# not (0, 0)
|
||||
num_rows = math.ceil(math.sqrt(len(items)))
|
||||
center = self.get_selection_center()
|
||||
diff = center - num_rows/2 * QtCore.QPointF(max_width, max_height)
|
||||
|
||||
iter_items = iter(items)
|
||||
positions = []
|
||||
for j in range(num_rows):
|
||||
for i in range(num_rows):
|
||||
try:
|
||||
item = next(iter_items)
|
||||
rect = self.itemsBoundingRect(items=[item])
|
||||
point = QtCore.QPointF(
|
||||
i * max_width + (max_width - rect.width())/2,
|
||||
j * max_height + (max_height - rect.height())/2)
|
||||
positions.append(point + diff)
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
self.undo_stack.push(commands.ArrangeItems(self, items, positions))
|
||||
|
||||
def flip_items(self, vertical=False):
|
||||
"""Flip selected items."""
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.push(
|
||||
commands.FlipItems(self.selectedItems(user_only=True),
|
||||
self.get_selection_center(),
|
||||
|
|
@ -256,15 +325,20 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
if item.is_image:
|
||||
item.enter_crop_mode()
|
||||
|
||||
def sample_color_at(self, position):
|
||||
item_at_pos = self.itemAt(position, self.views()[0].transform())
|
||||
if item_at_pos:
|
||||
return item_at_pos.sample_color_at(position)
|
||||
|
||||
def select_all_items(self):
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRect(self.itemsBoundingRect())
|
||||
# This is faster than looping through all items and calling setSelected
|
||||
self.setSelectionArea(path)
|
||||
|
||||
def deselect_all_items(self):
|
||||
self.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.clearSelection()
|
||||
|
||||
def has_selection(self):
|
||||
|
|
@ -313,16 +387,16 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
super().mousePressEvent(event)
|
||||
return
|
||||
if item_at_pos:
|
||||
self.move_active = True
|
||||
self.active_mode = self.MOVE_MODE
|
||||
elif self.items():
|
||||
self.rubberband_active = True
|
||||
self.active_mode = self.RUBBERBAND_MODE
|
||||
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.cancel_active_modes()
|
||||
item = self.itemAt(event.scenePos(), self.views()[0].transform())
|
||||
if item:
|
||||
self.move_active = False
|
||||
if not item.isSelected():
|
||||
item.setSelected(True)
|
||||
if item.is_editable:
|
||||
|
|
@ -336,7 +410,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.rubberband_active:
|
||||
if self.active_mode == self.RUBBERBAND_MODE:
|
||||
if not self.rubberband_item.scene():
|
||||
logger.debug('Activating rubberband selection')
|
||||
self.addItem(self.rubberband_item)
|
||||
|
|
@ -347,22 +421,19 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.rubberband_active:
|
||||
if self.rubberband_item.scene():
|
||||
logger.debug('Ending rubberband selection')
|
||||
self.removeItem(self.rubberband_item)
|
||||
self.rubberband_active = False
|
||||
if (self.move_active
|
||||
if self.active_mode == self.RUBBERBAND_MODE:
|
||||
self.end_rubberband_mode()
|
||||
if (self.active_mode == self.MOVE_MODE
|
||||
and self.has_selection()
|
||||
and not self.multi_select_item.is_action_active()
|
||||
and not self.selectedItems()[0].is_action_active()):
|
||||
and self.multi_select_item.active_mode is None
|
||||
and self.selectedItems()[0].active_mode is None):
|
||||
delta = event.scenePos() - self.event_start
|
||||
if not delta.isNull():
|
||||
self.undo_stack.push(
|
||||
commands.MoveItemsBy(self.selectedItems(),
|
||||
delta,
|
||||
ignore_first_redo=True))
|
||||
self.move_active = False
|
||||
self.active_mode = None
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def selectedItems(self, user_only=False):
|
||||
|
|
@ -377,6 +448,12 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
return list(filter(lambda i: hasattr(i, 'save_id'), items))
|
||||
return items
|
||||
|
||||
def items_by_type(self, itype):
|
||||
"""Returns all items of the given type."""
|
||||
|
||||
return filter(lambda i: getattr(i, 'TYPE', None) == itype,
|
||||
self.items())
|
||||
|
||||
def items_for_save(self):
|
||||
|
||||
"""Returns the items that are to be saved.
|
||||
|
|
@ -432,6 +509,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))
|
||||
|
|
@ -442,9 +523,12 @@ 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 not self.multi_select_item.scale_active
|
||||
and not self.multi_select_item.rotate_active):
|
||||
and self.multi_select_item.active_mode is None):
|
||||
self.multi_select_item.fit_selection_area(
|
||||
self.itemsBoundingRect(selection_only=True))
|
||||
|
||||
|
|
@ -467,7 +551,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
if not cls:
|
||||
# Just in case we add new item types in future versions
|
||||
logger.warning(f'Encountered item of unknown type: {typ}')
|
||||
cls = item_registry.get('text')
|
||||
cls = BeeErrorItem
|
||||
data['data'] = {'text': f'Item of unknown type: {typ}'}
|
||||
item = cls.create_from_data(**data)
|
||||
# Set the values common to all item types:
|
||||
|
|
|
|||
|
|
@ -118,6 +118,21 @@ class BaseItemMixin:
|
|||
"""The item's center in scene coordinates."""
|
||||
return self.mapToScene(self.center)
|
||||
|
||||
def set_cursor(self, cursor):
|
||||
# Can't use setCursor on the item itself because of bug
|
||||
# https://bugreports.qt.io/browse/QTBUG-4190
|
||||
if self.scene():
|
||||
self.scene().cursor_changed.emit(cursor)
|
||||
|
||||
def unset_cursor(self):
|
||||
# Can't use unsetCursor on the item itself because of bug
|
||||
# https://bugreports.qt.io/browse/QTBUG-4190
|
||||
if self.scene():
|
||||
self.scene().cursor_cleared.emit()
|
||||
|
||||
def sample_color_at(self, pos):
|
||||
return None
|
||||
|
||||
|
||||
class SelectableMixin(BaseItemMixin):
|
||||
"""Common code for selectable items: Selection outline, handles etc."""
|
||||
|
|
@ -128,6 +143,10 @@ class SelectableMixin(BaseItemMixin):
|
|||
SELECT_ROTATE_SIZE = 10 # size of hover area for rotating
|
||||
SELECT_FREE_CENTER = 20 # size of handle-free area in the center
|
||||
|
||||
SCALE_MODE = 1
|
||||
ROTATE_MODE = 2
|
||||
FLIP_MODE = 3
|
||||
|
||||
def init_selectable(self):
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setFlags(
|
||||
|
|
@ -135,19 +154,9 @@ class SelectableMixin(BaseItemMixin):
|
|||
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
|
||||
self.viewport_scale = 1
|
||||
self.reset_actions()
|
||||
self.active_mode = None
|
||||
self.is_editable = False
|
||||
|
||||
def reset_actions(self):
|
||||
self.scale_active = False
|
||||
self.rotate_active = False
|
||||
self.flip_active = False
|
||||
|
||||
def is_action_active(self):
|
||||
return any((self.scale_active,
|
||||
self.rotate_active,
|
||||
self.flip_active))
|
||||
|
||||
def fixed_length_for_viewport(self, value):
|
||||
"""The interactable areas need to stay the same size on the
|
||||
screen so we need to adjust the values according to the scale
|
||||
|
|
@ -215,6 +224,7 @@ class SelectableMixin(BaseItemMixin):
|
|||
pen.setWidth(self.SELECT_LINE_WIDTH)
|
||||
pen.setCosmetic(True)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QtGui.QBrush())
|
||||
|
||||
# Draw the main selection rectangle
|
||||
painter.drawRect(self.bounding_rect_unselected())
|
||||
|
|
@ -363,31 +373,31 @@ class SelectableMixin(BaseItemMixin):
|
|||
# This area should always trigger regular move operations,
|
||||
# even if it is covered by selection scale/flip/... handles.
|
||||
# This ensures that small items can always still be moved/edited.
|
||||
self.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.unset_cursor()
|
||||
return
|
||||
|
||||
for corner in self.corners:
|
||||
# See if we need to change the cursor for interactable areas
|
||||
if self.get_scale_bounds(corner).contains(event.pos()):
|
||||
self.setCursor(self.get_corner_scale_cursor(corner))
|
||||
self.scene().cursor_changed.emit(
|
||||
self.get_corner_scale_cursor(corner))
|
||||
self.set_cursor(self.get_corner_scale_cursor(corner))
|
||||
return
|
||||
elif self.get_rotate_bounds(corner).contains(event.pos()):
|
||||
self.setCursor(BeeAssets().cursor_rotate)
|
||||
self.set_cursor(BeeAssets().cursor_rotate)
|
||||
return
|
||||
for edge in self.get_flip_bounds():
|
||||
if edge['rect'].contains(event.pos()):
|
||||
if self.get_edge_flips_v(edge):
|
||||
self.setCursor(BeeAssets().cursor_flip_v)
|
||||
self.set_cursor(BeeAssets().cursor_flip_v)
|
||||
else:
|
||||
self.setCursor(BeeAssets().cursor_flip_h)
|
||||
self.set_cursor(BeeAssets().cursor_flip_h)
|
||||
return
|
||||
|
||||
self.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.unset_cursor()
|
||||
|
||||
def hoverEnterEvent(self, event):
|
||||
# Always return regular cursor when there aren't any selection handles
|
||||
if not self.has_selection_handles():
|
||||
self.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
def hoverLeaveEvent(self, event):
|
||||
self.unset_cursor()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.event_start = event.scenePos()
|
||||
|
|
@ -411,7 +421,7 @@ class SelectableMixin(BaseItemMixin):
|
|||
# Check if we are in one of the corner's scale areas
|
||||
if self.get_scale_bounds(corner).contains(event.pos()):
|
||||
# Start scale action for this corner
|
||||
self.scale_active = True
|
||||
self.active_mode = self.SCALE_MODE
|
||||
self.event_direction = self.get_direction_from_center(
|
||||
event.scenePos())
|
||||
self.event_anchor = self.mapToScene(
|
||||
|
|
@ -423,7 +433,7 @@ class SelectableMixin(BaseItemMixin):
|
|||
# Check if we are in one of the corner's rotate areas
|
||||
if self.get_rotate_bounds(corner).contains(event.pos()):
|
||||
# Start rotate action
|
||||
self.rotate_active = True
|
||||
self.active_mode = self.ROTATE_MODE
|
||||
self.event_anchor = self.center_scene_coords
|
||||
self.rotate_start_angle = self.get_rotate_angle(
|
||||
event.scenePos())
|
||||
|
|
@ -434,7 +444,7 @@ class SelectableMixin(BaseItemMixin):
|
|||
# Check if we are in one of the flip edges:
|
||||
for edge in self.get_flip_bounds():
|
||||
if edge['rect'].contains(event.pos()):
|
||||
self.flip_active = True
|
||||
self.active_mode = self.FLIP_MODE
|
||||
event.accept()
|
||||
self.scene().undo_stack.push(
|
||||
commands.FlipItems(
|
||||
|
|
@ -537,14 +547,14 @@ class SelectableMixin(BaseItemMixin):
|
|||
if (event.scenePos() - self.event_start).manhattanLength() > 5:
|
||||
self.scene().views()[0].reset_previous_transform()
|
||||
|
||||
if self.scale_active:
|
||||
if self.active_mode == self.SCALE_MODE:
|
||||
factor = self.get_scale_factor(event)
|
||||
for item in self.selection_action_items():
|
||||
item.setScale(item.scale_orig_factor * factor,
|
||||
item.mapFromScene(self.event_anchor))
|
||||
event.accept()
|
||||
return
|
||||
if self.rotate_active:
|
||||
if self.active_mode == self.ROTATE_MODE:
|
||||
snap = (event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||||
or event.modifiers() == Qt.KeyboardModifier.ShiftModifier)
|
||||
delta = self.get_rotate_delta(event.scenePos(), snap)
|
||||
|
|
@ -554,7 +564,7 @@ class SelectableMixin(BaseItemMixin):
|
|||
item.mapFromScene(self.event_anchor))
|
||||
event.accept()
|
||||
return
|
||||
if self.flip_active:
|
||||
if self.active_mode == self.FLIP_MODE:
|
||||
# We have already flipped on MousePress, but we
|
||||
# still need to accept the event here as to not
|
||||
# initiate an item move
|
||||
|
|
@ -564,7 +574,7 @@ class SelectableMixin(BaseItemMixin):
|
|||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.scale_active:
|
||||
if self.active_mode == self.SCALE_MODE:
|
||||
if self.get_scale_factor(event) != 1:
|
||||
self.scene().undo_stack.push(
|
||||
commands.ScaleItemsBy(
|
||||
|
|
@ -573,9 +583,9 @@ class SelectableMixin(BaseItemMixin):
|
|||
self.event_anchor,
|
||||
ignore_first_redo=True))
|
||||
event.accept()
|
||||
self.reset_actions()
|
||||
self.active_mode = None
|
||||
return
|
||||
elif self.rotate_active:
|
||||
elif self.active_mode == self.ROTATE_MODE:
|
||||
self.scene().on_selection_change()
|
||||
if self.get_rotate_delta(event.scenePos()) != 0:
|
||||
self.scene().undo_stack.push(
|
||||
|
|
@ -585,18 +595,18 @@ class SelectableMixin(BaseItemMixin):
|
|||
self.event_anchor,
|
||||
ignore_first_redo=True))
|
||||
event.accept()
|
||||
self.reset_actions()
|
||||
self.active_mode = None
|
||||
return
|
||||
elif self.flip_active:
|
||||
elif self.active_mode == self.FLIP_MODE:
|
||||
for edge in self.get_flip_bounds():
|
||||
if edge['rect'].contains(event.pos()):
|
||||
# We have already flipped on MousePress, but we
|
||||
# still need to accept the event here as to not
|
||||
# initiate an item move
|
||||
event.accept()
|
||||
self.reset_actions()
|
||||
self.active_mode = None
|
||||
return
|
||||
self.reset_actions()
|
||||
self.active_mode = None
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def on_view_scale_change(self):
|
||||
|
|
@ -634,12 +644,20 @@ class MultiSelectItem(SelectableMixin,
|
|||
def selection_action_items(self):
|
||||
"""The items affected by selection actions like scaling and rotating.
|
||||
"""
|
||||
return list(self.scene().selectedItems())
|
||||
if self.scene():
|
||||
return list(self.scene().selectedItems())
|
||||
return []
|
||||
|
||||
def lower_behind_selection(self):
|
||||
items = self.selection_action_items()
|
||||
if items:
|
||||
min_z = min(item.zValue() for item in items)
|
||||
self.setZValue(min_z - self.scene().Z_STEP)
|
||||
|
||||
def fit_selection_area(self, rect):
|
||||
"""Updates itself to fit the given selection area."""
|
||||
|
||||
logger.debug(f'Fit selection area to {rect}')
|
||||
logger.trace(f'Fit selection area to {rect}')
|
||||
|
||||
# Only update when values have changed, otherwise we end up in an
|
||||
# infinite event loop sceneChange -> itemChange -> sceneChange ...
|
||||
|
|
@ -688,4 +706,4 @@ class RubberbandItem(BaseItemMixin, QtWidgets.QGraphicsRectItem):
|
|||
"""Updates itself to fit the two given points."""
|
||||
|
||||
self.setRect(utils.get_rect_from_points(point1, point2))
|
||||
logger.debug(f'Updated rubberband {self}')
|
||||
logger.trace(f'Updated rubberband {self}')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -78,3 +79,31 @@ def get_file_extension_from_format(formatstr):
|
|||
extensions = re.match(r'.* \((.*)\)', formatstr).groups()[0]
|
||||
ext = extensions.split()[0]
|
||||
return ext.removeprefix('*.')
|
||||
|
||||
|
||||
def qcolor_to_hex(color):
|
||||
"""Returns the QColor as a hex represenation string:
|
||||
#RRGGBBAA if the color has transparencey, otherwise #RRGGBB.
|
||||
"""
|
||||
|
||||
if color.alpha() == 255:
|
||||
return color.name()
|
||||
|
||||
# The name method can only do HexRgb and HexArgb, not HexRgba, so
|
||||
# we have to do this ourselves:
|
||||
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)
|
||||
|
|
|
|||
293
beeref/view.py
293
beeref/view.py
|
|
@ -21,17 +21,18 @@ import os.path
|
|||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref.actions import ActionsMixin
|
||||
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
|
||||
from beeref.fileio.errors import IMG_LOADING_ERROR_MSG
|
||||
from beeref.fileio.export import exporter_registry, ImagesToDirectoryExporter
|
||||
from beeref import widgets
|
||||
from beeref.items import BeePixmapItem, BeeTextItem
|
||||
from beeref.main_controls import MainControlsMixin
|
||||
from beeref.scene import BeeGraphicsScene
|
||||
from beeref.utils import get_file_extension_from_format
|
||||
from beeref.utils import get_file_extension_from_format, qcolor_to_hex
|
||||
|
||||
|
||||
commandline_args = CommandlineArgs()
|
||||
|
|
@ -42,11 +43,16 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
QtWidgets.QGraphicsView,
|
||||
ActionsMixin):
|
||||
|
||||
PAN_MODE = 1
|
||||
ZOOM_MODE = 2
|
||||
SAMPLE_COLOR_MODE = 3
|
||||
|
||||
def __init__(self, app, parent=None):
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.parent = parent
|
||||
self.settings = BeeSettings()
|
||||
self.keyboard_settings = KeyboardSettings()
|
||||
self.welcome_overlay = widgets.welcome_overlay.WelcomeOverlay(self)
|
||||
|
||||
self.setBackgroundBrush(
|
||||
|
|
@ -62,12 +68,13 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
|
||||
self.filename = None
|
||||
self.previous_transform = None
|
||||
self.pan_active = False
|
||||
self.zoom_active = False
|
||||
self.active_mode = None
|
||||
|
||||
self.scene = BeeGraphicsScene(self.undo_stack)
|
||||
self.scene.changed.connect(self.on_scene_changed)
|
||||
self.scene.selectionChanged.connect(self.on_selection_changed)
|
||||
self.scene.cursor_changed.connect(self.on_cursor_changed)
|
||||
self.scene.cursor_cleared.connect(self.on_cursor_cleared)
|
||||
self.setScene(self.scene)
|
||||
|
||||
# Context menu and actions
|
||||
|
|
@ -75,9 +82,14 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.control_target = self
|
||||
self.init_main_controls(main_window=parent)
|
||||
|
||||
# Load file given via command line
|
||||
if commandline_args.filename:
|
||||
self.open_from_file(commandline_args.filename)
|
||||
# Load files given via command line
|
||||
if commandline_args.filenames:
|
||||
fn = commandline_args.filenames[0]
|
||||
if os.path.splitext(fn)[1] == '.bee':
|
||||
self.open_from_file(fn)
|
||||
else:
|
||||
self.do_insert_images(commandline_args.filenames)
|
||||
|
||||
self.update_window_title()
|
||||
|
||||
@property
|
||||
|
|
@ -92,6 +104,21 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.settings.update_recent_files(value)
|
||||
self.update_menu_and_actions()
|
||||
|
||||
def cancel_active_modes(self):
|
||||
self.scene.cancel_active_modes()
|
||||
self.cancel_sample_color_mode()
|
||||
self.active_mode = None
|
||||
|
||||
def cancel_sample_color_mode(self):
|
||||
logger.debug('Cancel sample color mode')
|
||||
self.active_mode = None
|
||||
self.viewport().unsetCursor()
|
||||
if hasattr(self, 'sample_color_widget'):
|
||||
self.sample_color_widget.hide()
|
||||
del self.sample_color_widget
|
||||
if self.scene.has_multi_selection():
|
||||
self.scene.multi_select_item.bring_to_front()
|
||||
|
||||
def update_window_title(self):
|
||||
clean = self.undo_stack.isClean()
|
||||
if clean and not self.filename:
|
||||
|
|
@ -143,6 +170,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
|
||||
def clear_scene(self):
|
||||
logging.debug('Clearing scene...')
|
||||
self.cancel_active_modes()
|
||||
self.scene.clear()
|
||||
self.undo_stack.clear()
|
||||
self.filename = None
|
||||
|
|
@ -177,6 +205,26 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
|
||||
logger.trace('Fit view done')
|
||||
|
||||
def get_confirmation_unsaved_changes(self, msg):
|
||||
confirm = self.settings.valueOrDefault('Save/confirm_close_unsaved')
|
||||
if confirm and not self.undo_stack.isClean():
|
||||
answer = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
'Discard unsaved changes?',
|
||||
msg,
|
||||
QtWidgets.QMessageBox.StandardButton.Yes |
|
||||
QtWidgets.QMessageBox.StandardButton.Cancel)
|
||||
return answer == QtWidgets.QMessageBox.StandardButton.Yes
|
||||
|
||||
return True
|
||||
|
||||
def on_action_new_scene(self):
|
||||
confirm = self.get_confirmation_unsaved_changes(
|
||||
'There are unsaved changes. '
|
||||
'Are you sure you want to open a new scene?')
|
||||
if confirm:
|
||||
self.clear_scene()
|
||||
|
||||
def on_action_fit_scene(self):
|
||||
self.fit_rect(self.scene.itemsBoundingRect())
|
||||
|
||||
|
|
@ -229,12 +277,12 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
|
||||
def on_action_undo(self):
|
||||
logger.debug('Undo: %s' % self.undo_stack.undoText())
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.undo()
|
||||
|
||||
def on_action_redo(self):
|
||||
logger.debug('Redo: %s' % self.undo_stack.redoText())
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.redo()
|
||||
|
||||
def on_action_select_all(self):
|
||||
|
|
@ -245,7 +293,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
|
||||
def on_action_delete_items(self):
|
||||
logger.debug('Deleting items...')
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.push(
|
||||
commands.DeleteItems(
|
||||
self.scene, self.scene.selectedItems(user_only=True)))
|
||||
|
|
@ -281,6 +329,9 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
def on_action_arrange_optimal(self):
|
||||
self.scene.arrange_optimal()
|
||||
|
||||
def on_action_arrange_square(self):
|
||||
self.scene.arrange_square()
|
||||
|
||||
def on_action_change_opacity(self):
|
||||
images = list(filter(
|
||||
lambda item: item.is_image,
|
||||
|
|
@ -305,33 +356,50 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.scene.flip_items(vertical=True)
|
||||
|
||||
def on_action_reset_scale(self):
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.push(commands.ResetScale(
|
||||
self.scene.selectedItems(user_only=True)))
|
||||
|
||||
def on_action_reset_rotation(self):
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.push(commands.ResetRotation(
|
||||
self.scene.selectedItems(user_only=True)))
|
||||
|
||||
def on_action_reset_flip(self):
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.push(commands.ResetFlip(
|
||||
self.scene.selectedItems(user_only=True)))
|
||||
|
||||
def on_action_reset_crop(self):
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.push(commands.ResetCrop(
|
||||
self.scene.selectedItems(user_only=True)))
|
||||
|
||||
def on_action_reset_transforms(self):
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
self.undo_stack.push(commands.ResetTransforms(
|
||||
self.scene.selectedItems(user_only=True)))
|
||||
|
||||
def on_action_show_color_gamut(self):
|
||||
widgets.color_gamut.GamutDialog(self, self.scene.selectedItems()[0])
|
||||
|
||||
def on_action_sample_color(self):
|
||||
self.cancel_active_modes()
|
||||
logger.debug('Entering sample color mode')
|
||||
self.viewport().setCursor(Qt.CursorShape.CrossCursor)
|
||||
self.active_mode = self.SAMPLE_COLOR_MODE
|
||||
|
||||
if self.scene.has_multi_selection():
|
||||
# We don't want to sample the multi select item, so
|
||||
# temporarily send it to the back:
|
||||
self.scene.multi_select_item.lower_behind_selection()
|
||||
|
||||
pos = self.mapFromGlobal(self.cursor().pos())
|
||||
self.sample_color_widget = widgets.SampleColorWidget(
|
||||
self,
|
||||
pos,
|
||||
self.scene.sample_color_at(self.mapToScene(pos)))
|
||||
|
||||
def on_items_loaded(self, value):
|
||||
logger.debug('On items loaded: add queued items')
|
||||
self.scene.add_queued_items()
|
||||
|
|
@ -348,6 +416,13 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.scene.add_queued_items()
|
||||
self.on_action_fit_scene()
|
||||
|
||||
def on_action_open_recent_file(self, filename):
|
||||
confirm = self.get_confirmation_unsaved_changes(
|
||||
'There are unsaved changes. '
|
||||
'Are you sure you want to open a new scene?')
|
||||
if confirm:
|
||||
self.open_from_file(filename)
|
||||
|
||||
def open_from_file(self, filename):
|
||||
logger.info(f'Opening file {filename}')
|
||||
self.clear_scene()
|
||||
|
|
@ -356,13 +431,19 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.worker.progress.connect(self.on_items_loaded)
|
||||
self.worker.finished.connect(self.on_loading_finished)
|
||||
self.progress = widgets.BeeProgressDialog(
|
||||
'Loading %s' % filename,
|
||||
f'Loading {filename}',
|
||||
worker=self.worker,
|
||||
parent=self)
|
||||
self.worker.start()
|
||||
|
||||
def on_action_open(self):
|
||||
self.scene.cancel_crop_mode()
|
||||
confirm = self.get_confirmation_unsaved_changes(
|
||||
'There are unsaved changes. '
|
||||
'Are you sure you want to open a new scene?')
|
||||
if not confirm:
|
||||
return
|
||||
|
||||
self.cancel_active_modes()
|
||||
filename, f = QtWidgets.QFileDialog.getOpenFileName(
|
||||
parent=self,
|
||||
caption='Open file',
|
||||
|
|
@ -390,13 +471,13 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
fileio.save_bee, filename, self.scene, create_new=create_new)
|
||||
self.worker.finished.connect(self.on_saving_finished)
|
||||
self.progress = widgets.BeeProgressDialog(
|
||||
'Saving %s' % filename,
|
||||
f'Saving {filename}',
|
||||
worker=self.worker,
|
||||
parent=self)
|
||||
self.worker.start()
|
||||
|
||||
def on_action_save_as(self):
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
directory = os.path.dirname(self.filename) if self.filename else None
|
||||
filename, f = QtWidgets.QFileDialog.getSaveFileName(
|
||||
parent=self,
|
||||
|
|
@ -407,7 +488,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.do_save(filename, create_new=True)
|
||||
|
||||
def on_action_save(self):
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
if not self.filename:
|
||||
self.on_action_save_as()
|
||||
else:
|
||||
|
|
@ -441,7 +522,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.worker = fileio.ThreadedIO(exporter.export, filename)
|
||||
self.worker.finished.connect(self.on_export_finished)
|
||||
self.progress = widgets.BeeProgressDialog(
|
||||
'Exporting %s' % filename,
|
||||
f'Exporting {filename}',
|
||||
worker=self.worker,
|
||||
parent=self)
|
||||
self.worker.start()
|
||||
|
|
@ -454,15 +535,51 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
'Problem writing file',
|
||||
f'<p>Problem writing file {filename}</p><p>{err_msg}</p>')
|
||||
|
||||
def on_action_export_images(self):
|
||||
directory = os.path.dirname(self.filename) if self.filename else None
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(
|
||||
parent=self,
|
||||
caption='Export Images',
|
||||
directory=directory)
|
||||
|
||||
if not directory:
|
||||
return
|
||||
|
||||
logger.debug(f'Got export directory {directory}')
|
||||
self.exporter = ImagesToDirectoryExporter(self.scene, directory)
|
||||
self.worker = fileio.ThreadedIO(self.exporter.export)
|
||||
self.worker.user_input_required.connect(
|
||||
self.on_export_images_file_exists)
|
||||
self.worker.finished.connect(self.on_export_finished)
|
||||
self.progress = widgets.BeeProgressDialog(
|
||||
f'Exporting to {directory}',
|
||||
worker=self.worker,
|
||||
parent=self)
|
||||
self.worker.start()
|
||||
|
||||
def on_export_images_file_exists(self, filename):
|
||||
dlg = widgets.ExportImagesFileExistsDialog(self, filename)
|
||||
if dlg.exec() == QtWidgets.QDialog.DialogCode.Accepted:
|
||||
self.exporter.handle_existing = dlg.get_answer()
|
||||
directory = self.exporter.dirname
|
||||
self.progress = widgets.BeeProgressDialog(
|
||||
f'Exporting to {directory}',
|
||||
worker=self.worker,
|
||||
parent=self)
|
||||
self.worker.start()
|
||||
|
||||
def on_action_quit(self):
|
||||
logger.info('User quit. Exiting...')
|
||||
self.app.quit()
|
||||
confirm = self.get_confirmation_unsaved_changes(
|
||||
'There are unsaved changes. Are you sure you want to quit?')
|
||||
if confirm:
|
||||
logger.info('User quit. Exiting...')
|
||||
self.app.quit()
|
||||
|
||||
def on_action_settings(self):
|
||||
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)
|
||||
|
|
@ -493,15 +610,14 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
errornames = [
|
||||
f'<li>{fn}</li>' for fn in errors]
|
||||
errornames = '<ul>%s</ul>' % '\n'.join(errornames)
|
||||
msg = ('{errors} image(s) out of {total} '
|
||||
'could not be opened:'.format(
|
||||
errors=len(errors), total=len(errors)))
|
||||
num = len(errors)
|
||||
msg = f'{num} image(s) could not be opened.<br/>'
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
'Problem loading images',
|
||||
msg + errornames)
|
||||
msg + IMG_LOADING_ERROR_MSG + errornames)
|
||||
self.scene.add_queued_items()
|
||||
self.scene.arrange_optimal()
|
||||
self.scene.arrange_default()
|
||||
self.undo_stack.endMacro()
|
||||
if new_scene:
|
||||
self.on_action_fit_scene()
|
||||
|
|
@ -509,6 +625,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,
|
||||
|
|
@ -526,6 +643,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.worker.start()
|
||||
|
||||
def on_action_insert_images(self):
|
||||
self.cancel_active_modes()
|
||||
formats = self.get_supported_image_formats(QtGui.QImageReader)
|
||||
logger.debug(f'Supported image types for reading: {formats}')
|
||||
filenames, f = QtWidgets.QFileDialog.getOpenFileNames(
|
||||
|
|
@ -535,6 +653,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.do_insert_images(filenames)
|
||||
|
||||
def on_action_insert_text(self):
|
||||
self.cancel_active_modes()
|
||||
item = BeeTextItem()
|
||||
pos = self.mapToScene(self.mapFromGlobal(self.cursor().pos()))
|
||||
item.setScale(1 / self.get_scale())
|
||||
|
|
@ -542,7 +661,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
|
||||
def on_action_copy(self):
|
||||
logger.debug('Copying to clipboard...')
|
||||
self.scene.cancel_crop_mode()
|
||||
self.cancel_active_modes()
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
items = self.scene.selectedItems(user_only=True)
|
||||
|
||||
|
|
@ -560,6 +679,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
'beeref/items', QtCore.QByteArray.number(len(items)))
|
||||
|
||||
def on_action_paste(self):
|
||||
self.cancel_active_modes()
|
||||
logger.debug('Pasting from clipboard...')
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
pos = self.mapToScene(self.mapFromGlobal(self.cursor().pos()))
|
||||
|
|
@ -587,7 +707,10 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
item.setScale(1 / self.get_scale())
|
||||
self.undo_stack.push(commands.InsertItems(self.scene, [item], pos))
|
||||
return
|
||||
logger.info('No image data or text in clipboard')
|
||||
|
||||
msg = 'No image data or text in clipboard or image too big'
|
||||
logger.info(msg)
|
||||
widgets.BeeNotification(self, msg)
|
||||
|
||||
def on_action_open_settings_dir(self):
|
||||
dirname = os.path.dirname(self.settings.fileName())
|
||||
|
|
@ -601,8 +724,21 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.scene.has_selection())
|
||||
self.actiongroup_set_enabled('active_when_single_image',
|
||||
self.scene.has_single_image_selection())
|
||||
|
||||
if self.scene.has_selection():
|
||||
item = self.scene.selectedItems(user_only=True)[0]
|
||||
grayscale = getattr(item, 'grayscale', False)
|
||||
actions.actions['grayscale'].qaction.setChecked(grayscale)
|
||||
self.viewport().repaint()
|
||||
|
||||
def on_cursor_changed(self, cursor):
|
||||
if self.active_mode is None:
|
||||
self.viewport().setCursor(cursor)
|
||||
|
||||
def on_cursor_cleared(self):
|
||||
if self.active_mode is None:
|
||||
self.viewport().unsetCursor()
|
||||
|
||||
def recalc_scene_rect(self):
|
||||
"""Resize the scene rectangle so that it is always one view width
|
||||
wider than all items' bounding box at each side and one view
|
||||
|
|
@ -698,34 +834,73 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.reset_previous_transform()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
self.zoom(event.angleDelta().y(), event.position())
|
||||
event.accept()
|
||||
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 action == 'pan_horizontal':
|
||||
self.pan(QtCore.QPointF(0, 0.5 * delta))
|
||||
event.accept()
|
||||
return
|
||||
if action == 'pan_vertical':
|
||||
self.pan(QtCore.QPointF(0.5 * delta, 0))
|
||||
event.accept()
|
||||
return
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self.mousePressEventMainControls(event):
|
||||
return
|
||||
|
||||
if (event.button() == Qt.MouseButton.MiddleButton
|
||||
and event.modifiers() == Qt.KeyboardModifier.ControlModifier):
|
||||
self.zoom_active = True
|
||||
self.event_start = event.position()
|
||||
self.event_anchor = event.position()
|
||||
if self.active_mode == self.SAMPLE_COLOR_MODE:
|
||||
if (event.button() == Qt.MouseButton.LeftButton):
|
||||
color = self.scene.sample_color_at(
|
||||
self.mapToScene(event.pos()))
|
||||
if color:
|
||||
name = qcolor_to_hex(color)
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
clipboard.setText(name)
|
||||
self.scene.internal_clipboard = []
|
||||
msg = f'Copied color to clipboard: {name}'
|
||||
logger.debug(msg)
|
||||
widgets.BeeNotification(self, msg)
|
||||
else:
|
||||
logger.debug('No color found')
|
||||
self.cancel_sample_color_mode()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if (event.button() == Qt.MouseButton.MiddleButton
|
||||
or (event.button() == Qt.MouseButton.LeftButton
|
||||
and event.modifiers() == Qt.KeyboardModifier.AltModifier)):
|
||||
self.pan_active = True
|
||||
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.setCursor(Qt.CursorShape.ClosedHandCursor)
|
||||
self.event_anchor = event.position()
|
||||
self.event_inverted = inverted
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if action == 'pan':
|
||||
logger.trace('Begin pan')
|
||||
self.active_mode = self.PAN_MODE
|
||||
self.event_start = event.position()
|
||||
self.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
|
||||
# ClosedHandCursor and OpenHandCursor don't work, but I
|
||||
# don't know if that's only on my system or a general
|
||||
# problem. It works with other cursors.
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.pan_active:
|
||||
if self.active_mode == self.PAN_MODE:
|
||||
self.reset_previous_transform()
|
||||
pos = event.position()
|
||||
self.pan(self.event_start - pos)
|
||||
|
|
@ -733,27 +908,37 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
event.accept()
|
||||
return
|
||||
|
||||
if self.zoom_active:
|
||||
if self.active_mode == self.ZOOM_MODE:
|
||||
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()
|
||||
return
|
||||
|
||||
if self.active_mode == self.SAMPLE_COLOR_MODE:
|
||||
self.sample_color_widget.update(
|
||||
event.position(),
|
||||
self.scene.sample_color_at(self.mapToScene(event.pos())))
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if self.mouseMoveEventMainControls(event):
|
||||
return
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.pan_active:
|
||||
self.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.pan_active = False
|
||||
if self.active_mode == self.PAN_MODE:
|
||||
logger.trace('End pan')
|
||||
self.viewport().unsetCursor()
|
||||
self.active_mode = None
|
||||
event.accept()
|
||||
return
|
||||
if self.zoom_active:
|
||||
self.zoom_active = False
|
||||
if self.active_mode == self.ZOOM_MODE:
|
||||
self.active_mode = None
|
||||
event.accept()
|
||||
return
|
||||
if self.mouseReleaseEventMainControls(event):
|
||||
|
|
@ -768,4 +953,8 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
def keyPressEvent(self, event):
|
||||
if self.keyPressEventMainControls(event):
|
||||
return
|
||||
if self.active_mode == self.SAMPLE_COLOR_MODE:
|
||||
self.cancel_sample_color_mode()
|
||||
event.accept()
|
||||
return
|
||||
super().keyPressEvent(event)
|
||||
|
|
|
|||
|
|
@ -13,15 +13,20 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from importlib.resources import files as rsc_files
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from PyQt6 import QtCore, QtWidgets
|
||||
from PyQt6 import QtCore, QtWidgets, QtGui
|
||||
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__)
|
||||
|
|
@ -39,6 +44,7 @@ class BeeProgressDialog(QtWidgets.QProgressDialog):
|
|||
worker.begin_processing.connect(self.on_begin_processing)
|
||||
worker.progress.connect(self.on_progress)
|
||||
worker.finished.connect(self.on_finished)
|
||||
worker.user_input_required.connect(self.on_finished)
|
||||
self.canceled.connect(worker.on_canceled)
|
||||
|
||||
def on_progress(self, value):
|
||||
|
|
@ -49,7 +55,7 @@ class BeeProgressDialog(QtWidgets.QProgressDialog):
|
|||
logger.debug(f'Beginn progress dialog: {value}')
|
||||
self.setMaximum(value)
|
||||
|
||||
def on_finished(self, filename, errors):
|
||||
def on_finished(self, *args, **kwargs):
|
||||
logger.debug('Finished progress dialog')
|
||||
self.setValue(self.maximum())
|
||||
self.reset()
|
||||
|
|
@ -61,20 +67,18 @@ class HelpDialog(QtWidgets.QDialog):
|
|||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f'{constants.APPNAME} Help')
|
||||
docdir = os.path.join(os.path.dirname(__file__),
|
||||
'..',
|
||||
'documentation')
|
||||
|
||||
tabs = QtWidgets.QTabWidget()
|
||||
|
||||
# Controls
|
||||
with open(os.path.join(docdir, 'controls.html')) as f:
|
||||
controls_txt = f.read()
|
||||
controls = QtWidgets.QLabel(controls_txt)
|
||||
controls.setTextInteractionFlags(
|
||||
controls_txt = rsc_files(
|
||||
'beeref.documentation').joinpath('controls.html').read_text()
|
||||
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()
|
||||
|
|
@ -239,3 +243,96 @@ class ChangeOpacityDialog(QtWidgets.QDialog):
|
|||
def reject(self):
|
||||
self.command.undo()
|
||||
return super().reject()
|
||||
|
||||
|
||||
class BeeNotification(QtWidgets.QWidget):
|
||||
def __init__(self, parent, text):
|
||||
super().__init__(parent)
|
||||
self.label = QtWidgets.QLabel(text)
|
||||
self.setObjectName('BeeNotification')
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
self.setAutoFillBackground(True)
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
self.setLayout(layout)
|
||||
color = constants.COLORS['Active:Window']
|
||||
self.setStyleSheet(
|
||||
f'background-color: rgba({color[0]}, {color[1]}, {color[2]}, 0.9);'
|
||||
'padding: 0.7em;'
|
||||
'border-radius: 5px;')
|
||||
self.show()
|
||||
# We only get own width after showing it;
|
||||
# updateGeometry doesn't work on hidden widgets
|
||||
x = (parent.width() - self.width()) / 2
|
||||
self.move(int(x), 10)
|
||||
|
||||
QtCore.QTimer.singleShot(1000 * 3, self.deleteLater)
|
||||
|
||||
|
||||
class SampleColorWidget(QtWidgets.QWidget):
|
||||
|
||||
OFFSET = 10 # Offset from mouse pointer
|
||||
SIZE = 50
|
||||
NONE_COLOR = QtGui.QColor(0, 0, 0, 0)
|
||||
|
||||
def __init__(self, parent, pos, color):
|
||||
super().__init__(parent)
|
||||
self.color = color
|
||||
self.set_pos(pos)
|
||||
self.show()
|
||||
|
||||
def set_pos(self, pos):
|
||||
self.setGeometry(int(pos.x() + self.OFFSET),
|
||||
int(pos.y() + self.OFFSET),
|
||||
self.SIZE, self.SIZE)
|
||||
|
||||
def paintEvent(self, event):
|
||||
color = self.color if self.color else self.NONE_COLOR
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setBrush(QtGui.QBrush(color))
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRect(0, 0, self.SIZE, self.SIZE)
|
||||
|
||||
def update(self, pos, color):
|
||||
self.set_pos(pos)
|
||||
self.color = color
|
||||
self.repaint()
|
||||
|
||||
|
||||
class ExportImagesFileExistsDialog(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, parent, filename):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle('File exists')
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
label = QtWidgets.QLabel(
|
||||
f'File already exists:\n{filename}')
|
||||
layout.addWidget(label)
|
||||
|
||||
choices = (('skip', 'Skip this file'),
|
||||
('skip_all', 'Skip all existing files'),
|
||||
('overwrite', 'Overwrite this file'),
|
||||
('overwrite_all', 'Overwrite all existing files'))
|
||||
|
||||
self.radio_buttons = {}
|
||||
for (value, label) in choices:
|
||||
btn = QtWidgets.QRadioButton(label)
|
||||
self.radio_buttons[value] = btn
|
||||
layout.addWidget(btn)
|
||||
self.radio_buttons['skip'].setChecked(True)
|
||||
|
||||
# Bottom row of buttons
|
||||
buttons = QtWidgets.QDialogButtonBox(
|
||||
QtWidgets.QDialogButtonBox.StandardButton.Ok
|
||||
| QtWidgets.QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.rejected.connect(self.reject)
|
||||
buttons.accepted.connect(self.accept)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def get_answer(self):
|
||||
for value, btn in self.radio_buttons.items():
|
||||
if btn.isChecked():
|
||||
return value
|
||||
|
|
|
|||
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,20 +16,20 @@
|
|||
from functools import partial
|
||||
import logging
|
||||
|
||||
from PyQt6 import QtWidgets, QtCore, QtGui
|
||||
from PyQt6 import QtWidgets
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
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):
|
||||
TITLE = None
|
||||
HELPTEXT = None
|
||||
KEY = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
|
@ -47,23 +47,31 @@ 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):
|
||||
if self.ignore_value_changed:
|
||||
return
|
||||
|
||||
value = self.convert_value_from_qt(value)
|
||||
if value != self.settings.valueOrDefault(self.KEY):
|
||||
logger.debug(f'Setting {self.KEY} changed to: {value}')
|
||||
self.settings.setValue(self.KEY, value)
|
||||
self.update_title()
|
||||
|
||||
def convert_value_from_qt(self, value):
|
||||
return value
|
||||
|
||||
def on_restore_defaults(self):
|
||||
new_value = self.settings.valueOrDefault(self.KEY)
|
||||
self.ignore_value_changed = True
|
||||
self.set_value(new_value)
|
||||
self.ignore_value_changed = False
|
||||
self.update_title()
|
||||
|
||||
|
||||
class RadioGroup(GroupBase):
|
||||
TITLE = None
|
||||
HELPTEXT = None
|
||||
KEY = None
|
||||
OPTIONS = None
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -83,38 +91,59 @@ class RadioGroup(GroupBase):
|
|||
self.ignore_value_changed = False
|
||||
self.layout.addStretch(100)
|
||||
|
||||
def on_restore_defaults(self):
|
||||
new_value = self.settings.valueOrDefault(self.KEY)
|
||||
self.ignore_value_changed = True
|
||||
for value, btn in self.buttons.items():
|
||||
btn.setChecked(value == new_value)
|
||||
self.ignore_value_changed = False
|
||||
self.update_title()
|
||||
def set_value(self, value):
|
||||
for old_value, btn in self.buttons.items():
|
||||
btn.setChecked(old_value == value)
|
||||
|
||||
|
||||
class IntegerGroup(GroupBase):
|
||||
TITLE = None
|
||||
HELPTEXT = None
|
||||
KEY = None
|
||||
MIN = None
|
||||
MAX = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.input = QtWidgets.QSpinBox()
|
||||
self.input.setValue(self.settings.valueOrDefault(self.KEY))
|
||||
self.input.setRange(self.MIN, self.MAX)
|
||||
self.set_value(self.settings.valueOrDefault(self.KEY))
|
||||
self.input.valueChanged.connect(self.on_value_changed)
|
||||
self.layout.addWidget(self.input)
|
||||
self.layout.addStretch(100)
|
||||
self.ignore_value_changed = False
|
||||
|
||||
def on_restore_defaults(self):
|
||||
new_value = self.settings.valueOrDefault(self.KEY)
|
||||
self.ignore_value_changed = True
|
||||
self.input.setValue(new_value)
|
||||
def set_value(self, value):
|
||||
self.input.setValue(value)
|
||||
|
||||
|
||||
class SingleCheckboxGroup(GroupBase):
|
||||
LABEL = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.input = QtWidgets.QCheckBox(self.LABEL)
|
||||
self.set_value(self.settings.valueOrDefault(self.KEY))
|
||||
self.input.checkStateChanged.connect(self.on_value_changed)
|
||||
self.layout.addWidget(self.input)
|
||||
self.layout.addStretch(100)
|
||||
self.ignore_value_changed = False
|
||||
self.update_title()
|
||||
|
||||
def set_value(self, value):
|
||||
self.input.setChecked(value)
|
||||
|
||||
def convert_value_from_qt(self, value):
|
||||
return value == Qt.CheckState.Checked
|
||||
|
||||
|
||||
class ArrangeDefaultWidget(RadioGroup):
|
||||
TITLE = 'Default Arrange Method:'
|
||||
HELPTEXT = ('How images are arranged when inserted in batch')
|
||||
KEY = 'Items/arrange_default'
|
||||
OPTIONS = (
|
||||
('optimal', 'Optimal', 'Arrange Optimal'),
|
||||
('horizontal', 'Horizontal (by filename)',
|
||||
'Arrange Horizontal (by filename)'),
|
||||
('vertical', 'Vertical (by filename)',
|
||||
'Arrange Vertical (by filename)'),
|
||||
('square', 'Square (by filename)', 'Arrannge Square (by filename)'))
|
||||
|
||||
|
||||
class ImageStorageFormatWidget(RadioGroup):
|
||||
|
|
@ -139,6 +168,24 @@ class ArrangeGapWidget(IntegerGroup):
|
|||
MAX = 200
|
||||
|
||||
|
||||
class AllocationLimitWidget(IntegerGroup):
|
||||
TITLE = 'Maximum Image Size:'
|
||||
HELPTEXT = ('The maximum image size that can be loaded (in megabytes). '
|
||||
'Set to 0 for no limitation.')
|
||||
KEY = 'Items/image_allocation_limit'
|
||||
MIN = 0
|
||||
MAX = 10000
|
||||
|
||||
|
||||
class ConfirmCloseUnsavedWidget(SingleCheckboxGroup):
|
||||
TITLE = 'Confirm when closing an unsaved file:'
|
||||
HELPTEXT = (
|
||||
'When about to close an unsaved file, should BeeRef ask for '
|
||||
'confirmation?')
|
||||
LABEL = 'Confirm when closing'
|
||||
KEY = 'Save/confirm_close_unsaved'
|
||||
|
||||
|
||||
class SettingsDialog(QtWidgets.QDialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
|
@ -149,10 +196,19 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||
misc = QtWidgets.QWidget()
|
||||
misc_layout = QtWidgets.QGridLayout()
|
||||
misc.setLayout(misc_layout)
|
||||
misc_layout.addWidget(ImageStorageFormatWidget(), 0, 0)
|
||||
misc_layout.addWidget(ArrangeGapWidget(), 0, 1)
|
||||
misc_layout.addWidget(ConfirmCloseUnsavedWidget(), 0, 0)
|
||||
tabs.addTab(misc, '&Miscellaneous')
|
||||
|
||||
# Images & Items
|
||||
items = QtWidgets.QWidget()
|
||||
items_layout = QtWidgets.QGridLayout()
|
||||
items.setLayout(items_layout)
|
||||
items_layout.addWidget(ImageStorageFormatWidget(), 0, 0)
|
||||
items_layout.addWidget(AllocationLimitWidget(), 0, 1)
|
||||
items_layout.addWidget(ArrangeGapWidget(), 1, 0)
|
||||
items_layout.addWidget(ArrangeDefaultWidget(), 1, 1)
|
||||
tabs.addTab(items, '&Images && Items')
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
layout.addWidget(tabs)
|
||||
|
|
@ -178,228 +234,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 data(self, index, role):
|
||||
if (role == QtCore.Qt.ItemDataRole.BackgroundRole
|
||||
and index.row() % 2):
|
||||
return QtGui.QColor(*constants.COLORS['Table:AlternativeRow'])
|
||||
else:
|
||||
return super().data(index, role)
|
||||
|
||||
def setData(self, index, value, role, remove_from_other=None):
|
||||
result = self.sourceModel().setData(
|
||||
self.mapToSource(index),
|
||||
value,
|
||||
role,
|
||||
remove_from_other=remove_from_other)
|
||||
return result
|
||||
|
||||
|
||||
class KeyboardShortcutsView(QtWidgets.QTableView):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setMinimumSize(QtCore.QSize(400, 200))
|
||||
self.setItemDelegate(KeyboardShortcutsDelegate())
|
||||
self.setShowGrid(False)
|
||||
self.setModel(KeyboardShortcutsProxy())
|
||||
self.horizontalHeader().setSectionResizeMode(
|
||||
0, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
||||
self.horizontalHeader().setSectionResizeMode(
|
||||
1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.setSelectionMode(
|
||||
QtWidgets.QHeaderView.SelectionMode.SingleSelection)
|
||||
settings_events.restore_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()
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class WelcomeOverlay(MainControlsMixin, QtWidgets.QWidget):
|
|||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.control_target = parent
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
||||
self.setAutoFillBackground(True)
|
||||
self.init_main_controls(main_window=parent.parent)
|
||||
|
||||
# Recent files
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="146.0" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect width="146.0" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#round)"><rect width="48.1" height="20" fill="#555"/><rect x="48.1" width="97.9" height="20" fill="#007ec6"/><rect width="146.0" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><text x="250.5" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="381.0" lengthAdjust="spacing">Python</text><text x="250.5" y="140" transform="scale(0.1)" textLength="381.0" lengthAdjust="spacing">Python</text><text x="960.5000000000001" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="879.0" lengthAdjust="spacing">3.9 | 3.10 | 3.11</text><text x="960.5000000000001" y="140" transform="scale(0.1)" textLength="879.0" lengthAdjust="spacing">3.9 | 3.10 | 3.11</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="146.0" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect width="146.0" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#round)"><rect width="48.1" height="20" fill="#555"/><rect x="48.1" width="97.9" height="20" fill="#007ec6"/><rect width="146.0" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><text x="250.5" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="381.0" lengthAdjust="spacing">Python</text><text x="250.5" y="140" transform="scale(0.1)" textLength="381.0" lengthAdjust="spacing">Python</text><text x="960.5000000000001" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="879.0" lengthAdjust="spacing">3.9 | 3.10 | 3.11 | 3.12</text><text x="960.5000000000001" y="140" transform="scale(0.1)" textLength="879.0" lengthAdjust="spacing">3.9 | 3.10 | 3.11 | 3.12</text></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -4,7 +4,7 @@
|
|||
<launchable type="desktop-id">org.beeref.BeeRef.desktop</launchable>
|
||||
<name>BeeRef</name>
|
||||
<developer_name>BeeRef</developer_name>
|
||||
<summary>A Simple Reference Image Viewer</summary>
|
||||
<summary>A simple reference image viewer</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<url type="homepage">https://beeref.org/</url>
|
||||
|
|
@ -14,10 +14,14 @@
|
|||
</description>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">https://github.com/rbreu/beeref/blob/main/images/screenshot.png?raw=true</screenshot>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/rbreu/beeref/main/images/screenshot.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<!-- <release version="0.3.4-dev" date="unreleased"></release> -->
|
||||
<release version="0.3.3" date="2024-05-05"></release>
|
||||
<release version="0.3.2" date="2024-01-21"></release>
|
||||
<release version="0.3.1" date="2023-12-10"></release>
|
||||
<release version="0.3.0" date="2023-11-23"></release>
|
||||
|
|
|
|||
37
pyproject.toml
Normal file
37
pyproject.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=61.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["beeref*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["*.html", "*.png"]
|
||||
|
||||
[project]
|
||||
name = "BeeRef"
|
||||
version = "0.3.4-dev"
|
||||
description = "A simple reference image viewer"
|
||||
readme = "README.rst"
|
||||
license = {file = "LICENSE"}
|
||||
authors = [
|
||||
{ name = "Rebecca Breu", email = "rebecca@rbreu.de" },
|
||||
]
|
||||
requires-python = ">=3.9,<3.13"
|
||||
dependencies = [
|
||||
"exif>=1.3.5,<=1.6.0",
|
||||
"lxml==5.1.0",
|
||||
"pyQt6-Qt6>=6.7.0,<=6.7.0",
|
||||
"pyQt6>=6.7.0,<=6.7.0",
|
||||
"rectangle-packer>=2.0.1,<=2.0.2",
|
||||
]
|
||||
|
||||
[project.gui-scripts]
|
||||
beeref = "beeref.__main__:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://beeref.org/"
|
||||
Repository = "https://github.com/rbreu/beeref"
|
||||
# Documentation =
|
||||
Issues = "https://github.com/rbreu/beeref/issues"
|
||||
Changelog = "https://github.com/rbreu/beeref/blob/main/CHANGELOG.rst"
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# For the test action on github
|
||||
# For the build action on github
|
||||
|
||||
pyinstaller==6.3.0
|
||||
pyinstaller==6.6.0
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
-r test.txt
|
||||
-r build.txt
|
||||
|
||||
flake8==6.1.0
|
||||
flake8==7.0.0
|
||||
pybadges==3.0.1
|
||||
yamllint==1.33.0
|
||||
yamllint==1.35.1
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# For the test action on github
|
||||
coverage==7.3.2
|
||||
coverage==7.5.1
|
||||
httpretty==1.1.4
|
||||
pytest==7.4.3
|
||||
pytest-qt==4.2.0
|
||||
pytest-cov==4.1.0
|
||||
pytest==8.2.0
|
||||
pytest-qt==4.4.0
|
||||
pytest-cov==5.0.0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
[flake8]
|
||||
exclude =
|
||||
squashfs-root
|
||||
build
|
||||
dist
|
||||
|
||||
[coverage:run]
|
||||
source = beeref
|
||||
|
||||
[tool:pytest]
|
||||
norecursedirs = squashfs-root
|
||||
addopts = --cov-report html --cov-config=setup.cfg
|
||||
34
setup.py
34
setup.py
|
|
@ -1,35 +1,3 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='BeeRef',
|
||||
version='0.3.2',
|
||||
author='Rebecca Breu',
|
||||
author_email='rebecca@rbreu.de',
|
||||
url='https://github.com/rbreu/beeref',
|
||||
license='LICENSE',
|
||||
description='A simple reference image viewer',
|
||||
install_requires=[
|
||||
'pyQt6>=6.5.0,<=6.6.1',
|
||||
'pyQt6-Qt6>=6.5.0,<=6.6.1',
|
||||
'rectangle-packer>=2.0.1,<=2.0.2',
|
||||
'exif>=1.3.5,<=1.6.0',
|
||||
],
|
||||
packages=[
|
||||
'beeref',
|
||||
'beeref.actions',
|
||||
'beeref.assets',
|
||||
'beeref.documentation',
|
||||
'beeref.fileio',
|
||||
'beeref.widgets',
|
||||
],
|
||||
entry_points={
|
||||
'gui_scripts': [
|
||||
'beeref = beeref.__main__:main'
|
||||
]
|
||||
},
|
||||
include_package_data=True,
|
||||
package_data={
|
||||
'beeref.assets': ['*.png'],
|
||||
'beeref': ['documentation/*.html'],
|
||||
},
|
||||
)
|
||||
setup()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -19,7 +20,7 @@ class FooWidget(QtWidgets.QWidget, ActionsMixin):
|
|||
def on_bar(self):
|
||||
pass
|
||||
|
||||
def open_from_file(self):
|
||||
def on_action_open_recent_file(self, filename):
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from beeref.config import CommandlineArgs
|
||||
from PyQt6 import QtGui
|
||||
|
||||
from beeref.config.settings import CommandlineArgs
|
||||
|
||||
|
||||
def test_command_line_args_singleton():
|
||||
|
|
@ -13,7 +16,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)
|
||||
|
|
@ -35,6 +38,52 @@ def test_command_line_args_get_unknown():
|
|||
CommandlineArgs._instance = None
|
||||
|
||||
|
||||
def test_settings_on_startup_sets_alloc_from_settings(settings):
|
||||
settings.setValue('Items/image_allocation_limit', 66)
|
||||
QtGui.QImageReader.setAllocationLimit(100)
|
||||
settings.on_startup()
|
||||
assert QtGui.QImageReader.allocationLimit() == 66
|
||||
|
||||
|
||||
def test_settings_on_startup_sets_alloc_from_environment(settings):
|
||||
settings.setValue('Items/image_allocation_limit', 66)
|
||||
os.environ['QT_IMAGEIO_MAXALLOC'] = '42'
|
||||
QtGui.QImageReader.setAllocationLimit(100)
|
||||
settings.on_startup()
|
||||
assert QtGui.QImageReader.allocationLimit() == 42
|
||||
|
||||
|
||||
def test_settings_set_value_without_callback(settings):
|
||||
settings.FIELDS = {'foo/bar': {}}
|
||||
settings.setValue('foo/bar', 100)
|
||||
assert settings.value('foo/bar') == 100
|
||||
|
||||
|
||||
def test_settings_set_value_with_callback(settings):
|
||||
foo_callback = MagicMock()
|
||||
settings.FIELDS = {'foo/bar': {'post_save_callback': foo_callback}}
|
||||
settings.setValue('foo/bar', 100)
|
||||
foo_callback.assert_called_once_with(100)
|
||||
assert settings.value('foo/bar') == 100
|
||||
|
||||
|
||||
def test_settings_remove_without_callback(settings):
|
||||
settings.FIELDS = {'foo/bar': {}}
|
||||
settings.remove('foo/bar')
|
||||
assert settings.value('foo/bar') is None
|
||||
|
||||
|
||||
def test_settings_remove_with_callback(settings):
|
||||
foo_callback = MagicMock()
|
||||
settings.FIELDS = {
|
||||
'foo/bar': {'default': 66,
|
||||
'post_save_callback': foo_callback}
|
||||
}
|
||||
settings.remove('foo/bar')
|
||||
foo_callback.assert_called_once_with(66)
|
||||
assert settings.value('foo/bar') is None
|
||||
|
||||
|
||||
def test_settings_value_or_default_gets_default(settings):
|
||||
assert settings.valueOrDefault('Items/image_storage_format') == 'best'
|
||||
|
||||
|
|
@ -117,48 +166,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
|
||||
|
|
@ -30,7 +30,7 @@ def reset_beeref_actions():
|
|||
def commandline_args():
|
||||
config_patcher = patch('beeref.view.commandline_args')
|
||||
config_mock = config_patcher.start()
|
||||
config_mock.filename = None
|
||||
config_mock.filenames = []
|
||||
yield config_mock
|
||||
config_patcher.stop()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
import os
|
||||
import stat
|
||||
from unittest.mock import patch, ANY, MagicMock
|
||||
import pytest
|
||||
|
||||
from PyQt6 import QtGui, QtCore
|
||||
|
||||
from beeref import constants
|
||||
from beeref.items import BeePixmapItem, BeeTextItem
|
||||
from beeref.fileio.errors import BeeFileIOError
|
||||
from beeref.fileio.export import (
|
||||
exporter_registry,
|
||||
SceneToPixmapExporter,
|
||||
|
|
@ -21,446 +13,3 @@ from beeref.fileio.export import (
|
|||
('svg', SceneToSVGExporter)])
|
||||
def test_registry(key, expected):
|
||||
exporter_registry[key] == expected
|
||||
|
||||
|
||||
@patch('beeref.widgets.SceneToPixmapExporterDialog.exec', return_value=True)
|
||||
@patch('beeref.widgets.SceneToPixmapExporterDialog.value',
|
||||
return_value=QtCore.QSize(100, 200))
|
||||
def test_scene_to_pixmap_exporter_get_user_input(value_mock, exec_mock, view):
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
value = exporter.get_user_input(None)
|
||||
assert value is True
|
||||
assert exporter.size == QtCore.QSize(100, 200)
|
||||
|
||||
|
||||
@patch('beeref.widgets.SceneToPixmapExporterDialog.exec', return_value=False)
|
||||
@patch('beeref.widgets.SceneToPixmapExporterDialog.value')
|
||||
def test_scene_to_pixmap_exporter_get_user_input_when_canceled(
|
||||
value_mock, exec_mock, view):
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
value = exporter.get_user_input(None)
|
||||
assert value is False
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_default_size_and_margin(view):
|
||||
item1 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(0, 0))
|
||||
view.scene.addItem(item1)
|
||||
|
||||
item2 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(200, 0))
|
||||
view.scene.addItem(item2)
|
||||
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100)
|
||||
assert (exporter.margin - 9) < 0.000001
|
||||
assert exporter.default_size == QtCore.QSize(318, 118)
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_default_size_and_margin_when_selection(view):
|
||||
item1 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(0, 0))
|
||||
view.scene.addItem(item1)
|
||||
|
||||
item2 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(200, 0))
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100)
|
||||
assert (exporter.margin - 9) < 0.000001
|
||||
assert exporter.default_size == QtCore.QSize(318, 118)
|
||||
|
||||
|
||||
@patch('beeref.scene.BeeGraphicsScene.render')
|
||||
def test_scene_to_pixmap_exporter_render_sets_margins(render_mock, view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32))
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
assert exporter.margin == 36
|
||||
assert exporter.default_size == QtCore.QSize(1072, 1272)
|
||||
|
||||
exporter.size = QtCore.QSize(536, 636)
|
||||
exporter.render_to_image()
|
||||
|
||||
render_mock.assert_called_once_with(
|
||||
ANY,
|
||||
source=QtCore.QRectF(0, 0, 1000, 1200),
|
||||
target=QtCore.QRectF(18, 18, 500, 600))
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_render_renders_scene(view):
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item_img.fill(QtGui.QColor(11, 22, 33))
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
assert exporter.margin == 36
|
||||
assert exporter.default_size == QtCore.QSize(1072, 1272)
|
||||
|
||||
exporter.size = QtCore.QSize(536, 636)
|
||||
image = exporter.render_to_image()
|
||||
assert image.pixel(1, 1) == QtGui.QColor(*constants.COLORS['Scene:Canvas'])
|
||||
assert image.pixel(100, 100) == QtGui.QColor(11, 22, 33)
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_writes_image(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
exporter.export(filename)
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_with_worker(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=False)
|
||||
exporter.export(filename, worker)
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(1)
|
||||
worker.finished.emit.assert_called_once_with(filename, [])
|
||||
with open(filename, 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_with_worker_when_canceled(
|
||||
view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=True)
|
||||
exporter.export(filename, worker)
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_not_called()
|
||||
worker.finished.emit.assert_called_once_with(filename, [])
|
||||
os.path.exists(filename) is False
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_when_file_not_writeable(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
with open(filename, 'w') as f:
|
||||
f.write('foo')
|
||||
os.chmod(filename, stat.S_IREAD)
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
|
||||
with pytest.raises(BeeFileIOError) as e:
|
||||
exporter.export(filename)
|
||||
assert e.filename == filename
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_when_file_not_writeable_with_worker(
|
||||
view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
with open(filename, 'w') as f:
|
||||
f.write('foo')
|
||||
os.chmod(filename, stat.S_IREAD)
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=False)
|
||||
|
||||
exporter.export(filename, worker=worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_not_called()
|
||||
worker.finished.emit.assert_called_once_with(
|
||||
filename, ['Error writing file'])
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_get_user_input(view):
|
||||
item1 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(0, 0))
|
||||
view.scene.addItem(item1)
|
||||
|
||||
item2 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(200, 0))
|
||||
view.scene.addItem(item2)
|
||||
|
||||
assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
value = exporter.get_user_input(None)
|
||||
assert value is True
|
||||
assert exporter.size == QtCore.QSize(318, 118)
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_items(view):
|
||||
item1 = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(20, 30))
|
||||
view.scene.addItem(item1)
|
||||
|
||||
item2 = BeePixmapItem(
|
||||
QtGui.QImage(70, 77, QtGui.QImage.Format.Format_RGB32))
|
||||
item2.setPos(QtCore.QPointF(50, 50))
|
||||
item2.setZValue(-1)
|
||||
view.scene.addItem(item2)
|
||||
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert svg.tag == 'svg'
|
||||
assert svg.get('width') == '200'
|
||||
assert svg.get('height') == '400'
|
||||
assert svg.get('xmlns') == 'http://www.w3.org/2000/svg'
|
||||
assert svg.get('xmlns:xlink') == 'http://www.w3.org/1999/xlink'
|
||||
assert len(svg) == 2
|
||||
|
||||
element = svg[0] # item2
|
||||
assert element.tag == 'image'
|
||||
assert element.get('xlink:href').startswith('data:image/png;base64,iVBOR')
|
||||
assert element.get('width') == '70.0'
|
||||
assert element.get('height') == '77.0'
|
||||
assert element.get('image-rendering') == 'optimizeQuality'
|
||||
assert element.get('transform') == 'rotate(0.0 35.0 25.0)'
|
||||
assert element.get('x') == '35.0'
|
||||
assert element.get('y') == '25.0'
|
||||
assert element.get('opacity') == '1.0'
|
||||
|
||||
element = svg[1] # item1
|
||||
assert element.tag == 'image'
|
||||
assert element.get('width') == '100.0'
|
||||
assert element.get('height') == '110.0'
|
||||
assert element.get('image-rendering') == 'optimizeQuality'
|
||||
assert element.get('transform') == 'rotate(0.0 5.0 5.0)'
|
||||
assert element.get('x') == '5.0'
|
||||
assert element.get('y') == '5.0'
|
||||
assert element.get('opacity') == '1.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_with_crop(view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
item.crop = QtCore.QRectF(20, 25, 30, 33)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'image'
|
||||
assert element.get('width') == '30.0'
|
||||
assert element.get('height') == '33.0'
|
||||
assert element.get('transform') == 'rotate(0.0 -15.0 -20.0)'
|
||||
assert element.get('x') == '5.0'
|
||||
assert element.get('y') == '5.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_with_rotation(view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
item.setRotation(90)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'image'
|
||||
assert element.get('transform') == 'rotate(90.0 115.0 5.0)'
|
||||
assert element.get('x') == '115.0'
|
||||
assert element.get('y') == '5.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_with_opacity(view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
item.setOpacity(0.75)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'image'
|
||||
assert element.get('opacity') == '0.75'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_with_flip(view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
item.do_flip()
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'image'
|
||||
assert element.get('transform') == (
|
||||
'translate(105.0 5.0) scale(-1.0 1)'
|
||||
' translate(-105.0 -5.0) rotate(0.0 105.0 5.0)')
|
||||
assert element.get('x') == '105.0'
|
||||
assert element.get('y') == '5.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_text(view):
|
||||
item = BeeTextItem('foo')
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'text'
|
||||
assert element.text == 'foo'
|
||||
assert element.get('dominant-baseline') == 'hanging'
|
||||
assert 'font-family' in element.get('style')
|
||||
assert element.get('transform') == 'rotate(0.0 5.0 5.0)'
|
||||
assert element.get('x') == '5.0'
|
||||
assert element.get('y') == '5.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_when_file_not_writeable(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
with open(filename, 'w') as f:
|
||||
f.write('foo')
|
||||
os.chmod(filename, stat.S_IREAD)
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
|
||||
with pytest.raises(BeeFileIOError) as e:
|
||||
exporter.export(filename)
|
||||
assert e.filename == filename
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_with_worker(view):
|
||||
item = BeeTextItem('foo')
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
|
||||
worker = MagicMock(canceled=False)
|
||||
svg = exporter.render_to_svg(worker=worker)
|
||||
assert len(svg) == 1
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_with_worker_canceled(view):
|
||||
item = BeeTextItem('foo')
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
|
||||
worker = MagicMock(canceled=True)
|
||||
svg = exporter.render_to_svg(worker=worker)
|
||||
assert svg is None
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_writes_svg(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
exporter.export(filename)
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
assert f.read().startswith(b'<?xml')
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_with_worker(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=False)
|
||||
exporter.export(filename, worker)
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with(filename, [])
|
||||
with open(filename, 'rb') as f:
|
||||
assert f.read().startswith(b'<?xml')
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_with_worker_canceled(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=True)
|
||||
exporter.export(filename, worker)
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with(filename, [])
|
||||
os.path.exists(filename) is False
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_when_file_not_writeable_with_worker(
|
||||
view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
with open(filename, 'w') as f:
|
||||
f.write('foo')
|
||||
os.chmod(filename, stat.S_IREAD)
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=False)
|
||||
|
||||
exporter.export(filename, worker=worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once()
|
||||
args = worker.finished.emit.call_args.args
|
||||
assert args[0] == filename
|
||||
assert len(args[1]) == 1
|
||||
|
|
|
|||
287
tests/fileio/test_export_images_to_directory.py
Normal file
287
tests/fileio/test_export_images_to_directory.py
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
import os
|
||||
import stat
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from PyQt6 import QtGui
|
||||
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref.fileio.errors import BeeFileIOError
|
||||
from beeref.fileio.export import ImagesToDirectoryExporter
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_writes_images(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
item2.save_id = 3
|
||||
view.scene.addItem(item2)
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.export()
|
||||
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
with open(os.path.join(tmpdir, '0004.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_file_exists_no_user_input(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item2)
|
||||
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
|
||||
assert f.write('foo')
|
||||
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.export()
|
||||
|
||||
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'r') as f:
|
||||
assert f.read() == 'foo'
|
||||
|
||||
assert exporter.start_from == 1
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_file_exists_skip(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item2)
|
||||
item3 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item3)
|
||||
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
|
||||
assert f.write('foo')
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'w') as f:
|
||||
assert f.write('bar')
|
||||
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.handle_existing = 'skip'
|
||||
exporter.export()
|
||||
|
||||
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'r') as f:
|
||||
assert f.read() == 'foo'
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'r') as f:
|
||||
assert f.read() == 'bar'
|
||||
|
||||
assert exporter.start_from == 2
|
||||
assert exporter.handle_existing is None
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_file_exists_skip_all(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item2)
|
||||
item3 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item3)
|
||||
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
|
||||
assert f.write('foo')
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'w') as f:
|
||||
assert f.write('bar')
|
||||
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.handle_existing = 'skip_all'
|
||||
exporter.export()
|
||||
|
||||
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'r') as f:
|
||||
assert f.read() == 'foo'
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'r') as f:
|
||||
assert f.read() == 'bar'
|
||||
|
||||
assert exporter.handle_existing == 'skip_all'
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_file_exists_overwrite(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item2)
|
||||
item3 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item3)
|
||||
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
|
||||
assert f.write('foo')
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'w') as f:
|
||||
assert f.write('bar')
|
||||
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.handle_existing = 'overwrite'
|
||||
exporter.export()
|
||||
|
||||
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'r') as f:
|
||||
assert f.read() == 'bar'
|
||||
|
||||
assert exporter.start_from == 2
|
||||
assert exporter.handle_existing is None
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_file_exists_overwrite_all(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item2)
|
||||
item3 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item3)
|
||||
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
|
||||
assert f.write('foo')
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'w') as f:
|
||||
assert f.write('bar')
|
||||
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.handle_existing = 'overwrite_all'
|
||||
exporter.export()
|
||||
|
||||
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
with open(os.path.join(tmpdir, '0002.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
with open(os.path.join(tmpdir, '0003.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
|
||||
assert exporter.handle_existing == 'overwrite_all'
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_with_worker(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
worker = MagicMock(canceled=False)
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.export(worker)
|
||||
|
||||
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_with(0)
|
||||
worker.finished.emit.assert_called_once_with(tmpdir, [])
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_with_worker_when_canceled(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
worker = MagicMock(canceled=True)
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.export(worker)
|
||||
|
||||
assert os.path.exists(os.path.join(tmpdir, '0001.png')) is False
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with(tmpdir, [])
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_with_worker_when_file_exists(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
|
||||
with open(os.path.join(tmpdir, '0001.png'), 'w') as f:
|
||||
assert f.write('foo')
|
||||
|
||||
worker = MagicMock(canceled=False)
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.export(worker)
|
||||
|
||||
imgfilename = os.path.join(tmpdir, '0001.png')
|
||||
with open(imgfilename, 'r') as f:
|
||||
assert f.read() == 'foo'
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_with(0)
|
||||
worker.user_input_required.emit.assert_called_once_with(imgfilename)
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_when_dir_not_writeable(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
|
||||
os.chmod(tmpdir, stat.S_IREAD)
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
|
||||
with pytest.raises(BeeFileIOError) as e:
|
||||
exporter.export()
|
||||
assert e.filename == tmpdir
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_when_dir_not_writeable_w_worker(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
|
||||
os.chmod(tmpdir, stat.S_IREAD)
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
worker = MagicMock(canceled=False)
|
||||
|
||||
exporter.export(worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.finished.emit.assert_called_once()
|
||||
args = worker.finished.emit.call_args.args
|
||||
assert args[0] == tmpdir
|
||||
assert len(args[1]) == 1
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_when_img_not_writeable(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
|
||||
imgfilename = os.path.join(tmpdir, '0001.png')
|
||||
with open(imgfilename, 'w') as f:
|
||||
assert f.write('foo')
|
||||
os.chmod(imgfilename, stat.S_IREAD)
|
||||
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.handle_existing = 'overwrite_all'
|
||||
|
||||
with pytest.raises(BeeFileIOError) as e:
|
||||
exporter.export()
|
||||
assert e.filename == tmpdir
|
||||
|
||||
|
||||
def test_images_to_directory_exporter_export_when_img_not_writeable_w_worker(
|
||||
view, tmpdir, imgdata3x3, imgfilename3x3,):
|
||||
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
|
||||
imgfilename = os.path.join(tmpdir, '0001.png')
|
||||
with open(imgfilename, 'w') as f:
|
||||
assert f.write('foo')
|
||||
os.chmod(imgfilename, stat.S_IREAD)
|
||||
|
||||
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
|
||||
exporter.handle_existing = 'overwrite_all'
|
||||
worker = MagicMock(canceled=False)
|
||||
|
||||
exporter.export(worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.finished.emit.assert_called_once()
|
||||
args = worker.finished.emit.call_args.args
|
||||
assert args[0] == imgfilename
|
||||
assert len(args[1]) == 1
|
||||
181
tests/fileio/test_export_scene_to_pixmap.py
Normal file
181
tests/fileio/test_export_scene_to_pixmap.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import os
|
||||
import stat
|
||||
from unittest.mock import patch, ANY, MagicMock
|
||||
import pytest
|
||||
|
||||
from PyQt6 import QtGui, QtCore
|
||||
|
||||
from beeref import constants
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref.fileio.errors import BeeFileIOError
|
||||
from beeref.fileio.export import SceneToPixmapExporter
|
||||
|
||||
|
||||
@patch('beeref.widgets.SceneToPixmapExporterDialog.exec', return_value=True)
|
||||
@patch('beeref.widgets.SceneToPixmapExporterDialog.value',
|
||||
return_value=QtCore.QSize(100, 200))
|
||||
def test_scene_to_pixmap_exporter_get_user_input(value_mock, exec_mock, view):
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
value = exporter.get_user_input(None)
|
||||
assert value is True
|
||||
assert exporter.size == QtCore.QSize(100, 200)
|
||||
|
||||
|
||||
@patch('beeref.widgets.SceneToPixmapExporterDialog.exec', return_value=False)
|
||||
@patch('beeref.widgets.SceneToPixmapExporterDialog.value')
|
||||
def test_scene_to_pixmap_exporter_get_user_input_when_canceled(
|
||||
value_mock, exec_mock, view):
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
value = exporter.get_user_input(None)
|
||||
assert value is False
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_default_size_and_margin(view):
|
||||
item1 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(0, 0))
|
||||
view.scene.addItem(item1)
|
||||
|
||||
item2 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(200, 0))
|
||||
view.scene.addItem(item2)
|
||||
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100)
|
||||
assert (exporter.margin - 9) < 0.000001
|
||||
assert exporter.default_size == QtCore.QSize(318, 118)
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_default_size_and_margin_when_selection(view):
|
||||
item1 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(0, 0))
|
||||
view.scene.addItem(item1)
|
||||
|
||||
item2 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(200, 0))
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100)
|
||||
assert (exporter.margin - 9) < 0.000001
|
||||
assert exporter.default_size == QtCore.QSize(318, 118)
|
||||
|
||||
|
||||
@patch('beeref.scene.BeeGraphicsScene.render')
|
||||
def test_scene_to_pixmap_exporter_render_sets_margins(render_mock, view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32))
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
assert exporter.margin == 36
|
||||
assert exporter.default_size == QtCore.QSize(1072, 1272)
|
||||
|
||||
exporter.size = QtCore.QSize(536, 636)
|
||||
exporter.render_to_image()
|
||||
|
||||
render_mock.assert_called_once_with(
|
||||
ANY,
|
||||
source=QtCore.QRectF(0, 0, 1000, 1200),
|
||||
target=QtCore.QRectF(18, 18, 500, 600))
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_render_renders_scene(view):
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item_img.fill(QtGui.QColor(11, 22, 33))
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
assert exporter.margin == 36
|
||||
assert exporter.default_size == QtCore.QSize(1072, 1272)
|
||||
|
||||
exporter.size = QtCore.QSize(536, 636)
|
||||
image = exporter.render_to_image()
|
||||
assert image.pixel(1, 1) == QtGui.QColor(*constants.COLORS['Scene:Canvas'])
|
||||
assert image.pixel(100, 100) == QtGui.QColor(11, 22, 33)
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_writes_image(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
exporter.export(filename)
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_with_worker(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=False)
|
||||
exporter.export(filename, worker)
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(1)
|
||||
worker.finished.emit.assert_called_once_with(filename, [])
|
||||
with open(filename, 'rb') as f:
|
||||
assert f.read().startswith(b'\x89PNG')
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_with_worker_when_canceled(
|
||||
view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=True)
|
||||
exporter.export(filename, worker)
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_not_called()
|
||||
worker.finished.emit.assert_called_once_with(filename, [])
|
||||
os.path.exists(filename) is False
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_when_file_not_writeable(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
with open(filename, 'w') as f:
|
||||
f.write('foo')
|
||||
os.chmod(filename, stat.S_IREAD)
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
|
||||
with pytest.raises(BeeFileIOError) as e:
|
||||
exporter.export(filename)
|
||||
assert e.filename == filename
|
||||
|
||||
|
||||
def test_scene_to_pixmap_exporter_export_when_file_not_writeable_with_worker(
|
||||
view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.png')
|
||||
with open(filename, 'w') as f:
|
||||
f.write('foo')
|
||||
os.chmod(filename, stat.S_IREAD)
|
||||
item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)
|
||||
item = BeePixmapItem(item_img)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToPixmapExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=False)
|
||||
|
||||
exporter.export(filename, worker=worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_not_called()
|
||||
worker.finished.emit.assert_called_once_with(
|
||||
filename, ['Error writing file'])
|
||||
283
tests/fileio/test_export_scene_to_svg.py
Normal file
283
tests/fileio/test_export_scene_to_svg.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
import os
|
||||
import stat
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from PyQt6 import QtGui, QtCore
|
||||
|
||||
from beeref.items import BeePixmapItem, BeeTextItem
|
||||
from beeref.fileio.errors import BeeFileIOError
|
||||
from beeref.fileio.export import SceneToSVGExporter
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_get_user_input(view):
|
||||
item1 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(0, 0))
|
||||
view.scene.addItem(item1)
|
||||
|
||||
item2 = BeePixmapItem(
|
||||
QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(200, 0))
|
||||
view.scene.addItem(item2)
|
||||
|
||||
assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
value = exporter.get_user_input(None)
|
||||
assert value is True
|
||||
assert exporter.size == QtCore.QSize(318, 118)
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_items(view):
|
||||
item1 = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item1.setPos(QtCore.QPointF(20, 30))
|
||||
view.scene.addItem(item1)
|
||||
|
||||
item2 = BeePixmapItem(
|
||||
QtGui.QImage(70, 77, QtGui.QImage.Format.Format_RGB32))
|
||||
item2.setPos(QtCore.QPointF(50, 50))
|
||||
item2.setZValue(-1)
|
||||
view.scene.addItem(item2)
|
||||
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert svg.tag == 'svg'
|
||||
assert svg.get('width') == '200'
|
||||
assert svg.get('height') == '400'
|
||||
assert svg.get('xmlns') == 'http://www.w3.org/2000/svg'
|
||||
assert svg.get('xmlns:xlink') == 'http://www.w3.org/1999/xlink'
|
||||
assert len(svg) == 2
|
||||
|
||||
element = svg[0] # item2
|
||||
assert element.tag == 'image'
|
||||
assert element.get('xlink:href').startswith('data:image/png;base64,iVBOR')
|
||||
assert element.get('width') == '70.0'
|
||||
assert element.get('height') == '77.0'
|
||||
assert element.get('image-rendering') == 'optimizeQuality'
|
||||
assert element.get('transform') == 'rotate(0.0 35.0 25.0)'
|
||||
assert element.get('x') == '35.0'
|
||||
assert element.get('y') == '25.0'
|
||||
assert element.get('opacity') == '1.0'
|
||||
|
||||
element = svg[1] # item1
|
||||
assert element.tag == 'image'
|
||||
assert element.get('width') == '100.0'
|
||||
assert element.get('height') == '110.0'
|
||||
assert element.get('image-rendering') == 'optimizeQuality'
|
||||
assert element.get('transform') == 'rotate(0.0 5.0 5.0)'
|
||||
assert element.get('x') == '5.0'
|
||||
assert element.get('y') == '5.0'
|
||||
assert element.get('opacity') == '1.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_with_crop(view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
item.crop = QtCore.QRectF(20, 25, 30, 33)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'image'
|
||||
assert element.get('width') == '30.0'
|
||||
assert element.get('height') == '33.0'
|
||||
assert element.get('transform') == 'rotate(0.0 -15.0 -20.0)'
|
||||
assert element.get('x') == '5.0'
|
||||
assert element.get('y') == '5.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_with_rotation(view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
item.setRotation(90)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'image'
|
||||
assert element.get('transform') == 'rotate(90.0 115.0 5.0)'
|
||||
assert element.get('x') == '115.0'
|
||||
assert element.get('y') == '5.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_with_opacity(view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
item.setOpacity(0.75)
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'image'
|
||||
assert element.get('opacity') == '0.75'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_pixmap_with_flip(view):
|
||||
item = BeePixmapItem(
|
||||
QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32))
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
item.do_flip()
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'image'
|
||||
assert element.get('transform') == (
|
||||
'translate(105.0 5.0) scale(-1.0 1)'
|
||||
' translate(-105.0 -5.0) rotate(0.0 105.0 5.0)')
|
||||
assert element.get('x') == '105.0'
|
||||
assert element.get('y') == '5.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_text(view):
|
||||
item = BeeTextItem('foo')
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
svg = exporter.render_to_svg()
|
||||
|
||||
assert len(svg) == 1
|
||||
|
||||
element = svg[0]
|
||||
assert element.tag == 'text'
|
||||
assert element.text == 'foo'
|
||||
assert element.get('dominant-baseline') == 'hanging'
|
||||
assert 'font-family' in element.get('style')
|
||||
assert element.get('transform') == 'rotate(0.0 5.0 5.0)'
|
||||
assert element.get('x') == '5.0'
|
||||
assert element.get('y') == '5.0'
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_when_file_not_writeable(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
with open(filename, 'w') as f:
|
||||
f.write('foo')
|
||||
os.chmod(filename, stat.S_IREAD)
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
|
||||
with pytest.raises(BeeFileIOError) as e:
|
||||
exporter.export(filename)
|
||||
assert e.filename == filename
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_with_worker(view):
|
||||
item = BeeTextItem('foo')
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
|
||||
worker = MagicMock(canceled=False)
|
||||
svg = exporter.render_to_svg(worker=worker)
|
||||
assert len(svg) == 1
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_render_with_worker_canceled(view):
|
||||
item = BeeTextItem('foo')
|
||||
item.setPos(QtCore.QPointF(20, 30))
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(200, 400)
|
||||
exporter.margin = 5
|
||||
|
||||
worker = MagicMock(canceled=True)
|
||||
svg = exporter.render_to_svg(worker=worker)
|
||||
assert svg is None
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_writes_svg(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
exporter.export(filename)
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
assert f.read().startswith(b'<?xml')
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_with_worker(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=False)
|
||||
exporter.export(filename, worker)
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with(filename, [])
|
||||
with open(filename, 'rb') as f:
|
||||
assert f.read().startswith(b'<?xml')
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_with_worker_canceled(view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=True)
|
||||
exporter.export(filename, worker)
|
||||
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with(filename, [])
|
||||
os.path.exists(filename) is False
|
||||
|
||||
|
||||
def test_scene_to_svg_exporter_export_when_file_not_writeable_with_worker(
|
||||
view, tmpdir):
|
||||
filename = os.path.join(tmpdir, 'foo.svg')
|
||||
with open(filename, 'w') as f:
|
||||
f.write('foo')
|
||||
os.chmod(filename, stat.S_IREAD)
|
||||
item = BeeTextItem('foo')
|
||||
view.scene.addItem(item)
|
||||
exporter = SceneToSVGExporter(view.scene)
|
||||
exporter.size = QtCore.QSize(100, 120)
|
||||
worker = MagicMock(canceled=False)
|
||||
|
||||
exporter.export(filename, worker=worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once()
|
||||
args = worker.finished.emit.call_args.args
|
||||
assert args[0] == filename
|
||||
assert len(args[1]) == 1
|
||||
|
|
@ -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'),
|
||||
|
|
@ -115,3 +122,65 @@ def test_load_image_loads_from_web_url_errors(view, imgfilename3x3):
|
|||
img, filename = load_image(QtCore.QUrl(url))
|
||||
assert img.isNull() is True
|
||||
assert filename == url
|
||||
|
||||
|
||||
@httpretty.activate
|
||||
def test_load_image_from_pinterest_finds_image(view, imgdata3x3):
|
||||
url = 'http://pinterest.com/a1b2c3/'
|
||||
img_url = 'http://pinterest.com/foo.png'
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url,
|
||||
body=f'<html><body><img src="{img_url}"/></body></html>',
|
||||
)
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
img_url,
|
||||
body=imgdata3x3,
|
||||
)
|
||||
img, filename = load_image(QtCore.QUrl(url))
|
||||
assert img.isNull() is False
|
||||
assert filename == img_url
|
||||
|
||||
|
||||
@httpretty.activate
|
||||
def test_load_image_from_pinterest_when_already_image(view, imgdata3x3):
|
||||
img_url = 'http://pinterest.com/foo.png'
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
img_url,
|
||||
body=imgdata3x3,
|
||||
)
|
||||
img, filename = load_image(QtCore.QUrl(img_url))
|
||||
assert img.isNull() is False
|
||||
assert filename == img_url
|
||||
|
||||
|
||||
@httpretty.activate
|
||||
def test_load_image_from_pinterest_when_img_url_not_found(view, imgdata3x3):
|
||||
url = 'http://pinterest.com/a1b2c3/'
|
||||
img_url = 'http://pinterest.com/foo.png'
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url,
|
||||
body='<html><body><p>no image here</p></body></html>',
|
||||
)
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
img_url,
|
||||
body=imgdata3x3,
|
||||
)
|
||||
img, filename = load_image(QtCore.QUrl(url))
|
||||
assert img.isNull() is True
|
||||
|
||||
|
||||
@httpretty.activate
|
||||
def test_load_image_from_pinterest_when_url_errors(view, imgdata3x3):
|
||||
url = 'http://pinterest.com/a1b2c3/'
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url,
|
||||
status=500,
|
||||
)
|
||||
img, filename = load_image(QtCore.QUrl(url))
|
||||
assert img.isNull() is True
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import pytest
|
|||
from beeref.fileio import schema, is_bee_file
|
||||
from beeref.fileio.errors import BeeFileIOError
|
||||
from beeref.fileio.sql import SQLiteIO
|
||||
from beeref.items import BeePixmapItem, BeeTextItem
|
||||
from beeref.items import BeePixmapItem, BeeTextItem, BeeErrorItem
|
||||
|
||||
|
||||
@pytest.mark.parametrize('filename,expected',
|
||||
|
|
@ -120,21 +120,21 @@ def test_all_migrations(tmpfile):
|
|||
assert result[3] == b'bla'
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_application_id(tmpfile):
|
||||
def test_sqliteio_write_meta_application_id(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.write_meta()
|
||||
result = io.fetchone('PRAGMA application_id')
|
||||
assert result[0] == schema.APPLICATION_ID
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_user_version(tmpfile):
|
||||
def test_sqliteio_write_meta_user_version(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.write_meta()
|
||||
result = io.fetchone('PRAGMA user_version')
|
||||
assert result[0] == schema.USER_VERSION
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_foreign_keys(tmpfile):
|
||||
def test_sqliteio_write_meta_foreign_keys(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.write_meta()
|
||||
result = io.fetchone('PRAGMA foreign_keys')
|
||||
|
|
@ -304,6 +304,8 @@ def test_sqliteio_write_updates_existing_text_item(tmpfile, view):
|
|||
item.save_id = 1
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
assert io.fetchone('SELECT COUNT(*) from items') == (1,)
|
||||
|
||||
item.setScale(0.7)
|
||||
item.setPos(20, 30)
|
||||
item.setZValue(0.33)
|
||||
|
|
@ -341,6 +343,8 @@ def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view):
|
|||
item.pixmap_to_bytes = MagicMock(return_value=(b'abc', 'png'))
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
assert io.fetchone('SELECT COUNT(*) from items') == (1,)
|
||||
|
||||
item.setScale(0.7)
|
||||
item.setPos(20, 30)
|
||||
item.setZValue(0.33)
|
||||
|
|
@ -374,6 +378,62 @@ def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view):
|
|||
assert result[7] == b'abc'
|
||||
|
||||
|
||||
def test_sqliteio_write_keeps_pixmap_item_of_error_item(tmpfile, view):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename='bee.png')
|
||||
view.scene.addItem(item)
|
||||
item.setScale(1.3)
|
||||
item.setPos(44, 55)
|
||||
item.setZValue(0.22)
|
||||
item.setRotation(33)
|
||||
item.setOpacity(0.2)
|
||||
item.save_id = 1
|
||||
item.crop = QtCore.QRectF(5, 5, 80, 100)
|
||||
item.pixmap_to_bytes = MagicMock(return_value=(b'abc', 'png'))
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
view.scene.removeItem(item)
|
||||
assert io.fetchone('SELECT COUNT(*) from items') == (1,)
|
||||
|
||||
err_item = BeeErrorItem('errormsg')
|
||||
err_item.original_save_id = 1
|
||||
err_item.setScale(0.7)
|
||||
err_item.setPos(20, 30)
|
||||
err_item.setZValue(0.33)
|
||||
err_item.setRotation(100)
|
||||
view.scene.addItem(err_item)
|
||||
io.create_new = False
|
||||
io.write()
|
||||
|
||||
assert io.fetchone('SELECT COUNT(*) from items') == (1,)
|
||||
result = io.fetchone(
|
||||
'SELECT x, y, z, scale, rotation, flip, items.data, sqlar.data '
|
||||
'FROM items '
|
||||
'INNER JOIN sqlar on sqlar.item_id = items.id')
|
||||
assert result[0] == 44
|
||||
assert result[1] == 55
|
||||
assert result[2] == 0.22
|
||||
assert result[3] == 1.3
|
||||
assert result[4] == 33
|
||||
assert result[5] == 1
|
||||
assert json.loads(result[6]) == {
|
||||
'filename': 'bee.png',
|
||||
'crop': [5, 5, 80, 100],
|
||||
'opacity': 0.2,
|
||||
'grayscale': False,
|
||||
}
|
||||
assert result[7] == b'abc'
|
||||
|
||||
|
||||
def test_sqliteio_doesnt_write_error_item_to_new_file(tmpfile, view):
|
||||
err_item = BeeErrorItem('errormsg')
|
||||
err_item.original_save_id = 1
|
||||
view.scene.addItem(err_item)
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.create_new = True
|
||||
io.write()
|
||||
assert io.fetchone('SELECT COUNT(*) from items') == (0,)
|
||||
|
||||
|
||||
def test_sqliteio_write_removes_nonexisting_text_item(tmpfile, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.setScale(1.3)
|
||||
|
|
@ -412,6 +472,7 @@ def test_sqliteio_write_removes_nonexisting_pixmap_item(tmpfile, view):
|
|||
|
||||
def test_sqliteio_write_update_recovers_from_borked_file(view, tmpfile):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename='bee.png')
|
||||
item.save_id = 1
|
||||
view.scene.addItem(item)
|
||||
|
||||
with open(tmpfile, 'w') as f:
|
||||
|
|
@ -423,6 +484,16 @@ def test_sqliteio_write_update_recovers_from_borked_file(view, tmpfile):
|
|||
assert result[0] == 1
|
||||
|
||||
|
||||
def test_sqliteio_write_update_recovers_from_nonexisting_file(view, tmpfile):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename='bee.png')
|
||||
item.save_id = 1
|
||||
view.scene.addItem(item)
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=False)
|
||||
io.write()
|
||||
result = io.fetchone('SELECT COUNT(*) FROM items')
|
||||
assert result[0] == 1
|
||||
|
||||
|
||||
def test_sqliteio_write_updates_progress(tmpfile, view):
|
||||
worker = MagicMock(canceled=False)
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True, worker=worker)
|
||||
|
|
@ -463,6 +534,7 @@ def test_sqliteio_read_reads_readonly_text_item(tmpfile, view):
|
|||
view.scene.add_queued_items()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert isinstance(item, BeeTextItem)
|
||||
assert item.isSelected() is False
|
||||
assert item.save_id == 1
|
||||
assert item.pos().x() == 22.2
|
||||
|
|
@ -493,6 +565,7 @@ def test_sqliteio_read_reads_readonly_pixmap_item(tmpfile, view, imgdata3x3):
|
|||
view.scene.add_queued_items()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert isinstance(item, BeePixmapItem)
|
||||
assert item.isSelected() is False
|
||||
assert item.save_id == 1
|
||||
assert item.pos().x() == 22.2
|
||||
|
|
@ -510,6 +583,29 @@ def test_sqliteio_read_reads_readonly_pixmap_item(tmpfile, view, imgdata3x3):
|
|||
assert view.scene.items_to_add.empty() is True
|
||||
|
||||
|
||||
def test_sqliteio_read_reads_readonly_pixmap_item_error(tmpfile, view):
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.create_schema_on_new()
|
||||
io.ex('INSERT INTO items '
|
||||
'(type, x, y, z, scale, rotation, flip, data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 22.2, 33.3, 0.22, 3.4, 45, -1,
|
||||
json.dumps({'filename': 'bee.png'})))
|
||||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)',
|
||||
(1, b'not an image'))
|
||||
io.connection.commit()
|
||||
del io
|
||||
|
||||
io = SQLiteIO(tmpfile, view.scene, readonly=True)
|
||||
io.read()
|
||||
view.scene.add_queued_items()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert isinstance(item, BeeErrorItem)
|
||||
item.toPlainText().startswith('Unknown')
|
||||
assert view.scene.items_to_add.empty() is True
|
||||
|
||||
|
||||
def test_sqliteio_read_updates_progress(tmpfile, view):
|
||||
worker = MagicMock(canceled=False)
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True,
|
||||
|
|
|
|||
114
tests/items/test_erroritem.py
Normal file
114
tests/items/test_erroritem.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from PyQt6 import QtCore, QtWidgets
|
||||
|
||||
from beeref.items import BeeErrorItem, item_registry
|
||||
|
||||
|
||||
def test_in_items_registry():
|
||||
assert item_registry['error'] == BeeErrorItem
|
||||
|
||||
|
||||
@patch('beeref.selection.SelectableMixin.init_selectable')
|
||||
def test_init(selectable_mock, qapp):
|
||||
item = BeeErrorItem('foo bar')
|
||||
assert hasattr(item, 'save_id') is False
|
||||
assert item.original_save_id is None
|
||||
assert item.width
|
||||
assert item.height
|
||||
assert item.scale() == 1
|
||||
assert item.toPlainText() == 'foo bar'
|
||||
assert item.is_editable is False
|
||||
assert item.is_image is False
|
||||
selectable_mock.assert_called_once()
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsTextItem.paint')
|
||||
def test_paint(paint_mock, qapp):
|
||||
item = BeeErrorItem('foo bar')
|
||||
item.paint_selectable = MagicMock()
|
||||
painter = MagicMock()
|
||||
option = MagicMock()
|
||||
item.paint(painter, option, 'widget')
|
||||
item.paint_selectable.assert_called_once()
|
||||
painter.drawRect.assert_called_once()
|
||||
assert option.state == QtWidgets.QStyle.StateFlag.State_Enabled
|
||||
paint_mock.assert_called_once_with(painter, option, 'widget')
|
||||
|
||||
|
||||
def test_update_from_data(qapp):
|
||||
item = BeeErrorItem('foo bar')
|
||||
item.update_from_data(
|
||||
save_id=3,
|
||||
x=11,
|
||||
y=22,
|
||||
z=1.2,
|
||||
scale=2.5,
|
||||
rotation=45,
|
||||
flip=-1,
|
||||
data={'opactiy': 0.5})
|
||||
assert item.original_save_id == 3
|
||||
assert item.pos() == QtCore.QPointF(11, 22)
|
||||
assert item.zValue() == 1.2
|
||||
assert item.rotation() == 45
|
||||
assert item.flip() == 1
|
||||
assert hasattr(item, 'save_id') is False
|
||||
|
||||
|
||||
def test_update_from_data_keeps_unset_values(qapp):
|
||||
item = BeeErrorItem('foo bar')
|
||||
item.setScale(3)
|
||||
item.update_from_data(rotation=45)
|
||||
assert item.scale() == 3
|
||||
assert item.flip() == 1
|
||||
|
||||
|
||||
def test_create_from_data(qapp):
|
||||
item = BeeErrorItem.create_from_data(data={'text': 'hello world'})
|
||||
item.toPlainText() == 'hello world'
|
||||
assert hasattr(item, 'save_id') is False
|
||||
|
||||
|
||||
def test_create_copy(qapp):
|
||||
item = BeeErrorItem('foo bar')
|
||||
item.setPos(20, 30)
|
||||
item.setRotation(33)
|
||||
item.setZValue(0.5)
|
||||
item.setScale(2.2)
|
||||
|
||||
copy = item.create_copy()
|
||||
assert copy.toPlainText() == 'foo bar'
|
||||
assert copy.pos() == QtCore.QPointF(20, 30)
|
||||
assert copy.rotation() == 33
|
||||
assert copy.zValue() == 0.5
|
||||
assert copy.scale() == 2.2
|
||||
assert copy.flip() == 1
|
||||
|
||||
|
||||
def test_item_to_clipboard(qapp):
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
item = BeeErrorItem('foo bar')
|
||||
item.copy_to_clipboard(clipboard)
|
||||
assert clipboard.text() == 'foo bar'
|
||||
|
||||
|
||||
def test_flip(qapp):
|
||||
item = BeeErrorItem('foo bar')
|
||||
item.do_flip()
|
||||
assert item.flip() == 1
|
||||
|
||||
|
||||
@patch('beeref.items.BeeErrorItem.boundingRect')
|
||||
def test_contains_when_inside_bounds(brect_mock, qapp):
|
||||
brect_mock.return_value = QtCore.QRectF(20, 30, 50, 50)
|
||||
item = BeeErrorItem('foo bar')
|
||||
item.contains(QtCore.QPointF(33, 45)) is True
|
||||
brect_mock.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.items.BeeErrorItem.boundingRect')
|
||||
def test_contains_when_outside_bounds(brect_mock, qapp):
|
||||
brect_mock.return_value = QtCore.QRectF(20, 30, 50, 50)
|
||||
item = BeeErrorItem('foo bar')
|
||||
item.contains(QtCore.QPointF(19, 29)) is False
|
||||
brect_mock.assert_called_once_with()
|
||||
46
tests/items/test_items.py
Normal file
46
tests/items/test_items.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from PyQt6 import QtGui
|
||||
|
||||
from beeref.items import sort_by_filename, BeePixmapItem, BeeTextItem
|
||||
|
||||
|
||||
def test_sort_by_filename(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.filename = 'foo.png'
|
||||
item2.save_id = 66
|
||||
|
||||
item3 = BeePixmapItem(QtGui.QImage())
|
||||
item3.save_id = 33
|
||||
|
||||
item4 = BeePixmapItem(QtGui.QImage())
|
||||
item4.filename = 'bar.png'
|
||||
item4.save_id = 77
|
||||
|
||||
item5 = BeePixmapItem(QtGui.QImage())
|
||||
item5.save_id = 22
|
||||
|
||||
result = sort_by_filename([item1, item2, item3, item4, item5])
|
||||
assert result == [item4, item2, item5, item3, item1]
|
||||
|
||||
|
||||
def test_sort_by_filename_when_only_by_filename(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
item1.filename = 'foo.png'
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.filename = 'bar.png'
|
||||
assert sort_by_filename([item1, item2]) == [item2, item1]
|
||||
|
||||
|
||||
def test_sort_by_filename_when_only_by_save_id(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
item1.save_id = 66
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.save_id = 33
|
||||
assert sort_by_filename([item1, item2]) == [item2, item1]
|
||||
|
||||
|
||||
def test_sort_by_filename_deals_with_text_items(view):
|
||||
item1 = BeeTextItem('Foo')
|
||||
item2 = BeeTextItem('Bar')
|
||||
assert len(sort_by_filename([item1, item2])) == 2
|
||||
|
|
@ -98,6 +98,43 @@ def test_get_extra_save_data(item):
|
|||
}
|
||||
|
||||
|
||||
def test_get_filename_for_export_when_save_id_and_filename(item):
|
||||
item.filename = 'foo.png'
|
||||
item.save_id = 5
|
||||
assert item.get_filename_for_export('jpg', 8) == '0005-foo.jpg'
|
||||
|
||||
|
||||
def test_get_filename_for_export_when_save_id_and_no_filename(item):
|
||||
item.filename = None
|
||||
item.save_id = 5
|
||||
assert item.get_filename_for_export('jpg', 8) == '0005.jpg'
|
||||
|
||||
|
||||
def test_get_filename_for_export_when_no_save_id_and_filename(item):
|
||||
item.filename = 'foo.png'
|
||||
item.save_id = None
|
||||
assert item.get_filename_for_export('jpg', 8) == '0008-foo.jpg'
|
||||
|
||||
|
||||
def test_get_filename_for_export_when_no_save_id_and_no_filename(item):
|
||||
item.filename = None
|
||||
item.save_id = None
|
||||
assert item.get_filename_for_export('jpg', 8) == '0008.jpg'
|
||||
|
||||
|
||||
def test_get_filename_for_export_when_save_id_and_no_default(item):
|
||||
item.filename = 'foo.png'
|
||||
item.save_id = 5
|
||||
assert item.get_filename_for_export('jpg') == '0005-foo.jpg'
|
||||
|
||||
|
||||
def test_get_filename_for_export_when_no_save_id_and_no_default(item):
|
||||
item.filename = 'foo.png'
|
||||
item.save_id = None
|
||||
with pytest.raises(AssertionError):
|
||||
assert item.get_filename_for_export('jpg')
|
||||
|
||||
|
||||
def test_get_imgformat_test_with_real_image(
|
||||
qapp, imgfilename3x3, item, settings):
|
||||
settings.setValue('Items/image_storage_format', 'best')
|
||||
|
|
@ -846,3 +883,42 @@ def test_mouse_release_event_when_not_crop_mode(mouse_mock, qapp, item):
|
|||
assert item.crop_mode is False
|
||||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
|
||||
|
||||
def test_sample_color_at_returns_color(qapp, view):
|
||||
color = QtGui.QColor(255, 0, 0, 3)
|
||||
img = QtGui.QImage(10, 10, QtGui.QImage.Format.Format_ARGB32)
|
||||
img.fill(color)
|
||||
item = BeePixmapItem(img, 'foo.png')
|
||||
view.scene.addItem(item)
|
||||
assert item.sample_color_at(QtCore.QPointF(2, 2)) == color
|
||||
|
||||
|
||||
def test_sample_color_at_returns_none_when_fully_transparent(qapp, view):
|
||||
color = QtGui.QColor(255, 0, 0, 0)
|
||||
img = QtGui.QImage(10, 10, QtGui.QImage.Format.Format_ARGB32)
|
||||
img.fill(color)
|
||||
item = BeePixmapItem(img, 'foo.png')
|
||||
view.scene.addItem(item)
|
||||
assert item.sample_color_at(QtCore.QPointF(2, 2)) is None
|
||||
|
||||
|
||||
def test_sample_color_in_greyscale_mode(qapp, view):
|
||||
color = QtGui.QColor(255, 0, 0)
|
||||
img = QtGui.QImage(10, 10, QtGui.QImage.Format.Format_ARGB32)
|
||||
img.fill(color)
|
||||
item = BeePixmapItem(img, 'foo.png')
|
||||
item.grayscale = True
|
||||
view.scene.addItem(item)
|
||||
gray = item.sample_color_at(QtCore.QPointF(2, 2))
|
||||
print(gray.red(), gray.green(), gray.blue(), gray.alpha())
|
||||
assert gray == QtGui.QColor(130, 130, 130)
|
||||
|
||||
|
||||
def test_sample_color_at_returns_none_when_transparent(qapp, view):
|
||||
color = QtGui.QColor(255, 0, 0, 0)
|
||||
img = QtGui.QImage(10, 10, QtGui.QImage.Format.Format_ARGB32)
|
||||
img.fill(color)
|
||||
item = BeePixmapItem(img, 'foo.png')
|
||||
view.scene.addItem(item)
|
||||
assert item.sample_color_at(QtCore.QPointF(2, 2)) is None
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ def test_init(selectable_mock, qapp):
|
|||
selectable_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_sample_color_at(qapp, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
assert item.sample_color_at(QtCore.QPointF(2.0, 2.0)) is None
|
||||
|
||||
|
||||
def test_set_pos_center(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
|
|
@ -196,9 +202,9 @@ def test_create_copy(qapp):
|
|||
assert copy.toPlainText() == 'foo bar'
|
||||
assert copy.pos() == QtCore.QPointF(20, 30)
|
||||
assert copy.rotation() == 33
|
||||
assert item.flip() == -1
|
||||
assert item.zValue() == 0.5
|
||||
assert item.scale() == 2.2
|
||||
assert copy.flip() == -1
|
||||
assert copy.zValue() == 0.5
|
||||
assert copy.scale() == 2.2
|
||||
|
||||
|
||||
def test_enter_edit_mode(view):
|
||||
|
|
|
|||
|
|
@ -47,6 +47,46 @@ def test_selection_action_items(view):
|
|||
assert action_items == {item1, item2, view.scene.multi_select_item}
|
||||
|
||||
|
||||
def test_lower_behind_selection_when_selection(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item3 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item3)
|
||||
item3.setSelected(False)
|
||||
|
||||
item1.setZValue(3)
|
||||
item2.setZValue(4)
|
||||
item3.setZValue(1)
|
||||
|
||||
view.scene.multi_select_item.setZValue(5)
|
||||
view.scene.multi_select_item.lower_behind_selection()
|
||||
assert view.scene.multi_select_item.zValue() == 2.999
|
||||
|
||||
|
||||
def test_lower_behind_selection_when_no_selection(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(False)
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(False)
|
||||
item3 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item3)
|
||||
item3.setSelected(False)
|
||||
|
||||
item1.setZValue(3)
|
||||
item2.setZValue(4)
|
||||
item3.setZValue(1)
|
||||
|
||||
view.scene.multi_select_item.setZValue(5)
|
||||
view.scene.multi_select_item.lower_behind_selection()
|
||||
assert view.scene.multi_select_item.zValue() == 5
|
||||
|
||||
|
||||
def test_fit_selection_area():
|
||||
item = MultiSelectItem()
|
||||
item.setScale(5)
|
||||
|
|
|
|||
|
|
@ -13,18 +13,7 @@ from beeref.items import BeePixmapItem
|
|||
def test_init_selectable(view):
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
assert item.viewport_scale == 1
|
||||
assert item.scale_active is False
|
||||
assert item.rotate_active is False
|
||||
assert item.flip_active is False
|
||||
|
||||
|
||||
def test_is_action_active_when_no_action(view, item):
|
||||
assert item.is_action_active() is False
|
||||
|
||||
|
||||
def test_is_action_active_when_action(view, item):
|
||||
item.scale_active = True
|
||||
assert item.is_action_active() is True
|
||||
assert item.active_mode is None
|
||||
|
||||
|
||||
def test_on_view_scale_change(view, item):
|
||||
|
|
@ -634,11 +623,11 @@ def test_hover_move_event_no_selection(view, item):
|
|||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
event.pos.return_value = QtCore.QPointF(0, 0)
|
||||
item.setCursor = MagicMock()
|
||||
item.set_cursor = MagicMock()
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
item.setCursor.assert_not_called()
|
||||
item.set_cursor.assert_not_called()
|
||||
|
||||
|
||||
def test_hover_move_event_small_item_inside_handle_free_center(view, item):
|
||||
|
|
@ -646,11 +635,11 @@ def test_hover_move_event_small_item_inside_handle_free_center(view, item):
|
|||
item.setSelected(True)
|
||||
event = MagicMock()
|
||||
event.pos.return_value = QtCore.QPointF(10, 10)
|
||||
item.setCursor = MagicMock()
|
||||
item.unset_cursor = MagicMock()
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 20, 20)):
|
||||
item.hoverMoveEvent(event)
|
||||
item.setCursor.assert_called_once_with(Qt.CursorShape.ArrowCursor)
|
||||
item.unset_cursor.assert_called_once_with()
|
||||
|
||||
|
||||
@mark.parametrize('pos,flipped,rotation, expected',
|
||||
|
|
@ -684,7 +673,7 @@ def test_hover_move_event_scale(
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == getattr(Qt.CursorShape, expected)
|
||||
assert view.viewport().cursor() == getattr(Qt.CursorShape, expected)
|
||||
|
||||
|
||||
def test_hover_move_event_scale_bottomright_very_wide_item(view, item):
|
||||
|
|
@ -695,7 +684,7 @@ def test_hover_move_event_scale_bottomright_very_wide_item(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 1000, 100)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == Qt.CursorShape.SizeFDiagCursor
|
||||
assert view.viewport().cursor() == Qt.CursorShape.SizeFDiagCursor
|
||||
|
||||
|
||||
def test_hover_move_event_rotate(view, item):
|
||||
|
|
@ -706,7 +695,7 @@ def test_hover_move_event_rotate(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == BeeAssets().cursor_rotate
|
||||
assert view.viewport().cursor() == BeeAssets().cursor_rotate
|
||||
|
||||
|
||||
def test_hover_flip_event_top_edge(view, item):
|
||||
|
|
@ -717,7 +706,7 @@ def test_hover_flip_event_top_edge(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == BeeAssets().cursor_flip_v
|
||||
assert view.viewport().cursor() == BeeAssets().cursor_flip_v
|
||||
|
||||
|
||||
def test_hover_flip_event_bottom_edge(view, item):
|
||||
|
|
@ -728,7 +717,7 @@ def test_hover_flip_event_bottom_edge(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == BeeAssets().cursor_flip_v
|
||||
assert view.viewport().cursor() == BeeAssets().cursor_flip_v
|
||||
|
||||
|
||||
def test_hover_flip_event_left_edge(view, item):
|
||||
|
|
@ -739,7 +728,7 @@ def test_hover_flip_event_left_edge(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == BeeAssets().cursor_flip_h
|
||||
assert view.viewport().cursor() == BeeAssets().cursor_flip_h
|
||||
|
||||
|
||||
def test_hover_flip_event_right_edge(view, item):
|
||||
|
|
@ -750,7 +739,7 @@ def test_hover_flip_event_right_edge(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == BeeAssets().cursor_flip_h
|
||||
assert view.viewport().cursor() == BeeAssets().cursor_flip_h
|
||||
|
||||
|
||||
def test_hover_flip_event_top_edge_rotated_90(view, item):
|
||||
|
|
@ -762,7 +751,7 @@ def test_hover_flip_event_top_edge_rotated_90(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == BeeAssets().cursor_flip_h
|
||||
assert view.viewport().cursor() == BeeAssets().cursor_flip_h
|
||||
|
||||
|
||||
def test_hover_flip_event_left_edge_when_rotated_90(view, item):
|
||||
|
|
@ -774,7 +763,7 @@ def test_hover_flip_event_left_edge_when_rotated_90(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == BeeAssets().cursor_flip_v
|
||||
assert view.viewport().cursor() == BeeAssets().cursor_flip_v
|
||||
|
||||
|
||||
def test_hover_move_event_not_in_handles(view, item):
|
||||
|
|
@ -785,24 +774,16 @@ def test_hover_move_event_not_in_handles(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 1000, 800)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == Qt.CursorShape.ArrowCursor
|
||||
assert view.viewport().cursor() == Qt.CursorShape.ArrowCursor
|
||||
|
||||
|
||||
def test_hover_enter_event_when_selected(view, item):
|
||||
def test_hover_leave_event(view, item):
|
||||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
item.setSelected(True)
|
||||
item.setCursor = MagicMock()
|
||||
item.hoverEnterEvent(event)
|
||||
item.setCursor.assert_not_called()
|
||||
|
||||
|
||||
def test_hover_enter_event_when_not_selected(view, item):
|
||||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
item.setSelected(False)
|
||||
item.hoverEnterEvent(event)
|
||||
assert item.cursor() == Qt.CursorShape.ArrowCursor
|
||||
item.unset_cursor = MagicMock()
|
||||
item.hoverLeaveEvent(event)
|
||||
item.unset_cursor.assert_called_once_with()
|
||||
|
||||
|
||||
def test_mouse_press_event_just_selected(view, item):
|
||||
|
|
@ -843,7 +824,7 @@ def test_mouse_press_event_topleft_scale(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.mousePressEvent(event)
|
||||
assert item.scale_active is True
|
||||
assert item.active_mode == item.SCALE_MODE
|
||||
assert item.event_start == QtCore.QPointF(-1, -1)
|
||||
assert item.event_direction.x() < 0
|
||||
assert item.event_direction.y() < 0
|
||||
|
|
@ -861,7 +842,7 @@ def test_mouse_press_event_bottomright_scale(view, item):
|
|||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.mousePressEvent(event)
|
||||
assert item.scale_active is True
|
||||
assert item.active_mode == item.SCALE_MODE
|
||||
assert item.event_start == QtCore.QPointF(101, 81)
|
||||
assert item.event_direction.x() > 0
|
||||
assert item.event_direction.y() > 0
|
||||
|
|
@ -880,7 +861,7 @@ def test_mouse_press_event_rotate(view, item):
|
|||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'):
|
||||
item.mousePressEvent(event)
|
||||
assert item.rotate_active is True
|
||||
assert item.active_mode == item.ROTATE_MODE
|
||||
assert item.event_anchor == QtCore.QPointF(50, 40)
|
||||
assert item.rotate_orig_degrees == 0
|
||||
event.accept.assert_called_once_with()
|
||||
|
|
@ -903,7 +884,7 @@ def test_mouse_press_event_flip(view, item):
|
|||
assert cmd.items == [item]
|
||||
assert cmd.anchor == QtCore.QPointF(50, 40)
|
||||
assert cmd.vertical is False
|
||||
assert item.flip_active is True
|
||||
assert item.active_mode == item.FLIP_MODE
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
|
|
@ -917,9 +898,7 @@ def test_mouse_press_event_not_in_handles(view, item):
|
|||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m:
|
||||
item.mousePressEvent(event)
|
||||
m.assert_called_once_with(event)
|
||||
assert item.scale_active is False
|
||||
assert item.rotate_active is False
|
||||
assert item.flip_active is False
|
||||
assert item.active_mode is None
|
||||
event.accept.assert_not_called()
|
||||
|
||||
|
||||
|
|
@ -953,7 +932,7 @@ def test_mouse_move_event_when_scale_action(view, item):
|
|||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
event.scenePos.return_value = QtCore.QPointF(20, 90)
|
||||
item.scale_active = True
|
||||
item.active_mode = item.SCALE_MODE
|
||||
item.event_direction = QtCore.QPointF(1, 1) / math.sqrt(2)
|
||||
item.event_anchor = QtCore.QPointF(100, 80)
|
||||
item.event_start = QtCore.QPointF(10, 10)
|
||||
|
|
@ -973,7 +952,7 @@ def test_mouse_move_event_when_rotate_action(view, item):
|
|||
event = MagicMock()
|
||||
event.scenePos.return_value = QtCore.QPointF(15, 25)
|
||||
item.event_start = QtCore.QPointF(10, 10)
|
||||
item.rotate_active = True
|
||||
item.active_mode = item.ROTATE_MODE
|
||||
item.rotate_orig_degrees = 0
|
||||
item.rotate_start_angle = -3
|
||||
item.event_anchor = QtCore.QPointF(10, 20)
|
||||
|
|
@ -989,7 +968,7 @@ def test_mouse_move_event_when_flip_action(view, item):
|
|||
event = MagicMock()
|
||||
event.scenePos.return_value = QtCore.QPointF(15, 25)
|
||||
item.event_start = QtCore.QPointF(10, 10)
|
||||
item.flip_active = True
|
||||
item.active_mode = item.FLIP_MODE
|
||||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m:
|
||||
item.mouseMoveEvent(event)
|
||||
m.assert_not_called()
|
||||
|
|
@ -999,13 +978,13 @@ def test_mouse_move_event_when_flip_action(view, item):
|
|||
def test_mouse_release_event_when_no_action(view, item):
|
||||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
item.flip_active = True
|
||||
item.active_mode = item.FLIP_MODE
|
||||
event.pos.return_value = QtCore.QPointF(-100, -100)
|
||||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem'
|
||||
'.mouseReleaseEvent') as m:
|
||||
item.mouseReleaseEvent(event)
|
||||
m.assert_called_once_with(event)
|
||||
item.flip_active is False
|
||||
item.active_mode is None
|
||||
event.accept.assert_not_called()
|
||||
|
||||
|
||||
|
|
@ -1013,7 +992,7 @@ def test_mouse_release_event_when_scale_action(view, item):
|
|||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
event.scenePos.return_value = QtCore.QPointF(20, 90)
|
||||
item.scale_active = True
|
||||
item.active_mode = item.SCALE_MODE
|
||||
item.event_direction = QtCore.QPointF(1, 1) / math.sqrt(2)
|
||||
item.event_anchor = QtCore.QPointF(100, 80)
|
||||
item.event_start = QtCore.QPointF(10, 10)
|
||||
|
|
@ -1031,7 +1010,7 @@ def test_mouse_release_event_when_scale_action(view, item):
|
|||
assert cmd.factor == approx(1.5, 0.01)
|
||||
assert cmd.anchor == QtCore.QPointF(100, 80)
|
||||
assert cmd.ignore_first_redo is True
|
||||
assert item.scale_active is False
|
||||
assert item.active_mode is None
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
|
|
@ -1039,7 +1018,7 @@ def test_mouse_release_event_when_scale_action_zero(view, item):
|
|||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
event.scenePos.return_value = QtCore.QPointF(20, 90)
|
||||
item.scale_active = True
|
||||
item.active_mode = item.SCALE_MODE
|
||||
item.event_direction = QtCore.QPointF(1, 1) / math.sqrt(2)
|
||||
item.event_anchor = QtCore.QPointF(100, 80)
|
||||
item.event_start = QtCore.QPointF(20, 90)
|
||||
|
|
@ -1050,7 +1029,7 @@ def test_mouse_release_event_when_scale_action_zero(view, item):
|
|||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.mouseReleaseEvent(event)
|
||||
view.scene.undo_stack.push.assert_not_called()
|
||||
assert item.scale_active is False
|
||||
assert item.active_mode is None
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
|
|
@ -1058,7 +1037,7 @@ def test_mouse_release_event_when_rotate_action(view, item):
|
|||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
event.scenePos.return_value = QtCore.QPointF(15, 25)
|
||||
item.rotate_active = True
|
||||
item.active_mode = item.ROTATE_MODE
|
||||
item.rotate_orig_degrees = 0
|
||||
item.rotate_start_angle = -3
|
||||
item.event_anchor = QtCore.QPointF(10, 20)
|
||||
|
|
@ -1073,7 +1052,7 @@ def test_mouse_release_event_when_rotate_action(view, item):
|
|||
assert cmd.delta == -42
|
||||
assert cmd.anchor == QtCore.QPointF(10, 20)
|
||||
assert cmd.ignore_first_redo is True
|
||||
assert item.rotate_active is False
|
||||
assert item.active_mode is None
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
|
|
@ -1081,7 +1060,7 @@ def test_mouse_release_event_when_rotate_action_zero(view, item):
|
|||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
event.scenePos.return_value = QtCore.QPointF(15, 25)
|
||||
item.rotate_active = True
|
||||
item.active_mode = item.ROTATE_MODE
|
||||
item.rotate_orig_degrees = 0
|
||||
item.rotate_start_angle = -45
|
||||
item.event_anchor = QtCore.QPointF(10, 20)
|
||||
|
|
@ -1089,7 +1068,7 @@ def test_mouse_release_event_when_rotate_action_zero(view, item):
|
|||
|
||||
item.mouseReleaseEvent(event)
|
||||
view.scene.undo_stack.push.assert_not_called()
|
||||
assert item.rotate_active is False
|
||||
assert item.active_mode is None
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
|
|
@ -1097,12 +1076,12 @@ def test_mouse_release_event_when_flip_action(view, item):
|
|||
view.scene.addItem(item)
|
||||
event = MagicMock()
|
||||
event.pos.return_value = QtCore.QPointF(0, 40)
|
||||
item.flip_active = True
|
||||
item.active_mode = item.FLIP_MODE
|
||||
view.scene.undo_stack = MagicMock(push=MagicMock())
|
||||
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.mouseReleaseEvent(event)
|
||||
view.scene.undo_stack.push.assert_not_called()
|
||||
assert item.flip_active is False
|
||||
assert item.active_mode is None
|
||||
event.accept.assert_called_once_with()
|
||||
|
|
|
|||
|
|
@ -29,11 +29,16 @@ def test_beerefapplication_fileopenevent(open_mock, qapp, main_window):
|
|||
|
||||
@patch('beeref.__main__.BeeRefApplication')
|
||||
@patch('beeref.__main__.CommandlineArgs')
|
||||
def test_main(args_mock, app_mock, qapp):
|
||||
@patch('beeref.config.BeeSettings.on_startup')
|
||||
def test_main(startup_mock, args_mock, app_mock, qapp):
|
||||
app_mock.return_value = qapp
|
||||
args_mock.return_value.filename = None
|
||||
args_mock.return_value.loglevel = 'WARN'
|
||||
args_mock.return_value.debug_raise_error = ''
|
||||
|
||||
with patch.object(qapp, 'exec') as exec_mock:
|
||||
main()
|
||||
args_mock.assert_called_once_with(with_check=True)
|
||||
exec_mock.assert_called_once_with()
|
||||
|
||||
args_mock.assert_called_once_with(with_check=True)
|
||||
startup_mock.assert_called()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import math
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
from pytest import approx
|
||||
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
|
|
@ -249,22 +250,37 @@ def test_normalize_size_when_no_items(view):
|
|||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value,expected_func,expected_kwargs',
|
||||
[('optimal', 'arrange_optimal', {}),
|
||||
('horizontal', 'arrange', {}),
|
||||
('vertical', 'arrange', {'vertical': True}),
|
||||
('square', 'arrange_square', {})])
|
||||
def test_arrange_default(
|
||||
value, expected_func, expected_kwargs, settings, view):
|
||||
settings.setValue('Items/arrange_default', value)
|
||||
setattr(view.scene, expected_func, MagicMock())
|
||||
view.scene.arrange_default()
|
||||
getattr(view.scene, expected_func).assert_called_once_with(
|
||||
**expected_kwargs)
|
||||
|
||||
|
||||
def test_arrange_horizontal(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
item1.filename = 'foo.png'
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item1.setPos(10, -100)
|
||||
item1.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.filename = 'bar.png'
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item2.setPos(-10, 40)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
item2.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
with patch.object(item1, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
with patch.object(item2, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
view.scene.arrange()
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.arrange()
|
||||
|
||||
assert item2.pos() == QtCore.QPointF(-50, -30)
|
||||
assert item1.pos() == QtCore.QPointF(50, -30)
|
||||
|
|
@ -273,21 +289,23 @@ def test_arrange_horizontal(view):
|
|||
|
||||
def test_arrange_horizontal_with_gap(view, settings):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
item1.filename = 'foo.png'
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item1.setPos(10, -100)
|
||||
item1.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.filename = 'bar.png'
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item2.setPos(-10, 40)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
item2.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
with patch.object(item1, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
with patch.object(item2, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
view.scene.arrange()
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.arrange()
|
||||
|
||||
assert item2.pos() == QtCore.QPointF(-50, -30)
|
||||
assert item1.pos() == QtCore.QPointF(56, -30)
|
||||
|
|
@ -296,47 +314,50 @@ def test_arrange_horizontal_with_gap(view, settings):
|
|||
|
||||
def test_arrange_vertical(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
item1.filename = 'foo.png'
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item1.setPos(10, -100)
|
||||
item1.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.filename = 'bar.png'
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item2.setPos(-10, 40)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
item2.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
with patch.object(item1, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
with patch.object(item2, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
view.scene.arrange(vertical=True)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.arrange(vertical=True)
|
||||
|
||||
assert item1.pos() == QtCore.QPointF(0, -70)
|
||||
assert item2.pos() == QtCore.QPointF(0, 10)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_vertical_with_gap(view, settings):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
item1.filename = 'foo.png'
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item1.setPos(10, -100)
|
||||
item1.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.filename = 'bar.png'
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item2.setPos(-10, 40)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
item2.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
with patch.object(item1, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
with patch.object(item2, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
view.scene.arrange(vertical=True)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.arrange(vertical=True)
|
||||
|
||||
assert item1.pos() == QtCore.QPointF(0, -70)
|
||||
assert item2.pos() == QtCore.QPointF(0, 16)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_when_rotated(view):
|
||||
|
|
@ -429,6 +450,85 @@ def test_arrange_optimal_when_no_items(view):
|
|||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_square(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item1.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.filename = 'foo.png'
|
||||
item2.save_id = 66
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item2.crop = QtCore.QRectF(0, 0, 80, 60)
|
||||
|
||||
item3 = BeePixmapItem(QtGui.QImage())
|
||||
item3.save_id = 33
|
||||
view.scene.addItem(item3)
|
||||
item3.setSelected(True)
|
||||
item3.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
item4 = BeePixmapItem(QtGui.QImage())
|
||||
item4.filename = 'bar.png'
|
||||
item4.save_id = 77
|
||||
view.scene.addItem(item4)
|
||||
item4.setSelected(True)
|
||||
item4.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.arrange_square()
|
||||
|
||||
assert item4.pos() == QtCore.QPointF(-50, -40)
|
||||
assert item2.pos() == QtCore.QPointF(60, -30)
|
||||
assert item3.pos() == QtCore.QPointF(-50, 40)
|
||||
assert item1.pos() == QtCore.QPointF(50, 40)
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_square_with_gap(view, settings):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item1.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.filename = 'foo.png'
|
||||
item2.save_id = 66
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item2.crop = QtCore.QRectF(0, 0, 80, 60)
|
||||
|
||||
item3 = BeePixmapItem(QtGui.QImage())
|
||||
item3.save_id = 33
|
||||
view.scene.addItem(item3)
|
||||
item3.setSelected(True)
|
||||
item3.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
item4 = BeePixmapItem(QtGui.QImage())
|
||||
item4.filename = 'bar.png'
|
||||
item4.save_id = 77
|
||||
view.scene.addItem(item4)
|
||||
item4.setSelected(True)
|
||||
item4.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.arrange_square()
|
||||
|
||||
assert item4.pos() == QtCore.QPointF(-53, -43)
|
||||
assert item2.pos() == QtCore.QPointF(63, -33)
|
||||
assert item3.pos() == QtCore.QPointF(-53, 43)
|
||||
assert item1.pos() == QtCore.QPointF(53, 43)
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_square_when_no_items(view):
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.arrange_square()
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_flip_items(view, item):
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
|
|
@ -495,6 +595,25 @@ def test_crop_item_when_not_image(view):
|
|||
item.enter_crop_mode.assert_not_called()
|
||||
|
||||
|
||||
def test_sample_color_at_when_pixmap_item(view):
|
||||
color = QtGui.QColor(255, 0, 0, 3)
|
||||
img = QtGui.QImage(10, 10, QtGui.QImage.Format.Format_ARGB32)
|
||||
img.fill(color)
|
||||
item = BeePixmapItem(img, 'foo.png')
|
||||
view.scene.addItem(item)
|
||||
assert view.scene.sample_color_at(QtCore.QPointF(2, 2)) == color
|
||||
|
||||
|
||||
def test_sample_color_at_when_text_item(view):
|
||||
item = BeeTextItem('foo bar baz')
|
||||
view.scene.addItem(item)
|
||||
assert view.scene.sample_color_at(QtCore.QPointF(2, 2)) is None
|
||||
|
||||
|
||||
def test_sample_color_at_when_no_item(view):
|
||||
assert view.scene.sample_color_at(QtCore.QPointF(2, 2)) is None
|
||||
|
||||
|
||||
def test_select_all_items_when_true(view):
|
||||
item1 = BeeTextItem('foo')
|
||||
view.scene.addItem(item1)
|
||||
|
|
@ -631,8 +750,7 @@ def test_mouse_press_event_when_left_click_over_item(mouse_mock, view, item):
|
|||
view.scene.mousePressEvent(event)
|
||||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.move_active is True
|
||||
assert view.scene.rubberband_active is False
|
||||
assert view.scene.active_mode == view.scene.MOVE_MODE
|
||||
assert view.scene.event_start == QtCore.QPointF(10, 20)
|
||||
|
||||
|
||||
|
|
@ -651,8 +769,7 @@ def test_mouse_press_event_when_left_click_over_item_in_edit_mode(
|
|||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
item.exit_edit_mode.assert_not_called()
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.rubberband_active is False
|
||||
assert view.scene.active_mode is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
|
|
@ -670,8 +787,7 @@ def test_mouse_press_event_when_left_click_over_diff_item_in_edit_mode(
|
|||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
txtitem.exit_edit_mode.assert_called_once_with()
|
||||
assert view.scene.move_active is True
|
||||
assert view.scene.rubberband_active is False
|
||||
assert view.scene.active_mode == view.scene.MOVE_MODE
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
|
|
@ -689,8 +805,7 @@ def test_mouse_press_event_when_left_click_over_no_item_in_edit_mode(
|
|||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
item.exit_edit_mode.assert_called_once_with()
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.rubberband_active is True
|
||||
assert view.scene.active_mode == view.scene.RUBBERBAND_MODE
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
|
|
@ -707,8 +822,7 @@ def test_mouse_press_event_when_left_click_over_item_in_crop_mode(
|
|||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
view.scene.cancel_crop_mode.assert_not_called()
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.rubberband_active is False
|
||||
assert view.scene.active_mode is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
|
|
@ -726,8 +840,7 @@ def test_mouse_press_event_when_left_click_over_diff_item_in_crop_mode(
|
|||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
assert view.scene.move_active is True
|
||||
assert view.scene.rubberband_active is False
|
||||
assert view.scene.active_mode is view.scene.MOVE_MODE
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
|
|
@ -744,8 +857,7 @@ def test_mouse_press_event_when_left_click_over_no_item_in_crop_mode(
|
|||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.rubberband_active is True
|
||||
assert view.scene.active_mode == view.scene.RUBBERBAND_MODE
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
|
|
@ -760,8 +872,7 @@ def test_mouse_press_event_when_left_click_not_over_item(
|
|||
view.scene.mousePressEvent(event)
|
||||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.rubberband_active is True
|
||||
assert view.scene.active_mode == view.scene.RUBBERBAND_MODE
|
||||
assert view.scene.event_start == QtCore.QPointF(10, 20)
|
||||
|
||||
|
||||
|
|
@ -775,15 +886,14 @@ def test_mouse_press_event_when_no_items(mouse_mock, view):
|
|||
view.scene.mousePressEvent(event)
|
||||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.rubberband_active is False
|
||||
assert view.scene.active_mode is None
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseDoubleClickEvent')
|
||||
def test_mouse_doubleclick_event_when_over_item(mouse_mock, view, item):
|
||||
event = MagicMock()
|
||||
view.scene.move_active = True
|
||||
view.scene.active_mode = view.scene.MOVE_MODE
|
||||
view.scene.addItem(item)
|
||||
item.setPos(30, 40)
|
||||
item.setSelected(True)
|
||||
|
|
@ -794,7 +904,7 @@ def test_mouse_doubleclick_event_when_over_item(mouse_mock, view, item):
|
|||
return_value=QtCore.QRectF(0, 0, 100, 100)):
|
||||
view.scene.mouseDoubleClickEvent(event)
|
||||
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.active_mode is None
|
||||
view.fit_rect.assert_called_once_with(
|
||||
QtCore.QRectF(30, 40, 100, 100), toggle_item=item)
|
||||
mouse_mock.assert_not_called()
|
||||
|
|
@ -807,7 +917,7 @@ def test_mouse_doubleclick_event_when_over_editable_item(
|
|||
item = BeeTextItem('foo bar')
|
||||
item.enter_edit_mode = MagicMock()
|
||||
event = MagicMock()
|
||||
view.scene.move_active = True
|
||||
view.scene.active_mode = view.scene.MOVE_MODE
|
||||
view.scene.addItem(item)
|
||||
item.setPos(30, 40)
|
||||
item.setSelected(True)
|
||||
|
|
@ -818,7 +928,7 @@ def test_mouse_doubleclick_event_when_over_editable_item(
|
|||
return_value=QtCore.QRectF(0, 0, 100, 100)):
|
||||
view.scene.mouseDoubleClickEvent(event)
|
||||
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.active_mode is None
|
||||
item.enter_edit_mode.assert_called_once_with()
|
||||
double_mock.assert_not_called()
|
||||
press_mock.assert_called_once_with(event)
|
||||
|
|
@ -828,7 +938,7 @@ def test_mouse_doubleclick_event_when_over_editable_item(
|
|||
def test_mouse_doubleclick_event_when_item_not_selected(
|
||||
mouse_mock, view, item):
|
||||
event = MagicMock()
|
||||
view.scene.move_active = True
|
||||
view.scene.active_mode = view.scene.MOVE_MODE
|
||||
view.scene.addItem(item)
|
||||
item.setPos(30, 40)
|
||||
item.setSelected(False)
|
||||
|
|
@ -839,7 +949,7 @@ def test_mouse_doubleclick_event_when_item_not_selected(
|
|||
return_value=QtCore.QRectF(0, 0, 100, 100)):
|
||||
view.scene.mouseDoubleClickEvent(event)
|
||||
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.active_mode is None
|
||||
view.fit_rect.assert_called_once_with(
|
||||
QtCore.QRectF(30, 40, 100, 100), toggle_item=item)
|
||||
mouse_mock.assert_not_called()
|
||||
|
|
@ -861,7 +971,7 @@ def test_mouse_move_event_when_rubberband_new(
|
|||
mouse_mock, view, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
view.scene.rubberband_active = True
|
||||
view.scene.active_mode = view.scene.RUBBERBAND_MODE
|
||||
view.scene.addItem = MagicMock()
|
||||
view.scene.event_start = QtCore.QPointF(0, 0)
|
||||
view.scene.rubberband_item.bring_to_front = MagicMock()
|
||||
|
|
@ -877,7 +987,7 @@ def test_mouse_move_event_when_rubberband_new(
|
|||
view.scene.rubberband_item.rect().bottomRight().x() == 10
|
||||
view.scene.rubberband_item.rect().bottomRight().y() == 20
|
||||
assert item.isSelected() is True
|
||||
assert mouse_mock.called_once_with(event)
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseMoveEvent')
|
||||
|
|
@ -885,7 +995,7 @@ def test_mouse_move_event_when_rubberband_not_new(
|
|||
mouse_mock, view, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
view.scene.rubberband_active = True
|
||||
view.scene.active_mode = view.scene.RUBBERBAND_MODE
|
||||
view.scene.event_start = QtCore.QPointF(0, 0)
|
||||
view.scene.rubberband_item.bring_to_front = MagicMock()
|
||||
view.scene.addItem(view.scene.rubberband_item)
|
||||
|
|
@ -901,14 +1011,14 @@ def test_mouse_move_event_when_rubberband_not_new(
|
|||
view.scene.rubberband_item.rect().bottomRight().x() == 10
|
||||
view.scene.rubberband_item.rect().bottomRight().y() == 20
|
||||
assert item.isSelected() is True
|
||||
assert mouse_mock.called_once_with(event)
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseMoveEvent')
|
||||
def test_mouse_move_event_when_no_rubberband(mouse_mock, view, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
view.scene.rubberband_active = False
|
||||
view.scene.active_mode = None
|
||||
view.scene.event_start = QtCore.QPointF(0, 0)
|
||||
view.scene.rubberband_item.bring_to_front = MagicMock()
|
||||
view.scene.addItem = MagicMock()
|
||||
|
|
@ -923,19 +1033,19 @@ def test_mouse_move_event_when_no_rubberband(mouse_mock, view, imgfilename3x3):
|
|||
view.scene.rubberband_item.rect().bottomRight().x() == 0
|
||||
view.scene.rubberband_item.rect().bottomRight().y() == 0
|
||||
assert item.isSelected() is False
|
||||
assert mouse_mock.called_once_with(event)
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
|
||||
def test_mouse_release_event_when_rubberband_active(mouse_mock, view):
|
||||
event = MagicMock()
|
||||
view.scene.rubberband_active = True
|
||||
view.scene.active_mode = view.scene.RUBBERBAND_MODE
|
||||
view.scene.addItem(view.scene.rubberband_item)
|
||||
view.scene.removeItem = MagicMock()
|
||||
|
||||
view.scene.mouseReleaseEvent(event)
|
||||
view.scene.removeItem.assert_called_once_with(view.scene.rubberband_item)
|
||||
view.scene.rubberband_active is False
|
||||
view.scene.active_mode is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
|
||||
|
|
@ -943,7 +1053,7 @@ def test_mouse_release_event_when_move_active(mouse_mock, view, item):
|
|||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
event = MagicMock(scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)))
|
||||
view.scene.move_active = True
|
||||
view.scene.active_mode = view.scene.MOVE_MODE
|
||||
view.scene.event_start = QtCore.QPoint(0, 0)
|
||||
view.scene.undo_stack = MagicMock(push=MagicMock())
|
||||
|
||||
|
|
@ -957,7 +1067,7 @@ def test_mouse_release_event_when_move_active(mouse_mock, view, item):
|
|||
assert cmd.delta.x() == 10
|
||||
assert cmd.delta.y() == 20
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.active_mode is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
|
||||
|
|
@ -965,13 +1075,13 @@ def test_mouse_release_event_when_move_not_active(mouse_mock, view, item):
|
|||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
event = MagicMock(scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)))
|
||||
view.scene.move_active = False
|
||||
view.scene.active_mode = None
|
||||
view.scene.undo_stack = MagicMock(push=MagicMock())
|
||||
|
||||
view.scene.mouseReleaseEvent(event)
|
||||
view.scene.undo_stack.push.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.active_mode is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
|
||||
|
|
@ -979,13 +1089,13 @@ def test_mouse_release_event_when_no_selection(mouse_mock, view, item):
|
|||
view.scene.addItem(item)
|
||||
item.setSelected(False)
|
||||
event = MagicMock(scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)))
|
||||
view.scene.move_active = True
|
||||
view.scene.active_mode = view.scene.MOVE_MODE
|
||||
view.scene.undo_stack = MagicMock(push=MagicMock())
|
||||
|
||||
view.scene.mouseReleaseEvent(event)
|
||||
view.scene.undo_stack.push.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.active_mode is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
|
||||
|
|
@ -993,14 +1103,14 @@ def test_mouse_release_event_when_item_action_active(mouse_mock, view, item):
|
|||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
event = MagicMock(scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)))
|
||||
view.scene.move_active = True
|
||||
item.scale_active = True
|
||||
item.active_mode = item.SCALE_MODE
|
||||
view.scene.active_mode = view.scene.MOVE_MODE
|
||||
view.scene.undo_stack = MagicMock(push=MagicMock())
|
||||
|
||||
view.scene.mouseReleaseEvent(event)
|
||||
view.scene.undo_stack.push.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.active_mode is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
|
||||
|
|
@ -1012,14 +1122,14 @@ def test_mouse_release_event_when_multiselect_action_active(mouse_mock, view):
|
|||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
event = MagicMock(scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)))
|
||||
view.scene.move_active = True
|
||||
view.scene.multi_select_item.scale_active = True
|
||||
view.scene.active_mode = view.scene.MOVE_MODE
|
||||
view.scene.multi_select_item.active_mode = BeePixmapItem.SCALE_MODE
|
||||
view.scene.undo_stack = MagicMock(push=MagicMock())
|
||||
|
||||
view.scene.mouseReleaseEvent(event)
|
||||
view.scene.undo_stack.push.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.active_mode is None
|
||||
|
||||
|
||||
def test_selected_items(view):
|
||||
|
|
@ -1048,6 +1158,14 @@ def test_selected_items_user_only(view):
|
|||
assert item2 in selected
|
||||
|
||||
|
||||
def test_items_by_tpe(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
item2 = BeeTextItem('foo')
|
||||
view.scene.addItem(item2)
|
||||
assert list(view.scene.items_by_type('text')) == [item2]
|
||||
|
||||
|
||||
def test_items_for_save(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
|
|
@ -1229,8 +1347,7 @@ def test_on_selection_change_when_multi_selection_ended(view):
|
|||
def test_on_change_when_multi_select_when_no_scale_no_rotate(view):
|
||||
view.scene.addItem(view.scene.multi_select_item)
|
||||
view.scene.multi_select_item.fit_selection_area = MagicMock()
|
||||
view.scene.multi_select_item.scale_active = False
|
||||
view.scene.multi_select_item.rotate_active = False
|
||||
view.scene.multi_select_item.active_mode = None
|
||||
view.scene.on_change(None)
|
||||
view.scene.multi_select_item.fit_selection_area.assert_called_once()
|
||||
|
||||
|
|
@ -1238,8 +1355,7 @@ def test_on_change_when_multi_select_when_no_scale_no_rotate(view):
|
|||
def test_on_change_when_multi_select_when_scale_active(view):
|
||||
view.scene.addItem(view.scene.multi_select_item)
|
||||
view.scene.multi_select_item.fit_selection_area = MagicMock()
|
||||
view.scene.multi_select_item.scale_active = True
|
||||
view.scene.multi_select_item.rotate_active = False
|
||||
view.scene.multi_select_item.active_mode = BeePixmapItem.SCALE_MODE
|
||||
view.scene.on_change(None)
|
||||
view.scene.multi_select_item.fit_selection_area.assert_not_called()
|
||||
|
||||
|
|
@ -1247,16 +1363,14 @@ def test_on_change_when_multi_select_when_scale_active(view):
|
|||
def test_on_change_when_multi_select_when_rotate_active(view):
|
||||
view.scene.addItem(view.scene.multi_select_item)
|
||||
view.scene.multi_select_item.fit_selection_area = MagicMock()
|
||||
view.scene.multi_select_item.scale_active = False
|
||||
view.scene.multi_select_item.rotate_active = True
|
||||
view.scene.multi_select_item.active_mode = BeePixmapItem.ROTATE_MODE
|
||||
view.scene.on_change(None)
|
||||
view.scene.multi_select_item.fit_selection_area.assert_not_called()
|
||||
|
||||
|
||||
def test_on_change_when_no_multi_select(view):
|
||||
view.scene.multi_select_item.fit_selection_area = MagicMock()
|
||||
view.scene.multi_select_item.scale_active = True
|
||||
view.scene.multi_select_item.rotate_active = True
|
||||
view.scene.multi_select_item.active_mode = BeePixmapItem.SCALE_MODE
|
||||
view.scene.on_change(None)
|
||||
view.scene.multi_select_item.fit_selection_area.assert_not_called()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
||||
from beeref.actions.actions import Action
|
||||
from beeref import utils
|
||||
|
||||
|
||||
|
|
@ -71,3 +72,26 @@ def test_round_to(number, base, expected):
|
|||
('JPEG (*.jpg *.jpeg)', 'jpg')])
|
||||
def test_get_file_extension_from_format(formatstr, expected):
|
||||
assert utils.get_file_extension_from_format(formatstr) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('rgba,expected',
|
||||
[((255, 0, 0, 255), '#ff0000'),
|
||||
((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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
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,11 @@
|
|||
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,
|
||||
ConfirmCloseUnsavedWidget,
|
||||
ImageStorageFormatWidget,
|
||||
KeyboardSettingsDialog,
|
||||
KeyboardShortcutsDelegate,
|
||||
KeyboardShortcutsEditor,
|
||||
KeyboardShortcutsModel,
|
||||
KeyboardShortcutsProxy,
|
||||
SettingsDialog,
|
||||
)
|
||||
|
||||
|
|
@ -37,7 +32,7 @@ def test_image_storage_format_selects_radiobox(settings, view):
|
|||
def test_image_storage_format_saves_change(settings, view):
|
||||
settings.setValue('Items/image_storage_format', 'best')
|
||||
widget = ImageStorageFormatWidget()
|
||||
widget.buttons['jpg'].setChecked(True)
|
||||
widget.set_value('jpg')
|
||||
assert widget.buttons['best'].isChecked() is False
|
||||
assert widget.buttons['png'].isChecked() is False
|
||||
assert widget.buttons['jpg'].isChecked() is True
|
||||
|
|
@ -47,7 +42,7 @@ def test_image_storage_format_saves_change(settings, view):
|
|||
|
||||
def test_image_storage_format_on_restore_defaults(settings, view):
|
||||
widget = ImageStorageFormatWidget()
|
||||
widget.buttons['jpg'].setChecked(True)
|
||||
widget.set_value('jpg')
|
||||
settings.setValue('Items/image_storage_format', 'best')
|
||||
widget.on_restore_defaults()
|
||||
assert widget.buttons['best'].isChecked() is True
|
||||
|
|
@ -76,442 +71,63 @@ def test_arrange_gap_sets_title_when_edited(settings, view):
|
|||
def test_arrange_gap_saves_change(settings, view):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
widget = ArrangeGapWidget()
|
||||
widget.input.setValue(8)
|
||||
widget.set_value(8)
|
||||
assert settings.valueOrDefault('Items/arrange_gap') == 8
|
||||
assert widget.title() == 'Arrange Gap: ✎'
|
||||
|
||||
|
||||
def test_arrange_gap_on_restore_defaults(settings, view):
|
||||
widget = ArrangeGapWidget()
|
||||
widget.input.setValue(7)
|
||||
widget.set_value(7)
|
||||
settings.setValue('Items/arrange_gap', 0)
|
||||
widget.on_restore_defaults()
|
||||
assert widget.input.value() == 0
|
||||
assert widget.title() == 'Arrange Gap:'
|
||||
|
||||
|
||||
def test_confirm_closed_initialises_input_from_settings(settings, view):
|
||||
settings.setValue('Save/confirm_close_unsaved', False)
|
||||
widget = ConfirmCloseUnsavedWidget()
|
||||
assert widget.input.isChecked() is False
|
||||
|
||||
|
||||
def test_confirm_closed_sets_title_when_not_edited(settings, view):
|
||||
widget = ConfirmCloseUnsavedWidget()
|
||||
assert widget.title() == 'Confirm when closing an unsaved file:'
|
||||
|
||||
|
||||
def test_confirm_closed_sets_title_when_edited(settings, view):
|
||||
settings.setValue('Save/confirm_close_unsaved', False)
|
||||
widget = ConfirmCloseUnsavedWidget()
|
||||
assert widget.title() == 'Confirm when closing an unsaved file: ✎'
|
||||
|
||||
|
||||
def test_confirm_closed_saves_change(settings, view):
|
||||
settings.setValue('Save/confirm_close_unsaved', True)
|
||||
widget = ConfirmCloseUnsavedWidget()
|
||||
widget.set_value(False)
|
||||
assert settings.valueOrDefault('Save/confirm_close_unsaved') is False
|
||||
assert widget.title() == 'Confirm when closing an unsaved file: ✎'
|
||||
|
||||
|
||||
def test_confirm_closed_on_restore_defaults(settings, view):
|
||||
widget = ConfirmCloseUnsavedWidget()
|
||||
widget.set_value(False)
|
||||
settings.setValue('Save/confirm_close_unsaved', True)
|
||||
widget.on_restore_defaults()
|
||||
assert widget.input.isChecked() is True
|
||||
assert widget.title() == 'Confirm when closing an unsaved file:'
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.Yes)
|
||||
def test_settings_dialog_on_restore_defaults(msg_mock, settings, view):
|
||||
dialog = SettingsDialog(view)
|
||||
settings.setValue('Items/image_storage_format', 'jpg')
|
||||
settings.setValue('Items/arrange_gap', 10)
|
||||
settings.setValue('Save/confirm_close_unsaved', False)
|
||||
dialog.on_restore_defaults()
|
||||
msg_mock.assert_called_once()
|
||||
assert settings.valueOrDefault('Items/image_storage_format') == 'best'
|
||||
assert settings.valueOrDefault('Items/arrange_gap') == 0
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_editor_no_conflicts(view):
|
||||
a1 = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
a2 = Action({'id': 'bar', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
|
||||
editor = KeyboardShortcutsEditor(
|
||||
view,
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)))
|
||||
editor.setKeySequence('Ctrl+A')
|
||||
editor.on_editing_finished()
|
||||
assert editor.keySequence().toString() == 'Ctrl+A'
|
||||
assert editor.remove_from_other is None
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_editor_reenter_existing_shortcut(view):
|
||||
a1 = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
a2 = Action({'id': 'bar', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
|
||||
editor = KeyboardShortcutsEditor(
|
||||
view,
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)))
|
||||
editor.setKeySequence('Ctrl+F')
|
||||
editor.on_editing_finished()
|
||||
assert editor.keySequence().toString() == 'Ctrl+F'
|
||||
assert editor.remove_from_other is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.No)
|
||||
def test_keyboard_shortcuts_editor_conflicts_choose_to_cancel(
|
||||
question_mock, view):
|
||||
a1 = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
a2 = Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
|
||||
editor = KeyboardShortcutsEditor(
|
||||
view,
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)))
|
||||
editor.setKeySequence('Ctrl+B')
|
||||
editor.on_editing_finished()
|
||||
assert editor.keySequence().toString() == 'Ctrl+F'
|
||||
assert editor.remove_from_other is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.Yes)
|
||||
def test_keyboard_shortcuts_editor_conflicts_choose_to_confirm(
|
||||
question_mock, view):
|
||||
a1 = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
a2 = Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
|
||||
editor = KeyboardShortcutsEditor(
|
||||
view,
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)))
|
||||
editor.setKeySequence('Ctrl+B')
|
||||
editor.on_editing_finished()
|
||||
assert editor.keySequence().toString() == 'Ctrl+B'
|
||||
assert editor.remove_from_other == a2
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.No)
|
||||
def test_keyboard_shortcuts_editor_conflicts_choose_to_cancel_when_no_shortcut(
|
||||
question_mock, view):
|
||||
a1 = Action({'id': 'foo', 'text': 'Foo'})
|
||||
a2 = Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
|
||||
editor = KeyboardShortcutsEditor(
|
||||
view,
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)))
|
||||
editor.setKeySequence('Ctrl+B')
|
||||
editor.on_editing_finished()
|
||||
assert editor.keySequence().toString() == ''
|
||||
assert a2.get_shortcuts() == ['Ctrl+B']
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_delegate_setmodeldata(view):
|
||||
a1 = Action({'id': 'foo', 'text': 'Foo'})
|
||||
a2 = Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
|
||||
model = KeyboardShortcutsModel()
|
||||
delegate = KeyboardShortcutsDelegate()
|
||||
editor = delegate.createEditor(view, None, index=model.index(0, 2))
|
||||
editor.setKeySequence('Ctrl+F')
|
||||
delegate.setModelData(
|
||||
editor, model, index=model.index(0, 2))
|
||||
assert a1.get_shortcuts() == ['Ctrl+F']
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_columncount():
|
||||
model = KeyboardShortcutsModel()
|
||||
model.columnCount(None) == 4
|
||||
|
||||
|
||||
@patch('beeref.widgets.settings.actions',
|
||||
ActionList([
|
||||
Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']}),
|
||||
Action({'id': 'bar', 'text': 'Bar', 'shortcuts': ['Ctrl+B']})
|
||||
]))
|
||||
def test_keyboard_shortcuts_model_rowcount():
|
||||
model = KeyboardShortcutsModel()
|
||||
model.rowCount(None) == 2
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_gets_text():
|
||||
action = Action({'id': 'foo', 'text': '&Foo', 'shortcuts': ['Ctrl+F']})
|
||||
action.menu_path = ['&Bar', 'Ba&z']
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=0),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value == 'Bar: Baz: Foo'
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_gets_changed_when_not_changed():
|
||||
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=1),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value is None
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_gets_changed_when_changed(kbsettings):
|
||||
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
action.set_shortcuts(['Ctrl+B'])
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=1),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value == '✎'
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_gets_shortcut():
|
||||
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert value == 'Ctrl+F'
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_tooltip_changed_when_not_changed():
|
||||
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=1),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value is None
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_tooltip_changed_when_changed():
|
||||
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
action.set_shortcuts(['Ctrl+B'])
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=1),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value == 'Changed from default'
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_tooltip_shortcut_when_not_changed():
|
||||
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value is None
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_tooltip_shortcut_not_changed_not_set():
|
||||
action = Action({'id': 'foo', 'text': 'Foo'})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value is None
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_tooltip_shortcut_when_changed():
|
||||
action = Action({'id': 'foo', 'text': 'Foo', 'shortcuts': ['Ctrl+F']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
action.set_shortcuts(['Ctrl+B'])
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value == 'Default: Ctrl+F'
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_data_tooltip_shortcut_changed_from_none():
|
||||
action = Action({'id': 'foo', 'text': 'Foo'})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
action.set_shortcuts(['Ctrl+B'])
|
||||
model = KeyboardShortcutsModel()
|
||||
value = model.data(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
role=QtCore.Qt.ItemDataRole.ToolTipRole)
|
||||
assert value == 'Default: -'
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_setdata_saves():
|
||||
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
model.setData(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
value=QtGui.QKeySequence('Ctrl+B'),
|
||||
role=None)
|
||||
assert action.get_shortcuts() == ['Ctrl+B']
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_setdata_saves_second_shortcut():
|
||||
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
model.setData(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
value=QtGui.QKeySequence('Ctrl+B'),
|
||||
role=None)
|
||||
assert action.get_shortcuts() == ['Ctrl+F', 'Ctrl+B']
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_setdata_saves_second_shortcut_no_first():
|
||||
action = Action({'id': 'foo'})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
model.setData(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
value=QtGui.QKeySequence('Ctrl+B'),
|
||||
role=None)
|
||||
assert action.get_shortcuts() == ['Ctrl+B']
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_setdata_removes_duplicate():
|
||||
action = Action({'id': 'foo', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([action])):
|
||||
model = KeyboardShortcutsModel()
|
||||
model.setData(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=3),
|
||||
row=MagicMock(return_value=0)),
|
||||
value=QtGui.QKeySequence('Ctrl+B'),
|
||||
role=None)
|
||||
assert action.get_shortcuts() == ['Ctrl+B']
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_setdata_remove_from_other():
|
||||
a1 = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
a2 = Action({'id': 'bar', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
|
||||
model = KeyboardShortcutsModel()
|
||||
model.setData(
|
||||
index=MagicMock(
|
||||
column=MagicMock(return_value=2),
|
||||
row=MagicMock(return_value=0)),
|
||||
value=QtGui.QKeySequence('Ctrl+B'),
|
||||
role=None,
|
||||
remove_from_other=a2)
|
||||
assert a1.get_shortcuts() == ['Ctrl+B']
|
||||
assert a2.get_shortcuts() == []
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_model_headerdata():
|
||||
model = KeyboardShortcutsModel()
|
||||
header = model.headerData(
|
||||
0,
|
||||
QtCore.Qt.Orientation.Horizontal,
|
||||
QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
assert header == 'Action'
|
||||
|
||||
|
||||
def test_flags_first_column():
|
||||
model = KeyboardShortcutsModel()
|
||||
flags = model.flags(model.index(0, 0))
|
||||
assert flags == (QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemNeverHasChildren)
|
||||
|
||||
|
||||
def test_flags_shortcut_column():
|
||||
model = KeyboardShortcutsModel()
|
||||
flags = model.flags(model.index(0, 2))
|
||||
assert flags == (QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemNeverHasChildren
|
||||
| QtCore.Qt.ItemFlag.ItemIsEditable)
|
||||
|
||||
|
||||
@patch('beeref.widgets.settings.actions',
|
||||
ActionList([Action({'id': 'bar', 'text': 'Bar'}),
|
||||
Action({'id': 'foo', 'text': 'Foo'}),
|
||||
Action({'id': 'baz', 'text': 'Baz'})]))
|
||||
def test_keyboard_shortcuts_proxy_data_unfiltered():
|
||||
proxy = KeyboardShortcutsProxy()
|
||||
color1 = proxy.data(
|
||||
proxy.index(0, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
|
||||
color2 = proxy.data(
|
||||
proxy.index(1, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
|
||||
color3 = proxy.data(
|
||||
proxy.index(2, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
|
||||
|
||||
assert color1 == color3
|
||||
assert color1 != color2
|
||||
|
||||
assert proxy.data(
|
||||
proxy.index(0, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Bar'
|
||||
assert proxy.data(
|
||||
proxy.index(1, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Foo'
|
||||
assert proxy.data(
|
||||
proxy.index(2, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Baz'
|
||||
|
||||
|
||||
@patch('beeref.widgets.settings.actions',
|
||||
ActionList([Action({'id': 'bar', 'text': 'Bar'}),
|
||||
Action({'id': 'foo', 'text': 'Foo'}),
|
||||
Action({'id': 'baz', 'text': 'Baz'})]))
|
||||
def test_keyboard_shortcuts_proxy_data_filtered():
|
||||
proxy = KeyboardShortcutsProxy()
|
||||
proxy.setFilterFixedString('b')
|
||||
color1 = proxy.data(
|
||||
proxy.index(0, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
|
||||
color2 = proxy.data(
|
||||
proxy.index(1, 0), QtCore.Qt.ItemDataRole.BackgroundRole)
|
||||
|
||||
assert color1 != color2
|
||||
|
||||
assert proxy.data(
|
||||
proxy.index(0, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Bar'
|
||||
assert proxy.data(
|
||||
proxy.index(1, 0), QtCore.Qt.ItemDataRole.DisplayRole) == 'Baz'
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_proxy_setdata_saves_correct_filtered_index():
|
||||
a1 = Action({'id': 'bar', 'text': 'Bar'})
|
||||
a2 = Action({'id': 'foo', 'text': 'Foo'})
|
||||
a3 = Action({'id': 'baz', 'text': 'Baz'})
|
||||
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2, a3])):
|
||||
proxy = KeyboardShortcutsProxy()
|
||||
proxy.setFilterFixedString('b')
|
||||
proxy.setData(
|
||||
index=proxy.index(1, 2),
|
||||
value=QtGui.QKeySequence('Ctrl+B'),
|
||||
role=None)
|
||||
assert a1.get_shortcuts() == []
|
||||
assert a2.get_shortcuts() == []
|
||||
assert a3.get_shortcuts() == ['Ctrl+B']
|
||||
|
||||
|
||||
def test_keyboard_shortcuts_proxy_setdata_remove_from_other():
|
||||
a1 = Action({'id': 'foo', 'shortcuts': ['Ctrl+F']})
|
||||
a2 = Action({'id': 'bar', 'shortcuts': ['Ctrl+B']})
|
||||
with patch('beeref.widgets.settings.actions', ActionList([a1, a2])):
|
||||
proxy = KeyboardShortcutsProxy()
|
||||
proxy.setData(
|
||||
index=proxy.index(0, 2),
|
||||
value=QtGui.QKeySequence('Ctrl+B'),
|
||||
role=None,
|
||||
remove_from_other=a2)
|
||||
assert a1.get_shortcuts() == ['Ctrl+B']
|
||||
assert a2.get_shortcuts() == []
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.Yes)
|
||||
@patch('beeref.config.KeyboardSettings.restore_defaults')
|
||||
def test_keyboard_settings_dialog_on_restore_defaults(
|
||||
restore_mock, msg_mock, kbsettings, view):
|
||||
dialog = KeyboardSettingsDialog(view)
|
||||
dialog.on_restore_defaults()
|
||||
msg_mock.assert_called_once()
|
||||
restore_mock.assert_called()
|
||||
assert settings.valueOrDefault('Save/confirm_close_unsaved') is True
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from PyQt6 import QtCore, QtWidgets, QtGui
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref.config import logfile_name
|
||||
from beeref.widgets import (
|
||||
BeeNotification,
|
||||
ChangeOpacityDialog,
|
||||
DebugLogDialog,
|
||||
ExportImagesFileExistsDialog,
|
||||
SampleColorWidget,
|
||||
SceneToPixmapExporterDialog,
|
||||
)
|
||||
|
||||
|
|
@ -94,3 +99,56 @@ def test_change_opacity_dialog_reject(view, item):
|
|||
dlg.reject()
|
||||
assert item.opacity() == 0.6
|
||||
assert len(stack) == 0
|
||||
|
||||
|
||||
@patch('PyQt6.QtCore.QTimer.singleShot')
|
||||
def test_bee_notification(single_shot_mock, view):
|
||||
widget = BeeNotification(view, 'Hello World')
|
||||
assert widget.label.text() == 'Hello World'
|
||||
single_shot_mock.assert_called_once_with(1000 * 3, widget.deleteLater)
|
||||
|
||||
|
||||
def test_sample_color_widget(view):
|
||||
widget = SampleColorWidget(
|
||||
view, QtCore.QPoint(2, 5), QtGui.QColor(255, 0, 0))
|
||||
assert widget.color == QtGui.QColor(255, 0, 0)
|
||||
assert widget.geometry() == QtCore.QRect(12, 15, 50, 50)
|
||||
|
||||
widget.update(QtCore.QPoint(13, 15), QtGui.QColor(0, 255, 0))
|
||||
assert widget.color == QtGui.QColor(0, 255, 0)
|
||||
assert widget.geometry() == QtCore.QRect(23, 25, 50, 50)
|
||||
|
||||
|
||||
def test_sample_color_widget_paint_event_when_color(view):
|
||||
widget = SampleColorWidget(
|
||||
view, QtCore.QPoint(2, 5), QtGui.QColor(255, 0, 0))
|
||||
with patch('PyQt6.QtGui.QPainter') as painter_cls_mock:
|
||||
painter_mock = MagicMock()
|
||||
painter_cls_mock.return_value = painter_mock
|
||||
widget.paintEvent(MagicMock())
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
|
||||
painter_mock.setBrush.assert_called_once_with(brush)
|
||||
painter_mock.drawRect.assert_called_once_with(0, 0, 50, 50)
|
||||
|
||||
|
||||
def test_sample_color_widget_paint_event_when_no_color(view):
|
||||
widget = SampleColorWidget(view, QtCore.QPoint(2, 5), None)
|
||||
with patch('PyQt6.QtGui.QPainter') as painter_cls_mock:
|
||||
painter_mock = MagicMock()
|
||||
painter_cls_mock.return_value = painter_mock
|
||||
widget.paintEvent(MagicMock())
|
||||
brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0))
|
||||
painter_mock.setBrush.assert_called_once_with(brush)
|
||||
painter_mock.drawRect.assert_called_once_with(0, 0, 50, 50)
|
||||
|
||||
|
||||
def test_export_images_file_exists_dialog(view):
|
||||
dlg = ExportImagesFileExistsDialog(view, '/tmp/foo.png')
|
||||
assert len(dlg.radio_buttons) == 4
|
||||
assert dlg.get_answer() == 'skip'
|
||||
|
||||
|
||||
def test_export_images_file_exists_dialog_get_answer(view):
|
||||
dlg = ExportImagesFileExistsDialog(view, '/tmp/foo.png')
|
||||
dlg.radio_buttons['overwrite'].setChecked(True)
|
||||
assert dlg.get_answer() == 'overwrite'
|
||||
|
|
|
|||
228
tools/build_appimage.py
Executable file
228
tools/build_appimage.py
Executable file
|
|
@ -0,0 +1,228 @@
|
|||
#!/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 glob
|
||||
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"
|
||||
"""]
|
||||
|
||||
runbee = f'"$APPDIR/opt/python{PYVER}/bin/python{PYVER}" -I -m beeref "$@"'
|
||||
logfile = '/tmp/BeeRefAppimageLog.txt'
|
||||
|
||||
content.extend([
|
||||
f'export LD_LIBRARY_PATH="{ld_paths}"',
|
||||
f'{runbee} 2> >(tee {logfile} >&2)',
|
||||
'if [ $? -ne 0 ]; then',
|
||||
# Workaround for:
|
||||
# https://bugreports.qt.io/browse/QTBUG-114635
|
||||
# See also https://github.com/rbreu/beeref/issues/102
|
||||
f' if grep -q wl_proxy_marshal_flags {logfile}; then',
|
||||
' echo "Wayland version error; trying again without Wayland"',
|
||||
f' QT_QPA_PLATFORM=xcb {runbee}',
|
||||
'fi; fi;'])
|
||||
|
||||
|
||||
with open('squashfs-root/AppRun', 'w') as f:
|
||||
f.write('\n'.join(content))
|
||||
os.chmod('squashfs-root/AppRun', 0o755)
|
||||
|
||||
|
||||
logger.info('Copying appdata.xml...')
|
||||
for f in glob.glob('squashfs-root/usr/share/metainfo/*'):
|
||||
os.remove(f)
|
||||
|
||||
filename = 'org.beeref.BeeRef.appdata.xml'
|
||||
shutil.copyfile(filename, f'squashfs-root/usr/share/metainfo/{filename}')
|
||||
|
||||
logger.info('Writing .desktop...')
|
||||
for f in glob.glob('squashfs-root/usr/share/applications/*'):
|
||||
os.remove(f)
|
||||
for f in glob.glob('squashfs-root/*.desktop'):
|
||||
os.remove(f)
|
||||
|
||||
content = f"""[Desktop Entry]
|
||||
Name=BeeRef
|
||||
GenericName=Image Viewer
|
||||
Comment=A simple reference image viewer
|
||||
Terminal=false
|
||||
Exec=BeeRef-{BEEVERSION}
|
||||
Type=Application
|
||||
Icon=logo
|
||||
|
||||
MimeType=application/x-beeref;
|
||||
Categories=Qt;KDE;Graphics;
|
||||
X-KDE-NativeMimeType=application/x-beeref
|
||||
X-KDE-ExtraNativeMimeTypes=
|
||||
|
||||
X-AppImage-Version={BEEVERSION}
|
||||
"""
|
||||
|
||||
filename = 'squashfs-root/usr/share/applications/org.beeref.BeeRef.desktop'
|
||||
with open(filename, 'w') as f:
|
||||
f.write(content)
|
||||
os.symlink('usr/share/applications/org.beeref.BeeRef.desktop',
|
||||
'squashfs-root/BeeRef.desktop')
|
||||
|
||||
logger.info('Copying logos...')
|
||||
shutil.copyfile('./beeref/assets/logo.svg', 'squashfs-root/logo.svg')
|
||||
shutil.copyfile('./beeref/assets/logo.png', 'squashfs-root/.DirIcon')
|
||||
|
||||
|
||||
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')
|
||||
175
tools/find_linux_libs.py
Executable file
175
tools/find_linux_libs.py
Executable file
|
|
@ -0,0 +1,175 @@
|
|||
#!/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'])
|
||||
excludes = set(data['excludes'])
|
||||
else:
|
||||
logger.info(f'No file {args.jsonfile}; starting from scratch')
|
||||
known_libs = []
|
||||
packages = set()
|
||||
excludes = set()
|
||||
|
||||
|
||||
for lib in iter_lsofoutput(output):
|
||||
links = what_links_to(lib)
|
||||
if len(links) == 0:
|
||||
pass
|
||||
elif 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 new 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)
|
||||
|
||||
|
||||
for ex in exclude_masterlist:
|
||||
for lib in (libs + known_libs):
|
||||
if lib.endswith(ex):
|
||||
excludes.add(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))
|
||||
245
tools/linux_libs.json
Normal file
245
tools/linux_libs.json
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
{
|
||||
"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",
|
||||
"libcanberra-gtk-module.so",
|
||||
"libcanberra-gtk3.so.0",
|
||||
"libcom_err.so.2",
|
||||
"libdl.so.2",
|
||||
"libexpat.so.1",
|
||||
"libfontconfig.so.1",
|
||||
"libfreetype.so.6",
|
||||
"libfribidi.so.0",
|
||||
"libgcc_s.so.1",
|
||||
"libgdk-3.so.0",
|
||||
"libgdk_pixbuf-2.0.so.0",
|
||||
"libgio-2.0.so.0",
|
||||
"libgpg-error.so.0",
|
||||
"libgvfscommon.so",
|
||||
"libgvfsdbus.so",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
147
tools/linux_libs_kde.json
Normal file
147
tools/linux_libs_kde.json
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"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/libutil.so.1",
|
||||
"/lib/x86_64-linux-gnu/libz.so.1",
|
||||
"/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/libXdmcp.so.6",
|
||||
"/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/libcrypto.so",
|
||||
"/usr/lib/x86_64-linux-gnu/libffi.so",
|
||||
"/usr/lib/x86_64-linux-gnu/libfontconfig.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libfreetype.so.6",
|
||||
"/usr/lib/x86_64-linux-gnu/libgcrypt.so.20",
|
||||
"/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/libgssapi_krb5.so.2",
|
||||
"/usr/lib/x86_64-linux-gnu/libgthread-2.0.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/liblz4.so.1",
|
||||
"/usr/lib/x86_64-linux-gnu/libmd.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/libuuid.so",
|
||||
"/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": [
|
||||
"libbrotli1",
|
||||
"libbsd0",
|
||||
"libbz2-1.0",
|
||||
"libc6",
|
||||
"libcom-err2",
|
||||
"libdbus-1-3",
|
||||
"libexpat1",
|
||||
"libffi7",
|
||||
"libfontconfig1",
|
||||
"libfreetype6",
|
||||
"libgcc-s1",
|
||||
"libgcrypt20",
|
||||
"libglib2.0-0",
|
||||
"libglvnd0",
|
||||
"libglx0",
|
||||
"libgpg-error0",
|
||||
"libgssapi-krb5-2",
|
||||
"libk5crypto3",
|
||||
"libkeyutils1",
|
||||
"libkrb5-3",
|
||||
"libkrb5support0",
|
||||
"liblz4-1",
|
||||
"liblzma5",
|
||||
"libmd0",
|
||||
"libpcre3",
|
||||
"libpng16-16",
|
||||
"libsqlite3-0",
|
||||
"libssl1.1",
|
||||
"libstdc++6",
|
||||
"libsystemd0",
|
||||
"libuuid1",
|
||||
"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",
|
||||
"libxdmcp6",
|
||||
"libxkbcommon-x11-0",
|
||||
"libxkbcommon0",
|
||||
"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",
|
||||
"libgcc_s.so.1",
|
||||
"libgpg-error.so.0",
|
||||
"libm.so.6",
|
||||
"libpthread.so.0",
|
||||
"libresolv.so.2",
|
||||
"librt.so.1",
|
||||
"libstdc++.so.6",
|
||||
"libutil.so.1",
|
||||
"libxcb.so.1",
|
||||
"libz.so.1"
|
||||
]
|
||||
}
|
||||
238
tools/linux_libs_nativefiledlg.json
Normal file
238
tools/linux_libs_nativefiledlg.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",
|
||||
"/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",
|
||||
"/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-pixbuf-2.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"
|
||||
]
|
||||
}
|
||||
20
tools/linux_libs_notes.rst
Normal file
20
tools/linux_libs_notes.rst
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
Appimage : Explanations for including/excluding libraries
|
||||
=========================================================
|
||||
|
||||
|
||||
GTK/Gnome
|
||||
---------
|
||||
|
||||
By default, in a Gnome session BeeRef pulls a couple of gtk/gdg libs. When opening a native Gnome/GTK file dialog, more gtk/gdl libs are loaded from the system, which might be incompatable.
|
||||
|
||||
https://github.com/rbreu/beeref/discussions/103
|
||||
|
||||
Current workaround: Exclude the following libs::
|
||||
|
||||
"libgvfsdbus.so"
|
||||
"libcanberra-gtk-module.so"
|
||||
"libgvfscommon.so"
|
||||
"libcanberra-gtk3.so.0"
|
||||
"libgdk-3.so.0"
|
||||
"libgdk_pixbuf-2.0.so.0"
|
||||
"libgio-2.0.so.0"
|
||||
Loading…
Reference in a new issue