• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SPF-OST / pytrnsys_gui / 11482354319

23 Oct 2024 02:43PM UTC coverage: 67.591% (+0.7%) from 66.906%
11482354319

push

github

web-flow
Merge pull request #554 from SPF-OST/548-open-recent-projects

expanded on open recent functionality

144 of 197 new or added lines in 8 files covered. (73.1%)

5 existing lines in 2 files now uncovered.

10413 of 15406 relevant lines covered (67.59%)

0.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.19
/trnsysGUI/mainWindow.py
1
# type: ignore
2
# pylint: skip-file
3

4
import os
1✔
5
import pathlib as _pl
1✔
6
import shutil
1✔
7
import subprocess
1✔
8

9
import PyQt5.QtGui as _qtg
1✔
10
import PyQt5.QtWidgets as _qtw
1✔
11
import pytrnsys.utils.log as _ulog
1✔
12
import pytrnsys.utils.result as _res
1✔
13
import pytrnsys.utils.warnings as _warn
1✔
14
from PyQt5.QtWidgets import QAction
1✔
15

16
import trnsysGUI.configFileUpdater as _cfu
1✔
17
import trnsysGUI.diagram.export as _dexp
1✔
18
import trnsysGUI.loggingCallback as _lgcb
1✔
19
import trnsysGUI.menus.projectMenu.exportPlaceholders as _eph
1✔
20
from trnsysGUI import buildDck as buildDck
1✔
21
from trnsysGUI import constants
1✔
22
from trnsysGUI import images as _img
1✔
23
from trnsysGUI import project as _prj
1✔
24
from trnsysGUI import settings as _settings
1✔
25
from trnsysGUI import settingsDlg as _sdlg
1✔
26
from trnsysGUI import warningsAndErrors as _werrors
1✔
27
from trnsysGUI.BlockItem import BlockItem
1✔
28
from trnsysGUI.MassFlowVisualizer import MassFlowVisualizer
1✔
29
from trnsysGUI.ProcessMain import ProcessMain
1✔
30
from trnsysGUI.RunMain import RunMain
1✔
31
from trnsysGUI.common import cancelled as _ccl
1✔
32
from trnsysGUI.diagram import Editor as _de
1✔
33
from trnsysGUI.messageBox import MessageBox
1✔
34
from trnsysGUI.recentProjectsHandler import RecentProjectsHandler
1✔
35
from trnsysGUI.storageTank.widget import StorageTank
1✔
36

37

38
class MainWindow(_qtw.QMainWindow):
1✔
39
    def __init__(self, logger, project: _prj.Project, parent=None):
1✔
40
        super().__init__(parent)
1✔
41

42
        self.editor = None
1✔
43

44
        self.jsonPath = None
1✔
45
        self.logger = logger
1✔
46

47
        self.labelVisState = False
1✔
48
        self.calledByVisualizeMf = False
1✔
49
        self.currentFile = "Untitled"
1✔
50
        self.showBoxOnClose = True
1✔
51

52
        # Toolbar actions
53
        saveDiaAction = _qtw.QAction(_img.INBOX_PNG.icon(), "Save", self)
1✔
54
        saveDiaAction.triggered.connect(self.saveDia)
1✔
55

56
        loadDiaAction = _qtw.QAction(_img.OUTBOX_PNG.icon(), "Open", self)
1✔
57
        loadDiaAction.triggered.connect(self.loadDia)
1✔
58

59
        updateConfigAction = _qtw.QAction(_img.UPDATE_CONFIG_PNG.icon(), "Update run.config", self)
1✔
60
        updateConfigAction.triggered.connect(self.updateRun)
1✔
61

62
        runSimulationAction = _qtw.QAction(_img.RUN_SIMULATION_PNG.icon(), "Run simulation", self)
1✔
63
        runSimulationAction.triggered.connect(self.runSimulation)
1✔
64

65
        processSimulationAction = _qtw.QAction(_img.PROCESS_SIMULATION_PNG.icon(), "Process data", self)
1✔
66
        processSimulationAction.triggered.connect(self.processSimulation)
1✔
67

68
        deleteDiaAction = _qtw.QAction(_img.TRASH_PNG.icon(), "Delete diagram", self)
1✔
69
        deleteDiaAction.triggered.connect(self.deleteDia)
1✔
70

71
        zoomInAction = _qtw.QAction(_img.ZOOM_IN_PNG.icon(), "Zoom in", self)
1✔
72
        zoomInAction.triggered.connect(self.setZoomIn)
1✔
73

74
        zoomOutAction = _qtw.QAction(_img.ZOOM_OUT_PNG.icon(), "Zoom out", self)
