Beispiel #1
0
 def setup_plot_spectrum_actions(self, context_menu, table):
     plot_spectrum_action = QAction(self.GRAPH_ICON,
                                    "Plot spectrum (values only)", self)
     plot_spectrum_action.triggered.connect(
         partial(self.presenter.action_plot_spectrum, table))
     plot_spectrum_with_errors_action = QAction(
         self.GRAPH_ICON, "Plot spectrum (values + errors)", self)
     plot_spectrum_with_errors_action.triggered.connect(
         partial(self.presenter.action_plot_spectrum_with_errors, table))
     overplot_spectrum_action = QAction(self.GRAPH_ICON,
                                        "Overplot spectrum (values only)",
                                        self)
     overplot_spectrum_action.triggered.connect(
         partial(self.presenter.action_overplot_spectrum, table))
     overplot_spectrum_with_errors_action = QAction(
         self.GRAPH_ICON, "Overplot spectrum (values + errors)", self)
     overplot_spectrum_with_errors_action.triggered.connect(
         partial(self.presenter.action_overplot_spectrum_with_errors,
                 table))
     overplot_spectrum_action.setEnabled(can_overplot())
     overplot_spectrum_with_errors_action.setEnabled(can_overplot())
     separator = QAction(self)
     separator.setSeparator(True)
     list(
         map(context_menu.addAction, [
             plot_spectrum_action, plot_spectrum_with_errors_action,
             separator, overplot_spectrum_action,
             overplot_spectrum_with_errors_action
         ]))
Beispiel #2
0
class Tool(object):
    def __init__(
        self,
        name,
        help_link="",
        icon=None,
        enabled=True,
        checkable=False,
        popup_menu=False,
    ):
        super(Tool, self).__init__()
        self.__icon = icon
        self.__name = name
        self.__parent = None
        self.__enabled = enabled
        self.__checkable = checkable
        self.__help_link = help_link
        self.__is_popup_menu = popup_menu

        self.__action = QAction(self.getIcon(), self.getName(), None)
        self.__action.setIconText(self.getName())
        self.__action.setEnabled(self.isEnabled())
        self.__action.setCheckable(checkable)
        self.__action.triggered.connect(self.trigger)

        HelpCenter.addHelpToAction(self.__action, self.getHelpLink())

    def getIcon(self):
        return self.__icon

    def getName(self):
        return self.__name

    def trigger(self):
        raise NotImplementedError()

    def setParent(self, parent):
        self.__parent = parent
        self.__action.setParent(parent)

    def parent(self):
        return self.__parent

    def isEnabled(self):
        return self.__enabled

    def getHelpLink(self):
        return self.__help_link

    def getAction(self):
        return self.__action

    def setVisible(self, visible):
        self.__action.setVisible(visible)

    def setEnabled(self, enabled):
        self.__action.setEnabled(enabled)

    def isPopupMenu(self):
        return self.__is_popup_menu
Beispiel #3
0
    def __init__(self, env):
        super().__init__()

        self._env = env
        self._extra_menu_actions = []
        self._extra_toolbar_actions = []

        self.update_title()

        main_menu = self.menuBar()
        self.main_menu = main_menu
        project_menu = main_menu.addMenu("&Project")

        toolbar = self.addToolBar("Top")
        self.toolbar = toolbar

        new_action = QAction("&New", self)
        new_action.setShortcut("Ctrl+N")
        new_action.setStatusTip("Create a new empty project")
        new_action.triggered.connect(lambda: env.new())
        project_menu.addAction(new_action)

        open_action = QAction("&Open...", self)
        open_action.setShortcut("Ctrl+O")
        open_action.setStatusTip("Open an existing project")
        open_action.triggered.connect(lambda: env.open())
        project_menu.addAction(open_action)

        save_action = QAction("&Save", self)
        self.save_action = save_action
        save_action.setShortcut("Ctrl+S")
        save_action.setIcon(QIcon.fromTheme("document-save"))
        #save_action.setIcon(self.style().standardIcon(
        #    self.style().SP_DialogSaveButton))
        save_action.setStatusTip("Save project")
        save_action.setEnabled(False)
        save_action.triggered.connect(lambda: env.save())
        project_menu.addAction(save_action)
        toolbar.addAction(save_action)

        save_as_action = QAction("Save &As...", self)
        save_as_action.setStatusTip("Save project under a new name")
        save_as_action.triggered.connect(lambda: env.save_as())
        project_menu.addAction(save_as_action)

        quit_action = QAction("&Quit", self)
        quit_action.setShortcut("Ctrl+Q")
        quit_action.setStatusTip("Quit Hildegard")
        quit_action.triggered.connect(self.handle_quit)
        project_menu.addAction(quit_action)

        self.tabs = QTabWidget()
        self.tabs.setTabsClosable(True)
        self.tabs.tabCloseRequested.connect(
            lambda index: env.close(self.tabs.widget(index).entity, quit=True))
        self.tabs.currentChanged.connect(self._handle_switch_to_tab)
        self.setCentralWidget(self.tabs)

        self.statusBar()
Beispiel #4
0
    def customMenuRequested(self, position):
        """Builds a custom menu for items items"""
        index = self.indexAt(position)  # type: QModelIndex
        menu = QMenu(self)

        if index.isValid():

            if index.data(EnsembleModel.data_type_role
                          ) == WorkspaceDataType.Ensemble:

                # Allow renaming the ensemble via the context menu
                rename_action = QAction("Rename Collection", menu)
                rename_action.triggered.connect(self._rename_action)
                menu.addAction(rename_action)

                # Allow toggling the active ensemble via the context menu
                # * there can only be at most 1 active ensemble
                # * there are only 0 active ensembles when data has not yet been loaded ???
                # * opening data updates the active ensemble to that data
                is_active = index.data(EnsembleModel.active_role)
                active_text = "Active"
                toggle_active_action = QAction(active_text, menu)
                toggle_active_action.setCheckable(True)
                if is_active is True:
                    toggle_active_action.setChecked(True)
                else:
                    toggle_active_action.setChecked(False)
                    toggle_active_action.setText(f"Not {active_text}")

                # Make sure to update the model with the active / deactivated ensemble
                toggle_active_action.toggled.connect(self._set_active_action)
                # Don't allow deactivating the active ensemble if there is only one loaded
                if self.model().rowCount() == 1:
                    toggle_active_action.setEnabled(False)
                menu.addAction(toggle_active_action)

                menu.addSeparator()

            remove_text = "Remove "
            data_type_role = index.data(EnsembleModel.data_type_role)
            if data_type_role == WorkspaceDataType.Ensemble:
                remove_text += "Ensemble"
            elif data_type_role == WorkspaceDataType.Catalog:
                remove_text += "Catalog"
            elif data_type_role == WorkspaceDataType.Intent:
                remove_text += "Item"
            remove_action = QAction(remove_text, menu)
            remove_action.triggered.connect(self._remove_action)
            menu.addAction(remove_action)

        else:
            create_ensemble_action = QAction("Create New Collection", menu)
            create_ensemble_action.triggered.connect(
                self._create_ensemble_action)
            menu.addAction(create_ensemble_action)

        # Display menu wherever the user right-clicked
        menu.popup(self.viewport().mapToGlobal(position))
Beispiel #5
0
    class TreeViewMenu(QMenu):
        def __init__(self, *args):
            QMenu.__init__(self, *args)
            # create new group action
            self.actionCreate_New_Group = QAction(self)
            self.actionCreate_New_Group.setEnabled(True)
            self.actionCreate_New_Group.setObjectName("actionCreate_New_Group")
            self.addAction(self.actionCreate_New_Group)

            self.actionDismiss_Group = QAction(self)
            self.actionDismiss_Group.setEnabled(True)
            self.actionDismiss_Group.setObjectName("actionDismiss_Group")
            self.addAction(self.actionDismiss_Group)

            self.actionAdd_To_Group = QAction(self)
            self.actionAdd_To_Group.setEnabled(True)
            self.actionAdd_To_Group.setObjectName("actionAdd_To_Group")
            self.addAction(self.actionAdd_To_Group)

            self.addSeparator()
            # item operations
            self.actionRemove_Station = QAction(self)
            self.actionRemove_Station.setEnabled(True)
            self.actionRemove_Station.setObjectName("actionRemove_Station")
            self.addAction(self.actionRemove_Station)

            self.addSeparator()
            # plot menu action
            self.actionPlot = QAction(self)
            self.actionPlot.setEnabled(False)
            self.actionPlot.setObjectName("actionPlot")
            self.addAction(self.actionPlot)

            self.retranslateUi()

        def retranslateUi(self):
            self.actionCreate_New_Group.setText(
                _translate("StationViewer", "Create New Group...", None))
            self.actionDismiss_Group.setText(
                _translate("StationViewer", "Dismiss Selected Group", None))
            self.actionAdd_To_Group.setText(
                _translate("StationViewer", "Add Selected to Group...", None))
            self.actionRemove_Station.setText(
                _translate("StationViewer", "Unload Selected Stations...",
                           None))
            self.actionPlot.setText(
                _translate("StationViewer", "Plot...", None))
Beispiel #6
0
def create_action(
    parent,
    title,
    triggered=None,
    toggled=None,
    shortcut=None,
    icon=None,
    tip=None,
    checkable=None,
    context=Qt.WindowShortcut,
    enabled=None,
):
    """
    Create a new QAction
    """
    action = QAction(title, parent)
    if triggered:
        if checkable:
            action.triggered.connect(triggered)
        else:
            action.triggered.connect(lambda checked=False: triggered())
    if checkable is not None:
        # Action may be checkable even if the toggled signal is not connected
        action.setCheckable(checkable)
    if toggled:
        action.toggled.connect(toggled)
        action.setCheckable(True)
    if icon is not None:
        assert isinstance(icon, QIcon)
        action.setIcon(icon)
    if shortcut is not None:
        action.setShortcut(shortcut)
    if tip is not None:
        action.setToolTip(tip)
        action.setStatusTip(tip)
    if enabled is not None:
        action.setEnabled(enabled)
    action.setShortcutContext(context)
    return action
Beispiel #7
0
    def _popup_menu(self, event):
        axes = self._find_calling_axes(event)  # find axes calling right click
        if axes is None: return

        pos = self.parent.mapFromGlobal(QtGui.QCursor().pos())

        popup_menu = QMenu(self.parent)
        xScaleMenu = popup_menu.addMenu('x-scale')
        yScaleMenu = popup_menu.addMenu('y-scale')

        for coord in ['x', 'y']:
            menu = eval(coord + 'ScaleMenu')
            for type in axes.scale[coord].keys():
                action = QAction(type, menu, checkable=True)
                if axes.scale[coord][type]:  # if it's checked
                    action.setEnabled(False)
                else:
                    action.setEnabled(True)
                menu.addAction(action)
                action.setChecked(axes.scale[coord][type])
                fcn = lambda event, coord=coord, type=type: self._set_scale(
                    coord, type, axes, True)
                action.triggered.connect(fcn)

        # Create menu for AutoScale options X Y All
        popup_menu.addSeparator()
        autoscale_options = ['AutoScale X', 'AutoScale Y', 'AutoScale All']
        for n, text in enumerate(autoscale_options):
            action = QAction(text, menu, checkable=True)
            if n < len(self.autoScale):
                action.setChecked(self.autoScale[n])
            else:
                action.setChecked(all(self.autoScale))
            popup_menu.addAction(action)
            action.toggled.connect(
                lambda event, n=n: self._setAutoScale(n, event, axes))

        popup_menu.exec_(self.parent.mapToGlobal(pos))
