Add ability to enable a menu bar

This commit is contained in:
Rebecca Breu 2021-05-18 18:12:25 +02:00
parent 1de1e2eec9
commit 45cd751de6
7 changed files with 109 additions and 120 deletions

View file

@ -29,19 +29,16 @@ from beeref.view import BeeGraphicsView
logger = logging.getLogger(__name__)
class BeeRefMainWindow(QtWidgets.QWidget):
class BeeRefMainWindow(QtWidgets.QMainWindow):
def __init__(self, app):
super().__init__()
app.setOrganizationName(constants.APPNAME)
app.setApplicationName(constants.APPNAME)
self.setWindowIcon(BeeAssets().logo)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
self.setLayout(layout)
self.resize(500, 300)
self.view = BeeGraphicsView(app, self)
layout.addWidget(self.view)
self.setCentralWidget(self.view)
self.show()
def __del__(self):

View file

@ -40,7 +40,7 @@ actions = [
},
{
'id': 'insert_images',
'text': '&Insert Images...',
'text': '&Images...',
'shortcuts': ['Ctrl+I'],
'callback': 'on_action_insert_images',
},
@ -218,6 +218,13 @@ actions = [
'settings': 'View/show_scrollbars',
'callback': 'on_action_show_scrollbars',
},
{
'id': 'show_menubar',
'text': 'Show &Menu Bar',
'checkable': True,
'settings': 'View/show_menubar',
'callback': 'on_action_show_menubar',
},
{
'id': 'fullscreen',
'text': '&Fullscreen',

View file

@ -56,9 +56,15 @@ menu_structure = [
'fullscreen',
'always_on_top',
'show_scrollbars',
'show_menubar',
],
},
{
'menu': '&Insert',
'items': [
'insert_images',
],
},
'insert_images',
{
'menu': '&Transform',
'items': [

View file

@ -31,31 +31,27 @@ class ActionsMixin:
for action in self.bee_actiongroups[group]:
action.setEnabled(value)
def build_menu_and_actions(self, menu=None):
def build_menu_and_actions(self):
"""Creates a new menu or rebuilds the given menu."""
if not menu:
menu = QtWidgets.QMenu(self)
self.clear_actions(menu)
self._create_actions()
menu = self._create_menu(self.bee_actions, menu, menu_structure)
self.actiongroup_set_enabled(
'active_when_can_redo', self.undo_stack.canRedo())
self.actiongroup_set_enabled(
'active_when_can_undo', self.undo_stack.canUndo())
self.actiongroup_set_enabled(
'active_when_selection', self.scene.has_selection())
return menu
def clear_actions(self, menu):
if hasattr(self, 'bee_actions'):
for action in self.bee_actions.values():
self.removeAction(action)
if menu:
menu.clear()
self.context_menu = QtWidgets.QMenu(self)
self.toplevel_menus = []
self.bee_actions = {}
self.bee_actiongroups = defaultdict(list)
self._post_create_functions = []
self._create_actions()
self._create_menu(self.bee_actions, self.context_menu, menu_structure)
for func, arg in self._post_create_functions:
func(arg)
del self._post_create_functions
def update_menu_and_actions(self):
self._build_recent_files()
def create_menubar(self):
menu_bar = QtWidgets.QMenuBar()
for menu in self.toplevel_menus:
menu_bar.addMenu(menu)
return menu_bar
def _store_checkable_setting(self, key, value):
self.settings.setValue(key, value)
@ -68,7 +64,7 @@ class ActionsMixin:
if settings_key:
val = self.settings.value(settings_key, False, type=bool)
qaction.setChecked(val)
callback(val)
self._post_create_functions.append((callback, val))
qaction.toggled.connect(
partial(self._store_checkable_setting, settings_key))
@ -86,10 +82,12 @@ class ActionsMixin:
self.bee_actions[action['id']] = qaction
if 'group' in action:
self.bee_actiongroups[action['group']].append(qaction)
qaction.setEnabled(False)
def _create_menu(self, actions, menu, items):
if isinstance(items, str):
items = getattr(self, items)()
getattr(self, items)(menu)
return menu
for item in items:
if isinstance(item, str):
menu.addAction(actions[item])
@ -97,11 +95,17 @@ class ActionsMixin:
menu.addSeparator()
if isinstance(item, dict):
submenu = menu.addMenu(item['menu'])
if menu == self.context_menu:
self.toplevel_menus.append(submenu)
self._create_menu(actions, submenu, item['items'])
return menu
def _build_recent_files(self):
def _build_recent_files(self, menu=None):
if menu:
self._recent_files_submenu = menu
self._clear_recent_files()
files = config.BeeSettings().get_recent_files(existing_only=True)
items = []
for i, filename in enumerate(files):
@ -111,6 +115,14 @@ class ActionsMixin:
qaction.setShortcuts([f'Ctrl+{key}'])
qaction.triggered.connect(partial(self.open_from_file, filename))
self.addAction(qaction)
self._recent_files_submenu.addAction(qaction)
self.bee_actions[f'recent_files_{i}'] = qaction
items.append(f'recent_files_{i}')
return items
def _clear_recent_files(self):
for action in self._recent_files_submenu.actions():
self.removeAction(action)
self._recent_files_submenu.clear()
for key in list(self.bee_actions.keys()):
if key.startswith('recent_files_'):
self.bee_actions.pop(key)

View file

@ -63,10 +63,10 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
self.setScene(self.scene)
# Context menu and actions
self.build_menu_and_actions()
self.setContextMenuPolicy(
Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.on_context_menu)
self.context_menu = self.build_menu_and_actions()
self.welcome_overlay = gui.WelcomeOverlay(self)
@ -85,7 +85,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
self.update_window_title()
if value:
self.settings.update_recent_files(value)
self.build_menu_and_actions(self.context_menu)
self.update_menu_and_actions()
def update_window_title(self):
clean = self.undo_stack.isClean()
@ -176,7 +176,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
def on_action_always_on_top(self, checked):
self.parent().setWindowFlag(
Qt.WindowFlags.WindowStaysOnTopHint, on=checked)
Qt.WindowType.WindowStaysOnTopHint, on=checked)
self.parent().destroy()
self.parent().create()
self.parent().show()
@ -193,6 +193,12 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
self.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
def on_action_show_menubar(self, checked):
if checked:
self.parent().setMenuBar(self.create_menubar())
else:
self.parent().setMenuBar(None)
def on_action_undo(self):
logger.debug('Undo: %s' % self.undo_stack.undoText())
self.undo_stack.undo()

View file

@ -101,18 +101,6 @@ class ActionsMixinTestCase(BeeTestCase):
assert toggle_mock.connect.call_count == 2
callback_mock.assert_called_once_with(True)
def test_create_actions_enabled_false(self):
self.actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'enabled': False,
}]
self.menu_mock.__iter__.return_value = ['foo']
self.widget.build_menu_and_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',
@ -135,15 +123,15 @@ class ActionsMixinTestCase(BeeTestCase):
}]
self.menu_mock.__iter__.return_value = ['foo']
with patch('PyQt6.QtWidgets.QMenu.addAction') as add_mock:
menu = self.widget.build_menu_and_actions()
assert isinstance(menu, QtWidgets.QMenu)
self.widget.build_menu_and_actions()
assert isinstance(self.widget.context_menu, QtWidgets.QMenu)
add_mock.assert_called_once_with(self.widget.bee_actions['foo'])
def test_build_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.build_menu_and_actions()
assert isinstance(menu, QtWidgets.QMenu)
self.widget.build_menu_and_actions()
assert isinstance(self.widget.context_menu, QtWidgets.QMenu)
sep_mock.assert_called_once_with()
def test_build_menu_and_actions_with_submenu(self):
@ -158,8 +146,8 @@ class ActionsMixinTestCase(BeeTestCase):
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.build_menu_and_actions()
assert isinstance(menu, QtWidgets.QMenu)
self.widget.build_menu_and_actions()
assert isinstance(self.widget.context_menu, QtWidgets.QMenu)
addmenu_mock.assert_called_once_with('&Bar')
add_mock.assert_called_once_with(
self.widget.bee_actions['foo'])
@ -182,25 +170,9 @@ class ActionsMixinTestCase(BeeTestCase):
self.menu_mock.__iter__.return_value = ['foo']
self.widget.build_menu_and_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
def test_build_menu_and_actions_enables_actiongroups(self):
self.widget.scene.has_selection.return_value = True
self.actions_mock.__iter__.return_value = [
{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'active_when_selection',
},
]
self.menu_mock.__iter__.return_value = ['foo']
self.widget.build_menu_and_actions()
qaction = self.widget.actions()[0]
assert qaction.isEnabled() is True
self.widget.actiongroup_set_enabled('g1', True)
assert self.widget.bee_actions['foo'].isEnabled() is True
assert self.widget.bee_actions['bar'].isEnabled() is False
def test_build_menu_and_actions_disables_actiongroups(self):
self.widget.scene.has_selection.return_value = False
@ -220,10 +192,9 @@ class ActionsMixinTestCase(BeeTestCase):
@patch('beeref.config.BeeSettings.get_recent_files')
@patch('PyQt6.QtGui.QAction.triggered')
def test_recent_files(self, triggered_mock, files_mock):
def test_create_recent_files(self, triggered_mock, files_mock):
files_mock.return_value = [
os.path.abspath(f'{i}.bee') for i in range(15)]
self.menu_mock.__iter__.return_value = [{
'menu': 'Open &Recent',
'items': '_build_recent_files',
@ -231,7 +202,6 @@ class ActionsMixinTestCase(BeeTestCase):
self.widget.build_menu_and_actions()
triggered_mock.connect.assert_called()
assert len(self.widget.actions()) == 15
qaction1 = self.widget.actions()[0]
assert qaction1.text() == '0.bee'
@ -249,46 +219,30 @@ class ActionsMixinTestCase(BeeTestCase):
assert qaction15.isEnabled() is True
assert self.widget.bee_actions['recent_files_14'] == qaction15
def test_build_menu_and_actions_updates_given_menu(self):
self.actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'foo',
@patch('beeref.config.BeeSettings.get_recent_files')
@patch('PyQt6.QtGui.QAction.triggered')
def test_update_recent_files(self, triggered_mock, files_mock):
files_mock.return_value = [os.path.abspath('foo.bee')]
self.menu_mock.__iter__.return_value = [{
'menu': 'Open &Recent',
'items': '_build_recent_files',
}]
self.menu_mock.__iter__.return_value = ['foo']
menu = self.widget.build_menu_and_actions()
qaction = self.widget.actions()[0]
assert qaction.text() == '&Foo'
self.actions_mock.__iter__.return_value = [{
'id': 'bar',
'text': '&Bar',
'callback': 'on_bar',
'group': 'bar',
}]
self.menu_mock.__iter__.return_value = ['bar']
self.widget.build_menu_and_actions(menu)
self.widget.build_menu_and_actions()
triggered_mock.connect.reset_mock()
assert len(self.widget.actions()) == 1
assert len(menu.actions()) == 1
qaction = self.widget.actions()[0]
assert qaction.text() == '&Bar'
assert self.widget.bee_actions == {'bar': qaction}
assert self.widget.bee_actiongroups['bar'] == [qaction]
qaction1 = self.widget.actions()[0]
assert qaction1.text() == 'foo.bee'
def test_clear_actions(self):
self.actions_mock.__iter__.return_value = [{
'id': 'foo',
'text': '&Foo',
'callback': 'on_foo',
'group': 'bar',
}]
self.menu_mock.__iter__.return_value = ['foo']
menu = self.widget.build_menu_and_actions()
assert menu.actions()
assert self.widget.actions()
self.widget.clear_actions(menu)
assert menu.actions() == []
assert self.widget.actions() == []
assert self.widget.bee_actions == {}
assert self.widget.bee_actiongroups == {}
files_mock.return_value = [os.path.abspath('bar.bee')]
self.widget.update_menu_and_actions()
triggered_mock.connect.assert_called()
assert len(self.widget.actions()) == 1
qaction1 = self.widget.actions()[0]
assert qaction1.text() == 'bar.bee'
def test_create_menubar(self):
self.widget.toplevel_menus = [QtWidgets.QMenu('Foo')]
menubar = self.widget.create_menubar()
assert isinstance(menubar, QtWidgets.QMenuBar)
assert len(menubar.actions()) == 1

View file

@ -21,7 +21,7 @@ class ViewBaseTestCase(BeeTestCase):
self.config_mock = config_patcher.start()
self.config_mock.filename = None
self.addCleanup(config_patcher.stop)
self.parent = QtWidgets.QWidget()
self.parent = QtWidgets.QMainWindow()
self.view = BeeGraphicsView(self.app, self.parent)
@ -32,11 +32,11 @@ class BeeGraphicsViewTestCase(ViewBaseTestCase):
self.config_mock = config_patcher.start()
self.config_mock.filename = None
self.addCleanup(config_patcher.stop)
self.parent = QtWidgets.QWidget()
self.parent = QtWidgets.QMainWindow()
self.view = BeeGraphicsView(self.app, self.parent)
def test_inits_menu(self):
parent = QtWidgets.QWidget()
parent = QtWidgets.QMainWindow()
view = BeeGraphicsView(self.app, parent)
assert isinstance(view.context_menu, QtWidgets.QMenu)
assert len(view.actions()) > 0
@ -46,7 +46,7 @@ class BeeGraphicsViewTestCase(ViewBaseTestCase):
@patch('beeref.view.BeeGraphicsView.open_from_file')
def test_init_without_filename(self, open_file_mock):
self.config_mock.filename = None
parent = QtWidgets.QWidget()
parent = QtWidgets.QMainWindow()
view = BeeGraphicsView(self.app, parent)
open_file_mock.assert_not_called()
assert parent.windowTitle() == 'BeeRef'
@ -55,7 +55,7 @@ class BeeGraphicsViewTestCase(ViewBaseTestCase):
@patch('beeref.view.BeeGraphicsView.open_from_file')
def test_init_with_filename(self, open_file_mock):
self.config_mock.filename = 'test.bee'
parent = QtWidgets.QWidget()
parent = QtWidgets.QMainWindow()
view = BeeGraphicsView(self.app, parent)
open_file_mock.assert_called_once_with('test.bee')
del view
@ -373,6 +373,13 @@ class BeeGraphicsViewTestCase(ViewBaseTestCase):
assert self.view.scene.items() == []
assert self.view.undo_stack.isClean() is False
def test_on_action_show_menubar(self):
self.view.toplevel_menus = [QtWidgets.QMenu('Foo')]
self.view.on_action_show_menubar(True)
assert len(self.view.parent().menuBar().actions()) == 1
self.view.on_action_show_menubar(False)
assert self.view.parent().menuBar().actions() == []
def test_on_action_delete_items(self):
item = BeePixmapItem(QtGui.QImage())
self.view.scene.addItem(item)