Ejemplo n.º 1
0
    def _create_cell_push_button(self, text, clicked_cb, icon=None):
        widget = QWidget()

        button = QPushButton()
        button.setText(text)
        if icon:
            button.setIcon(icon)
        button.clicked[bool].connect(clicked_cb)

        hlayout = QHBoxLayout(widget)
        hlayout.addWidget(button)
        hlayout.setAlignment(Qt.AlignCenter)
        hlayout.setContentsMargins(0, 0, 0, 0)

        widget.setLayout(hlayout)
        return widget
Ejemplo n.º 2
0
    def __init__(self, parent=None):
        super(VisualizerWidget, self).__init__(parent)
        self.setWindowTitle('Graph Profiler Visualizer')
        vbox = QVBoxLayout()
        self.setLayout(vbox)

        toolbar_layout = QHBoxLayout()
        refresh_button = QPushButton()
        refresh_button.setIcon(QIcon.fromTheme('view-refresh'))
        auto_refresh_checkbox = QCheckBox("Auto Refresh")
        hide_disconnected_topics = QCheckBox("Hide Disconnected Topics")
        topic_blacklist_button = QPushButton("Topic Blacklist")
        node_blacklist_button = QPushButton("Node Blacklist")

        refresh_button.clicked.connect(self._refresh)
        topic_blacklist_button.clicked.connect(self._edit_topic_blacklist)
        node_blacklist_button.clicked.connect(self._edit_node_blacklist)
        auto_refresh_checkbox.setCheckState(2)
        auto_refresh_checkbox.stateChanged.connect(self._autorefresh_changed)
        hide_disconnected_topics.setCheckState(2)
        hide_disconnected_topics.stateChanged.connect(
            self._hidedisconnectedtopics_changed)

        toolbar_layout.addWidget(refresh_button)
        toolbar_layout.addWidget(auto_refresh_checkbox)
        toolbar_layout.addStretch(0)
        toolbar_layout.addWidget(hide_disconnected_topics)
        toolbar_layout.addWidget(topic_blacklist_button)
        toolbar_layout.addWidget(node_blacklist_button)
        vbox.addLayout(toolbar_layout)

        # Initialize the Visualizer
        self._view = qt_view.QtView()
        self._adapter = rosprofiler_adapter.ROSProfileAdapter(self._view)
        self._adapter.set_topic_quiet_list(TOPIC_BLACKLIST)
        self._adapter.set_node_quiet_list(NODE_BLACKLIST)
        vbox.addWidget(self._view)
Ejemplo n.º 3
0
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 ParamClientWidget(GroupWidget):
    """
    A wrapper of dynamic_reconfigure.client instance.

    Represents a widget where users can view and modify ROS params.
    """
    def __init__(self, reconf, node_name):
        """
        :type reconf: dynamic_reconfigure.client
        :type node_name: str
        """
        group_desc = reconf.get_group_descriptions()
        logging.debug('ParamClientWidget.group_desc=%s', group_desc)
        super(ParamClientWidget, self).__init__(ParamUpdater(reconf),
                                                group_desc, node_name)

        # Save and load buttons
        self.button_widget = QWidget(self)
        self.button_header = QHBoxLayout(self.button_widget)
        self.button_header.setContentsMargins(QMargins(0, 0, 0, 0))

        self.load_button = QPushButton()
        self.save_button = QPushButton()

        self.load_button.setIcon(QIcon.fromTheme('document-open'))
        self.save_button.setIcon(QIcon.fromTheme('document-save'))

        self.load_button.clicked[bool].connect(self._handle_load_clicked)
        self.save_button.clicked[bool].connect(self._handle_save_clicked)

        self.button_header.addWidget(self.save_button)
        self.button_header.addWidget(self.load_button)

        self.setMinimumWidth(150)

        self.reconf = reconf
        self.updater.start()
        self.reconf.config_callback = self.config_callback
        self._node_grn = node_name

    def get_node_grn(self):
        return self._node_grn

    def config_callback(self, config):

        # TODO: Think about replacing callback architecture with signals.

        if config:
            # TODO: should use config.keys but this method doesnt exist

            names = [name for name, v in config.items()]
            # v isn't used but necessary to get key and put it into dict.
            # logging.debug('config_callback name={} v={}'.format(name, v))

            for widget in self.editor_widgets:
                if isinstance(widget, EditorWidget):
                    if widget.param_name in names:
                        logging.debug('EDITOR widget.param_name=%s',
                                      widget.param_name)
                        widget.update_value(config[widget.param_name])
                elif isinstance(widget, GroupWidget):
                    cfg = find_cfg(config, widget.param_name)
                    logging.debug('GROUP widget.param_name=%s',
                                  widget.param_name)
                    widget.update_group(cfg)

    def _handle_load_clicked(self):
        filename = QFileDialog.getOpenFileName(
            self, self.tr('Load from File'), '.',
            self.tr('YAML file {.yaml} (*.yaml)'))
        if filename[0] != '':
            self.load_param(filename[0])

    def _handle_save_clicked(self):
        filename = QFileDialog.getSaveFileName(
            self, self.tr('Save parameters to file...'), '.',
            self.tr('YAML files {.yaml} (*.yaml)'))
        if filename[0] != '':
            self.save_param(filename[0])

    def save_param(self, filename):
        configuration = self.reconf.get_configuration()
        if configuration is not None:
            with open(filename, 'w') as f:
                yaml.dump(configuration, f)

    def load_param(self, filename):
        with open(filename, 'r') as f:
            configuration = {}
            for doc in yaml.load_all(f.read()):
                configuration.update(doc)

        try:
            self.reconf.update_configuration(configuration)
        except ServiceException as e:
            logging.warn(
                "Call for reconfiguration wasn't successful because: %s",
                e.message)
        except DynamicReconfigureParameterException as e:
            logging.warn("Reconfiguration wasn't successful because: %s",
                         e.message)
        except DynamicReconfigureCallbackException as e:
            logging.warn("Reconfiguration wasn't successful because: %s",
                         e.message)

    def close(self):
        self.reconf.close()
        self.updater.stop()

        for w in self.editor_widgets:
            w.close()

        self.deleteLater()

    def filter_param(self, filter_key):
        # TODO impl
        pass
