def __init__(self, updater, config, nodename): """ :param config: :type config: Dictionary? defined in dynamic_reconfigure.client.Client :type nodename: str """ super(GroupWidget, self).__init__() self.state = config['state'] self.param_name = config['name'] self._toplevel_treenode_name = nodename # TODO: .ui file needs to be back into usage in later phase. # ui_file = os.path.join(rp.get_path('rqt_reconfigure'), # 'resource', 'singlenode_parameditor.ui') # loadUi(ui_file, self) verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(QMargins(0, 0, 0, 0)) _widget_nodeheader = QWidget() _h_layout_nodeheader = QHBoxLayout(_widget_nodeheader) _h_layout_nodeheader.setContentsMargins(QMargins(0, 0, 0, 0)) self.nodename_qlabel = QLabel(self) font = QFont('Trebuchet MS, Bold') font.setUnderline(True) font.setBold(True) # Button to close a node. _icon_disable_node = QIcon.fromTheme('window-close') _bt_disable_node = QPushButton(_icon_disable_node, '', self) _bt_disable_node.setToolTip('Hide this node') _bt_disable_node_size = QSize(36, 24) _bt_disable_node.setFixedSize(_bt_disable_node_size) _bt_disable_node.pressed.connect(self._node_disable_bt_clicked) _h_layout_nodeheader.addWidget(self.nodename_qlabel) _h_layout_nodeheader.addWidget(_bt_disable_node) self.nodename_qlabel.setAlignment(Qt.AlignCenter) font.setPointSize(10) self.nodename_qlabel.setFont(font) grid_widget = QWidget(self) self.grid = QFormLayout(grid_widget) verticalLayout.addWidget(_widget_nodeheader) verticalLayout.addWidget(grid_widget, 1) # Again, these UI operation above needs to happen in .ui file. self.tab_bar = None # Every group can have one tab bar self.tab_bar_shown = False self.updater = updater self.editor_widgets = [] self._param_names = [] self._create_node_widgets(config) logging.debug('Groups node name={}'.format(nodename)) self.nodename_qlabel.setText(nodename)
def initUI(self): QToolTip.setFont(QFont('SansSerif', 10)) self.setToolTip('This is a <b>QWidget</b> widget') p = self.palette() p.setColor(self.backgroundRole(), QColor(100, 100, 255)) self.setPalette(p) btn = QPushButton('Button', self) btn.setToolTip('This is a <b>QPushButton</b> widget') btn.resize(btn.sizeHint()) btn.move(50, 50) btn2 = QPushButton('Button', self) btn2.setToolTip('This is a <b>QPushButton 2</b> widget') btn2.resize(btn.sizeHint()) btn2.move(50, 150) self.setGeometry(300, 300, 250, 150) self.setWindowTitle('Tooltips Test Noob') self.show()
class PlotWidget(QWidget): def __init__(self, timeline, parent, topic): super(PlotWidget, self).__init__(parent) self.setObjectName('PlotWidget') self.timeline = timeline msg_type = self.timeline.get_datatype(topic) self.msgtopic = topic self.start_stamp = self.timeline._get_start_stamp() self.end_stamp = self.timeline._get_end_stamp() # the current region-of-interest for our bag file # all resampling and plotting is done with these limits self.limits = [0, (self.end_stamp - self.start_stamp).to_sec()] rp = rospkg.RosPack() ui_file = os.path.join(rp.get_path('rqt_bag_plugins'), 'resource', 'plot.ui') loadUi(ui_file, self) self.message_tree = MessageTree(msg_type, self) self.data_tree_layout.addWidget(self.message_tree) # TODO: make this a dropdown with choices for "Auto", "Full" and # "Custom" # I continue to want a "Full" option here self.auto_res.stateChanged.connect(self.autoChanged) self.resolution.editingFinished.connect(self.settingsChanged) self.resolution.setValidator(QDoubleValidator(0.0, 1000.0, 6, self.resolution)) self.timeline.selected_region_changed.connect(self.region_changed) self.recompute_timestep() self.plot = DataPlot(self) self.plot.set_autoscale(x=False) self.plot.set_autoscale(y=DataPlot.SCALE_VISIBLE) self.plot.autoscroll(False) self.plot.set_xlim(self.limits) self.data_plot_layout.addWidget(self.plot) self._home_button = QPushButton() self._home_button.setToolTip("Reset View") self._home_button.setIcon(QIcon.fromTheme('go-home')) self._home_button.clicked.connect(self.home) self.plot_toolbar_layout.addWidget(self._home_button) self._config_button = QPushButton("Configure Plot") self._config_button.clicked.connect(self.plot.doSettingsDialog) self.plot_toolbar_layout.addWidget(self._config_button) self.set_cursor(0) self.paths_on = set() self._lines = None # get bag from timeline bag = None start_time = self.start_stamp while bag is None: bag, entry = self.timeline.get_entry(start_time, topic) if bag is None: start_time = self.timeline.get_entry_after(start_time)[1].time self.bag = bag # get first message from bag msg = bag._read_message(entry.position) self.message_tree.set_message(msg[1]) # state used by threaded resampling self.resampling_active = False self.resample_thread = None self.resample_fields = set() def set_cursor(self, position): self.plot.vline(position, color=DataPlot.RED) self.plot.redraw() def add_plot(self, path): self.resample_data([path]) def update_plot(self): if len(self.paths_on) > 0: self.resample_data(self.paths_on) def remove_plot(self, path): self.plot.remove_curve(path) self.paths_on.remove(path) self.plot.redraw() def load_data(self): """get a generator for the specified time range on our bag""" return self.bag.read_messages(self.msgtopic, self.start_stamp + rospy.Duration.from_sec(self.limits[0]), self.start_stamp + rospy.Duration.from_sec(self.limits[1])) def resample_data(self, fields): if self.resample_thread: # cancel existing thread and join self.resampling_active = False self.resample_thread.join() for f in fields: self.resample_fields.add(f) # start resampling thread self.resampling_active = True self.resample_thread = threading.Thread(target=self._resample_thread) # explicitly mark our resampling thread as a daemon, because we don't # want to block program exit on a long resampling operation self.resample_thread.setDaemon(True) self.resample_thread.start() def _resample_thread(self): # TODO: # * look into doing partial display updates for long resampling # operations # * add a progress bar for resampling operations x = {} y = {} for path in self.resample_fields: x[path] = [] y[path] = [] # bag object is not thread-safe; lock it while we resample with self.timeline._bag_lock: try: msgdata = self.load_data() except ValueError: # bag is closed or invalid; we're done here self.resampling_active = False return for entry in msgdata: # detect if we're cancelled and return early if not self.resampling_active: return for path in self.resample_fields: # this resampling method is very unstable, because it picks # representative points rather than explicitly representing # the minimum and maximum values present within a sample # If the data has spikes, this is particularly bad because they # will be missed entirely at some resolutions and offsets if x[path] == [] or (entry[2] - self.start_stamp).to_sec() - x[path][-1] >= self.timestep: y_value = entry[1] for field in path.split('.'): index = None if field.endswith(']'): field = field[:-1] field, _, index = field.rpartition('[') y_value = getattr(y_value, field) if index: index = int(index) y_value = y_value[index] y[path].append(y_value) x[path].append((entry[2] - self.start_stamp).to_sec()) # TODO: incremental plot updates would go here... # we should probably do incremental updates based on time; # that is, push new data to the plot maybe every .5 or .1 # seconds # time is a more useful metric than, say, messages loaded or # percentage, because it will give a reasonable refresh rate # without overloading the computer # if we had a progress bar, we could emit a signal to update it here # update the plot with final resampled data for path in self.resample_fields: if len(x[path]) < 1: qWarning("Resampling resulted in 0 data points for %s" % path) else: if path in self.paths_on: self.plot.clear_values(path) self.plot.update_values(path, x[path], y[path]) else: self.plot.add_curve(path, path, x[path], y[path]) self.paths_on.add(path) self.plot.redraw() self.resample_fields.clear() self.resampling_active = False def recompute_timestep(self): # this is only called if we think the timestep has changed; either # by changing the limits or by editing the resolution limits = self.limits if self.auto_res.isChecked(): timestep = round((limits[1] - limits[0]) / 200.0, 5) else: timestep = float(self.resolution.text()) self.resolution.setText(str(timestep)) self.timestep = timestep def region_changed(self, start, end): # this is the only place where self.limits is set limits = [(start - self.start_stamp).to_sec(), (end - self.start_stamp).to_sec()] # cap the limits to the start and end of our bag file if limits[0] < 0: limits = [0.0, limits[1]] if limits[1] > (self.end_stamp - self.start_stamp).to_sec(): limits = [limits[0], (self.end_stamp - self.start_stamp).to_sec()] self.limits = limits self.recompute_timestep() self.plot.set_xlim(limits) self.plot.redraw() self.update_plot() def settingsChanged(self): # resolution changed. recompute the timestep and resample self.recompute_timestep() self.update_plot() def autoChanged(self, state): if state == 2: # auto mode enabled. recompute the timestep and resample self.resolution.setDisabled(True) self.recompute_timestep() self.update_plot() else: # auto mode disabled. enable the resolution text box # no change to resolution yet, so no need to redraw self.resolution.setDisabled(False) def home(self): # TODO: re-add the button for this. It's useful for restoring the # X and Y limits so that we can see all of the data # effectively a "zoom all" button # reset the plot to our current limits self.plot.set_xlim(self.limits) # redraw the plot; this forces a Y autoscaling self.plot.redraw()
class PlotWidget(QWidget): def __init__(self, timeline, parent, topic): super(PlotWidget, self).__init__(parent) self.setObjectName('PlotWidget') self.timeline = timeline msg_type = self.timeline.get_datatype(topic) self.msgtopic = topic self.start_stamp = self.timeline._get_start_stamp() self.end_stamp = self.timeline._get_end_stamp() # the current region-of-interest for our bag file # all resampling and plotting is done with these limits self.limits = [0,(self.end_stamp-self.start_stamp).to_sec()] rp = rospkg.RosPack() ui_file = os.path.join(rp.get_path('rqt_bag_plugins'), 'resource', 'plot.ui') loadUi(ui_file, self) self.message_tree = MessageTree(msg_type, self) self.data_tree_layout.addWidget(self.message_tree) # TODO: make this a dropdown with choices for "Auto", "Full" and # "Custom" # I continue to want a "Full" option here self.auto_res.stateChanged.connect(self.autoChanged) self.resolution.editingFinished.connect(self.settingsChanged) self.resolution.setValidator(QDoubleValidator(0.0,1000.0,6,self.resolution)) self.timeline.selected_region_changed.connect(self.region_changed) self.recompute_timestep() self.plot = DataPlot(self) self.plot.set_autoscale(x=False) self.plot.set_autoscale(y=DataPlot.SCALE_VISIBLE) self.plot.autoscroll(False) self.plot.set_xlim(self.limits) self.data_plot_layout.addWidget(self.plot) self._home_button = QPushButton() self._home_button.setToolTip("Reset View") self._home_button.setIcon(QIcon.fromTheme('go-home')) self._home_button.clicked.connect(self.home) self.plot_toolbar_layout.addWidget(self._home_button) self._config_button = QPushButton("Configure Plot") self._config_button.clicked.connect(self.plot.doSettingsDialog) self.plot_toolbar_layout.addWidget(self._config_button) self.set_cursor(0) self.paths_on = set() self._lines = None # get bag from timeline bag = None start_time = self.start_stamp while bag is None: bag,entry = self.timeline.get_entry(start_time, topic) if bag is None: start_time = self.timeline.get_entry_after(start_time)[1].time self.bag = bag # get first message from bag msg = bag._read_message(entry.position) self.message_tree.set_message(msg[1]) # state used by threaded resampling self.resampling_active = False self.resample_thread = None self.resample_fields = set() def set_cursor(self, position): self.plot.vline(position, color=DataPlot.RED) self.plot.redraw() def add_plot(self, path): self.resample_data([path]) def update_plot(self): if len(self.paths_on)>0: self.resample_data(self.paths_on) def remove_plot(self, path): self.plot.remove_curve(path) self.paths_on.remove(path) self.plot.redraw() def load_data(self): """get a generator for the specified time range on our bag""" return self.bag.read_messages(self.msgtopic, self.start_stamp+rospy.Duration.from_sec(self.limits[0]), self.start_stamp+rospy.Duration.from_sec(self.limits[1])) def resample_data(self, fields): if self.resample_thread: # cancel existing thread and join self.resampling_active = False self.resample_thread.join() for f in fields: self.resample_fields.add(f) # start resampling thread self.resampling_active = True self.resample_thread = threading.Thread(target=self._resample_thread) # explicitly mark our resampling thread as a daemon, because we don't # want to block program exit on a long resampling operation self.resample_thread.setDaemon(True) self.resample_thread.start() def _resample_thread(self): # TODO: # * look into doing partial display updates for long resampling # operations # * add a progress bar for resampling operations x = {} y = {} for path in self.resample_fields: x[path] = [] y[path] = [] msgdata = self.load_data() for entry in msgdata: # detect if we're cancelled and return early if not self.resampling_active: return for path in self.resample_fields: # this resampling method is very unstable, because it picks # representative points rather than explicitly representing # the minimum and maximum values present within a sample # If the data has spikes, this is particularly bad because they # will be missed entirely at some resolutions and offsets if x[path]==[] or (entry[2]-self.start_stamp).to_sec()-x[path][-1] >= self.timestep: y_value = entry[1] for field in path.split('.'): index = None if field.endswith(']'): field = field[:-1] field, _, index = field.rpartition('[') y_value = getattr(y_value, field) if index: index = int(index) y_value = y_value[index] y[path].append(y_value) x[path].append((entry[2]-self.start_stamp).to_sec()) # TODO: incremental plot updates would go here... # we should probably do incremental updates based on time; # that is, push new data to the plot maybe every .5 or .1 # seconds # time is a more useful metric than, say, messages loaded or # percentage, because it will give a reasonable refresh rate # without overloading the computer # if we had a progress bar, we could emit a signal to update it here # update the plot with final resampled data for path in self.resample_fields: if len(x[path]) < 1: qWarning("Resampling resulted in 0 data points for %s" % path) else: if path in self.paths_on: self.plot.clear_values(path) self.plot.update_values(path, x[path], y[path]) else: self.plot.add_curve(path, path, x[path], y[path]) self.paths_on.add(path) self.plot.redraw() self.resample_fields.clear() self.resampling_active = False def recompute_timestep(self): # this is only called if we think the timestep has changed; either # by changing the limits or by editing the resolution limits = self.limits if self.auto_res.isChecked(): timestep = round((limits[1]-limits[0])/200.0,5) else: timestep = float(self.resolution.text()) self.resolution.setText(str(timestep)) self.timestep = timestep def region_changed(self, start, end): # this is the only place where self.limits is set limits = [ (start - self.start_stamp).to_sec(), (end - self.start_stamp).to_sec() ] # cap the limits to the start and end of our bag file if limits[0]<0: limits = [0.0,limits[1]] if limits[1]>(self.end_stamp-self.start_stamp).to_sec(): limits = [limits[0],(self.end_stamp-self.start_stamp).to_sec()] self.limits = limits self.recompute_timestep() self.plot.set_xlim(limits) self.plot.redraw() self.update_plot() def settingsChanged(self): # resolution changed. recompute the timestep and resample self.recompute_timestep() self.update_plot() def autoChanged(self, state): if state==2: # auto mode enabled. recompute the timestep and resample self.resolution.setDisabled(True) self.recompute_timestep() self.update_plot() else: # auto mode disabled. enable the resolution text box # no change to resolution yet, so no need to redraw self.resolution.setDisabled(False) def home(self): # TODO: re-add the button for this. It's useful for restoring the # X and Y limits so that we can see all of the data # effectively a "zoom all" button # reset the plot to our current limits self.plot.set_xlim(self.limits) # redraw the plot; this forces a Y autoscaling self.plot.redraw()
def __init__(self, updater, config, nodename): ''' :param config: :type config: Dictionary? defined in dynamic_reconfigure.client.Client :type nodename: str ''' super(GroupWidget, self).__init__() self.state = config['state'] self.param_name = config['name'] self._toplevel_treenode_name = nodename # TODO: .ui file needs to be back into usage in later phase. # ui_file = os.path.join(rp.get_path('rqt_reconfigure'), # 'resource', 'singlenode_parameditor.ui') # loadUi(ui_file, self) verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(QMargins(0, 0, 0, 0)) _widget_nodeheader = QWidget() _h_layout_nodeheader = QHBoxLayout(_widget_nodeheader) _h_layout_nodeheader.setContentsMargins(QMargins(0, 0, 0, 0)) self.nodename_qlabel = QLabel(self) font = QFont('Trebuchet MS, Bold') font.setUnderline(True) font.setBold(True) # Button to close a node. _icon_disable_node = QIcon.fromTheme('window-close') _bt_disable_node = QPushButton(_icon_disable_node, '', self) _bt_disable_node.setToolTip('Hide this node') _bt_disable_node_size = QSize(36, 24) _bt_disable_node.setFixedSize(_bt_disable_node_size) _bt_disable_node.pressed.connect(self._node_disable_bt_clicked) _h_layout_nodeheader.addWidget(self.nodename_qlabel) _h_layout_nodeheader.addWidget(_bt_disable_node) self.nodename_qlabel.setAlignment(Qt.AlignCenter) font.setPointSize(10) self.nodename_qlabel.setFont(font) grid_widget = QWidget(self) self.grid = QFormLayout(grid_widget) verticalLayout.addWidget(_widget_nodeheader) verticalLayout.addWidget(grid_widget, 1) # Again, these UI operation above needs to happen in .ui file. self.tab_bar = None # Every group can have one tab bar self.tab_bar_shown = False self.updater = updater self.editor_widgets = [] self._param_names = [] self._create_node_widgets(config) rospy.logdebug('Groups node name={}'.format(nodename)) self.nodename_qlabel.setText(nodename)
def __init__(self, context, node_name): """ Initializaze things. :type node_name: str """ super(ParamClientWidget, self).__init__() self._node_grn = node_name self._toplevel_treenode_name = node_name self._editor_widgets = {} self._param_client = create_param_client( context.node, node_name, self._handle_param_event ) verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(QMargins(0, 0, 0, 0)) widget_nodeheader = QWidget() h_layout_nodeheader = QHBoxLayout(widget_nodeheader) h_layout_nodeheader.setContentsMargins(QMargins(0, 0, 0, 0)) nodename_qlabel = QLabel(self) font = QFont('Trebuchet MS, Bold') font.setUnderline(True) font.setBold(True) font.setPointSize(10) nodename_qlabel.setFont(font) nodename_qlabel.setAlignment(Qt.AlignCenter) nodename_qlabel.setText(node_name) h_layout_nodeheader.addWidget(nodename_qlabel) # Button to close a node. icon_disable_node = QIcon.fromTheme('window-close') bt_disable_node = QPushButton(icon_disable_node, '', self) bt_disable_node.setToolTip('Hide this node') bt_disable_node_size = QSize(36, 24) bt_disable_node.setFixedSize(bt_disable_node_size) bt_disable_node.pressed.connect(self._node_disable_bt_clicked) h_layout_nodeheader.addWidget(bt_disable_node) grid_widget = QWidget(self) self.grid = QFormLayout(grid_widget) verticalLayout.addWidget(widget_nodeheader) verticalLayout.addWidget(grid_widget, 1) # Again, these UI operation above needs to happen in .ui file. param_names = self._param_client.list_parameters() self.add_editor_widgets( self._param_client.get_parameters(param_names), self._param_client.describe_parameters(param_names) ) # Save and load buttons button_widget = QWidget(self) button_header = QHBoxLayout(button_widget) button_header.setContentsMargins(QMargins(0, 0, 0, 0)) load_button = QPushButton() save_button = QPushButton() load_button.setIcon(QIcon.fromTheme('document-open')) save_button.setIcon(QIcon.fromTheme('document-save')) load_button.clicked[bool].connect(self._handle_load_clicked) save_button.clicked[bool].connect(self._handle_save_clicked) button_header.addWidget(save_button) button_header.addWidget(load_button) self.setMinimumWidth(150)