1✔
75
        zoomOutAction.triggered.connect(self.setZoomOut)
1✔
76

77
        toggleConnLabels = _qtw.QAction(_img.LABEL_TOGGLE_PNG.icon(), "Toggle labels", self)
1✔
78
        toggleConnLabels.triggered.connect(self.toggleConnLabels)
1✔
79

80
        exportHydraulicsAction = _qtw.QAction(_img.EXPORT_HYDRAULICS_PNG.icon(), "Export hydraulic.ddck", self)
1✔
81
        exportHydraulicsAction.triggered.connect(self.exportHydraulicsDdck)
1✔
82

83
        exportHydCtrlAction = _qtw.QAction(
1✔
84
            _img.EXPORT_HYDRAULIC_CONTROL_PNG.icon(),
85
            "Export hydraulic_control.ddck",
86
            self,
87
        )
88
        exportHydCtrlAction.triggered.connect(self.exportHydraulicControl)
1✔
89

90
        exportDckAction = _qtw.QAction(_img.EXPORT_DCK_PNG.icon(), "Export dck", self)
1✔
91
        exportDckAction.triggered.connect(self.exportDck)
1✔
92

93
        toggleSnapAction = _qtw.QAction("Toggle snap grid", self)
1✔
94
        toggleSnapAction.triggered.connect(self.toggleSnap)
1✔
95
        toggleSnapAction.setShortcut("a")
1✔
96

97
        toggleAlignModeAction = _qtw.QAction("Toggle align mode", self)
1✔
98
        toggleAlignModeAction.triggered.connect(self.toggleAlignMode)
1✔
99
        toggleAlignModeAction.setShortcut("q")
1✔
100

101
        runMassflowSolverAction = _qtw.QAction(_img.RUN_MFS_PNG.icon(), "Run the massflow solver", self)
1✔
102
        runMassflowSolverAction.triggered.connect(self.runAndVisMf)
1✔
103

104
        openVisualizerAction = _qtw.QAction(_img.VIS_MFS_PNG.icon(), "Start visualization of mass flows", self)
1✔
105
        openVisualizerAction.triggered.connect(self.visualizeMf)
1✔
106

107
        # Tool bar
108
        tb = self.addToolBar("Main Toolbar...")
1✔
109
        tb.setObjectName("Toolbar")
1✔
110
        tb.addAction(saveDiaAction)
1✔
111
        tb.addAction(loadDiaAction)
1✔
112
        tb.addAction(zoomInAction)
1✔
113
        tb.addAction(zoomOutAction)
1✔
114
        tb.addAction(toggleConnLabels)
1✔
115
        tb.addAction(runMassflowSolverAction)
1✔
116
        tb.addAction(openVisualizerAction)
1✔
117
        tb.addAction(exportHydraulicsAction)
1✔
118
        tb.addAction(exportHydCtrlAction)
1✔
119
        tb.addAction(updateConfigAction)
1✔
120
        tb.addAction(exportDckAction)
1✔
121
        tb.addAction(runSimulationAction)
1✔
122
        tb.addAction(processSimulationAction)
1✔
123
        tb.addAction(deleteDiaAction)
1✔
124

125
        # Menu bar actions
126
        self.fileMenu = _qtw.QMenu("File")
1✔
127

128
        fileMenuNewAction = _qtw.QAction("New", self)
1✔
129
        fileMenuNewAction.triggered.connect(self.newDia)
1✔
130
        fileMenuNewAction.setShortcut("Ctrl+n")
1✔
131
        self.fileMenu.addAction(fileMenuNewAction)
1✔
132

133
        fileMenuOpenAction = _qtw.QAction("Open", self)
1✔
134
        fileMenuOpenAction.triggered.connect(self.openFile)
1✔
135
        fileMenuOpenAction.setShortcut("Ctrl+o")
1✔
136
        self.fileMenu.addAction(fileMenuOpenAction)
1✔
137

138
        self.recentProjectsMenu = _qtw.QMenu("Recent Projects")
1✔
139
        # ===============================================================================
140
        # Must be monospaced font to ensure paths align nicely.
141
        self.recentProjectsMenu.setFont(_qtg.QFont(constants.DEFAULT_MONOSPACED_FONT))
1✔
142
        # ===============================================================================
143
        self.recentProjectsMenu.triggered.connect(self.openRecentFile)
1✔
144
        self.fileMenu.addMenu(self.recentProjectsMenu)
1✔
145
        self.updateRecentFileMenu(project.jsonFilePath)
1✔
146

147
        fileMenuSaveAction = _qtw.QAction("Save", self)