class DynreconfClientWidget(GroupWidget):
    """
    A wrapper of dynamic_reconfigure.client instance.
    Represents a widget where users can view and modify ROS params.
    """

    def __init__(self, reconf, node_name):
        """
        :type reconf: dynamic_reconfigure.client
        :type node_name: str
        """

        group_desc = reconf.get_group_descriptions()
        rospy.logdebug('DynreconfClientWidget.group_desc=%s', group_desc)
        super(DynreconfClientWidget, self).__init__(ParamUpdater(reconf),
                                                    group_desc, node_name)

        # Save and load buttons
        self.button_widget = QWidget(self)
        self.button_header = QHBoxLayout(self.button_widget)
        self.button_header.setContentsMargins(QMargins(0, 0, 0, 0))

        self.load_button = QPushButton()
        self.save_button = QPushButton()

        self.load_button.setIcon(QIcon.fromTheme('document-open'))
        self.save_button.setIcon(QIcon.fromTheme('document-save'))

        self.load_button.clicked[bool].connect(self._handle_load_clicked)
        self.save_button.clicked[bool].connect(self._handle_save_clicked)

        self.button_header.addWidget(self.save_button)
        self.button_header.addWidget(self.load_button)

        self.setMinimumWidth(150)

        self.reconf = reconf
        self.updater.start()
        self.reconf.config_callback = self.config_callback
        self._node_grn = node_name

    def get_node_grn(self):

        return self._node_grn

    def config_callback(self, config):

        #TODO: Think about replacing callback architecture with signals.

        if config:
            # TODO: should use config.keys but this method doesnt exist

            names = [name for name, v in config.items()]
            # v isn't used but necessary to get key and put it into dict.
            rospy.logdebug('config_callback name={} v={}'.format(name, v))

            for widget in self.editor_widgets:
                if isinstance(widget, EditorWidget):
                    if widget.param_name in names:
                        rospy.logdebug('EDITOR widget.param_name=%s',
                                       widget.param_name)
                        widget.update_value(config[widget.param_name])
                elif isinstance(widget, GroupWidget):
                    cfg = find_cfg(config, widget.param_name)
                    rospy.logdebug('GROUP widget.param_name=%s',
                                   widget.param_name)
                    widget.update_group(cfg)

    def _handle_load_clicked(self):
        filename = QFileDialog.getOpenFileName(
                self, self.tr('Load from File'), '.',
                self.tr('YAML file {.yaml} (*.yaml)'))
        if filename[0] != '':
            self.load_param(filename[0])

    def _handle_save_clicked(self):
        filename = QFileDialog.getSaveFileName(
                self, self.tr('Save parameters to file...'), '.',
                self.tr('YAML files {.yaml} (*.yaml)'))
        if filename[0] != '':
            self.save_param(filename[0])

    def save_param(self, filename):
        configuration = self.reconf.get_configuration()
        if configuration is not None:
            with file(filename, 'w') as f:
                yaml.dump(configuration, f)

    def load_param(self, filename):
        with file(filename, 'r') as f:
            configuration = {}
            for doc in yaml.load_all(f.read()):
                configuration.update(doc)
        self.reconf.update_configuration(configuration)

    def close(self):
        self.reconf.close()
        self.updater.stop()

        for w in self.editor_widgets:
            w.close()

        self.deleteLater()

    def filter_param(self, filter_key):
        #TODO impl
        pass
Ejemplo n.º 6
0
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()
Ejemplo n.º 7
0
    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)