Beispiel #8
0
class BasePSControlWidget(QWidget):
    """Base widget class to control power supply."""

    HORIZONTAL = 0
    VERTICAL = 1

    def __init__(self, subsection=None, orientation=0, parent=None):
        """Class constructor.

        Parameters:
        psname_list - a list of power supplies, will be filtered based on
                      patterns defined in the subclass;
        orientation - how the different groups(defined in subclasses) will be
                      laid out.
        """
        super(BasePSControlWidget, self).__init__(parent)
        self._orientation = orientation
        self._subsection = subsection
        self._dev_list = PSSearch.get_psnames(self._getFilter(subsection))
        dev0 = PVName(self._dev_list[0])
        if dev0.sec == 'LI':
            if dev0.dev == 'Slnd':
                idcs = [int(PVName(dev).idx) for dev in self._dev_list]
                self._dev_list = [
                    x for _, x in sorted(zip(idcs, self._dev_list))
                ]
            if 'Q' in dev0.dev:
                all_props = dict()
                for dev in self._dev_list:
                    all_props.update(get_prop2label(dev))
                self.all_props = sort_propties(all_props)
            else:
                self.all_props = get_prop2label(self._dev_list[0])
        else:
            self.all_props = get_prop2label(self._dev_list[0])

        self.visible_props = self._getVisibleProps()
        if 'trim' in self.all_props:
            self.visible_props.append('trim')
        self.visible_props = sort_propties(self.visible_props)

        # Data used to filter the widgets
        self.ps_widgets_dict = dict()
        self.containers_dict = dict()
        self.filtered_widgets = set()  # Set with key of visible widgets

        # Setup the UI
        self.groups = self._getGroups()
        self._setup_ui()
        self._create_actions()
        self._enable_actions()
        if len(self.groups) in [1, 3]:
            self.setObjectName('cw')
            self.setStyleSheet('#cw{min-height: 40em;}')

    def _setup_ui(self):
        self.layout = QVBoxLayout()

        # Create filters
        self.search_le = QLineEdit(parent=self)
        self.search_le.setObjectName("search_lineedit")
        self.search_le.setPlaceholderText("Search for a power supply...")
        self.search_le.textEdited.connect(self._filter_pwrsupplies)
        self.filter_pb = QPushButton(qta.icon('mdi.view-column'), '', self)
        self.search_menu = QMenu(self.filter_pb)
        self.filter_pb.setMenu(self.search_menu)
        for prop, label in self.all_props.items():
            act = self.search_menu.addAction(label)
            act.setObjectName(prop)
            act.setCheckable(True)
            act.setChecked(prop in self.visible_props)
            act.toggled.connect(self._set_widgets_visibility)
        hlay_filter = QHBoxLayout()
        hlay_filter.addWidget(self.search_le)
        hlay_filter.addWidget(self.filter_pb)
        self.layout.addLayout(hlay_filter)

        self.count_label = QLabel(parent=self)
        self.count_label.setSizePolicy(QSzPlcy.Maximum, QSzPlcy.Maximum)
        self.layout.addWidget(self.count_label)

        self.pwrsupplies_layout = self._getSplitter()
        self.layout.addWidget(self.pwrsupplies_layout)
        if len(self.groups) == 3:
            splitt_v = QSplitter(Qt.Vertical)

        # Build power supply Layout
        # Create group boxes and pop. layout
        for idx, group in enumerate(self.groups):

            # Get power supplies that belong to group
            pwrsupplies = list()
            pattern = re.compile(group[1])
            for el in self._dev_list:
                if pattern.search(el):
                    pwrsupplies.append(el)

            # Create header
            header = SummaryHeader(pwrsupplies[0],
                                   visible_props=self.visible_props,
                                   parent=self)
            self.containers_dict['header ' + group[0]] = header
            self.filtered_widgets.add('header ' + group[0])

            # Loop power supply to create all the widgets of a groupbox
            group_widgets = list()
            for psname in pwrsupplies:
                ps_widget = SummaryWidget(name=psname,
                                          visible_props=self.visible_props,
                                          parent=self)
                pscontainer = PSContainer(ps_widget, self)
                group_widgets.append(pscontainer)
                self.containers_dict[psname] = pscontainer
                self.filtered_widgets.add(psname)
                self.ps_widgets_dict[psname] = ps_widget

            # Create group
            wid_type = 'groupbox' if group[0] else 'widget'
            group_wid = self._createGroupWidget(group[0],
                                                header,
                                                group_widgets,
                                                wid_type=wid_type)

            # Add group box to grid layout
            if len(self.groups) == 3:
                if idx in [0, 1]:
                    splitt_v.addWidget(group_wid)
                else:
                    self.pwrsupplies_layout.addWidget(splitt_v)
                    self.pwrsupplies_layout.addWidget(group_wid)
            else:
                self.pwrsupplies_layout.addWidget(group_wid)

        self.count_label.setText("Showing {} power supplies.".format(
            len(self.filtered_widgets) - len(self.groups)))
        self.setLayout(self.layout)

    def _createGroupWidget(self,
                           title,
                           header,
                           widget_group,
                           wid_type='groupbox'):
        scr_area_wid = QWidget(self)
        scr_area_wid.setObjectName('scr_ar_wid')
        scr_area_wid.setStyleSheet(
            '#scr_ar_wid {background-color: transparent;}')
        w_lay = QVBoxLayout(scr_area_wid)
        w_lay.setSpacing(0)
        w_lay.setContentsMargins(0, 0, 0, 0)
        for widget in widget_group:
            w_lay.addWidget(widget, alignment=Qt.AlignLeft)
        w_lay.addStretch()

        scr_area = QScrollArea(self)
        scr_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scr_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scr_area.setWidgetResizable(True)
        scr_area.setFrameShape(QFrame.NoFrame)
        scr_area.setWidget(scr_area_wid)

        wid = QGroupBox(title, self) if wid_type == 'groupbox' \
            else QWidget(self)
        gb_lay = QVBoxLayout(wid)
        gb_lay.addWidget(header, alignment=Qt.AlignLeft)
        gb_lay.addWidget(scr_area)
        return wid

    def _getSplitter(self):
        if self._orientation == self.HORIZONTAL:
            return QSplitter(Qt.Horizontal)
        else:
            return QSplitter(Qt.Vertical)

    def _getVisibleProps(self):
        """Default visible properties."""
        return [
            'detail', 'state', 'intlk', 'setpoint', 'monitor', 'strength_sp',
            'strength_mon'
        ]

    def _filter_pwrsupplies(self, text):
        """Filter power supply widgets based on text inserted at line edit."""
        try:
            pattern = re.compile(text, re.I)
        except Exception:  # Ignore malformed patterns?
            pattern = re.compile("malformed")

        # Clear filtered widgets and add the ones that match the new pattern
        self.filtered_widgets.clear()
        for name, container in self.containers_dict.items():
            cond = 'header' in name
            if not cond:
                cond |= bool(pattern.search(name))
                cond |= bool(pattern.search(container.bbbname))
                cond |= bool(pattern.search(container.udcname))
                for dc in container.dclinks:
                    cond |= bool(pattern.search(dc))
                for dc in container.dclinksbbbname:
                    cond |= bool(pattern.search(dc))
                for dc in container.dclinksudcname:
                    cond |= bool(pattern.search(dc))
            if cond:
                self.filtered_widgets.add(name)

        # Set widgets visibility and the number of widgets matched
        self._set_widgets_visibility()
        self.count_label.setText("Showing {} power supplies".format(
            len(self.filtered_widgets) - len(self.groups)))

        # Scroll to top
        for scroll_area in self.findChildren(QScrollArea):
            scroll_area.verticalScrollBar().setValue(0)

    def _set_widgets_visibility(self):
        """Set visibility of the widgets."""
        props = [
            act.objectName() for act in self.search_menu.actions()
            if act.isChecked()
        ]
        self.visible_props = sort_propties(props)
        self._enable_actions()
        for key, wid in self.containers_dict.items():
            wid.update_visible_props(props)
            if 'header' in key:
                for ob in wid.findChildren(QWidget):
                    name = ob.objectName()
                    ob.setVisible(name in props or 'Hidden' in name)
            else:
                vis = key in self.filtered_widgets
                wid.setVisible(vis)
                if not vis:
                    continue
                objs = wid.findChildren(SummaryWidget)
                objs.extend(wid.findChildren(SummaryHeader))
                for ob in objs:
                    chil = ob.findChildren(QWidget,
                                           options=Qt.FindDirectChildrenOnly)
                    for c in chil:
                        name = c.objectName()
                        if isinstance(ob, SummaryWidget) and name in props:
                            ob.fillWidget(name)
                        c.setVisible(name in props)

    # Actions methods
    def _create_actions(self):
        self.turn_on_act = QAction("Turn On", self)
        self.turn_on_act.triggered.connect(lambda: self._set_pwrstate(True))
        self.turn_on_act.setEnabled(False)
        self.turn_off_act = QAction("Turn Off", self)
        self.turn_off_act.triggered.connect(lambda: self._set_pwrstate(False))
        self.turn_off_act.setEnabled(False)
        self.ctrlloop_close_act = QAction("Close Control Loop", self)
        self.ctrlloop_close_act.triggered.connect(
            lambda: self._set_ctrlloop(True))
        self.ctrlloop_close_act.setEnabled(False)
        self.ctrlloop_open_act = QAction("Open Control Loop", self)
        self.ctrlloop_open_act.triggered.connect(
            lambda: self._set_ctrlloop(False))
        self.ctrlloop_open_act.setEnabled(False)
        self.set_slowref_act = QAction("Set OpMode to SlowRef", self)
        self.set_slowref_act.triggered.connect(self._set_slowref)
        self.set_slowref_act.setEnabled(False)
        self.set_current_sp_act = QAction("Set Current SP", self)
        self.set_current_sp_act.triggered.connect(self._set_current_sp)
        self.set_current_sp_act.setEnabled(False)
        self.reset_act = QAction("Reset Interlocks", self)
        self.reset_act.triggered.connect(self._reset_interlocks)
        self.reset_act.setEnabled(False)
        self.wfmupdate_on_act = QAction("Wfm Update Auto Enable", self)
        self.wfmupdate_on_act.triggered.connect(
            lambda: self._set_wfmupdate(True))
        self.wfmupdate_on_act.setEnabled(False)
        self.wfmupdate_off_act = QAction("Wfm Update Auto Disable", self)
        self.wfmupdate_off_act.triggered.connect(
            lambda: self._set_wfmupdate(False))
        self.wfmupdate_off_act.setEnabled(False)
        self.updparms_act = QAction("Update Parameters", self)
        self.updparms_act.triggered.connect(self._update_params)
        self.updparms_act.setEnabled(False)

    def _enable_actions(self):
        if 'state' in self.visible_props and \
                not self.turn_on_act.isEnabled():
            self.turn_on_act.setEnabled(True)
            self.turn_off_act.setEnabled(True)
        if 'ctrlloop' in self.visible_props and \
                not self.ctrlloop_close_act.isEnabled():
            self.ctrlloop_close_act.setEnabled(True)
            self.ctrlloop_open_act.setEnabled(True)
        if 'opmode' in self.visible_props and \
                not self.set_slowref_act.isEnabled():
            self.set_slowref_act.setEnabled(True)
        if 'setpoint' in self.visible_props and \
                not self.set_current_sp_act.isEnabled():
            self.set_current_sp_act.setEnabled(True)
        if 'reset' in self.visible_props and \
                not self.reset_act.isEnabled():
            self.reset_act.setEnabled(True)
        if 'wfmupdate' in self.visible_props and \
                not self.wfmupdate_on_act.isEnabled():
            self.wfmupdate_on_act.setEnabled(True)
            self.wfmupdate_off_act.setEnabled(True)
        if 'updparms' in self.visible_props and \
                not self.updparms_act.isEnabled():
            self.updparms_act.setEnabled(True)

    @Slot(bool)
    def _set_pwrstate(self, state):
        """Execute turn on/off actions."""
        for key, widget in self.ps_widgets_dict.items():
            if key in self.filtered_widgets:
                try:
                    if state:
                        widget.turn_on()
                    else:
                        widget.turn_off()
                except TypeError:
                    pass

    @Slot(bool)
    def _set_ctrlloop(self, state):
        """Execute close/open control loop actions."""
        for key, widget in self.ps_widgets_dict.items():
            if key in self.filtered_widgets:
                try:
                    if state:
                        widget.ctrlloop_close()
                    else:
                        widget.ctrlloop_open()
                except TypeError:
                    pass

    @Slot()
    def _set_slowref(self):
        """Set opmode to SlowRef for every visible widget."""
        for key, widget in self.ps_widgets_dict.items():
            if key in self.filtered_widgets:
                try:
                    widget.set_opmode_slowref()
                except TypeError:
                    pass

    @Slot()
    def _set_current_sp(self):
        """Set current setpoint for every visible widget."""
        dlg = QInputDialog(self)
        dlg.setLocale(QLocale(QLocale.English))
        new_value, ok = dlg.getDouble(self, "Insert current setpoint", "Value")
        if ok:
            for key, widget in self.ps_widgets_dict.items():
                if key in self.filtered_widgets:
                    sp = widget.setpoint.sp_lineedit
                    sp.setText(str(new_value))
                    try:
                        sp.send_value()
                    except TypeError:
                        pass

    @Slot()
    def _reset_interlocks(self):
        """Reset interlocks."""
        for key, widget in self.ps_widgets_dict.items():
            if key in self.filtered_widgets:
                try:
                    widget.reset()
                except TypeError:
                    pass

    @Slot(bool)
    def _set_wfmupdate(self, state):
        """Execute turn WfmUpdateAuto on/off actions."""
        for key, widget in self.ps_widgets_dict.items():
            if key in self.filtered_widgets:
                try:
                    if state:
                        widget.wfmupdate_on()
                    else:
                        widget.wfmupdate_off()
                except TypeError:
                    pass

    @Slot()
    def _update_params(self):
        """Update parameters."""
        for key, widget in self.ps_widgets_dict.items():
            if key in self.filtered_widgets:
                try:
                    widget.update_params()
                except TypeError:
                    pass

    # Overloaded method
    def contextMenuEvent(self, event):
        """Show a custom context menu."""
        point = event.pos()
        menu = QMenu("Actions", self)
        menu.addAction(self.turn_on_act)
        menu.addAction(self.turn_off_act)
        menu.addAction(self.ctrlloop_close_act)
        menu.addAction(self.ctrlloop_open_act)
        menu.addAction(self.set_current_sp_act)
        if not self._dev_list[0].dev in ('FCH', 'FCV'):
            menu.addAction(self.set_slowref_act)
            menu.addAction(self.reset_act)
            menu.addAction(self.wfmupdate_on_act)
            menu.addAction(self.wfmupdate_off_act)
            menu.addAction(self.updparms_act)
        menu.addSeparator()
        action = menu.addAction('Show Connections...')
        action.triggered.connect(self.show_connections)
        menu.popup(self.mapToGlobal(point))

    def show_connections(self, checked):
        """."""
        _ = checked
        conn = ConnectionInspector(self)
        conn.show()

    def get_summary_widgets(self):
        """Return Summary Widgets."""
        return self.findChildren(SummaryWidget)