1✔
148
        fileMenuSaveAction.triggered.connect(self.saveDia)
1✔
149
        fileMenuSaveAction.setShortcut("Ctrl+s")
1✔
150
        self.fileMenu.addAction(fileMenuSaveAction)
1✔
151

152
        fileMenuCopyToNewAction = _qtw.QAction("Copy to new folder", self)
1✔
153
        fileMenuCopyToNewAction.triggered.connect(self.copyToNew)
1✔
154
        self.fileMenu.addAction(fileMenuCopyToNewAction)
1✔
155

156
        exportDiagram = _qtw.QAction("Export diagram", self)
1✔
157
        exportDiagram.triggered.connect(self.exportDiagram)
1✔
158
        exportDiagram.setShortcut("Ctrl+e")
1✔
159
        self.fileMenu.addAction(exportDiagram)
1✔
160

161
        debugConnections = _qtw.QAction("Debug Conn", self)
1✔
162
        debugConnections.triggered.connect(self.debugConns)
1✔
163
        self.fileMenu.addAction(debugConnections)
1✔
164

165
        def askForSaveAndLoadSettings() -> None:
1✔
166
            self.askUserForSettingsValuesAndSave()
×
167
            self.loadTrnsysPath()
×
168

169
        setTransysPath = _qtw.QAction("Set TRNSYS path", self)
1✔
170
        setTransysPath.triggered.connect(askForSaveAndLoadSettings)
1✔
171
        self.fileMenu.addAction(setTransysPath)
1✔
172

173
        self.editMenu = _qtw.QMenu("Edit")
1✔
174
        self.editMenu.addAction(toggleSnapAction)
1✔
175
        self.editMenu.addAction(toggleAlignModeAction)
1✔
176

177
        runMassflowSolverActionMenu = _qtw.QAction("Run mass flow solver", self)
1✔
178
        runMassflowSolverActionMenu.triggered.connect(self.runAndVisMf)
1✔
179

180
        openVisualizerActionMenu = _qtw.QAction("Start mass flow visualizer", self)
1✔
181
        openVisualizerActionMenu.triggered.connect(self.visualizeMf)
1✔
182

183
        exportHydraulicsActionMenu = _qtw.QAction("Export hydraulic.ddck", self)
1✔
184
        exportHydraulicsActionMenu.triggered.connect(self.exportHydraulicsDdck)
1✔
185

186
        exportHydCtrlActionMenu = _qtw.QAction("Export hydraulic_control.ddck", self)
1✔
187
        exportHydCtrlActionMenu.triggered.connect(self.exportHydraulicControl)
1✔
188

189
        exportDdckPlaceHolderValuesJsonFileActionMenu = _qtw.QAction("Export ddck placeholder values JSON file", self)
1✔
190
        exportDdckPlaceHolderValuesJsonFileActionMenu.triggered.connect(self.exportDdckPlaceHolderValuesJson)
1✔
191

192
        updateConfigActionMenu = _qtw.QAction("Update run.config", self)
1✔
193
        updateConfigActionMenu.triggered.connect(self.updateRun)
1✔
194

195
        exportDckActionMenu = _qtw.QAction("Export dck", self)
1✔
196
        exportDckActionMenu.triggered.connect(self.exportDck)
1✔
197

198
        runSimulationActionMenu = _qtw.QAction("Run simulation...", self)
1✔
199
        runSimulationActionMenu.triggered.connect(self.runSimulation)
1✔
200

201
        processSimulationActionMenu = _qtw.QAction("Process simulation...", self)
1✔
202
        processSimulationActionMenu.triggered.connect(self.processSimulation)
1✔
203

204
        self.projectMenu = _qtw.QMenu("Project")
1✔
205
        self.projectMenu.addAction(runMassflowSolverActionMenu)
1✔
206
        self.projectMenu.addAction(openVisualizerActionMenu)
1✔
207
        self.projectMenu.addAction(exportHydraulicsActionMenu)
1✔
208
        self.projectMenu.addAction(exportHydCtrlActionMenu)
1✔
209
        self.projectMenu.addAction(exportDdckPlaceHolderValuesJsonFileActionMenu)
1✔
210
        self.projectMenu.addAction(updateConfigActionMenu)
1✔
211
        self.projectMenu.addAction(exportDckActionMenu)
1✔
212
        self.projectMenu.addAction(runSimulationActionMenu)
1✔
213
        self.projectMenu.addAction(processSimulationActionMenu)
1✔
214

215
        pytrnsysOnlineDocAction = _qtw.QAction("pytrnsys online documentation", self)
