340 lines
12 KiB
Python
340 lines
12 KiB
Python
# ***************************************************************************
|
|
# * Copyright (c) 2017 Markus Hovorka <m.hovorka@live.de> *
|
|
# * *
|
|
# * This file is part of the FreeCAD CAx development system. *
|
|
# * *
|
|
# * This program is free software; you can redistribute it and/or modify *
|
|
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
# * as published by the Free Software Foundation; either version 2 of *
|
|
# * the License, or (at your option) any later version. *
|
|
# * for detail see the LICENCE text file. *
|
|
# * *
|
|
# * This program is distributed in the hope that it will be useful, *
|
|
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
# * GNU Library General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Library General Public *
|
|
# * License along with this program; if not, write to the Free Software *
|
|
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
# * USA *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
__title__ = "FreeCAD FEM solver job control task panel"
|
|
__author__ = "Markus Hovorka"
|
|
__url__ = "https://www.freecad.org"
|
|
|
|
## \addtogroup FEM
|
|
# @{
|
|
|
|
from PySide import QtCore
|
|
from PySide import QtGui
|
|
|
|
import FreeCADGui as Gui
|
|
|
|
import femsolver.report
|
|
import femsolver.run
|
|
|
|
|
|
_UPDATE_INTERVAL = 50
|
|
_REPORT_TITLE = "Run Report"
|
|
_REPORT_ERR = "Failed to run. Please try again after all of the following errors are resolved."
|
|
|
|
|
|
class ControlTaskPanel(QtCore.QObject):
|
|
|
|
machineChanged = QtCore.Signal(object)
|
|
machineStarted = QtCore.Signal(object)
|
|
machineStopped = QtCore.Signal(object)
|
|
machineStatusChanged = QtCore.Signal(str)
|
|
machineStatusCleared = QtCore.Signal()
|
|
machineTimeChanged = QtCore.Signal(float)
|
|
machineStateChanged = QtCore.Signal(float)
|
|
|
|
def __init__(self, machine):
|
|
super().__init__()
|
|
self.form = ControlWidget()
|
|
self._machine = None
|
|
|
|
# Timer that updates the duration indicator.
|
|
self._timer = QtCore.QTimer()
|
|
self._timer.setInterval(_UPDATE_INTERVAL)
|
|
self._timer.timeout.connect(self._timeProxy)
|
|
|
|
# Connect object to widget.
|
|
self.form.writeClicked.connect(self.write)
|
|
self.form.editClicked.connect(self.edit)
|
|
self.form.runClicked.connect(self.run)
|
|
self.form.abortClicked.connect(self.abort)
|
|
self.form.directoryChanged.connect(self.updateMachine)
|
|
|
|
# Seems that the task panel does not get destroyed. Disconnect
|
|
# as soon as the widget of the task panel gets destroyed.
|
|
self.form.destroyed.connect(self._disconnectMachine)
|
|
self.form.destroyed.connect(self._timer.stop)
|
|
# self.form.destroyed.connect(
|
|
# lambda: self.machineStatusChanged.disconnect(
|
|
# self.form.appendStatus))
|
|
|
|
# Connect all proxy signals.
|
|
self.machineStarted.connect(self._timer.start)
|
|
self.machineStarted.connect(self.form.updateState)
|
|
self.machineStopped.connect(self._timer.stop)
|
|
self.machineStopped.connect(self._displayReport)
|
|
self.machineStopped.connect(self.form.updateState)
|
|
self.machineStatusChanged.connect(self.form.appendStatus)
|
|
self.machineStatusCleared.connect(self.form.clearStatus)
|
|
self.machineTimeChanged.connect(self.form.setTime)
|
|
self.machineStateChanged.connect(lambda: self.form.updateState(self.machine))
|
|
self.machineChanged.connect(self._updateTimer)
|
|
|
|
# Set initial machine. Signal updates the widget.
|
|
self.machineChanged.connect(self.updateWidget)
|
|
self.form.destroyed.connect(lambda: self.machineChanged.disconnect(self.updateWidget))
|
|
|
|
self.machine = machine
|
|
|
|
@property
|
|
def machine(self):
|
|
return self._machine
|
|
|
|
@machine.setter
|
|
def machine(self, value):
|
|
self._connectMachine(value)
|
|
self._machine = value
|
|
self.machineChanged.emit(value)
|
|
|
|
@QtCore.Slot()
|
|
def write(self):
|
|
self.machine.reset()
|
|
self.machine.target = femsolver.run.PREPARE
|
|
self.machine.start()
|
|
|
|
@QtCore.Slot()
|
|
def run(self):
|
|
self.machine.reset(femsolver.run.SOLVE)
|
|
self.machine.target = femsolver.run.RESULTS
|
|
self.machine.start()
|
|
|
|
@QtCore.Slot()
|
|
def edit(self):
|
|
self.machine.reset(femsolver.run.SOLVE)
|
|
self.machine.solver.Proxy.edit(self.machine.directory)
|
|
|
|
@QtCore.Slot()
|
|
def abort(self):
|
|
self.machine.abort()
|
|
|
|
@QtCore.Slot()
|
|
def updateWidget(self):
|
|
self.form.setDirectory(self.machine.directory)
|
|
self.form.setStatus(self.machine.status)
|
|
self.form.setTime(self.machine.time)
|
|
self.form.updateState(self.machine)
|
|
|
|
@QtCore.Slot()
|
|
def updateMachine(self):
|
|
if self.form.directory() != self.machine.directory:
|
|
self.machine = femsolver.run.getMachine(self.machine.solver, self.form.directory())
|
|
|
|
@QtCore.Slot()
|
|
def _updateTimer(self):
|
|
if self.machine.running:
|
|
self._timer.start()
|
|
|
|
@QtCore.Slot(object)
|
|
def _displayReport(self, machine):
|
|
text = _REPORT_ERR if machine.failed else None
|
|
femsolver.report.display(machine.report, _REPORT_TITLE, text)
|
|
|
|
def getStandardButtons(self):
|
|
return QtGui.QDialogButtonBox.Close
|
|
|
|
def reject(self):
|
|
Gui.ActiveDocument.resetEdit()
|
|
|
|
def _connectMachine(self, machine):
|
|
self._disconnectMachine()
|
|
machine.signalStatus.add(self._statusProxy)
|
|
machine.signalStatusCleared.add(self._statusClearedProxy)
|
|
machine.signalStarted.add(self._startedProxy)
|
|
machine.signalStopped.add(self._stoppedProxy)
|
|
machine.signalState.add(self._stateProxy)
|
|
|
|
def _disconnectMachine(self):
|
|
if self.machine is not None:
|
|
self.machine.signalStatus.remove(self._statusProxy)
|
|
self.machine.signalStatusCleared.add(self._statusClearedProxy)
|
|
self.machine.signalStarted.remove(self._startedProxy)
|
|
self.machine.signalStopped.remove(self._stoppedProxy)
|
|
self.machine.signalState.remove(self._stateProxy)
|
|
|
|
def _startedProxy(self):
|
|
self.machineStarted.emit(self.machine)
|
|
|
|
def _stoppedProxy(self):
|
|
self.machineStopped.emit(self.machine)
|
|
|
|
def _statusProxy(self, line):
|
|
self.machineStatusChanged.emit(line)
|
|
|
|
def _statusClearedProxy(self):
|
|
self.machineStatusCleared.emit()
|
|
|
|
def _timeProxy(self):
|
|
time = self.machine.time
|
|
self.machineTimeChanged.emit(time)
|
|
|
|
def _stateProxy(self):
|
|
state = self.machine.state
|
|
self.machineStateChanged.emit(state)
|
|
|
|
|
|
class ControlWidget(QtGui.QWidget):
|
|
|
|
writeClicked = QtCore.Signal()
|
|
editClicked = QtCore.Signal()
|
|
runClicked = QtCore.Signal()
|
|
abortClicked = QtCore.Signal()
|
|
directoryChanged = QtCore.Signal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._setupUi()
|
|
self._inputFileName = ""
|
|
|
|
def _setupUi(self):
|
|
self.setWindowTitle(self.tr("Solver Control"))
|
|
# Working directory group box
|
|
self._directoryTxt = QtGui.QLineEdit()
|
|
self._directoryTxt.editingFinished.connect(self.directoryChanged)
|
|
directoryBtt = QtGui.QToolButton()
|
|
directoryBtt.setText("...")
|
|
directoryBtt.clicked.connect(self._selectDirectory)
|
|
directoryLyt = QtGui.QHBoxLayout()
|
|
directoryLyt.addWidget(self._directoryTxt)
|
|
directoryLyt.addWidget(directoryBtt)
|
|
self._directoryGrp = QtGui.QGroupBox()
|
|
self._directoryGrp.setTitle(self.tr("Working Directory"))
|
|
self._directoryGrp.setLayout(directoryLyt)
|
|
|
|
# Action buttons (Write, Edit, Run)
|
|
self._writeBtt = QtGui.QPushButton(self.tr("Write"))
|
|
self._editBtt = QtGui.QPushButton(self.tr("Edit"))
|
|
self._runBtt = QtGui.QPushButton()
|
|
self._writeBtt.clicked.connect(self.writeClicked)
|
|
self._editBtt.clicked.connect(self.editClicked)
|
|
actionLyt = QtGui.QGridLayout()
|
|
actionLyt.addWidget(self._writeBtt, 0, 0)
|
|
actionLyt.addWidget(self._editBtt, 0, 1)
|
|
actionLyt.addWidget(self._runBtt, 1, 0, 1, 2)
|
|
|
|
# Solver status log
|
|
self._statusEdt = QtGui.QPlainTextEdit()
|
|
self._statusEdt.setReadOnly(True)
|
|
# for the log we need a certain height
|
|
# set it so to almost match the size of the CCX solver panel
|
|
self._statusEdt.setMinimumHeight(300)
|
|
|
|
# Elapsed time indicator
|
|
timeHeaderLbl = QtGui.QLabel(self.tr("Elapsed Time:"))
|
|
self._timeLbl = QtGui.QLabel()
|
|
timeLyt = QtGui.QHBoxLayout()
|
|
timeLyt.addWidget(timeHeaderLbl)
|
|
timeLyt.addWidget(self._timeLbl)
|
|
timeLyt.addStretch()
|
|
timeLyt.setContentsMargins(0, 0, 0, 0)
|
|
self._timeWid = QtGui.QWidget()
|
|
self._timeWid.setLayout(timeLyt)
|
|
|
|
# Main layout
|
|
layout = QtGui.QVBoxLayout()
|
|
layout.addWidget(self._directoryGrp)
|
|
layout.addLayout(actionLyt)
|
|
layout.addWidget(self._statusEdt)
|
|
layout.addWidget(self._timeWid)
|
|
self.setLayout(layout)
|
|
|
|
@QtCore.Slot(str)
|
|
def setStatus(self, text):
|
|
if text is None:
|
|
text = ""
|
|
self._statusEdt.setPlainText(text)
|
|
self._statusEdt.moveCursor(QtGui.QTextCursor.End)
|
|
|
|
def status(self):
|
|
return self._statusEdt.plainText()
|
|
|
|
@QtCore.Slot(str)
|
|
def appendStatus(self, line):
|
|
self._statusEdt.moveCursor(QtGui.QTextCursor.End)
|
|
self._statusEdt.insertPlainText(line)
|
|
self._statusEdt.moveCursor(QtGui.QTextCursor.End)
|
|
|
|
@QtCore.Slot(str)
|
|
def clearStatus(self):
|
|
self._statusEdt.setPlainText("")
|
|
|
|
@QtCore.Slot(float)
|
|
def setTime(self, time):
|
|
timeStr = "<b>%05.1f</b>" % time if time is not None else ""
|
|
self._timeLbl.setText(timeStr)
|
|
|
|
def time(self):
|
|
if self._timeLbl.text() == "":
|
|
return None
|
|
return float(self._timeLbl.text())
|
|
|
|
@QtCore.Slot(float)
|
|
def setDirectory(self, directory):
|
|
self._directoryTxt.setText(directory)
|
|
|
|
def directory(self):
|
|
return self._directoryTxt.text()
|
|
|
|
@QtCore.Slot(int)
|
|
def updateState(self, machine):
|
|
if machine.state <= femsolver.run.PREPARE:
|
|
self._writeBtt.setText(self.tr("Write"))
|
|
self._editBtt.setText(self.tr("Edit"))
|
|
self._runBtt.setText(self.tr("Run"))
|
|
elif machine.state <= femsolver.run.SOLVE:
|
|
self._writeBtt.setText(self.tr("Re-write"))
|
|
self._editBtt.setText(self.tr("Edit"))
|
|
self._runBtt.setText(self.tr("Run"))
|
|
else:
|
|
self._writeBtt.setText(self.tr("Re-write"))
|
|
self._editBtt.setText(self.tr("Edit"))
|
|
self._runBtt.setText(self.tr("Re-run"))
|
|
if machine.running:
|
|
self._runBtt.setText(self.tr("Abort"))
|
|
self.setRunning(machine)
|
|
|
|
@QtCore.Slot()
|
|
def _selectDirectory(self):
|
|
path = QtGui.QFileDialog.getExistingDirectory(self)
|
|
self.setDirectory(path)
|
|
self.directoryChanged.emit()
|
|
|
|
def setRunning(self, machine):
|
|
if machine.running:
|
|
self._runBtt.clicked.connect(self.runClicked)
|
|
self._runBtt.clicked.disconnect()
|
|
self._runBtt.clicked.connect(self.abortClicked)
|
|
self._directoryGrp.setDisabled(True)
|
|
self._writeBtt.setDisabled(True)
|
|
self._editBtt.setDisabled(True)
|
|
else:
|
|
self._runBtt.clicked.connect(self.abortClicked)
|
|
self._runBtt.clicked.disconnect()
|
|
self._runBtt.clicked.connect(self.runClicked)
|
|
self._directoryGrp.setDisabled(False)
|
|
self._writeBtt.setDisabled(False)
|
|
self._editBtt.setDisabled(
|
|
not machine.solver.Proxy.editSupported() or machine.state <= femsolver.run.PREPARE
|
|
)
|
|
|
|
|
|
## @}
|