Beispiel #9
0
class PSContainer(QWidget):
    """PSContainer."""
    def __init__(self, widget, parent=None):
        super().__init__(parent)
        self._widget = widget
        self.name = widget.devname
        self.bbbname = widget.bbbname
        self.udcname = widget.udcname

        self.dclinks = list()
        self.dclinks_type = ''
        self.dclink_widgets = list()
        self.dclinksbbbname = set()
        self.dclinksudcname = set()
        dclinks = PSSearch.conv_psname_2_dclink(self.name)
        if dclinks:
            self.dclinks = dclinks
            self.dclinks_type = PSSearch.conv_psname_2_psmodel(dclinks[0])
            if self.dclinks_type != 'REGATRON_DCLink':
                for dc in dclinks:
                    self.dclinksbbbname.add(PSSearch.conv_psname_2_bbbname(dc))
                    self.dclinksudcname.add(PSSearch.conv_psname_2_udc(dc))
            self.all_props = get_prop2label(PVName(dclinks[0]))

        self.visible_props = sort_propties(
            ['detail', 'state', 'intlk', 'setpoint', 'monitor'])
        self._setup_ui()
        self._create_actions()
        self._enable_actions()
        self.setStyleSheet("""
            #HideButton {
                min-width: 10px;
                max-width: 10px;
            }
            #DCLinkContainer {
                background-color: lightgrey;
            }
        """)

    def _setup_ui(self):
        """Setup widget UI."""
        self._layout = QGridLayout()
        self._layout.setSpacing(10)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(self._layout)

        self._dclink_container = QWidget(self)
        self._dclink_container.setObjectName('DCLinkContainer')
        self._dclink_container.setLayout(QVBoxLayout())
        self._dclink_is_filled = False
        if self.dclinks:
            self._hide = QPushButton(qta.icon('mdi.plus'), '', self)
        else:
            self._hide = QPushButton('', self)
            self._hide.setEnabled(False)
        self._hide.setObjectName('HideButton')
        self._hide.setSizePolicy(QSzPlcy.Maximum, QSzPlcy.Maximum)
        self._hide.setFlat(True)

        self._layout.addWidget(self._hide, 0, 0, Qt.AlignCenter)
        self._layout.addWidget(self._widget, 0, 1)
        self._layout.addWidget(self._dclink_container, 1, 1)

        # Configure
        self._dclink_container.setHidden(True)
        self._hide.clicked.connect(self._toggle_dclink)

    def _toggle_dclink(self):
        if self._dclink_container.isHidden():
            if not self._dclink_is_filled:
                self._fill_dclink_container()
                self._enable_actions()
            self._hide.setIcon(qta.icon('mdi.minus'))
            self._dclink_container.setHidden(False)
        else:
            self._hide.setIcon(qta.icon('mdi.plus'))
            self._dclink_container.setHidden(True)

    def _fill_dclink_container(self):
        self._dclink_is_filled = True
        self._dclink_container.layout().addWidget(
            SummaryHeader(self.dclinks[0], self.visible_props, self))
        for dclink_name in self.dclinks:
            w = SummaryWidget(dclink_name, self.visible_props, self)
            if self.dclinks_type == 'REGATRON_DCLink':
                connect_newprocess(w.detail_bt, [
                    'sirius-hla-as-ps-regatron-individual', '-dev', dclink_name
                ],
                                   parent=self,
                                   is_pydm=True)
            else:
                connect_window(w.detail_bt,
                               PSDetailWindow,
                               self,
                               psname=dclink_name)
            self._dclink_container.layout().addWidget(w)
            self.dclink_widgets.append(w)

    def update_visible_props(self, new_value):
        self.visible_props = sort_propties(new_value)
        self._enable_actions()

    # Action methods
    def _create_actions(self):
        self._turn_on_action = QAction('Turn DCLinks On', self)
        self._turn_on_action.triggered.connect(
            lambda: self._set_dclink_pwrstate(True))
        self._turn_on_action.setEnabled(False)
        self._turn_off_action = QAction('Turn DCLinks Off', self)
        self._turn_off_action.triggered.connect(
            lambda: self._set_dclink_pwrstate(False))
        self._turn_off_action.setEnabled(False)
        self._open_loop_action = QAction('Open DCLinks Control Loop', self)
        self._open_loop_action.triggered.connect(
            lambda: self._set_dclink_control_loop(False))
        self._open_loop_action.setEnabled(False)
        self._close_loop_action = QAction('Close DCLinks Control Loop', self)
        self._close_loop_action.triggered.connect(
            lambda: self._set_dclink_control_loop(True))
        self._close_loop_action.setEnabled(False)
        self._set_setpoint_action = QAction('Set DCLinks Voltage', self)
        self._set_setpoint_action.triggered.connect(self._set_setpoint)
        self._set_setpoint_action.setEnabled(False)
        self._reset_intlk_action = QAction('Reset DCLinks Interlocks', self)
        self._reset_intlk_action.triggered.connect(self._reset_intlk)
        self._reset_intlk_action.setEnabled(False)

    def _enable_actions(self):
        if 'state' in self.visible_props and \
                not self._turn_on_action.isEnabled():
            self._turn_on_action.setEnabled(True)
            self._turn_off_action.setEnabled(True)
        if 'ctrlloop' in self.visible_props and \
                not self._open_loop_action.isEnabled():
            self._open_loop_action.setEnabled(True)
            self._close_loop_action.setEnabled(True)
        if 'setpoint' in self.visible_props and \
                not self._set_setpoint_action.isEnabled():
            self._set_setpoint_action.setEnabled(True)
        if 'reset' in self.visible_props and \
                not self._reset_intlk_action.isEnabled():
            self._reset_intlk_action.setEnabled(True)

    def _set_dclink_pwrstate(self, value):
        for dclink in self.dclink_widgets:
            if value:
                dclink.turn_on()
            else:
                dclink.turn_off()

    def _set_dclink_control_loop(self, value):
        for dclink in self.dclink_widgets:
            btn = dclink.ctrlloop_bt
            if value:
                if btn._bit_val:
                    btn.send_value()
            else:
                if not btn._bit_val:
                    btn.send_value()

    def _set_setpoint(self):
        """Set current setpoint for every visible widget."""
        dlg = QInputDialog(self)
        dlg.setLocale(QLocale(QLocale.English))
        new_value, ok = dlg.getDouble(self, "New setpoint", "Value")
        if ok:
            for dclink in self.dclink_widgets:
                sp = dclink.setpoint.sp_lineedit
                sp.setText(str(new_value))
                try:
                    sp.send_value()
                except TypeError:
                    pass

    def _reset_intlk(self):
        for dclink in self.dclink_widgets:
            dclink.reset()

    # Overloaded method
    def contextMenuEvent(self, event):
        """Overload to create a custom context menu."""
        widget = self.childAt(event.pos())
        parent = widget.parent()
        grand_parent = parent.parent()
        if widget.objectName() == 'DCLinkContainer' or \
                parent.objectName() == 'DCLinkContainer' or \
                grand_parent.objectName() == 'DCLinkContainer':
            menu = QMenu(self)
            menu.addAction(self._turn_on_action)
            menu.addAction(self._turn_off_action)
            menu.addSeparator()
            menu.addAction(self._close_loop_action)
            menu.addAction(self._open_loop_action)
            menu.addSeparator()
            menu.addAction(self._set_setpoint_action)
            menu.addSeparator()
            menu.addAction(self._reset_intlk_action)
            menu.addSeparator()
            action = menu.addAction('Show Connections...')
            action.triggered.connect(self.show_connections)
            menu.popup(event.globalPos())
        else:
            super().contextMenuEvent(event)

    def show_connections(self, checked):
        """."""
        _ = checked
        c = ConnectionInspector(self)
        c.show()
Beispiel #10
0
class Viewer(ConfigurableMoveableTabContainer):
    """
    Contains multiple TabbedViewingAreas
    """
    tab_titles = Signal([tuple])
    num_viewing_areas = Integer(2, config=True)

    def __init__(self, *args, menuBar, **kwargs):
        super().__init__(*args, **kwargs)
        self._run_to_tabs = collections.defaultdict(list)
        self._title_to_tab = {}
        self._tabs_from_streaming = []
        self._overplot = OverPlotState.individual_tab
        self._overplot_target = None
        self._live_enabled = False

        self._live_run_router = RunRouter([self.route_live_stream])

        self._containers = [
            TabbedViewingArea(viewer=self, menuBar=menuBar)
            for _ in range(self.num_viewing_areas)
        ]
        layout = QVBoxLayout()
        splitter = QSplitter(Qt.Vertical)
        layout.addWidget(splitter)
        for container in self._containers:
            splitter.addWidget(container)
        self.setLayout(layout)

        overplot_group = QActionGroup(self)
        self.off = QAction('&Off', self)
        self.off.setStatusTip('Drop streaming data.')
        self.individual_tab = QAction('&New Tab', self)
        self.individual_tab.setStatusTip('Open a new viewer tab for each Run.')
        self.latest_live = QAction('&Latest Live Tab', self)
        self.latest_live.setStatusTip(
            'Attempt to overplot on the most recent live Run.')
        self.fixed = QAction('&Fixed Tab...', self)
        self.fixed.setStatusTip('Attempt to overplot on a specific tab.')
        self.fixed.setEnabled(False)
        overplot_group.addAction(self.off)
        overplot_group.addAction(self.individual_tab)
        overplot_group.addAction(self.latest_live)
        overplot_group.addAction(self.fixed)
        for action in overplot_group.actions():
            action.setCheckable(True)
        overplot_group.setExclusive(True)
        self.off.setChecked(True)

        overplot_menu = menuBar().addMenu('&Streaming')
        overplot_menu.addActions(overplot_group.actions())

        self.off.triggered.connect(self.disable_live)
        self.individual_tab.triggered.connect(
            partial(self.set_overplot_state, OverPlotState.individual_tab))
        self.latest_live.triggered.connect(
            partial(self.set_overplot_state, OverPlotState.latest_live))

        def set_overplot_target():
            item, ok = QInputDialog.getItem(self, "Select Tab", "Tab",
                                            tuple(self._title_to_tab), 0,
                                            False)
            if not ok:
                # Abort and fallback to Off. Would be better to fall back to
                # previous state (which could be latest_live) but it's not
                # clear how to know what that state was.
                self.off.setChecked(True)
                return
            self.set_overplot_state(OverPlotState.fixed)
            self._overplot_target = item

        self.fixed.triggered.connect(set_overplot_target)

    def enable_live(self):
        self._live_enabled = True

    def disable_live(self):
        self._live_enabled = False

    def consumer(self, item):
        """Slot that receives (name, doc) and unpacks it into RunRouter."""
        self._live_run_router(*item)

    def route_live_stream(self, name, start_doc):
        """Create or choose a Viewer to receive this Run."""
        if not self._live_enabled:
            log.debug("Streaming Run ignored because Streaming is disabled.")
            return [], []
        self.fixed.setEnabled(True)
        target_area = self._containers[0]
        uid = start_doc['uid']
        if self._overplot == OverPlotState.individual_tab:
            viewer = RunViewer()
            tab_title = uid[:8]
            index = target_area.addTab(viewer, tab_title)
            self._title_to_tab[tab_title] = viewer
            self._tabs_from_streaming.append(viewer)
            target_area.setCurrentIndex(index)
            self.tab_titles.emit(tuple(self._title_to_tab))
        elif self._overplot == OverPlotState.fixed:
            viewer = self._title_to_tab[self._overplot_target]
        elif self._overplot == OverPlotState.latest_live:
            if self._tabs_from_streaming:
                viewer = self._tabs_from_streaming[-1]
            else:
                viewer = RunViewer()
                tab_title = uid[:8]
                index = target_area.addTab(viewer, tab_title)
                self._title_to_tab[tab_title] = viewer
                self._tabs_from_streaming.append(viewer)
                target_area.setCurrentIndex(index)
                self.tab_titles.emit(tuple(self._title_to_tab))
        self._run_to_tabs[uid].append(viewer)
        viewer.run_router('start', start_doc)

        return [viewer.run_router], []

    def show_entries(self, target, entries):
        self.fixed.setEnabled(True)
        target_area = self._containers[0]
        if not target:
            # Add new Viewer tab.
            viewer = RunViewer()
            if len(entries) == 1:
                entry, = entries
                uid = entry.describe()['metadata']['start']['uid']
                tab_title = uid[:8]
            else:
                tab_title = self.get_title()
            index = target_area.addTab(viewer, tab_title)
            self._title_to_tab[tab_title] = viewer
            target_area.setCurrentIndex(index)
            self.tab_titles.emit(tuple(self._title_to_tab))
        else:
            viewer = self._title_to_tab[target]
        for entry in entries:
            viewer.load_entry(entry)
            uid = entry.describe()['metadata']['start']['uid']
            self._run_to_tabs[uid].append(viewer)
        # TODO Make last entry in the list the current widget.

    def get_title(self):
        for i in itertools.count(1):
            title = f'Group {i}'
            if title in self._title_to_tab:
                continue
            return title

    def set_overplot_state(self, state):
        self.enable_live()
        log.debug('Overplot state is %s', state)
        self._overplot = state

    def close_run_viewer(self, widget):
        try:
            self._tabs_from_streaming.remove(widget)
        except ValueError:
            pass
        for uid in widget.uids:
            self._run_to_tabs[uid].remove(widget)
            for title, tab in list(self._title_to_tab.items()):
                if tab == widget:
                    del self._title_to_tab[title]
                    self.tab_titles.emit(tuple(self._title_to_tab))
                    if title == self._overplot_target:
                        self.set_overplot_state(OverPlotState.off)
        if not self._title_to_tab:
            self.fixed.setEnabled(False)
Beispiel #11
0
class ObjectExplorer(QTreeView):
    nodevalue_changed = pyqtSignal(AbstractJsonItem)
    nodeproperty_changed = pyqtSignal(AbstractJsonItem)

    def __init__(self, rootnode: JsonNode, parent):
        super().__init__(parent)
        self.mainwindow = parent
        self.setModel(JsonDataModel(rootnode, self.mainwindow))
        self.model().dataChanged.connect(self.data_changed)
        self.setItemDelegate(MyItemDelegate())
        self.setDragDropMode(QTreeView.DragDrop)
        self.setDragEnabled(True)
        self.setAcceptDrops(True)
        self.setDropIndicatorShown(True)
        self.doubleClicked.connect(self.double_clicked)

        # context menu
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_contextmenu)

        # actions
        # properties action
        self.propertiesAction = QAction(self.tr("properties"), self)
        self.propertiesAction.setIcon(QIcon(pixmap("document_properties.png")))
        self.propertiesAction.triggered.connect(self.show_properties)

        # edit action
        self.editAction = QAction(self.tr("edit value"), self)
        self.editAction.setShortcut("F2")
        self.editAction.setIcon(QIcon(pixmap("edit.png")))
        self.editAction.triggered.connect(self.edit_value)

        # edit key action
        self.editkeyAction = QAction(self.tr("edit key"), self)
        self.editkeyAction.setIcon(QIcon(pixmap("kgpg_key1_kgpg.png")))
        self.editkeyAction.triggered.connect(self.edit_key)

        # insert item action
        self.insertitemAction = QAction(self.tr("insert item"), self)
        self.insertitemAction.setIcon(QIcon(pixmap("list_add.png")))
        self.insertitemAction.triggered.connect(self.insert_item)

        # insert node action
        self.insertnodeAction = QAction(self.tr("insert node"), self)
        self.insertnodeAction.setIcon(QIcon(pixmap("list_add.png")))
        self.insertnodeAction.triggered.connect(self.insert_node)

        # remove item action
        self.removeitemAction = QAction(self.tr("remove"), self)
        self.removeitemAction.setIcon(QIcon(pixmap("list_remove")))
        self.removeitemAction.triggered.connect(self.remove_item)

    def data_changed(self, topleft, bottomright, *args):
        node = topleft.internalPointer()
        if node is not None and isinstance(node, JsonItem):
            self.nodevalue_changed.emit(node)

    def double_clicked(self, *args, **kwargs):
        index = self.currentIndex()

        if not index.isValid():
            return

        column = self.model().columns[index.column()]
        if column.name == "value":
            self.edit_value()
        else:
            self.show_properties()

    def edit_key(self):
        index = self.currentIndex()
        if index.isValid():
            node = index.internalPointer()
            key, b = QInputDialog.getText(
                self, "Edit Json item", "Insert new key for item:",
                text=node.key
            )

            if not b:
                return

            node.key = key

            try:  # PyQt5
                self.model().dataChanged.emit(index, index, [Qt.DisplayRole])
            except TypeError:  # PyQt4, PySide
                self.model().dataChanged.emit(index, index)

    def edit_value(self):
        index = self.currentIndex()
        if not index.isValid():
            return

        i = self.model().index(index.row(), 2, index.parent())
        self.edit(i)

    def insert_item(self):
        index = self.currentIndex()

        if index.isValid():
            node = index.internalPointer()
        else:
            node = self.model().root

        key, b = QInputDialog.getText(
            self, "Insert Json item", "Insert key for new item:")

        if not b:
            return

        item = JsonItem(key)
        node.add(item)
        row = node.index(item)
        self.model().beginInsertRows(index, row, row)
        self.model().endInsertRows()

    def insert_node(self):
        index = self.currentIndex()
        parentnode = index.internalPointer() or self.model().root

        key, b = QInputDialog.getText(
            self, "Insert Json node", "Insert key for new node:")
        if not b:
            return
        node = JsonNode(key)
        parentnode.add(node)
        row = parentnode.index(node)
        self.model().beginInsertRows(index, row, row)
        self.model().endInsertRows()

    def mousePressEvent(self, event):
        index = self.indexAt(event.pos())
        if not index.isValid():
            self.setCurrentIndex(QModelIndex())
        super().mousePressEvent(event)

    def refresh(self):
        self.model().refresh()

    def remove_item(self):
        index = self.currentIndex()
        self.model().beginRemoveRows(index.parent(), index.row(), index.row())

        if index.isValid():
            node = index.internalPointer()
            if node.parent is not None:
                node.parent.remove(node.key)

        self.model().refresh()
        self.model().endRemoveRows()

    def show_contextmenu(self, pos: QPoint):
        menu = QMenu(self)
        index = self.currentIndex()
        node = index.internalPointer()

        # insert item and node
        menu.addAction(self.insertitemAction)
        menu.addAction(self.insertnodeAction)

        # edit key
        if isinstance(node, (JsonNode, JsonItem)):
            menu.addSeparator()
            menu.addAction(self.editkeyAction)
            if isinstance(node, JsonItem):
                menu.addAction(self.editAction)
                self.editAction.setEnabled(not node.readonly)

        # remove
        if isinstance(node, (JsonNode, JsonItem)):
            menu.addSeparator()
            menu.addAction(self.removeitemAction)

        # properties
        if isinstance(node, JsonItem):
            menu.addSeparator()
            menu.addAction(self.propertiesAction)
            menu.setDefaultAction(self.propertiesAction)

        menu.popup(self.viewport().mapToGlobal(pos), self.editAction)

    def show_properties(self):
        index = self.currentIndex()
        node = index.internalPointer()
        if not (index.isValid() and isinstance(node, JsonItem)):
            return

        dlg = ItemPropertyDialog(node, self.parent())
        if dlg.exec_() == QDialog.Accepted:
            self.nodeproperty_changed.emit(node)