1✔
216
        pytrnsysOnlineDocAction.triggered.connect(self.openPytrnsysOnlineDoc)
1✔
217

218
        self.helpMenu = _qtw.QMenu("Help")
1✔
219
        self.helpMenu.addAction(pytrnsysOnlineDocAction)
1✔
220

221
        # Menu bar
222
        self.mb = self.menuBar()
1✔
223
        self.mb.addMenu(self.fileMenu)
1✔
224
        self.mb.addMenu(self.projectMenu)
1✔
225
        self.mb.addMenu(self.editMenu)
1✔
226
        self.mb.addMenu(self.helpMenu)
1✔
227
        self.mb.addSeparator()
1✔
228

229
        # Status bar
230
        self.sb = self.statusBar()
1✔
231

232
        # QUndo framework
233
        self.undoStack = _qtw.QUndoStack(self)
1✔
234
        undoAction = self.undoStack.createUndoAction(self, "Undo")
1✔
235
        undoAction.setShortcut("Ctrl+z")
1✔
236

237
        redoAction = self.undoStack.createRedoAction(self, "Redo")
1✔
238
        redoAction.setShortcut("Ctrl+y")
1✔
239

240
        self.editMenu.addAction(undoAction)
1✔
241
        self.editMenu.addAction(redoAction)
1✔
242

243
        self._resetEditor(project)
1✔
244

245
    def isRunning(self):
1✔
246
        return self.editor.isRunning()
×
247

248
    def start(self) -> None:
1✔
249
        _lgcb.configureLoggingCallback(self.logger, self._loggingCallback, _ulog.FORMAT)
×
250
        self.editor.start()
×
251

252
    def shutdown(self) -> None:
1✔
253
        _lgcb.removeLoggingCallback(self.logger, self._loggingCallback)
×
254
        self.editor.shutdown()
×
255

256
    def newDia(self):
1✔
NEW
257
        if MessageBox.create(messageText=constants.UNSAVED_PROGRESS_LOST) == _qtw.QMessageBox.Cancel:
×
258
            return
×
259

260
        startingDirectoryPath = self._projectDirPath.parent
×
261

262
        createProjectMaybeCancelled = _prj.getCreateProject(startingDirectoryPath)
×
263
        if _ccl.isCancelled(createProjectMaybeCancelled):
×
264
            return
×
265

266
        createProject = _ccl.value(createProjectMaybeCancelled)
×
267

NEW
268
        self.updateRecentFileMenu(createProject.jsonFilePath)
×
UNCOV
269
        self._resetEditor(createProject)
×
270

271
    def saveDia(self):
1✔
272
        self.logger.info("Saving diagram")
×
NEW
273
        self.editor.saveProject()
×
274

275
    def copyToNew(self):
1✔
276
        currentProjectFolderPath = self._projectDirPath
×
277

278
        startingDirectoryPath = currentProjectFolderPath.parent
×
279

280
        maybeCancelled = _prj.getExistingEmptyDirectory(startingDirectoryPath)
×
281
        if _ccl.isCancelled(maybeCancelled):
×
282
            return
×
283
        newProjectFolderPath = _ccl.value(maybeCancelled)
×
284

285
        oldProjectFolderPath = self._projectDirPath
×
286

287
        self.copyContentsToNewFolder(newProjectFolderPath, oldProjectFolderPath)
×
288

289
    def copyContentsToNewFolder(self, newProjectFolderPath, oldProjectFolderPath):
1✔
290
        self._copyContents(oldProjectFolderPath, newProjectFolderPath)
1✔
291
        newJsonFilePath = self._changeAndGetNewJsonFileName(newProjectFolderPath, oldProjectFolderPath)
1✔
292

293
        loadProject = _prj.LoadProject(newJsonFilePath)
1✔
294

295
        self._resetEditor(loadProject)
1✔
296

297
        self.editor.saveProject(showWarning=False)
1✔
298

299
    @staticmethod
1✔
300
    def _copyContents(oldProjectFolderPath, newProjectFolderPath):
1✔
301
        for child in oldProjectFolderPath.iterdir():
1✔
302
            destinationPath = newProjectFolderPath / child.name
1✔
303
            if child.is_dir():
1✔
304
                shutil.copytree(child, destinationPath)
1✔
305
            else:
306
                shutil.copy(child, destinationPath)
1✔
307

308
    @staticmethod
1✔
309
    def _changeAndGetNewJsonFileName(newProjectFolderPath, oldProjectFolderPath):
1✔
310
        oldJsonFileName = f"{oldProjectFolderPath.name}.json"
