#! /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 )