Beispiel #12
0
class TyphosSidebarItem(ptypes.ParameterItem):
    """
    Class to display a Device or Tool in the sidebar
    """
    def __init__(self, param, depth):
        super().__init__(param, depth)
        # Configure a QToolbar
        self.toolbar = QToolBar()
        self.toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.toolbar.setIconSize(QSize(15, 15))
        # Setup the action to open the widget
        self.open_action = QAction(qta.icon('fa.square', color='green'),
                                   'Open', self.toolbar)
        self.open_action.triggered.connect(self.open_requested)
        # Setup the action to embed the widget
        self.embed_action = QAction(qta.icon('fa.th-large', color='yellow'),
                                    'Embed', self.toolbar)
        self.embed_action.triggered.connect(self.embed_requested)
        # Setup the action to hide the widget
        self.hide_action = QAction(qta.icon('fa.times-circle', color='red'),
                                   'Close', self.toolbar)
        self.hide_action.triggered.connect(self.hide_requested)
        self.hide_action.setEnabled(False)
        # Add actions to toolbars
        self.toolbar.addAction(self.open_action)
        self.toolbar.addAction(self.hide_action)
        if self.param.embeddable:
            self.toolbar.insertAction(self.hide_action, self.embed_action)

    def open_requested(self, triggered):
        """Request to open display for sidebar item"""
        self.param.sigOpen.emit(self)
        self._mark_shown()

    def embed_requested(self, triggered):
        """Request to open embedded display for sidebar item"""
        self.param.sigEmbed.emit(self)
        self._mark_shown()

    def hide_requested(self, triggered):
        """Request to hide display for sidebar item"""
        self.param.sigHide.emit(self)
        self._mark_hidden()

    def _mark_shown(self):
        self.open_action.setEnabled(False)
        self.embed_action.setEnabled(False)
        self.hide_action.setEnabled(True)

    def _mark_hidden(self):
        self.open_action.setEnabled(True)
        self.embed_action.setEnabled(True)
        self.hide_action.setEnabled(False)

    def treeWidgetChanged(self):
        """Update the widget when add to a QTreeWidget"""
        super().treeWidgetChanged()
        tree = self.treeWidget()
        if tree is None:
            return
        tree.setItemWidget(self, 1, self.toolbar)
Beispiel #13
0
class PyDMAlarmTree(QTreeView, PyDMWritableWidget):
    def __init__(self,
                 parent,
                 init_channel=None,
                 config_name=None,
                 edit_mode=False):
        super(PyDMAlarmTree, self).__init__()

        QTreeView.__init__(self, parent)
        PyDMWritableWidget.__init__(self)

        self.setup_ui()

        self._nodes = []

        self.config_name = config_name

        self.tree_model = AlarmTreeModel(self)
        self.setModel(self.tree_model)
        self.edit_mode = edit_mode

        self.setContextMenuPolicy(Qt.CustomContextMenu)

        if not edit_mode:
            self.customContextMenuRequested.connect(self._open_menu)

        self.expandAll()

    def setup_ui(self):
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)

        self.setDragDropOverwriteMode(False)
        self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        #  self.setHeaderHidden(True)
        self.setColumnWidth(0, 160)
        self.setColumnWidth(1, 160)
        self.setColumnWidth(2, 160)

    def get_configuration_name(self):
        return self.config_name

    def set_configuration_name(self, config_name):
        self.config_name = config_name

    configuration_name = Property(str,
                                  get_configuration_name,
                                  set_configuration_name,
                                  designable=False)

    def _open_menu(self, point):
        menu = QMenu()
        index = self.indexAt(point)
        item = self.model().getItem(index)
        self.value_action = QAction(item.status, self)
        self.value_action.setEnabled(False)

        menu.addAction(self.value_action)

        self.acknowledge_action = QAction("Acknowledge", self)
        self.acknowledge_action.triggered.connect(
            partial(self._acknowledge_at_index, index))
        menu.addAction(self.acknowledge_action)

        self.remove_acknowledge_action = QAction("Remove Acknowledge", self)
        self.remove_acknowledge_action.triggered.connect(
            partial(self._remove_acknowledge_at_index, index))
        self.remove_acknowledge_action.setEnabled(False)

        menu.addAction(self.remove_acknowledge_action)

        menu.exec_(self.viewport().mapToGlobal(point))

    def _acknowledge_at_index(self, index):
        item = self.tree_model.getItem(index)
        item.acknowledge()

    def _remove_acknowledge_at_index(self, index):
        item = self.tree_model.getItem(index)
        item.unacknowledge()

    def mousePressEvent(self, event):
        self.clearSelection()
        self.selectionModel().reset()
        QTreeView.mousePressEvent(self, event)
Beispiel #14
0
class StrainStressViewer(QMainWindow):
    def __init__(self, model, ctrl, parent=None):
        self._model = model
        self._model.propertyUpdated.connect(self.updatePropertyFromModel)
        self._model.failureMsg.connect(self.show_failure_msg)
        self._ctrl = ctrl

        super().__init__(parent)

        self.setWindowTitle("PyRS Strain-Stress Viewer")

        mainMenu = self.menuBar()
        fileMenu = mainMenu.addMenu('File')
        self.saveAction = QAction('&Save state', self)
        self.saveAction.setShortcut('Ctrl+S')
        self.saveAction.setStatusTip('Save application state')
        self.saveAction.triggered.connect(self.save)
        self.saveAction.setEnabled(False)
        fileMenu.addAction(self.saveAction)
        self.loadAction = QAction('&Load state', self)
        self.loadAction.setStatusTip('Load application state')
        self.loadAction.triggered.connect(self.load)
        fileMenu.addAction(self.loadAction)
        fileMenu.addSeparator()
        exitAction = QAction('&Exit', self)
        exitAction.setShortcut('Ctrl+Q')
        exitAction.setStatusTip('Exit application')
        exitAction.triggered.connect(self.close)
        fileMenu.addAction(exitAction)

        self.splitter = QSplitter()
        self.splitter.setHandleWidth(10)
        self.setCentralWidget(self.splitter)

        left = QWidget()
        left_layout = QVBoxLayout()

        self.stressCase = StressCase(self)
        self.stressCase.dimChanged.connect(self.dimChanged)
        left_layout.addWidget(self.stressCase)

        self.fileLoading = FileLoading(self)
        left_layout.addWidget(self.fileLoading)

        self.peak_selection = PeakSelection(self)
        self.peak_selection.peak_select.currentTextChanged.connect(
            self.controller.peakSelected)
        left_layout.addWidget(self.peak_selection)

        self.d0 = D0(self)
        left_layout.addWidget(self.d0)

        self.mechanicalConstants = MechanicalConstants(self)
        self.mechanicalConstants.youngModulus.editingFinished.connect(
            self.update_plot)
        self.mechanicalConstants.poissonsRatio.editingFinished.connect(
            self.update_plot)
        left_layout.addWidget(self.mechanicalConstants)

        self.csvExport = CSVExport(self)
        left_layout.addWidget(self.csvExport)

        left_layout.addStretch(0)

        left.setLayout(left_layout)

        self.splitter.addWidget(left)

        right = QWidget()
        right_layout = QVBoxLayout()

        self.plot_select = PlotSelect(self)
        self.plot_select.measure_dir.currentTextChanged.connect(
            self.update_plot)
        self.plot_select.plot_param.currentTextChanged.connect(
            self.update_plot)
        right_layout.addWidget(self.plot_select)

        self.viz_tab = VizTabs(self)
        right_layout.addWidget(self.viz_tab)

        right.setLayout(right_layout)

        self.splitter.addWidget(right)
        self.splitter.setStretchFactor(0, 1)
        self.splitter.setStretchFactor(1, 5)

    @property
    def controller(self):
        return self._ctrl

    @property
    def model(self):
        return self._model

    def dimChanged(self, bool2d):
        self.fileLoading.file_load_e33.setDisabled(bool2d)
        self.update_plot()

    def measure_dir_changed(self):
        self.update_plot()

    def update_plot(self):
        if self.plot_select.get_plot_param() == 'stress' or (
                self.plot_select.get_plot_param() == 'strain'
                and self.plot_select.get_direction() == "33"
                and self.stressCase.get_stress_case() == "In-plane stress"):
            validated = self.controller.validate_stress_selection(
                self.stressCase.get_stress_case(),
                self.mechanicalConstants.youngModulus.text(),
                self.mechanicalConstants.poissonsRatio.text())
        else:
            validated = self.controller.validate_selection(
                self.plot_select.get_direction(),
                self.stressCase.get_stress_case() != 'diagonal')

        if validated is None:
            if self.plot_select.get_plot_param() == 'stress' or (
                    self.plot_select.get_plot_param() == 'strain'
                    and self.plot_select.get_direction() == "33" and
                    self.stressCase.get_stress_case() == "In-plane stress"):
                self.calculate_stress()

            self.viz_tab.set_ws(
                self.model.get_field(
                    direction=self.plot_select.get_direction(),
                    plot_param=self.plot_select.get_plot_param(),
                    stress_case=self.stressCase.get_stress_case()))
        else:
            self.viz_tab.set_ws(None)
            self.viz_tab.set_message(validated)

        if self.controller.validate_stress_selection(
                self.stressCase.get_stress_case(),
                self.mechanicalConstants.youngModulus.text(),
                self.mechanicalConstants.poissonsRatio.text()) is None:
            self.calculate_stress()
            self.enable_stress_output(True)
        else:
            self.enable_stress_output(False)

    def enable_stress_output(self, enable):
        self.csvExport.setEnabled(enable)
        self.d0.setEnabled(enable)
        self.saveAction.setEnabled(enable)

    def updatePropertyFromModel(self, name):
        getattr(self, name)(getattr(self.model, name))

    def peakTags(self, peak_tags):
        self.peak_selection.peak_select.currentTextChanged.disconnect()
        self.peak_selection.clear_peak_tags()
        self.peak_selection.peak_select.currentTextChanged.connect(
            self.controller.peakSelected)
        self.peak_selection.set_peak_tags(peak_tags)

    def selectedPeak(self, peak):
        self.update_plot()

    def modelUpdated(self, modelUpdated):
        stressCase, selectedPeak, youngs_modulus, poisson_ratio = modelUpdated

        self.mechanicalConstants.set_values(youngs_modulus, poisson_ratio)

        self.peak_selection.peak_select.currentTextChanged.disconnect()
        self.peak_selection.set_selected_peak(selectedPeak)
        self.peak_selection.peak_select.currentTextChanged.connect(
            self.controller.peakSelected)

        self.stressCase.dimChanged.disconnect()
        self.stressCase.set_stress_case(stressCase)
        self.stressCase.dimChanged.connect(self.dimChanged)
        self.fileLoading.file_load_e33.setDisabled(
            stressCase.lower() != "diagonal")

        for d in ("11", "22", "33"):
            self.fileLoading.set_text_values(
                d, ", ".join(
                    os.path.basename(filename)
                    for filename in self.model.get_filenames_for_direction(d)))
        self.update_d0_from_model()
        self.update_plot()

    def show_failure_msg(self, msg, info, details):
        self.viz_tab.set_message(msg)
        msgBox = QMessageBox()
        msgBox.setIcon(QMessageBox.Critical)
        msgBox.setText(msg)
        msgBox.setInformativeText(info)
        msgBox.setDetailedText(details)
        msgBox.exec()

    def calculate_stress(self):
        self.controller.calculate_stress(
            self.stressCase.get_stress_case(),
            self.mechanicalConstants.youngModulus.text(),
            self.mechanicalConstants.poissonsRatio.text(), self.d0.get_d0())
        self.update_d0_from_model()

    def update_d0_from_model(self):
        d0 = self.model.d0
        if d0 is None:
            self.d0.set_d0(None, None)
            self.d0.set_d0_field(None, None, None, None, None)
        else:
            self.d0.set_d0(d0.values[0], d0.errors[0])
            self.d0.set_d0_field(d0.x, d0.y, d0.z, d0.values, d0.errors)

    def save(self):
        filename, _ = QFileDialog.getSaveFileName(
            self, "Save Stress state", "", "JSON (*.json);;All Files (*)")
        if filename:
            self.controller.save(filename)

    def load(self):
        filename, _ = QFileDialog.getOpenFileName(
            self, "Load Stress state", "", "JSON (*.json);;All Files (*)")
        if filename:
            self.controller.load(filename)
