From 9c08aa0918138653b0def1a7b45dd28b5928d203 Mon Sep 17 00:00:00 2001
From: Elvaerwyn <75058709+Licentyius@users.noreply.github.com>
Date: Fri, 8 May 2026 00:59:33 -0600
Subject: [PATCH 1/3] Add files via upload
Changes for fixing floor having been lost on last fix. Added logic for foot position.
---
scene.py | 296 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 296 insertions(+)
create mode 100644 scene.py
diff --git a/scene.py b/scene.py
new file mode 100644
index 0000000..00f23d3
--- /dev/null
+++ b/scene.py
@@ -0,0 +1,296 @@
+"""
+ License information: data/licenses/makehuman_license.txt
+ Author: black-punkduck, Elvaerwyn_MH2 v1.1
+
+ Classes:
+ * Scene
+"""
+from opengl.material import Material
+from opengl.prims import CoordinateSystem, Grid, BoneList, VisLights, DiamondSkeleton, Cuboid
+
+class Scene():
+ """
+ Class to hold additional objects and common elements
+
+ :param globalObjects glob: pointer to global objects
+ :param OpenGLView parent: parent QOpenGLWidget widget
+ :param ShaderRepository shaders: pointer to shader repository
+ :param function update: the parent update function to redraw elements
+ """
+ def __init__(self, glob, parent, shaders, update):
+
+ self.glob = glob
+ self.parent = parent
+ self.context = parent.context()
+ self.shaders = shaders
+ self.update = update
+ self.env = glob.env
+
+ # floor
+ #
+ self.floor = True
+ self.floortex = None
+ self.floorsize = [10.0, 0.2, 10.0]
+ self.floortexname = "default.png"
+
+ # scene components
+ #
+ self.sysmaterials = []
+ self.prims = {}
+ self.visLights = None
+ self.diamondskel = False
+ self.other_objects_invisible = False
+
+ # create system uni-colored materials
+ #
+
+ for name in ("black", "white", "orange", "red", "blue", "normal", "floor"):
+ m = Material(self.glob, name, "system")
+ self.sysmaterials.append(m)
+
+ self.black = self.sysmaterials[0].uniColor([0.0, 0.0, 0.0])
+ self.white = self.sysmaterials[1].uniColor([1.0, 1.0, 1.0])
+ self.orange= self.sysmaterials[2].uniColor([1.0, 0.5, 0.0])
+ self.red = self.sysmaterials[3].uniColor([1.0, 0.5, 0.5])
+ self.blue = self.sysmaterials[3].uniColor([0.5, 0.5, 1.0])
+ self.normal= self.sysmaterials[4].uniColor([0.5, 0.5, 1.0])
+
+ # and floor texture
+ self.floorTexture(self.floortexname)
+
+
+ def lowestPos(self, posed=False):
+ if self.glob.baseClass is not None:
+ return self.glob.baseClass.getLowestPos(posed)
+ else:
+ return 20
+
+ def createPrims(self, light):
+ """
+ create all primitives, axes, floor, light
+
+ :param Light light: light class
+ """
+ shader = self.shaders.getShader("fixcolor")
+ self.prims["axes"] = CoordinateSystem(self.context, shader, "axes", 10.0)
+ lowestPos = self.lowestPos()
+
+ self.prims["xygrid"] = Grid(self.context, shader, "xygrid", 10.0, lowestPos, "xy")
+ self.prims["yzgrid"] = Grid(self.context, shader, "yzgrid", 10.0, lowestPos, "yz")
+ self.prims["xzgrid"] = Grid(self.context, shader, "xzgrid", 10.0, lowestPos, "xz")
+ self.prims["floorcuboid"] = Cuboid(self.context, self.shaders, "floorcuboid", self.floorsize, [0.0, lowestPos, 0.0], self.floortex)
+
+
+ # visualization of lamps (if obj is not found, no lamps are presented)
+ #
+ self.visLights = VisLights(self.parent, light)
+ success =self.visLights.setup()
+ if not success:
+ self.visLights = None
+
+ def togglePrims(self, name, status):
+ """
+ toggle a primitive, floor can be the cuboid or a grid, skeleton is presented in pose color or normal color
+
+ :param str name: name of the primitive
+ :param bool status: currenct status
+ """
+ if name == "floor":
+ name = "floorcuboid" if self.floor else "xzgrid"
+ if name in self.prims:
+ if status is True:
+ if name == "floorcuboid":
+ self.prims[name].newGeometry(self.lowestPos())
+ elif name.endswith("grid"):
+ direction = name[:2]
+ self.prims[name].newGeometry(self.lowestPos(), direction)
+ elif name == "skeleton":
+ posed = (self.glob.baseClass.bvh is not None) or (self.glob.baseClass.expression is not None)
+ self.prims[name].newGeometry(posed)
+ self.prims[name].setVisible(status)
+ self.update()
+
+ # floor
+ #
+ def setFloor(self, v):
+ """
+ sets floor, also changes between cuboid or xzgrid
+
+ :param bool v: currenct status
+ """
+ self.floor = v
+ if self.glob.baseClass is not None:
+ if v and self.prims["xzgrid"].isVisible():
+ self.togglePrims("xzgrid", False)
+ self.togglePrims("floorcuboid", True)
+ elif not v and self.prims["floorcuboid"].isVisible():
+ self.togglePrims("floorcuboid", False)
+ self.togglePrims("xzgrid", True)
+ self.update()
+
+ def setFloorSize(self, sq=0.0, d=0.0):
+ """
+ sets the size of the floor
+
+ :param float sq: size of the plane (x, z)
+ :param float d: density/height of the floor
+ """
+ if self.glob.baseClass is not None:
+ if sq != 0.0:
+ self.floorsize[0] = self.floorsize[2] = sq
+ if d != 0.0:
+ self.floorsize[1] = d
+ self.prims["floorcuboid"].newSize(self.floorsize)
+ self.update()
+
+ def hasFloor(self):
+ """
+ returns if scene has a floor
+ """
+ if self.glob.baseClass is not None:
+ return self.prims["xzgrid"].isVisible() or self.prims["floorcuboid"].isVisible()
+ return False
+
+ def floorTexture(self, name):
+ """
+ sets a floor texture (if available)
+
+ :param str name: name of the texture in shaders folder
+ """
+ floorpath = self.env.existDataFile("shaders", "floor", name)
+ self.floortex = self.sysmaterials[5].setDiffuse(floorpath, self.red, None )
+ self.floortexname = name
+
+ def modFloorTexture(self, name):
+ """
+ sets a new Floor-Texture
+
+ :param str name: name of the texture in shaders folder
+ """
+ self.floorTexture(name)
+ self.prims["floorcuboid"].setTexture(self.floortex)
+
+
+ def newFloorPosition(self, posed=False):
+ """
+ sets a new floor position to fit under character
+
+ :param bool posed: if character is posed
+ """
+ if self.glob.baseClass.floorCalcMethod == 0:
+ floorpos = self.lowestPos(posed)
+ else:
+ floorpos = 0.0
+ self.prims["floorcuboid"].newGeometry(floorpos)
+ self.prims["xzgrid"].newGeometry(floorpos, "xz")
+
+ # skeleton
+ #
+ def setObjectsInvisible(self, value):
+ self.other_objects_invisible = value
+
+ def prepareSkeleton(self, posed=False):
+ """
+ prepare graphical presentation of skeleton
+ posed mode: orange, normal mode blue, internal=no skeleton red
+
+ :param bool posed: if character is posed
+ """
+
+ # delete old one
+ #
+ self.delSkeleton()
+
+ bc = self.glob.baseClass
+
+ # really no skeleton
+ #
+ if bc is None or (bc.skeleton is None and bc.default_skeleton is None):
+ return
+
+ if posed:
+ skeleton = bc.pose_skeleton
+ col = [1.0, 0.5, 0.0]
+ coltex= self.orange
+ else:
+ skeleton = bc.skeleton
+ col = [0.5, 0.5, 1.0]
+ coltex= self.blue
+
+ if skeleton is None:
+ skeleton = bc.default_skeleton
+ col = [1.0, 0.5, 0.5]
+ coltex= self.red
+
+ shader = self.shaders.getShader("fixcolor")
+ if self.diamondskel:
+ self.prims["skeleton"] = DiamondSkeleton(self.context, self.shaders, "diamondskel", skeleton, coltex)
+ else:
+ self.prims["skeleton"] = BoneList(self.context, shader, "skeleton", skeleton, col)
+ if self.other_objects_invisible is True:
+ self.togglePrims("skeleton", True)
+ self.update()
+
+ def hasSkeleton(self):
+ """
+ returns if a skeleton is available
+ """
+ return "skeleton" in self.prims
+
+ def delSkeleton(self):
+ """
+ deletes the skeleton, if available
+ """
+ if "skeleton" in self.prims:
+ self.prims["skeleton"].delete()
+ del self.prims["skeleton"]
+
+ def setDiamondSkeleton(self, v):
+ """
+ toggles between diamond and stick-skeleton
+ """
+ self.diamondskel = v
+ if self.glob.baseClass is not None:
+ self.prepareSkeleton(self.glob.baseClass.in_posemode)
+ self.update()
+
+ # drawing & modification
+ #
+
+ def setYRotation(self, angle):
+ """
+ set y rotation (only skeleton rotates)
+
+ :param float angle: angle of rotation
+ """
+ if "skeleton" in self.prims:
+ self.prims["skeleton"].setYRotation(angle)
+
+ def draw(self, proj_view_matrix, campos, showskel):
+ """
+ draws the assets
+
+ :param QMatrix4x4 proj_view_matrix: view-matrix
+ :param QVector3D campos: camera position
+ :param bool showskel: if the skeleton should be drawn
+ """
+ bc = self.glob.baseClass
+
+ if self.visLights is not None and self.prims["axes"].isVisible():
+ self.visLights.draw(proj_view_matrix, campos, self.white)
+
+ for name in self.prims:
+ if name == "skeleton":
+ if showskel:
+ if self.diamondskel:
+ self.prims[name].draw(proj_view_matrix, bc.in_posemode)
+ else:
+ self.prims[name].newGeometry(bc.in_posemode)
+ self.prims[name].draw(proj_view_matrix)
+ else:
+ self.prims[name].draw(proj_view_matrix)
+
+ def cleanUp(self):
+ for m in self.sysmaterials:
+ m.freeTextures()
+
From ff2853f8e23788407fa9bf3c730eecdedbfbe8c8 Mon Sep 17 00:00:00 2001
From: Elvaerwyn <75058709+Licentyius@users.noreply.github.com>
Date: Sun, 10 May 2026 23:17:55 -0600
Subject: [PATCH 2/3] Added ability to resize windows
Added the ability to resize main windows in UI with limitations set on them.
---
mainwindow.py | 1299 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1299 insertions(+)
create mode 100644 mainwindow.py
diff --git a/mainwindow.py b/mainwindow.py
new file mode 100644
index 0000000..0cfb661
--- /dev/null
+++ b/mainwindow.py
@@ -0,0 +1,1299 @@
+"""
+ License information: data/licenses/makehuman_license.txt
+ Author: black-punkduck, Elvaerwyn_MH2 Makehuman 2 2026
+
+ The mainwindow containing menus, left, center and right column
+
+ Classes:
+ * MHMainWindow
+"""
+from PySide6.QtWidgets import (
+ QAbstractItemView, QDialogButtonBox, QFrame, QGroupBox, QHBoxLayout,
+ QLabel, QListWidget, QMainWindow, QMdiArea, QMdiSubWindow,
+ QMessageBox, QPushButton, QScrollArea, QSizePolicy, QSplitter,
+ QVBoxLayout, QWidget
+)
+
+
+from PySide6.QtGui import QIcon, QCloseEvent, QAction, QDesktopServices
+from PySide6.QtCore import QSize, Qt, QUrl, QTimer
+from gui.prefwindow import MHPrefWindow
+from gui.logwindow import MHLogWindow
+from gui.infowindow import MHInfoWindow
+from gui.tablewindow import MHMemWindow
+from gui.measurewindow import MHCharMeasWindow
+from gui.scenewindow import MHSceneWindow
+from gui.graphwindow import MHGraphicWindow, NavigationEvent
+from gui.randomwindow import RandomForm, RandomValues
+from gui.fileactions import BaseSelect, SaveMHMForm
+from gui.downloads import DownLoadImport
+from gui.exporter import ExportLeftPanel, ExportRightPanel, ExporterValues
+from gui.poseactions import AnimPlayer, AnimPlayerValues, AnimMode
+from gui.poseeditor import AnimExpressionEdit, AnimPoseEdit
+from gui.slider import ScaleComboArray
+from gui.imageselector import ImageSelection
+from gui.renderer import Renderer, RendererValues
+from gui.common import DialogBox, ErrorBox, WorkerThread, MHBusyWindow, MHGroupBox, IconButton, TextBox, MHFileRequest
+from gui.qtreeselect import MHTreeView
+from core.baseobj import baseClass
+from core.apisocket import apiSocket
+from core.attached_asset import attachedAsset
+from opengl.info import GLDebug
+
+import os
+
+class MHMainWindow(QMainWindow):
+ """
+ Main Window class
+ """
+ def __init__(self, glob):
+ self.env = glob.env
+ env = glob.env
+ self.glob = glob
+
+ self.prog_window = None # will hold the progress bar
+
+ self.leftColumn = None
+ self.LeftBox = None # layouts to fill
+ self.lastForm = None # needed for close functions
+
+ self.rightColumn = None
+ self.visRightColumn = None # QWidget to hide right column
+ self.ToolBox = None
+
+ self.graph = None
+
+ self.qTree = None
+ self.qtreefilter = None
+
+ self.ButtonBox = None
+ self.CategoryBox = None
+ self.baseSelector = None
+
+ self.bckproc = None # background process is running
+
+ self.tool_mode = 0 # 0 = files, 1 = modelling, 2 = equipment, 3 = pose, 4 = render, 10 = information
+ self.category_mode = 0 # the categories according to tool_mode
+
+ self.equipment = [
+ { "func": None, "menu": None, "name": "clothes", "mode": 1 },
+ { "func": None, "menu": None, "name": "hair", "mode": 0 },
+ { "func": None, "menu": None, "name": "eyes", "mode": 0 },
+ { "func": None, "menu": None, "name": "eyebrows", "mode": 0 },
+ { "func": None, "menu": None, "name": "eyelashes", "mode": 0 },
+ { "func": None, "menu": None, "name": "teeth", "mode": 0 },
+ { "func": None, "menu": None, "name": "tongue", "mode": 0 },
+ { "func": None, "menu": None, "name": "proxy", "mode": 0 }
+ ]
+ self.animation = [
+ { "func": None, "menu": None, "name": "rigs", "mode": 0 },
+ { "func": None, "menu": None, "name": "poses", "mode": 0 },
+ { "func": None, "menu": None, "name": "animation", "mode": 0 },
+ { "func": None, "menu": None, "name": "expressions", "mode": 0 },
+ { "func": None, "menu": None, "name": "expression editor", "mode": 0 },
+ { "func": None, "menu": None, "name": "pose editor", "mode": 0 }
+ ]
+
+ self.model_buttons = [
+ { "button": None, "icon": "reset.png", "tip": "Reset all targets", "func": self.reset_call, "check": False },
+ { "button": None, "icon": "symm1.png", "tip": "Symmetry, right to left", "func": self.symRToL, "check": False },
+ { "button": None, "icon": "symm2.png", "tip": "Symmetry, left to right", "func": self.symLToR, "check": False },
+ { "button": None, "icon": "symm.png", "tip": "Symmetry applied always", "func": self.symSwitch }
+ ]
+
+ self.tool_buttons = [
+ { "button": None, "icon": "files.png", "tip": "Files", "func": self.setCategoryIcons},
+ { "button": None, "icon": "sculpt.png", "tip": "Modelling, Change character", "func": self.setCategoryIcons},
+ { "button": None, "icon": "equip.png", "tip": "Add equipment", "func": self.setCategoryIcons },
+ { "button": None, "icon": "pose.png", "tip": "Pose", "func": self.setCategoryIcons },
+ { "button": None, "icon": "render.png", "tip": "Render", "func": self.setCategoryIcons }
+ ]
+ self.category_buttons = [
+ [
+ { "button": None, "h": False, "icon": "f_newbase.png", "tip": "select basemesh", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "skin.png", "tip": "change skin", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "f_load.png", "tip": "load character", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "f_save.png", "tip": "save character", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "f_export.png", "tip": "export character", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "f_download.png", "tip": "download assets", "func": self.callCategory}
+ ], [
+ { "button": None, "h": False, "icon": "measurement.png", "tip": "modelling by category", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "randomhuman.png", "tip": "randomize", "func": self.callCategory}
+ ], [
+ { "button": None, "h": False, "ref": "clothes", "icon": "eq_clothes.png", "tip": "Clothes", "func": self.callCategory },
+ { "button": None, "h": False, "ref": "hair", "icon": "eq_hair.png", "tip": "Hair", "func": self.callCategory },
+ { "button": None, "h": False, "ref": "eyes", "icon": "eq_eyes.png", "tip": "Eyes", "func": self.callCategory },
+ { "button": None, "h": False, "ref": "eyebrows", "icon": "eq_eyebrows.png", "tip": "Eyebrows", "func": self.callCategory },
+ { "button": None, "h": False, "ref": "eyelashes", "icon": "eq_eyelashes.png", "tip": "Eyelashes", "func": self.callCategory },
+ { "button": None, "h": False, "ref": "teeth", "icon": "eq_teeth.png", "tip": "Teeth", "func": self.callCategory },
+ { "button": None, "h": False, "ref": "tongue", "icon": "eq_tongue.png", "tip": "Tongue", "func": self.callCategory },
+ { "button": None, "h": False, "ref": "proxy", "icon": "eq_proxy.png", "tip": "Topology, Proxies", "func": self.callCategory }
+ ], [
+ { "button": None, "h": False, "icon": "an_skeleton.png", "tip": "Skeleton", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "an_pose.png", "tip": "Load pose or animation", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "an_movie.png", "tip": "Play animation", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "an_expression.png", "tip": "Expressions", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "an_expressedit.png", "tip": "Expression editor", "func": self.callCategory},
+ { "button": None, "h": False, "icon": "an_modpose.png", "tip": "Pose editor", "func": self.callCategory}
+ ], [
+ ]
+ ]
+
+ super().__init__()
+
+ s = env.session["mainwinsize"]
+ self.resize (s["w"], s["h"])
+
+ menu_bar = self.menuBar()
+
+ about_menu = menu_bar.addMenu(QIcon(os.path.join(env.path_sysicon, "makehuman.png")), "&About")
+ self.addActCallBack(about_menu, "Info", self.info_call)
+
+ file_menu = menu_bar.addMenu("&File")
+ self.addActCallBack(file_menu, "Load Model", self.loadmhm_call)
+ self.addActCallBack(file_menu, "Save Model", self.savemhm_call)
+ self.addActCallBack(file_menu, "Export Model", self.exportmhm_call)
+ self.addActCallBack(file_menu, "Download Assets", self.download_call)
+ self.addActCallBack(file_menu, "Quit", self.quit_call)
+
+ set_menu = menu_bar.addMenu("&Settings")
+ self.addActCallBack(set_menu, "Preferences", self.pref_call)
+ self.addActCallBack(set_menu, "Lights and Scene", self.scene_call)
+ self.addActCallBack(set_menu, "Messages", self.log_call)
+
+ binaries = set_menu.addMenu("Create Binaries")
+ self.addActCallBack(binaries, "User 3d Objects", self.compress_user3dobjs)
+ self.addActCallBack(binaries, "User Targets", self.compress_usertargets)
+
+ if env.admin:
+ self.addActCallBack(binaries, "System 3d Objects", self.compress_sys3dobjs)
+ self.addActCallBack(binaries, "System Targets", self.compress_systargets)
+
+ set_menu.addSeparator()
+ regenerate = set_menu.addMenu("Regenerate all Binaries")
+ self.addActCallBack(regenerate, "User 3d Objects", self.regenerate_user3dobjs)
+
+ if env.admin:
+ self.addActCallBack(regenerate, "System 3d Objects", self.regenerate_sys3dobjs)
+
+ set_menu.addSeparator()
+ self.addActCallBack(set_menu, "Backup User Database", self.exportUserDB)
+ self.addActCallBack(set_menu, "Restore User Database", self.importUserDB)
+
+ tools_menu = menu_bar.addMenu("&Tools")
+ self.addActCallBack(tools_menu, "Select Basemesh", self.base_call)
+ self.addActCallBack(tools_menu, "Select Base Material", self.skin_call)
+ self.addActCallBack(tools_menu, "Change Character", self.morph_call)
+ self.addActCallBack(tools_menu, "Randomize Character", self.random_call)
+
+ self.equip = tools_menu.addMenu("Equipment")
+ self.animenu = tools_menu.addMenu("Animation")
+
+ act_menu = menu_bar.addMenu("&Activate")
+
+ act = QAction('Diamond skeleton', act_menu, checkable=True)
+ act_menu.addAction(act)
+ act.triggered.connect(self.dimskel_call)
+
+ act = QAction('Floor instead of grid', act_menu, checkable=True)
+ act_menu.addAction(act)
+ act.triggered.connect(self.floor_call)
+
+ act = QAction('Socket active', act_menu, checkable=True)
+ act_menu.addAction(act)
+ act.triggered.connect(self.socket_call)
+
+ act = QAction('Display camera position', act_menu, checkable=True)
+ act_menu.addAction(act)
+ act.triggered.connect(self.deb_cam)
+
+ deb_menu = act_menu.addMenu("Debug messages")
+
+ for num, text in self.env.helpVerbose():
+ checked = ((self.env.verbose & num) != 0)
+ act = QAction(text, deb_menu, checkable=True, checked=checked)
+ deb_menu.addAction(act)
+ act.triggered.connect(self.loglevel_call)
+ act.setData(num)
+
+ act = QAction('mid level messages', deb_menu, checkable=True)
+ deb_menu.addAction(act)
+ act.triggered.connect(self.loglevel_call)
+ act.setData(2)
+
+ info_menu = menu_bar.addMenu("&Information")
+ self.addActCallBack(info_menu, "Character Info", self.measure_call)
+ self.addActCallBack(info_menu, "Memory Info", self.memory_call)
+ self.addActCallBack(info_menu, "Local OpenGL Information", self.glinfo_call)
+ self.addActCallBack(info_menu, "Used library versions", self.vers_call)
+ self.addActCallBack(info_menu, "License", self.lic_call)
+
+ lics = info_menu.addMenu("3rd Party licenses")
+ for elem in ["PySide6", "PyOpenGL", "NumPy"]:
+ self.addActCallBack(lics, elem, self.lic_call)
+
+ self.addActCallBack(info_menu, "Credits", self.lic_call)
+
+ if "support_urls" in self.env.release_info:
+ for elem in self.env.release_info["support_urls"]:
+ urlname = self.env.release_info["support_urls"][elem]
+ if urlname in self.env.release_info:
+ self.addActCallBack(info_menu, elem, self.url_info_call)
+
+ help_menu = menu_bar.addMenu("&Help")
+ self.addActCallBack(help_menu, "Context Help", self.context_help)
+ self.addActCallBack(help_menu, "Short Summary", self.descr_help)
+ self.addActCallBack(help_menu, "Navigation", self.nav_help)
+ self.addActCallBack(help_menu, "File System", self.fsys_help)
+
+ if self.glob.baseClass is not None:
+ self.createImageSelection()
+
+ # generate tool buttons, model_buttons (save ressources)
+ #
+ for elem in (self.tool_buttons, self.model_buttons):
+ for n, b in enumerate(elem):
+ c = b["check"] if "check" in b else True
+ b["button"] = IconButton(n, os.path.join(self.env.path_sysicon, b["icon"]), b["tip"], b["func"], checkable=c)
+ self.markSelectedButtons(self.tool_buttons, self.tool_buttons[0])
+
+ # generate category buttons
+ #
+ for m, tool in enumerate(self.category_buttons):
+ offset = (m + 1) * 100
+ for n, b in enumerate(tool):
+ if b["h"] is False:
+ b["button"] = IconButton(offset+n, os.path.join(self.env.path_sysicon, b["icon"]), b["tip"], b["func"], checkable=True)
+ self.markSelectedButtons(self.category_buttons[0], self.category_buttons[0][0])
+
+ # generate random values
+ if self.glob.baseClass is not None:
+ self.setPresets()
+
+ self.createCentralWidget()
+ self.setWindowTitle("default character")
+
+
+ def debug(self, text):
+ self.env.logLine(2, "MainWindow: " + text)
+
+ def addActCallBack(self, menu, title, callback):
+ entry = menu.addAction(title)
+ entry.triggered.connect(callback)
+ return entry
+
+ def createImageSelection(self):
+ """
+ called once to create all image-selections for baseclass
+ """
+ binfo = self.glob.baseClass.baseInfo
+
+ # clear equipment menu in case of change
+ #
+ self.equip.clear()
+
+ # create equipment button row according to base.json, at least: clothes
+ # (is done by setting h to True if "ref" is part of the dictionary)
+ #
+ eqrow = self.category_buttons[2]
+ equipment = binfo["equipment"] if "equipment" in binfo else ["clothes"]
+ for elem in eqrow:
+ elem["h"] = not ("ref" in elem and elem["ref"] in equipment)
+
+ for elem in self.equipment:
+ if elem["name"] in equipment:
+ elem["func"] = ImageSelection(self, self.glob.cachedInfo, elem["name"], elem["mode"], self.equipCallback, doubleclick=True)
+ elem["func"].prepare()
+ elem["menu"] = self.addActCallBack(self.equip, elem["name"], self.equip_call)
+ else:
+ elem["func"] = None # reset unsused functions
+
+ # always create skins and models collection
+ #
+ self.skinselect = ImageSelection(self, self.glob.cachedInfo, "skins", 0, self.selectSkinCallback, 2, True)
+ self.skinselect.prepare()
+
+ self.charselect = ImageSelection(self, self.glob.cachedInfo, "models", 0, self.loadByIconCallback, 3, True)
+ self.charselect.prepare()
+
+ # create selection of rigs, poses, animation ... pose-editor
+ #
+ self.animenu.clear()
+ for elem in self.animation:
+ elem["func"] = ImageSelection(self, self.glob.cachedInfo, elem["name"], elem["mode"], self.animCallback, doubleclick=True)
+ elem["func"].prepare()
+ elem["menu"] = self.addActCallBack(self.animenu, elem["name"], self.anim_call)
+
+ def redoImageSelectionRepos(self):
+ """
+ this function is used after e.g. a download took place
+ """
+ cache = self.glob.rescanAssets()
+ for elem in self.equipment:
+ if elem["func"] is not None:
+ elem["func"].prepare(cache)
+ self.charselect.prepare(cache)
+ for elem in self.animation:
+ elem["func"].prepare(cache)
+ self.glob.baseClass.markAllAttachedAssets()
+
+
+ def setWindowTitle(self, text):
+ suffix = " [" + self.env.release_info["suffix"] + "] " if self.env.release_info["suffix"] != "" else ""
+ title = self.env.release_info["name"] + suffix + " (" + text + ")"
+ super().setWindowTitle(title)
+
+
+ def equipCallback(self, selected, eqtype, multi):
+ self.glob.project_changed = True
+ if isinstance(selected, str):
+ self.glob.baseClass.delAsset(selected)
+ return
+ if selected.status == 0:
+ self.glob.baseClass.delAsset(selected.filename)
+ elif selected.status == 1:
+ self.glob.baseClass.addAndDisplayAsset(selected.filename, eqtype, multi)
+
+ def animCallback(self, selected, eqtype, multi):
+ self.glob.project_changed = True
+ if eqtype == "rigs":
+ if selected.status == 0:
+ self.glob.baseClass.delSkeleton(selected.filename)
+ else:
+ self.glob.baseClass.modSkeleton(selected.name, selected.filename)
+ elif eqtype == "poses":
+ if selected.status == 0:
+ self.glob.baseClass.delPose(selected.filename)
+ else:
+ if not self.glob.baseClass.addPose(selected.name, selected.filename):
+ ErrorBox(self.central_widget, self.env.last_error)
+ else:
+ self.glob.baseClass.corrections = None
+ self.graph.view.Tweak()
+ elif eqtype == "expressions":
+ if selected.status == 0:
+ self.glob.baseClass.delExpression(selected.filename)
+ else:
+ self.glob.baseClass.addExpression(selected.name, selected.filename)
+ self.graph.view.Tweak()
+
+ def buttonRow(self, subtool):
+ if len(subtool) == 0:
+ return None
+
+ row=QHBoxLayout()
+ row.setSpacing(0)
+ for n, b in enumerate(subtool):
+ if "h" not in b or b["h"] is False:
+ row.addWidget(b["button"])
+ row.addStretch()
+
+ return row
+
+ def markSelectedButtons(self, row, button):
+ sel = button["button"]
+ for elem in row:
+ b = elem["button"]
+ if b is not None:
+ b.setChecked(b == sel)
+ if self.glob.baseClass is None:
+ for elem in row[1:]:
+ b = elem["button"]
+ if b is not None:
+ b.setEnabled(False)
+ else:
+ for elem in row:
+ b = elem["button"]
+ if b is not None:
+ b.setEnabled(True)
+
+ def setCategoryIcons(self):
+ s = self.sender()
+ self.setToolModeAndPanel(s._funcid, 0)
+
+ def callCategory(self):
+ s = self.sender()
+ m = s._funcid -100
+ self.setToolModeAndPanel(m // 100, m % 100)
+
+ def createCentralWidget(self):
+ """
+ create central widget with 3 columns and resizable dividers
+ """
+ env = self.env
+ self.central_widget = QWidget()
+ self.glob.centralWidget = self.central_widget
+
+ # 1. Create the Horizontal Splitter and connect movement logic
+ self.mainSplitter = QSplitter(Qt.Horizontal)
+ self.mainSplitter.splitterMoved.connect(self.enforce_limits)
+
+ # --- LEFT COLUMN SETUP ---
+ leftColumnContainer = QWidget()
+ v2Layout = QVBoxLayout(leftColumnContainer)
+ v2Layout.setContentsMargins(0, 0, 0, 0)
+
+ rowgroup1 = QFrame()
+ rowgroup1.setObjectName("gboxseltools")
+ self.ToolSelBox = QVBoxLayout()
+ row = self.buttonRow(self.tool_buttons)
+ self.ToolSelBox.addLayout(row)
+ rowgroup1.setLayout(self.ToolSelBox)
+
+ rowgroup2 = QFrame()
+ rowgroup2.setObjectName("gboxnontitle")
+ self.ButtonBox = QVBoxLayout()
+ # Correctly index based on current mode
+ self.CategoryBox = self.buttonRow(self.category_buttons[self.tool_mode])
+ self.ButtonBox.addLayout(self.CategoryBox)
+ rowgroup2.setLayout(self.ButtonBox)
+
+ self.LeftBox = QVBoxLayout()
+ self.leftColumn = MHGroupBox("Base")
+ self.leftColumn.setMinimumWidth(300)
+ self.drawLeftPanel()
+
+ v2Layout.addWidget(rowgroup1, 0)
+ v2Layout.addWidget(rowgroup2, 0)
+ v2Layout.addLayout(self.leftColumn.MHLayout(self.LeftBox), 1)
+
+ # --- MIDDLE COLUMN (VIEWPORT) ---
+ self.glob.midColumn = self.graph = MHGraphicWindow(self.glob)
+ gLayout = self.graph.createLayout()
+
+ self.eventFilter = NavigationEvent(self.graph)
+ self.installEventFilter(self.eventFilter)
+
+ frame = MHGroupBox("Viewport")
+ middleWidget = QWidget()
+ middleWidget.setLayout(frame.MHLayout(gLayout))
+
+ # --- RIGHT COLUMN ---
+ self.visRightColumn = QWidget()
+ self.ToolBox = QVBoxLayout()
+ vis = self.drawRightPanel()
+ self.visRightColumn.setVisible(vis)
+ self.rightColumn = MHGroupBox("Default")
+ self.rightColumn.setMinimumWidth(400)
+ self.visRightColumn.setLayout(self.rightColumn.MHLayout(self.ToolBox))
+
+ # 2. Add the three columns to the Splitter
+ self.mainSplitter.addWidget(leftColumnContainer)
+ self.mainSplitter.addWidget(middleWidget)
+ self.mainSplitter.addWidget(self.visRightColumn)
+
+ # Logical Stops: Prevent panels from collapsing to zero
+ self.mainSplitter.setCollapsible(0, False)
+ self.mainSplitter.setCollapsible(1, False)
+ self.mainSplitter.setCollapsible(2, False)
+
+ # 3. Set Stretch Factors (Center column expands)
+ self.mainSplitter.setStretchFactor(0, 0)
+ self.mainSplitter.setStretchFactor(1, 1)
+ self.mainSplitter.setStretchFactor(2, 0)
+
+ # 4. Final Layout Assembly
+ layout = QHBoxLayout(self.central_widget)
+ layout.setContentsMargins(2, 3, 2, 3)
+ layout.addWidget(self.mainSplitter)
+
+ # Set initial widths after UI initializes to avoid layout break
+ QTimer.singleShot(0, self.setInitialSplitterSizes)
+
+ self.setCentralWidget(self.central_widget)
+
+ def setInitialSplitterSizes(self):
+ """Sets widths after startup: [Left, Middle, Right]"""
+ self.mainSplitter.setSizes([350, 800, 350])
+
+ def enforce_limits(self, pos, index):
+ """
+ Prevents panels from crossing 50% width
+ index 1 = Left divider | index 2 = Right divider
+ """
+ half_way = self.width() // 2
+
+ # Handle the Left Divider (Handle 1)
+ if index == 1 and pos > half_way:
+ self.mainSplitter.moveSplitter(half_way, index)
+
+ # Handle the Right Divider (Handle 2)
+ # pos is the absolute position from the left edge of the window
+ if index == 2 and pos < half_way:
+ self.mainSplitter.moveSplitter(half_way, index)
+
+
+ def drawLeftPanel(self):
+ """
+ draw left panel
+ """
+ env = self.env
+
+ # extra code for no basemesh selected
+ #
+ if (self.tool_mode == 0 and self.category_mode == 0) or env.basename is None:
+ self.leftColumn.setTitle("Base mesh :: selection")
+ self.baseSelector = BaseSelect(self, self.selectmesh_call)
+ self.LeftBox.addLayout(self.baseSelector)
+ self.LeftBox.addStretch()
+ return
+
+ if self.tool_mode == 0:
+ if self.category_mode == 1:
+ self.lastForm = self.skinselect
+ self.leftColumn.setTitle("Select skin :: parameters")
+ layout = self.skinselect.leftPanel()
+ self.LeftBox.addLayout(layout)
+ elif self.category_mode == 2:
+ extra = ["Load complete character", "Load only targets", "Load only head-targets"]
+ self.leftColumn.setTitle("Load file :: filter")
+ layout = self.charselect.leftPanel(extra)
+ self.LeftBox.addLayout(layout)
+ elif self.category_mode == 3:
+ self.leftColumn.setTitle("Save file :: parameters")
+ self.saveForm = SaveMHMForm(self, self.graph.view, self.charselect, self.setWindowTitle)
+ self.LeftBox.addLayout(self.saveForm)
+ elif self.category_mode == 4:
+ self.leftColumn.setTitle("Export file :: parameters")
+ self.lastForm = self.exportForm = ExportLeftPanel(self)
+ self.LeftBox.addLayout(self.exportForm)
+ elif self.category_mode == 5:
+ self.leftColumn.setTitle("Import file :: parameters")
+ dlform = DownLoadImport(self, self.graph.view, self.setWindowTitle)
+ self.LeftBox.addLayout(dlform)
+ self.LeftBox.addStretch()
+ return
+
+
+ if self.tool_mode == 1:
+ if self.glob.targetCategories is None:
+ self.env.logLine(1, self.env.last_error )
+ return
+ if self.category_mode == 0:
+ self.leftColumn.setTitle("Modify character :: categories")
+ self.qTree = MHTreeView(self.glob.targetCategories, "Modelling", self.redrawNewCategory, None)
+ self.qtreefilter = self.qTree.getStartPattern()
+ self.LeftBox.addWidget(self.qTree)
+ row = self.buttonRow(self.model_buttons)
+ self.LeftBox.addLayout(row)
+ return
+ else:
+ self.leftColumn.setTitle("Random character :: parameters")
+ self.randForm = RandomForm(self, self.graph.view)
+ self.LeftBox.addLayout(self.randForm)
+ return
+
+ elif self.tool_mode == 2:
+ self.lastForm = self.equipment[self.category_mode]["func"]
+ self.leftColumn.setTitle("Character equipment :: filter")
+ layout = self.lastForm.leftPanel()
+ self.LeftBox.addLayout(layout)
+
+ elif self.tool_mode == 3:
+ if self.glob.baseClass.pose_skeleton is None:
+ ErrorBox(self.central_widget, "no poseskeleton added")
+ return
+ if self.category_mode == 0:
+ self.leftColumn.setTitle("Rigs :: filter")
+ layout = self.animation[self.category_mode]["func"].leftPanel()
+ self.LeftBox.addLayout(layout)
+ elif self.category_mode == 1:
+ self.leftColumn.setTitle("Poses :: filter")
+ self.lastForm = AnimMode(self.glob)
+ layout = self.animation[self.category_mode]["func"].leftPanel()
+ self.LeftBox.addLayout(layout)
+ elif self.category_mode == 2:
+ self.leftColumn.setTitle("Animation Player")
+ self.lastForm = AnimPlayer(self.glob)
+ self.lastForm.enter()
+ self.LeftBox.addLayout(self.lastForm)
+ self.LeftBox.addStretch()
+ elif self.category_mode == 3:
+ self.leftColumn.setTitle("Expressions :: filter")
+ self.lastForm = AnimMode(self.glob)
+ layout = self.animation[self.category_mode]["func"].leftPanel()
+ self.LeftBox.addLayout(layout)
+ elif self.category_mode == 4:
+ self.leftColumn.setTitle("Expressions :: editor")
+ self.lastForm = AnimExpressionEdit(self, self.glob)
+ units = self.glob.baseClass.getFaceUnits()
+ filterparam = units.createFilterDict() if units is not None else {}
+ self.qTree = MHTreeView(filterparam, "Categories", self.redrawNewExpression, None)
+ self.qtreefilter = self.qTree.getStartPattern()
+ self.LeftBox.addWidget(self.qTree)
+ layout = self.lastForm.addClassWidgets()
+ self.LeftBox.addLayout(layout)
+ else:
+ self.leftColumn.setTitle("Pose :: editor")
+ self.lastForm = AnimPoseEdit(self, self.glob)
+ units = self.glob.baseClass.getBodyUnits()
+ filterparam = units.createFilterDict() if units is not None else {}
+ self.qTree = MHTreeView(filterparam, "Categories", self.redrawNewPose, None)
+ self.qtreefilter = self.qTree.getStartPattern()
+ self.LeftBox.addWidget(self.qTree)
+ layout = self.lastForm.addClassWidgets()
+ self.LeftBox.addLayout(layout)
+
+ elif self.tool_mode == 4:
+ self.leftColumn.setTitle("Rendering :: parameters")
+ self.lastForm = Renderer(self, self.glob)
+ self.lastForm.enter()
+ self.LeftBox.addLayout(self.lastForm)
+ self.LeftBox.addStretch()
+ else:
+ self.leftColumn.setTitle("Not yet implemented") # not reached
+
+
+ def drawExpressionPanel(self, text="None"):
+ if text == "None":
+ text = self.qTree.getLastHeadline()
+ self.rightColumn.setTitle("Expressions, category: " + text)
+ widget = QWidget()
+ sweep = os.path.join(self.glob.env.path_sysicon, "sweep.png")
+ if self.lastForm is not None:
+ expressions = self.lastForm.fillExpressions()
+ self.exprArray = ScaleComboArray(widget, expressions, self.qtreefilter, sweep)
+ widget.setLayout(self.exprArray.layout)
+ scrollArea = QScrollArea()
+ scrollArea.setWidget(widget)
+ scrollArea.setWidgetResizable(True)
+ self.ToolBox.addWidget(scrollArea)
+
+ def drawPosePanel(self, text="None"):
+ if text == "None":
+ text = self.qTree.getLastHeadline()
+ self.rightColumn.setTitle("Poses, category: " + text)
+ widget = QWidget()
+ sweep = os.path.join(self.glob.env.path_sysicon, "sweep.png")
+ if self.lastForm is not None:
+ poses = self.lastForm.fillPoses()
+ self.poseArray = ScaleComboArray(widget, poses, self.qtreefilter, sweep)
+ widget.setLayout(self.poseArray.layout)
+ scrollArea = QScrollArea()
+ scrollArea.setWidget(widget)
+ scrollArea.setWidgetResizable(True)
+ self.ToolBox.addWidget(scrollArea)
+
+ def drawMorphPanel(self, text="None"):
+ if text == "None":
+ text = self.qTree.getLastHeadline()
+ self.rightColumn.setTitle("Morph, category: " + text)
+ if self.glob.Targets is not None:
+ widget = QWidget()
+ sweep = os.path.join(self.glob.env.path_sysicon, "sweep.png")
+ self.scalerArray = ScaleComboArray(widget, self.glob.Targets.modelling_targets, self.qtreefilter, sweep)
+ widget.setLayout(self.scalerArray.layout)
+ scrollArea = QScrollArea()
+ scrollArea.setWidget(widget)
+ scrollArea.setWidgetResizable(True)
+ self.ToolBox.addWidget(scrollArea)
+
+ def drawImageSelector(self, category, text, buttonmask=3):
+ self.rightColumn.setTitle(text)
+ layout = category.rightPanel(buttonmask)
+ self.ToolBox.addLayout(layout)
+
+ def drawExportPanel(self, connector, text):
+ self.rightColumn.setTitle(text)
+ layout = ExportRightPanel(self, connector)
+ self.ToolBox.addLayout(layout)
+
+ def drawRightPanel(self, text="None"):
+ """
+ create panel for right column according to tool_mode and category_mode
+ :param text: headline
+ :returns: True (to be generated) else False
+ """
+ if self.glob.baseClass is None:
+ return False
+
+ self.glob.openGLWindow.delMarker()
+
+ if self.tool_mode == 0:
+ if self.category_mode == 0:
+ if self.rightColumn is None:
+ return False
+ return False
+ elif self.category_mode == 1:
+ self.drawImageSelector(self.skinselect, "Base material: skin", 7)
+ elif self.category_mode == 2:
+ self.drawImageSelector(self.charselect, "Character MHM Files", 4)
+ elif self.category_mode == 3:
+ self.drawImageSelector(self.charselect, "Character MHM Files (select to replace file)", 0)
+ elif self.category_mode == 4:
+ self.drawExportPanel(self.exportForm, "Export character")
+ elif self.category_mode == 5:
+ return False
+ else:
+ return False
+ elif self.tool_mode == 1:
+ if self.category_mode == 0:
+ self.drawMorphPanel(text)
+ else:
+ return False
+ elif self.tool_mode == 2:
+ mask = 13 if self.category_mode == 7 else 15
+ equip = self.equipment[self.category_mode]
+ text = "Equipment, category: " + equip["name"]
+ self.drawImageSelector(equip["func"], text, mask)
+ elif self.tool_mode == 3:
+ if self.category_mode == 0 or self.category_mode == 1 or self.category_mode == 3:
+ equip = self.animation[self.category_mode]
+ text = "Pose and animation, category: " + equip["name"]
+ self.drawImageSelector(equip["func"], text, 13)
+ elif self.category_mode == 4:
+ self.drawExpressionPanel(text)
+ elif self.category_mode == 5:
+ self.drawPosePanel(text)
+ else:
+ return False
+ else:
+ return False
+ return True
+
+
+ def emptyLayout(self, layout):
+ if layout is not None and hasattr(layout,"count"):
+ #print("-- -- input layout: "+str(layout))
+ for i in reversed(range(layout.count())):
+ layoutItem = layout.itemAt(i)
+ if layoutItem.widget() is not None:
+ widgetToRemove = layoutItem.widget()
+ widgetToRemove.setParent(None)
+ layout.removeWidget(widgetToRemove)
+ #print("found widget: " + str(widgetToRemove))
+ elif layoutItem.spacerItem() is not None:
+ layout.removeItem(layoutItem)
+ else:
+ layoutToRemove = layout.itemAt(i)
+ self.emptyLayout(layoutToRemove)
+
+ def emptyAll(self):
+ self.emptyLayout(self.LeftBox)
+ self.emptyLayout(self.ToolBox)
+ self.emptyLayout(self.CategoryBox)
+ """
+ if self.LeftBox.isEmpty():
+ print ("LeftBox is empty")
+ else:
+ print ("LeftBox is NOT empty")
+ if self.ToolBox.isEmpty():
+ print ("ToolBox is empty")
+ else:
+ print ("ToolBox is NOT empty")
+ if self.CategoryBox is not None and self.CategoryBox.isEmpty():
+ print ("CategoryBox is empty")
+ else:
+ print ("CategoryBox is NOT empty")
+ """
+
+ def setToolModeAndPanel(self, tool, category):
+ if self.tool_mode != tool or self.category_mode != category:
+ if self.lastForm is not None:
+ self.lastForm.leave()
+ self.lastForm = None
+ """
+ self.emptyLayout(self.LeftBox)
+ self.emptyLayout(self.ToolBox)
+ self.emptyLayout(self.CategoryBox)
+ """
+ self.emptyAll()
+ self.tool_mode = tool
+ self.category_mode = category
+ self.markSelectedButtons(self.tool_buttons, self.tool_buttons[tool])
+ buttons = self.category_buttons[tool]
+ self.CategoryBox= self.buttonRow(buttons)
+ if self.CategoryBox is not None:
+ self.ButtonBox.insertLayout(0, self.CategoryBox)
+ self.markSelectedButtons(buttons, buttons[category])
+ self.drawLeftPanel()
+ vis = self.drawRightPanel()
+ self.visRightColumn.setVisible(vis)
+ else:
+ # refresh status
+ self.markSelectedButtons(self.tool_buttons, self.tool_buttons[tool])
+ buttons = self.category_buttons[tool]
+ if len(buttons) > 0:
+ self.markSelectedButtons(buttons, buttons[category])
+
+ def setPresets(self):
+ self.glob.guiPresets["Randomizer"] = RandomValues(self.glob)
+ self.glob.guiPresets["Animplayer"] = AnimPlayerValues(self.glob)
+ self.glob.guiPresets["Renderer"] = RendererValues(self.glob)
+ self.glob.guiPresets["Exporter"] = ExporterValues(self.glob)
+
+ def deb_cam(self):
+ self.graph.setDebug(self.sender().isChecked())
+
+ def closeEvent(self, event):
+ self.quit_call(event)
+
+ def base_call(self):
+ self.setToolModeAndPanel(0, 0)
+
+ def skin_call(self):
+ self.setToolModeAndPanel(0, 1)
+
+ def morph_call(self):
+ if self.glob.baseClass is not None:
+ self.setToolModeAndPanel(1, 0)
+
+ def random_call(self):
+ if self.glob.baseClass is not None:
+ self.setToolModeAndPanel(1, 1)
+
+ def equip_call(self):
+ s = self.sender()
+ for n, elem in enumerate(self.equipment):
+ if elem["menu"] == s:
+ self.setToolModeAndPanel(2, n)
+ break
+
+ def anim_call(self):
+ s = self.sender()
+ for n, elem in enumerate(self.animation):
+ if elem["menu"] == s:
+ self.setToolModeAndPanel(3, n)
+ break
+
+ # open sub-windows
+ #
+ def pref_call(self):
+ self.glob.showSubwindow("pref", self, MHPrefWindow)
+
+ def log_call(self):
+ self.glob.showSubwindow("log", self, MHLogWindow)
+
+ def memory_call(self):
+ self.glob.showSubwindow("memory", self, MHMemWindow)
+
+ def measure_call(self):
+ if self.glob.baseClass is not None:
+ self.glob.showSubwindow("measure", self, MHCharMeasWindow)
+
+ def info_call(self):
+ self.glob.showSubwindow("about", self.glob, MHInfoWindow)
+
+ def scene_call(self):
+ self.glob.showSubwindow("scene", self, MHSceneWindow)
+
+ #
+ def changesLost(self, text):
+ confirmed = 1
+ if self.glob.project_changed:
+ dbox = DialogBox(text + ": all recent changes will be lost.\nPress cancel to abort", QDialogButtonBox.Ok)
+ confirmed = dbox.exec()
+ return confirmed
+
+ def parallelLoad(self, bckproc, *args):
+ self.glob.baseClass.loadMHMFile(args[0][0], self.prog_window)
+ # self.prog_window.setLabelText(elem.folder + ": create binary " + os.path.split(elem.path)[1])
+
+ def finishLoad(self):
+ gl = self.graph.view
+ gl.setCameraCenter()
+ gl.addAssets()
+ gl.newSkin(self.glob.baseClass.baseMesh)
+ gl.scene.prepareSkeleton()
+ if self.prog_window is not None:
+ self.prog_window.progress.close()
+ self.prog_window = None
+ self.glob.openGLBlock = False
+ gl.scene.newFloorPosition()
+ gl.Tweak()
+ self.setWindowTitle(self.glob.baseClass.name)
+ self.graph.setSizeInfo()
+ self.glob.parallel = None
+
+ def newCharacter(self, filename, mode=0):
+ gl = self.graph.view
+ if filename is None:
+ self.glob.project_changed = False
+ return
+
+ if mode != 0:
+ print ("character only modified by targets")
+ self.glob.baseClass.loadMHMTargetsOnly(filename, mode)
+ gl.setCameraCenter()
+ gl.scene.prepareSkeleton()
+ gl.scene.newFloorPosition()
+ gl.Tweak()
+ self.graph.setSizeInfo()
+
+ elif self.glob.parallel is None:
+ self.setToolModeAndPanel(0, 0)
+ self.glob.openGLBlock = True
+ gl.noGLObjects(leavebase=True)
+ self.glob.textureRepo.cleanup()
+ self.glob.baseClass.reset()
+ self.glob.baseClass.baseMesh.initMaterial()
+ self.prog_window = MHBusyWindow("Load character", "start")
+ self.prog_window.progress.forceShow()
+ self.glob.parallel = WorkerThread(self.parallelLoad, filename)
+ self.glob.parallel.start()
+ self.glob.parallel.finished.connect(self.finishLoad)
+ gl.setCameraCenter()
+ self.glob.project_changed = False
+
+ def loadmhm_call(self):
+ if self.glob.baseClass is not None:
+ self.setToolModeAndPanel(0, 2)
+
+ def selectSkinCallback(self, asset, eqtype, multi):
+ self.glob.baseClass.setSkinMaterial(asset)
+
+ def loadByIconCallback(self, asset, eqtype, multi):
+
+ # in case of save add data into formular only
+ #
+ if self.category_mode == 3:
+ self.saveForm.addDataFromSelected(asset)
+ return
+
+ if asset.status != 1:
+ return
+ if self.changesLost("Load character"):
+ self.newCharacter(asset.filename, self.charselect.getExtraIndex())
+
+ def savemhm_call(self):
+ if self.glob.baseClass is not None:
+ self.setToolModeAndPanel(0, 3)
+
+ def exportmhm_call(self):
+ if self.glob.baseClass is not None:
+ self.setToolModeAndPanel(0, 4)
+
+ def download_call(self):
+ if self.glob.baseClass is not None:
+ self.setToolModeAndPanel(0, 5)
+
+ def initParams(self):
+ self.graph.getFocusText()
+
+ def reset_call(self):
+ if self.glob.Targets is not None:
+ if self.changesLost("Reset character"):
+ print ("Reset")
+ self.glob.Targets.reset(True)
+ self.glob.project_changed = False
+ self.redrawNewCategory(self.qtreefilter)
+ self.glob.baseClass.applyAllTargets()
+ self.graph.setSizeInfo()
+ self.graph.view.Tweak()
+
+ def symSwitch(self):
+ self.scalerArray.comboUnexpand()
+ v = not self.glob.Targets.getSym()
+ self.glob.Targets.setSym(v)
+ self.sender().setChecked(v)
+
+ def symLToR(self):
+ self.glob.Targets.makeSym(False)
+ self.glob.baseClass.parApplyTargets()
+ #self.graph.view.Tweak()
+
+ def symRToL(self):
+ self.glob.Targets.makeSym(True)
+ self.glob.baseClass.parApplyTargets()
+ #self.graph.view.Tweak()
+
+ def loadNewClass(self, basename):
+ """
+ :param str basename: the base name like 'hm08'
+ """
+ self.env.logLine(1, "New base class " + basename)
+
+ base = baseClass(self.glob, basename)
+ okay = base.prepareClass()
+ if not okay:
+ ErrorBox(self.central_widget, self.env.last_error)
+ return
+
+ self.glob.openGLBlock = True
+ self.graph.view.newMesh()
+ self.createImageSelection()
+ self.emptyLayout(self.ToolBox)
+ vis = self.drawRightPanel()
+ self.setPresets()
+ self.visRightColumn.setVisible(vis)
+
+ self.ToolBox.update()
+
+ self.emptyLayout(self.LeftBox)
+ self.drawLeftPanel()
+ self.LeftBox.update()
+ self.graph.setSizeInfo()
+
+ self.graph.update()
+ self.glob.openGLBlock = False
+ self.markSelectedButtons(self.tool_buttons, self.tool_buttons[0])
+ self.markSelectedButtons(self.category_buttons[0], self.category_buttons[0][0])
+
+ def selectmesh_call(self):
+ base = self.baseSelector.getSelectedItem()
+ if base is not None:
+ #
+ if base == self.env.basename:
+ return
+ if self.changesLost("New basemesh") == 0:
+ return
+ self.loadNewClass(base)
+
+ def redrawNewCategory(self, category, text=None):
+ self.qtreefilter, text = self.qTree.getValidCategory(category, text)
+ self.emptyLayout(self.ToolBox)
+ self.drawMorphPanel(text)
+ self.ToolBox.update()
+
+ def redrawNewExpression(self, category, text=None):
+ self.qtreefilter, text = self.qTree.getValidCategory(category, text)
+ self.emptyLayout(self.ToolBox)
+ self.drawExpressionPanel(text)
+ self.ToolBox.update()
+
+ def redrawNewPose(self, category, text=None):
+ self.qtreefilter, text = self.qTree.getValidCategory(category, text)
+ self.emptyLayout(self.ToolBox)
+ self.drawPosePanel(text)
+ self.ToolBox.update()
+
+ def finished_bckproc(self):
+ if self.prog_window is not None:
+ self.prog_window.progress.close()
+ self.prog_window = None
+ QMessageBox.information(self, "Done!", self.bckproc.finishmsg)
+ self.bckproc = None
+
+ def compress_systargets(self):
+ if self.glob.Targets is not None and self.bckproc is None:
+ self.prog_window = MHBusyWindow("System targets", "compressing ...")
+ self.prog_window.progress.forceShow()
+ self.bckproc = WorkerThread(self.glob.Targets.saveBinaryTargets, 1)
+ self.bckproc.finishmsg = "System targets had been compressed"
+ self.bckproc.start()
+ self.bckproc.finished.connect(self.finished_bckproc)
+
+ def compress_usertargets(self):
+ if self.glob.Targets is not None and self.bckproc is None:
+ self.prog_window = MHBusyWindow("User targets", "compressing ...")
+ self.prog_window.progress.forceShow()
+ self.bckproc = WorkerThread(self.glob.Targets.saveBinaryTargets, 2)
+ self.bckproc.finishmsg = "User targets had been compressed"
+ self.bckproc.start()
+ self.bckproc.finished.connect(self.finished_bckproc)
+
+ def compressObjs(self, bckproc, *args):
+ """
+ compresses assets (either objects or mhclo)
+ :param bck_proc: unused pointer to background process
+ :param args: [0][0] True = system, False user
+ """
+ system = args[0][0]
+ force = args[0][1]
+ bc = self.glob.baseClass
+
+ # first compress base itself
+ #
+ syspath = bc.baseMesh.filename.startswith(self.env.path_sysdata)
+ if syspath == system:
+ (okay, err) = bc.baseMesh.exportBinary()
+ if not okay:
+ bckproc.finishmsg = err
+ return
+
+ elems_compressed = 0
+ elems_untouched = 0
+ for elem in self.glob.cachedInfo:
+
+ if elem.folder in ["clothes", "eyebrows", "eyelashes", "eyes", "hair", "proxy", "teeth", "tongue"]:
+ syspath = elem.path.startswith(self.env.path_sysdata)
+ if syspath == system:
+ okay = False
+ if force or self.env.isSourceFileNewer(elem.mhbin_file, elem.path):
+ self.prog_window.setLabelText(elem.folder + ": create binary " + os.path.split(elem.path)[1])
+
+ attach = attachedAsset(self.glob, elem.folder)
+ (okay, err) = attach.mhcloToMHBin(elem.path)
+ if not okay:
+ bckproc.finishmsg = err
+ return
+ elems_compressed += 1
+ else:
+ elems_untouched += 1
+
+ bckproc.finishmsg = "Binaries created: " + str(elems_compressed) + "\nEntries up-to-date before: " + str(elems_untouched)
+ return
+
+ def compressObjsWorker(self, system, force):
+ if self.glob.baseClass is not None and self.bckproc is None:
+ objects = "System Objects" if system else "User Objects"
+ self.prog_window = MHBusyWindow(objects, "creating binaries ...")
+ self.prog_window.progress.forceShow()
+ self.bckproc = WorkerThread(self.compressObjs, system, force)
+ self.bckproc.start()
+ self.bckproc.finished.connect(self.finished_bckproc)
+
+ def compress_sys3dobjs(self):
+ self.compressObjsWorker(True, False)
+
+ def compress_user3dobjs(self):
+ self.compressObjsWorker(False, False)
+
+ def regenerate_sys3dobjs(self):
+ self.compressObjsWorker(True, True)
+
+ def regenerate_user3dobjs(self):
+ self.compressObjsWorker(False, True)
+
+ def exportUserDB(self):
+ if self.glob.baseClass is None:
+ return
+ directory = self.env.stdUserPath()
+ freq = MHFileRequest(self.glob, "Database export for backup", "Json-files (*.json)", directory, save=".json")
+ filename = freq.request()
+ if filename is not None:
+ if self.env.fileCache.exportUserInfo(filename):
+ QMessageBox.information(self.central_widget, "Done!", "User database exported as " + filename)
+ else:
+ ErrorBox(self.central_widget, self.env.last_error)
+
+ def importUserDB(self):
+ if self.glob.baseClass is None:
+ return
+ directory = self.env.stdUserPath()
+ freq = MHFileRequest(self.glob, "Database import to restore tags", "Json-files (*.json)", directory)
+ filename = freq.request()
+ if filename is not None:
+ if self.env.fileCache.importUserInfo(filename):
+ QMessageBox.information(self.central_widget, "Done!", "User database restored, please restart program.")
+ #self.glob.rescanAssets()
+ # TODO: better way?
+ else:
+ ErrorBox(self.central_widget, self.env.last_error)
+
+ def url_info_call(self):
+ """
+ open an URL
+ """
+ s = self.sender().text()
+ urlname = self.env.release_info["support_urls"][s]
+ if urlname in self.env.release_info:
+ QDesktopServices.openUrl(QUrl(self.env.release_info[urlname], QUrl.TolerantMode))
+
+ def lic_call(self):
+ """
+ open a text box with license
+ """
+ name = self.sender().text()
+ if name == "License":
+ licname = "makehuman_license.txt"
+ boxname = "MakeHuman License"
+ image = self.env.stdLogo()
+ elif name == "Credits":
+ licname = "credits.txt"
+ boxname = "Credits"
+ image = self.env.stdLogo()
+ else:
+ licname = name.lower() + "-license.txt"
+ boxname = name + " License"
+ image = None
+
+ text = self.env.convertToRichFile(os.path.join(self.env.path_sysdata, "licenses", licname))
+ TextBox(self, boxname, image, text)
+
+ def dimskel_call(self):
+ self.graph.view.scene.setDiamondSkeleton(self.sender().isChecked())
+
+ def floor_call(self):
+ self.graph.view.scene.setFloor(self.sender().isChecked())
+
+ def loglevel_call(self):
+ bit = self.sender().data()
+ if self.sender().isChecked():
+ self.env.setVerboseBit(bit)
+ else:
+ self.env.resetVerboseBit(bit)
+
+ def socket_call(self):
+ if self.sender().isChecked() and self.glob.apiSocket is None:
+ self.glob.apiSocket = apiSocket(self.glob)
+ self.glob.apiSocket.viewRedisplay.connect(self.socket_finish)
+ self.glob.apiSocket.start()
+ else:
+ if self.glob.apiSocket is not None:
+ self.glob.apiSocket.stopListening()
+ self.glob.apiSocket.wait()
+ self.glob.apiSocket = None
+
+ def socket_finish(self):
+ self.glob.Targets.setSkinDiffuseColor()
+ self.graph.view.Tweak()
+
+ def context_help(self, filename=None):
+ if filename:
+ path = os.path.join(self.env.path_sysdata, "help", "help-" + filename + ".txt")
+ else:
+ path = os.path.join(self.env.path_sysdata, "help",
+ "help-" + str(self.tool_mode) + "-" + str(self.category_mode) + ".txt")
+ try:
+ with open(path) as f:
+ text = f.read()
+ except IOError as e:
+ text = "Error: " + str(e)
+ TextBox(self, "Context Help", None, text, modal=False)
+
+ def descr_help(self):
+ self.context_help("description")
+
+ def nav_help(self):
+ self.context_help("navigation")
+
+ def fsys_help(self):
+ self.context_help("filesystem")
+
+ def vers_call(self):
+ text = "Numpy: " + ".".join([str(x) for x in self.env.numpy_version]) + "
" + \
+ "PyOpenGL: " + self.env.GL_Info + "
" + \
+ "PySide6: " + ".".join([str(x) for x in self.env.QT_Info["version"]])
+ image = self.env.stdLogo()
+ TextBox(self, "Used library versions", image, text)
+
+ def glinfo_call(self):
+ deb = GLDebug(self.env.osindex)
+ text = deb.getTextInfo()
+ image = self.env.stdLogo()
+ TextBox(self, "Local OpenGL Information", image, text)
+
+ def quit_call(self, event=None):
+ """
+ save session (if desired)
+ also make a check, when project was changed
+ """
+ if self.glob.closing is True:
+ return
+
+ confirmed = self.changesLost("Exit program")
+ if confirmed == 0:
+ if isinstance(event,QCloseEvent):
+ event.ignore()
+ print ("Close event")
+ return
+
+ if self.graph is not None:
+ self.graph.cleanUp()
+
+ if self.glob.closing is False:
+ self.glob.closing = True # avoid double call by closeAllWindows
+ s = self.env.session["mainwinsize"]
+
+ geom = self.screen().availableGeometry() # avoid bigger values than screen size when saving
+ s["w"] = min(self.width(), geom.width())
+ s["h"] = min(self.height(), geom.height())
+
+ self.env.saveSession()
+ if self.glob.apiSocket is not None:
+ self.glob.apiSocket.stopListening()
+ self.glob.apiSocket.wait()
+ self.env.cleanup()
+ self.glob.app.closeAllWindows()
+ self.glob.app.quit()
From 17210080128bb19b1be90f13cb91fb741488d97f Mon Sep 17 00:00:00 2001
From: Elvaerwyn <75058709+Licentyius@users.noreply.github.com>
Date: Sun, 10 May 2026 23:27:52 -0600
Subject: [PATCH 3/3] Changes to allow window resizing
Changes to allow resizing of main windows with limitations set up.