1✔
311
        newJsonFileName = f"{newProjectFolderPath.name}.json"
1✔
312
        shutil.move(
1✔
313
            newProjectFolderPath / oldJsonFileName,
314
            newProjectFolderPath / newJsonFileName,
315
        )
316
        newJsonFilePath = newProjectFolderPath / newJsonFileName
1✔
317
        return newJsonFilePath
1✔
318

319
    def loadDia(self):
1✔
320
        self.logger.info("Loading diagram")
×
321
        self.openFile()
×
322

323
    def updateRun(self):
1✔
324
        configFilePath = self._projectDirPath / "run.config"
×
325

326
        if configFilePath.is_dir():
×
327
            _qtw.QMessageBox.information(
×
328
                self,
329
                "`run.config` is a directory",
330
                f"Could not create file {configFilePath} because a directory of the same name exists. "
331
                "Please remove or rename the directory before trying to update the `run.config` file.",
332
            )
333
            return
×
334

335
        configFilePath.touch()
×
336

337
        runConfig = _cfu.ConfigFileUpdater(configFilePath)
×
338
        runConfig.updateConfig()
×
339

340
    @property
1✔
341
    def _projectDirPath(self) -> _pl.Path:
1✔
342
        return _pl.Path(self.projectFolder)
1✔
343

344
    def runSimulation(self):
1✔
345
        ddckPath = os.path.join(self.projectFolder, "ddck")
×
346

347
        #   Check hydraulic.ddck
348
        hydraulicPath = os.path.join(ddckPath, "hydraulic\\hydraulic.ddck")
×
349
        if not os.path.isfile(hydraulicPath):
×
350
            self.exportHydraulicsDdck()
×
351

352
        #   Check ddcks of storage tanks
353
        storageWithoutFile = []
×
354
        for object in self.editor.trnsysObj:
×
355
            if isinstance(object, StorageTank):
×
356
                storageTankFile = os.path.join(object.displayName, object.displayName + ".ddck")
×
357
                storageTankPath = os.path.join(ddckPath, storageTankFile)
×
358
                if not (os.path.isfile(storageTankPath)):
×
359
                    storageWithoutFile.append(object.displayName + "\n")
×
360

361
        if storageWithoutFile:
×
362
            errorMessage = "The following storage tank(s) do(es) not have a corresponding ddck:\n\n"
×
363
            for storage in storageWithoutFile:
×
364
                errorMessage += storage
×
365
            errorMessage += (
×
366
                "\nPlease make sure you that you export the ddck for every storage tank before starting a simulation."
367
            )
368
            _werrors.showMessageBox(errorMessage)
×
369
            return
×
370

371
        #   Update run.config
372
        self.updateRun()
×
373

374
        #   Start simulation
375
        runApp = RunMain()
×
376
        executionFailed, errorStatement = runApp.runAction(self.logger, self.editor.projectFolder)
×
377

378
        if executionFailed:
×
379
            errorMessage = f"Exception while trying to execute RunParallelTrnsys:\n\n{errorStatement}"
×
380
            _werrors.showMessageBox(errorMessage)
×
381

382
        return
×
383

384
    def processSimulation(self):
1✔
385
        processPath = os.path.join(self.projectFolder, "process.config")
×
386
        if not os.path.isfile(processPath):
×
387
            errorMessage = f"No such file: {processPath}"
×
388
            _werrors.showMessageBox(errorMessage)
×
389
            return
×
390
        processApp = ProcessMain()
×
391
        result = processApp.processAction(self.logger, self.editor.projectFolder)
×
392

393
        if _res.isError(result):
×
394
            error = _res.error(result)
×
395
            _werrors.showMessageBox(error.message)
×
396

397
        return
×
398

399
    def exportDdckPlaceHolderValuesJson(self):
1✔
400
        result = _eph.exportDdckPlaceHolderValuesJsonFile(self.editor)
×
401
        if _res.isError(result):
×
402
            errorMessage = f"The json file could not be generated: {result.message}"
×
403
            _werrors.showMessageBox(errorMessage)
×
404

405
    def renameDia(self):
1✔
406
        self.logger.info("Renaming diagram...")
×
407
        self.editor.showDiagramDlg()
×
408

409
    def deleteDia(self):
1✔
410
        qmb = _qtw.QMessageBox()
×
411
        qmb.setText('Are you sure you want to delete the diagram? (There is no possibility to "undo".)')
×
412
        qmb.setStandardButtons(_qtw.QMessageBox.Yes | _qtw.QMessageBox.Cancel)
×
413
        qmb.setDefaultButton(_qtw.QMessageBox.Cancel)
