mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Refactor creation of menu and actions
This commit is contained in:
parent
4e31ec8bac
commit
0d4b5da84b
8 changed files with 368 additions and 121 deletions
19
beeref/actions/__init__.py
Normal file
19
beeref/actions/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# 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 .mixin import ActionsMixin
|
||||
|
||||
|
||||
__all__ = ['ActionsMixin']
|
||||
101
beeref/actions/actions.py
Normal file
101
beeref/actions/actions.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# 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/>.
|
||||
|
||||
actions = [
|
||||
{
|
||||
'id': 'open',
|
||||
'text': '&Open',
|
||||
'shortcuts': ['Ctrl+O'],
|
||||
'callback': 'on_action_open',
|
||||
},
|
||||
{
|
||||
'id': 'save',
|
||||
'text': '&Save',
|
||||
'shortcuts': ['Ctrl+S'],
|
||||
'callback': 'on_action_save',
|
||||
},
|
||||
{
|
||||
'id': 'save_as',
|
||||
'text': 'Save &As...',
|
||||
'shortcuts': ['Ctrl+Shift+S'],
|
||||
'callback': 'on_action_save_as',
|
||||
},
|
||||
{
|
||||
'id': 'quit',
|
||||
'text': '&Quit...',
|
||||
'shortcuts': ['Ctrl+Q'],
|
||||
'callback': 'on_action_quit',
|
||||
},
|
||||
{
|
||||
'id': 'insert_images',
|
||||
'text': '&Insert Images...',
|
||||
'shortcuts': ['Ctrl+I'],
|
||||
'callback': 'on_action_insert_images',
|
||||
},
|
||||
{
|
||||
'id': 'undo',
|
||||
'text': '&Undo',
|
||||
'shortcuts': ['Ctrl+Z'],
|
||||
'callback': 'on_action_undo',
|
||||
'group': 'active_when_can_undo',
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'id': 'redo',
|
||||
'text': '&Redo',
|
||||
'shortcuts': ['Ctrl+Shift+Z'],
|
||||
'callback': 'on_action_redo',
|
||||
'group': 'active_when_can_redo',
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'id': 'paste',
|
||||
'text': '&Paste',
|
||||
'shortcuts': ['Ctrl+V'],
|
||||
'callback': 'on_action_paste',
|
||||
},
|
||||
{
|
||||
'id': 'delete',
|
||||
'text': '&Delete',
|
||||
'shortcuts': ['Del'],
|
||||
'callback': 'on_action_delete_items',
|
||||
'group': 'active_when_selection',
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'id': 'normalize_height',
|
||||
'text': '&Height',
|
||||
'shortcuts': ['Shift+H'],
|
||||
'callback': 'on_action_normalize_height',
|
||||
'group': 'active_when_selection',
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'id': 'normalize_width',
|
||||
'text': '&Width',
|
||||
'shortcuts': ['Shift+W'],
|
||||
'callback': 'on_action_normalize_width',
|
||||
'group': 'active_when_selection',
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'id': 'normalize_size',
|
||||
'text': '&Size',
|
||||
'shortcuts': ['Shift+S'],
|
||||
'callback': 'on_action_normalize_size',
|
||||
'group': 'active_when_selection',
|
||||
'enabled': False,
|
||||
},
|
||||
]
|
||||
47
beeref/actions/menu_structure.py
Normal file
47
beeref/actions/menu_structure.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# 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/>.
|
||||
|
||||
MENU_SEPARATOR = 0
|
||||
|
||||
menu_structure = [
|
||||
{
|
||||
'menu': '&File',
|
||||
'items': [
|
||||
'open',
|
||||
'save',
|
||||
'save_as',
|
||||
MENU_SEPARATOR,
|
||||
'quit',
|
||||
],
|
||||
},
|
||||
'insert_images',
|
||||
{
|
||||
'menu': '&Edit',
|
||||
'items': [
|
||||
'undo',
|
||||
'redo',
|
||||
'paste',
|
||||
'delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
'menu': '&Normalize',
|
||||
'items': [
|
||||
'normalize_height',
|
||||
'normalize_width',
|
||||
'normalize_size',
|
||||
],
|
||||
},
|
||||
]
|
||||
62
beeref/actions/mixin.py
Normal file
62
beeref/actions/mixin.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# 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 collections import defaultdict
|
||||
|
||||
from PyQt6 import QtGui, QtWidgets
|
||||
|
||||
from .actions import actions
|
||||
from .menu_structure import menu_structure, MENU_SEPARATOR
|
||||
|
||||
|
||||
class ActionsMixin:
|
||||
|
||||
def actiongroup_set_enabled(self, group, value):
|
||||
for action in self.bee_actiongroups[group]:
|
||||
action.setEnabled(value)
|
||||
|
||||
def create_menu_and_actions(self):
|
||||
self._create_actions()
|
||||
menu = QtWidgets.QMenu(self)
|
||||
menu = self._create_menu(
|
||||
self.bee_actions, QtWidgets.QMenu(self), menu_structure)
|
||||
return menu
|
||||
|
||||
def _create_actions(self):
|
||||
self.bee_actions = {}
|
||||
self.bee_actiongroups = defaultdict(list)
|
||||
|
||||
for action in actions:
|
||||
qaction = QtGui.QAction(action['text'], self)
|
||||
if 'shortcuts' in action:
|
||||
qaction.setShortcuts(action['shortcuts'])
|
||||
qaction.triggered.connect(getattr(self, action['callback']))
|
||||
self.addAction(qaction)
|
||||
qaction.setEnabled(action.get('enabled', True))
|
||||
self.bee_actions[action['id']] = qaction
|
||||
if 'group' in action:
|
||||
self.bee_actiongroups[action['group']].append(qaction)
|
||||
|
||||
def _create_menu(self, actions, menu, items):
|
||||
for item in items:
|
||||
if isinstance(item, str):
|
||||
menu.addAction(actions[item])
|
||||
if item == MENU_SEPARATOR:
|
||||
menu.addSeparator()
|
||||
if isinstance(item, dict):
|
||||
submenu = menu.addMenu(item['menu'])
|
||||
self._create_menu(actions, submenu, item['items'])
|
||||
|
||||
return menu
|
||||
127
beeref/view.py
127
beeref/view.py
|
|
@ -18,6 +18,7 @@ import logging
|
|||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref.actions import ActionsMixin
|
||||
from beeref import commands
|
||||
from beeref.config import CommandlineArgs
|
||||
from beeref import fileio
|
||||
|
|
@ -30,7 +31,7 @@ commandline_args = CommandlineArgs()
|
|||
logger = logging.getLogger('BeeRef')
|
||||
|
||||
|
||||
class BeeGraphicsView(QtWidgets.QGraphicsView):
|
||||
class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
||||
|
||||
def __init__(self, app, parent=None):
|
||||
super().__init__(parent)
|
||||
|
|
@ -67,8 +68,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView):
|
|||
self.setContextMenuPolicy(
|
||||
Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.on_context_menu)
|
||||
self.context_menu = QtWidgets.QMenu(self)
|
||||
self.build_actions()
|
||||
self.context_menu = self.create_menu_and_actions()
|
||||
|
||||
self.welcome_overlay = WelcomeOverlay(self)
|
||||
|
||||
|
|
@ -84,124 +84,11 @@ class BeeGraphicsView(QtWidgets.QGraphicsView):
|
|||
self.welcome_overlay.hide()
|
||||
self.recalc_scene_rect()
|
||||
|
||||
def build_actions(self):
|
||||
|
||||
self.actions_active_when_selection = []
|
||||
self.actions_active_when_can_undo = []
|
||||
self.actions_active_when_can_redo = []
|
||||
|
||||
def add_to_menu(menu, actions):
|
||||
for action in actions:
|
||||
qaction = QtGui.QAction(action['text'], self)
|
||||
if 'shortcuts' in action:
|
||||
qaction.setShortcuts(action['shortcuts'])
|
||||
qaction.triggered.connect(action['callback'])
|
||||
self.addAction(qaction)
|
||||
menu.addAction(qaction)
|
||||
if 'group' in action:
|
||||
action['group'].append(qaction)
|
||||
qaction.setEnabled(action.get('enabled', True))
|
||||
|
||||
# File menu
|
||||
actions = [
|
||||
{
|
||||
'text': '&Open',
|
||||
'shortcuts': ['Ctrl+O'],
|
||||
'callback': self.on_action_open,
|
||||
},
|
||||
{
|
||||
'text': '&Save',
|
||||
'shortcuts': ['Ctrl+S'],
|
||||
'callback': self.on_action_save,
|
||||
},
|
||||
{
|
||||
'text': 'Save &As...',
|
||||
'shortcuts': ['Ctrl+Shift+S'],
|
||||
'callback': self.on_action_save_as,
|
||||
},
|
||||
{
|
||||
'text': '&Quit...',
|
||||
'shortcuts': ['Ctrl+Q'],
|
||||
'callback': self.on_action_quit,
|
||||
},
|
||||
]
|
||||
add_to_menu(self.context_menu.addMenu('&File'), actions)
|
||||
|
||||
# Main menu
|
||||
actions = [
|
||||
{
|
||||
'text': '&Insert Images...',
|
||||
'shortcuts': ['Ctrl+I'],
|
||||
'callback': self.on_action_insert_images,
|
||||
},
|
||||
|
||||
]
|
||||
add_to_menu(self.context_menu, actions)
|
||||
|
||||
# Edit menu
|
||||
actions = [
|
||||
{
|
||||
'text': '&Undo',
|
||||
'shortcuts': ['Ctrl+Z'],
|
||||
'callback': self.on_action_undo,
|
||||
'group': self.actions_active_when_can_undo,
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'text': '&Redo',
|
||||
'shortcuts': ['Ctrl+Shift+Z'],
|
||||
'callback': self.on_action_redo,
|
||||
'group': self.actions_active_when_can_redo,
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'text': '&Paste',
|
||||
'shortcuts': ['Ctrl+V'],
|
||||
'callback': self.on_action_paste,
|
||||
},
|
||||
{
|
||||
'text': '&Delete',
|
||||
'shortcuts': ['Del'],
|
||||
'callback': self.on_action_delete_items,
|
||||
'group': self.actions_active_when_selection,
|
||||
'enabled': False,
|
||||
},
|
||||
]
|
||||
add_to_menu(self.context_menu.addMenu('&Edit'), actions)
|
||||
|
||||
items_menu = self.context_menu.addMenu('&Items')
|
||||
actions = [
|
||||
{
|
||||
'text': '&Height',
|
||||
'shortcuts': ['Shift+H'],
|
||||
'callback': self.on_action_normalize_height,
|
||||
'group': self.actions_active_when_selection,
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'text': '&Width',
|
||||
'shortcuts': ['Shift+W'],
|
||||
'callback': self.on_action_normalize_width,
|
||||
'group': self.actions_active_when_selection,
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'text': '&Size',
|
||||
'shortcuts': ['Shift+S'],
|
||||
'callback': self.on_action_normalize_size,
|
||||
'group': self.actions_active_when_selection,
|
||||
'enabled': False,
|
||||
},
|
||||
]
|
||||
add_to_menu(items_menu.addMenu('&Normalize'), actions)
|
||||
|
||||
def on_can_redo_changed(self, can_redo):
|
||||
for action in self.actions_active_when_can_redo:
|
||||
action.setEnabled(can_redo)
|
||||
self.actiongroup_set_enabled('active_when_can_redo', can_redo)
|
||||
|
||||
def on_can_undo_changed(self, can_undo):
|
||||
for action in self.actions_active_when_can_undo:
|
||||
action.setEnabled(can_undo)
|
||||
self.actiongroup_set_enabled('active_when_can_undo', can_undo)
|
||||
|
||||
def on_context_menu(self, point):
|
||||
self.context_menu.exec(self.mapToGlobal(point))
|
||||
|
|
@ -361,8 +248,8 @@ class BeeGraphicsView(QtWidgets.QGraphicsView):
|
|||
def on_selection_changed(self):
|
||||
logger.debug('Currently selected items: %s',
|
||||
len(self.scene.selectedItems()))
|
||||
for action in self.actions_active_when_selection:
|
||||
action.setEnabled(self.scene.has_selection())
|
||||
self.actiongroup_set_enabled('active_when_selection',
|
||||
self.scene.has_selection())
|
||||
self.viewport().repaint()
|
||||
|
||||
def recalc_scene_rect(self):
|
||||
|
|
|
|||
0
tests/actions/__init__.py
Normal file
0
tests/actions/__init__.py
Normal file
124
tests/actions/test_mixin.py
Normal file
124
tests/actions/test_mixin.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from beeref.actions import ActionsMixin
|
||||
from beeref.actions.menu_structure import MENU_SEPARATOR
|
||||
from ..base import BeeTestCase
|
||||
|
||||
|
||||
class FooWidget(QtWidgets.QWidget, ActionsMixin):
|
||||
def on_foo(self):
|
||||
pass
|
||||
|
||||
|
||||
class ActionsMixinTestCase(BeeTestCase):
|
||||
|
||||
def setUp(self):
|
||||
menu_patcher = patch('beeref.actions.mixin.menu_structure')
|
||||
self.menu_mock = menu_patcher.start()
|
||||
self.addCleanup(menu_patcher.stop)
|
||||
actions_patcher = patch('beeref.actions.mixin.actions')
|
||||
self.actions_mock = actions_patcher.start()
|
||||
self.addCleanup(actions_patcher.stop)
|
||||
self.widget = FooWidget()
|
||||
|
||||
def test_create_actions(self):
|
||||
self.actions_mock.__iter__.return_value = [{
|
||||
'id': 'foo',
|
||||
'text': '&Foo',
|
||||
'shortcuts': ['Ctrl+F'],
|
||||
'callback': 'on_foo',
|
||||
}]
|
||||
|
||||
with patch('PyQt6.QtGui.QAction.triggered') as trigger_mock:
|
||||
self.widget._create_actions()
|
||||
trigger_mock.connect.assert_called_once_with(self.widget.on_foo)
|
||||
assert len(self.widget.actions()) == 1
|
||||
qaction = self.widget.actions()[0]
|
||||
qaction.text() == '&Foo'
|
||||
qaction.shortcut() == 'Ctrl+F'
|
||||
qaction.isEnabled() is True
|
||||
assert self.widget.bee_actions['foo'] == qaction
|
||||
|
||||
def test_create_actions_enabled_false(self):
|
||||
self.actions_mock.__iter__.return_value = [{
|
||||
'id': 'foo',
|
||||
'text': '&Foo',
|
||||
'callback': 'on_foo',
|
||||
'enabled': False,
|
||||
}]
|
||||
self.widget._create_actions()
|
||||
qaction = self.widget.actions()[0]
|
||||
qaction.isEnabled() is False
|
||||
|
||||
def test_create_actions_with_group(self):
|
||||
self.actions_mock.__iter__.return_value = [{
|
||||
'id': 'foo',
|
||||
'text': '&Foo',
|
||||
'callback': 'on_foo',
|
||||
'group': 'bar',
|
||||
}]
|
||||
self.widget._create_actions()
|
||||
qaction = self.widget.actions()[0]
|
||||
len(self.widget.bee_actiongroups) == 1
|
||||
self.widget.bee_actiongroups['bar'] == [qaction]
|
||||
|
||||
def test_create_menu_and_actions_with_actions(self):
|
||||
self.actions_mock.__iter__.return_value = [{
|
||||
'id': 'foo',
|
||||
'text': '&Foo',
|
||||
'callback': 'on_foo',
|
||||
'group': 'bar',
|
||||
}]
|
||||
self.menu_mock.__iter__.return_value = ['foo']
|
||||
with patch('PyQt6.QtWidgets.QMenu.addAction') as add_mock:
|
||||
menu = self.widget.create_menu_and_actions()
|
||||
assert isinstance(menu, QtWidgets.QMenu)
|
||||
add_mock.assert_called_once_with(self.widget.bee_actions['foo'])
|
||||
|
||||
def test_create_menu_and_actions_with_separator(self):
|
||||
self.menu_mock.__iter__.return_value = [MENU_SEPARATOR]
|
||||
with patch('PyQt6.QtWidgets.QMenu.addSeparator') as sep_mock:
|
||||
menu = self.widget.create_menu_and_actions()
|
||||
assert isinstance(menu, QtWidgets.QMenu)
|
||||
sep_mock.assert_called_once_with()
|
||||
|
||||
def test_create_menu_and_actions_with_submenu(self):
|
||||
self.actions_mock.__iter__.return_value = [{
|
||||
'id': 'foo',
|
||||
'text': '&Foo',
|
||||
'callback': 'on_foo',
|
||||
'group': 'bar',
|
||||
}]
|
||||
self.menu_mock.__iter__.return_value = [
|
||||
{'menu': '&Bar', 'items': ['foo']}]
|
||||
with patch('PyQt6.QtWidgets.QMenu.addAction') as add_mock:
|
||||
with patch('PyQt6.QtWidgets.QMenu.addMenu') as addmenu_mock:
|
||||
addmenu_mock.return_value = QtWidgets.QMenu()
|
||||
menu = self.widget.create_menu_and_actions()
|
||||
assert isinstance(menu, QtWidgets.QMenu)
|
||||
addmenu_mock.assert_called_once_with('&Bar')
|
||||
add_mock.assert_called_once_with(
|
||||
self.widget.bee_actions['foo'])
|
||||
|
||||
def test_actiongroup_set_enabled(self):
|
||||
self.actions_mock.__iter__.return_value = [
|
||||
{
|
||||
'id': 'foo',
|
||||
'text': '&Foo',
|
||||
'callback': 'on_foo',
|
||||
'group': 'g1',
|
||||
},
|
||||
{
|
||||
'id': 'bar',
|
||||
'text': '&Bar',
|
||||
'callback': 'on_foo',
|
||||
'group': 'g2',
|
||||
},
|
||||
]
|
||||
|
||||
self.widget._create_actions()
|
||||
self.widget.actiongroup_set_enabled('g1', False)
|
||||
assert self.widget.bee_actions['foo'].isEnabled() is False
|
||||
assert self.widget.bee_actions['bar'].isEnabled() is True
|
||||
|
|
@ -2,7 +2,7 @@ import os.path
|
|||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from PyQt6 import QtGui
|
||||
from PyQt6 import QtGui, QtWidgets
|
||||
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref import fileio
|
||||
|
|
@ -22,6 +22,13 @@ class BeeGraphicsViewTestCase(BeeTestCase):
|
|||
def tearDown(self):
|
||||
del self.view
|
||||
|
||||
def test_inits_menu(self):
|
||||
view = BeeGraphicsView(self.app)
|
||||
assert isinstance(view.context_menu, QtWidgets.QMenu)
|
||||
assert len(view.actions()) > 0
|
||||
assert view.bee_actions
|
||||
assert view.bee_actiongroups
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.open_from_file')
|
||||
def test_init_without_filename(self, open_file_mock):
|
||||
self.config_mock.filename = None
|
||||
|
|
|
|||
Loading…
Reference in a new issue