class ObjectTree(QWidget, ComponentMixin): name = 'Object Tree' _stash = [] preferences = Parameter.create(name='Preferences', children=[{ 'name': 'Preserve properties on reload', 'type': 'bool', 'value': False }, { 'name': 'Clear all before each run', 'type': 'bool', 'value': True }, { 'name': 'STL precision', 'type': 'float', 'value': .1 }]) sigObjectsAdded = pyqtSignal([list], [list, bool]) sigObjectsRemoved = pyqtSignal(list) sigCQObjectSelected = pyqtSignal(object) sigAISObjectsSelected = pyqtSignal(list) sigItemChanged = pyqtSignal(QTreeWidgetItem, int) sigObjectPropertiesChanged = pyqtSignal() def __init__(self, parent): super(ObjectTree, self).__init__(parent) self.tree = tree = QTreeWidget( self, selectionMode=QAbstractItemView.ExtendedSelection) self.properties_editor = ParameterTree(self) tree.setHeaderHidden(True) tree.setItemsExpandable(False) tree.setRootIsDecorated(False) tree.setContextMenuPolicy(Qt.ActionsContextMenu) #forward itemChanged singal tree.itemChanged.connect(\ lambda item,col: self.sigItemChanged.emit(item,col)) #handle visibility changes form tree tree.itemChanged.connect(self.handleChecked) self.CQ = CQRootItem() self.Imports = ImportRootItem() self.Helpers = HelpersRootItem() root = tree.invisibleRootItem() root.addChild(self.CQ) root.addChild(self.Imports) root.addChild(self.Helpers) tree.expandToDepth(1) self._export_STL_action = \ QAction('Export as STL', self, enabled=False, triggered=lambda: \ self.export('stl', self.preferences['STL precision'])) self._export_STEP_action = \ QAction('Export as STEP', self, enabled=False, triggered=lambda: \ self.export('step')) self._clear_current_action = QAction(icon('delete'), 'Clear current', self, enabled=False, triggered=self.removeSelected) self._toolbar_actions = \ [QAction(icon('delete-many'),'Clear all',self,triggered=self.removeObjects), self._clear_current_action,] self.prepareMenu() tree.itemSelectionChanged.connect(self.handleSelection) tree.customContextMenuRequested.connect(self.showMenu) self.prepareLayout() def prepareMenu(self): self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self._context_menu = QMenu(self) self._context_menu.addActions(self._toolbar_actions) self._context_menu.addActions( (self._export_STL_action, self._export_STEP_action)) def prepareLayout(self): self._splitter = splitter((self.tree, self.properties_editor), stretch_factors=(2, 1), orientation=Qt.Vertical) layout(self, (self._splitter, ), top_widget=self) self._splitter.show() def showMenu(self, position): self._context_menu.exec_(self.tree.viewport().mapToGlobal(position)) def menuActions(self): return {'Tools': [self._export_STL_action, self._export_STEP_action]} def toolbarActions(self): return self._toolbar_actions def addLines(self): origin = (0, 0, 0) ais_list = [] for name, color, direction in zip(('X', 'Y', 'Z'), ('red', 'lawngreen', 'blue'), ((1, 0, 0), (0, 1, 0), (0, 0, 1))): line_placement = Geom_Line( gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction))) line = AIS_Line(line_placement) line.SetColor(to_occ_color(color)) self.Helpers.addChild(ObjectTreeItem(name, ais=line)) ais_list.append(line) self.sigObjectsAdded.emit(ais_list) def _current_properties(self): current_params = {} for i in range(self.CQ.childCount()): child = self.CQ.child(i) current_params[child.properties['Name']] = child.properties return current_params def _restore_properties(self, obj, properties): for p in properties[obj.properties['Name']]: obj.properties[p.name()] = p.value() @pyqtSlot(dict, bool) @pyqtSlot(dict) def addObjects(self, objects, clean=False, root=None): if root is None: root = self.CQ request_fit_view = True if root.childCount() == 0 else False preserve_props = self.preferences['Preserve properties on reload'] if preserve_props: current_props = self._current_properties() if clean or self.preferences['Clear all before each run']: self.removeObjects() ais_list = [] #remove empty objects objects_f = { k: v for k, v in objects.items() if not is_obj_empty(v.shape) } for name, obj in objects_f.items(): ais, shape_display = make_AIS(obj.shape, obj.options) child = ObjectTreeItem(name, shape=obj.shape, shape_display=shape_display, ais=ais, sig=self.sigObjectPropertiesChanged) if preserve_props and name in current_props: self._restore_properties(child, current_props) if child.properties['Visible']: ais_list.append(ais) root.addChild(child) if request_fit_view: self.sigObjectsAdded[list, bool].emit(ais_list, True) else: self.sigObjectsAdded[list].emit(ais_list) @pyqtSlot(object, str, object) def addObject(self, obj, name='', options={}): root = self.CQ ais, shape_display = make_AIS(obj, options) root.addChild( ObjectTreeItem(name, shape=obj, shape_display=shape_display, ais=ais, sig=self.sigObjectPropertiesChanged)) self.sigObjectsAdded.emit([ais]) @pyqtSlot(list) @pyqtSlot() def removeObjects(self, objects=None): if objects: removed_items_ais = [self.CQ.takeChild(i).ais for i in objects] else: removed_items_ais = [ch.ais for ch in self.CQ.takeChildren()] self.sigObjectsRemoved.emit(removed_items_ais) @pyqtSlot(bool) def stashObjects(self, action: bool): if action: self._stash = self.CQ.takeChildren() removed_items_ais = [ch.ais for ch in self._stash] self.sigObjectsRemoved.emit(removed_items_ais) else: self.removeObjects() self.CQ.addChildren(self._stash) ais_list = [el.ais for el in self._stash] self.sigObjectsAdded.emit(ais_list) @pyqtSlot() def removeSelected(self): ixs = self.tree.selectedIndexes() rows = [ix.row() for ix in ixs] self.removeObjects(rows) def export(self, export_type, precision=None): items = self.tree.selectedItems() # if CQ models is selected get all children if [item for item in items if item is self.CQ]: CQ = self.CQ shapes = [CQ.child(i).shape for i in range(CQ.childCount())] # otherwise collect all selected children of CQ else: shapes = [item.shape for item in items if item.parent() is self.CQ] fname = get_save_filename(export_type) if fname is not '': export(shapes, export_type, fname, precision) @pyqtSlot() def handleSelection(self): items = self.tree.selectedItems() if len(items) == 0: self._export_STL_action.setEnabled(False) self._export_STEP_action.setEnabled(False) return # emit list of all selected ais objects (might be empty) ais_objects = [item.ais for item in items if item.parent() is self.CQ] self.sigAISObjectsSelected.emit(ais_objects) # handle context menu and emit last selected CQ object (if present) item = items[-1] if item.parent() is self.CQ: self._export_STL_action.setEnabled(True) self._export_STEP_action.setEnabled(True) self._clear_current_action.setEnabled(True) self.sigCQObjectSelected.emit(item.shape) self.properties_editor.setParameters(item.properties, showTop=False) self.properties_editor.setEnabled(True) elif item is self.CQ and item.childCount() > 0: self._export_STL_action.setEnabled(True) self._export_STEP_action.setEnabled(True) else: self._export_STL_action.setEnabled(False) self._export_STEP_action.setEnabled(False) self._clear_current_action.setEnabled(False) self.properties_editor.setEnabled(False) self.properties_editor.clear() @pyqtSlot(list) def handleGraphicalSelection(self, shapes): self.tree.clearSelection() CQ = self.CQ for i in range(CQ.childCount()): item = CQ.child(i) for shape in shapes: if item.shape_display.wrapped.IsEqual(shape): item.setSelected(True) @pyqtSlot(QTreeWidgetItem, int) def handleChecked(self, item, col): if type(item) is ObjectTreeItem: if item.checkState(0): item.properties['Visible'] = True else: item.properties['Visible'] = False
class BeeActions(QObject): log_signal = pyqtSignal(str) def __init__(self, dockarea): super().__init__() self.dockarea = dockarea self.dockarea.dock_signal.connect(self.save_layout_state_auto) self.mainwindow = dockarea.parent() self.chrono = ChronoTimer(dockarea) self.author = 'Aurore Avargues' self.h5saver = H5Saver() self.h5saver.new_file_sig.connect(self.create_new_file) self.settings = None self.shortcut_file = None self.shortcuts = [] self.shortcut_manager = ShortCutManager(list_actions) self.timestamp_array = None self.action_array = None self.bee_array = None self.setup_ui() def setup_ui(self): #creating the menubar self.menubar = self.mainwindow.menuBar() self.create_menu(self.menubar) #disconnect normal chrono/timer behaviour with the start button self.chrono.start_pb.disconnect() self.chrono.start_pb.clicked.connect(self.set_scan) self.chrono.reset_pb.clicked.connect(self.stop_daq) self.settings_dock = Dock('Settings') self.dockarea.addDock(self.settings_dock, 'bottom', self.chrono.dock_controls) self.dock_daq = Dock('Data Acquisition') self.dockarea.addDock(self.dock_daq, 'right') self.logger_list = QtWidgets.QListWidget() self.logger_list.setMinimumWidth(300) self.dock_daq.addWidget(self.logger_list) self.init_tree = ParameterTree() self.init_tree.setMinimumWidth(300) self.init_tree.setMinimumHeight(150) self.settings_dock.addWidget(self.init_tree) self.settings_dock.addWidget(self.h5saver.settings_tree) self.h5saver.settings.child(('save_type')).hide() self.h5saver.settings.child(('save_2D')).hide() self.h5saver.settings.child(('save_raw_only')).hide() self.h5saver.settings.child(('do_save')).hide() self.h5saver.settings.child(('custom_name')).hide() self.settings = Parameter.create(name='init_settings', type='group', children=[ { 'title': 'Loaded presets', 'name': 'loaded_files', 'type': 'group', 'children': [ { 'title': 'Shortcut file', 'name': 'shortcut_file', 'type': 'str', 'value': '', 'readonly': True }, { 'title': 'Layout file', 'name': 'layout_file', 'type': 'str', 'value': '', 'readonly': True }, ] }, { 'title': 'Settings', 'name': 'settings', 'type': 'group', 'children': [ { 'title': 'Save Bee number', 'name': 'save_bee_number', 'type': 'bool', 'value': True }, ] }, { 'title': 'Shortcuts', 'name': 'shortcuts', 'type': 'group', 'children': [] }, ]) self.init_tree.setParameters(self.settings, showTop=False) self.settings.sigTreeStateChanged.connect(self.parameter_tree_changed) #params about dataset attributes and scan attibutes date = QDateTime(QDate.currentDate(), QTime.currentTime()) params_dataset = [{ 'title': 'Dataset information', 'name': 'dataset_info', 'type': 'group', 'children': [{ 'title': 'Author:', 'name': 'author', 'type': 'str', 'value': self.author }, { 'title': 'Date/time:', 'name': 'date_time', 'type': 'date_time', 'value': date }, { 'title': 'Sample:', 'name': 'sample', 'type': 'str', 'value': '' }, { 'title': 'Experiment type:', 'name': 'experiment_type', 'type': 'str', 'value': '' }, { 'title': 'Description:', 'name': 'description', 'type': 'text', 'value': '' }] }] params_scan = [{ 'title': 'Scan information', 'name': 'scan_info', 'type': 'group', 'children': [ { 'title': 'Author:', 'name': 'author', 'type': 'str', 'value': self.author }, { 'title': 'Date/time:', 'name': 'date_time', 'type': 'date_time', 'value': date }, { 'title': 'Scan type:', 'name': 'scan_type', 'type': 'list', 'value': 'Scan1D', 'values': ['Scan1D', 'Scan2D'] }, { 'title': 'Scan name:', 'name': 'scan_name', 'type': 'str', 'value': '', 'readonly': True }, { 'title': 'Description:', 'name': 'description', 'type': 'text', 'value': '' }, ] }] self.dataset_attributes = Parameter.create(name='Attributes', type='group', children=params_dataset) self.scan_attributes = Parameter.create(name='Attributes', type='group', children=params_scan) def parameter_tree_changed(self, param, changes): """ | Check eventual changes in the changes list parameter. | | In case of changed values, emit the signal containing the current path and parameter via update_settings_signal to the connected hardware. =============== ==================================== ================================================== **Parameters** **Type** **Description** *param* instance of pyqtgraph parameter The parameter to be checked *changes* (parameter,change,infos) tuple list The (parameter,change,infos) list to be treated =============== ==================================== ================================================== """ for param, change, data in changes: path = self.settings.childPath(param) if path is not None: childName = '.'.join(path) else: childName = param.name() if change == 'childAdded': pass elif change == 'value': if param.name() in custom_tree.iter_children( self.settings.child(('shortcuts')), []): if param.parent().name() == 'shortcuts': param_index = custom_tree.iter_children( self.settings.child(('shortcuts')), []).index(param.name()) action = self.shortcut_manager.shortcut_params.child( ('actions')).children()[param_index].child( ('action')).value() self.activate_shortcut(self.shortcuts[param_index], action, activate=param.value()) elif change == 'parent': pass def activate_shortcut(self, shortcut, action='', activate=True): if activate: shortcut.activated.connect(self.create_activated_slot(action)) else: try: shortcut.activated.disconnect() except: pass def create_activated_slot(self, action): return lambda: self.log_data(action) def log_data(self, action=''): now = self.chrono.get_elapsed_time() if self.settings.child('settings', 'save_bee_number').value(): widget = QtWidgets.QWidget() index, res = QtWidgets.QInputDialog.getInt( widget, 'Bee number', 'Pick a number for this bee!') if res: new_item = QtWidgets.QListWidgetItem( f'Elapsed time: {int(now)} s, Bee {index} did :{action}') self.logger_list.insertItem(0, new_item) self.h5saver.append(self.action_array, action) self.h5saver.append(self.timestamp_array, np.array([now])) self.h5saver.append(self.bee_array, np.array([index])) else: new_item = QtWidgets.QListWidgetItem( f'Elapsed time: {int(now)} s:{action}') self.logger_list.insertItem(0, new_item) self.h5saver.append(self.action_array, action) self.h5saver.append(self.timestamp_array, np.array([now])) self.h5saver.flush() def create_shortcuts(self): pass def create_new_file(self, new_file): self.h5saver.init_file(update_h5=new_file) res = self.update_file_settings(new_file) return res def set_scan(self): """ Sets the current scan given the selected settings. Makes some checks, increments the h5 file scans. In case the dialog is cancelled, return False and aborts the scan """ try: if self.shortcut_file is not None: # set the filename and path res = self.create_new_file(False) if not res: return #create the arrays within the current scan group self.timestamp_array = self.h5saver.add_array( self.h5saver.current_scan_group, 'time_axis', 'data', scan_type='scan1D', enlargeable=True, array_to_save=np.array([ 0., ]), data_shape=(1, ), title='Timestamps', metadata=dict(units='seconds')) self.action_array = self.h5saver.add_string_array( self.h5saver.current_scan_group, 'actions', title='Actions', metadata=dict([])) if self.settings.child('settings', 'save_bee_number').value(): self.bee_array = self.h5saver.add_array( self.h5saver.current_scan_group, 'bees', 'data', scan_type='scan1D', enlargeable=True, array_to_save=np.array([ 0, ]), title='Bees', data_shape=(1, )) current_filename = self.h5saver.settings.child( ('current_scan_name')).value() self.init_tree.setEnabled(False) self.h5saver.settings_tree.setEnabled(False) self.logger_list.clear() self.h5saver.current_scan_group._v_attrs['scan_done'] = False # if all metadat steps have been validated, start the chrono self.chrono.start() return True else: mssg = QtWidgets.QMessageBox() mssg.setText( 'You have to load a shortcut file configuration before starting' ) mssg.exec() return False except Exception as e: self.update_status(getLineInfo() + str(e)) def stop_daq(self): self.h5saver.current_scan_group._v_attrs['scan_done'] = True self.init_tree.setEnabled(True) self.h5saver.settings_tree.setEnabled(True) self.h5saver.flush() def update_file_settings(self, new_file=False): try: if self.h5saver.current_scan_group is None: new_file = True if new_file: self.set_metadata_about_dataset() self.save_metadata(self.h5saver.raw_group, 'dataset_info') if self.h5saver.current_scan_name is None: self.h5saver.add_scan_group() elif not self.h5saver.is_node_in_group( self.h5saver.raw_group, self.h5saver.current_scan_name): self.h5saver.add_scan_group() #set attributes to the current group, such as scan_type.... self.scan_attributes.child('scan_info', 'scan_type').setValue('Scan1D') self.scan_attributes.child('scan_info', 'scan_name').setValue( self.h5saver.current_scan_group._v_name) self.scan_attributes.child('scan_info', 'description').setValue( self.h5saver.current_scan_group._v_attrs['description']) res = self.set_metadata_about_current_scan() self.save_metadata(self.h5saver.current_scan_group, 'scan_info') return res except Exception as e: self.update_status(getLineInfo() + str(e)) def set_metadata_about_current_scan(self): """ Set the date/time and author values of the scan_info child of the scan_attributes tree. Show the 'scan' file attributes. See Also -------- show_file_attributes """ date = QDateTime(QDate.currentDate(), QTime.currentTime()) self.scan_attributes.child('scan_info', 'date_time').setValue(date) self.scan_attributes.child('scan_info', 'author').setValue( self.dataset_attributes.child('dataset_info', 'author').value()) res = self.show_file_attributes('scan') return res def set_metadata_about_dataset(self): """ Set the date value of the data_set_info-date_time child of the data_set_attributes tree. Show the 'dataset' file attributes. See Also -------- show_file_attributes """ date = QDateTime(QDate.currentDate(), QTime.currentTime()) self.dataset_attributes.child('dataset_info', 'date_time').setValue(date) res = self.show_file_attributes('dataset') return res def show_file_attributes(self, type_info='dataset'): """ """ dialog = QtWidgets.QDialog() vlayout = QtWidgets.QVBoxLayout() tree = ParameterTree() tree.setMinimumWidth(400) tree.setMinimumHeight(500) if type_info == 'scan': tree.setParameters(self.scan_attributes, showTop=False) elif type_info == 'dataset': tree.setParameters(self.dataset_attributes, showTop=False) vlayout.addWidget(tree) dialog.setLayout(vlayout) buttonBox = QtWidgets.QDialogButtonBox(parent=dialog) buttonBox.addButton('Cancel', buttonBox.RejectRole) buttonBox.addButton('Apply', buttonBox.AcceptRole) buttonBox.rejected.connect(dialog.reject) buttonBox.accepted.connect(dialog.accept) vlayout.addWidget(buttonBox) dialog.setWindowTitle( 'Fill in information about this {}'.format(type_info)) res = dialog.exec() return res def save_metadata(self, node, type_info='dataset_info'): """ """ attr = node._v_attrs if type_info == 'dataset_info': attr['type'] = 'dataset' params = self.dataset_attributes else: attr['type'] = 'scan' params = self.scan_attributes for child in params.child((type_info)).children(): if isinstance(child.value(), QDateTime): attr[child.name()] = child.value().toString( 'dd/mm/yyyy HH:MM:ss') else: attr[child.name()] = child.value() if type_info == 'dataset_info': #save contents of given parameter object into an xml string under the attribute settings settings_str = b'<All_settings title="All Settings" type="group">' settings_str += custom_tree.parameter_to_xml_string(params) settings_str += custom_tree.parameter_to_xml_string(self.settings) if hasattr(self.shortcut_manager, 'shortcut_params'): settings_str += custom_tree.parameter_to_xml_string( self.shortcut_manager.shortcut_params) settings_str += b'</All_settings>' attr.settings = settings_str elif type_info == 'scan_info': settings_str = b'<All_settings title="All Settings" type="group">' + \ custom_tree.parameter_to_xml_string(params) + \ custom_tree.parameter_to_xml_string(self.settings) + \ custom_tree.parameter_to_xml_string(self.h5saver.settings) + b'</All_settings>' attr.settings = settings_str def show_log(self): import webbrowser webbrowser.open(logging.getLoggerClass().root.handlers[0].baseFilename) def show_file(self): self.h5saver.flush() self.h5saver.show_file_content() def create_menu(self, menubar): menubar.clear() # %% create Settings menu self.file_menu = menubar.addMenu('File') self.file_menu.addAction('Show log file', self.show_log) self.file_menu.addAction('Show data file', self.show_file) self.file_menu.addSeparator() quit_action = self.file_menu.addAction('Quit') quit_action.triggered.connect(self.quit_fun) self.settings_menu = menubar.addMenu('Settings') docked_menu = self.settings_menu.addMenu('Docked windows') action_load = docked_menu.addAction('Load Layout') action_save = docked_menu.addAction('Save Layout') action_load.triggered.connect(self.load_layout_state) action_save.triggered.connect(self.save_layout_state) docked_menu.addSeparator() self.preset_menu = menubar.addMenu('Preset Shortcuts') action_new_preset = self.preset_menu.addAction('New preset') # action.triggered.connect(lambda: self.show_file_attributes(type_info='preset')) action_new_preset.triggered.connect(self.create_preset) action_modify_preset = self.preset_menu.addAction('Modify preset') action_modify_preset.triggered.connect(self.modify_shortcuts) self.preset_menu.addSeparator() load_preset = self.preset_menu.addMenu('Load presets') slots = dict([]) for ind_file, file in enumerate(os.listdir(shortcut_path)): if file.endswith(".xml"): (filesplited, ext) = os.path.splitext(file) slots[filesplited] = load_preset.addAction(filesplited) slots[filesplited].triggered.connect( self.create_menu_slot(os.path.join(shortcut_path, file))) def modify_shortcuts(self): try: path = select_file(start_path=shortcut_path, save=False, ext='xml') if path != '': self.shortcut_manager.set_file_preset(str(path)) mssg = QtWidgets.QMessageBox() mssg.setText( f'You have to restart the application to take the modifications into account! ' f'Quitting the application...') mssg.exec() self.quit_fun() else: # cancel pass except Exception as e: self.update_status(getLineInfo() + str(e)) def create_menu_slot(self, filename): return lambda: self.set_shortcut_mode(filename) def set_shortcut_mode(self, filename): #TODO: apply shortcuts to this widget tail, fileext = os.path.split(filename) file, ext = os.path.splitext(fileext) if ext == '.xml': self.shortcut_file = filename self.shortcut_manager.set_file_preset(filename, show=False) self.settings.child('loaded_files', 'shortcut_file').setValue(filename) self.author = self.shortcut_manager.shortcut_params.child( ('author')).value() self.dataset_attributes.child('dataset_info', 'author').setValue(self.author) self.scan_attributes.child('scan_info', 'author').setValue(self.author) path = os.path.join(layout_path, file + '.dock') if os.path.isfile(path): self.load_layout_state(path) #remove existing shorcuts while len(self.shortcuts): self.shortcuts.pop(0) for ind, shortcut in enumerate( self.shortcut_manager.shortcut_params.child( ('actions')).children()): stc = QtWidgets.QShortcut( QtGui.QKeySequence(shortcut.child(('shortcut')).value()), self.dockarea) self.settings.child(('shortcuts')).addChild({ 'title': f"Shortcut{ind:02d}: {shortcut.child(('action')).value()} {shortcut.child(('shortcut')).value()}:", 'name': f'shortcut{ind:02d}', 'type': 'led_push', 'value': True }) self.shortcuts.append(stc) self.activate_shortcut(stc, shortcut.child(('action')).value(), activate=True) def create_preset(self): try: self.shortcut_manager.set_new_preset() self.create_menu(self.menubar) except Exception as e: self.update_status(getLineInfo() + str(e)) def save_layout_state(self, file=None): """ Save the current layout state in the select_file obtained pathname file. Once done dump the pickle. See Also -------- utils.select_file """ try: dockstate = self.dockarea.saveState() if file is None: file = select_file(start_path=None, save=True, ext='dock') if file is not None: with open(str(file), 'wb') as f: pickle.dump(dockstate, f, pickle.HIGHEST_PROTOCOL) except: pass def save_layout_state_auto(self): if self.shortcut_file is not None: file = os.path.split(self.shortcut_file)[1] file = os.path.splitext(file)[0] path = os.path.join(layout_path, file + '.dock') self.save_layout_state(path) def load_layout_state(self, file=None): """ Load and restore a layout state from the select_file obtained pathname file. See Also -------- utils.select_file """ try: if file is None: file = select_file(save=False, ext='dock') if file is not None: with open(str(file), 'rb') as f: dockstate = pickle.load(f) self.dockarea.restoreState(dockstate) file = os.path.split(file)[1] self.settings.child('loaded_files', 'layout_file').setValue(file) except: pass def quit_fun(self): """ Quit the current instance of DAQ_scan and close on cascade move and detector modules. See Also -------- quit_fun """ try: areas = self.dockarea.tempAreas[:] for area in areas: area.win.close() QtWidgets.QApplication.processEvents() QThread.msleep(1000) QtWidgets.QApplication.processEvents() if hasattr(self, 'mainwindow'): self.mainwindow.close() except Exception as e: pass def update_status(self, txt): """ Show the txt message in the status bar with a delay of wait_time ms. =============== =========== ======================= **Parameters** **Type** **Description** *txt* string The message to show *wait_time* int the delay of showing *log_type* string the type of the log =============== =========== ======================= """ try: self.log_signal.emit(txt) logging.info(txt) except Exception as e: pass
class liveSpecWrangler(wranglerWidget): """Widget for controlling spec and reading in data as it is being collected. attributes: fname: str, file path for data storage file_lock: multiprocessing Condition, multiprocess safe lock to ensure only one process accesses data file at a time. ui: Qt Ui_Form, holds all gui widgets made with qtdesigner specCommandLine: commandLine, widget to simulate terminal line commands: list, set of previously entered commands command_queue: Queue, used to send commands to thread current: int, index of current command file_lock, mp.Condition, process safe lock for file access fname: str, path to data file keep_trying: bool, unused parameters: pyqtgraph Parameter, set of parameters for controling widget scan_name: str, current scan name, used to handle syncing data sphere_args: dict, used as **kwargs in sphere initialization. see EwaldSphere. tree: pyqtgraph ParameterTree, tree which holds parameters thread: liveSpecThread, thread which controls processes to watch for new data and to run integration methods: enabled: enables or disables all gui elements. send_command: sends command through to spec via the bServer set_fname: Method to safely change file name set_image_dir: sets the image directory set_out_dir: sets the output directory for new data files set_pdi_dir: sets the pdi directory set_poni_file: sets the calibration poni file setup: sets up the thread attribute to ensure all parameters are properly synced. stop_watching: end the watcher process. update_file: update the current scan name and file path attributes signals: finished: Should be connected to thread.finished signal showLabel: str, text to be set as specLabel. sigStart: Tells staticWidget to start the thread and prepare for new data. sigUpdateData: int, signals a new arch has been added. sigUpdateFile: (str, str), sends new scan_name and file name to staticWidget. """ showLabel = Qt.QtCore.Signal(str) def __init__(self, fname, file_lock, parent=None): """fname: str, path to data file. file_lock: Condition, process safe lock. """ super().__init__(fname, file_lock, parent) # Setup gui elements self.ui = Ui_Form() self.ui.setupUi(self) self.ui.startButton.clicked.connect(self.sigStart.emit) self.ui.stopButton.clicked.connect(self.stop_watching) self.specCommandLine = commandLine(self) self.specCommandLine.send_command = self.send_command self.ui.commandLayout.addWidget(self.specCommandLine) self.buttonSend = QtWidgets.QPushButton(self) self.buttonSend.setText('Send') self.buttonSend.clicked.connect(self.send_command) self.ui.commandLayout.addWidget(self.buttonSend) self.commands = [''] self.current = -1 self.keep_trying = True self.showLabel.connect(self.ui.specLabel.setText) # Setup the parameter tree self.tree = ParameterTree() self.parameters = Parameter.create(name='live_spec_wrangler', type='group', children=params) self.tree.setParameters(self.parameters, showTop=False) self.layout = Qt.QtWidgets.QVBoxLayout(self.ui.paramFrame) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.tree) self.parameters.child('image_dir_browse').sigActivated.connect( self.set_image_dir) self.parameters.child('pdi_dir_browse').sigActivated.connect( self.set_pdi_dir) self.parameters.child('poni_file_browse').sigActivated.connect( self.set_poni_file) self.parameters.child('out_dir_browse').sigActivated.connect( self.set_out_dir) self.parameters.sigTreeStateChanged.connect(self.update) # Setup the liveSpecThread self.thread = liveSpecThread( command_queue=self.command_queue, sphere_args=self.sphere_args, fname=self.fname, file_lock=self.file_lock, mp_inputs=self._get_mp_inputs(), img_dir=self.parameters.child('Image Directory').value(), pdi_dir=self.parameters.child('PDI Directory').value(), out_dir=self.parameters.child('Out Directory').value(), filetypes=self.parameters.child('File Types').value().split(), pollingperiod=self.parameters.child('Polling Period').value(), parent=self) self.thread.showLabel.connect(self.ui.specLabel.setText) self.thread.sigUpdateFile.connect(self.update_file) self.thread.finished.connect(self.finished.emit) self.thread.sigUpdate.connect(self.sigUpdateData.emit) self.setup() def setup(self): """Syncs all attributes of liveSpecThread with parameters """ self.thread.sphere_args.update(self.sphere_args) self.thread.fname = self.fname self.thread.mp_inputs.update(self._get_mp_inputs()) self.thread.img_dir = self.parameters.child('Image Directory').value() self.thread.pdi_dir = self.parameters.child('PDI Directory').value() self.thread.out_dir = self.parameters.child('Out Directory').value() self.thread.filetypes = self.parameters.child( 'File Types').value().split() self.thread.set_queues() self.thread.pollingperiod = self.parameters.child( 'Polling Period').value() def send_command(self): """Sends command in command line to spec, and calls commandLine send_command method to add command to list of commands. """ command = self.specCommandLine.text() if not (command.isspace() or command == ''): try: specCommand(command, queue=True) except Exception as e: print(e) print(f"Command '{command}' not sent") commandLine.send_command(self.specCommandLine) def set_image_dir(self): """Opens file dialogue and sets the image directory """ dname = Qt.QtWidgets.QFileDialog.getExistingDirectory(self) if dname != '': self.parameters.child('Image Directory').setValue(dname) def set_pdi_dir(self): """Opens file dialogue and sets the pdi directory """ dname = Qt.QtWidgets.QFileDialog.getExistingDirectory(self) if dname != '': self.parameters.child('PDI Directory').setValue(dname) def set_out_dir(self): """Opens file dialogue and sets the output directory """ dname = Qt.QtWidgets.QFileDialog.getExistingDirectory(self) if dname != '': self.parameters.child('Out Directory').setValue(dname) def set_poni_file(self): """Opens file dialogue and sets the calibration file """ fname, _ = Qt.QtWidgets.QFileDialog().getOpenFileName() if fname != '': self.parameters.child('Calibration PONI File').setValue(fname) def _get_mp_inputs(self): """Organizes inputs for MakePONI from parameters. """ mp_inputs = OrderedDict(rotations={ "rot1": None, "rot2": None, "rot3": None }, calib_rotations={ "rot1": 0, "rot2": 0, "rot3": 0 }, poni_file=None, spec_dict={}) rot_mot = self.parameters.child('Rotation Motors') for child in rot_mot: if child.value() == "": mp_inputs['rotations'][child.name().lower()] = None else: mp_inputs['rotations'][child.name().lower()] = child.value() cal_rot = self.parameters.child('Calibration Angles') for child in cal_rot: if child.value() == 0: pass else: mp_inputs['calib_rotations'][ child.name().lower()] = child.value() mp_inputs['poni_file'] = self.parameters.child( 'Calibration PONI File').value() return mp_inputs def update_file(self, name, fname): """updates the current scan name and file path attributes, emits them back to main widget. args: name: str, scan name fname: str, path to data file """ self.scan_name = name self.fname = fname self.sigUpdateFile.emit(name, fname) def enabled(self, enable): """Sets tree and start button to enable. args: enable: bool, True for enabled False for disabled. """ self.tree.setEnabled(enable) self.ui.startButton.setEnabled(enable) def stop_watching(self): """Sends stop command to thread. """ self.command_queue.put('stop')
class specWrangler(wranglerWidget): """Widget for integrating data associated with spec file. Can be used "live", will continue to poll data folders until image data and corresponding spec data are available. attributes: command_queue: Queue, used to send commands to thread file_lock, mp.Condition, process safe lock for file access fname: str, path to data file parameters: pyqtgraph Parameter, stores parameters from user scan_name: str, current scan name, used to handle syncing data sphere_args: dict, used as **kwargs in sphere initialization. see EwaldSphere. thread: wranglerThread or subclass, QThread for controlling processes timeout: int, how long before thread stops looking for new data. tree: pyqtgraph ParameterTree, stores and organizes parameters ui: Ui_Form from qtdesigner methods: cont, pause, stop: functions to pass continue, pause, and stop commands to thread via command_queue enabled: Enables or disables interactivity set_image_dir: sets the image directory set_poni_file: sets the calibration poni file set_spec_file: sets the spec data file set_fname: Method to safely change file name setup: Syncs thread parameters prior to starting signals: finished: Connected to thread.finished signal sigStart: Tells tthetaWidget to start the thread and prepare for new data. sigUpdateData: int, signals a new arch has been added. sigUpdateFile: (str, str), sends new scan_name and file name to tthetaWidget. showLabel: str, connected to thread showLabel signal, sets text in specLabel """ showLabel = Qt.QtCore.Signal(str) def __init__(self, fname, file_lock, parent=None): """fname: str, file path file_lock: mp.Condition, process safe lock """ # ic() super().__init__(fname, file_lock, parent) # Setup gui elements self.ui = Ui_Form() self.ui.setupUi(self) self.ui.startButton.clicked.connect(self.sigStart.emit) self.ui.pauseButton.clicked.connect(self.pause) self.ui.stopButton.clicked.connect(self.stop) self.ui.continueButton.clicked.connect(self.cont) # Setup parameter tree self.tree = ParameterTree() self.parameters = Parameter.create(name='spec_wrangler', type='group', children=params) self.tree.setParameters(self.parameters, showTop=False) self.layout = Qt.QtWidgets.QVBoxLayout(self.ui.paramFrame) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.tree) # Wire signals from parameter tree based buttons self.parameters.child('spec_file_browse').sigActivated.connect( self.set_spec_file) self.parameters.child('image_dir_browse').sigActivated.connect( self.set_image_dir) self.parameters.child('poni_file_browse').sigActivated.connect( self.set_poni_file) # Set attributes self.scan_number = self.parameters.child('Scan Number').value() self.timeout = self.parameters.child('Timeout').value() self.parameters.sigTreeStateChanged.connect(self.setup) # Setup thread self.thread = specThread(self.command_queue, self.sphere_args, self.fname, self.file_lock, self.scan_name, 0, {}, {}, None, 5, self) self.thread.showLabel.connect(self.ui.specLabel.setText) self.thread.sigUpdateFile.connect(self.sigUpdateFile.emit) self.thread.finished.connect(self.finished.emit) self.thread.sigUpdate.connect(self.sigUpdateData.emit) self.mask = None self.mask_widget = MaskWidget() key = self.parameters.child("Detector").value() data = np.zeros(DETECTOR_DICT[key]["shape"]) data[0, 0] = 1 self.mask_widget.set_data(data.T) self.mask_widget.hide() self.parameters.child('set_mask').sigActivated.connect( self.launch_mask_widget) self.mask_widget.newMask.connect(self.set_mask) self.setup() def setup(self): """Sets up the child thread, syncs all parameters. """ # ic() self.thread.mp_inputs.update(self._get_mp_inputs()) lsf_inputs = self._get_lsf_inputs() self.thread.lsf_inputs.update(lsf_inputs) self.scan_number = self.parameters.child('Scan Number').value() self.scan_name = lsf_inputs['spec_file_name'] + '_scan' + \ str(self.scan_number) self.fname = os.path.join(lsf_inputs['spec_file_path'], self.scan_name + '.hdf5') self.thread.fname = self.fname self.thread.scan_name = self.scan_name self.thread.scan_number = self.scan_number self.thread.img_dir = self.parameters.child('Image Directory').value() self.timeout = self.parameters.child('Timeout').value() self.thread.timeout = self.parameters.child('Timeout').value() self.thread.file_lock = self.file_lock self.thread.sphere_args = self.sphere_args self.thread.mask = self.mask if self.parameters.child('Rotation Motors').child( 'Rot2').value() == '': self.parameters.child('Rotation Motors').child('Rot2').setValue( 'tth') def pause(self): if self.thread.isRunning(): self.command_queue.put('pause') def cont(self): if self.thread.isRunning(): self.command_queue.put('continue') def stop(self): if self.thread.isRunning(): self.command_queue.put('stop') def set_spec_file(self): """Opens file dialogue and sets the spec data file """ # ic() fname, _ = Qt.QtWidgets.QFileDialog().getOpenFileName() if fname != '': self.parameters.child('Spec File').setValue(fname) def set_image_dir(self): """Opens file dialogue and sets the image directory """ # ic() dname = Qt.QtWidgets.QFileDialog.getExistingDirectory(self) if dname != '': self.parameters.child('Image Directory').setValue(dname) def set_poni_file(self): """Opens file dialogue and sets the calibration file """ # ic() fname, _ = Qt.QtWidgets.QFileDialog().getOpenFileName() if fname != '': self.parameters.child('Calibration PONI File').setValue(fname) def _get_mp_inputs(self): """Organizes inputs for MakePONI from parameters. """ # ic() mp_inputs = OrderedDict(rotations={ "rot1": None, "rot2": 'tth', "rot3": None }, calib_rotations={ "rot1": 0, "rot2": 0, "rot3": 0 }, poni_file=None, spec_dict={}) rot_mot = self.parameters.child('Rotation Motors') for child in rot_mot: if child.value() == "": mp_inputs['rotations'][child.name().lower()] = None else: mp_inputs['rotations'][child.name().lower()] = child.value() cal_rot = self.parameters.child('Calibration Angles') for child in cal_rot: if child.value() == 0: pass else: mp_inputs['calib_rotations'][ child.name().lower()] = child.value() mp_inputs['poni_file'] = self.parameters.child( 'Calibration PONI File').value() return mp_inputs def _get_lsf_inputs(self): """Organizes inputs for LoadSpecFile from parameters. No longer used. """ # ic() dirname, fname = os.path.split( self.parameters.child('Spec File').value()) lsf_inputs = OrderedDict(spec_file_path=dirname, spec_file_name=fname) return lsf_inputs def enabled(self, enable): """Sets tree and start button to enable. args: enable: bool, True for enabled False for disabled. """ # ic() self.tree.setEnabled(enable) self.ui.startButton.setEnabled(enable) def set_mask(self, idx, mask): self.mask = np.arange(self.mask_widget.data.size)[mask.ravel() == 1] self.thread.mask = self.mask def launch_mask_widget(self): key = self.parameters.child("Detector").value() data = np.zeros(DETECTOR_DICT[key]["shape"]) data[0, 0] = 1 if self.mask is not None: _mask = np.zeros_like(data.T) _mask.ravel()[self.mask] = 1 self.mask_widget.set_data(data.T, base=_mask) else: self.mask_widget.set_data(data.T) self.mask_widget.show()
class ObjectTree(QWidget, ComponentMixin): name = 'Object Tree' _stash = [] preferences = Parameter.create(name='Preferences', children=[{ 'name': 'Clear all before each run', 'type': 'bool', 'value': True }, { 'name': 'STL precision', 'type': 'float', 'value': .1 }]) sigObjectsAdded = pyqtSignal(list) sigObjectsRemoved = pyqtSignal(list) sigCQObjectSelected = pyqtSignal(object) sigItemChanged = pyqtSignal(QTreeWidgetItem, int) sigObjectPropertiesChanged = pyqtSignal() def __init__(self, parent): super(ObjectTree, self).__init__(parent) self.tree = tree = QTreeWidget(self) self.properties_editor = ParameterTree(self) tree.setHeaderHidden(True) tree.setItemsExpandable(False) tree.setRootIsDecorated(False) tree.setContextMenuPolicy(Qt.ActionsContextMenu) #forward itemChanged singal tree.itemChanged.connect(\ lambda item,col: self.sigItemChanged.emit(item,col)) #handle visibility changes form tree tree.itemChanged.connect(self.handleChecked) self.CQ = CQRootItem() self.Imports = ImportRootItem() self.Helpers = HelpersRootItem() root = tree.invisibleRootItem() root.addChild(self.CQ) root.addChild(self.Imports) root.addChild(self.Helpers) self._export_STL_action = \ QAction('Export as STL', self, enabled=False, triggered=lambda: \ self.export('*stl','stl', self.preferences['STL precision'])) self._export_STEP_action = \ QAction('Export as SETP', self, enabled=False, triggered=lambda: \ self.export('*step','step',[])) self._clear_current_action = QAction(icon('delete'), 'Clear current', self, enabled=False, triggered=self.removeSelected) self._toolbar_actions = \ [QAction(icon('delete-many'),'Clear all',self,triggered=self.removeObjects), self._clear_current_action,] self.prepareMenu() tree.itemSelectionChanged.connect(self.handleSelection) tree.customContextMenuRequested.connect(self.showMenu) self.prepareLayout() def prepareMenu(self): self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self._context_menu = QMenu(self) self._context_menu.addActions(self._toolbar_actions) self._context_menu.addActions( (self._export_STL_action, self._export_STEP_action)) def prepareLayout(self): self._splitter = splitter((self.tree, self.properties_editor), stretch_factors=(2, 1), orientation=Qt.Vertical) layout(self, (self._splitter, ), top_widget=self) self._splitter.show() def showMenu(self, position): item = self.tree.selectedItems()[-1] if item.parent() is self.CQ: self._export_STL_action.setEnabled(True) else: self._export_STL_action.setEnabled(False) self._context_menu.exec_(self.tree.viewport().mapToGlobal(position)) def menuActions(self): return {'Tools': [self._export_STL_action]} def toolbarActions(self): return self._toolbar_actions def addLines(self): origin = (0, 0, 0) ais_list = [] for name, color, direction in zip(('X', 'Y', 'Z'), (RED, GREEN, BLUE), ((1, 0, 0), (0, 1, 0), (0, 0, 1))): line_placement = Geom_Line( gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction))) line = AIS_Line(line_placement.GetHandle()) line.SetColor(color) self.Helpers.addChild(ObjectTreeItem(name, ais=line)) ais_list.append(line) self.sigObjectsAdded.emit(ais_list) self.tree.expandToDepth(1) @pyqtSlot(dict, bool) @pyqtSlot(dict) def addObjects(self, objects, clean=False, root=None, alpha=0.): if clean or self.preferences['Clear all before each run']: self.removeObjects() ais_list = [] #if root is None: root = self.CQ #remove Vector objects objects_f = \ {k:v for k,v in objects.items() if type(v.val()) not in (Vector,)} for name, shape in objects_f.items(): ais = make_AIS(shape) ais.SetTransparency(alpha) ais_list.append(ais) root.addChild( ObjectTreeItem(name, shape=shape, ais=ais, sig=self.sigObjectPropertiesChanged)) self.sigObjectsAdded.emit(ais_list) @pyqtSlot(object, str, float) def addObject( self, obj, name='', alpha=.0, ): root = self.CQ ais = make_AIS(obj) ais.SetTransparency(alpha) root.addChild( ObjectTreeItem(name, shape=obj, ais=ais, sig=self.sigObjectPropertiesChanged)) self.sigObjectsAdded.emit([ais]) @pyqtSlot(list) @pyqtSlot() def removeObjects(self, objects=None): if objects: removed_items_ais = [self.CQ.takeChild(i).ais for i in objects] else: removed_items_ais = [ch.ais for ch in self.CQ.takeChildren()] self.sigObjectsRemoved.emit(removed_items_ais) @pyqtSlot(bool) def stashObjects(self, action: bool): if action: self._stash = self.CQ.takeChildren() removed_items_ais = [ch.ais for ch in self._stash] self.sigObjectsRemoved.emit(removed_items_ais) else: self.removeObjects() self.CQ.addChildren(self._stash) ais_list = [el.ais for el in self._stash] self.sigObjectsAdded.emit(ais_list) @pyqtSlot() def removeSelected(self): ixs = self.tree.selectedIndexes() rows = [ix.row() for ix in ixs] self.removeObjects(rows) def export(self, file_wildcard, export_type, precision=None): item = self.tree.selectedItems()[-1] if item.parent() is self.CQ: shape = item.shape else: return fname, _ = QFileDialog.getSaveFileName(self, filter=file_wildcard) if fname is not '': export(shape, export_type, fname, precision) @pyqtSlot() def handleSelection(self): item = self.tree.selectedItems()[-1] if item.parent() is self.CQ: self._export_STL_action.setEnabled(True) self._export_STEP_action.setEnabled(True) self._clear_current_action.setEnabled(True) self.sigCQObjectSelected.emit(item.shape) self.properties_editor.setParameters(item.properties, showTop=False) self.properties_editor.setEnabled(True) else: self._export_STL_action.setEnabled(False) self._export_STEP_action.setEnabled(False) self._clear_current_action.setEnabled(False) self.properties_editor.setEnabled(False) self.properties_editor.clear() @pyqtSlot(QTreeWidgetItem, int) def handleChecked(self, item, col): if type(item) is ObjectTreeItem: if item.checkState(0): item.properties['Visible'] = True else: item.properties['Visible'] = False