×
414
        ret = qmb.exec()
×
415
        if ret == _qtw.QMessageBox.Yes:
×
416
            self.logger.info("Deleting diagram")
×
417
            self.editor.delBlocks()
×
418
        else:
419
            self.logger.info("Canceling")
×
420
            return
×
421

422
    def setZoomIn(self):
1✔
423
        self.logger.info("Setting zoom in")
×
424
        self.editor.diagramView.scale(1.2, 1.2)
×
425

426
    def setZoomOut(self):
1✔
427
        self.logger.info("Setting zoom out")
×
428
        self.editor.diagramView.scale(0.8, 0.8)
×
429

430
    def setZoom0(self):
1✔
431
        self.logger.info("Setting zoom 0")
×
432
        self.editor.diagramView.resetTransform()
×
433

434
    def runAndVisMf(self):
1✔
435
        self.calledByVisualizeMf = True
×
436
        filePaths = self.runMassflowSolver()
×
437
        if not filePaths:
×
438
            self.logger.error("Could not execute runMassflowSolver")
×
439
            return
×
440

441
        mfrFile, tempFile = filePaths
×
442
        if not os.path.isfile(mfrFile) or not os.path.isfile(tempFile):
×
443
            self.logger.error("No mfrFile or tempFile found!")
×
444
            return
×
445

446
        MassFlowVisualizer(self, mfrFile, tempFile)
×
447

448
    def visualizeMf(self):
1✔
449
        qmb = _qtw.QMessageBox()
×
450
        qmb.setText("Please select the mass flow rate prt-file that you want to visualize.")
×
451
        qmb.setStandardButtons(_qtw.QMessageBox.Ok)
×
452
        qmb.setDefaultButton(_qtw.QMessageBox.Ok)
×
453
        qmb.exec()
×
454

455
        mfrFile = _qtw.QFileDialog.getOpenFileName(self, "Open diagram", filter="*Mfr.prt")[0].replace("/", "\\")
×
456
        tempFile = mfrFile.replace("Mfr", "T")
×
457
        self.calledByVisualizeMf = True
×
458
        if os.path.isfile(mfrFile) and os.path.isfile(tempFile):
×
459
            MassFlowVisualizer(self, mfrFile, tempFile)
×
460
        else:
461
            self.logger.info("No mfrFile or tempFile found!")
×
462

463
    def openFile(self):
1✔
464
        self.logger.info("Opening diagram")
×
465

NEW
466
        if MessageBox.create(messageText=constants.UNSAVED_PROGRESS_LOST) == _qtw.QMessageBox.Cancel:
×
467
            return
×
468

469
        maybeCancelled = _prj.getLoadOrMigrateProject()
×
470
        if _ccl.isCancelled(maybeCancelled):
×
471
            return
×
472
        project = _ccl.value(maybeCancelled)
×
NEW
473
        RecentProjectsHandler.addProject(project.jsonFilePath)
×
NEW
474
        self.updateRecentFileMenu(project.jsonFilePath)
×
UNCOV
475
        self._resetEditor(project)
×
476

477
        if isinstance(project, _prj.MigrateProject):
×
NEW
478
            self.editor.saveProject()
×
479

480
    def openRecentFile(self, actionClicked: QAction):
1✔
481
        if MessageBox.create(messageText=constants.UNSAVED_PROGRESS_LOST) == _qtw.QMessageBox.Cancel:
1✔
482
            return
×
483

484
        maybeCancelled = _prj.loadRecentProject(actionClicked.data())
1✔
485
        if _ccl.isCancelled(maybeCancelled):
1✔
NEW
486
            self.recentProjectsMenu.removeAction(actionClicked)
×
487
            return
×
488

489
        project = _ccl.value(maybeCancelled)
1✔
490
        RecentProjectsHandler.addProject(project.jsonFilePath)
1✔
491
        self.updateRecentFileMenu(project.jsonFilePath)
1✔
492
        self._resetEditor(project)
1✔
493

494
    def _resetEditor(self, project):
1✔
495
        wasRunning = self.editor and self.editor.isRunning()
1✔
496

497
        self.undoStack.clear()
1✔
498

499
        self.editor = self._createDiagramEditor(project)
1✔
500
        self.setCentralWidget(self.editor)
1✔
501

502
        if wasRunning:
1✔
503
            self.editor.start()
×
504

505
    def toggleConnLabels(self):
1✔
506
        self.labelVisState = not self.labelVisState
×
507
        self.editor.setConnLabelVis(self.labelVisState)
×
508

509
    def exportHydraulicsDdck(self):
