You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
400 lines
18 KiB
400 lines
18 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
This application 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.
|
|
|
|
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import logging; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
|
|
#Standard Library
|
|
|
|
#Third Party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
|
|
|
|
#Template Modules
|
|
from .about import About
|
|
from .changelog import Changelog
|
|
from .resources import *
|
|
|
|
#User Modules
|
|
import engine.api as api
|
|
from engine.config import * #imports METADATA
|
|
|
|
class Menu(object):
|
|
|
|
def __init__(self, mainWindow):
|
|
"""We can receive a dict of extra menu actions, which also override our hardcoded ones"""
|
|
|
|
self.mainWindow = mainWindow
|
|
self.ui = mainWindow.ui #just shorter to write.
|
|
|
|
#Hover Shortcuts
|
|
self.hoverShortcutDict = HoverActionDictionary(self.mainWindow) #for temporary hover shortcuts. key=actual key, value=QAction
|
|
self.lastHoverAction = None #the last time the mouse hovered over a menu it was this QAction. This gets never empty again, but works because we only check it during menu KeyPressEvent
|
|
def _rememberHover(action): self.lastHoverAction = action
|
|
self.ui.menubar.hovered.connect(_rememberHover)
|
|
self._originalMenuKeyPressEvent = self.ui.menubar.keyPressEvent
|
|
self.ui.menubar.keyPressEvent = self.menuKeyPressInterceptor
|
|
|
|
#Order Matters. First is inserted first
|
|
self._setupFileMenu()
|
|
self._setupEditMenu()
|
|
self._setupHelpMenu()
|
|
self._setupDebugMenu() #Setup the provided debug menu, keep it separate from actual code
|
|
|
|
#Set the label of undo and redo to show the info the api provides
|
|
api.callbacks.historyChanged.append(self.renameUndoRedoByHistory)
|
|
|
|
def renameUndoRedoByHistory(self, undoList, redoList):
|
|
undoString = QtCore.QCoreApplication.translate("TemplateMainWindow", "Undo")
|
|
redoString = QtCore.QCoreApplication.translate("TemplateMainWindow", "Redo")
|
|
|
|
if undoList:
|
|
undoInfo = QtCore.QCoreApplication.translate("NOOPengineHistory", undoList[-1])
|
|
self.ui.actionUndo.setText(undoString +": {}".format(undoInfo))
|
|
self.ui.actionUndo.setEnabled(True)
|
|
else:
|
|
self.ui.actionUndo.setText(undoString)
|
|
self.ui.actionUndo.setEnabled(False)
|
|
|
|
if redoList:
|
|
redoInfo = QtCore.QCoreApplication.translate("NOOPengineHistory", redoList[-1])
|
|
self.ui.actionRedo.setText(redoString + ": {}".format(redoInfo))
|
|
self.ui.actionRedo.setEnabled(True)
|
|
else:
|
|
self.ui.actionRedo.setText(redoString)
|
|
self.ui.actionRedo.setEnabled(False)
|
|
|
|
def _setupHelpMenu(self):
|
|
self.addSubmenu("menuHelp", QtCore.QCoreApplication.translate("TemplateMainWindow", "Help"))
|
|
self.addMenuEntry("menuHelp", "actionAbout", QtCore.QCoreApplication.translate("TemplateMainWindow", "About and Tips"), connectedFunction=self.mainWindow.about.show)
|
|
self.addMenuEntry("menuHelp", "actionUser_Manual", QtCore.QCoreApplication.translate("TemplateMainWindow", "User Manual"), connectedFunction=self.mainWindow.userManual.show)
|
|
self.addMenuEntry("menuHelp", "actionChangelog", QtCore.QCoreApplication.translate("TemplateMainWindow", "News and Changelog"), connectedFunction=self.mainWindow.changelog.show)
|
|
|
|
def _setupEditMenu(self):
|
|
self.addSubmenu("menuEdit", QtCore.QCoreApplication.translate("TemplateMainWindow", "Edit"))
|
|
self.addMenuEntry("menuEdit", "actionUndo", QtCore.QCoreApplication.translate("TemplateMainWindow", "Undo"), connectedFunction=api.undo, shortcut="Ctrl+Z")
|
|
self.addMenuEntry("menuEdit", "actionRedo", QtCore.QCoreApplication.translate("TemplateMainWindow", "Redo"), connectedFunction=api.redo, shortcut="Ctrl+Shift+Z")
|
|
|
|
def _setupFileMenu(self):
|
|
self.addSubmenu("menuFile", QtCore.QCoreApplication.translate("TemplateMainWindow", "File"))
|
|
self.addMenuEntry("menuFile", "actionSave", QtCore.QCoreApplication.translate("TemplateMainWindow", "Save"), connectedFunction=api.save, shortcut="Ctrl+S")
|
|
self.addMenuEntry("menuFile", "actionQuit", QtCore.QCoreApplication.translate("TemplateMainWindow", "Quit (without saving)"), connectedFunction=self.mainWindow.nsmClient.serverSendExitToSelf, shortcut="Ctrl+Q")
|
|
|
|
def _setupDebugMenu(self):
|
|
"""Debug entries are not translated"""
|
|
def guiCallback_menuDebugVisibility(state):
|
|
self.ui.menuDebug.menuAction().setVisible(state)
|
|
api.session.guiSharedDataToSave["actionToggle_Debug_Menu_Visibility"] = state
|
|
|
|
self.addSubmenu("menuDebug", "Debug")
|
|
self.addMenuEntry("menuDebug", "actionToggle_Debug_Menu_Visibility", "Toggle Debug Menu Visibility", connectedFunction=guiCallback_menuDebugVisibility, shortcut="Ctrl+Alt+Shift+D")
|
|
self.ui.actionToggle_Debug_Menu_Visibility.setCheckable(True)
|
|
self.ui.actionToggle_Debug_Menu_Visibility.setEnabled(True)
|
|
|
|
self.addMenuEntry("menuDebug", "actionRun_Script_File", "Run Script File", connectedFunction=self.mainWindow.debugScriptRunner.run) #runs the file debugscript.py in our session dir.
|
|
self.addMenuEntry("menuDebug", "actionEmpty_Menu_Entry", "Empty Menu Entry")
|
|
deleteSessionText = "Delete our session data and exit: {}".format(self.mainWindow.nsmClient.ourPath) #Include the to-be-deleted path into the menu text. Better verbose than sorry.
|
|
self.addMenuEntry("menuDebug", "actionReset_Own_Session_Data", deleteSessionText, connectedFunction=self.mainWindow.nsmClient.debugResetDataAndExit)
|
|
|
|
#Now that the actions are connected we can create the initial setup
|
|
if not "actionToggle_Debug_Menu_Visibility" in api.session.guiSharedDataToSave:
|
|
api.session.guiSharedDataToSave["actionToggle_Debug_Menu_Visibility"] = False
|
|
state = api.session.guiSharedDataToSave["actionToggle_Debug_Menu_Visibility"]
|
|
self.ui.actionToggle_Debug_Menu_Visibility.setChecked(state) #does not trigger the connected action...
|
|
self.ui.menuDebug.menuAction().setVisible(state) #...therefore we hide manually.
|
|
|
|
def menuKeyPressInterceptor(self, event):
|
|
"""
|
|
Triggered when a menu is open and mouse hovers over a menu.
|
|
|
|
Menu Entries without their own shortcut can be temporarily assigned one of the ten
|
|
number keypad keys as shortcuts.
|
|
|
|
If the numpad key is already in use it will be rerouted (freed first).
|
|
"""
|
|
strings = set(("Num+1", "Num+2", "Num+3", "Num+4", "Num+5", "Num+6", "Num+7", "Num+8", "Num+9", "Num+0"))
|
|
if self.lastHoverAction and (not self.lastHoverAction.menu()) and event.modifiers() == QtCore.Qt.KeypadModifier and ((not self.lastHoverAction.shortcut().toString()) or self.lastHoverAction.shortcut().toString() in strings):
|
|
key = event.text() #just number 0-9 #event.key() is 49, 50...
|
|
self.hoverShortcutDict[key] = self.lastHoverAction
|
|
event.accept()
|
|
else:
|
|
self._originalMenuKeyPressEvent(event)
|
|
|
|
|
|
def addSubmenu(self, submenuActionAsString, text):
|
|
"""
|
|
Returns the submenu.
|
|
|
|
|
|
submenuActionAsString is something like menuHelp
|
|
|
|
It is not possible to create nested submenus yet.
|
|
text must already be translated.
|
|
|
|
If the menu already exists (decided by submenuActionAsString, not by text) it will be
|
|
returned instead."""
|
|
|
|
try:
|
|
return getattr(self.ui, submenuActionAsString)
|
|
except AttributeError:
|
|
pass
|
|
|
|
setattr(self.ui, submenuActionAsString, QtWidgets.QMenu(self.ui.menubar))
|
|
menu = getattr(self.ui, submenuActionAsString)
|
|
menu.setObjectName(submenuActionAsString)
|
|
menu.menuAction().setObjectName(submenuActionAsString)
|
|
menu.setTitle(text)
|
|
self.ui.menubar.addAction(menu.menuAction())
|
|
return menu
|
|
|
|
def getSubmenuOrder(self):
|
|
"""Returns a list of strings with submenu names, compatible with orderSubmenus"""
|
|
#text() shows the translated string. We wan't the attribute name like actionEdit
|
|
#action.objectName() gives us the name, but only if set. QtDesigner does not set.
|
|
#action.menu().objectName() works.
|
|
listOfStrings = []
|
|
for a in self.ui.menubar.actions():
|
|
if a.menu():
|
|
listOfStrings.append(a.menu().objectName())
|
|
else: #separator
|
|
listOfStrings.append(None)
|
|
return listOfStrings
|
|
|
|
def getTemplateSubmenus(self):
|
|
"""Returns a list of strings with submenu names, compatible with orderSubmenus.
|
|
Only contains the ones set in the template.
|
|
Not in order."""
|
|
#Hardcoded.
|
|
return ["menuFile", "menuEdit", "menuHelp", "menuDebug"]
|
|
|
|
def getClientSubmenus(self):
|
|
"""opposite of getTemplateSubmenus.
|
|
In order"""
|
|
allMenus = self.getSubmenuOrder()
|
|
templateMenus = self.getTemplateSubmenus()
|
|
return [st for st in allMenus if st not in templateMenus]
|
|
|
|
def orderSubmenus(self, listOfStrings):
|
|
"""sort after listOfStrings.
|
|
|
|
List of strings can contain None to insert a separator at this position.
|
|
|
|
It is expected that you give the complete list of menus."""
|
|
self.ui.menubar.clear()
|
|
for submenuActionAsString in listOfStrings:
|
|
if submenuActionAsString:
|
|
menu = getattr(self.ui, submenuActionAsString)
|
|
self.ui.menubar.addAction(menu.menuAction())
|
|
else:
|
|
self.ui.menubar.addSeparator()
|
|
|
|
def hideSubmenu(self, submenu:str):
|
|
menuAction = getattr(self.ui, submenu).menuAction()
|
|
menuAction.setVisible(False)
|
|
|
|
def removeSubmenu(self, submenuAction:str):
|
|
raise NotImplementedError #TODO
|
|
|
|
def addMenuEntry(self, submenu, actionAsString:str, text:str, connectedFunction=None, shortcut:str="", tooltip:str="", iconResource:str="", checkable=False, startChecked=False):
|
|
"""
|
|
parameterstrings must already be translated.
|
|
|
|
Returns the QAction
|
|
|
|
Add a new menu entry to an existing submenu.
|
|
If you want a new submenu use the create submenu function first.
|
|
|
|
submenu is the actual menu action name, like "menuHelp". You need to know the variable names.
|
|
|
|
actionAsString is the unique action name including the word "action", text is the display name. Text must be already translated,
|
|
actionAsString must never be translated.
|
|
|
|
shortcut is an optional string like "Ctrl+Shift+Z"
|
|
|
|
If the action already exists (decided by submenuActionAsString, not by text) it will be
|
|
returned instead.
|
|
"""
|
|
|
|
try:
|
|
return getattr(self.ui, actionAsString)
|
|
except AttributeError:
|
|
pass
|
|
|
|
setattr(self.ui, actionAsString, QtWidgets.QAction(self.mainWindow))
|
|
action = getattr(self.ui, actionAsString)
|
|
|
|
action.setObjectName(actionAsString)
|
|
action.setText(text) #Text must already be translated
|
|
action.setToolTip(tooltip) #Text must already be translated
|
|
if iconResource:
|
|
assert QtCore.QFile.exists(iconResource)
|
|
ico = QtGui.QIcon(iconResource)
|
|
action.setIcon(ico)
|
|
if checkable:
|
|
action.setCheckable(True)
|
|
action.setChecked(startChecked)
|
|
|
|
self.connectMenuEntry(actionAsString, connectedFunction, shortcut)
|
|
|
|
submenu = getattr(self.ui, submenu)
|
|
submenu.addAction(action)
|
|
|
|
return action
|
|
|
|
def hideMenuEntry(self, menuAction:str):
|
|
action = getattr(self.ui, menuAction)
|
|
action.setVisible(False)
|
|
|
|
def removeMenuEntry(self, menuAction:str):
|
|
raise NotImplementedError #TODO
|
|
|
|
def addSeparator(self, submenu):
|
|
submenu = getattr(self.ui, submenu)
|
|
submenu.addSeparator()
|
|
|
|
def connectMenuEntry(self, actionAsString, connectedFunction, shortcut=""):
|
|
"""
|
|
Returns the submenu.
|
|
Disconnects the old action first
|
|
|
|
shortcut is an optional string like "Ctrl+Shift+Z"
|
|
|
|
You can give None for connectedFunction. That makes automatisation in the program easier.
|
|
However it is possible to set a shortcut without a connectedFunction.
|
|
Also this helps to disconnect a menu entry of all its function.
|
|
"""
|
|
|
|
action = getattr(self.ui, actionAsString)
|
|
try:
|
|
action.triggered.disconnect()
|
|
except TypeError:
|
|
#Action has no connected function on triggered-signal.
|
|
pass
|
|
|
|
if connectedFunction:
|
|
action.triggered.connect(connectedFunction)
|
|
|
|
if shortcut:
|
|
action.setShortcut(shortcut)
|
|
|
|
return action
|
|
|
|
def disable(self):
|
|
"""This is meant for a short temporary disabling."""
|
|
|
|
for a in self.mainWindow.findChildren(QtWidgets.QAction):
|
|
if not a.menu():
|
|
a.setEnabled(False)
|
|
|
|
def enable(self):
|
|
for a in self.mainWindow.findChildren(QtWidgets.QAction):
|
|
a.setEnabled(True)
|
|
|
|
self.renameUndoRedoByHistory(*api.getUndoLists())
|
|
|
|
|
|
|
|
class HoverActionDictionary(dict):
|
|
"""Key = number from 0-9 as string
|
|
Value = QAction
|
|
|
|
Singleton.
|
|
"""
|
|
|
|
def __init__(self, mainWindow, *args):
|
|
self.mainWindow = mainWindow
|
|
|
|
if not "hoverActionDictionary" in api.session.guiSharedDataToSave:
|
|
# key = action.text() , value = key sequence as text 'Num3+' key. Yes, the plus is in the string
|
|
# Basically both values and keys should be unique.
|
|
api.session.guiSharedDataToSave["hoverActionDictionary"] = {}
|
|
|
|
dict.__init__(self, args)
|
|
|
|
self.qShortcuts = {
|
|
"1" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_1,
|
|
"2" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_2,
|
|
"3" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_3,
|
|
"4" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_4,
|
|
"5" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_5,
|
|
"6" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_6,
|
|
"7" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_7,
|
|
"8" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_8,
|
|
"9" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_9,
|
|
"0" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_0,
|
|
}
|
|
|
|
dict.__setitem__(self, "0", None)
|
|
dict.__setitem__(self, "1", None)
|
|
dict.__setitem__(self, "2", None)
|
|
dict.__setitem__(self, "3", None)
|
|
dict.__setitem__(self, "4", None)
|
|
dict.__setitem__(self, "5", None)
|
|
dict.__setitem__(self, "6", None)
|
|
dict.__setitem__(self, "7", None)
|
|
dict.__setitem__(self, "8", None)
|
|
dict.__setitem__(self, "9", None)
|
|
"""
|
|
for i in range(10):
|
|
emptyAction = QtWidgets.QAction("empty{}".format(i))
|
|
self.mainWindow.addAction(emptyAction) #no actions without a widget
|
|
emptyAction.triggered.connect(api.nothing)
|
|
emptyAction.setShortcut(QtGui.QKeySequence(self.qShortcuts[str(i)]))
|
|
dict.__setitem__(self, str(i), emptyAction)
|
|
"""
|
|
|
|
#Load the hover shortcuts from last session.
|
|
#We assume the shortcuts are clean and there is no mismatch or ambiguity.
|
|
if "hoverActionDictionary" in api.session.guiSharedDataToSave: # key = action.text() , value = key sequence as text 'Num3+' key. Yes, the plus is in the string
|
|
for action in self.mainWindow.findChildren(QtWidgets.QAction): #search the complete menu
|
|
#TODO: if the menu label changes, maybe through translation, this will break. Non-critical, but annoying. Find a way to find unique ids.
|
|
if action.text() in api.session.guiSharedDataToSave["hoverActionDictionary"]: #A saved action was found
|
|
keySequenceAsString = api.session.guiSharedDataToSave["hoverActionDictionary"][action.text()]
|
|
justTheNumberAsString = keySequenceAsString[-1 ] #-1 is a number as string
|
|
action.setShortcut(QtGui.QKeySequence(self.qShortcuts[justTheNumberAsString]))
|
|
dict.__setitem__(self, justTheNumberAsString, action) #does not trigger our own set item!
|
|
|
|
def __setitem__(self, key, menuAction):
|
|
"""key is a string of a single number 0-9"""
|
|
assert key in "0 1 2 3 4 5 6 7 8 9".split()
|
|
|
|
try:
|
|
self[key].setShortcut(QtGui.QKeySequence()) #reset the action that previously had that shortcut
|
|
except AttributeError:
|
|
pass
|
|
|
|
#Remove old entry from save dict if shortcut already in there. shortcut is the value, so we have to do it by iteration
|
|
for k,v in api.session.guiSharedDataToSave["hoverActionDictionary"].items():
|
|
if v == "Num+"+key:
|
|
del api.session.guiSharedDataToSave["hoverActionDictionary"][k]
|
|
break
|
|
assert not key in api.session.guiSharedDataToSave["hoverActionDictionary"].values()
|
|
|
|
try:
|
|
dict.__setitem__(self, menuAction.shortcut().toString()[-1], None) #-1 is a number as string
|
|
except IndexError:
|
|
pass
|
|
|
|
menuAction.setShortcut(QtGui.QKeySequence()) #reset the new action to remove existing shortcuts
|
|
menuAction.setShortcut(QtGui.QKeySequence(self.qShortcuts[key]))
|
|
api.session.guiSharedDataToSave["hoverActionDictionary"][menuAction.text()] = "Num+"+key
|
|
dict.__setitem__(self, key, menuAction)
|
|
|
|
|