class MainWindow(QWidget): def __init__(self, appinst, profilecon, settingscon, *args, **kwargs): super().__init__(*args, **kwargs) self.app = appinst self.profilecon = profilecon # connector class instance for reading/writing profile settings self.settingscon = settingscon self.gcode = None self.machine = None self.backgroundTask = None self.postBackgroundTask = None self.coord_plot_items = list( ) # list of all plot items added to the coord plot self.mainlayout = QVBoxLayout(self) self.mainlayout.setContentsMargins(0, 0, 0, 0) self.toolBar = QToolBar() self.toolBar.setStyleSheet("""QToolBar {background-color: white; border-top: 1px solid black}""" ) self.mainlayout.addWidget(self.toolBar) self.add_toolbar_action("./res/folder.svg", "Open", self.open_file_dialog) self.add_toolbar_action("./res/x-square.svg", "Close", self.close_file) self.add_toolbar_action("./res/save.svg", "Export", self.export) self.toolBar.addSeparator() self.add_toolbar_action("./res/sliders.svg", "Settings", self.open_settings_dialog) self.add_toolbar_action("./res/play.svg", "Simulate", self.start_simulation) self.toolBar.addSeparator() self.add_toolbar_action("./res/maximize.svg", "Fit to View", self.fit_plot_to_window) self.add_toolbar_action("./res/maximize-2.svg", "Reset View", self.reset_plot_view) self.toolBar.addSeparator() self.profileSelector = QComboBox() for name in self.profilecon.list_profiles(): self.profileSelector.addItem(name) self.profileSelector.setCurrentText( self.settingscon.get_value("Current_Profile")) self.profilecon.select_profile( self.settingscon.get_value("Current_Profile")) self.toolBar.addWidget(self.profileSelector) self.profileSelector.currentTextChanged.connect( self.selected_profile_changed) divider = QWidget() divider.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.toolBar.addWidget(divider) self.add_toolbar_action("./res/info.svg", "About", self.open_about_dialog) self.contentLayout = QHBoxLayout() self.contentLayout.setContentsMargins(10, 10, 10, 10) self.mainlayout.addLayout(self.contentLayout) self.layerSlider = QSlider() self.layerSlider.setMinimum(0) self.layerSlider.setValue(0) self.layerSlider.setDisabled(True) self.layerSlider.valueChanged.connect(self.show_layer) self.contentLayout.addWidget(self.layerSlider) self.coordPlot = PlotWidget() self.coordPlot.setAspectLocked(True) # self.coordPlot.setLimits(xMin=0, yMin=0) self.configure_plot( ) # is done in a seperate funciton because values need to be updated after settings are changed self.contentLayout.addWidget(self.coordPlot) self.sidebarlayout = QVBoxLayout() self.contentLayout.addLayout(self.sidebarlayout) self.sidebarheader = QLabel("Options") self.sidebarheader.setFixedSize(300, 50) self.sidebarlayout.addWidget(self.sidebarheader) def configure_plot(self): self.coordPlot.invertX( self.profilecon.get_value("invert_x") ) # needs to be done before setting the axis ranges because self.coordPlot.invertY( self.profilecon.get_value("invert_y") ) # inverting does not update the viewbox, but setting the range does self.coordPlot.setXRange(self.profilecon.get_value("bed_min_x"), self.profilecon.get_value("bed_max_x")) self.coordPlot.setYRange(self.profilecon.get_value("bed_min_y"), self.profilecon.get_value("bed_max_y")) def selected_profile_changed(self, new_profile): # select the new profile in the settings connector and update the ui accordingly self.profilecon.select_profile(new_profile) self.settingscon.set_value("Current_Profile", new_profile) # remember selected profile self.settingscon.save_to_file() self.configure_plot() def add_toolbar_action(self, icon, text, function): # wrapper function for adding a toolbar button and connecting it to trigger a function open_icon = QIcon(icon) action = self.toolBar.addAction(open_icon, text) action.triggered.connect(function) def finish_background_task(self): # function is called when a background task finishes if self.postBackgroundTask: # run cleanup task (i.e. ui update); runs on main ui thread! self.postBackgroundTask() # reset variables self.postBackgroundTask = None self.backgroundTask = None def run_in_background(self, task, after=None, args=None): # wrapper function for creating and starting a thread to run a function in the background # arguments can be passed to the function in the thread and a cleanup function can be specified # which is run on the main ui thread when the background task is finished self.backgroundTask = BackgroundTask(task) if args: self.backgroundTask.set_arguments(args) self.backgroundTask.finished.connect(self.finish_background_task) self.postBackgroundTask = after self.backgroundTask.start() def open_file_dialog(self): # in case a file is open already, close it properly first if self.machine: ret = self.close_file() if not ret: # user canceled closing of current file; can't open new one return # open dialog for selecting a gcode file to be loaded dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.ExistingFile) filters = ["G-code (*.gcode)", "Any files (*)"] dialog.setNameFilters(filters) dialog.selectNameFilter(filters[0]) dialog.setViewMode(QFileDialog.Detail) filename = None if dialog.exec_(): filename = dialog.selectedFiles() if filename: self.run_in_background(self.load_data, after=self.show_layer, args=filename) def open_settings_dialog(self): # open a dialog with settings dialog = SettingsDialog(self, self.profilecon) dialog.exec() # update settings self.configure_plot() def open_about_dialog(self): # open the about dialog dialog = QDialog() dialog.setWindowTitle("About...") layout = QVBoxLayout() dialog.setLayout(layout) text = QLabel(strings.about) layout.addWidget(text) dialog.exec() def close_file(self): # close the current gcode file, discard all data # Before, ask for user confirmation cfmsgbox = QMessageBox() cfmsgbox.setWindowTitle("Close file?") cfmsgbox.setText( "Are you sure you want to close the current file and discard all unsaved data?" ) cfmsgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) cfmsgbox.setDefaultButton(QMessageBox.No) ret = cfmsgbox.exec() if ret == QMessageBox.Yes: for item in self.coord_plot_items: self.coordPlot.removeItem(item) self.machine = None self.gcode = None # TODO: fix: this will not terminate a running background process return True return False def export(self): pass def start_simulation(self): pass def fit_plot_to_window(self): x, y = self.machine.get_path_coordinates( layer_number=self.layerSlider.value()) self.coordPlot.setRange(xRange=(min(x), max(x)), yRange=(min(y), max(y))) def reset_plot_view(self): self.coordPlot.setXRange(self.profilecon.get_value("bed_min_x"), self.profilecon.get_value("bed_max_x")) self.coordPlot.setYRange(self.profilecon.get_value("bed_min_y"), self.profilecon.get_value("bed_max_y")) def load_data(self, filename): # initalizes a virtual machine from the gcode in the file given # all path data for this gcode is calculated; this is a cpu intensive task! self.gcode = GCode() self.gcode.load_file(filename) self.machine = Machine(self.gcode, self.profilecon) self.machine.create_path() # set the layer sliders maximum to represent the given amount of layers and enable the slider self.layerSlider.setMaximum(len(self.machine.layers) - 1) self.layerSlider.setEnabled(True) def show_layer(self): # plot path for the layer selected by the layer slider x, y = self.machine.get_path_coordinates( layer_number=self.layerSlider.value()) pltitm = self.coordPlot.plot(x, y, clear=True) self.coord_plot_items.append(pltitm)