1✔
510
        self.editor.exportHydraulics(exportTo="ddck")
×
511

512
    def exportHydraulicControl(self):
1✔
513
        self.editor.exportHydraulicControl()
×
514

515
    def exportDck(self):
1✔
516
        jsonResult = _eph.exportDdckPlaceHolderValuesJsonFile(self.editor)
1✔
517
        if _res.isError(jsonResult):
1✔
518
            errorMessage = f"The placeholder values JSON file could not be generated: {jsonResult.message}"
×
519
            _werrors.showMessageBox(errorMessage)
×
520
            return
×
521

522
        builder = buildDck.DckBuilder(self._projectDirPath)
1✔
523

524
        result = builder.buildTrnsysDeck()
1✔
525
        if _res.isError(result):
1✔
526
            errorMessage = f"The deck file could not be generated: {result.message}"
×
527
            _werrors.showMessageBox(errorMessage)
×
528
            return
×
529
        warnings: _warn.ValueWithWarnings[str | None] = _res.value(result)
1✔
530

531
        if warnings.hasWarnings():
1✔
532
            warningMessage = warnings.toWarningMessage()
×
533
            _werrors.showMessageBox(warningMessage, _werrors.Title.WARNING)
×
534

535
    def toggleAlignMode(self):
1✔
536
        self.logger.info("Toggling alignMode")
×
537
        self.editor.alignMode = not self.editor.alignMode
×
538

539
    def toggleSnap(self):
1✔
540
        self.editor.toggleSnap()
×
541

542
    def runMassflowSolver(self):
1✔
543
        self.logger.info("Running massflow solver...")
×
544

545
        exportPath = self.editor.exportHydraulics(exportTo="mfs")
×
546
        if not exportPath:
×
547
            return None
×
548

549
        self.exportedTo = exportPath
×
550
        self.logger.info(exportPath)
×
551

552
        if not self.editor.trnsysPath.is_file():
×
553
            errorMessage = "TRNExe.exe not found! Consider correcting the path in the settings."
×
554
            _werrors.showMessageBox(errorMessage)
×
555
            return None
×
556

557
        try:
×
558
            subprocess.run([str(self.editor.trnsysPath), exportPath, "/H"], check=True)
×
559
            mfrFile = os.path.join(self.projectFolder, self.projectFolder.split("\\")[-1] + "_Mfr.prt")
×
560
            tempFile = os.path.join(self.projectFolder, self.projectFolder.split("\\")[-1] + "_T.prt")
×
561
            self.calledByVisualizeMf = False
×
562
            return mfrFile, tempFile
×
563
        except Exception as exception:
×
564
            errorMessage = f"An exception occurred while trying to execute the mass flow solver: {exception}"
×
565
            _werrors.showMessageBox(errorMessage)
×
566
            self.logger.error(errorMessage)
×
567

568
            return None
×
569

570
    def movePorts(self):
1✔
571
        self.editor.moveDirectPorts = True
×
572

573
    def openPytrnsysOnlineDoc(self):
1✔
574
        os.system('start "" https://pytrnsys.readthedocs.io')
×
575

576
    def exportDiagram(self):
1✔
577
        fileName, _ = _qtw.QFileDialog.getSaveFileName(
×
578
            self,
579
            "Export PDF",
580
            None,
581
            "PDF files (*.pdf);;SVG files (*.svg);;All Files (*)",
582
            "PDF files (*.svg)",
583
        )
584
        if fileName == "":
×
585
            return
×
586

587
        if _dexp.getExtension(fileName) == "":
×
588
            fileName += ".pdf"
×
589

590
        _dexp.export(self.editor.diagramScene, fileName)
×
591

592
    def closeEvent(self, e):
1✔
593
        if self.showBoxOnClose:
1✔
NEW
594
            ret = MessageBox.create(
×
595
                messageText=constants.SAVE_BEFORE_CLOSE,
596
                buttons=[
597
                    _qtw.QMessageBox.Save,
598
                    _qtw.QMessageBox.Close,
599
                    _qtw.QMessageBox.Cancel,
600
                ],
601
                defaultButton=_qtw.QMessageBox.Cancel,
602
            )
603
            if ret == _qtw.QMessageBox.Cancel:
×
604
                e.ignore()
×
NEW
605
                return
×
NEW
606
            if ret == _qtw.QMessageBox.Save:
×
NEW
607
                self.editor.saveProject()
×
608

NEW
609
            RecentProjectsHandler.save()
×
UNCOV
610
            e.accept()
×
611

612
    def ensureSettingsExist(self):
1✔
NEW
613
        if not _settings.Settings.tryLoadOrNone():
