Exemple #1
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._ui_filename = None
        self._macros_filename = None
        self.template_widget = PyDMEmbeddedDisplay(parent=self)
        self.template_widget.hide()
        self.template_widget.loadWhenShown = False
        self._macros = []
        self._channel_headers = []
        self._macro_headers = []
        self._header_map = {}
        self._channels = []
        self._filters = {}
        self._initial_sort_header = 'index'
        self._initial_sort_ascend = True
        self._hide_headers = []

        # Table settings
        self.setShowGrid(True)
        self.setSortingEnabled(False)
        self.setSelectionMode(self.NoSelection)
        self.horizontalHeader().setStretchLastSection(True)
        self.horizontalHeader().hide()

        self.configurable = False

        self._watching_cells = False
Exemple #2
0
    def setup_outputs(self):
        ffs = self.config.get('fastfaults')
        if not ffs:
            return
        outs_container = self.ui.arbiter_outputs_content
        if outs_container is None:
            return
        count = 0
        for ff in ffs:
            prefix = ff.get('prefix')
            ffo_start = ff.get('ffo_start')
            ffo_end = ff.get('ffo_end')

            ffos_zfill = len(str(ffo_end)) + 1

            entries = range(ffo_start, ffo_end + 1)

            template = 'templates/arbiter_outputs_entry.ui'
            for _ffo in entries:
                s_ffo = str(_ffo).zfill(ffos_zfill)
                macros = dict(index=count, P=prefix, FFO=s_ffo)
                widget = PyDMEmbeddedDisplay(parent=outs_container)
                widget.macros = json.dumps(macros)
                widget.filename = template
                widget.disconnectWhenHidden = False
                outs_container.layout().addWidget(widget)
                count += 1
        print(f'Added {count} arbiter outputs')
Exemple #3
0
 def setup_ui(self):
     self.setLayout(QVBoxLayout())
     self.titleLabel = QLabel(self)
     self.titleLabel.setText("{} Displays".format(
         self.formatted_subsystem()))
     self.layout().addWidget(self.titleLabel)
     self.tab_widget = PyDMTabWidget(self)
     self.layout().addWidget(self.tab_widget)
     top = os.path.dirname(os.path.realpath(__file__))
     with open(os.path.join(top, 'utilities', 'sectors.json')) as f:
         sectors = json.load(f)
         for i, sector in enumerate(sectors):
             page = QWidget()
             page.setLayout(QVBoxLayout())
             page.layout().setContentsMargins(0, 0, 0, 0)
             emb = PyDMEmbeddedDisplay()
             emb.macros = json.dumps(sector)
             emb.loadWhenShown = False
             emb.disconnectWhenHidden = True
             page.layout().addWidget(emb)
             self.tab_widget.addTab(page, sector['name'])
             if sector['name'] == self.selected_area:
                 self.tab_widget.setCurrentIndex(i)
                 self.tab_changed(i)
     self.tab_widget.currentChanged.connect(self.tab_changed)
Exemple #4
0
    def do_search(self):
        # First lets clear the old connections
        self.app.close_widget_connections(self.frm_result)

        # For each widget inside the results frame, lets destroy them
        for widget in self.frm_result.findChildren(QWidget):
            widget.setParent(None)
            widget.deleteLater()
        
        # Grab the filter text
        filter_text = self.txt_filter.text()

        # For every entry in the dataset...
        for entry in self.data:
            # Check if they match our filter
            if filter_text.upper() not in entry.upper():
                continue
            # Create a PyDMEmbeddedDisplay for this entry
            disp = PyDMEmbeddedDisplay(parent=self)
            disp.macros = json.dumps({"MOTOR":entry})
            disp.filename = 'inline_motor.ui'
            disp.setMinimumWidth(700)
            disp.setMinimumHeight(40)
            disp.setMaximumHeight(100)
            # Add the Embedded Display to the Results Layout
            self.results_layout.addWidget(disp)
        # Recursively establish the connection for widgets
        # inside the Results Frame
        self.app.establish_widget_connections(self.frm_result)
