[Core] FreeCADInit and FreeCADGuiInit refactoring (#23413)
This commit is contained in:
committed by
GitHub
parent
a8fbfae786
commit
da43de8ae0
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2002 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. *
|
||||
# * *
|
||||
@@ -21,17 +22,35 @@
|
||||
# * *
|
||||
# ***************************************************************************/
|
||||
|
||||
# FreeCAD test module
|
||||
# FreeCAD init module - Tests
|
||||
#
|
||||
# Gathering all the information to start FreeCAD.
|
||||
# This is the third of four init scripts:
|
||||
# +------+------------------+-----------------------------+
|
||||
# | This | Script | Runs |
|
||||
# +------+------------------+-----------------------------+
|
||||
# | | CMakeVariables | always |
|
||||
# | | FreeCADInit | always |
|
||||
# | >>>> | FreeCADTest | only if test and not Gui |
|
||||
# | | FreeCADGuiInit | only if Gui is up |
|
||||
# +------+------------------+-----------------------------+
|
||||
|
||||
# Testing the function of the base system and run
|
||||
# (if existing) the test function of the modules
|
||||
|
||||
import FreeCAD
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from __main__ import Log
|
||||
|
||||
Log("FreeCAD test running...\n\n")
|
||||
Log("Init: starting App::FreeCADTest.py\n")
|
||||
Log("░░░▀█▀░█▀█░▀█▀░▀█▀░░░▀█▀░█▀▀░█▀▀░▀█▀░█▀▀░░░\n")
|
||||
Log("░░░░█░░█░█░░█░░░█░░░░░█░░█▀░░▀▀█░░█░░▀▀█░░░\n")
|
||||
Log("░░░▀▀▀░▀░▀░▀▀▀░░▀░░░░░▀░░▀▀▀░▀▀▀░░▀░░▀▀▀░░░\n")
|
||||
|
||||
import sys
|
||||
|
||||
import FreeCAD
|
||||
import TestApp
|
||||
|
||||
testCase = FreeCAD.ConfigGet("TestCase")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# ***************************************************************************
|
||||
# * 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. *
|
||||
# * *
|
||||
@@ -20,24 +21,45 @@
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************/
|
||||
from dataclasses import dataclass
|
||||
|
||||
# FreeCAD gui init module
|
||||
# FreeCAD init module - Gui
|
||||
#
|
||||
# Gathering all the information to start FreeCAD
|
||||
# This is the second one of three init scripts, the third one
|
||||
# runs when the gui is up
|
||||
# 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 |
|
||||
# +------+------------------+-----------------------------+
|
||||
|
||||
# imports the one and only
|
||||
import FreeCAD, FreeCADGui
|
||||
from enum import IntEnum, Enum
|
||||
from dataclasses import dataclass
|
||||
import traceback
|
||||
import typing
|
||||
import re
|
||||
from pathlib import Path
|
||||
import importlib
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
# shortcuts
|
||||
Gui = FreeCADGui
|
||||
App = FreeCAD
|
||||
|
||||
# this is to keep old code working
|
||||
Gui.listCommands = Gui.Command.listAll
|
||||
Gui.isCommandActive = lambda cmd: Gui.Command.get(cmd).isActive()
|
||||
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
|
||||
@@ -63,6 +85,14 @@ class ToggleVisibilityMode(Enum):
|
||||
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
|
||||
|
||||
|
||||
@@ -74,11 +104,11 @@ class Workbench:
|
||||
ToolTip = ""
|
||||
Icon = None
|
||||
|
||||
__Workbench__: "Workbench" # Injected by FreeCAD, see: Application::activateWorkbench
|
||||
|
||||
def Initialize(self):
|
||||
"""Initializes this workbench."""
|
||||
App.Console.PrintWarning(
|
||||
str(self) + ": Workbench.Initialize() not implemented in subclass!"
|
||||
)
|
||||
App.Console.PrintWarning(f"{self!s}: Workbench.Initialize() not implemented in subclass!")
|
||||
|
||||
def ContextMenu(self, recipient):
|
||||
pass
|
||||
@@ -132,7 +162,8 @@ class Workbench:
|
||||
|
||||
|
||||
class StandardWorkbench(Workbench):
|
||||
"""A workbench defines the tool bars, command bars, menus,
|
||||
"""
|
||||
A workbench defines the tool bars, command bars, menus,
|
||||
context menu and dockable windows of the main window.
|
||||
"""
|
||||
|
||||
@@ -219,74 +250,85 @@ Gui.InputHint = InputHint
|
||||
Gui.HintManager = HintManager()
|
||||
|
||||
|
||||
def InitApplications():
|
||||
import sys, os, traceback
|
||||
import io as cStringIO
|
||||
class ModGui:
|
||||
"""
|
||||
Mod Gui Loader.
|
||||
"""
|
||||
|
||||
# Searching modules dirs +++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
# (additional module paths are already cached)
|
||||
ModDirs = FreeCAD.__ModDirs__
|
||||
# print ModDirs
|
||||
Log("Init: Searching modules\n")
|
||||
mod: typing.Any
|
||||
|
||||
def RunInitGuiPy(Dir) -> bool:
|
||||
InstallFile = os.path.join(Dir, "InitGui.py")
|
||||
if os.path.exists(InstallFile):
|
||||
try:
|
||||
with open(InstallFile, "rt", encoding="utf-8") as f:
|
||||
exec(compile(f.read(), InstallFile, "exec"))
|
||||
except Exception as inst:
|
||||
Log("Init: Initializing " + Dir + "... failed\n")
|
||||
Log("-" * 100 + "\n")
|
||||
Log(traceback.format_exc())
|
||||
Log("-" * 100 + "\n")
|
||||
Err(
|
||||
'During initialization the error "'
|
||||
+ str(inst)
|
||||
+ '" occurred in '
|
||||
+ InstallFile
|
||||
+ "\n"
|
||||
)
|
||||
Err("Look into the log file for further information\n")
|
||||
mod_name = os.path.normpath(Dir).split(os.path.sep)[-1].lower()
|
||||
if hasattr(FreeCAD, "__failed_mods__"):
|
||||
FreeCAD.__failed_mods__.append(mod_name)
|
||||
else:
|
||||
FreeCAD.__failed_mods__ = [mod_name]
|
||||
if mod_name not in FreeCAD.__fallback_mods__:
|
||||
Err("Could not evaluate module '" + mod_name + "' for fallbacks\n")
|
||||
elif len(FreeCAD.__fallback_mods__[mod_name]) > 1:
|
||||
new_path = os.path.normpath(FreeCAD.__fallback_mods__[mod_name][-2])
|
||||
Err(f"A fallback module was found for module '{mod_name}': {new_path}\n")
|
||||
Err(f"Rename or remove {os.path.normpath(Dir)} to use the fallback module\n")
|
||||
else:
|
||||
Log("Init: Initializing " + Dir + "... done\n")
|
||||
return True
|
||||
else:
|
||||
Log("Init: Initializing " + Dir + "(InitGui.py not found)... ignore\n")
|
||||
def run_init_gui(self, sub_workbench: Path | None = None) -> bool:
|
||||
return False
|
||||
|
||||
def processMetadataFile(Dir, MetadataFile):
|
||||
meta = FreeCAD.Metadata(MetadataFile)
|
||||
if not meta.supportsCurrentFreeCAD():
|
||||
return None
|
||||
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)
|
||||
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(Dir)
|
||||
FreeCAD.Gui.addIconPath(str(self.mod.path))
|
||||
workbenches = content["workbench"]
|
||||
for workbench_metadata in workbenches:
|
||||
if not workbench_metadata.supportsCurrentFreeCAD():
|
||||
return None
|
||||
subdirectory = (
|
||||
workbench_metadata.Name
|
||||
if not workbench_metadata.Subdirectory
|
||||
else workbench_metadata.Subdirectory
|
||||
)
|
||||
subdirectory = subdirectory.replace("/", os.path.sep)
|
||||
subdirectory = os.path.join(Dir, subdirectory)
|
||||
ran_init = RunInitGuiPy(subdirectory)
|
||||
continue
|
||||
|
||||
if ran_init:
|
||||
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:
|
||||
@@ -294,137 +336,100 @@ def InitApplications():
|
||||
wb_handle = FreeCAD.Gui.getWorkbench(classname)
|
||||
except Exception:
|
||||
Log(
|
||||
f"Failed to get handle to {classname} -- no icon\
|
||||
can be generated,\n check classname in package.xml\n"
|
||||
f"Failed to get handle to {classname} -- no icon "
|
||||
"can be generated, check classname in package.xml\n"
|
||||
)
|
||||
else:
|
||||
GeneratePackageIcon(dir, subdirectory, workbench_metadata, wb_handle)
|
||||
GeneratePackageIcon(
|
||||
str(subdirectory),
|
||||
workbench_metadata,
|
||||
wb_handle,
|
||||
)
|
||||
return processed
|
||||
|
||||
def tryProcessMetadataFile(Dir, MetadataFile):
|
||||
|
||||
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:
|
||||
processMetadataFile(Dir, MetadataFile)
|
||||
except Exception as exc:
|
||||
Err(str(exc))
|
||||
|
||||
def checkIfAddonIsDisabled(Dir):
|
||||
DisabledAddons = FreeCAD.ConfigGet("DisabledAddons").split(";")
|
||||
Name = os.path.basename(Dir)
|
||||
|
||||
if Name in DisabledAddons:
|
||||
Msg(
|
||||
f'NOTICE: Addon "{Name}" disabled by presence of "--disable-addon {Name}" argument\n'
|
||||
)
|
||||
return True
|
||||
|
||||
stopFileName = "ALL_ADDONS_DISABLED"
|
||||
stopFile = os.path.join(Dir, os.path.pardir, stopFileName)
|
||||
if os.path.exists(stopFile):
|
||||
Msg(f'NOTICE: Addon "{Dir}" disabled by presence of {stopFileName} stopfile\n')
|
||||
return True
|
||||
|
||||
stopFileName = "ADDON_DISABLED"
|
||||
stopFile = os.path.join(Dir, stopFileName)
|
||||
if os.path.exists(stopFile):
|
||||
Msg(f'NOTICE: Addon "{Dir}" disabled by presence of {stopFileName} stopfile\n')
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||
for Dir in ModDirs:
|
||||
if Dir not in ["", "CVS", "__init__.py"]:
|
||||
if checkIfAddonIsDisabled(Dir):
|
||||
continue
|
||||
MetadataFile = os.path.join(Dir, "package.xml")
|
||||
if os.path.exists(MetadataFile):
|
||||
tryProcessMetadataFile(Dir, MetadataFile)
|
||||
else:
|
||||
RunInitGuiPy(Dir)
|
||||
|
||||
def InitApplications():
|
||||
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")
|
||||
|
||||
try:
|
||||
import pkgutil
|
||||
import importlib
|
||||
import freecad
|
||||
|
||||
freecad.gui = FreeCADGui
|
||||
for _, freecad_module_name, freecad_module_ispkg in pkgutil.iter_modules(
|
||||
freecad.__path__, "freecad."
|
||||
):
|
||||
# Check for a stopfile
|
||||
stopFile = os.path.join(
|
||||
FreeCAD.getUserAppDataDir(), "Mod", freecad_module_name[8:], "ADDON_DISABLED"
|
||||
)
|
||||
if os.path.exists(stopFile):
|
||||
continue
|
||||
|
||||
# Make sure that package.xml (if present) does not exclude this version of FreeCAD
|
||||
MetadataFile = os.path.join(
|
||||
FreeCAD.getUserAppDataDir(), "Mod", freecad_module_name[8:], "package.xml"
|
||||
)
|
||||
if os.path.exists(MetadataFile):
|
||||
meta = FreeCAD.Metadata(MetadataFile)
|
||||
if not meta.supportsCurrentFreeCAD():
|
||||
continue
|
||||
|
||||
if freecad_module_ispkg:
|
||||
Log("Init: Initializing " + freecad_module_name + "\n")
|
||||
try:
|
||||
freecad_module = importlib.import_module(freecad_module_name)
|
||||
if any(
|
||||
module_name == "init_gui"
|
||||
for _, module_name, ispkg in pkgutil.iter_modules(freecad_module.__path__)
|
||||
):
|
||||
importlib.import_module(freecad_module_name + ".init_gui")
|
||||
Log("Init: Initializing " + freecad_module_name + "... done\n")
|
||||
else:
|
||||
Log(
|
||||
"Init: No init_gui module found in "
|
||||
+ freecad_module_name
|
||||
+ ", skipping\n"
|
||||
)
|
||||
except Exception as inst:
|
||||
Err(
|
||||
'During initialization the error "'
|
||||
+ str(inst)
|
||||
+ '" occurred in '
|
||||
+ freecad_module_name
|
||||
+ "\n"
|
||||
)
|
||||
Err("-" * 80 + "\n")
|
||||
Err(traceback.format_exc())
|
||||
Err("-" * 80 + "\n")
|
||||
Log("Init: Initializing " + freecad_module_name + "... failed\n")
|
||||
Log("-" * 80 + "\n")
|
||||
Log(traceback.format_exc())
|
||||
Log("-" * 80 + "\n")
|
||||
except ImportError as inst:
|
||||
Err('During initialization the error "' + str(inst) + '" occurred\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(
|
||||
dir: str, subdirectory: str, workbench_metadata: FreeCAD.Metadata, wb_handle: Workbench
|
||||
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 = os.path.join(subdirectory, relative_filename)
|
||||
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(f" ... replacing with icon from package.xml data.\n")
|
||||
wb_handle.__dict__["Icon"] = absolute_filename
|
||||
Log(" ... replacing with icon from package.xml data.\n")
|
||||
wb_handle.__dict__["Icon"] = str(absolute_filename.resolve())
|
||||
|
||||
|
||||
Log("Init: Running FreeCADGuiInit.py start script...\n")
|
||||
|
||||
|
||||
# init the gui
|
||||
|
||||
# signal that the gui is up
|
||||
App.GuiUp = 1
|
||||
App.Gui = FreeCADGui
|
||||
@@ -448,12 +453,19 @@ 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")
|
||||
FreeCAD.addExportType("Portable Document Format (*.pdf)", "FreeCADGui")
|
||||
|
||||
del InitApplications
|
||||
del NoneWorkbench
|
||||
del StandardWorkbench
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user