×
614
            self.askUserForSettingsValuesAndSave()
×
615

616
    def askUserForSettingsValuesAndSave(self):
1✔
617
        newSettings = _sdlg.SettingsDlg.showDialogAndGetSettings(parent=self)
×
618
        while newSettings == _sdlg.CANCELLED:
×
619
            newSettings = _sdlg.SettingsDlg.showDialogAndGetSettings(parent=self)
×
620
        newSettings.save()
×
621

622
    def loadTrnsysPath(self):
1✔
623
        settings = _settings.Settings.load()
×
624
        self.editor.trnsysPath = _pl.Path(settings.trnsysBinaryPath)
×
625

626
    def debugConns(self):
1✔
627
        """
628
        Check each block items for error connections.
629
        Returns warning message if blockitem contains two input connections or two output connections
630
        """
631
        self.logger.info("trnsysObjs:", self.editor.trnsysObj)
×
632
        self.noErrorConns = True
×
633
        for o in self.editor.trnsysObj:
×
634
            self.logger.info(o)
×
635
            if isinstance(o, BlockItem) and len(o.outputs) == 1 and len(o.inputs) == 1:
×
636
                self.logger.info("Checking block connections", o.displayName)
×
637
                objInput = o.inputs[0]
×
638
                objOutput = o.outputs[0]
×
639
                connToInputToPort = objInput.connectionList[0].toPort
×
640
                connToOutputToPort = objOutput.connectionList[0].toPort
×
641
                connToInputFromPort = objInput.connectionList[0].fromPort
×
642
                connToOutputFromPort = objOutput.connectionList[0].fromPort
×
643
                connName1 = objInput.connectionList[0].displayName
×
644
                connName2 = objOutput.connectionList[0].displayName
×
645
                objName = o.displayName
×
646

647
                if objInput == connToInputToPort and objOutput == connToOutputToPort:
×
648
                    msgBox = _qtw.QMessageBox()
×
649
                    msgBox.setText("both %s and %s are input ports into %s" % (connName1, connName2, objName))
×
650
                    msgBox.exec_()
×
651
                    self.noErrorConns = False
×
652

653
                elif objInput == connToInputFromPort and objOutput == connToOutputFromPort:
×
654
                    msgBox = _qtw.QMessageBox()
×
655
                    msgBox.setText("both %s and %s are output ports from %s" % (connName1, connName2, objName))
×
656
                    msgBox.exec_()
×
657
                    self.noErrorConns = False
×
658
        return self.noErrorConns
×
659

660
    def _createDiagramEditor(self, project: _prj.Project) -> _de.Editor:
1✔
661
        if isinstance(project, _prj.LoadProject):
1✔
662
            self.projectFolder = str(project.jsonFilePath.parent)
1✔
663
            return _de.Editor(
1✔
664
                self,
665
                str(project.jsonFilePath.parent),
666
                jsonPath=None,
667
                loadValue="load",
668
                logger=self.logger,
669
            )
670

671
        if isinstance(project, _prj.MigrateProject):
×
672
            self.projectFolder = str(project.newProjectFolderPath)
×
673
            return _de.Editor(
×
674
                self,
675
                str(project.newProjectFolderPath),
676
                str(project.oldJsonFilePath),
677
                loadValue="json",
678
                logger=self.logger,
679
            )
680

681
        if isinstance(project, _prj.CreateProject):
×
682
            self.projectFolder = str(project.jsonFilePath.parent)
×
683
            return _de.Editor(
×
684
                self,
685
                self.projectFolder,
686
                jsonPath=str(project.jsonFilePath),
687
                loadValue="new",
688
                logger=self.logger,
689
            )
690

691
        raise AssertionError(f"Unknown `project' type: {type(project)}")
×
692

693
    def _loggingCallback(self, logMessage: str) -> None:
1✔
694
        self.editor.loggingTextEdit.appendPlainText(logMessage)
×
695

696
    def updateRecentFileMenu(self, currentProject: _pl.Path):
1✔
697
        self.recentProjectsMenu.clear()
1✔
698
        maxLength = RecentProjectsHandler.getLengthOfLongestFileName()
1✔
699
        for recentProject in RecentProjectsHandler.recentProjects:
1✔
700
            if recentProject != currentProject:
1✔
701
                formattedFileName = recentProject.stem.ljust(maxLength)
1✔
702
                recentProjectAction = _qtw.QAction(f"{formattedFileName} {recentProject}", self)
1✔
703
                recentProjectAction.setData(recentProject)
1✔
704
                self.recentProjectsMenu.addAction(recentProjectAction)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc