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.
 
 

399 lines
18 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, 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 .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", "actionUser_Manual", QtCore.QCoreApplication.translate("TemplateMainWindow", "User Manual"), connectedFunction=self.mainWindow.userManual.show)
self.addSeparator("menuHelp")
self.addMenuEntry("menuHelp", "actionAbout", QtCore.QCoreApplication.translate("TemplateMainWindow", "About and Tips"), connectedFunction=self.mainWindow.about.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):
menuAction = getattr(self.ui, submenu).menuAction()
raise NotImplementedError #TODO
def addMenuEntry(self, submenu, actionAsString:str, text:str, connectedFunction=None, shortcut:str="", tooltip:str="", iconResource:str="", checkable=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)
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)