Exemple #5
0
 def show_motors(self):
     for m in self.motors:
         disp = PyDMEmbeddedDisplay(parent=self)
         disp.macros = json.dumps({"MOTOR": m})
         if os.path.exists(os.path.join(LOCAL_PATH, 'motors.ui')):
             disp.filename = os.path.join(LOCAL_PATH, 'motors.ui')
         disp.setMinimumWidth(300)
         disp.setMinimumHeight(24)
         disp.setMaximumHeight(30)
         # Add the Embedded Display to the Results Layout
         self.results_layout.addWidget(disp)
    def do_search(self):
        # For each widget inside the results frame, lets destroy them
        for widget in self.frmLT_result.findChildren(QWidget):
            widget.setParent(None)
            widget.deleteLater()
        for widget in self.frmBO_result.findChildren(QWidget):
            widget.setParent(None)
            widget.deleteLater()
        for widget in self.frmSR_result.findChildren(QWidget):
            widget.setParent(None)
            widget.deleteLater()
        for widget in self.frmDCL_result.findChildren(QWidget):
            widget.setParent(None)
            widget.deleteLater()

        # Grab the filter text
        filter_text = self.txt_filter.text()

        # For every entry in the dataset...
        for entry in self.BBB_PS_list:
            # Check if they match our filter
            if filter_text.upper() not in entry.upper():
                continue

            # Create macros list
            dict_macro_BBB = {
                "PS_CON": entry,
                "PYTHON":
                "python" if platform.system() == "Windows" else "python3"
            }
            for i in range(1, len(self.BBB_PS_list[entry]) + 1):
                dict_macro_BBB["PS{}".format(i)] = self.BBB_PS_list[entry][i -
                                                                           1]
            # Create a PyDMEmbeddedDisplay for this entry
            disp = PyDMEmbeddedDisplay(parent=self)
            PyDMApplication.instance().close_widget_connections(disp)
            disp.macros = json.dumps(dict_macro_BBB)
            disp.filename = 'PS_Controller.ui'
            disp.setMinimumWidth(700)
            disp.setMinimumHeight(40)
            disp.setMaximumHeight(100)

            # Add the Embedded Display to the Results Layout
            if "DCL" in entry:
                self.resultsDCL_layout.addWidget(disp)
            elif "SI" in entry:
                self.resultsSR_layout.addWidget(disp)
            elif "BO" in entry:
                self.resultsBO_layout.addWidget(disp)
            elif ("TB" in entry) or ("TS" in entry):
                self.resultsLT_layout.addWidget(disp)

            PyDMApplication.instance().establish_widget_connections(disp)
Exemple #7
0
    def add_row(self, macros: dict[str, str]) -> None:
        """
        Adds a single row to the table.

        Each row will be created from the same UI file template.
        The macros used must have the same keys as all the previously
        added rows, or else the table will not work correctly.

        Parameters
        ----------
        macros : dict of str
            The macro substitutions for the UI file. These must be
            strings because we're effectively substituting them into
            the file's text.
        """
        widget = PyDMEmbeddedDisplay(parent=self)
        widget.macros = json.dumps(macros)
        widget.filename = self.ui_filename
        widget.loadWhenShown = False
        widget.disconnectWhenHidden = False
        self.add_context_menu_to_children(widget.embedded_widget)

        row_position = self.rowCount()
        self.insertRow(row_position)

        # Put the widget into the table
        self.setCellWidget(row_position, 0, widget)
        self._header_map['widget'] = 0
        self.setRowHeight(row_position, widget.height())

        # Put the index into the table
        item = ChannelTableWidgetItem(
            header='index',
            default=row_position,
        )
        self.setItem(row_position, 1, item)
        self._header_map['index'] = 1
        # Put the macros into the table
        index = 2
        for key, value in macros.items():
            item = ChannelTableWidgetItem(
                header=key,
                default=value,
            )
            self.setItem(row_position, index, item)
            self._header_map[key] = index
            index += 1
        # Set up the data columns and the channels
        for header in self._channel_headers:
            source = widget.findChild(QtCore.QObject, header)
            item = ChannelTableWidgetItem(
                header=header,
                channel=source.channel,
            )
            self.setItem(row_position, index, item)
            self._header_map[header] = index
            if item.pydm_channel is not None:
                self._channels.append(item.pydm_channel)
            index += 1
