def _scheduleDelayedCallEvent(self, event: "_CallFunctionEvent") -> None: if event.delay is None: return timer = QTimer(self) timer.setSingleShot(True) timer.setInterval(event.delay * 1000 * (1 + self.TIME_TOLERANCE)) timer_callback = lambda e=event: self._onDelayReached(e) timer.timeout.connect(timer_callback) timer.start() self._delayed_events[event] = { "event": event, "timer": timer, "timer_callback": timer_callback, }
class SettingPropertyProvider(QObject): """This class provides the value and change notifications for the properties of a single setting Since setting values and other properties are provided by a stack, we need some way to query the stack from QML to provide us with those values. This class takes care of that. This class provides the property values through QObject dynamic properties so that they are available from QML. """ def __init__(self, parent=None) -> None: super().__init__(parent=parent) self._property_map = QQmlPropertyMap(self) self._stack = None # type: Optional[ContainerStack] self._key = "" self._relations = set() # type: Set[str] self._watched_properties = [] # type: List[str] self._store_index = 0 self._value_used = None # type: Optional[bool] self._stack_levels = [] # type: List[int] self._remove_unused_value = True self._validator = None # type: Optional[Validator] self._update_timer = QTimer(self) self._update_timer.setInterval(100) self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._update) self.storeIndexChanged.connect(self._storeIndexChanged) def setContainerStack(self, stack: Optional[ContainerStack]) -> None: if self._stack == stack: return # Nothing to do, attempting to set stack to the same value. if self._stack: self._stack.propertiesChanged.disconnect(self._onPropertiesChanged) self._stack.containersChanged.disconnect(self._containersChanged) self._stack = stack if self._stack: self._stack.propertiesChanged.connect(self._onPropertiesChanged) self._stack.containersChanged.connect(self._containersChanged) self._validator = None self._updateDelayed() self.containerStackChanged.emit() def setContainerStackId(self, stack_id: str) -> None: """Set the containerStackId property.""" if stack_id == self.containerStackId: return # No change. if stack_id: if stack_id == "global": self.setContainerStack( Application.getInstance().getGlobalContainerStack()) else: stacks = ContainerRegistry.getInstance().findContainerStacks( id=stack_id) if stacks: self.setContainerStack(stacks[0]) else: self.setContainerStack(None) containerStackIdChanged = pyqtSignal() """Emitted when the containerStackId property changes.""" @pyqtProperty(str, fset=setContainerStackId, notify=containerStackIdChanged) def containerStackId(self) -> str: """The ID of the container stack we should query for property values.""" if self._stack: return self._stack.id return "" containerStackChanged = pyqtSignal() @pyqtProperty(QObject, fset=setContainerStack, notify=containerStackChanged) def containerStack(self) -> Optional[ContainerInterface]: return self._stack removeUnusedValueChanged = pyqtSignal() def setRemoveUnusedValue(self, remove_unused_value: bool) -> None: if self._remove_unused_value != remove_unused_value: self._remove_unused_value = remove_unused_value self.removeUnusedValueChanged.emit() @pyqtProperty(bool, fset=setRemoveUnusedValue, notify=removeUnusedValueChanged) def removeUnusedValue(self) -> bool: return self._remove_unused_value def setWatchedProperties(self, properties: List[str]) -> None: """Set the watchedProperties property.""" if properties != self._watched_properties: self._watched_properties = properties self._updateDelayed() self.watchedPropertiesChanged.emit() watchedPropertiesChanged = pyqtSignal() """Emitted when the watchedProperties property changes.""" @pyqtProperty("QStringList", fset=setWatchedProperties, notify=watchedPropertiesChanged) def watchedProperties(self) -> List[str]: """A list of property names that should be watched for changes.""" return self._watched_properties def setKey(self, key: str) -> None: """Set the key property.""" if key != self._key: self._key = key self._validator = None self._updateDelayed() self.keyChanged.emit() keyChanged = pyqtSignal() """Emitted when the key property changes.""" @pyqtProperty(str, fset=setKey, notify=keyChanged) def key(self): """The key of the setting that we should provide property values for.""" return self._key propertiesChanged = pyqtSignal() @pyqtProperty(QQmlPropertyMap, notify=propertiesChanged) def properties(self): return self._property_map @pyqtSlot() def forcePropertiesChanged(self): self._onPropertiesChanged(self._key, self._watched_properties) def setStoreIndex(self, index): if index != self._store_index: self._store_index = index self.storeIndexChanged.emit() storeIndexChanged = pyqtSignal() @pyqtProperty(int, fset=setStoreIndex, notify=storeIndexChanged) def storeIndex(self): return self._store_index stackLevelChanged = pyqtSignal() @pyqtProperty("QVariantList", notify=stackLevelChanged) def stackLevels(self): """At what levels in the stack does the value(s) for this setting occur?""" if not self._stack: return [-1] return self._stack_levels @pyqtSlot(str, "QVariant") def setPropertyValue(self, property_name, property_value): """Set the value of a property. :param stack_index: At which level in the stack should this property be set? :param property_name: The name of the property to set. :param property_value: The value of the property to set. """ if not self._stack or not self._key: return if property_name not in self._watched_properties: Logger.log("w", "Tried to set a property that is not being watched") return container = self._stack.getContainer(self._store_index) if isinstance(container, DefinitionContainer): return # In some cases we clean some stuff and the result is as when nothing as been changed manually. if property_name == "value" and self._remove_unused_value: for index in self._stack_levels: if index > self._store_index: old_value = self.getPropertyValue(property_name, index) key_state = str( self._stack.getContainer( self._store_index).getProperty(self._key, "state")) # The old_value might be a SettingFunction, like round(), sum(), etc. # In this case retrieve the value to compare if isinstance(old_value, SettingFunction): old_value = old_value(self._stack) # sometimes: old value is int, property_value is float # (and the container is not removed, so the revert button appears) if str(old_value) == str( property_value ) and key_state != "InstanceState.Calculated": # If we change the setting so that it would be the same as a deeper setting, we can just remove # the value. Note that we only do this when this is not caused by the calculated state # In this case the setting does need to be set, as it needs to be stored in the user settings. self.removeFromContainer(self._store_index) return else: # First value that we encountered was different, stop looking & continue as normal. break # _remove_unused_value is used when the stack value differs from the effective value # i.e. there is a resolve function if self._property_map.value( property_name) == property_value and self._remove_unused_value: return container.setProperty(self._key, property_name, property_value) @pyqtSlot(str, int, result="QVariant") def getPropertyValue(self, property_name: str, stack_level: int) -> Any: """Manually request the value of a property. The most notable difference with the properties is that you have more control over at what point in the stack you want the setting to be retrieved (instead of always taking the top one) :param property_name: The name of the property to get the value from. :param stack_level: the index of the container to get the value from. """ try: # Because we continue to count if there are multiple linked stacks, we need to check what stack is targeted current_stack = self._stack while current_stack: num_containers = len(current_stack.getContainers()) if stack_level >= num_containers: stack_level -= num_containers current_stack = current_stack.getNextStack() else: break # Found the right stack if not current_stack: Logger.log( "w", "Could not find the right stack for setting %s at stack level %d while trying to get property %s", self._key, stack_level, property_name) return None value = current_stack.getContainers()[stack_level].getProperty( self._key, property_name) except IndexError: # Requested stack level does not exist Logger.log( "w", "Tried to get property of type %s from %s but it did not exist on requested index %d", property_name, self._key, stack_level) return None return value @pyqtSlot(str, result=str) def getPropertyValueAsString(self, property_name: str) -> str: return self._getPropertyValue(property_name) @pyqtSlot(int) def removeFromContainer(self, index: int) -> None: current_stack = self._stack while current_stack: num_containers = len(current_stack.getContainers()) if index >= num_containers: index -= num_containers current_stack = current_stack.getNextStack() else: break # Found the right stack if not current_stack: Logger.log( "w", "Unable to remove instance from container because the right stack at stack level %d could not be found", index) return container = current_stack.getContainer(index) if not container or not isinstance(container, InstanceContainer): Logger.log( "w", "Unable to remove instance from container as it was either not found or not an instance container" ) return container.removeInstance(self._key) isValueUsedChanged = pyqtSignal() @pyqtProperty(bool, notify=isValueUsedChanged) def isValueUsed(self) -> bool: if self._value_used is not None: return self._value_used if not self._stack: return False definition = self._stack.getSettingDefinition(self._key) if not definition: return False relation_count = 0 value_used_count = 0 for key in self._relations: # If the setting is not a descendant of this setting, ignore it. if not definition.isDescendant(key): continue relation_count += 1 if self._stack.getProperty(key, "state") != InstanceState.User: value_used_count += 1 break # If the setting has a formula the value is still used. if isinstance(self._stack.getRawProperty(key, "value"), SettingFunction): value_used_count += 1 break self._value_used = relation_count == 0 or (relation_count > 0 and value_used_count != 0) return self._value_used def _onPropertiesChanged(self, key: str, property_names: List[str]) -> None: if key != self._key: if key in self._relations: self._value_used = None try: self.isValueUsedChanged.emit() except RuntimeError: # QtObject has been destroyed, no need to handle the signals anymore. # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying # logic to emit pyqtSignals is gone. return return has_values_changed = False for property_name in property_names: if property_name not in self._watched_properties: continue has_values_changed = True try: self._property_map.insert( property_name, self._getPropertyValue(property_name)) except RuntimeError: # QtObject has been destroyed, no need to handle the signals anymore. # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying # logic to emit pyqtSignals is gone. return self._updateStackLevels() if has_values_changed: try: self.propertiesChanged.emit() except RuntimeError: # QtObject has been destroyed, no need to handle the signals anymore. # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying # logic to emit pyqtSignals is gone. return def _update(self, container=None): if not self._stack or not self._watched_properties or not self._key: return self._updateStackLevels() relations = self._stack.getProperty(self._key, "relations") if relations: # If the setting doesn't have the property relations, None is returned for relation in filter( lambda r: r.type == RelationType.RequiredByTarget and r. role == "value", relations): self._relations.add(relation.target.key) for property_name in self._watched_properties: self._property_map.insert(property_name, self._getPropertyValue(property_name)) # Notify that the properties have been changed.Kewl self.propertiesChanged.emit() # Force update of value_used self._value_used = None self.isValueUsedChanged.emit() def _updateDelayed(self, container=None) -> None: try: self._update_timer.start() except RuntimeError: # Sometimes the python object is not yet deleted, but the wrapped part is already gone. # In that case there is nothing else to do but ignore this. pass def _containersChanged(self, container=None): self._updateDelayed() def _storeIndexChanged(self): self._updateDelayed() def _updateStackLevels(self) -> None: """Updates the self._stack_levels field, which indicates at which levels in the stack the property is set.""" levels = [] # Start looking at the stack this provider is attached to. current_stack = self._stack index = 0 while current_stack: for container in current_stack.getContainers(): try: if container.getProperty(self._key, "value") is not None: levels.append(index) except AttributeError: pass index += 1 # If there is a next stack, check that one as well. current_stack = current_stack.getNextStack() if levels != self._stack_levels: self._stack_levels = levels self.stackLevelChanged.emit() def _getPropertyValue(self, property_name): # Use the evaluation context to skip certain containers context = PropertyEvaluationContext(self._stack) context.context["evaluate_from_container_index"] = self._store_index property_value = self._stack.getProperty(self._key, property_name, context=context) if isinstance(property_value, SettingFunction): property_value = property_value(self._stack) if property_name == "value": setting_type = self._stack.getProperty(self._key, "type") if setting_type is not None: property_value = SettingDefinition.settingValueToString( setting_type, property_value) else: property_value = "" elif property_name == "validationState": # Setting is not validated. This can happen if there is only a setting definition. # We do need to validate it, because a setting defintions value can be set by a function, which could # be an invalid setting. if property_value is None: if not self._validator: definition = self._stack.getSettingDefinition(self._key) if definition: validator_type = SettingDefinition.getValidatorForType( definition.type) if validator_type: self._validator = validator_type(self._key) if self._validator: property_value = self._validator(self._stack) return str(property_value)
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) # region Create CartPole instance and load initial settings # Create CartPole instance self.initial_state = create_cartpole_state() self.CartPoleInstance = CartPole(initial_state=self.initial_state) # Set timescales self.CartPoleInstance.dt_simulation = dt_simulation self.CartPoleInstance.dt_controller = controller_update_interval self.CartPoleInstance.dt_save = save_interval # set other settings self.CartPoleInstance.set_controller(controller_init) self.CartPoleInstance.stop_at_90 = stop_at_90_init self.set_random_experiment_generator_init_params() # endregion # region Decide whether to save the data in "CartPole memory" or not self.save_history = save_history_init self.show_experiment_summary = show_experiment_summary_init if self.save_history or self.show_experiment_summary: self.CartPoleInstance.save_data_in_cart = True else: self.CartPoleInstance.save_data_in_cart = False # endregion # region Other variables initial values as provided in gui_default_parameters.py # Start user controlled experiment/ start random experiment/ load and replay - on start button self.simulator_mode = simulator_mode_init self.slider_on_click = slider_on_click_init # Update slider on click/update slider while hoovering over it self.speedup = speedup_init # Default simulation speed-up # endregion # region Initialize loop-timer # This timer allows to relate the simulation time to user time # And (if your computer is fast enough) run simulation # slower or faster than real-time by predefined factor (speedup) self.looper = loop_timer( dt_target=(self.CartPoleInstance.dt_simulation / self.speedup)) # endregion # region Variables controlling the state of various processes (DO NOT MODIFY) self.terminate_experiment_or_replay_thread = False # True: gives signal causing thread to terminate self.pause_experiment_or_replay_thread = False # True: gives signal causing the thread to pause self.run_set_labels_thread = True # True if gauges (labels) keep being repeatedly updated # Stop threads by setting False # Flag indicating if the "START! / STOP!" button should act as start or as stop when pressed. # Can take values "START!" or "STOP!" self.start_or_stop_action = "START!" # Flag indicating whether the pause button should pause or unpause. self.pause_or_unpause_action = "PAUSE" # Flag indicating that saving of experiment recording to csv file has finished self.experiment_or_replay_thread_terminated = False self.user_time_counter = 0 # Measures the user time # Slider instant value (which is draw in GUI) differs from value saved in CartPole instance # if the option updating slider "on-click" is enabled. self.slider_instant_value = self.CartPoleInstance.slider_value self.noise = 'OFF' self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise # endregion # region Create GUI Layout # region - Create container for top level layout layout = QVBoxLayout() # endregion # region - Change geometry of the main window self.setGeometry(300, 300, 2500, 1000) # endregion # region - Matplotlib figures (CartPole drawing and Slider) # Draw Figure self.fig = Figure( figsize=(25, 10) ) # Regulates the size of Figure in inches, before scaling to window size. self.canvas = FigureCanvas(self.fig) self.fig.AxCart = self.canvas.figure.add_subplot(211) self.fig.AxSlider = self.canvas.figure.add_subplot(212) self.fig.AxSlider.set_ylim(0, 1) self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) # Attach figure to the layout lf = QVBoxLayout() lf.addWidget(self.canvas) # endregion # region - Radio buttons selecting current controller self.rbs_controllers = [] for controller_name in self.CartPoleInstance.controller_names: self.rbs_controllers.append(QRadioButton(controller_name)) # Ensures that radio buttons are exclusive self.controllers_buttons_group = QButtonGroup() for button in self.rbs_controllers: self.controllers_buttons_group.addButton(button) lr_c = QVBoxLayout() lr_c.addStretch(1) for rb in self.rbs_controllers: rb.clicked.connect(self.RadioButtons_controller_selection) lr_c.addWidget(rb) lr_c.addStretch(1) self.rbs_controllers[self.CartPoleInstance.controller_idx].setChecked( True) # endregion # region - Create central part of the layout for figures and radio buttons and add it to the whole layout lc = QHBoxLayout() lc.addLayout(lf) lc.addLayout(lr_c) layout.addLayout(lc) # endregion # region - Gauges displaying current values of various states and parameters (time, velocity, angle,...) # First row ld = QHBoxLayout() # User time self.labTime = QLabel("User's time (s): ") self.timer = QTimer() self.timer.setInterval(100) # Tick every 1/10 of the second self.timer.timeout.connect(self.set_user_time_label) self.timer.start() ld.addWidget(self.labTime) # Speed, angle, motor power (Q) self.labSpeed = QLabel('Speed (m/s):') self.labAngle = QLabel('Angle (deg):') self.labMotor = QLabel('') self.labTargetPosition = QLabel('') ld.addWidget(self.labSpeed) ld.addWidget(self.labAngle) ld.addWidget(self.labMotor) ld.addWidget(self.labTargetPosition) layout.addLayout(ld) # Second row of labels # Simulation time, Measured (real) speed-up, slider-value ld2 = QHBoxLayout() self.labTimeSim = QLabel('Simulation Time (s):') ld2.addWidget(self.labTimeSim) self.labSpeedUp = QLabel('Speed-up (measured):') ld2.addWidget(self.labSpeedUp) self.labSliderInstant = QLabel('') ld2.addWidget(self.labSliderInstant) layout.addLayout(ld2) # endregion # region - Buttons "START!" / "STOP!", "PAUSE", "QUIT" self.bss = QPushButton("START!") self.bss.pressed.connect(self.start_stop_button) self.bp = QPushButton("PAUSE") self.bp.pressed.connect(self.pause_unpause_button) bq = QPushButton("QUIT") bq.pressed.connect(self.quit_application) lspb = QHBoxLayout() # Sub-Layout for Start/Stop and Pause Buttons lspb.addWidget(self.bss) lspb.addWidget(self.bp) # endregion # region - Sliders setting initial state and buttons for kicking the pole # Sliders setting initial position and angle lb = QVBoxLayout() # Layout for buttons lb.addLayout(lspb) lb.addWidget(bq) ip = QHBoxLayout() # Layout for initial position sliders self.initial_position_slider = QSlider( orientation=Qt.Orientation.Horizontal) self.initial_position_slider.setRange( -int(float(1000 * TrackHalfLength)), int(float(1000 * TrackHalfLength))) self.initial_position_slider.setValue(0) self.initial_position_slider.setSingleStep(1) self.initial_position_slider.valueChanged.connect( self.update_initial_position) self.initial_angle_slider = QSlider( orientation=Qt.Orientation.Horizontal) self.initial_angle_slider.setRange(-int(float(100 * np.pi)), int(float(100 * np.pi))) self.initial_angle_slider.setValue(0) self.initial_angle_slider.setSingleStep(1) self.initial_angle_slider.valueChanged.connect( self.update_initial_angle) ip.addWidget(QLabel("Initial position:")) ip.addWidget(self.initial_position_slider) ip.addWidget(QLabel("Initial angle:")) ip.addWidget(self.initial_angle_slider) ip.addStretch(0.01) # Slider setting latency self.LATENCY_SLIDER_RANGE_INT = 1000 self.latency_slider = QSlider(orientation=Qt.Orientation.Horizontal) self.latency_slider.setRange(0, self.LATENCY_SLIDER_RANGE_INT) self.latency_slider.setValue( int(self.CartPoleInstance.LatencyAdderInstance.latency * self.LATENCY_SLIDER_RANGE_INT / self.CartPoleInstance.LatencyAdderInstance.max_latency)) self.latency_slider.setSingleStep(1) self.latency_slider.valueChanged.connect(self.update_latency) ip.addWidget(QLabel("Latency:")) ip.addWidget(self.latency_slider) self.labLatency = QLabel('Latency (ms): {:.1f}'.format( self.CartPoleInstance.LatencyAdderInstance.latency * 1000)) ip.addWidget(self.labLatency) # Buttons activating noise self.rbs_noise = [] for mode_name in ['ON', 'OFF']: self.rbs_noise.append(QRadioButton(mode_name)) # Ensures that radio buttons are exclusive self.noise_buttons_group = QButtonGroup() for button in self.rbs_noise: self.noise_buttons_group.addButton(button) lr_n = QHBoxLayout() lr_n.addWidget(QLabel('Noise:')) for rb in self.rbs_noise: rb.clicked.connect(self.RadioButtons_noise_on_off) lr_n.addWidget(rb) self.rbs_noise[1].setChecked(True) ip.addStretch(0.01) ip.addLayout(lr_n) ip.addStretch(0.01) # Buttons giving kick to the pole kick_label = QLabel("Kick pole:") kick_left_button = QPushButton() kick_left_button.setText("Left") kick_left_button.adjustSize() kick_left_button.clicked.connect(self.kick_pole) kick_right_button = QPushButton() kick_right_button.setText("Right") kick_right_button.adjustSize() kick_right_button.clicked.connect(self.kick_pole) ip.addWidget(kick_label) ip.addWidget(kick_left_button) ip.addWidget(kick_right_button) lb.addLayout(ip) layout.addLayout(lb) # endregion # region - Text boxes and Combobox to provide settings concerning generation of random experiment l_generate_trace = QHBoxLayout() l_generate_trace.addWidget(QLabel('Random experiment settings:')) l_generate_trace.addWidget(QLabel('Length (s):')) self.textbox_length = QLineEdit() l_generate_trace.addWidget(self.textbox_length) l_generate_trace.addWidget(QLabel('Turning Points (m):')) self.textbox_turning_points = QLineEdit() l_generate_trace.addWidget(self.textbox_turning_points) l_generate_trace.addWidget(QLabel('Interpolation:')) self.cb_interpolation = QComboBox() self.cb_interpolation.addItems( ['0-derivative-smooth', 'linear', 'previous']) self.cb_interpolation.currentIndexChanged.connect( self.cb_interpolation_selectionchange) self.cb_interpolation.setCurrentText( self.CartPoleInstance.interpolation_type) l_generate_trace.addWidget(self.cb_interpolation) layout.addLayout(l_generate_trace) # endregion # region - Textbox to provide csv file name for saving or loading data l_text = QHBoxLayout() textbox_title = QLabel('CSV file name:') self.textbox = QLineEdit() l_text.addWidget(textbox_title) l_text.addWidget(self.textbox) layout.addLayout(l_text) # endregion # region - Make strip of layout for checkboxes l_cb = QHBoxLayout() # endregion # region - Textbox to provide the target speed-up value l_text_speedup = QHBoxLayout() tx_speedup_title = QLabel('Speed-up (target):') self.tx_speedup = QLineEdit() l_text_speedup.addWidget(tx_speedup_title) l_text_speedup.addWidget(self.tx_speedup) self.tx_speedup.setText(str(self.speedup)) l_cb.addLayout(l_text_speedup) self.wrong_speedup_msg = QMessageBox() self.wrong_speedup_msg.setWindowTitle("Speed-up value problem") self.wrong_speedup_msg.setIcon(QMessageBox.Icon.Critical) # endregion # region - Checkboxes # region -- Checkbox: Save/don't save experiment recording self.cb_save_history = QCheckBox('Save results', self) if self.save_history: self.cb_save_history.toggle() self.cb_save_history.toggled.connect(self.cb_save_history_f) l_cb.addWidget(self.cb_save_history) # endregion # region -- Checkbox: Display plots showing dynamic evolution of the system as soon as experiment terminates self.cb_show_experiment_summary = QCheckBox('Show experiment summary', self) if self.show_experiment_summary: self.cb_show_experiment_summary.toggle() self.cb_show_experiment_summary.toggled.connect( self.cb_show_experiment_summary_f) l_cb.addWidget(self.cb_show_experiment_summary) # endregion # region -- Checkbox: Block pole if it reaches +/-90 deg self.cb_stop_at_90_deg = QCheckBox('Stop-at-90-deg', self) if self.CartPoleInstance.stop_at_90: self.cb_stop_at_90_deg.toggle() self.cb_stop_at_90_deg.toggled.connect(self.cb_stop_at_90_deg_f) l_cb.addWidget(self.cb_stop_at_90_deg) # endregion # region -- Checkbox: Update slider on click/update slider while hoovering over it self.cb_slider_on_click = QCheckBox('Update slider on click', self) if self.slider_on_click: self.cb_slider_on_click.toggle() self.cb_slider_on_click.toggled.connect(self.cb_slider_on_click_f) l_cb.addWidget(self.cb_slider_on_click) # endregion # endregion # region - Radio buttons selecting simulator mode: user defined experiment, random experiment, replay # List available simulator modes - constant self.available_simulator_modes = [ 'Slider-Controlled Experiment', 'Random Experiment', 'Replay' ] self.rbs_simulator_mode = [] for mode_name in self.available_simulator_modes: self.rbs_simulator_mode.append(QRadioButton(mode_name)) # Ensures that radio buttons are exclusive self.simulator_mode_buttons_group = QButtonGroup() for button in self.rbs_simulator_mode: self.simulator_mode_buttons_group.addButton(button) lr_sm = QHBoxLayout() lr_sm.addStretch(1) lr_sm.addWidget(QLabel('Simulator mode:')) for rb in self.rbs_simulator_mode: rb.clicked.connect(self.RadioButtons_simulator_mode) lr_sm.addWidget(rb) lr_sm.addStretch(1) self.rbs_simulator_mode[self.available_simulator_modes.index( self.simulator_mode)].setChecked(True) l_cb.addStretch(1) l_cb.addLayout(lr_sm) l_cb.addStretch(1) # endregion # region - Add checkboxes to layout layout.addLayout(l_cb) # endregion # region - Create an instance of a GUI window w = QWidget() w.setLayout(layout) self.setCentralWidget(w) self.show() self.setWindowTitle('CartPole Simulator') # endregion # endregion # region Open controller-specific popup windows self.open_additional_controller_widget() # endregion # region Activate functions capturing mouse movements and clicks over the slider # This line links function capturing the mouse position on the canvas of the Figure self.canvas.mpl_connect("motion_notify_event", self.on_mouse_movement) # This line links function capturing the mouse position on the canvas of the Figure click self.canvas.mpl_connect("button_press_event", self.on_mouse_click) # endregion # region Introducing multithreading # To ensure smooth functioning of the app, # the calculations and redrawing of the figures have to be done in a different thread # than the one capturing the mouse position and running the animation self.threadpool = QThreadPool() # endregion # region Starts a thread repeatedly redrawing gauges (labels) of the GUI # It runs till the QUIT button is pressed worker_labels = Worker(self.set_labels_thread) self.threadpool.start(worker_labels) # endregion # region Start animation repeatedly redrawing changing elements of matplotlib figures (CartPole drawing and slider) # This animation runs ALWAYS when the GUI is open # The buttons of GUI only decide if new parameters are calculated or not self.anim = self.CartPoleInstance.run_animation(self.fig) # endregion # region Thread performing CartPole experiment, slider-controlled or random # It iteratively updates CartPole state and save data to a .csv file # It also put simulation time in relation to user time def experiment_thread(self): # Necessary only for debugging in Visual Studio Code IDE try: ptvsd.debug_this_thread() except: pass self.looper.start_loop() while not self.terminate_experiment_or_replay_thread: if self.pause_experiment_or_replay_thread: time.sleep(0.1) else: # Calculations of the Cart state in the next timestep self.CartPoleInstance.update_state() # Terminate thread if random experiment reached its maximal length if ((self.CartPoleInstance.use_pregenerated_target_position is True) and (self.CartPoleInstance.time >= self.CartPoleInstance.t_max_pre)): self.terminate_experiment_or_replay_thread = True # FIXME: when Speedup empty in GUI I expected inf speedup but got error Loop timer was not initialized properly self.looper.sleep_leftover_time() # Save simulation history if user chose to do so at the end of the simulation if self.save_history: csv_name = self.textbox.text() self.CartPoleInstance.save_history_csv( csv_name=csv_name, mode='init', length_of_experiment=np.around( self.CartPoleInstance.dict_history['time'][-1], decimals=2)) self.CartPoleInstance.save_history_csv(csv_name=csv_name, mode='save offline') self.experiment_or_replay_thread_terminated = True # endregion # region Thread replaying a saved experiment recording def replay_thread(self): # Necessary only for debugging in Visual Studio Code IDE try: ptvsd.debug_this_thread() except: pass # Check what is in the csv textbox csv_name = self.textbox.text() # Load experiment history history_pd, filepath = self.CartPoleInstance.load_history_csv( csv_name=csv_name) # Set cartpole in the right mode (just to ensure slider behaves properly) with open(filepath, newline='') as f: reader = csv.reader(f) for line in reader: line = line[0] if line[:len('# Controller: ')] == '# Controller: ': controller_set = self.CartPoleInstance.set_controller( line[len('# Controller: '):].rstrip("\n")) if controller_set: self.rbs_controllers[self.CartPoleInstance. controller_idx].setChecked(True) else: self.rbs_controllers[1].setChecked( True) # Set first, but not manual stabilization break # Augment the experiment history with simulation time step size dt = [] row_iterator = history_pd.iterrows() _, last = next(row_iterator) # take first item from row_iterator for i, row in row_iterator: dt.append(row['time'] - last['time']) last = row dt.append(dt[-1]) history_pd['dt'] = np.array(dt) # Initialize loop timer (with arbitrary dt) replay_looper = loop_timer(dt_target=0.0) # Start looping over history replay_looper.start_loop() global L for index, row in history_pd.iterrows(): self.CartPoleInstance.s[POSITION_IDX] = row['position'] self.CartPoleInstance.s[POSITIOND_IDX] = row['positionD'] self.CartPoleInstance.s[ANGLE_IDX] = row['angle'] self.CartPoleInstance.time = row['time'] self.CartPoleInstance.dt = row['dt'] try: self.CartPoleInstance.u = row['u'] except KeyError: pass self.CartPoleInstance.Q = row['Q'] self.CartPoleInstance.target_position = row['target_position'] if self.CartPoleInstance.controller_name == 'manual-stabilization': self.CartPoleInstance.slider_value = self.CartPoleInstance.Q else: self.CartPoleInstance.slider_value = self.CartPoleInstance.target_position / TrackHalfLength # TODO: Make it more general for all possible parameters try: L[...] = row['L'] except KeyError: pass except: print('Error while assigning L') print("Unexpected error:", sys.exc_info()[0]) print("Unexpected error:", sys.exc_info()[1]) dt_target = (self.CartPoleInstance.dt / self.speedup) replay_looper.dt_target = dt_target replay_looper.sleep_leftover_time() if self.terminate_experiment_or_replay_thread: # Means that stop button was pressed break while self.pause_experiment_or_replay_thread: # Means that pause button was pressed time.sleep(0.1) if self.show_experiment_summary: self.CartPoleInstance.dict_history = history_pd.loc[:index].to_dict( orient='list') self.experiment_or_replay_thread_terminated = True # endregion # region "START! / STOP!" button -> run/stop slider-controlled experiment, random experiment or replay experiment recording # Actions to be taken when "START! / STOP!" button is clicked def start_stop_button(self): # If "START! / STOP!" button in "START!" mode... if self.start_or_stop_action == 'START!': self.bss.setText("STOP!") self.start_thread() # If "START! / STOP!" button in "STOP!" mode... elif self.start_or_stop_action == 'STOP!': self.bss.setText("START!") self.bp.setText("PAUSE") # This flag is periodically checked by thread. It terminates if set True. self.terminate_experiment_or_replay_thread = True # The stop_thread function is called automatically by the thread when it terminates # It is implemented this way, because thread my terminate not only due "STOP!" button # (e.g. replay thread when whole experiment is replayed) def pause_unpause_button(self): # Only Pause if experiment is running if self.pause_or_unpause_action == 'PAUSE' and self.start_or_stop_action == 'STOP!': self.pause_or_unpause_action = 'UNPAUSE' self.pause_experiment_or_replay_thread = True self.bp.setText("UNPAUSE") elif self.pause_or_unpause_action == 'UNPAUSE' and self.start_or_stop_action == 'STOP!': self.pause_or_unpause_action = 'PAUSE' self.pause_experiment_or_replay_thread = False self.bp.setText("PAUSE") # Run thread. works for all simulator modes. def start_thread(self): # Check if value provided in speed-up textbox makes sense # If not abort start speedup_updated = self.get_speedup() if not speedup_updated: return # Disable GUI elements for features which must not be changed in runtime # For other features changing in runtime may not cause errors, but will stay without effect for current run self.cb_save_history.setEnabled(False) for rb in self.rbs_simulator_mode: rb.setEnabled(False) for rb in self.rbs_controllers: rb.setEnabled(False) if self.simulator_mode != 'Replay': self.cb_show_experiment_summary.setEnabled(False) # Set user-provided initial values for state (or its part) of the CartPole # Search implementation for more detail # The following line is important as it let the user to set with the slider the starting target position # After the slider was reset at the end of last experiment # With the small sliders he can also adjust starting initial_state self.reset_variables( 2, s=np.copy(self.initial_state), target_position=self.CartPoleInstance.target_position) if self.simulator_mode == 'Random Experiment': self.CartPoleInstance.use_pregenerated_target_position = True if self.textbox_length.text() == '': self.CartPoleInstance.length_of_experiment = length_of_experiment_init else: self.CartPoleInstance.length_of_experiment = float( self.textbox_length.text()) turning_points_list = [] if self.textbox_turning_points.text() != '': for turning_point in self.textbox_turning_points.text().split( ', '): turning_points_list.append(float(turning_point)) self.CartPoleInstance.turning_points = turning_points_list self.CartPoleInstance.setup_cartpole_random_experiment() self.looper.dt_target = self.CartPoleInstance.dt_simulation / self.speedup # Pass the function to execute if self.simulator_mode == "Replay": worker = Worker(self.replay_thread) elif self.simulator_mode == 'Slider-Controlled Experiment' or self.simulator_mode == 'Random Experiment': worker = Worker(self.experiment_thread) worker.signals.finished.connect(self.finish_thread) # Execute self.threadpool.start(worker) # Determine what should happen when "START! / STOP!" is pushed NEXT time self.start_or_stop_action = "STOP!" # finish_threads works for all simulation modes # Some lines mya be redundant for replay, # however as they do not take much computation time we leave them here # As it my code shorter, while hopefully still clear. # It is called automatically at the end of experiment_thread def finish_thread(self): self.CartPoleInstance.use_pregenerated_target_position = False self.initial_state = create_cartpole_state() self.initial_position_slider.setValue(0) self.initial_angle_slider.setValue(0) self.CartPoleInstance.s = self.initial_state # Some controllers may collect they own statistics about their usage and print it after experiment terminated if self.simulator_mode != 'Replay': try: self.CartPoleInstance.controller.controller_report() except: pass if self.show_experiment_summary: self.w_summary = SummaryWindow( summary_plots=self.CartPoleInstance.summary_plots) # Reset variables and redraw the figures self.reset_variables(0) # Draw figures self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) self.canvas.draw() # Enable back all elements of GUI: self.cb_save_history.setEnabled(True) self.cb_show_experiment_summary.setEnabled(True) for rb in self.rbs_simulator_mode: rb.setEnabled(True) for rb in self.rbs_controllers: rb.setEnabled(True) self.start_or_stop_action = "START!" # What should happen when "START! / STOP!" is pushed NEXT time # endregion # region Methods: "Get, set, reset, quit" # Set parameters from gui_default_parameters related to generating a random experiment target position def set_random_experiment_generator_init_params(self): self.CartPoleInstance.track_relative_complexity = track_relative_complexity_init self.CartPoleInstance.length_of_experiment = length_of_experiment_init self.CartPoleInstance.interpolation_type = interpolation_type_init self.CartPoleInstance.turning_points_period = turning_points_period_init self.CartPoleInstance.start_random_target_position_at = start_random_target_position_at_init self.CartPoleInstance.end_random_target_position_at = end_random_target_position_at_init self.CartPoleInstance.turning_points = turning_points_init # Method resetting variables which change during experimental run def reset_variables(self, reset_mode=1, s=None, target_position=None): self.CartPoleInstance.set_cartpole_state_at_t0( reset_mode, s=s, target_position=target_position) self.user_time_counter = 0 # "Try" because this function is called for the first time during initialisation of the Window # when the timer label instance is not yer there. try: self.labt.setText("Time (s): " + str(float(self.user_time_counter) / 10.0)) except: pass self.experiment_or_replay_thread_terminated = False # This is a flag informing thread terminated self.terminate_experiment_or_replay_thread = False # This is a command to terminate a thread self.pause_experiment_or_replay_thread = False # This is a command to pause a thread self.start_or_stop_action = "START!" self.pause_or_unpause_action = "PAUSE" self.looper.first_call_done = False ###################################################################################################### # (Marcin) Below are methods with less critical functions. # A thread redrawing labels (except for timer, which has its own function) of GUI every 0.1 s def set_labels_thread(self): while (self.run_set_labels_thread): self.labSpeed.setText( "Speed (m/s): " + str(np.around(self.CartPoleInstance.s[POSITIOND_IDX], 2))) self.labAngle.setText("Angle (deg): " + str( np.around( self.CartPoleInstance.s[ANGLE_IDX] * 360 / (2 * np.pi), 2))) self.labMotor.setText("Motor power (Q): {:.3f}".format( np.around(self.CartPoleInstance.Q, 2))) if self.CartPoleInstance.controller_name == 'manual-stabilization': self.labTargetPosition.setText("") else: self.labTargetPosition.setText( "Target position (m): " + str(np.around(self.CartPoleInstance.target_position, 2))) if self.CartPoleInstance.controller_name == 'manual_stabilization': self.labSliderInstant.setText( "Slider instant value (-): " + str(np.around(self.slider_instant_value, 2))) else: self.labSliderInstant.setText( "Slider instant value (m): " + str(np.around(self.slider_instant_value, 2))) self.labTimeSim.setText('Simulation time (s): {:.2f}'.format( self.CartPoleInstance.time)) mean_dt_real = np.mean(self.looper.circ_buffer_dt_real) if mean_dt_real > 0: self.labSpeedUp.setText('Speed-up (measured): x{:.2f}'.format( self.CartPoleInstance.dt_simulation / mean_dt_real)) sleep(0.1) # Function to measure the time of simulation as experienced by user # It corresponds to the time of simulation according to equations only if real time mode is on # TODO (Marcin) I just retained this function from some example being my starting point # It seems it sometimes counting time to slow. Consider replacing in future def set_user_time_label(self): # "If": Increment time counter only if simulation is running if self.start_or_stop_action == "STOP!": # indicates what start button was pressed and some process is running self.user_time_counter += 1 # The updates are done smoother if the label is updated here # and not in the separate thread self.labTime.setText("Time (s): " + str(float(self.user_time_counter) / 10.0)) # The actions which has to be taken to properly terminate the application # The method is evoked after QUIT button is pressed # TODO: Can we connect it somehow also the the default cross closing the application? def quit_application(self): # Stops animation (updating changing elements of the Figure) self.anim._stop() # Stops the two threads updating the GUI labels and updating the state of Cart instance self.run_set_labels_thread = False self.terminate_experiment_or_replay_thread = True self.pause_experiment_or_replay_thread = False # Closes the GUI window self.close() # The standard command # It seems however not to be working by its own # I don't know how it works QApplication.quit() # endregion # region Mouse interaction """ These are some methods GUI uses to capture mouse effect while hoovering or clicking over/on the charts """ # Function evoked at a mouse movement # If the mouse cursor is over the lower chart it reads the corresponding value # and updates the slider def on_mouse_movement(self, event): if self.simulator_mode == 'Slider-Controlled Experiment': if event.xdata == None or event.ydata == None: pass else: if event.inaxes == self.fig.AxSlider: self.slider_instant_value = event.xdata if not self.slider_on_click: self.CartPoleInstance.update_slider( mouse_position=event.xdata) # Function evoked at a mouse click # If the mouse cursor is over the lower chart it reads the corresponding value # and updates the slider def on_mouse_click(self, event): if self.simulator_mode == 'Slider-Controlled Experiment': if event.xdata == None or event.ydata == None: pass else: if event.inaxes == self.fig.AxSlider: self.CartPoleInstance.update_slider( mouse_position=event.xdata) # endregion # region Changing "static" options: radio buttons, text boxes, combo boxes, check boxes """ This section collects methods used to change some ''static option'': e.g. change current controller, switch between saving and not saving etc. These are functions associated with radio buttons, check boxes, textfilds etc. The functions of "START! / STOP!" button is much more complex and we put them hence in a separate section. """ # region - Radio buttons # Chose the controller method which should be used with the CartPole def RadioButtons_controller_selection(self): # Change the mode variable depending on the Radiobutton state for i in range(len(self.rbs_controllers)): if self.rbs_controllers[i].isChecked(): self.CartPoleInstance.set_controller(controller_idx=i) # Reset the state of GUI and of the Cart instance after the mode has changed # TODO: Do I need the follwowing lines? self.reset_variables(0) self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) self.canvas.draw() self.open_additional_controller_widget() # Chose the simulator mode - effect of start/stop button def RadioButtons_simulator_mode(self): # Change the mode variable depending on the Radiobutton state for i in range(len(self.rbs_simulator_mode)): sleep(0.001) if self.rbs_simulator_mode[i].isChecked(): self.simulator_mode = self.available_simulator_modes[i] # Reset the state of GUI and of the Cart instance after the mode has changed # TODO: Do I need the follwowing lines? self.reset_variables(0) self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) self.canvas.draw() # Chose the noise mode - effect of start/stop button def RadioButtons_noise_on_off(self): # Change the mode variable depending on the Radiobutton state if self.rbs_noise[0].isChecked(): self.noise = 'ON' self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise elif self.rbs_noise[1].isChecked(): self.noise = 'OFF' self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise else: raise Exception('Something wrong with ON/OFF button for noise') self.open_additional_noise_widget() # endregion # region - Text Boxes # Read speedup provided by user from appropriate GUI textbox def get_speedup(self): """ Get speedup provided by user from appropriate textbox. Speed-up gives how many times faster or slower than real time the simulation or replay should run. The provided values may not always be reached due to computer speed limitation """ speedup = self.tx_speedup.text() if speedup == '': self.speedup = np.inf return True else: try: speedup = float(speedup) except ValueError: self.wrong_speedup_msg.setText( 'You have provided the input for speed-up which is not convertible to a number' ) x = self.wrong_speedup_msg.exec_() return False if speedup == 0.0: self.wrong_speedup_msg.setText( 'You cannot run an experiment with 0 speed-up (stopped time flow)' ) x = self.wrong_speedup_msg.exec_() return False else: self.speedup = speedup return True # endregion # region - Combo Boxes # Select how to interpolate between turning points of randomly chosen target positions def cb_interpolation_selectionchange(self, i): """ Select interpolation type for random target positions of randomly generated experiment """ self.CartPoleInstance.interpolation_type = self.cb_interpolation.currentText( ) # endregion # region - Check boxes # Action toggling between saving and not saving simulation results def cb_save_history_f(self, state): if state: self.save_history = 1 else: self.save_history = 0 if self.save_history or self.show_experiment_summary: self.CartPoleInstance.save_data_in_cart = True else: self.CartPoleInstance.save_data_in_cart = False # Action toggling between saving and not saving simulation results def cb_show_experiment_summary_f(self, state): if state: self.show_experiment_summary = 1 else: self.show_experiment_summary = 0 if self.save_history or self.show_experiment_summary: self.CartPoleInstance.save_data_in_cart = True else: self.CartPoleInstance.save_data_in_cart = False # Action toggling between stopping (or not) the pole if it reaches 90 deg def cb_stop_at_90_deg_f(self, state): if state: self.CartPoleInstance.stop_at_90 = True else: self.CartPoleInstance.stop_at_90 = False # Action toggling between updating CarPole slider value on click or by hoovering over it def cb_slider_on_click_f(self, state): if state: self.slider_on_click = True else: self.slider_on_click = False # endregion # region - Additional GUI Popups def open_additional_controller_widget(self): # Open up additional options widgets depending on the controller type if self.CartPoleInstance.controller_name == 'mppi': self.optionsControllerWidget = MPPIOptionsWindow() else: try: self.optionsControllerWidget.close() except: pass self.optionsControllerWidget = None def open_additional_noise_widget(self): # Open up additional options widgets depending on the controller type if self.noise == 'ON': self.optionsNoiseWidget = NoiseOptionsWindow() else: try: self.optionsNoiseWidget.close() except: pass self.optionsNoiseWidget = None # endregion # region - Sliders setting initial position and angle of the CartPole def update_initial_position(self, value: str): self.initial_state[POSITION_IDX] = float(value) / 1000.0 def update_initial_angle(self, value: str): self.initial_state[ANGLE_IDX] = float(value) / 100.0 # endregion # region - Slider setting latency of the controller def update_latency(self, value: str): latency_slider = float(value) latency = latency_slider * self.CartPoleInstance.LatencyAdderInstance.max_latency / self.LATENCY_SLIDER_RANGE_INT # latency in seconds self.CartPoleInstance.LatencyAdderInstance.set_latency(latency) self.labLatency.setText('{:.1f} ms'.format(latency * 1000.0)) # latency in ms # endregion # region Buttons for providing a kick to the pole def kick_pole(self): if self.sender().text() == "Left": self.CartPoleInstance.s[ANGLED_IDX] += .6 elif self.sender().text() == "Right": self.CartPoleInstance.s[ANGLED_IDX] -= .6
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupTrayicon() self.setupVariables() self.setupUi() self.setupConnections() self.show() def setupVariables(self): settings = QSettings() self.workEndTime = QTime( int(settings.value(workHoursKey, 0)), int(settings.value(workMinutesKey, 25)), int(settings.value(workSecondsKey, 0)), ) self.restEndTime = QTime( int(settings.value(restHoursKey, 0)), int(settings.value(restMinutesKey, 5)), int(settings.value(restSecondsKey, 0)), ) self.timeFormat = "hh:mm:ss" self.time = QTime(0, 0, 0, 0) self.workTime = QTime(0, 0, 0, 0) self.restTime = QTime(0, 0, 0, 0) self.totalTime = QTime(0, 0, 0, 0) self.currentMode = Mode.work self.maxRepetitions = -1 self.currentRepetitions = 0 def setupConnections(self): """ Create button connections """ self.startButton.clicked.connect(self.startTimer) self.startButton.clicked.connect( lambda: self.startButton.setDisabled(True)) self.startButton.clicked.connect( lambda: self.pauseButton.setDisabled(False)) self.startButton.clicked.connect( lambda: self.resetButton.setDisabled(False)) self.pauseButton.clicked.connect(self.pauseTimer) self.pauseButton.clicked.connect( lambda: self.startButton.setDisabled(False)) self.pauseButton.clicked.connect( lambda: self.pauseButton.setDisabled(True)) self.pauseButton.clicked.connect( lambda: self.resetButton.setDisabled(False)) self.pauseButton.clicked.connect( lambda: self.startButton.setText("continue")) self.resetButton.clicked.connect(self.resetTimer) self.resetButton.clicked.connect( lambda: self.startButton.setDisabled(False)) self.resetButton.clicked.connect( lambda: self.pauseButton.setDisabled(True)) self.resetButton.clicked.connect( lambda: self.resetButton.setDisabled(True)) self.resetButton.clicked.connect( lambda: self.startButton.setText("start")) self.acceptTaskButton.pressed.connect(self.insertTask) self.deleteTaskButton.pressed.connect(self.deleteTask) """ Create spinbox connections """ self.workHoursSpinBox.valueChanged.connect(self.updateWorkEndTime) self.workMinutesSpinBox.valueChanged.connect(self.updateWorkEndTime) self.workSecondsSpinBox.valueChanged.connect(self.updateWorkEndTime) self.restHoursSpinBox.valueChanged.connect(self.updateRestEndTime) self.restMinutesSpinBox.valueChanged.connect(self.updateRestEndTime) self.restSecondsSpinBox.valueChanged.connect(self.updateRestEndTime) self.repetitionsSpinBox.valueChanged.connect(self.updateMaxRepetitions) """ Create combobox connections """ self.modeComboBox.currentTextChanged.connect(self.updateCurrentMode) """ Create tablewidget connections """ self.tasksTableWidget.cellDoubleClicked.connect( self.markTaskAsFinished) def setupUi(self): self.size_policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) """ Create tabwidget """ self.tabWidget = QTabWidget() """ Create tab widgets """ timerWidget = self.setupTimerTab() tasksWidget = self.setupTasksTab() statisticsWidget = self.setupStatisticsTab() """ add tab widgets to tabwidget""" self.timerTab = self.tabWidget.addTab(timerWidget, makeIcon("timer"), "Timer") self.tasksTab = self.tabWidget.addTab(tasksWidget, makeIcon("tasks"), "Tasks") self.statisticsTab = self.tabWidget.addTab(statisticsWidget, makeIcon("statistics"), "Statistics") """ Set mainwindows central widget """ self.setCentralWidget(self.tabWidget) def setupTimerTab(self): settings = QSettings() self.timerContainer = QWidget(self) self.timerContainerLayout = QVBoxLayout(self.timerContainer) self.timerContainer.setLayout(self.timerContainerLayout) """ Create work groupbox""" self.workGroupBox = QGroupBox("Work") self.workGroupBoxLayout = QHBoxLayout(self.workGroupBox) self.workGroupBox.setLayout(self.workGroupBoxLayout) self.workHoursSpinBox = QSpinBox( minimum=0, maximum=24, value=int(settings.value(workHoursKey, 0)), suffix="h", sizePolicy=self.size_policy, ) self.workMinutesSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(workMinutesKey, 25)), suffix="m", sizePolicy=self.size_policy, ) self.workSecondsSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(workSecondsKey, 0)), suffix="s", sizePolicy=self.size_policy, ) """ Create rest groupbox""" self.restGroupBox = QGroupBox("Rest") self.restGroupBoxLayout = QHBoxLayout(self.restGroupBox) self.restGroupBox.setLayout(self.restGroupBoxLayout) self.restHoursSpinBox = QSpinBox( minimum=0, maximum=24, value=int(settings.value(restHoursKey, 0)), suffix="h", sizePolicy=self.size_policy, ) self.restMinutesSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(restMinutesKey, 5)), suffix="m", sizePolicy=self.size_policy, ) self.restSecondsSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(restSecondsKey, 0)), suffix="s", sizePolicy=self.size_policy, ) self.restGroupBoxLayout.addWidget(self.restHoursSpinBox) self.restGroupBoxLayout.addWidget(self.restMinutesSpinBox) self.restGroupBoxLayout.addWidget(self.restSecondsSpinBox) """ Create other groupbox""" self.otherGroupBox = QGroupBox("Other") self.otherGroupBoxLayout = QHBoxLayout(self.otherGroupBox) self.otherGroupBox.setLayout(self.otherGroupBoxLayout) self.repetitionsLabel = QLabel("Repetitions") self.repetitionsSpinBox = QSpinBox( minimum=0, maximum=10000, value=0, sizePolicy=self.size_policy, specialValueText="∞", ) self.modeLabel = QLabel("Mode") self.modeComboBox = QComboBox(sizePolicy=self.size_policy) self.modeComboBox.addItems(["work", "rest"]) self.otherGroupBoxLayout.addWidget(self.repetitionsLabel) self.otherGroupBoxLayout.addWidget(self.repetitionsSpinBox) self.otherGroupBoxLayout.addWidget(self.modeLabel) self.otherGroupBoxLayout.addWidget(self.modeComboBox) """ Create timer groupbox""" self.lcdDisplayGroupBox = QGroupBox("Time") self.lcdDisplayGroupBoxLayout = QHBoxLayout(self.lcdDisplayGroupBox) self.lcdDisplayGroupBox.setLayout(self.lcdDisplayGroupBoxLayout) self.timeDisplay = QLCDNumber(8, sizePolicy=self.size_policy) self.timeDisplay.setFixedHeight(100) self.timeDisplay.display("00:00:00") self.lcdDisplayGroupBoxLayout.addWidget(self.timeDisplay) """ Create pause, start and reset buttons""" self.buttonContainer = QWidget() self.buttonContainerLayout = QHBoxLayout(self.buttonContainer) self.buttonContainer.setLayout(self.buttonContainerLayout) self.startButton = self.makeButton("start", disabled=False) self.resetButton = self.makeButton("reset") self.pauseButton = self.makeButton("pause") """ Add widgets to container """ self.workGroupBoxLayout.addWidget(self.workHoursSpinBox) self.workGroupBoxLayout.addWidget(self.workMinutesSpinBox) self.workGroupBoxLayout.addWidget(self.workSecondsSpinBox) self.timerContainerLayout.addWidget(self.workGroupBox) self.timerContainerLayout.addWidget(self.restGroupBox) self.timerContainerLayout.addWidget(self.otherGroupBox) self.timerContainerLayout.addWidget(self.lcdDisplayGroupBox) self.buttonContainerLayout.addWidget(self.pauseButton) self.buttonContainerLayout.addWidget(self.startButton) self.buttonContainerLayout.addWidget(self.resetButton) self.timerContainerLayout.addWidget(self.buttonContainer) return self.timerContainer def setupTasksTab(self): settings = QSettings() """ Create vertical tasks container """ self.tasksWidget = QWidget(self.tabWidget) self.tasksWidgetLayout = QVBoxLayout(self.tasksWidget) self.tasksWidget.setLayout(self.tasksWidgetLayout) """ Create horizontal input container """ self.inputContainer = QWidget() self.inputContainer.setFixedHeight(50) self.inputContainerLayout = QHBoxLayout(self.inputContainer) self.inputContainerLayout.setContentsMargins(0, 0, 0, 0) self.inputContainer.setLayout(self.inputContainerLayout) """ Create text edit """ self.taskTextEdit = QTextEdit( placeholderText="Describe your task briefly.", undoRedoEnabled=True) """ Create vertical buttons container """ self.inputButtonContainer = QWidget() self.inputButtonContainerLayout = QVBoxLayout( self.inputButtonContainer) self.inputButtonContainerLayout.setContentsMargins(0, 0, 0, 0) self.inputButtonContainer.setLayout(self.inputButtonContainerLayout) """ Create buttons """ self.acceptTaskButton = QToolButton(icon=makeIcon("check")) self.deleteTaskButton = QToolButton(icon=makeIcon("trash")) """ Create tasks tablewidget """ self.tasksTableWidget = QTableWidget(0, 1) self.tasksTableWidget.setHorizontalHeaderLabels(["Tasks"]) self.tasksTableWidget.horizontalHeader().setStretchLastSection(True) self.tasksTableWidget.verticalHeader().setVisible(False) self.tasksTableWidget.setWordWrap(True) self.tasksTableWidget.setTextElideMode(Qt.TextElideMode.ElideNone) self.tasksTableWidget.setEditTriggers( QAbstractItemView.EditTriggers.NoEditTriggers) self.tasksTableWidget.setSelectionMode( QAbstractItemView.SelectionMode.SingleSelection) self.insertTasks(*settings.value(tasksKey, [])) """ Add widgets to container widgets """ self.inputButtonContainerLayout.addWidget(self.acceptTaskButton) self.inputButtonContainerLayout.addWidget(self.deleteTaskButton) self.inputContainerLayout.addWidget(self.taskTextEdit) self.inputContainerLayout.addWidget(self.inputButtonContainer) self.tasksWidgetLayout.addWidget(self.inputContainer) self.tasksWidgetLayout.addWidget(self.tasksTableWidget) return self.tasksWidget def setupStatisticsTab(self): """ Create statistics container """ self.statisticsContainer = QWidget() self.statisticsContainerLayout = QVBoxLayout(self.statisticsContainer) self.statisticsContainer.setLayout(self.statisticsContainerLayout) """ Create work time groupbox """ self.statisticsWorkTimeGroupBox = QGroupBox("Work Time") self.statisticsWorkTimeGroupBoxLayout = QHBoxLayout() self.statisticsWorkTimeGroupBox.setLayout( self.statisticsWorkTimeGroupBoxLayout) self.statisticsWorkTimeDisplay = QLCDNumber(8) self.statisticsWorkTimeDisplay.display("00:00:00") self.statisticsWorkTimeGroupBoxLayout.addWidget( self.statisticsWorkTimeDisplay) """ Create rest time groupbox """ self.statisticsRestTimeGroupBox = QGroupBox("Rest Time") self.statisticsRestTimeGroupBoxLayout = QHBoxLayout() self.statisticsRestTimeGroupBox.setLayout( self.statisticsRestTimeGroupBoxLayout) self.statisticsRestTimeDisplay = QLCDNumber(8) self.statisticsRestTimeDisplay.display("00:00:00") self.statisticsRestTimeGroupBoxLayout.addWidget( self.statisticsRestTimeDisplay) """ Create total time groupbox """ self.statisticsTotalTimeGroupBox = QGroupBox("Total Time") self.statisticsTotalTimeGroupBoxLayout = QHBoxLayout() self.statisticsTotalTimeGroupBox.setLayout( self.statisticsTotalTimeGroupBoxLayout) self.statisticsTotalTimeDisplay = QLCDNumber(8) self.statisticsTotalTimeDisplay.display("00:00:00") self.statisticsTotalTimeGroupBoxLayout.addWidget( self.statisticsTotalTimeDisplay) """ Add widgets to container """ self.statisticsContainerLayout.addWidget( self.statisticsTotalTimeGroupBox) self.statisticsContainerLayout.addWidget( self.statisticsWorkTimeGroupBox) self.statisticsContainerLayout.addWidget( self.statisticsRestTimeGroupBox) return self.statisticsContainer def setupTrayicon(self): self.trayIcon = QSystemTrayIcon(makeIcon("tomato")) self.trayIcon.setContextMenu(QMenu()) self.quitAction = self.trayIcon.contextMenu().addAction( makeIcon("exit"), "Quit", self.exit) self.quitAction.triggered.connect(self.exit) self.trayIcon.activated.connect(self.onActivate) self.trayIcon.show() self.trayIcon.setToolTip("Pomodoro") self.toast = ToastNotifier() def leaveEvent(self, event): super(MainWindow, self).leaveEvent(event) self.tasksTableWidget.clearSelection() def closeEvent(self, event): super(MainWindow, self).closeEvent(event) settings = QSettings() settings.setValue(workHoursKey, self.workHoursSpinBox.value()) settings.setValue( workMinutesKey, self.workMinutesSpinBox.value(), ) settings.setValue( workSecondsKey, self.workSecondsSpinBox.value(), ) settings.setValue(restHoursKey, self.restHoursSpinBox.value()) settings.setValue( restMinutesKey, self.restMinutesSpinBox.value(), ) settings.setValue( restSecondsKey, self.restSecondsSpinBox.value(), ) tasks = [] for i in range(self.tasksTableWidget.rowCount()): item = self.tasksTableWidget.item(i, 0) if not item.font().strikeOut(): tasks.append(item.text()) settings.setValue(tasksKey, tasks) def startTimer(self): try: if not self.timer.isActive(): self.createTimer() except: self.createTimer() def createTimer(self): self.timer = QTimer() self.timer.timeout.connect(self.updateTime) self.timer.timeout.connect(self.maybeChangeMode) self.timer.setInterval(1000) self.timer.setSingleShot(False) self.timer.start() def pauseTimer(self): try: self.timer.stop() self.timer.disconnect() except: pass def resetTimer(self): try: self.pauseTimer() self.time = QTime(0, 0, 0, 0) self.displayTime() except: pass def maybeStartTimer(self): if self.currentRepetitions != self.maxRepetitions: self.startTimer() started = True else: self.currentRepetitions = 0 started = False return started def updateWorkEndTime(self): self.workEndTime = QTime( self.workHoursSpinBox.value(), self.workMinutesSpinBox.value(), self.workSecondsSpinBox.value(), ) def updateRestEndTime(self): self.restEndTime = QTime( self.restHoursSpinBox.value(), self.restMinutesSpinBox.value(), self.restSecondsSpinBox.value(), ) def updateCurrentMode(self, mode: str): self.currentMode = Mode.work if mode == "work" else Mode.rest def updateTime(self): self.time = self.time.addSecs(1) self.totalTime = self.totalTime.addSecs(1) if self.modeComboBox.currentText() == "work": self.workTime = self.workTime.addSecs(1) else: self.restTime = self.restTime.addSecs(1) self.displayTime() def updateMaxRepetitions(self, value): if value == 0: self.currentRepetitions = 0 self.maxRepetitions = -1 else: self.maxRepetitions = 2 * value def maybeChangeMode(self): if self.currentMode is Mode.work and self.time >= self.workEndTime: self.resetTimer() self.modeComboBox.setCurrentIndex(1) self.incrementCurrentRepetitions() started = self.maybeStartTimer() self.showWindowMessage( Status.workFinished if started else Status.repetitionsReached) if not started: self.resetButton.click() elif self.currentMode is Mode.rest and self.time >= self.restEndTime: self.resetTimer() self.modeComboBox.setCurrentIndex(0) self.incrementCurrentRepetitions() started = self.maybeStartTimer() self.showWindowMessage( Status.restFinished if started else Status.repetitionsReached) if not started: self.resetButton.click() def incrementCurrentRepetitions(self): if self.maxRepetitions > 0: self.currentRepetitions += 1 def insertTask(self): task = self.taskTextEdit.toPlainText() self.insertTasks(task) def insertTasks(self, *tasks): for task in tasks: if task: rowCount = self.tasksTableWidget.rowCount() self.tasksTableWidget.setRowCount(rowCount + 1) self.tasksTableWidget.setItem(rowCount, 0, QTableWidgetItem(task)) self.tasksTableWidget.resizeRowsToContents() self.taskTextEdit.clear() def deleteTask(self): selectedIndexes = self.tasksTableWidget.selectedIndexes() if selectedIndexes: self.tasksTableWidget.removeRow(selectedIndexes[0].row()) def markTaskAsFinished(self, row, col): item = self.tasksTableWidget.item(row, col) font = self.tasksTableWidget.item(row, col).font() font.setStrikeOut(False if item.font().strikeOut() else True) item.setFont(font) def displayTime(self): self.timeDisplay.display(self.time.toString(self.timeFormat)) self.statisticsRestTimeDisplay.display( self.restTime.toString(self.timeFormat)) self.statisticsWorkTimeDisplay.display( self.workTime.toString(self.timeFormat)) self.statisticsTotalTimeDisplay.display( self.totalTime.toString(self.timeFormat)) def showWindowMessage(self, status): if status is Status.workFinished: title, text = "Break", choice(work_finished_phrases) elif status is Status.restFinished: title, text = "Work", choice(rest_finished_phrases) else: title, text = "Finished", choice(work_finished_phrases) self.trayIcon.showMessage(title, text, makeIcon("tomato")) self.toast.show_toast(title, text, icon_path="pomodoro/data/icons/tomato.ico", duration=10, threaded=True) def makeButton(self, text, iconName=None, disabled=True): button = QPushButton(text, sizePolicy=self.size_policy) if iconName: button.setIcon(makeIcon(iconName)) button.setDisabled(disabled) return button def exit(self): self.close() app = QApplication.instance() if app: app.quit() def onActivate(self, reason): if reason == QSystemTrayIcon.ActivationReason.Trigger: self.show()
class TabAntiWarping(Tool): def __init__(self): super().__init__() # variable for menu dialog self._UseSize = 0.0 self._UseOffset = 0.0 self._AsCapsule = False self._Nb_Layer = 1 # Shortcut if not VERSION_QT5: self._shortcut_key = Qt.Key.Key_I else: self._shortcut_key = Qt.Key_I self._controller = self.getController() self._selection_pass = None # self._i18n_catalog = None self.Major=1 self.Minor=0 # Logger.log('d', "Info Version CuraVersion --> " + str(Version(CuraVersion))) Logger.log('d', "Info CuraVersion --> " + str(CuraVersion)) # Test version for Cura Master # https://github.com/smartavionics/Cura if "master" in CuraVersion : self.Major=4 self.Minor=20 else: try: self.Major = int(CuraVersion.split(".")[0]) self.Minor = int(CuraVersion.split(".")[1]) except: pass self.setExposedProperties("SSize", "SOffset", "SCapsule", "NLayer") CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled) # Note: if the selection is cleared with this tool active, there is no way to switch to # another tool than to reselect an object (by clicking it) because the tool buttons in the # toolbar will have been disabled. That is why we need to ignore the first press event # after the selection has been cleared. Selection.selectionChanged.connect(self._onSelectionChanged) self._had_selection = False self._skip_press = False self._had_selection_timer = QTimer() self._had_selection_timer.setInterval(0) self._had_selection_timer.setSingleShot(True) self._had_selection_timer.timeout.connect(self._selectionChangeDelay) # set the preferences to store the default value self._preferences = CuraApplication.getInstance().getPreferences() self._preferences.addPreference("customsupportcylinder/p_size", 10) # convert as float to avoid further issue self._UseSize = float(self._preferences.getValue("customsupportcylinder/p_size")) self._preferences.addPreference("customsupportcylinder/p_offset", 0.16) # convert as float to avoid further issue self._UseOffset = float(self._preferences.getValue("customsupportcylinder/p_offset")) self._preferences.addPreference("customsupportcylinder/as_capsule", False) # convert as float to avoid further issue self._AsCapsule = bool(self._preferences.getValue("customsupportcylinder/as_capsule")) self._preferences.addPreference("customsupportcylinder/nb_layer", 1) # convert as float to avoid further issue self._Nb_Layer = int(self._preferences.getValue("customsupportcylinder/nb_layer")) def event(self, event): super().event(event) modifiers = QApplication.keyboardModifiers() if not VERSION_QT5: ctrl_is_active = modifiers & Qt.KeyboardModifier.ControlModifier else: ctrl_is_active = modifiers & Qt.ControlModifier if event.type == Event.MousePressEvent and MouseEvent.LeftButton in event.buttons and self._controller.getToolsEnabled(): if ctrl_is_active: self._controller.setActiveTool("TranslateTool") return if self._skip_press: # The selection was previously cleared, do not add/remove an support mesh but # use this click for selection and reactivating this tool only. self._skip_press = False return if self._selection_pass is None: # The selection renderpass is used to identify objects in the current view self._selection_pass = CuraApplication.getInstance().getRenderer().getRenderPass("selection") picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y)) if not picked_node: # There is no slicable object at the picked location return node_stack = picked_node.callDecoration("getStack") if node_stack: if node_stack.getProperty("support_mesh", "value"): self._removeSupportMesh(picked_node) return elif node_stack.getProperty("anti_overhang_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("support_mesh", "value"): # Only "normal" meshes can have support_mesh added to them return # Create a pass for picking a world-space location from the mouse location active_camera = self._controller.getScene().getActiveCamera() picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) picking_pass.render() picked_position = picking_pass.getPickedPosition(event.x, event.y) # Add the support_mesh cube at the picked location self._createSupportMesh(picked_node, picked_position) def _createSupportMesh(self, parent: CuraSceneNode, position: Vector): node = CuraSceneNode() node.setName("RoundTab") node.setSelectable(True) # long=Support Height _long=position.y # get layer_height_0 used to define pastille height _id_ex=0 # This function can be triggered in the middle of a machine change, so do not proceed if the machine change # has not done yet. global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() #extruder = global_container_stack.extruderList[int(_id_ex)] extruder_stack = CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks()[0] _layer_h_i = extruder_stack.getProperty("layer_height_0", "value") _layer_height = extruder_stack.getProperty("layer_height", "value") _line_w = extruder_stack.getProperty("line_width", "value") # Logger.log('d', 'layer_height_0 : ' + str(_layer_h_i)) _layer_h = (_layer_h_i * 1.2) + (_layer_height * (self._Nb_Layer -1) ) _line_w = _line_w * 1.2 if self._AsCapsule: # Capsule creation Diameter , Increment angle 4°, length, layer_height_0*1.2 , line_width mesh = self._createCapsule(self._UseSize,4,_long,_layer_h,_line_w) else: # Cylinder creation Diameter , Increment angle 4°, length, layer_height_0*1.2 mesh = self._createPastille(self._UseSize,4,_long,_layer_h) node.setMeshData(mesh.build()) active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate node.addDecorator(BuildPlateDecorator(active_build_plate)) node.addDecorator(SliceableObjectDecorator()) stack = node.callDecoration("getStack") # created by SettingOverrideDecorator that is automatically added to CuraSceneNode settings = stack.getTop() # support_mesh type definition = stack.getSettingDefinition("support_mesh") new_instance = SettingInstance(definition, settings) new_instance.setProperty("value", True) new_instance.resetState() # Ensure that the state is not seen as a user state. settings.addInstance(new_instance) definition = stack.getSettingDefinition("support_mesh_drop_down") new_instance = SettingInstance(definition, settings) new_instance.setProperty("value", False) new_instance.resetState() # Ensure that the state is not seen as a user state. settings.addInstance(new_instance) if self._AsCapsule: s_p = global_container_stack.getProperty("support_type", "value") if s_p == 'buildplate' : Message(text = "Info modification current profile support_type parameter\nNew value : everywhere", title = catalog.i18nc("@info:title", "Warning ! Tab Anti Warping")).show() Logger.log('d', 'support_type different : ' + str(s_p)) # Define support_type=everywhere global_container_stack.setProperty("support_type", "value", 'everywhere') # Define support_xy_distance definition = stack.getSettingDefinition("support_xy_distance") new_instance = SettingInstance(definition, settings) new_instance.setProperty("value", self._UseOffset) # new_instance.resetState() # Ensure that the state is not seen as a user state. settings.addInstance(new_instance) # Fix some settings in Cura to get a better result id_ex=0 global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() extruder_stack = CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks()[0] #extruder = global_container_stack.extruderList[int(id_ex)] # hop to fix it in a futur release # https://github.com/Ultimaker/Cura/issues/9882 # if self.Major < 5 or ( self.Major == 5 and self.Minor < 1 ) : _xy_distance = extruder_stack.getProperty("support_xy_distance", "value") if self._UseOffset != _xy_distance : _msg = "New value : %8.3f" % (self._UseOffset) Message(text = "Info modification current profile support_xy_distance parameter\nNew value : %8.3f" % (self._UseOffset), title = catalog.i18nc("@info:title", "Warning ! Tab Anti Warping")).show() Logger.log('d', 'support_xy_distance different : ' + str(_xy_distance)) # Define support_xy_distance extruder_stack.setProperty("support_xy_distance", "value", self._UseOffset) if self._Nb_Layer >1 : s_p = int(extruder_stack.getProperty("support_infill_rate", "value")) Logger.log('d', 'support_infill_rate actual : ' + str(s_p)) if s_p < 99 : Message(text = "Info modification current profile support_infill_rate parameter\nNew value : 100%", title = catalog.i18nc("@info:title", "Warning ! Tab Anti Warping")).show() Logger.log('d', 'support_infill_rate different : ' + str(s_p)) # Define support_infill_rate=100% extruder_stack.setProperty("support_infill_rate", "value", 100) op = GroupedOperation() # First add node to the scene at the correct position/scale, before parenting, so the support mesh does not get scaled with the parent op.addOperation(AddSceneNodeOperation(node, self._controller.getScene().getRoot())) op.addOperation(SetParentOperation(node, parent)) op.push() node.setPosition(position, CuraSceneNode.TransformSpace.World) CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node) def _removeSupportMesh(self, node: CuraSceneNode): parent = node.getParent() if parent == self._controller.getScene().getRoot(): parent = None op = RemoveSceneNodeOperation(node) op.push() if parent and not Selection.isSelected(parent): Selection.add(parent) CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node) def _updateEnabled(self): plugin_enabled = False global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_container_stack: plugin_enabled = global_container_stack.getProperty("support_mesh", "enabled") CuraApplication.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled) def _onSelectionChanged(self): # When selection is passed from one object to another object, first the selection is cleared # and then it is set to the new object. We are only interested in the change from no selection # to a selection or vice-versa, not in a change from one object to another. A timer is used to # "merge" a possible clear/select action in a single frame if Selection.hasSelection() != self._had_selection: self._had_selection_timer.start() def _selectionChangeDelay(self): has_selection = Selection.hasSelection() if not has_selection and self._had_selection: self._skip_press = True else: self._skip_press = False self._had_selection = has_selection # Capsule creation def _createCapsule(self, size, nb , lg, He, lw): mesh = MeshBuilder() # Per-vertex normals require duplication of vertices r = size / 2 # First layer length sup = -lg + He if self._Nb_Layer >1 : sup_c = -lg + (He * 2) else: sup_c = -lg + (He * 3) l = -lg rng = int(360 / nb) ang = math.radians(nb) r_sup=math.tan(math.radians(45))*(He * 3)+r # Top inside radius ri=r_sup-(1.8*lw) # Top radius rit=r-(1.8*lw) verts = [] for i in range(0, rng): # Top verts.append([ri*math.cos(i*ang), sup_c, ri*math.sin(i*ang)]) verts.append([r_sup*math.cos((i+1)*ang), sup_c, r_sup*math.sin((i+1)*ang)]) verts.append([r_sup*math.cos(i*ang), sup_c, r_sup*math.sin(i*ang)]) verts.append([ri*math.cos((i+1)*ang), sup_c, ri*math.sin((i+1)*ang)]) verts.append([r_sup*math.cos((i+1)*ang), sup_c, r_sup*math.sin((i+1)*ang)]) verts.append([ri*math.cos(i*ang), sup_c, ri*math.sin(i*ang)]) #Side 1a verts.append([r_sup*math.cos(i*ang), sup_c, r_sup*math.sin(i*ang)]) verts.append([r_sup*math.cos((i+1)*ang), sup_c, r_sup*math.sin((i+1)*ang)]) verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)]) #Side 1b verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)]) verts.append([r*math.cos(i*ang), l, r*math.sin(i*ang)]) verts.append([r_sup*math.cos(i*ang), sup_c, r_sup*math.sin(i*ang)]) #Side 2a verts.append([rit*math.cos((i+1)*ang), sup, rit*math.sin((i+1)*ang)]) verts.append([ri*math.cos((i+1)*ang), sup_c, ri*math.sin((i+1)*ang)]) verts.append([ri*math.cos(i*ang), sup_c, ri*math.sin(i*ang)]) #Side 2b verts.append([ri*math.cos(i*ang), sup_c, ri*math.sin(i*ang)]) verts.append([rit*math.cos(i*ang), sup, rit*math.sin(i*ang)]) verts.append([rit*math.cos((i+1)*ang), sup, rit*math.sin((i+1)*ang)]) #Bottom Top verts.append([0, sup, 0]) verts.append([rit*math.cos((i+1)*ang), sup, rit*math.sin((i+1)*ang)]) verts.append([rit*math.cos(i*ang), sup, rit*math.sin(i*ang)]) #Bottom verts.append([0, l, 0]) verts.append([r*math.cos(i*ang), l, r*math.sin(i*ang)]) verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)]) mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32)) indices = [] # for every angle increment 24 Vertices tot = rng * 24 for i in range(0, tot, 3): # indices.append([i, i+1, i+2]) mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32)) mesh.calculateNormals() return mesh # Cylinder creation def _createPastille(self, size, nb , lg, He): mesh = MeshBuilder() # Per-vertex normals require duplication of vertices r = size / 2 # First layer length sup = -lg + He l = -lg rng = int(360 / nb) ang = math.radians(nb) verts = [] for i in range(0, rng): # Top verts.append([0, sup, 0]) verts.append([r*math.cos((i+1)*ang), sup, r*math.sin((i+1)*ang)]) verts.append([r*math.cos(i*ang), sup, r*math.sin(i*ang)]) #Side 1a verts.append([r*math.cos(i*ang), sup, r*math.sin(i*ang)]) verts.append([r*math.cos((i+1)*ang), sup, r*math.sin((i+1)*ang)]) verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)]) #Side 1b verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)]) verts.append([r*math.cos(i*ang), l, r*math.sin(i*ang)]) verts.append([r*math.cos(i*ang), sup, r*math.sin(i*ang)]) #Bottom verts.append([0, l, 0]) verts.append([r*math.cos(i*ang), l, r*math.sin(i*ang)]) verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)]) mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32)) indices = [] # for every angle increment 12 Vertices tot = rng * 12 for i in range(0, tot, 3): # indices.append([i, i+1, i+2]) mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32)) mesh.calculateNormals() return mesh def getSSize(self) -> float: """ return: golabl _UseSize in mm. """ return self._UseSize def setSSize(self, SSize: str) -> None: """ param SSize: Size in mm. """ try: s_value = float(SSize) except ValueError: return if s_value <= 0: return #Logger.log('d', 's_value : ' + str(s_value)) self._UseSize = s_value self._preferences.setValue("customsupportcylinder/p_size", s_value) def getNLayer(self) -> int: """ return: golabl _Nb_Layer """ return self._Nb_Layer def setNLayer(self, NLayer: str) -> None: """ param NLayer: NLayer as integer >1 """ try: i_value = int(NLayer) except ValueError: return if i_value < 1: return Logger.log('d', 'i_value : ' + str(i_value)) self._Nb_Layer = i_value self._preferences.setValue("customsupportcylinder/nb_layer", i_value) def getSOffset(self) -> float: """ return: golabl _UseOffset in mm. """ return self._UseOffset def setSOffset(self, SOffset: str) -> None: """ param SOffset: SOffset in mm. """ try: s_value = float(SOffset) except ValueError: return #Logger.log('d', 's_value : ' + str(s_value)) self._UseOffset = s_value self._preferences.setValue("customsupportcylinder/p_offset", s_value) def getSCapsule(self) -> bool: """ return: golabl _AsCapsule as boolean """ return self._AsCapsule def setSCapsule(self, SCapsule: bool) -> None: """ param SCapsule: as boolean. """ self._AsCapsule = SCapsule self._preferences.setValue("customsupportcylinder/as_capsule", SCapsule)
class HttpRequestData(QObject): # Add some tolerance for scheduling the QTimer to check for timeouts, because the QTimer may trigger the event a # little earlier. For example, with a 5000ms interval, the timer event can be triggered after 4752ms, so a request # may never timeout if we don't add some tolerance. # 4752ms (actual) and 5000ms (expected) has about 6% difference, so here I use 15% to be safer. TIMEOUT_CHECK_TOLERANCE = 0.15 def __init__(self, request_id: str, http_method: str, request: "QNetworkRequest", manager_timeout_callback: Callable[["HttpRequestData"], None], data: Optional[Union[bytes, bytearray]] = None, callback: Optional[Callable[["QNetworkReply"], None]] = None, error_callback: Optional[ Callable[["QNetworkReply", "QNetworkReply.NetworkError"], None]] = None, download_progress_callback: Optional[Callable[[int, int], None]] = None, upload_progress_callback: Optional[Callable[[int, int], None]] = None, timeout: Optional[float] = None, reply: Optional["QNetworkReply"] = None, parent: Optional["QObject"] = None) -> None: super().__init__(parent=parent) # Sanity checks if timeout is not None and timeout <= 0: raise ValueError( "Timeout must be a positive value, but got [%s] instead." % timeout) self._request_id = request_id self.http_method = http_method self.request = request self.data = data self.callback = callback self.error_callback = error_callback self.download_progress_callback = download_progress_callback self.upload_progress_callback = upload_progress_callback self._timeout = timeout self.reply = reply # For benchmarking. For calculating the time a request spent pending. self._create_time = time.time() # The timestamp when this request was initially issued to the QNetworkManager. This field to used to track and # manage timeouts (if set) for the requests. self._start_time = None # type: Optional[float] self.is_aborted_due_to_timeout = False self._last_response_time = float(0) self._timeout_timer = QTimer(parent=self) if self._timeout is not None: self._timeout_timer.setSingleShot(True) timeout_check_interval = int(self._timeout * 1000 * (1 + self.TIMEOUT_CHECK_TOLERANCE)) self._timeout_timer.setInterval(timeout_check_interval) self._timeout_timer.timeout.connect(self._onTimeoutTimerTriggered) self._manager_timeout_callback = manager_timeout_callback @property def request_id(self) -> str: return self._request_id @property def timeout(self) -> Optional[float]: return self._timeout @property def start_time(self) -> Optional[float]: return self._start_time # For benchmarking. Time in seconds that this request stayed in the pending queue. @property def pending_time(self) -> Optional[float]: if self._start_time is None: return None return self._start_time - self._create_time # Sets the start time of this request. This is called when this request is issued to the QNetworkManager. def setStartTime(self, start_time: float) -> None: self._start_time = start_time # Prepare timeout handling if self._timeout is not None: self._last_response_time = start_time self._timeout_timer.start() # Do some cleanup, such as stopping the timeout timer. def setDone(self) -> None: if self._timeout is not None: self._timeout_timer.stop() self._timeout_timer.timeout.disconnect( self._onTimeoutTimerTriggered) # Since Qt 5.12, pyqtSignal().connect() will return a Connection instance that represents a connection. This # Connection instance can later be used to disconnect for cleanup purpose. We are using Qt 5.10 and this feature # is not available yet, and I'm not sure if disconnecting a lambda can potentially cause issues. For this reason, # I'm using the following facade callback functions to handle the lambda function cases. def onDownloadProgressCallback(self, bytes_received: int, bytes_total: int) -> None: # Update info for timeout handling if self._timeout is not None: now = time.time() time_last = now - self._last_response_time self._last_response_time = time.time() # We've got a response, restart the timeout timer self._timeout_timer.start() if self.download_progress_callback is not None: self.download_progress_callback(bytes_received, bytes_total) def onUploadProgressCallback(self, bytes_sent: int, bytes_total: int) -> None: # Update info for timeout handling if self._timeout is not None: now = time.time() time_last = now - self._last_response_time self._last_response_time = time.time() # We've got a response, restart the timeout timer self._timeout_timer.start() if self.upload_progress_callback is not None: self.upload_progress_callback(bytes_sent, bytes_total) def _onTimeoutTimerTriggered(self) -> None: # Make typing happy if self._timeout is None: return if self.reply is None: return now = time.time() time_last = now - self._last_response_time if self.reply.isRunning() and time_last >= self._timeout: self._manager_timeout_callback(self) else: self._timeout_timer.start() def __str__(self) -> str: data = "no-data" if self.data: data = str(self.data[:10]) if len(self.data) > 10: data += "..." return "request[{id}][{method}][{url}][timeout={timeout}][{data}]".format( id=self._request_id[:8], method=self.http_method, url=self.request.url(), timeout=self._timeout, data=data)
class TranslateTool(Tool): """Provides the tool to move meshes and groups. The tool exposes a ToolHint to show the distance of the current operation. """ def __init__(self) -> None: super().__init__() self._handle = TranslateToolHandle.TranslateToolHandle( ) #type: TranslateToolHandle.TranslateToolHandle #Because for some reason MyPy thinks this variable contains Optional[ToolHandle]. self._enabled_axis = [ ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis ] self._grid_snap = False self._grid_size = 10 self._moved = False self._shortcut_key = Qt.Key.Key_T self._distance_update_time = None #type: Optional[float] self._distance = None #type: Optional[Vector] self.setExposedProperties("ToolHint", "X", "Y", "Z", SceneNodeSettings.LockPosition) self._update_selection_center_timer = QTimer() self._update_selection_center_timer.setInterval(50) self._update_selection_center_timer.setSingleShot(True) self._update_selection_center_timer.timeout.connect( self.propertyChanged.emit) # Ensure that the properties (X, Y & Z) are updated whenever the selection center is changed. Selection.selectionCenterChanged.connect( self._onSelectionCenterChanged) # CURA-5966 Make sure to render whenever objects get selected/deselected. Selection.selectionChanged.connect(self.propertyChanged) def _onSelectionCenterChanged(self): self._update_selection_center_timer.start() def getX(self) -> float: """Get the x-location of the selection bounding box center. :return: X location in mm. """ if Selection.hasSelection(): return float(Selection.getBoundingBox().center.x) return 0.0 def getY(self) -> float: """Get the y-location of the selection bounding box center. :return: Y location in mm. """ if Selection.hasSelection(): # Note; The switching of z & y is intentional. We display z as up for the user, # But store the data in openGL space. return float(Selection.getBoundingBox().center.z) return 0.0 def getZ(self) -> float: """Get the z-location of the selection bounding box bottom The bottom is used as opposed to the center, because the biggest use case is to push the selection into the build plate. :return: Z location in mm. """ # We want to display based on the bottom instead of the actual coordinate. if Selection.hasSelection(): # Note; The switching of z & y is intentional. We display z as up for the user, # But store the data in openGL space. return float(Selection.getBoundingBox().bottom) return 0.0 @staticmethod def _parseFloat(str_value: str) -> float: try: parsed_value = float(str_value) except ValueError: parsed_value = float(0) return parsed_value def setX(self, x: str) -> None: """Set the x-location of the selected object(s) by translating relative to the selection bounding box center. :param x: Location in mm. """ parsed_x = self._parseFloat(x) bounding_box = Selection.getBoundingBox() if not Float.fuzzyCompare(parsed_x, float(bounding_box.center.x), DIMENSION_TOLERANCE): selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors() if len(selected_nodes) > 1: op = GroupedOperation() for selected_node in self._getSelectedObjectsWithoutSelectedAncestors( ): world_position = selected_node.getWorldPosition() new_position = world_position.set( x=parsed_x + (world_position.x - bounding_box.center.x)) node_op = TranslateOperation(selected_node, new_position, set_position=True) op.addOperation(node_op) op.push() else: for selected_node in self._getSelectedObjectsWithoutSelectedAncestors( ): world_position = selected_node.getWorldPosition() new_position = world_position.set( x=parsed_x + (world_position.x - bounding_box.center.x)) TranslateOperation(selected_node, new_position, set_position=True).push() self._controller.toolOperationStopped.emit(self) def setY(self, y: str) -> None: """Set the y-location of the selected object(s) by translating relative to the selection bounding box center. :param y: Location in mm. """ parsed_y = self._parseFloat(y) bounding_box = Selection.getBoundingBox() if not Float.fuzzyCompare(parsed_y, float(bounding_box.center.z), DIMENSION_TOLERANCE): selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors() if len(selected_nodes) > 1: op = GroupedOperation() for selected_node in selected_nodes: # Note; The switching of z & y is intentional. We display z as up for the user, # But store the data in openGL space. world_position = selected_node.getWorldPosition() new_position = world_position.set( z=parsed_y + (world_position.z - bounding_box.center.z)) node_op = TranslateOperation(selected_node, new_position, set_position=True) op.addOperation(node_op) op.push() else: for selected_node in selected_nodes: world_position = selected_node.getWorldPosition() new_position = world_position.set( z=parsed_y + (world_position.z - bounding_box.center.z)) TranslateOperation(selected_node, new_position, set_position=True).push() self._controller.toolOperationStopped.emit(self) def setZ(self, z: str) -> None: """Set the y-location of the selected object(s) by translating relative to the selection bounding box bottom. :param z: Location in mm. """ parsed_z = self._parseFloat(z) bounding_box = Selection.getBoundingBox() if not Float.fuzzyCompare(parsed_z, float(bounding_box.bottom), DIMENSION_TOLERANCE): selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors() if len(selected_nodes) > 1: op = GroupedOperation() for selected_node in selected_nodes: # Note: The switching of z & y is intentional. We display z as up for the user, # But store the data in openGL space. world_position = selected_node.getWorldPosition() new_position = world_position.set( y=parsed_z + (world_position.y - bounding_box.bottom)) node_op = TranslateOperation(selected_node, new_position, set_position=True) op.addOperation(node_op) op.push() else: for selected_node in selected_nodes: world_position = selected_node.getWorldPosition() new_position = world_position.set( y=parsed_z + (world_position.y - bounding_box.bottom)) TranslateOperation(selected_node, new_position, set_position=True).push() self._controller.toolOperationStopped.emit(self) def setEnabledAxis(self, axis: List[int]) -> None: """Set which axis/axes are enabled for the current translate operation :param axis: List of axes (expressed as ToolHandle enum). """ self._enabled_axis = axis self._handle.setEnabledAxis(axis) def setLockPosition(self, value: bool) -> None: """Set lock setting to the object. This setting will be used to prevent model movement on the build plate. :param value: The setting state. """ for selected_node in self._getSelectedObjectsWithoutSelectedAncestors( ): selected_node.setSetting(SceneNodeSettings.LockPosition, str(value)) def getLockPosition(self) -> Union[str, bool]: total_size = Selection.getCount() false_state_counter = 0 true_state_counter = 0 if not Selection.hasSelection(): return False for selected_node in self._getSelectedObjectsWithoutSelectedAncestors( ): if selected_node.getSetting(SceneNodeSettings.LockPosition, "False") != "False": true_state_counter += 1 else: false_state_counter += 1 if total_size == false_state_counter: # No locked positions return False elif total_size == true_state_counter: # All selected objects are locked return True else: return "partially" # At least one, but not all are locked def event(self, event: Event) -> bool: """Handle mouse and keyboard events. :param event: The event to handle. :return: Whether this event has been caught by this tool (True) or should be passed on (False). """ super().event(event) # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes if event.type == Event.ToolActivateEvent: for node in self._getSelectedObjectsWithoutSelectedAncestors(): node.boundingBoxChanged.connect(self.propertyChanged) if event.type == Event.ToolDeactivateEvent: for node in self._getSelectedObjectsWithoutSelectedAncestors(): node.boundingBoxChanged.disconnect(self.propertyChanged) if event.type == Event.KeyPressEvent and cast( KeyEvent, event).key == KeyEvent.ShiftKey: return False if event.type == Event.MousePressEvent and self._controller.getToolsEnabled( ): # Start a translate operation if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: return False if not self._selection_pass: return False id = self._selection_pass.getIdAtPosition( cast(MouseEvent, event).x, cast(MouseEvent, event).y) if not id: return False if id in self._enabled_axis: self.setLockedAxis(id) elif self._handle.isAxis(id): return False self._moved = False camera = self._controller.getScene().getActiveCamera() if not camera: return False camera_direction = camera.getPosition().normalized() abs_x = abs(camera_direction.x) abs_y = abs(camera_direction.y) # We have to define a plane vector that is suitable for the selected toolhandle axis # and at the same time the camera direction should not be exactly perpendicular to the plane vector if id == ToolHandle.XAxis: plane_vector = Vector(0, camera_direction.y, camera_direction.z).normalized() elif id == ToolHandle.YAxis: plane_vector = Vector(camera_direction.x, 0, camera_direction.z).normalized() elif id == ToolHandle.ZAxis: plane_vector = Vector(camera_direction.x, camera_direction.y, 0).normalized() else: if abs_y > DIRECTION_TOLERANCE: plane_vector = Vector(0, 1, 0) elif abs_x > DIRECTION_TOLERANCE: plane_vector = Vector(1, 0, 0) self.setLockedAxis( ToolHandle.ZAxis) # Do not move y / vertical else: plane_vector = Vector(0, 0, 1) self.setLockedAxis( ToolHandle.XAxis) # Do not move y / vertical self.setDragPlane(Plane(plane_vector, 0)) return True if event.type == Event.MouseMoveEvent: # Perform a translate operation if not self.getDragPlane(): return False x = cast(MouseEvent, event).x y = cast(MouseEvent, event).y if not self.getDragStart(): self.setDragStart(x, y) return False drag = self.getDragVector(x, y) if drag: if self._grid_snap and drag.length() < self._grid_size: return False if self.getLockedAxis() == ToolHandle.XAxis: drag = drag.set(y=0, z=0) elif self.getLockedAxis() == ToolHandle.YAxis: drag = drag.set(x=0, z=0) elif self.getLockedAxis() == ToolHandle.ZAxis: drag = drag.set(x=0, y=0) if not self._moved: self._moved = True self._distance = Vector(0, 0, 0) self.operationStarted.emit(self) selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors( ) if len(selected_nodes) > 1: op = GroupedOperation() for node in selected_nodes: if node.getSetting(SceneNodeSettings.LockPosition, "False") == "False": op.addOperation(TranslateOperation(node, drag)) op.push() else: for node in selected_nodes: if node.getSetting(SceneNodeSettings.LockPosition, "False") == "False": TranslateOperation(node, drag).push() if not self._distance: self._distance = Vector(0, 0, 0) self._distance += drag self.setDragStart(x, y) # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._distance_update_time or new_time - self._distance_update_time > 0.1: self.propertyChanged.emit() self._distance_update_time = new_time return True if event.type == Event.MouseReleaseEvent: # Finish a translate operation if self.getDragPlane(): self.operationStopped.emit(self) self._distance = None self.propertyChanged.emit() self.setLockedAxis(ToolHandle.NoAxis) self.setDragPlane(None) self.setDragStart( cast(MouseEvent, event).x, cast(MouseEvent, event).y) return True return False def getToolHint(self) -> Optional[str]: """Return a formatted distance of the current translate operation. :return: Fully formatted string showing the distance by which the mesh(es) are dragged. """ return "%.2f mm" % self._distance.length() if self._distance else None
class InstanceContainersModel(ListModel): """Model that holds instance containers. By setting the filter property the instances held by this model can be changed. """ NameRole = Qt.ItemDataRole.UserRole + 1 # Human readable name (string) IdRole = Qt.ItemDataRole.UserRole + 2 # Unique ID of the InstanceContainer MetaDataRole = Qt.ItemDataRole.UserRole + 3 ReadOnlyRole = Qt.ItemDataRole.UserRole + 4 SectionRole = Qt.ItemDataRole.UserRole + 5 def __init__(self, parent=None) -> None: super().__init__(parent) self.addRoleName(self.NameRole, "name") self.addRoleName(self.IdRole, "id") self.addRoleName(self.MetaDataRole, "metadata") self.addRoleName(self.ReadOnlyRole, "readOnly") self.addRoleName(self.SectionRole, "section") #We keep track of two sets: One for normal containers that are already fully loaded, and one for containers of which only metadata is known. #Both of these are indexed by their container ID. self._instance_containers = {} #type: Dict[str, InstanceContainer] self._instance_containers_metadata = { } # type: Dict[str, Dict[str, Any]] self._section_property = "" # Listen to changes ContainerRegistry.getInstance().containerAdded.connect( self._onContainerChanged) ContainerRegistry.getInstance().containerRemoved.connect( self._onContainerChanged) ContainerRegistry.getInstance().containerLoadComplete.connect( self._onContainerLoadComplete) self._container_change_timer = QTimer() self._container_change_timer.setInterval(150) self._container_change_timer.setSingleShot(True) self._container_change_timer.timeout.connect(self._update) # List of filters for queries. The result is the union of the each list of results. self._filter_dicts = [] # type: List[Dict[str, str]] self._container_change_timer.start() def _onContainerChanged(self, container: ContainerInterface) -> None: """Handler for container added / removed events from registry""" # We only need to update when the changed container is a instanceContainer if isinstance(container, InstanceContainer): self._container_change_timer.start() def _update(self) -> None: """Private convenience function to reset & repopulate the model.""" #You can only connect on the instance containers, not on the metadata. #However the metadata can't be edited, so it's not needed. for container in self._instance_containers.values(): container.metaDataChanged.disconnect(self._updateMetaData) self._instance_containers, self._instance_containers_metadata = self._fetchInstanceContainers( ) for container in self._instance_containers.values(): container.metaDataChanged.connect(self._updateMetaData) new_items = list(self._recomputeItems()) if new_items != self._items: self.setItems(new_items) def _recomputeItems(self) -> Generator[Dict[str, Any], None, None]: """Computes the items that need to be in this list model. This does not set the items in the list itself. It is intended to be overwritten by subclasses that add their own roles to the model. """ registry = ContainerRegistry.getInstance() result = [] for container in self._instance_containers.values(): result.append({ "name": container.getName(), "id": container.getId(), "metadata": container.getMetaData().copy(), "readOnly": registry.isReadOnly(container.getId()), "section": container.getMetaDataEntry(self._section_property, ""), "weight": int(container.getMetaDataEntry("weight", 0)) }) for container_metadata in self._instance_containers_metadata.values(): result.append({ "name": container_metadata["name"], "id": container_metadata["id"], "metadata": container_metadata.copy(), "readOnly": registry.isReadOnly(container_metadata["id"]), "section": container_metadata.get(self._section_property, ""), "weight": int(container_metadata.get("weight", 0)) }) yield from sorted(result, key=self._sortKey) def _fetchInstanceContainers( self ) -> Tuple[Dict[str, InstanceContainer], Dict[str, Dict[str, Any]]]: """Fetch the list of containers to display. This method is intended to be overridable by subclasses. :return: A tuple of an ID-to-instance mapping that includes all fully loaded containers, and an ID-to-metadata mapping that includes the containers of which only the metadata is known. """ registry = ContainerRegistry.getInstance() #Cache this for speed. containers = { } #type: Dict[str, InstanceContainer] #Mapping from container ID to container. metadatas = { } #type: Dict[str, Dict[str, Any]] #Mapping from container ID to metadata. for filter_dict in self._filter_dicts: this_filter = registry.findInstanceContainersMetadata( **filter_dict) for metadata in this_filter: if metadata["id"] not in containers and metadata[ "id"] not in metadatas: #No duplicates please. if registry.isLoaded( metadata["id"] ): #Only add it to the full containers if it's already fully loaded. containers[metadata["id"]] = cast( InstanceContainer, registry.findContainers(id=metadata["id"])[0]) else: metadatas[metadata["id"]] = metadata return containers, metadatas def setSectionProperty(self, property_name: str) -> None: if self._section_property != property_name: self._section_property = property_name self.sectionPropertyChanged.emit() self._container_change_timer.start() sectionPropertyChanged = pyqtSignal() @pyqtProperty(str, fset=setSectionProperty, notify=sectionPropertyChanged) def sectionProperty(self) -> str: return self._section_property def setFilter(self, filter_dict: Dict[str, str]) -> None: """Set the filter of this model based on a string. :param filter_dict: :type{Dict} Dictionary to do the filtering by. """ self.setFilterList([filter_dict]) filterChanged = pyqtSignal() @pyqtProperty("QVariantMap", fset=setFilter, notify=filterChanged) def filter(self) -> Dict[str, str]: return self._filter_dicts[0] if len(self._filter_dicts) != 0 else {} def setFilterList(self, filter_list: List[Dict[str, str]]) -> None: """Set a list of filters to use when fetching containers. :param filter_list: List of filter dicts to fetch multiple sets of containers. The final result is the union of these sets. """ if filter_list != self._filter_dicts: self._filter_dicts = filter_list self.filterChanged.emit() self._container_change_timer.start() @pyqtProperty("QVariantList", fset=setFilterList, notify=filterChanged) def filterList(self) -> List[Dict[str, str]]: return self._filter_dicts @pyqtSlot(str, result="QVariantList") def getFileNameFilters(self, io_type: str) -> List[str]: """Gets a list of the possible file filters that the plugins have registered they can read or write. The convenience meta-filters "All Supported Types" and "All Files" are added when listing readers, but not when listing writers. :param io_type: Name of the needed IO type :return: A list of strings indicating file name filters for a file dialog. """ #TODO: This function should be in UM.Resources! filters = [] all_types = [] for plugin_id, meta_data in self._getIOPlugins(io_type): for io_plugin in meta_data[io_type]: filters.append(io_plugin["description"] + " (*." + io_plugin["extension"] + ")") all_types.append("*.{0}".format(io_plugin["extension"])) if "_reader" in io_type: # if we're listing readers, add the option to show all supported files as the default option filters.insert( 0, catalog.i18nc("@item:inlistbox", "All Supported Types ({0})", " ".join(all_types))) filters.append( catalog.i18nc("@item:inlistbox", "All Files (*)") ) # Also allow arbitrary files, if the user so prefers. return filters @pyqtSlot(result=QUrl) def getDefaultPath(self) -> QUrl: return QUrl.fromLocalFile(os.path.expanduser("~/")) def _getIOPlugins(self, io_type: str) -> List[Tuple[str, Dict[str, Any]]]: """Gets a list of profile reader or writer plugins :return: List of tuples of (plugin_id, meta_data). """ pr = PluginRegistry.getInstance() active_plugin_ids = pr.getActivePlugins() result = [] for plugin_id in active_plugin_ids: meta_data = pr.getMetaData(plugin_id) if io_type in meta_data: result.append((plugin_id, meta_data)) return result def _sortKey(self, item: Dict[str, Any]) -> List[Any]: result = [] if self._section_property: result.append(item.get(self._section_property, "")) result.append( not ContainerRegistry.getInstance().isReadOnly(item["id"])) result.append(int(item.get("weight", 0))) result.append(item["name"]) return result def _updateMetaData(self, container: InstanceContainer) -> None: index = self.find("id", container.id) if self._section_property: self.setProperty( index, "section", container.getMetaDataEntry(self._section_property, "")) self.setProperty(index, "metadata", container.getMetaData()) self.setProperty(index, "name", container.getName()) self.setProperty(index, "id", container.getId()) def _onContainerLoadComplete(self, container_id: str) -> None: """If a container has loaded fully (rather than just metadata) we need to move it from the dict of metadata to the dict of full containers. """ if container_id in self._instance_containers_metadata: del self._instance_containers_metadata[container_id] self._instance_containers[ container_id] = ContainerRegistry.getInstance().findContainers( id=container_id)[0] self._instance_containers[container_id].metaDataChanged.connect( self._updateMetaData)