Some checks failed
Build and Test / build (pull_request) Has been cancelled
The previous fix (e10841a6c8) passed {"__file__": ...} as the globals
argument to exec(), which replaced the caller's globals dict entirely.
This stripped FreeCAD, FreeCADGui, Workbench, and other names that
modules expect to be available, causing NameError failures across
Material, Tux, Mesh, ReverseEngineering, OpenSCAD, Inspection, Robot,
AddonManager, MeshPart, and others.
Fix by merging __file__ into the caller's globals with
{**globals(), "__file__": str(init_py)} so both __file__ and all
existing names remain available in the executed code.
494 lines
17 KiB
Python
494 lines
17 KiB
Python
# ***************************************************************************
|
|
# * Copyright (c) 2002,2003 Jürgen Riegel <juergen.riegel@web.de> *
|
|
# * Copyright (c) 2025 Frank Martínez <mnesarco at gmail dot com> *
|
|
# * *
|
|
# * This file is part of the FreeCAD CAx development system. *
|
|
# * *
|
|
# * This program is free software; you can redistribute it and/or modify *
|
|
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
# * as published by the Free Software Foundation; either version 2 of *
|
|
# * the License, or (at your option) any later version. *
|
|
# * for detail see the LICENCE text file. *
|
|
# * *
|
|
# * FreeCAD 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 Lesser General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Library General Public *
|
|
# * License along with FreeCAD; if not, write to the Free Software *
|
|
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
# * USA *
|
|
# * *
|
|
# ***************************************************************************/
|
|
|
|
# FreeCAD init module - Gui
|
|
#
|
|
# Gathering all the information to start FreeCAD Gui.
|
|
# This is the forth of four init scripts:
|
|
# +------+------------------+-----------------------------+
|
|
# | This | Script | Runs |
|
|
# +------+------------------+-----------------------------+
|
|
# | | CMakeVariables | always |
|
|
# | | FreeCADInit | always |
|
|
# | | FreeCADTest | only if test and not Gui |
|
|
# | >>>> | FreeCADGuiInit | only if Gui is up |
|
|
# +------+------------------+-----------------------------+
|
|
|
|
import importlib
|
|
import re
|
|
import traceback
|
|
import typing
|
|
from dataclasses import dataclass
|
|
from enum import Enum, IntEnum
|
|
from pathlib import Path
|
|
|
|
import FreeCAD
|
|
import FreeCADGui
|
|
|
|
# shortcuts
|
|
Gui = FreeCADGui
|
|
App = FreeCAD
|
|
|
|
App.Console.PrintLog("Init: Running FreeCADGuiInit.py start script...\n")
|
|
App.Console.PrintLog("░░░▀█▀░█▀█░▀█▀░▀█▀░░░█▀▀░█░█░▀█▀░░\n")
|
|
App.Console.PrintLog("░░░░█░░█░█░░█░░░█░░░░█░█░█░█░░█░░░\n")
|
|
App.Console.PrintLog("░░░▀▀▀░▀░▀░▀▀▀░░▀░░░░▀▀▀░▀▀▀░▀▀▀░░\n")
|
|
|
|
|
|
# Declare symbols already defined in global by previous scripts to make linter happy.
|
|
if typing.TYPE_CHECKING:
|
|
Log: typing.Callable = None
|
|
Err: typing.Callable = None
|
|
ModState: typing.Any = None
|
|
|
|
|
|
# The values must match with that of the C++ enum class ResolveMode
|
|
class ResolveMode(IntEnum):
|
|
NoResolve = 0
|
|
OldStyleElement = 1
|
|
NewStyleElement = 2
|
|
FollowLink = 3
|
|
|
|
|
|
Gui.Selection.ResolveMode = ResolveMode
|
|
|
|
|
|
# The values must match with that of the C++ enum class SelectionStyle
|
|
class SelectionStyle(IntEnum):
|
|
NormalSelection = 0
|
|
GreedySelection = 1
|
|
|
|
|
|
# The values must match with that of the Python enum class in ViewProvider.pyi
|
|
class ToggleVisibilityMode(Enum):
|
|
CanToggleVisibility = "CanToggleVisibility"
|
|
NoToggleVisibility = "NoToggleVisibility"
|
|
|
|
|
|
def _isCommandActive(name: str) -> bool:
|
|
cmd = Gui.Command.get(name)
|
|
return bool(cmd and cmd.isActive())
|
|
|
|
|
|
# this is to keep old code working
|
|
Gui.listCommands = Gui.Command.listAll
|
|
Gui.isCommandActive = _isCommandActive
|
|
Gui.Selection.SelectionStyle = SelectionStyle
|
|
|
|
|
|
# Important definitions
|
|
class Workbench:
|
|
"""The workbench base class."""
|
|
|
|
MenuText = ""
|
|
ToolTip = ""
|
|
Icon = None
|
|
|
|
__Workbench__: (
|
|
"Workbench" # Injected by FreeCAD, see: Application::activateWorkbench
|
|
)
|
|
|
|
def Initialize(self):
|
|
"""Initializes this workbench."""
|
|
App.Console.PrintWarning(
|
|
f"{self!s}: Workbench.Initialize() not implemented in subclass!"
|
|
)
|
|
|
|
def ContextMenu(self, recipient):
|
|
pass
|
|
|
|
def appendToolbar(self, name, cmds, visibility=None):
|
|
if visibility is not None:
|
|
self.__Workbench__.appendToolbar(name, cmds, visibility)
|
|
else:
|
|
self.__Workbench__.appendToolbar(name, cmds)
|
|
|
|
def removeToolbar(self, name):
|
|
self.__Workbench__.removeToolbar(name)
|
|
|
|
def listToolbars(self):
|
|
return self.__Workbench__.listToolbars()
|
|
|
|
def getToolbarItems(self):
|
|
return self.__Workbench__.getToolbarItems()
|
|
|
|
def appendCommandbar(self, name, cmds):
|
|
self.__Workbench__.appendCommandbar(name, cmds)
|
|
|
|
def removeCommandbar(self, name):
|
|
self.__Workbench__.removeCommandbar(name)
|
|
|
|
def listCommandbars(self):
|
|
return self.__Workbench__.listCommandbars()
|
|
|
|
def appendMenu(self, name, cmds):
|
|
self.__Workbench__.appendMenu(name, cmds)
|
|
|
|
def removeMenu(self, name):
|
|
self.__Workbench__.removeMenu(name)
|
|
|
|
def listMenus(self):
|
|
return self.__Workbench__.listMenus()
|
|
|
|
def appendContextMenu(self, name, cmds):
|
|
self.__Workbench__.appendContextMenu(name, cmds)
|
|
|
|
def removeContextMenu(self, name):
|
|
self.__Workbench__.removeContextMenu(name)
|
|
|
|
def reloadActive(self):
|
|
self.__Workbench__.reloadActive()
|
|
|
|
def name(self):
|
|
return self.__Workbench__.name()
|
|
|
|
def GetClassName(self):
|
|
"""Return the name of the associated C++ class."""
|
|
# as default use this to simplify writing workbenches in Python
|
|
return "Gui::PythonWorkbench"
|
|
|
|
|
|
class StandardWorkbench(Workbench):
|
|
"""
|
|
A workbench defines the tool bars, command bars, menus,
|
|
context menu and dockable windows of the main window.
|
|
"""
|
|
|
|
def Initialize(self):
|
|
"""Initialize this workbench."""
|
|
# load the module
|
|
Log("Init: Loading FreeCAD GUI\n")
|
|
|
|
def GetClassName(self):
|
|
"""Return the name of the associated C++ class."""
|
|
return "Gui::StdWorkbench"
|
|
|
|
|
|
class NoneWorkbench(Workbench):
|
|
"""An empty workbench."""
|
|
|
|
MenuText = "<none>"
|
|
ToolTip = "The default empty workbench"
|
|
|
|
def Initialize(self):
|
|
"""Initialize this workbench."""
|
|
# load the module
|
|
Log("Init: Loading FreeCAD GUI\n")
|
|
|
|
def GetClassName(self):
|
|
"""Return the name of the associated C++ class."""
|
|
return "Gui::NoneWorkbench"
|
|
|
|
|
|
@dataclass
|
|
class InputHint:
|
|
"""
|
|
Represents a single input hint (shortcut suggestion).
|
|
|
|
The message is a Qt formatting string with placeholders like %1, %2, ...
|
|
The placeholders are replaced with input representations - be it keys, mouse buttons etc.
|
|
Each placeholder corresponds to one input sequence. Sequence can either be:
|
|
- one input from Gui.UserInput enum
|
|
- tuple of mentioned enum values representing the input sequence
|
|
|
|
>>> InputHint("%1 change mode", Gui.UserInput.KeyM)
|
|
will result in a hint displaying `[M] change mode`
|
|
|
|
>>> InputHint("%1 new line", (Gui.UserInput.KeyControl, Gui.UserInput.KeyEnter))
|
|
will result in a hint displaying `[ctrl][enter] new line`
|
|
|
|
>>> InputHint("%1/%2 increase/decrease ...", Gui.UserInput.KeyU, Gui.UserInput.KeyJ)
|
|
will result in a hint displaying `[U]/[J] increase / decrease ...`
|
|
"""
|
|
|
|
InputSequence = Gui.UserInput | tuple[Gui.UserInput, ...]
|
|
|
|
message: str
|
|
sequences: list[InputSequence]
|
|
|
|
def __init__(self, message: str, *sequences: InputSequence):
|
|
self.message = message
|
|
self.sequences = list(sequences)
|
|
|
|
|
|
class HintManager:
|
|
"""
|
|
A convenience class for managing input hints (shortcut suggestions) displayed to the user.
|
|
It is here mostly to provide well-defined and easy to reach API from python without developers needing
|
|
to call low-level functions on the main window directly.
|
|
"""
|
|
|
|
def show(self, *hints: InputHint):
|
|
"""
|
|
Displays the specified input hints to the user.
|
|
|
|
:param hints: List of hints to show.
|
|
"""
|
|
Gui.getMainWindow().showHint(*hints)
|
|
|
|
def hide(self):
|
|
"""
|
|
Hides all currently displayed input hints.
|
|
"""
|
|
Gui.getMainWindow().hideHint()
|
|
|
|
|
|
Gui.InputHint = InputHint
|
|
Gui.HintManager = HintManager()
|
|
|
|
|
|
class ModGui:
|
|
"""
|
|
Mod Gui Loader.
|
|
"""
|
|
|
|
mod: typing.Any
|
|
|
|
def run_init_gui(self, sub_workbench: Path | None = None) -> bool:
|
|
return False
|
|
|
|
def process_metadata(self) -> bool:
|
|
return False
|
|
|
|
def load(self) -> None:
|
|
"""
|
|
Load the Mod Gui.
|
|
"""
|
|
try:
|
|
if self.mod.state == ModState.Loaded and not self.process_metadata():
|
|
self.run_init_gui()
|
|
except Exception as ex:
|
|
self.mod.state = ModState.Failed
|
|
Err(str(ex))
|
|
|
|
|
|
class DirModGui(ModGui):
|
|
"""
|
|
Dir Mod Gui Loader.
|
|
"""
|
|
|
|
INIT_GUI_PY = "InitGui.py"
|
|
|
|
def __init__(self, mod):
|
|
self.mod = mod
|
|
|
|
def run_init_gui(self, sub_workbench: Path | None = None) -> bool:
|
|
target = sub_workbench or self.mod.path
|
|
init_gui_py = target / self.INIT_GUI_PY
|
|
if init_gui_py.exists():
|
|
try:
|
|
source = init_gui_py.read_text(encoding="utf-8")
|
|
code = compile(source, init_gui_py, "exec")
|
|
exec(code, {**globals(), "__file__": str(init_gui_py)})
|
|
except Exception as ex:
|
|
sep = "-" * 100 + "\n"
|
|
Log(f"Init: Initializing {target!s}... failed\n")
|
|
Log(sep)
|
|
Log(traceback.format_exc())
|
|
Log(sep)
|
|
Err(
|
|
f'During initialization the error "{ex!s}" occurred in {init_gui_py!s}\n'
|
|
)
|
|
Err("Look into the log file for further information\n")
|
|
else:
|
|
Log(f"Init: Initializing {target!s}... done\n")
|
|
return True
|
|
else:
|
|
Log(
|
|
f"Init: Initializing {target!s} (InitGui.py not found)... ignore\n"
|
|
)
|
|
return False
|
|
|
|
def process_metadata(self) -> bool:
|
|
meta = self.mod.metadata
|
|
if not meta:
|
|
return False
|
|
|
|
content = meta.Content
|
|
processed = False
|
|
if "workbench" in content:
|
|
FreeCAD.Gui.addIconPath(str(self.mod.path))
|
|
workbenches = content["workbench"]
|
|
for workbench_metadata in workbenches:
|
|
if not workbench_metadata.supportsCurrentFreeCAD():
|
|
continue
|
|
|
|
subdirectory = (
|
|
workbench_metadata.Subdirectory or workbench_metadata.Name
|
|
)
|
|
subdirectory = self.mod.path / Path(*re.split(r"[/\\]+", subdirectory))
|
|
if not subdirectory.exists():
|
|
continue
|
|
|
|
if self.run_init_gui(subdirectory):
|
|
processed = True
|
|
# Try to generate a new icon from the metadata-specified information
|
|
classname = workbench_metadata.Classname
|
|
if classname:
|
|
try:
|
|
wb_handle = FreeCAD.Gui.getWorkbench(classname)
|
|
except Exception:
|
|
Log(
|
|
f"Failed to get handle to {classname} -- no icon "
|
|
"can be generated, check classname in package.xml\n"
|
|
)
|
|
else:
|
|
GeneratePackageIcon(
|
|
str(subdirectory),
|
|
workbench_metadata,
|
|
wb_handle,
|
|
)
|
|
return processed
|
|
|
|
|
|
class ExtModGui(ModGui):
|
|
"""
|
|
Ext Mod Gui Loader.
|
|
"""
|
|
|
|
def __init__(self, mod):
|
|
self.mod = mod
|
|
|
|
def run_init_gui(self, _sub_workbench: Path | None = None) -> bool:
|
|
Log(f"Init: Initializing {self.mod.name}\n")
|
|
try:
|
|
try:
|
|
importlib.import_module(f"{self.mod.name}.init_gui")
|
|
except ModuleNotFoundError:
|
|
Log(f"Init: No init_gui module found in {self.mod.name}, skipping\n")
|
|
else:
|
|
Log(f"Init: Initializing {self.mod.name}... done\n")
|
|
return True
|
|
except ImportError as ex:
|
|
Err(f'During initialization the error "{ex!s}" occurred\n')
|
|
except Exception as ex:
|
|
sep = "-" * 80 + "\n"
|
|
Err(
|
|
f'During initialization the error "{ex!s}" occurred in {self.mod.name}\n'
|
|
)
|
|
Err(sep)
|
|
Err(traceback.format_exc())
|
|
Err(sep)
|
|
Log(f"Init: Initializing {self.mod.name}... failed\n")
|
|
Log(sep)
|
|
Log(traceback.format_exc())
|
|
Log(sep)
|
|
return False
|
|
|
|
|
|
def InitApplications():
|
|
|
|
# Patch freecad module with gui alias of FreeCADGui
|
|
import freecad
|
|
|
|
freecad.gui = FreeCADGui
|
|
|
|
Log("Init: Searching modules\n")
|
|
|
|
def mod_gui_init(kind: str, mod_type: type, output: list[str]) -> None:
|
|
for mod in App.__ModCache__:
|
|
if mod.kind == kind:
|
|
if mod.state == ModState.Loaded:
|
|
gui = mod_type(mod)
|
|
gui.load()
|
|
if mod.init_mode:
|
|
row = f"| {mod.name:<24.24} | {mod.state.name:<10.10} | {mod.init_mode:<6.6} |\n"
|
|
output.append(row)
|
|
|
|
output = []
|
|
output.append(f"+-{'--':-<24}-+-{'--------':-<10}-+-{'---':-<6}-+\n")
|
|
output.append(f"| {'Mod':<24} | {'Gui State':<10} | {'Mode':<6} |\n")
|
|
output.append(output[0])
|
|
|
|
mod_gui_init("Dir", DirModGui, output)
|
|
Log("All modules with GUIs using InitGui.py are now initialized\n")
|
|
|
|
mod_gui_init("Ext", ExtModGui, output)
|
|
Log("All modules with GUIs initialized using pkgutil are now initialized\n")
|
|
|
|
Log("FreeCADGuiInit Mod summary:\n")
|
|
for line in output:
|
|
Log(line)
|
|
Log(output[0])
|
|
|
|
|
|
def GeneratePackageIcon(
|
|
subdirectory: str, workbench_metadata: FreeCAD.Metadata, wb_handle: Workbench
|
|
) -> None:
|
|
relative_filename = workbench_metadata.Icon
|
|
if not relative_filename:
|
|
# Although a required element, this content item does not have an icon. Just bail out
|
|
return
|
|
absolute_filename = Path(subdirectory) / Path(relative_filename)
|
|
if hasattr(wb_handle, "Icon") and wb_handle.Icon:
|
|
Log(
|
|
f"Init: Packaged workbench {workbench_metadata.Name} specified icon\
|
|
in class {workbench_metadata.Classname}"
|
|
)
|
|
Log(" ... replacing with icon from package.xml data.\n")
|
|
wb_handle.__dict__["Icon"] = str(absolute_filename.resolve())
|
|
|
|
|
|
# signal that the gui is up
|
|
App.GuiUp = 1
|
|
App.Gui = FreeCADGui
|
|
FreeCADGui.Workbench = Workbench
|
|
|
|
Gui.addWorkbench(NoneWorkbench())
|
|
|
|
# init modules
|
|
InitApplications()
|
|
|
|
# set standard workbench (needed as fallback)
|
|
Gui.activateWorkbench("NoneWorkbench")
|
|
|
|
# Register .py, .FCScript and .FCMacro
|
|
FreeCAD.addImportType("Inventor V2.1 (*.iv *.IV)", "FreeCADGui")
|
|
FreeCAD.addImportType(
|
|
"VRML V2.0 (*.wrl *.WRL *.vrml *.VRML *.wrz *.WRZ *.wrl.gz *.WRL.GZ)", "FreeCADGui"
|
|
)
|
|
FreeCAD.addImportType(
|
|
"Python (*.py *.FCMacro *.FCScript *.fcmacro *.fcscript)", "FreeCADGui"
|
|
)
|
|
FreeCAD.addExportType("Inventor V2.1 (*.iv)", "FreeCADGui")
|
|
FreeCAD.addExportType("VRML V2.0 (*.wrl *.vrml *.wrz *.wrl.gz)", "FreeCADGui")
|
|
FreeCAD.addExportType("X3D Extensible 3D (*.x3d *.x3dz)", "FreeCADGui")
|
|
FreeCAD.addExportType("WebGL/X3D (*.xhtml)", "FreeCADGui")
|
|
FreeCAD.addExportType("Portable Document Format (*.pdf)", "FreeCADGui")
|
|
# FreeCAD.addExportType("IDTF (for 3D PDF) (*.idtf)","FreeCADGui")
|
|
# FreeCAD.addExportType("3D View (*.svg)","FreeCADGui")
|
|
|
|
Log("Init: Running FreeCADGuiInit.py start script... done\n")
|
|
|
|
# ┌────────────────────────────────────────────────┐
|
|
# │ Cleanup │
|
|
# └────────────────────────────────────────────────┘
|
|
|
|
if not typing.TYPE_CHECKING:
|
|
del InitApplications
|
|
del NoneWorkbench
|
|
del StandardWorkbench
|
|
del App.__ModCache__, ModGui, DirModGui, ExtModGui
|
|
del typing, re, Path, importlib
|