Beispiel #15
0
class PayloadWindow(FramelessWindow):
    get_build_options = Signal()
    start_build = Signal(str, str, list)
    stop_build = Signal()

    def __init__(self, parent):
        super(PayloadWindow, self).__init__(parent)
        self.logger = logging.getLogger(self.__class__.__name__)
        self.setContentsMargins(11, 11, 11, 11)
        self.progress_windows = []

        self.content_widget = QWidget(self)
        self.addContentWidget(self.content_widget)

        self.widget_layout = QFormLayout(self.content_widget)
        self.content_widget.setLayout(self.widget_layout)

        self.spinner = WaitingSpinner(self,
                                      modality=Qt.WindowModal,
                                      roundness=70.0,
                                      fade=70.0,
                                      radius=15.0,
                                      lines=6,
                                      line_length=25.0,
                                      line_width=4.0,
                                      speed=1.0)

        self.build_name_label = QLabel("Name", self.content_widget)
        self.build_name_edit = QLineEdit(self.content_widget)
        self.widget_layout.addRow(self.build_name_label, self.build_name_edit)

        self.icon_label = QLabel("Icon", self.content_widget)
        self.icon_combobox = QComboBox(self.content_widget)
        self.widget_layout.addRow(self.icon_label, self.icon_combobox)

        self.generators_label = QLabel("Generators", self.content_widget)
        self.generators_list = QListWidget(self.content_widget)
        self.widget_layout.addRow(self.generators_label, self.generators_list)

        self.build_button = QPushButton("Build", self.content_widget)
        self.build_button.clicked.connect(self.on_build_button_clicked)
        self.widget_layout.addWidget(self.build_button)

        self.menu = QMenu(self)
        self.menu.setTitle("Payload")
        self.reload_action = QAction(self.menu)
        self.reload_action.setText("Reload options")
        self.reload_action.triggered.connect(self.setupUi)
        self.menu.addAction(self.reload_action)
        self.addMenu(self.menu)

    @Slot()
    def setupUi(self):
        self.spinner.start()
        self.get_build_options.emit()

    @Slot(dict)
    def process_build_message(self, message):
        if self.isVisible():
            event = message.get("event")
            if event == "options":
                options = message.get("options")
                self.set_options(options.get("generators"),
                                 options.get("icons"))
            elif event == "started":
                self.set_started(message.get("generator_name"))
            elif event == "stopped":
                self.set_stopped()
            elif event == "progress":
                self.set_progress(message.get("generator_name"),
                                  message.get("progress"))
            elif event == "error":
                self.set_error(message.get("error"))
            elif event == "generator_finished":
                self.set_generator_finished(message.get("generator_name"),
                                            message.get("exit_code"))
            elif event == "build_finished":
                self.set_build_finished()

    @Slot(str, str)
    def set_progress(self, generator_name, progress):
        for win in self.progress_windows:
            if win.generator() == generator_name:
                win.appendProgress(progress)

    @Slot(str)
    def set_started(self, generator_name):
        win = ProgressWindow(self, generator_name)
        win.show()
        self.progress_windows.append(win)
        self.logger.info("Generator {} started!".format(generator_name))
        self.reload_action.setEnabled(False)
        self.spinner.start()

    @Slot(str, int)
    def set_generator_finished(self, generator_name, exit_code):
        self.logger.info("Generator {} finished with exit code {}.".format(
            generator_name, exit_code))

    @Slot(list, list)
    def set_options(self, generators, icons):
        self.build_name_edit.clear()
        self.generators_list.clear()
        self.icon_combobox.clear()
        for generator in generators:
            item = QListWidgetItem(generator, self.generators_list)
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked)
            self.generators_list.addItem(item)
        for icon in icons:
            name = icon.get("name")
            pix = QPixmap()
            pix.loadFromData(base64.b64decode(icon.get("ico")))
            ico = QIcon()
            ico.addPixmap(pix)
            self.icon_combobox.addItem(ico, name)
        self.spinner.stop()

    @Slot()
    def set_stopped(self):
        self.on_build_finished()
        self.build_button.setText("Build")
        self.stopped_messagebox = FramelessInformationMessageBox(self)
        self.stopped_messagebox.setText(
            "Build process has been stopped successfully.")
        self.stopped_messagebox.setStandardButtons(QDialogButtonBox.Ok)
        self.stopped_messagebox.button(QDialogButtonBox.Ok).clicked.connect(
            self.stopped_messagebox.close)
        self.stopped_messagebox.show()

    @Slot(str)
    def set_error(self, error):
        self.on_build_finished()
        self.build_button.setText("Build")
        self.error_messagebox = FramelessCriticalMessageBox(self)
        self.error_messagebox.setText(error)
        self.error_messagebox.setStandardButtons(QDialogButtonBox.Ok)
        self.error_messagebox.button(QDialogButtonBox.Ok).clicked.connect(
            self.error_messagebox.close)
        self.error_messagebox.show()

    @Slot()
    def set_build_finished(self):
        self.on_build_finished()
        self.build_button.setText("Build")
        self.build_finished_messagebox = FramelessInformationMessageBox(self)
        self.build_finished_messagebox.setText(
            "Build process has been finished.")
        self.build_finished_messagebox.setStandardButtons(QDialogButtonBox.Ok)
        self.build_finished_messagebox.button(
            QDialogButtonBox.Ok).clicked.connect(
                self.build_finished_messagebox.close)
        self.build_finished_messagebox.show()

    @Slot()
    def on_build_button_clicked(self):
        if self.build_button.text() == "Build":
            generators = []
            for i in range(self.generators_list.count()):
                item = self.generators_list.item(i)
                if item.checkState() == Qt.Checked:
                    generators.append(item.text())
            self.start_build.emit(self.build_name_edit.text(),
                                  self.icon_combobox.currentText(), generators)
            self.build_button.setText("Stop")
        else:
            self.stop_build_messagebox = FramelessQuestionMessageBox(self)
            self.stop_build_messagebox.setText(
                "Do you want to stop build process?")
            self.stop_build_messagebox.setStandardButtons(
                QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
            self.stop_build_messagebox.button(
                QDialogButtonBox.Cancel).clicked.connect(
                    self.stop_build_messagebox.close)
            self.stop_build_messagebox.button(
                QDialogButtonBox.Ok).clicked.connect(self.stop_build.emit)
            self.stop_build_messagebox.button(
                QDialogButtonBox.Ok).clicked.connect(
                    self.stop_build_messagebox.close)
            self.stop_build_messagebox.show()

    @Slot()
    def on_build_finished(self):
        self.reload_action.setEnabled(True)
        self.spinner.stop()

    @Slot()
    def close(self) -> bool:
        for win in self.progress_windows:
            win.close()
        return super().close()
Beispiel #16
0
class MainWindow(QMainWindow):

    signal_fitting_parameters_changed = Signal()

    def __init__(self, *, gpc):
        """
        Parameters
        ----------
        gpc: object
            reference to a class that holds references to processing classes.
        """
        super().__init__()

        self._cursor_set = False  # Indicates if temporary 'wait' cursor is set

        self.gpc = gpc
        self.gui_vars = global_gui_variables
        self.gui_vars["ref_main_window"] = self

        self.wnd_manage_emission_lines = WndManageEmissionLines(gpc=self.gpc, gui_vars=self.gui_vars)
        self.wnd_compute_roi_maps = WndComputeRoiMaps(gpc=self.gpc, gui_vars=self.gui_vars)
        self.wnd_image_wizard = WndImageWizard(gpc=self.gpc, gui_vars=self.gui_vars)
        self.wnd_load_quantitative_calibration = WndLoadQuantitativeCalibration(
            gpc=self.gpc, gui_vars=self.gui_vars
        )
        self.wnd_general_fitting_settings = WndGeneralFittingSettings(gpc=self.gpc, gui_vars=self.gui_vars)
        self.wnd_fitting_parameters_shared = WndDetailedFittingParamsShared(gpc=self.gpc, gui_vars=self.gui_vars)
        self.wnd_fitting_parameters_lines = WndDetailedFittingParamsLines(gpc=self.gpc, gui_vars=self.gui_vars)
        # Indicates that the window was closed (used mostly for testing)
        self._is_closed = False

        global_gui_variables["gui_state"]["databroker_available"] = self.gpc.is_databroker_available()

        self.initialize()

        self.central_widget.left_panel.load_data_widget.update_main_window_title.connect(self.update_window_title)

        # Set the callback for update forms with fitting parameters
        def update_fitting_parameter_forms():
            self.signal_fitting_parameters_changed.emit()

        gpc.add_parameters_changed_cb(update_fitting_parameter_forms)
        gpc.param_model.parameters_changed()

    def initialize(self):

        self.resize(_main_window_geometry["initial_width"], _main_window_geometry["initial_height"])

        self.setMinimumWidth(_main_window_geometry["min_width"])
        self.setMinimumHeight(_main_window_geometry["min_height"])

        self.setWindowTitle(self.gpc.get_window_title())

        self.central_widget = TwoPanelWidget(gpc=self.gpc, gui_vars=self.gui_vars)
        self.setCentralWidget(self.central_widget)

        # Status bar
        self.statusLabel = QLabel()
        self.statusBar().addWidget(self.statusLabel)
        self.statusProgressBar = QProgressBar()
        self.statusProgressBar.setFixedWidth(200)
        self.statusBar().addPermanentWidget(self.statusProgressBar)

        self.statusLabelDefaultText = "No data is loaded"
        self.statusLabel.setText(self.statusLabelDefaultText)

        # 'Scan Data' menu item
        self.action_read_file = QAction("&Read File...", self)
        self.action_read_file.setStatusTip("Load data from HDF5 file")
        self.action_read_file.triggered.connect(self.central_widget.left_panel.load_data_widget.pb_file.clicked)

        self.action_load_run = QAction("&Load Run...", self)
        self.action_load_run.setEnabled(self.gui_vars["gui_state"]["databroker_available"])
        self.action_load_run.setStatusTip("Load data from database (Databroker)")
        self.action_load_run.triggered.connect(self.central_widget.left_panel.load_data_widget.pb_dbase.clicked)

        self.action_view_metadata = QAction("View Metadata...", self)
        self.action_view_metadata.setEnabled(self.gpc.is_scan_metadata_available())
        self.action_view_metadata.setStatusTip("View metadata for loaded run")
        self.action_view_metadata.triggered.connect(
            self.central_widget.left_panel.load_data_widget.pb_view_metadata.clicked
        )

        # Main menu
        menubar = self.menuBar()
        # Disable native menu bar (it doesn't work on MacOS 10.15 with PyQt<=5.11)
        #   It may work with later versions of PyQt when they become available.
        menubar.setNativeMenuBar(False)
        loadData = menubar.addMenu("Scan &Data")
        loadData.addAction(self.action_read_file)
        loadData.addAction(self.action_load_run)
        loadData.addSeparator()
        loadData.addAction(self.action_view_metadata)

        # 'Fitting Model' menu item
        self.action_lines_find_automatically = QAction("Find &Automatically...", self)
        self.action_lines_find_automatically.setStatusTip("Automatically find emission lines in total spectrum")
        self.action_lines_find_automatically.triggered.connect(
            self.central_widget.left_panel.model_widget.pb_find_elines.clicked
        )

        self.action_lines_load_from_file = QAction("Load From &File...", self)
        self.action_lines_load_from_file.setStatusTip(
            "Load processing parameters, including selected emission lines, from JSON file"
        )
        self.action_lines_load_from_file.triggered.connect(
            self.central_widget.left_panel.model_widget.pb_load_elines.clicked
        )

        self.action_lines_load_quant_standard = QAction("Load &Quantitative Standards...", self)
        self.action_lines_load_quant_standard.setStatusTip(
            "Load quantitative standard. The emission lines from the standard are automatically selected"
        )
        self.action_lines_load_quant_standard.triggered.connect(
            self.central_widget.left_panel.model_widget.pb_load_qstandard.clicked
        )

        self.action_add_remove_emission_lines = QAction("&Add/Remove Emission Lines...", self)
        self.action_add_remove_emission_lines.setStatusTip("Manually add and remove emission lines")
        self.action_add_remove_emission_lines.triggered.connect(
            self.central_widget.left_panel.model_widget.pb_manage_emission_lines.clicked
        )

        self.action_save_model_params = QAction("&Save Model Parameters...", self)
        self.action_save_model_params.setStatusTip("Save model parameters to JSON file")
        self.action_save_model_params.triggered.connect(
            self.central_widget.left_panel.model_widget.pb_save_elines.clicked
        )

        self.action_add_remove_emission_lines = QAction("Start Model &Fitting", self)
        self.action_add_remove_emission_lines.setStatusTip("Run computations: start fitting for total spectrum")
        self.action_add_remove_emission_lines.triggered.connect(
            self.central_widget.left_panel.model_widget.pb_start_fitting.clicked
        )

        fittingModel = menubar.addMenu("Fitting &Model")
        emissionLines = fittingModel.addMenu("&Emission Lines")
        emissionLines.addAction(self.action_lines_find_automatically)
        emissionLines.addAction(self.action_lines_load_from_file)
        emissionLines.addAction(self.action_lines_load_quant_standard)
        fittingModel.addAction(self.action_add_remove_emission_lines)
        fittingModel.addSeparator()
        fittingModel.addAction(self.action_save_model_params)
        fittingModel.addSeparator()
        fittingModel.addAction(self.action_add_remove_emission_lines)

        # "XRF Maps" menu item
        self.action_start_xrf_map_fitting = QAction("Start XRF Map &Fitting", self)
        self.action_start_xrf_map_fitting.setStatusTip("Run computations: start fitting for XRF maps")
        self.action_start_xrf_map_fitting.triggered.connect(
            self.central_widget.left_panel.fit_maps_widget.pb_start_map_fitting.clicked
        )

        self.action_compute_rois = QAction("Compute &ROIs...", self)
        self.action_compute_rois.setStatusTip("Compute XRF Maps based on spectral ROIs")
        self.action_compute_rois.triggered.connect(
            self.central_widget.left_panel.fit_maps_widget.pb_compute_roi_maps.clicked
        )

        self.action_load_quant_calibration = QAction("&Load Quantitative Calibration...", self)
        self.action_load_quant_calibration.setStatusTip(
            "Load quantitative calibration from JSON file. Calibration is used for scaling of XRF Maps"
        )
        self.action_load_quant_calibration.triggered.connect(
            self.central_widget.left_panel.fit_maps_widget.pb_load_quant_calib.clicked
        )

        self.action_save_quant_calibration = QAction("&Save Quantitative Calibration...", self)
        self.action_save_quant_calibration.setStatusTip(
            "Save Quantitative Calibration based on XRF map of the standard sample"
        )
        self.action_save_quant_calibration.triggered.connect(
            self.central_widget.left_panel.fit_maps_widget.pb_save_q_calibration.clicked
        )

        self.action_export_to_tiff_and_txt = QAction("&Export to TIFF and TXT...", self)
        self.action_export_to_tiff_and_txt.setStatusTip("Export XRF Maps as TIFF and/or TXT files")
        self.action_export_to_tiff_and_txt.triggered.connect(
            self.central_widget.left_panel.fit_maps_widget.pb_export_to_tiff_and_txt.clicked
        )

        xrfMaps = menubar.addMenu("XRF &Maps")
        xrfMaps.addAction(self.action_start_xrf_map_fitting)
        xrfMaps.addAction(self.action_compute_rois)
        xrfMaps.addSeparator()
        xrfMaps.addAction(self.action_load_quant_calibration)
        xrfMaps.addSeparator()
        xrfMaps.addAction(self.action_save_quant_calibration)
        xrfMaps.addAction(self.action_export_to_tiff_and_txt)

        # "View" menu item
        self.action_show_matplotlib_toolbar = QAction("Show &Matplotlib Toolbar", self)
        self.action_show_matplotlib_toolbar.setCheckable(True)
        self.action_show_matplotlib_toolbar.setChecked(True)
        self.action_show_matplotlib_toolbar.setStatusTip("Show Matplotlib Toolbar on the plots")
        self.action_show_matplotlib_toolbar.toggled.connect(self.action_show_matplotlib_toolbar_toggled)

        self.action_show_widget_tooltips = QAction("Show &Tooltips", self)
        self.action_show_widget_tooltips.setCheckable(True)
        self.action_show_widget_tooltips.setChecked(self.gui_vars["show_tooltip"])
        self.action_show_widget_tooltips.setStatusTip("Show widget tooltips")
        self.action_show_widget_tooltips.toggled.connect(self.action_show_widget_tooltips_toggled)

        options = menubar.addMenu("&Options")
        options.addAction(self.action_show_widget_tooltips)
        options.addAction(self.action_show_matplotlib_toolbar)

        # "Help" menu item
        self.action_online_docs = QAction("Online &Documentation", self)
        self.action_online_docs.setStatusTip("Open online documentation in the default browser")
        self.action_online_docs.triggered.connect(self.action_online_docs_triggered)

        self.action_about = QAction("&About PyXRF", self)
        self.action_about.setStatusTip("Show information about this program")
        self.action_about.triggered.connect(self.action_about_triggered)

        help = menubar.addMenu("&Help")
        help.addAction(self.action_online_docs)
        help.addSeparator()
        help.addAction(self.action_about)

        self.update_widget_state()

        # Connect signals
        self.central_widget.left_panel.load_data_widget.update_preview_map_range.connect(
            self.central_widget.right_panel.tab_preview_plots.preview_plot_count.update_map_range
        )

        # Before loading a new file or run
        self.central_widget.left_panel.load_data_widget.signal_loading_new_run.connect(
            self.central_widget.right_panel.slot_activate_tab_preview
        )

        # Open a new file or run
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(
            self.central_widget.left_panel.slot_activate_load_data_tab
        )
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(
            self.central_widget.right_panel.slot_activate_tab_preview
        )
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(self.slot_new_run_loaded)
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(
            self.wnd_image_wizard.slot_update_table
        )
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(
            self.central_widget.right_panel.tab_plot_xrf_maps.slot_update_dataset_info
        )
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(
            self.central_widget.right_panel.tab_plot_rgb_maps.slot_update_dataset_info
        )
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(
            self.wnd_load_quantitative_calibration.update_all_data
        )
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(
            self.central_widget.left_panel.fit_maps_widget.slot_update_for_new_loaded_run
        )

        # New model is loaded or processing parameters (incident energy) was changed
        self.central_widget.left_panel.model_widget.signal_incident_energy_or_range_changed.connect(
            self.central_widget.right_panel.tab_preview_plots.preview_plot_spectrum.redraw_preview_plot
        )
        self.central_widget.left_panel.model_widget.signal_incident_energy_or_range_changed.connect(
            self.wnd_manage_emission_lines.update_widget_data
        )
        self.central_widget.left_panel.model_widget.signal_incident_energy_or_range_changed.connect(
            self.central_widget.right_panel.tab_plot_fitting_model.redraw_plot_fit
        )

        self.central_widget.left_panel.model_widget.signal_model_loaded.connect(
            self.central_widget.right_panel.tab_preview_plots.preview_plot_spectrum.redraw_preview_plot
        )
        self.central_widget.left_panel.model_widget.signal_model_loaded.connect(
            self.central_widget.right_panel.slot_activate_tab_fitting_model
        )
        self.central_widget.left_panel.model_widget.signal_model_loaded.connect(
            self.central_widget.right_panel.tab_plot_fitting_model.update_controls
        )
        self.central_widget.left_panel.model_widget.signal_model_loaded.connect(
            self.wnd_manage_emission_lines.update_widget_data
        )

        # XRF Maps dataset changed
        self.central_widget.right_panel.tab_plot_xrf_maps.signal_maps_dataset_selection_changed.connect(
            self.wnd_image_wizard.slot_update_table
        )

        self.central_widget.right_panel.tab_plot_xrf_maps.signal_maps_dataset_selection_changed.connect(
            self.central_widget.right_panel.tab_plot_rgb_maps.combo_select_dataset_update_current_index
        )
        self.central_widget.right_panel.tab_plot_rgb_maps.signal_rgb_maps_dataset_selection_changed.connect(
            self.central_widget.right_panel.tab_plot_xrf_maps.combo_select_dataset_update_current_index
        )

        self.central_widget.right_panel.tab_plot_xrf_maps.signal_maps_norm_changed.connect(
            self.wnd_image_wizard.slot_update_ranges
        )

        # Quantitative calibration changed
        self.wnd_load_quantitative_calibration.signal_quantitative_calibration_changed.connect(
            self.central_widget.right_panel.tab_plot_rgb_maps.slot_update_ranges
        )
        self.wnd_load_quantitative_calibration.signal_quantitative_calibration_changed.connect(
            self.wnd_image_wizard.slot_update_ranges
        )

        # Selected element is changed (tools for emission line selection)
        self.wnd_manage_emission_lines.signal_selected_element_changed.connect(
            self.central_widget.right_panel.tab_plot_fitting_model.slot_selection_item_changed
        )
        self.central_widget.right_panel.tab_plot_fitting_model.signal_selected_element_changed.connect(
            self.wnd_manage_emission_lines.slot_selection_item_changed
        )
        self.central_widget.right_panel.tab_plot_fitting_model.signal_add_line.connect(
            self.wnd_manage_emission_lines.pb_add_eline_clicked
        )
        self.central_widget.right_panel.tab_plot_fitting_model.signal_remove_line.connect(
            self.wnd_manage_emission_lines.pb_remove_eline_clicked
        )
        self.wnd_manage_emission_lines.signal_update_element_selection_list.connect(
            self.central_widget.right_panel.tab_plot_fitting_model.slot_update_eline_selection_list
        )
        self.wnd_manage_emission_lines.signal_update_add_remove_btn_state.connect(
            self.central_widget.right_panel.tab_plot_fitting_model.slot_update_add_remove_btn_state
        )

        self.wnd_manage_emission_lines.signal_selected_element_changed.connect(
            self.central_widget.left_panel.model_widget.slot_selection_item_changed
        )
        self.central_widget.right_panel.tab_plot_fitting_model.signal_selected_element_changed.connect(
            self.central_widget.left_panel.model_widget.slot_selection_item_changed
        )

        # Total spectrum fitting completed
        self.central_widget.left_panel.model_widget.signal_total_spectrum_fitting_completed.connect(
            self.wnd_manage_emission_lines.update_eline_table
        )
        # Total spectrum invalidated
        self.wnd_manage_emission_lines.signal_parameters_changed.connect(
            self.central_widget.left_panel.model_widget.update_fit_status
        )
        # New dataset loaded or different channel selected. Compute fit parameters.
        self.central_widget.left_panel.load_data_widget.signal_data_channel_changed.connect(
            self.central_widget.left_panel.model_widget.clear_fit_status
        )
        self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(
            self.central_widget.left_panel.model_widget.clear_fit_status
        )
        self.central_widget.left_panel.model_widget.signal_incident_energy_or_range_changed.connect(
            self.central_widget.left_panel.model_widget.clear_fit_status
        )

        # Update map datasets (Fitted maps)
        self.central_widget.left_panel.fit_maps_widget.signal_map_fitting_complete.connect(
            self.central_widget.right_panel.tab_plot_xrf_maps.slot_update_dataset_info
        )
        self.central_widget.left_panel.fit_maps_widget.signal_map_fitting_complete.connect(
            self.central_widget.right_panel.tab_plot_rgb_maps.slot_update_dataset_info
        )
        self.central_widget.left_panel.fit_maps_widget.signal_activate_tab_xrf_maps.connect(
            self.central_widget.right_panel.slot_activate_tab_xrf_maps
        )

        # Update map datasets (ROI maps)
        self.wnd_compute_roi_maps.signal_roi_computation_complete.connect(
            self.central_widget.right_panel.tab_plot_xrf_maps.slot_update_dataset_info
        )
        self.wnd_compute_roi_maps.signal_roi_computation_complete.connect(
            self.central_widget.right_panel.tab_plot_rgb_maps.slot_update_dataset_info
        )
        self.wnd_compute_roi_maps.signal_activate_tab_xrf_maps.connect(
            self.central_widget.right_panel.slot_activate_tab_xrf_maps
        )

        self.signal_fitting_parameters_changed.connect(self.wnd_general_fitting_settings.update_form_data)
        self.signal_fitting_parameters_changed.connect(self.wnd_fitting_parameters_shared.update_form_data)
        self.signal_fitting_parameters_changed.connect(self.wnd_fitting_parameters_lines.update_form_data)
        self.signal_fitting_parameters_changed.connect(self.wnd_manage_emission_lines.update_widget_data)

    @Slot()
    @Slot(str)
    def update_widget_state(self, condition=None):
        # Update the state of the menu bar
        state = not self.gui_vars["gui_state"]["running_computations"]
        self.menuBar().setEnabled(state)

        state_computations = self.gui_vars["gui_state"]["running_computations"]
        if state_computations:
            if not self._cursor_set:
                QGuiApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
                self._cursor_set = True
        else:
            if self._cursor_set:
                QGuiApplication.restoreOverrideCursor()
                self._cursor_set = False

        # Forward to children
        self.central_widget.update_widget_state(condition)

        # Forward the updates to open windows
        self.wnd_manage_emission_lines.update_widget_state(condition)
        self.wnd_compute_roi_maps.update_widget_state(condition)
        self.wnd_image_wizard.update_widget_state(condition)
        self.wnd_load_quantitative_calibration.update_widget_state(condition)
        self.wnd_general_fitting_settings.update_widget_state(condition)
        self.wnd_fitting_parameters_shared.update_widget_state(condition)
        self.wnd_fitting_parameters_lines.update_widget_state(condition)

    def closeEvent(self, event):
        mb_close = QMessageBox(
            QMessageBox.Question,
            "Exit",
            "Are you sure you want to EXIT the program?",
            QMessageBox.Yes | QMessageBox.No,
            parent=self,
        )
        mb_close.setDefaultButton(QMessageBox.No)

        if mb_close.exec() == QMessageBox.Yes:
            event.accept()

            self.wnd_manage_emission_lines.close()
            self.wnd_compute_roi_maps.close()
            self.wnd_image_wizard.close()
            self.wnd_load_quantitative_calibration.close()
            self.wnd_general_fitting_settings.close()
            self.wnd_fitting_parameters_shared.close()
            self.wnd_fitting_parameters_lines.close()

            # This flag is used for CI tests
            self._is_closed = True
        else:
            event.ignore()

    def action_online_docs_triggered(self):
        """
        Display online documentation: open the URL in the default browser.
        """
        doc_url = "http://nsls-ii.github.io/PyXRF/"
        try:
            webbrowser.open(doc_url, autoraise=True)
        except Exception as ex:
            logger.error(f"Error occurred while opening URL '{doc_url}' in the default browser")
            msg = f"Failed to Open Online Documentation. \n  Exception: {str(ex)}"
            msgbox = QMessageBox(QMessageBox.Critical, "Error", msg, QMessageBox.Ok, parent=self)
            msgbox.exec()

    def action_about_triggered(self):
        """
        Display 'About' dialog box
        """
        dlg = DialogAbout()
        dlg.exec()

    def action_show_matplotlib_toolbar_toggled(self, state):
        """
        Turn tooltips on or off
        """
        self.gui_vars["show_matplotlib_toolbar"] = state
        self.update_widget_state()

    def action_show_widget_tooltips_toggled(self, state):
        """
        Turn tooltips on or off
        """
        self.gui_vars["show_tooltip"] = state
        self.update_widget_state("tooltips")

    @Slot()
    def update_window_title(self):
        self.setWindowTitle(self.gpc.get_window_title())

    @Slot(bool)
    def slot_new_run_loaded(self, success):
        if success:
            # Update status bar
            file_name = self.gpc.get_loaded_file_name()
            run_id, run_uid = "", ""
            if self.gpc.is_scan_metadata_available():
                try:
                    run_id = self.gpc.get_metadata_scan_id()
                except Exception:
                    pass
                try:
                    run_uid = self.gpc.get_metadata_scan_uid()
                except Exception:
                    pass
            else:
                if self.gpc.get_current_run_id() >= 0:
                    run_id = self.gpc.get_current_run_id()
            s = ""
            if run_id:
                s += f"ID: {run_id}  "
            if run_uid:
                if run_id:
                    s += f"({run_uid})  "
                else:
                    s += f"UID: {run_uid}  "
            if file_name:
                s += f"File: '{file_name}'"
            self._set_status_bar_text(s)

            # Activate/deactivate "View Metadata ..." menu item
            if self.gpc.is_scan_metadata_available():
                self.action_view_metadata.setEnabled(True)
            else:
                self.action_view_metadata.setEnabled(False)

        else:
            self._set_status_bar_text()

    def _set_status_bar_text(self, text=None):
        if text is None:
            text = self.statusLabelDefaultText
        self.statusLabel.setText(text)
Beispiel #17
0
class StructureSynthesis(QWidget, Ui_Form):
    """Number and type synthesis widget.

    Calculate the combinations of mechanism family and show the atlas.
    """
    assortment: Dict[Assortment, List[Assortment]]
    answer: List[Graph]

    def __init__(self, parent: MainWindowBase):
        """Reference names:

        + IO functions from main window.
        + Table data from PMKS expression.
        + Graph data function from main window.
        """
        super(StructureSynthesis, self).__init__(parent)
        self.setupUi(self)
        header = self.link_assortment_list.header()
        header.setSectionResizeMode(QHeaderView.ResizeToContents)

        # Function references
        self.output_to = parent.output_to
        self.save_reply_box = parent.save_reply_box
        self.input_from_multiple = parent.input_from_multiple
        self.vpoints = parent.vpoint_list
        self.vlinks = parent.vlink_list
        self.get_graph = parent.get_graph
        self.prefer = parent.prefer
        self.add_collection = parent.collections.structure_widget.add_collection
        # Answer list
        self.assortment = {}
        self.answer = []
        # Signals
        self.nl_input.valueChanged.connect(self.__adjust_structure_data)
        self.nj_input.valueChanged.connect(self.__adjust_structure_data)
        self.graph_engine.addItems(engines)
        self.structure_list.customContextMenuRequested.connect(
            self.__structure_list_context_menu
        )

        # Context menu
        self.pop_menu_topo = QMenu(self)
        self.to_collection = QAction(
            QIcon(QPixmap(":/icons/collections.png")),
            "Add to collections",
            self
        )
        self.copy_edges = QAction("Copy edges", self)
        self.copy_image = QAction("Copy image", self)
        self.pop_menu_topo.addActions([
            self.to_collection,
            self.copy_edges,
            self.copy_image,
        ])

        self.nl_input_old_value = 0
        self.nj_input_old_value = 0
        self.clear()

    def clear(self) -> None:
        """Clear all sub-widgets."""
        self.edges_text.clear()
        self.__clear_assortment()
        self.__clear_structure_list()
        self.nl_input.setValue(0)
        self.nj_input.setValue(0)
        self.nl_input_old_value = 0
        self.nj_input_old_value = 0
        self.dof.setValue(1)

    @Slot(name='on_assortment_clear_button_clicked')
    def __clear_assortment(self) -> None:
        """Clear the number synthesis list."""
        self.link_assortment_list.clear()
        self.assortment.clear()

    @Slot(name='on_structure_list_clear_button_clicked')
    def __clear_structure_list(self) -> None:
        """Clear the structure list."""
        self.answer.clear()
        self.structure_list.clear()
        self.time_label.setText("")

    @Slot(name='on_from_mechanism_button_clicked')
    def __from_mechanism(self) -> None:
        """From a generalized mechanism of main canvas."""
        if self.vpoints and self.vlinks:
            graph, _, _, _, _, _ = self.get_graph()
        else:
            graph = Graph([])
        if graph.edges:
            self.edges_text.setText(str(list(graph.edges)))
        else:
            self.edges_text.setText("")
        keep_dof_checked = self.keep_dof.isChecked()
        self.keep_dof.setChecked(False)
        self.nl_input.setValue(len(graph.vertices))
        self.nj_input.setValue(len(graph.edges))
        self.keep_dof.setChecked(keep_dof_checked)

        # Show attributes
        QMessageBox.information(
            self,
            "Generalization",
            f"Link assortment:\n{link_assortment(graph)}\n"
            f"Contracted link assortment:\n{contracted_link_assortment(graph)}"
            if graph.edges else
            "Is a empty graph."
        )

    def __adjust_structure_data(self) -> None:
        """Update NJ and NL values.

        If user don't want to keep the DOF:
        Change the DOF then exit.
        """
        if not self.keep_dof.isChecked():
            self.dof.setValue(
                3 * (self.nl_input.value() - 1)
                - 2 * self.nj_input.value()
            )
            return

        # N2: Get the user's adjusted value.
        # NL_func: Get the another value of parameters (N1) by degrees of freedom formula.
        # is_above: Is value increase or decrease?
        if self.sender() is self.nj_input:
            n2 = self.nj_input.value()

            def nl_func() -> float:
                return (self.dof.value() + 2 * n2) / 3 + 1

            is_above = n2 > self.nj_input_old_value
        else:
            n2 = self.nl_input.value()

            def nl_func() -> float:
                return (3 * (n2 - 1) - self.dof.value()) / 2

            is_above = n2 > self.nl_input_old_value
        n1 = nl_func()
        while not n1.is_integer():
            n2 += 1 if is_above else -1
            n1 = nl_func()
            if n1 == 0 or n2 == 0:
                break

        n1 = int(n1)
        n2 = int(n2)
        # Return the result values
        # + Value of widgets.
        # + Setting old value record.
        if self.sender() is self.nl_input:
            self.nj_input.setValue(n1)
            self.nl_input.setValue(n2)
            self.nj_input_old_value = n1
            self.nl_input_old_value = n2
        else:
            self.nj_input.setValue(n2)
            self.nl_input.setValue(n1)
            self.nj_input_old_value = n2
            self.nl_input_old_value = n1

    @Slot(name='on_number_synthesis_button_clicked')
    def __number_synthesis(self) -> None:
        """Synthesis of link assortment."""
        self.__clear_assortment()
        nl = self.nl_input.value()
        nj = self.nj_input.value()
        dlg = SynthesisProgressDialog(
            "Link assortment",
            f"Number of links: {nl}\n"
            f"Number of joints: {nj}",
            1,
            self
        )

        @Slot(dict)
        def update_result(assortment: Dict[Assortment, List[Assortment]]) -> None:
            """Update results."""
            self.assortment.update(assortment)
            for la, cla_list in assortment.items():
                la_item = QTreeWidgetItem([", ".join(
                    f"NL{i + 2} = {a}" for i, a in enumerate(la)
                ), "N/A"])
                for cla in cla_list:
                    la_item.addChild(QTreeWidgetItem([", ".join(
                        f"NC{i + 1} = {a}" for i, a in enumerate(cla)
                    ), "N/A"]))
                self.link_assortment_list.addTopLevelItem(la_item)
            first_item = self.link_assortment_list.topLevelItem(0)
            self.link_assortment_list.setCurrentItem(first_item)
            dlg.deleteLater()

        work = LinkThread(nl, nj, dlg)
        work.progress_update.connect(dlg.setValue)
        work.size_update.connect(dlg.setMaximum)
        work.result.connect(update_result)
        dlg.show()
        work.start()

    def __set_time_count(self, t: float, count: int) -> None:
        """Set time and count digit to label."""
        self.time_label.setText(f"{t:.04f} s ({count})")

    def __set_paint_time(self, t: float) -> None:
        """Set painting time of atlas."""
        self.paint_time_label.setText(f"{t:.04f}s")

    @Slot(name='on_structure_synthesis_button_clicked')
    def __structure_synthesis(self) -> None:
        """Structural synthesis - find by contracted links."""
        self.__clear_structure_list()
        item = self.link_assortment_list.currentItem()
        if item is None:
            self.__number_synthesis()
            item = self.link_assortment_list.currentItem()
        root = item.parent()
        if root is None:
            # Find by link assortment
            try:
                # Test
                assortment_eval(item.text(0))
            except ValueError:
                return
            jobs = [item.child(i) for i in range(item.childCount())]
        else:
            # Find by contracted link assortment
            jobs = [item]
        self.__structural_combine(jobs)

    @Slot(name='on_structure_synthesis_all_button_clicked')
    def __structure_synthesis_all(self) -> None:
        """Structural synthesis - find all."""
        self.__clear_structure_list()
        jobs = []
        for i in range(self.link_assortment_list.topLevelItemCount()):
            root = self.link_assortment_list.topLevelItem(i)
            for j in range(root.childCount()):
                jobs.append(root.child(j))
        self.__structural_combine(jobs)

    def __structural_combine(self, jobs: Sequence[QTreeWidgetItem]) -> None:
        """Structural combine by iterator."""
        t0 = process_time()
        dlg = SynthesisProgressDialog(
            "Structural Synthesis",
            f"Number of cases: {len(jobs)}",
            len(jobs),
            self
        )

        @Slot(QTreeWidgetItem, int)
        def update_count(item: QTreeWidgetItem, count: int) -> None:
            """Update the number of graphs."""
            item.setText(1, f"{count}")

        @Slot(list)
        def update_result(answer: List[Graph]) -> None:
            """Update the result of atlas."""
            self.answer = answer
            dlg.deleteLater()
            for i in range(self.link_assortment_list.topLevelItemCount()):
                root = self.link_assortment_list.topLevelItem(i)
                count = 0
                for j in range(root.childCount()):
                    item = root.child(j)
                    try:
                        count += int(item.text(1))
                    except ValueError:
                        pass
                root.setText(1, f"{count}")
            self.__set_time_count(process_time() - t0, len(self.answer))
            self.__reload_atlas()

        work = GraphThread(jobs, self.graph_degenerate.currentIndex(), dlg)
        work.count_update.connect(update_count)
        work.progress_update.connect(dlg.setValue)
        work.result.connect(update_result)
        dlg.show()
        work.start()

    @Slot(name='on_reload_atlas_clicked')
    @Slot(bool, name='on_graph_link_as_node_toggled')
    @Slot(bool, name='on_graph_show_label_toggled')
    @Slot(int, name='on_graph_engine_currentIndexChanged')
    def __reload_atlas(self, *_) -> None:
        """Reload the atlas."""
        scroll_bar: QScrollBar = self.structure_list.verticalScrollBar()
        scroll_pos = scroll_bar.sliderPosition()
        index = self.structure_list.currentRow()
        self.structure_list.clear()

        if not self.answer:
            return

        dlg = SynthesisProgressDialog(
            "Structural Synthesis",
            f"Drawing atlas ({len(self.answer)}) ...",
            len(self.answer),
            self
        )
        dlg.show()
        t0 = process_time()
        for i, G in enumerate(self.answer):
            QCoreApplication.processEvents()
            if dlg.wasCanceled():
                return
            if self.__draw_atlas(i, G):
                dlg.setValue(i + 1)
            else:
                break
        self.__set_paint_time(process_time() - t0)
        dlg.setValue(dlg.maximum())
        dlg.deleteLater()
        scroll_bar.setSliderPosition(scroll_pos)
        self.structure_list.setCurrentRow(index)

    def __draw_atlas(self, i: int, g: Graph) -> bool:
        """Draw atlas and return True if done."""
        item = QListWidgetItem(f"No. {i + 1}")
        item.setIcon(graph2icon(
            g,
            self.structure_list.iconSize().width(),
            self.graph_link_as_node.isChecked(),
            self.graph_show_label.isChecked(),
            self.prefer.monochrome_option,
            engine=self.graph_engine.currentText()
        ))
        item.setToolTip(
            f"Edge Set: {list(g.edges)}\n"
            f"Link assortment: {link_assortment(g)}\n"
            f"Contracted Link assortment: {contracted_link_assortment(g)}\n"
            f"Degree code: {g.degree_code()}"
        )
        self.structure_list.addItem(item)
        return True

    def __atlas_image(self, row: Optional[int] = None) -> QImage:
        """Capture a result item icon to image."""
        if row is None:
            item = self.structure_list.currentItem()
        else:
            item = self.structure_list.item(row)
        return item.icon().pixmap(self.structure_list.iconSize()).toImage()

    @Slot(QPoint)
    def __structure_list_context_menu(self, point) -> None:
        """Context menu for the type synthesis results."""
        index = self.structure_list.currentIndex().row()
        self.to_collection.setEnabled(index > -1)
        self.copy_edges.setEnabled(index > -1)
        self.copy_image.setEnabled(index > -1)
        action = self.pop_menu_topo.exec_(self.structure_list.mapToGlobal(point))
        if not action:
            return
        clipboard = QApplication.clipboard()
        if action == self.to_collection:
            self.add_collection(self.answer[index].edges)
        elif action == self.copy_edges:
            clipboard.setText(str(self.answer[index].edges))
        elif action == self.copy_image:
            # Turn the transparent background to white
            image1 = self.__atlas_image()
            image2 = QImage(image1.size(), image1.format())
            image2.fill(Qt.white)
            painter = QPainter(image2)
            painter.drawImage(QPointF(0, 0), image1)
            painter.end()
            clipboard.setPixmap(QPixmap.fromImage(image2))

    @Slot(name='on_expr_copy_clicked')
    def __copy_expr(self) -> None:
        """Copy expression button."""
        string = self.edges_text.text()
        if string:
            QApplication.clipboard().setText(string)
            self.edges_text.selectAll()

    @Slot(name='on_expr_add_collection_clicked')
    def __add_collection(self) -> None:
        """Add this expression to collections widget."""
        string = self.edges_text.text()
        if string:
            self.add_collection(eval(string))

    @Slot(name='on_save_atlas_clicked')
    def __save_atlas(self) -> None:
        """Saving all the atlas to image file.

        We should turn transparent background to white first.
        Then using QImage class to merge into one image.
        """
        count = self.structure_list.count()
        if count < 1:
            return

        lateral = self.__save_atlas_ask()
        if not lateral:
            return

        file_name = self.output_to("atlas image", qt_image_format)
        if not file_name:
            return

        width = self.structure_list.iconSize().width()
        image_main = QImage(QSize(
            lateral * width if count > lateral else count * width,
            ((count // lateral) + bool(count % lateral)) * width
        ), self.__atlas_image(0).format())
        image_main.fill(Qt.transparent)
        painter = QPainter(image_main)
        for row in range(count):
            image = self.__atlas_image(row)
            painter.drawImage(QPointF(row % lateral, row // lateral) * width, image)
        painter.end()
        pixmap = QPixmap.fromImage(image_main)
        pixmap.save(file_name)
        self.save_reply_box("Atlas", file_name)

    def __save_atlas_ask(self) -> int:
        """Ask when saving the atlas."""
        lateral, ok = QInputDialog.getInt(
            self,
            "Atlas",
            "The number of lateral:",
            5, 1
        )
        if not ok:
            return 0
        return lateral

    @Slot(name='on_save_edges_clicked')
    def __save_edges(self) -> None:
        """Saving all the atlas to text file."""
        file_name = ""
        count = self.structure_list.count()
        if count < 1:
            return
        if not file_name:
            file_name = self.output_to("atlas edges expression", ["Text file (*.txt)"])
        if not file_name:
            return
        with open(file_name, 'w+', encoding='utf-8') as f:
            f.write('\n'.join(str(G.edges) for G in self.answer))
        self.save_reply_box("edges expression", file_name)

    @Slot(name='on_edges2atlas_button_clicked')
    def __edges2atlas(self) -> None:
        """Turn the text files into a atlas image.

        This operation will load all edges to list widget first.
        """
        file_names = self.input_from_multiple(
            "edges data",
            ["Text file (*.txt)"]
        )
        if not file_names:
            return

        read_data = []
        for file_name in file_names:
            with open(file_name, 'r', encoding='utf-8') as f:
                for line in f:
                    read_data.append(line)

        answer = []
        for edges in read_data:
            try:
                g = Graph(eval(edges))
            except (SyntaxError, TypeError):
                QMessageBox.warning(
                    self,
                    "Wrong format",
                    "Please check text format."
                )
            else:
                answer.append(g)

        if not answer:
            QMessageBox.information(
                self,
                "No data",
                "The graph data is empty."
            )
            return

        self.answer = answer
        self.__set_time_count(0, len(answer))
        self.__reload_atlas()
        self.__save_atlas()
Beispiel #18
0
class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.recording_enabled = False
        self.serial = serial.Serial()
        self.rootnode = JsonNode('')
        self._connected = False
        self._dirty = False
        self._filename = None

        # settings
        self.settings = QSettingsManager()
        set_default_settings(self.settings)

        # Controller Settings
        self.settingsDialog = None

        # object explorer
        self.objectexplorer = ObjectExplorer(self.rootnode, self)
        self.objectexplorer.nodevalue_changed.connect(self.send_serialdata)
        self.objectexplorer.nodeproperty_changed.connect(self.set_dirty)
        self.objectexplorerDockWidget = QDockWidget(self.tr("object explorer"),
                                                    self)
        self.objectexplorerDockWidget.setObjectName(
            "objectexplorer_dockwidget")
        self.objectexplorerDockWidget.setWidget(self.objectexplorer)

        # plot widget
        self.plot = PlotWidget(self.rootnode, self.settings, self)

        # plot settings
        self.plotsettings = PlotSettingsWidget(self.settings, self.plot, self)
        self.plotsettingsDockWidget = QDockWidget(self.tr("plot settings"),
                                                  self)
        self.plotsettingsDockWidget.setObjectName("plotsettings_dockwidget")
        self.plotsettingsDockWidget.setWidget(self.plotsettings)

        # log widget
        self.loggingWidget = LoggingWidget(self)
        self.loggingDockWidget = QDockWidget(self.tr("logger"), self)
        self.loggingDockWidget.setObjectName("logging_dockwidget")
        self.loggingDockWidget.setWidget(self.loggingWidget)

        # record widget
        self.recordWidget = RecordWidget(self.rootnode, self)
        self.recordDockWidget = QDockWidget(self.tr("data recording"), self)
        self.recordDockWidget.setObjectName("record_dockwidget")
        self.recordDockWidget.setWidget(self.recordWidget)

        # actions and menus
        self._init_actions()
        self._init_menus()

        # statusbar
        statusbar = self.statusBar()
        statusbar.setVisible(True)
        self.connectionstateLabel = QLabel(self.tr("Not connected"))
        statusbar.addPermanentWidget(self.connectionstateLabel)
        statusbar.showMessage(self.tr("Ready"))

        # layout
        self.setCentralWidget(self.plot)
        self.addDockWidget(Qt.LeftDockWidgetArea,
                           self.objectexplorerDockWidget)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.plotsettingsDockWidget)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.loggingDockWidget)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.recordDockWidget)

        self.load_settings()

    def _init_actions(self):
        # Serial Dialog
        self.serialdlgAction = QAction(self.tr("Serial Settings..."), self)
        self.serialdlgAction.setShortcut("F6")
        self.serialdlgAction.setIcon(QIcon(pixmap("configure.png")))
        self.serialdlgAction.triggered.connect(self.show_serialdlg)

        # Connect
        self.connectAction = QAction(self.tr("Connect"), self)
        self.connectAction.setShortcut("F5")
        self.connectAction.setIcon(QIcon(pixmap("network-connect-3.png")))
        self.connectAction.triggered.connect(self.toggle_connect)

        # Quit
        self.quitAction = QAction(self.tr("Quit"), self)
        self.quitAction.setShortcut("Alt+F4")
        self.quitAction.setIcon(QIcon(pixmap("window-close-3.png")))
        self.quitAction.triggered.connect(self.close)

        # Save Config as
        self.saveasAction = QAction(self.tr("Save as..."), self)
        self.saveasAction.setShortcut("Ctrl+Shift+S")
        self.saveasAction.setIcon(QIcon(pixmap("document-save-as-5.png")))
        self.saveasAction.triggered.connect(self.show_savecfg_dlg)

        # Save file
        self.saveAction = QAction(self.tr("Save"), self)
        self.saveAction.setShortcut("Ctrl+S")
        self.saveAction.setIcon(QIcon(pixmap("document-save-5.png")))
        self.saveAction.triggered.connect(self.save_file)

        # Load file
        self.loadAction = QAction(self.tr("Open..."), self)
        self.loadAction.setShortcut("Ctrl+O")
        self.loadAction.setIcon(QIcon(pixmap("document-open-7.png")))
        self.loadAction.triggered.connect(self.show_opencfg_dlg)

        # New
        self.newAction = QAction(self.tr("New"), self)
        self.newAction.setShortcut("Ctrl+N")
        self.newAction.setIcon(QIcon(pixmap("document-new-6.png")))
        self.newAction.triggered.connect(self.new)

        # start recording
        self.startrecordingAction = QAction(self.tr("Start recording"), self)
        self.startrecordingAction.setShortcut("F9")
        self.startrecordingAction.setIcon(QIcon(pixmap("media-record-6.png")))
        self.startrecordingAction.triggered.connect(self.start_recording)

        # stop recording
        self.stoprecordingAction = QAction(self.tr("Stop recording"), self)
        self.stoprecordingAction.setShortcut("F10")
        self.stoprecordingAction.setIcon(QIcon(pixmap("media-playback-stop-8.png")))
        self.stoprecordingAction.setEnabled(False)
        self.stoprecordingAction.triggered.connect(self.stop_recording)

        # clear record
        self.clearrecordAction = QAction(self.tr("Clear"), self)
        self.clearrecordAction.setIcon(QIcon(pixmap("editclear.png")))
        self.clearrecordAction.triggered.connect(self.clear_record)

        # export record
        self.exportcsvAction = QAction(self.tr("Export to csv..."), self)
        self.exportcsvAction.setIcon(QIcon(pixmap("text_csv.png")))
        self.exportcsvAction.triggered.connect(self.export_csv)

        # show record settings
        self.recordsettingsAction = QAction(self.tr("Settings..."), self)
        self.recordsettingsAction.setIcon(QIcon(pixmap("configure.png")))
        self.recordsettingsAction.triggered.connect(self.show_recordsettings)

        # Info
        self.infoAction = QAction(self.tr("Info"), self)
        self.infoAction.setShortcut("F1")
        self.infoAction.triggered.connect(self.show_info)

    def _init_menus(self):
        # file menu
        self.fileMenu = self.menuBar().addMenu(self.tr("File"))
        self.fileMenu.addAction(self.newAction)
        self.fileMenu.addAction(self.loadAction)
        self.fileMenu.addAction(self.saveAction)
        self.fileMenu.addAction(self.saveasAction)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.connectAction)
        self.fileMenu.addAction(self.serialdlgAction)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.quitAction)

        # view menu
        self.viewMenu = self.menuBar().addMenu(self.tr("View"))
        self.viewMenu.addAction(
            self.objectexplorerDockWidget.toggleViewAction())
        self.viewMenu.addAction(self.plotsettingsDockWidget.toggleViewAction())
        self.viewMenu.addAction(self.loggingDockWidget.toggleViewAction())
        self.viewMenu.addAction(self.recordDockWidget.toggleViewAction())

        # record menu
        self.recordMenu = self.menuBar().addMenu(self.tr("Record"))
        self.recordMenu.addAction(self.startrecordingAction)
        self.recordMenu.addAction(self.stoprecordingAction)
        self.recordMenu.addAction(self.exportcsvAction)
        self.recordMenu.addSeparator()
        self.recordMenu.addAction(self.clearrecordAction)
        self.recordMenu.addSeparator()
        self.recordMenu.addAction(self.recordsettingsAction)

        # info menu
        self.menuBar().addAction(self.infoAction)

    def show_info(self):
        QMessageBox.about(
            self, QApplication.applicationName(),
            "%s %s\n"
            "Copyright (c) by %s" %
            (
                QCoreApplication.applicationName(),
                QCoreApplication.applicationVersion(),
                QCoreApplication.organizationName(),
            )
        )

    def load_file(self, filename):
        old_filename = self.filename if self.filename != filename else None
        self.filename = filename

        try:
            with open(filename, 'rb') as f:
                try:
                    self.objectexplorer.model().beginResetModel()
                    self.rootnode.load(bytearray_to_utf8(f.read()))
                    self.objectexplorer.model().endResetModel()
                except ValueError as e:
                    critical(self, "File '%s' is not a valid config file."
                             % filename)
                    logger.error(str(e))
                    if old_filename is not None:
                        self.load_file(old_filename)
                    else:
                        self.filename = None

        except FileNotFoundError as e:
            logger.error(str(e))
            self.filename = None

        self.objectexplorer.refresh()

    def load_settings(self):
        settings = QSettings()

        # window geometry
        try:
            self.restoreGeometry(settings.value(GEOMETRY_SETTING))
        except:
            logger.debug("error restoring window geometry")

        # window state
        try:
            self.restoreState(settings.value(WINDOWSTATE_SETTING))
        except:
            logger.debug("error restoring window state")

        # filename
        self.filename = settings.value(FILENAME_SETTING)
        if self.filename is not None:
            self.load_file(self.filename)

    def save_settings(self):
        settings = QSettings()
        settings.setValue(WINDOWSTATE_SETTING, self.saveState())
        settings.setValue(GEOMETRY_SETTING, self.saveGeometry())
        settings.setValue(FILENAME_SETTING, self.filename)

    def closeEvent(self, event):
        if self.dirty:
            res = QMessageBox.question(
                self,
                QCoreApplication.applicationName(),
                self.tr("Save changes to file '%s'?" %
                        self.filename
                        if self.filename is not None else "unknown"),
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
            )
            if res == QMessageBox.Cancel:
                event.ignore()
                return
            elif res == QMessageBox.Yes:
                self.save_file()

        self.save_settings()

        try:
            self.worker.quit()
        except AttributeError:
            pass

        try:
            self.serial.close()
        except (SerialException, AttributeError):
            pass

    def new(self):
        self.objectexplorer.model().beginResetModel()
        self.rootnode.clear()
        self.objectexplorer.model().endResetModel()

    def send_reset(self):
        jsonstring = json.dumps({"resetpid": 1})
        self.serial.write(bytearray(jsonstring, 'utf-8'))

    def receive_serialdata(self, time, data):
        self.loggingWidget.log_input(data)

        try:
            self.rootnode.from_json(data)
        except ValueError as e:
            logger.error(str(e))

        # refresh widgets
        self.objectexplorer.refresh()
        self.plot.refresh(time)
        if self.recording_enabled:
            self.recordWidget.add_data(time, self.rootnode)

    def send_serialdata(self, node):
        if isinstance(node, JsonItem):
            if self.serial.isOpen():
                s = node.to_json()
                self.serial.write(utf8_to_bytearray(s + '\n'))
                self.loggingWidget.log_output(s.strip())

    def show_serialdlg(self):
        dlg = SerialDialog(self.settings, self)
        dlg.exec_()

    def toggle_connect(self):
        if self.serial.isOpen():
            self.disconnect()
        else:
            self.connect()

    def connect(self):
        # Load port setting
        port = self.settings.get(PORT_SETTING)
        baudrate = self.settings.get(BAUDRATE_SETTING)

        # If no port has been selected before show serial settings dialog
        if port is None:
            if self.show_serialdlg() == QDialog.Rejected:
                return
            port = self.settings.get(PORT_SETTING)
            baudrate = self.settings.get(BAUDRATE_SETTING)

        # Serial connection
        try:
            self.serial.port = port
            self.serial.baudrate = baudrate
            self.serial.open()
        except ValueError:
            QMessageBox.critical(
                self, QCoreApplication.applicationName(),
                self.tr("Serial parameters e.g. baudrate, databits are out "
                        "of range.")
            )
        except SerialException:
            QMessageBox.critical(
                self, QCoreApplication.applicationName(),
                self.tr("The device '%s' can not be found or can not be "
                        "configured." % port)
            )
        else:
            self.worker = SerialWorker(self.serial, self)
            self.worker.data_received.connect(self.receive_serialdata)
            self.worker.start()

            self.connectAction.setText(self.tr("Disconnect"))
            self.connectAction.setIcon(QIcon(pixmap("network-disconnect-3.png")))
            self.serialdlgAction.setEnabled(False)
            self.connectionstateLabel.setText(
                self.tr("Connected to %s") % port)
            self._connected = True
            self.objectexplorer.refresh()

    def disconnect(self):
        self.worker.quit()
        self.serial.close()
        self.connectAction.setText(self.tr("Connect"))
        self.connectAction.setIcon(QIcon(pixmap("network-connect-3.png")))
        self.serialdlgAction.setEnabled(True)
        self.connectionstateLabel.setText(self.tr("Not connected"))
        self._connected = False
        self.objectexplorer.refresh()

    def show_savecfg_dlg(self):
        filename, _ = QFileDialog.getSaveFileName(
            self, self.tr("Save configuration file..."),
            directory=os.path.expanduser("~"),
            filter="Json file (*.json)"
        )

        if filename:
            self.filename = filename
            self.save_file()

    def save_file(self):
        if self.filename is not None:
            config_string = self.rootnode.dump()
            with open(self.filename, 'w') as f:
                f.write(config_string)
            self.dirty = False
        else:
            self.show_savecfg_dlg()

    def show_opencfg_dlg(self):
        # show file dialog
        filename, _ = QFileDialog.getOpenFileName(
            self, self.tr("Open configuration file..."),
            directory=os.path.expanduser("~"),
            filter=self.tr("Json file (*.json);;All files (*.*)")
        )

        # load config file
        if filename:
            self.load_file(filename)

    def refresh_window_title(self):
        s = "%s %s" % (QCoreApplication.applicationName(),
                       QCoreApplication.applicationVersion())
        if self.filename is not None:
            s += " - " + self.filename
        if self.dirty:
            s += "*"
        self.setWindowTitle(s)

    def start_recording(self):
        self.recording_enabled = True
        self.startrecordingAction.setEnabled(False)
        self.stoprecordingAction.setEnabled(True)

    def stop_recording(self):
        self.recording_enabled = False
        self.startrecordingAction.setEnabled(True)
        self.stoprecordingAction.setEnabled(False)

    def export_csv(self):
        filename, _ = QFileDialog.getSaveFileName(
            self, QCoreApplication.applicationName(),
            filter="CSV files(*.csv);;All files (*.*)"
        )

        if filename == "":
            return

        # get current dataframe and export to csv
        df = self.recordWidget.dataframe
        decimal = self.settings.get(DECIMAL_SETTING)
        df = df.applymap(lambda x: str(x).replace(".", decimal))
        df.to_csv(
            filename, index_label="time",
            sep=self.settings.get(SEPARATOR_SETTING)
        )

    def clear_record(self):
        self.recordWidget.clear()

    def show_recordsettings(self):
        dlg = CSVSettingsDialog(self)
        dlg.exec_()

    # filename property
    @property
    def filename(self):
        return self._filename

    @filename.setter
    def filename(self, value=""):
        self._filename = value
        self.refresh_window_title()

    # dirty property
    @property
    def dirty(self):
        return self._dirty

    @dirty.setter
    def dirty(self, value):
        self._dirty = value
        self.refresh_window_title()

    def set_dirty(self):
        self.dirty = True

    # connected property
    @property
    def connected(self):
        return self._connected