def __setup_vtk_widget(self): " setup the vtk widget " self.vtkwidget = VtkWidget(self.project, parent=self) self.ui.horizontalLayoutModelGraphics.addWidget(self.vtkwidget) # register with project manager self.project.register_widget( self.vtkwidget, ["xmin", "xlength", "ymin", "ylength", "zmin", "zlength", "imax", "jmax", "kmax"] )
class MfixGui(QtGui.QMainWindow): """ Main window class handling all gui interactions """ def __init__(self, app, parent=None): QtGui.QMainWindow.__init__(self, parent) # reference to qapp instance self.app = app # load ui file self.customWidgets = { "LineEdit": LineEdit, "CheckBox": CheckBox, "ComboBox": ComboBox, "DoubleSpinBox": DoubleSpinBox, "SpinBox": SpinBox, } self.ui = uic.loadUi(os.path.join("uifiles", "gui.ui"), self) # load settings self.settings = QSettings("MFIX", "MFIX") # set title and icon self.setWindowTitle("MFIX") self.setWindowIcon(get_icon("mfix.png")) # build keyword documentation from namelist docstrings self.keyword_doc = buildKeywordDoc(os.path.join(SCRIPT_DIRECTORY, os.pardir, "model")) self.project = ProjectManager(self, self.keyword_doc) # --- data --- self.modebuttondict = { "modeler": self.ui.pushButtonModeler, "workflow": self.ui.pushButtonWorkflow, "developer": self.ui.pushButtonDeveloper, } self.booleanbtndict = { "union": self.ui.toolbutton_geometry_union, "intersection": self.ui.toolbutton_geometry_intersect, "difference": self.ui.toolbutton_geometry_difference, } self.animation_speed = 400 self.animating = False self.stack_animation = None # --- icons --- # loop through all widgets, because I am lazy for widget in widget_iter(self.ui): if isinstance(widget, QtGui.QToolButton): name = str(widget.objectName()) if "add" in name: widget.setIcon(get_icon("add.png")) elif "delete" in name or "remove" in name: widget.setIcon(get_icon("remove.png")) elif "copy" in name: widget.setIcon(get_icon("copy.png")) self.ui.toolbutton_new.setIcon(get_icon("newfolder.png")) self.ui.toolbutton_open.setIcon(get_icon("openfolder.png")) self.ui.toolbutton_save.setIcon(get_icon("save.png")) self.ui.toolbutton_compile.setIcon(get_icon("build.png")) self.ui.toolbutton_run.setIcon(get_icon("play.png")) self.ui.toolbutton_restart.setIcon(get_icon("restart.png")) self.ui.toolbutton_interact.setIcon(get_icon("flash.png")) self.ui.toolbutton_add_geometry.setIcon(get_icon("geometry.png")) self.ui.toolbutton_add_filter.setIcon(get_icon("filter.png")) self.ui.toolbutton_geometry_union.setIcon(get_icon("union.png")) self.ui.toolbutton_geometry_intersect.setIcon(get_icon("intersect.png")) self.ui.toolbutton_geometry_difference.setIcon(get_icon("difference.png")) self.ui.toolButtonTFMSolidsDatabase.setIcon(get_icon("download.png")) # --- Connect Signals to Slots--- # open/save/new project self.ui.toolbutton_open.pressed.connect(self.open_project) self.ui.toolbutton_save.pressed.connect(self.save_project) # mode (modeler, workflow, developer) for mode, btn in self.modebuttondict.items(): btn.released.connect(make_callback(self.mode_changed, mode)) # navigation tree self.ui.treewidget_model_navigation.itemSelectionChanged.connect(self.navigation_changed) # build/run/connect MFIX self.ui.build_mfix_button.pressed.connect(self.build_mfix) self.ui.run_mfix_button.pressed.connect(self.run_mfix) self.ui.connect_mfix_button.pressed.connect(self.connect_mfix) self.ui.clear_output_button.pressed.connect(self.clear_output) # --- Threads --- self.build_thread = BuildThread(self) self.run_thread = RunThread(self) self.clear_thread = ClearThread(self) def make_handler(qtextbrowser): " make a closure to read stdout from external process " def handle_line(line): " closure to read stdout from external process " log = logging.getLogger(__name__) log.debug(str(line).strip()) cursor = qtextbrowser.textCursor() cursor.movePosition(cursor.End) cursor.insertText(line) qtextbrowser.ensureCursorVisible() return handle_line self.build_thread.line_printed.connect(make_handler(self.ui.command_output)) self.run_thread.line_printed.connect(make_handler(self.ui.command_output)) self.clear_thread.line_printed.connect(make_handler(self.ui.command_output)) # --- setup simple widgets --- self.__setup_simple_keyword_widgets() # --- vtk setup --- self.__setup_vtk_widget() # --- workflow setup --- if NodeWidget is not None: self.__setup_workflow_widget() # --- default --- self.mode_changed("modeler") self.change_pane("geometry") # autoload last project if self.get_project_dir(): self.open_project(self.get_project_dir()) def __setup_simple_keyword_widgets(self): """ Look for and connect simple keyword widgets to the project manager. Keyword informtation from the namelist doc strings is added to each keyword widget. The widget must be named: *_keyword_<keyword> where <keyword> is the actual keyword. For example: lineedit_keyword_run_name """ # loop through all widgets looking for *_keyword_<keyword> for widget in widget_iter(self.ui): name_list = str(widget.objectName()).lower().split("_") if "keyword" in name_list: keyword = "_".join(name_list[name_list.index("keyword") + 1 :]) # set the key attribute to the keyword widget.key = keyword # add info from keyword documentation if keyword in self.keyword_doc: widget.setdtype(self.keyword_doc[keyword]["dtype"]) if "required" in self.keyword_doc[keyword]: widget.setValInfo(req=self.keyword_doc[keyword]["required"] == "true") if "validrange" in self.keyword_doc[keyword]: if "max" in self.keyword_doc[keyword]["validrange"]: widget.setValInfo(_max=self.keyword_doc[keyword]["validrange"]["max"]) if "min" in self.keyword_doc[keyword]["validrange"]: widget.setValInfo(_min=self.keyword_doc[keyword]["validrange"]["min"]) if "initpython" in self.keyword_doc[keyword]: widget.default(self.keyword_doc[keyword]["initpython"]) if isinstance(widget, QtGui.QComboBox) and widget.count() < 1: widget.addItems(list(self.keyword_doc[keyword]["valids"].keys())) # register the widget with the project manager self.project.register_widget(widget, [keyword]) # connect to unsaved method widget.value_updated.connect(self.unsaved) def __setup_vtk_widget(self): " setup the vtk widget " self.vtkwidget = VtkWidget(self.project, parent=self) self.ui.horizontalLayoutModelGraphics.addWidget(self.vtkwidget) # register with project manager self.project.register_widget( self.vtkwidget, ["xmin", "xlength", "ymin", "ylength", "zmin", "zlength", "imax", "jmax", "kmax"] ) def __setup_workflow_widget(self): self.nodeChart = NodeWidget(showtoolbar=False) # Build defualt node library self.nodeChart.nodeLibrary.buildDefualtLibrary() self.ui.horizontalLayoutPyqtnode.addWidget(self.nodeChart) def get_project_dir(self): " get the current project directory" last_dir = self.settings.value("project_dir") if last_dir: return last_dir else: return None def mode_changed(self, mode): " change the Modeler, Workflow, Developer tab" current_index = 0 for i in range(self.ui.stackedwidget_mode.count()): widget = self.ui.stackedwidget_mode.widget(i) if mode == str(widget.objectName()): current_index = i break for key, btn in self.modebuttondict.items(): btn.setChecked(mode == key) self.animate_stacked_widget( self.ui.stackedwidget_mode, self.ui.stackedwidget_mode.currentIndex(), current_index, "horizontal" ) # --- modeler pane navigation --- def change_pane(self, name): """ change to the specified pane """ clist = self.ui.treewidget_model_navigation.findItems( name, QtCore.Qt.MatchContains | QtCore.Qt.MatchRecursive, 0 ) for item in clist: if str(item.text(0)).lower() == name.lower(): break self.ui.treewidget_model_navigation.setCurrentItem(item) self.navigation_changed() def navigation_changed(self): """ an item in the tree was selected, change panes """ current_selection = self.ui.treewidget_model_navigation.selectedItems() if current_selection: text = str(current_selection[-1].text(0)) text = "_".join(text.lower().split(" ")) current_index = 0 for i in range(self.ui.stackedWidgetTaskPane.count()): widget = self.ui.stackedWidgetTaskPane.widget(i) if text == str(widget.objectName()): current_index = i break self.animate_stacked_widget( self.ui.stackedWidgetTaskPane, self.ui.stackedWidgetTaskPane.currentIndex(), current_index ) # --- animation methods --- def animate_stacked_widget( self, stackedwidget, from_, to, direction="vertical", line=None, to_btn=None, btn_layout=None ): """ animate changing of qstackedwidget """ # check to see if already animating if self.animating and self.stack_animation is not None: self.stack_animation.stop() from_widget = stackedwidget.widget(from_) to_widget = stackedwidget.widget(to) # get from geometry width = from_widget.frameGeometry().width() height = from_widget.frameGeometry().height() # offset # bottom to top if direction == "vertical" and from_ < to: offsetx = 0 offsety = height # top to bottom elif direction == "vertical" and from_ > to: offsetx = 0 offsety = -height elif direction == "horizontal" and from_ < to: offsetx = width offsety = 0 elif direction == "horizontal" and from_ > to: offsetx = -width offsety = 0 else: return # move to widget and show # set the geometry of the next widget to_widget.setGeometry(0 + offsetx, 0 + offsety, width, height) to_widget.show() to_widget.raise_() # animate # from widget animnow = QtCore.QPropertyAnimation(from_widget, "pos") animnow.setDuration(self.animation_speed) animnow.setEasingCurve(QtCore.QEasingCurve.InOutQuint) animnow.setStartValue(QtCore.QPoint(0, 0)) animnow.setEndValue(QtCore.QPoint(0 - offsetx, 0 - offsety)) # to widget animnext = QtCore.QPropertyAnimation(to_widget, "pos") animnext.setDuration(self.animation_speed) animnext.setEasingCurve(QtCore.QEasingCurve.InOutQuint) animnext.setStartValue(QtCore.QPoint(0 + offsetx, 0 + offsety)) animnext.setEndValue(QtCore.QPoint(0, 0)) # line animline = None if line is not None and to_btn is not None: animline = QtCore.QPropertyAnimation(line, "pos") animline.setDuration(self.animation_speed) animline.setEasingCurve(QtCore.QEasingCurve.InOutQuint) animline.setStartValue(QtCore.QPoint(line.geometry().x(), line.geometry().y())) animline.setEndValue(QtCore.QPoint(to_btn.geometry().x(), line.geometry().y())) # animation group self.stack_animation = QtCore.QParallelAnimationGroup() self.stack_animation.addAnimation(animnow) self.stack_animation.addAnimation(animnext) if animline is not None: self.stack_animation.addAnimation(animline) self.stack_animation.finished.connect( make_callback(self.animate_stacked_widget_finished, stackedwidget, from_, to, btn_layout, line) ) self.stack_animation.stateChanged.connect( make_callback(self.animate_stacked_widget_finished, stackedwidget, from_, to, btn_layout, line) ) self.animating = True self.stack_animation.start() def animate_stacked_widget_finished(self, widget, from_, to, btn_layout=None, line=None): """ cleanup after animation """ if self.stack_animation.state() == QtCore.QAbstractAnimation.Stopped: widget.setCurrentIndex(to) from_widget = widget.widget(from_) from_widget.hide() from_widget.move(0, 0) if btn_layout is not None and line is not None: btn_layout.addItem(btn_layout.takeAt(btn_layout.indexOf(line)), 1, to) self.animating = False # --- helper methods --- def message( self, title="Warning", icon="warning", text="This is a warning.", buttons=["ok"], default="ok", infoText=None, detailedtext=None, ): """ Create a message box: title = 'title' icon = 'warning' or 'info' text = 'test to show' buttons = ['ok',...] where value is 'ok', 'yes', 'no', 'cancel', 'discard' default = 'ok' the default selected button infotext = 'extended information text' detailedtext = 'Some details' """ msgBox = QtGui.QMessageBox(self) msgBox.setWindowTitle(title) # Icon if icon == "warning": icon = QtGui.QMessageBox.Warning else: icon = QtGui.QMessageBox.Information msgBox.setIcon(icon) # Text msgBox.setText(text) if infoText: msgBox.setInformativeText(infoText) if detailedtext: msgBox.setDetailedText(detailedtext) # buttons qbuttonDict = { "ok": QtGui.QMessageBox.Ok, "yes": QtGui.QMessageBox.Yes, "no": QtGui.QMessageBox.No, "cancel": QtGui.QMessageBox.Cancel, "discard": QtGui.QMessageBox.Discard, } for button in buttons: msgBox.addButton(qbuttonDict[button]) if button == default: msgBox.setDefaultButton(qbuttonDict[button]) ret = msgBox.exec_() for key, value in qbuttonDict.items(): if value == ret: result = key break return result def print_internal(self, text): if not text.endswith("\n"): text += "\n" LOG.debug(str(text).strip()) cursor = self.ui.command_output.textCursor() cursor.movePosition(cursor.End) cursor.insertText(text) cursor.movePosition(cursor.End) vbar = self.ui.command_output.verticalScrollBar() vbar.triggerAction(QtGui.QAbstractSlider.SliderToMaximum) self.ui.command_output.ensureCursorVisible() # --- mfix methods --- def set_mpi(self): " set MPI checkbox if more than one node selected " nodesi = int(self.ui.nodes_i.text()) nodesj = int(self.ui.nodes_j.text()) nodesk = int(self.ui.nodes_k.text()) if max(nodesi, nodesj, nodesk) > 1: self.ui.dmp_button.setChecked(True) def set_nodes(self): " set all nodes to one if MPI checkbox is unchecked " if self.ui.dmp_button.isChecked(): self.ui.nodes_i.setValue(1) self.ui.nodes_j.setValue(1) self.ui.nodes_k.setValue(1) def make_build_cmd(self): """ build mfix """ mfix_home = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) dmp = "--dmp" if self.ui.dmp_button.isChecked() else "" smp = "--smp" if self.ui.smp_button.isChecked() else "" return os.path.join(mfix_home, "configure_mfix --python %s %s && make -j pymfix" % (smp, dmp)) def build_mfix(self): """ build mfix """ self.build_thread.start_command(self.make_build_cmd(), self.get_project_dir()) def clear_output(self): """ build mfix """ self.run_thread.start_command( 'echo "Removing:";' " ls *.LOG *.OUT *.RES *.SP? *.pvd *vtp;" " rm -f *.LOG *.OUT *.RES *.SP? *.pvd *vtp", self.get_project_dir(), ) def run_mfix(self): """ build mfix """ if not self.ui.dmp_button.isChecked(): pymfix_exe = os.path.join(self.get_project_dir(), "pymfix") else: nodesi = int(self.ui.nodes_i.text()) nodesj = int(self.ui.nodes_j.text()) nodesk = int(self.ui.nodes_k.text()) total = nodesi * nodesj * nodesk pymfix_exe = "mpirun -np {} ./pymfix NODESI={} NODESJ={} NODESK={}".format(total, nodesi, nodesj, nodesk) build_and_run_cmd = "{} && {}".format(self.make_build_cmd(), pymfix_exe) self.run_thread.start_command(build_and_run_cmd, self.get_project_dir()) def update_residuals(self): self.ui.residuals.setText(self.updater.residuals) if self.updater.job_done: self.ui.mfix_browser.setHTML("") def connect_mfix(self): """ connect to running instance of mfix """ url = "http://{}:{}".format(self.ui.mfix_host.text(), self.ui.mfix_port.text()) log = logging.getLogger(__name__) log.debug("trying to connect to {}".format(url)) qurl = QUrl(url) self.ui.mfix_browser.load(qurl) self.updater = UpdateResidualsThread() self.updater.sig.connect(self.update_residuals) self.updater.start() self.change_pane("interact") # --- open/save/new --- def save_project(self): # make sure the button is not down self.ui.toolbutton_save.setDown(False) project_dir = self.settings.value("project_dir") # export geometry self.vtkwidget.export_stl(os.path.join(project_dir, "geometry.stl")) self.setWindowTitle("MFIX - %s" % project_dir) self.project.writeDatFile(os.path.join(project_dir, "mfix.dat")) def unsaved(self): project_dir = self.settings.value("project_dir") self.setWindowTitle("MFIX - %s *" % project_dir) def new_project(self, project_dir=None): if not project_dir: project_dir = str( QtGui.QFileDialog.getExistingDirectory( self, "Create Project in Directory", "", QtGui.QFileDialog.ShowDirsOnly ) ) if len(project_dir) < 1: return try: shutil.copyfile("mfix.dat.template", os.path.join(project_dir, "mfix.dat")) except IOError: self.message( title="Warning", icon="warning", text=("You do not have write access to this " "directory.\n"), buttons=["ok"], default="ok", ) return self.open_project(project_dir) def open_project(self, project_dir=None): """ Open MFiX Project """ # make sure the button is not left down self.ui.toolbutton_open.setDown(False) if not project_dir: project_dir = str( QtGui.QFileDialog.getExistingDirectory( self, "Open Project Directory", "", QtGui.QFileDialog.ShowDirsOnly ) ) if len(project_dir) < 1: return writable = True try: import tempfile testfile = tempfile.TemporaryFile(dir=project_dir) testfile.close() except IOError: writable = False if not writable: self.message( title="Warning", icon="warning", text=("You do not have write access to this " "directory.\n"), buttons=["ok"], default="ok", ) return mfix_dat = os.path.abspath(os.path.join(project_dir, "mfix.dat")) if not os.path.exists(mfix_dat): self.message( title="Warning", icon="warning", text=("mfix.dat file does not exist in this " "directory.\n"), buttons=["ok"], default="ok", ) return self.settings.setValue("project_dir", project_dir) self.setWindowTitle("MFIX - %s" % project_dir) # read the file with open(mfix_dat, "r") as mfix_dat_file: src = mfix_dat_file.read() self.ui.mfix_dat_source.setPlainText(src) # self.mode_changed('developer') self.project.load_mfix_dat(mfix_dat) self.ui.energy_eq.setChecked(self.project["energy_eq"])