Exemple #8
0
    def __init__(self,
                 parent=None,
                 macros=None,
                 args=None,
                 average="Gamma Detectors"):
        super().__init__(parent=parent,
                         args=args,
                         macros=macros,
                         ui_filename=OVERVIEW_UI)
        self.setWindowTitle("Overview of Time Bases")
        self.alpha = [0, 0, 0, 0, 0]  # Initially doesn't show none graph
        self.average = average
        self.groups = ["{:0>2d}".format(sec) for sec in range(1, 21)]
        self.x = numpy.arange(len(self.groups))
        self.width = 0.185

        self.dict_pvs_tb = {}
        self.dict_macro_gamma = {}

        self.gamma_1 = [0] * 20
        self.gamma_2 = [0] * 20
        self.gamma_3 = [0] * 20
        self.gamma_4 = [0] * 20
        self.gamma_5 = [0] * 20

        self.fig, self.ax = plt.subplots(figsize=(12, 8))  #
        self.fig.canvas.set_window_title("Overview")  #
        self.fig.subplots_adjust(left=0.05, bottom=0.08, right=0.95,
                                 top=0.95)  # Adjustments of graphics
        plt.subplots_adjust(left=0.1)  #

        self.fig.text(0.03, 0.25, "Control of\n Graphic", ha="center")
        self.ani = FuncAnimation(fig=self.fig,
                                 func=self.animate,
                                 interval=10000)
        self.animate()
        self.checkButtons_setting()
        plt.show()

        if self.average == "Gamma Detectors":  # If user chose 'Counting - Overview'
            for PV in range(1, 21):
                for s_sec in range(2):
                    self.dict_pvs_tb["valueTB{}{}".format(
                        PV, counters[s_sec]
                    )] = "ca://SI-{:0>2d}{}:CO-Counter:TimeBase-SP".format(
                        PV, counters[s_sec])
                if PV != 20:
                    self.dict_pvs_tb["valueTB{}M1".format(
                        PV
                    )] = "ca://SI-{:0>2d}M1:CO-Counter:TimeBase-SP".format(PV +
                                                                           1)
                else:
                    self.dict_pvs_tb["valueTB{}M1".format(
                        PV)] = "ca://SI-01M1:CO-Counter:TimeBase-SP"

            for location in range(1, 21):
                for s_sec in range(len(Det_Location)):
                    self.dict_macro_gamma["DET{}".format(
                        s_sec)] = "SI-{:0>2d}{}:CO-Gamma".format(
                            location, Det_Location[s_sec])
                    if s_sec < 3:
                        self.dict_macro_gamma["TimeBase{}".format(
                            s_sec)] = "{}".format(
                                self.dict_pvs_tb["valueTB{}{}".format(
                                    location, counters[s_sec])])

                    a = PyDMChannel(
                        address="ca://SI-{:0>2d}{}:CO-Gamma:Count-Mon".format(
                            location, Det_Location[s_sec]),
                        value_slot=partial(self.plot,
                                           location=location,
                                           det=s_sec),
                    )  # Connect to Counting PVs
                    a.connect()

                self.disp = PyDMEmbeddedDisplay(
                    parent=self)  # Creates the window of Time Bases
                PyDMApplication.instance().close_widget_connections(self.disp)
                self.disp.macros = json.dumps(self.dict_macro_gamma)
                self.disp.filename = LAYOUT_OVERVIEW_UI
                self.disp.setMinimumWidth(300)
                self.disp.setMinimumHeight(140)
                self.verticalLayout.addWidget(self.disp)

                PyDMApplication.instance().establish_widget_connections(
                    self.disp)
        else:  # If user chose some Average
            for location in range(1, 21):
                for s_sec in range(len(Det_Location)):
                    a = PyDMChannel(
                        address="ca://SI-{:0>2d}{}:CO-Gamma:{}-Mon".format(
                            location, Det_Location[s_sec], self.average),
                        value_slot=partial(self.plot,
                                           location=location,
                                           det=s_sec),
                    )  # Connect to Averages PVs
                    a.connect()
Exemple #9
0
class Overview(Display):
    global counters, Det_Location

    def __init__(self,
                 parent=None,
                 macros=None,
                 args=None,
                 average="Gamma Detectors"):
        super().__init__(parent=parent,
                         args=args,
                         macros=macros,
                         ui_filename=OVERVIEW_UI)
        self.setWindowTitle("Overview of Time Bases")
        self.alpha = [0, 0, 0, 0, 0]  # Initially doesn't show none graph
        self.average = average
        self.groups = ["{:0>2d}".format(sec) for sec in range(1, 21)]
        self.x = numpy.arange(len(self.groups))
        self.width = 0.185

        self.dict_pvs_tb = {}
        self.dict_macro_gamma = {}

        self.gamma_1 = [0] * 20
        self.gamma_2 = [0] * 20
        self.gamma_3 = [0] * 20
        self.gamma_4 = [0] * 20
        self.gamma_5 = [0] * 20

        self.fig, self.ax = plt.subplots(figsize=(12, 8))  #
        self.fig.canvas.set_window_title("Overview")  #
        self.fig.subplots_adjust(left=0.05, bottom=0.08, right=0.95,
                                 top=0.95)  # Adjustments of graphics
        plt.subplots_adjust(left=0.1)  #

        self.fig.text(0.03, 0.25, "Control of\n Graphic", ha="center")
        self.ani = FuncAnimation(fig=self.fig,
                                 func=self.animate,
                                 interval=10000)
        self.animate()
        self.checkButtons_setting()
        plt.show()

        if self.average == "Gamma Detectors":  # If user chose 'Counting - Overview'
            for PV in range(1, 21):
                for s_sec in range(2):
                    self.dict_pvs_tb["valueTB{}{}".format(
                        PV, counters[s_sec]
                    )] = "ca://SI-{:0>2d}{}:CO-Counter:TimeBase-SP".format(
                        PV, counters[s_sec])
                if PV != 20:
                    self.dict_pvs_tb["valueTB{}M1".format(
                        PV
                    )] = "ca://SI-{:0>2d}M1:CO-Counter:TimeBase-SP".format(PV +
                                                                           1)
                else:
                    self.dict_pvs_tb["valueTB{}M1".format(
                        PV)] = "ca://SI-01M1:CO-Counter:TimeBase-SP"

            for location in range(1, 21):
                for s_sec in range(len(Det_Location)):
                    self.dict_macro_gamma["DET{}".format(
                        s_sec)] = "SI-{:0>2d}{}:CO-Gamma".format(
                            location, Det_Location[s_sec])
                    if s_sec < 3:
                        self.dict_macro_gamma["TimeBase{}".format(
                            s_sec)] = "{}".format(
                                self.dict_pvs_tb["valueTB{}{}".format(
                                    location, counters[s_sec])])

                    a = PyDMChannel(
                        address="ca://SI-{:0>2d}{}:CO-Gamma:Count-Mon".format(
                            location, Det_Location[s_sec]),
                        value_slot=partial(self.plot,
                                           location=location,
                                           det=s_sec),
                    )  # Connect to Counting PVs
                    a.connect()

                self.disp = PyDMEmbeddedDisplay(
                    parent=self)  # Creates the window of Time Bases
                PyDMApplication.instance().close_widget_connections(self.disp)
                self.disp.macros = json.dumps(self.dict_macro_gamma)
                self.disp.filename = LAYOUT_OVERVIEW_UI
                self.disp.setMinimumWidth(300)
                self.disp.setMinimumHeight(140)
                self.verticalLayout.addWidget(self.disp)

                PyDMApplication.instance().establish_widget_connections(
                    self.disp)
        else:  # If user chose some Average
            for location in range(1, 21):
                for s_sec in range(len(Det_Location)):
                    a = PyDMChannel(
                        address="ca://SI-{:0>2d}{}:CO-Gamma:{}-Mon".format(
                            location, Det_Location[s_sec], self.average),
                        value_slot=partial(self.plot,
                                           location=location,
                                           det=s_sec),
                    )  # Connect to Averages PVs
                    a.connect()

    def checkButtons_setting(self):  # Configures of check button
        visibility = [line.patches[0].get_alpha() == 1 for line in self.graph]
        self.rax = plt.axes(position=[0.005, 0.08, 0.05, 0.15])
        self.labels = ["     " + str(line.get_label()) for line in self.graph]
        self.check = CheckButtons(self.rax, self.labels, visibility)
        self.check.on_clicked(self.hide_show)

    def hide_show(self, label):  # Set graph Visibilities
        index = self.labels.index(label)
        if self.graph[index].patches[index].get_alpha() == 1:
            for i in range(20):
                self.graph[index].patches[i].set_alpha(0)
            self.alpha[index] = self.graph[index].patches[index].get_alpha()

        else:
            for i in range(20):
                self.graph[index].patches[i].set_alpha(1)
                self.alpha[index] = self.graph[index].patches[i].get_alpha()
        plt.draw()
        self.animate(i)

    def animate(self, *args):  # Function to update the graph
        self.ax.clear()
        self.rects1 = self.ax.bar(
            self.x - self.width * 2,
            self.gamma_1,
            self.width,
            label="M2",
            alpha=self.alpha[0],
        )
        self.rects2 = self.ax.bar(
            self.x - self.width,
            self.gamma_2,
            self.width,
            label="C1",
            alpha=self.alpha[1],
        )
        self.rects3 = self.ax.bar(self.x,
                                  self.gamma_3,
                                  self.width,
                                  label="C2",
                                  alpha=self.alpha[2])
        self.rects4 = self.ax.bar(
            self.x + self.width,
            self.gamma_4,
            self.width,
            label="C3",
            alpha=self.alpha[3],
        )
        self.rects5 = self.ax.bar(
            self.x + self.width * 2,
            self.gamma_5,
            self.width,
            label="C4",
            alpha=self.alpha[4],
        )

        self.ax.set_title("Overview of {}".format(self.average))
        self.ax.set_xlabel("Sectors of Storage Ring")
        self.ax.set_ylabel("Pulses per second")
        self.ax.set_xticklabels(self.groups)
        self.ax.set_xticks(self.x)
        self.ax.set_yscale("log")
        self.ax.legend()

        self.autolabel(self.rects1, self.alpha[0] == 1)
        self.autolabel(self.rects2, self.alpha[1] == 1)
        self.autolabel(self.rects3, self.alpha[2] == 1)
        self.autolabel(self.rects4, self.alpha[3] == 1)
        self.autolabel(self.rects5, self.alpha[4] == 1)

        self.graph = [
            self.rects1, self.rects2, self.rects3, self.rects4, self.rects5
        ]

    def autolabel(self, rects,
                  vis):  # Sets the visualization of counting above of bars
        for rect in rects:
            height = rect.get_height()
            self.ax.annotate(
                "{}".format(height),
                xy=(rect.get_x() + rect.get_width() / 2, height),
                xytext=(0, 3),  # 3 points vertical offset
                textcoords="offset points",
                ha="center",
                va="bottom",
                fontsize=8,
                rotation=90,
                visible=vis,
            )

    def plot(self,
             value="",
             location="",
             det=""):  # Updates the list of last counts
        eval("self.gamma_{}".format(det + 1)).insert(location - 1,
                                                     round(value, 5))
        del eval("self.gamma_{}".format(det + 1))[location]
Exemple #10
0
class FilterSortWidgetTable(QtWidgets.QTableWidget):
    """
    Displays repeated widgets that are sortable and filterable.

    This will allow you to sort or filter based on macros and based on the
    values in each pydm widget.
    """
    # Public instance variables
    template_widget: PyDMEmbeddedDisplay

    # Private instance variables
    _ui_filename: Optional[str]
    _macros_filename: Optional[str]
    _macros: list[dict[str, str]]
    _channel_headers: list[str]
    _macro_headers: list[str]
    _header_map: dict[str, int]
    _channels: list[PyDMChannel]
    _filters: dict[str, FilterInfo]
    _initial_sort_header: str
    _initial_sort_ascend: bool
    _hide_headers: list[str]
    _configurable: bool
    _watching_cells: bool

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._ui_filename = None
        self._macros_filename = None
        self.template_widget = PyDMEmbeddedDisplay(parent=self)
        self.template_widget.hide()
        self.template_widget.loadWhenShown = False
        self._macros = []
        self._channel_headers = []
        self._macro_headers = []
        self._header_map = {}
        self._channels = []
        self._filters = {}
        self._initial_sort_header = 'index'
        self._initial_sort_ascend = True
        self._hide_headers = []

        # Table settings
        self.setShowGrid(True)
        self.setSortingEnabled(False)
        self.setSelectionMode(self.NoSelection)
        self.horizontalHeader().setStretchLastSection(True)
        self.horizontalHeader().hide()

        self.configurable = False

        self._watching_cells = False

    def channels(self) -> list[PyDMChannel]:
        """
        Tell PyDM about our table channels so it knows to close them at exit.
        """
        return self._channels

    @QtCore.Property(str)
    def ui_filename(self) -> str:
        """
        Name of the ui file that is to be repeated to fill the table.

        This is currently required. When this is changed, we'll rebuild the
        table.
        """
        return self._ui_filename

    @ui_filename.setter
    def ui_filename(self, filename: str):
        self._ui_filename = filename
        self.reload_ui_file()
        self.reinit_table()

    def reload_ui_file(self) -> None:
        """
        Load the UI file and inspect it for PyDM channels.
        """
        try:
            self.template_widget.filename = self.ui_filename
        except Exception:
            logger.exception(
                "Reloading the UI file %s failed",
                self.ui_filename,
            )
            return
        # Let's find all the widgets with channels and save their names
        self._channel_headers = []
        for widget in self.template_widget.embedded_widget.children():
            try:
                ch = widget.channels()
            except Exception:
                # It is expected that some widgets do not have channels
                continue
            if ch:
                self._channel_headers.append(widget.objectName())

    @QtCore.Property(str)
    def macros_filename(self) -> str:
        """
        Json file defining PyDM macros. Optional.

        This follows the same format as used for the PyDM Template Repeater.
        If omitted, you should pass in macros using the set_macros method
        instead.
        """
        return self._macros_filename

    @macros_filename.setter
    def macros_filename(self, filename: str):
        self._macros_filename = filename
        self.reload_macros_file()

    def reload_macros_file(self) -> None:
        """
        Load the macros_filename and call set_macros.
        """
        if not self.macros_filename:
            return
        try:
            with open(self.macros_filename, 'r') as fd:
                macros = json.load(fd)
            self.set_macros(macros)
        except Exception:
            logger.exception('')
            return

    def set_macros(self, macros_list: list[dict[str, str]]) -> None:
        """
        Change the PyDM macros we use to load the table widgets.

        This causes the table to be rebuilt.

        Parameters
        ----------
        macros_list : list of dict
            A list where each element is a dictionary that defines the macros
            to pass in to one instance of the repeated widget. All dicts must
            have the same keys or this will not work properly.
        """
        self._macros = macros_list
        self._macro_headers = (list(self._macros[0].keys())
                               if self._macros else [])
        self.reinit_table()

    def reinit_table(self) -> None:
        """
        Rebuild the table based on the ui_filename and the newest macros.
        """
        if self._watching_cells:
            self.cellChanged.disconnect(self.handle_item_changed)
            self._watching_cells = False
        for channel in self._channels:
            channel.disconnect()
        self._channels = []
        self.clear()
        self.clearContents()
        self.setRowCount(0)
        self._header_map = {}
        if not self._macros and self._channel_headers:
            return
        # Column 1 displays widget, 2 is index, the rest hold values
        ncols = 2 + len(self._channel_headers) + len(self._macro_headers)
        self.setColumnCount(ncols)
        for col in range(1, ncols):
            self.hideColumn(col)
        for macros in self._macros:
            self.add_row(macros)

        self._watching_cells = True
        self.cellChanged.connect(self.handle_item_changed)
        self.update_all_filters()

    def add_row(self, macros: dict[str, str]) -> None:
        """
        Adds a single row to the table.

        Each row will be created from the same UI file template.
        The macros used must have the same keys as all the previously
        added rows, or else the table will not work correctly.

        Parameters
        ----------
        macros : dict of str
            The macro substitutions for the UI file. These must be
            strings because we're effectively substituting them into
            the file's text.
        """
        widget = PyDMEmbeddedDisplay(parent=self)
        widget.macros = json.dumps(macros)
        widget.filename = self.ui_filename
        widget.loadWhenShown = False
        widget.disconnectWhenHidden = False
        self.add_context_menu_to_children(widget.embedded_widget)

        row_position = self.rowCount()
        self.insertRow(row_position)

        # Put the widget into the table
        self.setCellWidget(row_position, 0, widget)
        self._header_map['widget'] = 0
        self.setRowHeight(row_position, widget.height())

        # Put the index into the table
        item = ChannelTableWidgetItem(
            header='index',
            default=row_position,
        )
        self.setItem(row_position, 1, item)
        self._header_map['index'] = 1
        # Put the macros into the table
        index = 2
        for key, value in macros.items():
            item = ChannelTableWidgetItem(
                header=key,
                default=value,
            )
            self.setItem(row_position, index, item)
            self._header_map[key] = index
            index += 1
        # Set up the data columns and the channels
        for header in self._channel_headers:
            source = widget.findChild(QtCore.QObject, header)
            item = ChannelTableWidgetItem(
                header=header,
                channel=source.channel,
            )
            self.setItem(row_position, index, item)
            self._header_map[header] = index
            if item.pydm_channel is not None:
                self._channels.append(item.pydm_channel)
            index += 1

    def add_context_menu_to_children(self, widget: QtWidgets.QWidget) -> None:
        """
        Distribute the context menu to child widgets.

        This makes it so you can right click to configure the table from
        within any of the contained widgets.
        """
        for widget in widget.children():
            widget.contextMenuEvent = self.contextMenuEvent

    def contextMenuEvent(self, _event) -> None:
        """
        On right click, create and open a settings menu.
        """
        menu = QtWidgets.QMenu(parent=self)
        configure_action = menu.addAction('Configure')
        configure_action.setCheckable(True)
        configure_action.setChecked(self.configurable)
        configure_action.toggled.connect(self.request_configurable)
        active_sort_action = menu.addAction('Active Re-sort')
        active_sort_action.setCheckable(True)
        active_sort_action.setChecked(self.isSortingEnabled())
        active_sort_action.toggled.connect(self.setSortingEnabled)
        sort_menu = menu.addMenu('Sorting')
        for header_name in self._header_map.keys():
            if header_name == 'widget':
                continue
            if header_name in self.hide_headers_in_menu:
                continue
            inner_menu = sort_menu.addMenu(header_name.lower())
            asc = inner_menu.addAction('Ascending')
            asc.triggered.connect(
                functools.partial(
                    self.menu_sort,
                    header=header_name,
                    ascending=True,
                ))
            dec = inner_menu.addAction('Descending')
            dec.triggered.connect(
                functools.partial(
                    self.menu_sort,
                    header=header_name,
                    ascending=False,
                ))
        filter_menu = menu.addMenu('Filters')
        for filter_name, filter_info in self._filters.items():
            inner_action = filter_menu.addAction(filter_name)
            inner_action.setCheckable(True)
            inner_action.setChecked(filter_info.active)
            inner_action.toggled.connect(
                functools.partial(
                    self.activate_filter,
                    filter_name=filter_name,
                ))
        menu.exec_(QtGui.QCursor.pos())

    def get_row_values(self, row: int) -> dict[str, Any]:
        """
        Get the current values for a specific numbered row of the table.

        Parameters
        ----------
        row : int
            The row index to inspect. 0 is the current top row.

        Returns
        -------
        values : dict
            A mapping from str to value for each named widget in the template
            that has a PyDM channel. There is one additional special str, which
            is the 'connected' str, which is True if all channels are
            connected.
        """
        values = {'connected': True}
        for col in range(1, self.columnCount()):
            item = self.item(row, col)
            values[item.header] = item.get_value()
            if not item.connected:
                values['connected'] = False
        return values

    def add_filter(self,
                   filter_name: str,
                   filter_func: Callable[[dict[str, Any]], bool],
                   active: bool = True) -> None:
        """
        Add a new visibility filter to the table.

        Filters are functions with the following signature:
        ``filt(values: dict[str, Any]) -> bool``
        Where values is the output from get_row_values,
        and the boolean return value is True if the row should be displayed.
        If we have multiple filters, we need all of them to be True to display
        the row.

        Parameters
        ----------
        filter_name : str
            A name assigned to the filter to help us keep track of it.
        filter_func : func
            A callable with the correct signature.
        active : bool, optional.
            True if we want the filter to start as active. An inactive filter
            does not act on the table until the user requests it from the
            right-click context menu. Defaults to True.
        """
        # Filters take in a dict of values from header to value
        # Return True to show, False to hide
        self._filters[filter_name] = FilterInfo(
            filter_func=filter_func,
            active=active,
            name=filter_name,
        )
        self.update_all_filters()

    def remove_filter(self, filter_name: str) -> None:
        """
        Remove a specific named visibility filter from the table.

        This is a filter that was previously added using add_filter.

        Parameters
        ----------
        filter_name : str
            A name assigned to the filter to help us keep track of it.
        """
        del self._filters[filter_name]
        self.update_all_filters()

    def clear_filters(self) -> None:
        """
        Remove all visbility filters from the table.
        """
        self._filters = {}
        self.update_all_filters()

    def update_all_filters(self) -> None:
        """
        Apply all filters to all rows of the table.
        """
        for row in range(self.rowCount()):
            self.update_filter(row)

    def update_filter(self, row: int) -> None:
        """
        Apply all filters to one row of the table.

        Parameters
        ----------
        row : int
            The row index to inspect. 0 is the current top row.
        """
        if self._filters:
            values = self.get_row_values(row)
            show_row = []
            for filt_info in self._filters.values():
                if filt_info.active:
                    try:
                        should_show = filt_info.filter_func(values)
                    except Exception:
                        logger.debug(
                            'Error in filter function %s',
                            filt_info.name,
                            exc_info=True,
                        )
                        should_show = True
                    show_row.append(should_show)
                else:
                    # If inactive, record it as unfiltered/shown
                    show_row.append(True)
            if all(show_row):
                self.showRow(row)
            else:
                self.hideRow(row)
        else:
            self.showRow(row)

    def activate_filter(self, filter_name: str, active: bool) -> None:
        """
        Activate or deactivate a filter by name.

        Parameters
        ----------
        active : bool
            True if we want to activate a filter, False if we want to
            deactivate it.
        filter_name : str
            The name associated with the filter, chosen when we add the filter
            to the table.
        """
        self._filters[filter_name].active = active
        self.update_all_filters()

    def handle_item_changed(self, row: int, col: int) -> None:
        """
        Slot that is run when any element in the table changes.

        Currently, this updates the filters for the row that changed.
        """
        self.update_filter(row)

    @QtCore.Property(str)
    def initial_sort_header(self) -> str:
        """
        Column to sort on after initializing.

        Use this to initialize the sort order rather than using the sort_table
        function.
        """
        return self._initial_sort_header

    @initial_sort_header.setter
    def initial_sort_header(self, header: str):
        self._initial_sort_header = header
        if not is_qt_designer():
            # Do a sort after a short timer
            # HACK: this is because it doesn't work if done immediately
            # This is due to some combination of qt designer properties being
            # applied in some random order post-__init__ combined with it
            # taking a short bit of time for the items to be ready to sort.
            # Some items will never connect and never be ready to sort,
            # so for now we'll do a best-effort one-second wait.
            timer = QtCore.QTimer(parent=self)
            timer.singleShot(1000, self.initial_sort)

    @QtCore.Property(bool)
    def initial_sort_ascending(self) -> bool:
        """
        Whether to do the initial sort in ascending or descending order.
        """
        return self._initial_sort_ascend

    @initial_sort_ascending.setter
    def initial_sort_ascending(self, ascending: bool):
        self._initial_sort_ascend = ascending

    def initial_sort(self) -> None:
        """
        Called if the user specifies an initial_sort_header.
        """
        self.sort_table(self.initial_sort_header, self.initial_sort_ascending)

    @QtCore.Property('QStringList')
    def hide_headers_in_menu(self) -> list[str]:
        """
        A list of headers that we don't want to see in the sort menu.
        """
        return self._hide_headers

    @hide_headers_in_menu.setter
    def hide_headers_in_menu(self, headers: list[str]):
        self._hide_headers = headers

    def sort_table(self, header: str, ascending: bool) -> None:
        """
        Rearrange the ordering of the table based on any of the value fields.

        Parameters
        ----------
        header : str
            The name of any of the value fields to use to sort on. Valid
            headers are 'index', which is the original sort order, strings that
            match the macro keys, and strings that match widget names in the
            template.
        ascending : bool
            If True, we'll sort in ascending order. If False, we'll sort in
            descending order.
        """
        self.reset_manual_sort()
        if ascending:
            order = QtCore.Qt.AscendingOrder
        else:
            order = QtCore.Qt.DescendingOrder
        col = self._header_map[header]
        self.sortByColumn(col, order)

    def menu_sort(self, checked: bool, header: str, ascending: bool):
        """
        sort_table wrapped to recieve the checked bool from a signal.

        Ignore the checked boolean.
        """
        self.sort_table(header, ascending)

    def reset_manual_sort(self) -> None:
        """
        Rearrange the table to undo all manual drag/drop sorting.
        """
        header = self.verticalHeader()
        for row in range(self.rowCount()):
            header.moveSection(header.visualIndex(row), row)

    @QtCore.Property(bool, designable=False)
    def configurable(self) -> bool:
        """
        Whether or not the table can be manipulated from the UI.

        If True, the table rows can be dragged/dropped/rearranged.
        If False, the table rows can no longer be selected.

        This begins as False if unset and can be changed in the context menu.
        """
        return self._configurable

    @configurable.setter
    def configurable(self, conf: bool):
        self._configurable = conf
        if conf:
            self.verticalHeader().setSectionsMovable(True)
            self.verticalHeader().show()
        else:
            self.verticalHeader().setSectionsMovable(False)
            self.verticalHeader().hide()

    @QtCore.Slot(bool)
    def request_configurable(self, conf: bool):
        """
        Designable slot for toggling config mode.
        """
        self.configurable = conf
    def setup_requests(self):
        """Populate the table from the config file and the item_info_list."""
        if not self.config:
            return
        reqs = self.config.get('preemptive_requests')
        if not reqs:
            return
        reqs_table = self.ui.reqs_table_widget
        # setup table
        ncols = len(item_info_list) + 2
        reqs_table.setColumnCount(ncols)
        # hide extra sort columns: these just hold values for easy sorting
        for col in range(1, ncols):
            reqs_table.hideColumn(col)

        if reqs_table is None:
            return
        count = 0
        for req in reqs:
            prefix = req.get('prefix')
            arbiter = req.get('arbiter_instance')
            pool_start = req.get('assertion_pool_start')
            pool_end = req.get('assertion_pool_end')

            pool_zfill = len(str(pool_end)) + 1

            template = 'templates/preemptive_requests_entry.ui'
            for pool_id in range(pool_start, pool_end + 1):
                pool = str(pool_id).zfill(pool_zfill)
                macros = dict(index=count,
                              P=prefix,
                              ARBITER=arbiter,
                              POOL=pool)
                widget = PyDMEmbeddedDisplay(parent=reqs_table)
                widget.prefixes = macros
                widget.macros = json.dumps(macros)
                widget.filename = template
                widget.loadWhenShown = False
                widget.disconnectWhenHidden = False

                # insert the widget you see into the table
                row_position = reqs_table.rowCount()
                reqs_table.insertRow(row_position)
                reqs_table.setCellWidget(row_position, 0, widget)

                # insert a cell to preserve the original sort order
                item = PMPSTableWidgetItem(
                    store_type=int,
                    data_type=int,
                    default=count,
                )
                item.setSizeHint(widget.size())
                reqs_table.setItem(row_position, 1, item)

                # insert invisible customized QTableWidgetItems for sorting
                for num, info in enumerate(item_info_list):
                    inner_widget = widget.findChild(
                        info.widget_class,
                        info.widget_name,
                    )
                    item = PMPSTableWidgetItem(
                        store_type=info.store_type,
                        data_type=info.data_type,
                        default=info.default,
                        channel=inner_widget.channel,
                    )
                    item.setSizeHint(widget.size())
                    reqs_table.setItem(row_position, num + 2, item)
                    self._channels.append(item.pydm_channel)

                count += 1
        reqs_table.resizeRowsToContents()
        self.row_count = count
        print(f'Added {count} preemptive requests')