def load_ui(name, custom_widgets=[], parent=None): loader = QUiLoader() for cw in custom_widgets: loader.registerCustomWidget(cw) path = resource_path(posixpath.join("ui", name)) ui_file = QFile(path) if not ui_file.open(QFile.ReadOnly): logger.error("Cannot open {}: {}".format(path, ui_file.errorString())) sys.exit(-1) ui = loader.load(ui_file, parent) ui_file.close() return ui
def load_ui(name, custom_widgets=[], parent=None): loader = QUiLoader() for cw in custom_widgets: loader.registerCustomWidget(cw) path = os.path.join(os.path.dirname(__file__), "ui", name) ui_file = QFile(path) if not ui_file.open(QFile.ReadOnly): logging.critical("Cannot open {}: {}".format(path, ui_file.errorString())) sys.exit(-1) ui = loader.load(ui_file, parent) ui_file.close() return ui
class QtFrontend(ViewSBFrontend): """ Qt Frontend that consumes packets for display. """ UI_NAME = 'qt' UI_DESCRIPTION = 'unstable GUI in Qt' # So, Qt's tree widgets require that column 0 have the expand arrow, but you _can_ change # where column 0 is displayed. # We want the summary column to have the expand arrow, so we'll swap it # with the timestamp column in __init__(). COLUMN_TIMESTAMP = 5 COLUMN_DEVICE = 1 COLUMN_ENDPOINT = 2 COLUMN_DIRECTION = 3 COLUMN_LENGTH = 4 COLUMN_SUMMARY = 0 COLUMN_STATUS = 6 COLUMN_DATA = 7 @staticmethod def reason_to_be_disabled(): try: import PySide6 except (ImportError, ModuleNotFoundError): return "PySide6 (Qt library) not available." return None @staticmethod def _stringify_list(lst): """ Tiny helper than runs the str constructor on every item in a list, but specifically handles two cases: 1) the object in question is None, which we instead want to display as an empty string, 2) the resulting string contains a null character, which Qt doesn't like, so we'll represent it to the user as, literally, \0. """ return [ str(x).replace('\0', r'\0') if x is not None else '' for x in lst ] def _create_item_for_packet(self, viewsb_packet): """ Creates a QTreeWidgetItem for a given ViewSBPacket. Args: viewsb_packet -- The ViewSBPacket to create the QTreeWidgetItem from. Returns a QTreeWidgetItem. """ def get_packet_string_array(viewsb_packet): """ Tiny helper to return and stringify the common fields used for the columns of tree items. """ direction = viewsb_packet.direction.name if viewsb_packet.direction is not None else '' length = len( viewsb_packet.data) if viewsb_packet.data is not None else '' return self._stringify_list([ viewsb_packet.summarize(), viewsb_packet.device_address, viewsb_packet.endpoint_number, direction, length, viewsb_packet.timestamp, viewsb_packet.summarize_status(), viewsb_packet.summarize_data() ]) + [viewsb_packet] item = QTreeWidgetItem(get_packet_string_array(viewsb_packet)) # Give the item a reference to the original packet object. item.setData(0, QtCore.Qt.UserRole, viewsb_packet) return item def _recursively_walk_packet(self, viewsb_packet): """ Recursively walks packet subordinates, batching QTreeWidgetItem.addChildren as much as possible. Args: viewsb_packet -- The top-level packet (as far as the caller's context is concerned). """ packet_item = self._create_item_for_packet(viewsb_packet) packet_children_list = [] for sub_packet in viewsb_packet.subordinate_packets: # Create the item for this packet, and recursively fill its children. packet_children_list.append( self._recursively_walk_packet(sub_packet)) packet_item.addChildren(packet_children_list) return packet_item def __init__(self): """ Sets up the Qt UI. """ QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) signal.signal( signal.SIGINT, signal.SIG_DFL) # fix SIGINT handling - cleanly exit on ctrl+c self.app = QApplication.instance() or QApplication([]) try: import qt_material qt_material.apply_stylesheet(self.app, 'light_blue.xml') except ImportError: pass self.ui_file = QtCore.QFile( os.path.dirname(os.path.realpath(__file__)) + '/qt.ui') self.loader = QUiLoader() self.loader.registerCustomWidget(ViewSBQTreeWidget) self.loader.registerCustomWidget(ViewSBHexView) self.window = self.loader.load(self.ui_file) # type: QMainWindow # Swap columns 0 and 5 to put the expand arrow on the summary column. self.window.usb_tree_widget.header().swapSections(0, 5) self.window.usb_tree_widget.header().setSectionResizeMode( QHeaderView.ResizeToContents) self.window.update_timer = QtCore.QTimer() self.window.update_timer.timeout.connect(self._update) self.window.usb_tree_widget.currentItemChanged.connect( self._tree_current_item_changed) self.window.usb_tree_widget = self.window.usb_tree_widget self.window.usb_tree_widget.sortByColumn(0, Qt.SortOrder.AscendingOrder) def ready(self): """ Called when the backend is ready to stream. """ self.window.showMaximized() def _update(self): """ Called by the QTimer `update_timer`; collects packets the queue and adds them to the tree view. We use this instead of calling `handle_communications` and defining `handle_incoming_packet`, because adding items one at a time as we receive them is slower than batching them. Note: Since this is called via a QTimer signal, this method runs in the UI thread. """ # Handle exceptions if self._exception_conn.poll(): self.handle_exception(*self._exception_conn.recv()) # TODO: overide handle_exception to show a Qt dialog message # If the process manager told us to stop (which might happen if e.g. the backend exits), # then stop and exit. if self.termination_event.is_set(): self.app.closeAllWindows() packet_list = [] try: # Get as many packets as we can as quick as we can. while True: packet = self.data_queue.get_nowait() packet_list.append(packet) # But the instant it's empty, don't wait for any more; just send them to be processed. except multiprocessing.queues.Empty: pass finally: self.add_packets(packet_list) def _tree_current_item_changed(self, current_item, _previous_item): """ Handler for the QTreeWidget.currentItemChanged() signal that populates the side panels with detail fields and a hex representation of the current packet. """ # Clear the details widget. self.window.usb_details_tree_widget.clear() current_packet = current_item.data(0, QtCore.Qt.UserRole) # A list of 2-tuples: first element is a table title, and the second is usually a string:string dict. detail_fields = current_packet.get_detail_fields() if detail_fields: self.update_detail_fields(detail_fields) self.window.usb_hex_view.populate(current_packet.get_raw_data()) def update_detail_fields(self, detail_fields): """ Populates the detail view with the relevant fields for the selected packet. """ # Each table will have a root item in the details view. root_items = [] for table in detail_fields: title = table[0] root = QTreeWidgetItem([title]) children = [] fields = table[1] # The usual case: a str:str dict. if isinstance(fields, dict): for key, value in fields.items(): children.append( QTreeWidgetItem(self._stringify_list([key, value]))) # Sometimes it'll just be a 1-column list. elif isinstance(fields, list): for item in fields: children.append( QTreeWidgetItem(self._stringify_list([item]))) # Sometimes it'll just be a string, or a `bytes` instance. else: children.append(QTreeWidgetItem(self._stringify_list([fields ]))) root.addChildren(children) # Add an empty "item" between each table. root_items.extend([root, QTreeWidgetItem([])]) self.window.usb_details_tree_widget.addTopLevelItems(root_items) self.window.usb_details_tree_widget.expandAll() self.window.usb_details_tree_widget.resizeColumnToContents(0) self.window.usb_details_tree_widget.resizeColumnToContents(1) def add_packets(self, viewsb_packets): """ Adds a list of top-level ViewSB packets to the tree. We're in the UI thread; every bit of overhead counts, so let's batch as much as possible. """ top_level_items_list = [] for viewsb_packet in viewsb_packets: # Create the item for this packet, and recursively fill its children. top_level_items_list.append( self._recursively_walk_packet(viewsb_packet)) self.window.usb_tree_widget.addTopLevelItems(top_level_items_list) def run(self): """ Overrides ViewSBFrontend.run(). """ self.wait_for_backend_ready() # TODO: is there a better value than 100 ms? Should it be configurable by the Analyzer? self.window.update_timer.start(100) self.app.exec_() self.stop() def stop(self): self.app.closeAllWindows() self.termination_event.set()
def __init__(self, data_model, main_app): """Initialize the widget. Args: data_model: the data model holding the data. """ self.data_model = data_model loader = QUiLoader() loader.registerCustomWidget(pg.PlotWidget) loader.registerCustomWidget(pg.SpinBox) self.ui = loader.load(resources.path("tailor.resources", "plot_tab.ui")) # store reference to this code in widget self.ui.code = self self.main_app = main_app self.ui.param_layout = QtWidgets.QVBoxLayout() self.ui.param_layout.setContentsMargins(4, 0, 0, 0) self.ui.parameter_box.setLayout(self.ui.param_layout) self._params = {} self._symbols = set(asteval.Interpreter().symtable.keys()) # FIXME move this to create_plot, or vice versa? self._initial_param_plot = self.ui.plot_widget.plot(symbol=None, pen=pg.mkPen( color="00F4", width=4)) self._fit_plot = self.ui.plot_widget.plot(symbol=None, pen=pg.mkPen(color="r", width=4)) self.ui.fit_domain_area = pg.LinearRegionItem(movable=True, brush="00F1") # Set options affecting the UI self.ui.fit_start_box.setOpts(value=-np.inf, dec=True, step=0.1, finite=True, compactHeight=False) self.ui.fit_end_box.setOpts(value=np.inf, dec=True, step=0.1, finite=True, compactHeight=False) self.ui.fit_start_box.setMaximumWidth(75) self.ui.fit_end_box.setMaximumWidth(75) self.ui.draw_curve_option.addItems(DRAW_CURVE_OPTIONS) # lambda is necessary to gobble the 'index' parameter of the # currentIndexChanged signal self.ui.draw_curve_option.currentIndexChanged.connect( lambda index: self.update_best_fit_plot()) # Connect signals self.ui.model_func.textChanged.connect(self.update_fit_params) self.ui.show_initial_fit.stateChanged.connect(self.plot_initial_model) self.ui.fit_start_box.sigValueChanging.connect(self.update_fit_domain) self.ui.fit_end_box.sigValueChanging.connect(self.update_fit_domain) self.ui.use_fit_domain.stateChanged.connect(self.toggle_use_fit_domain) self.ui.fit_domain_area.sigRegionChanged.connect( self.fit_domain_region_changed) self.ui.fit_button.clicked.connect(self.perform_fit) self.ui.xlabel.textChanged.connect(self.update_xlabel) self.ui.ylabel.textChanged.connect(self.update_ylabel) self.ui.xmin.textChanged.connect(self.update_limits) self.ui.xmax.textChanged.connect(self.update_limits) self.ui.ymin.textChanged.connect(self.update_limits) self.ui.ymax.textChanged.connect(self.update_limits) self.ui.set_limits_button.clicked.connect(self.update_limits) self.ui.plot_widget.sigXRangeChanged.connect(self.updated_plot_range) self.ui.plot_widget.setMenuEnabled(False) self.ui.plot_widget.hideButtons()