Exemplo n.º 1
0
def test_populate_menu_create_checkable(qtbot):
    """Test the populate_menu function with checkable actions."""

    mock = MagicMock()
    menu = QMenu()
    populate_menu(menu, [{"text": "test", "slot": mock, "checkable": True}])
    assert len(menu.actions()) == 1
    assert menu.actions()[0].text() == "test"
    assert menu.actions()[0].isCheckable() is True
    with qtbot.waitSignal(menu.actions()[0].triggered):
        menu.actions()[0].trigger()
    mock.assert_called_once_with(True)
    mock.reset_mock()
    with qtbot.waitSignal(menu.actions()[0].triggered):
        menu.actions()[0].trigger()
    mock.assert_called_once_with(False)
Exemplo n.º 2
0
def test_populate_menu_create(qtbot):
    """Test the populate_menu function."""

    mock = MagicMock()
    menu = QMenu()
    populate_menu(menu, [{"text": "test", "slot": mock}])
    assert len(menu.actions()) == 1
    assert menu.actions()[0].text() == "test"
    assert menu.actions()[0].isCheckable() is False
    with qtbot.waitSignal(menu.actions()[0].triggered):
        menu.actions()[0].trigger()
    mock.assert_called_once()
Exemplo n.º 3
0
    def add_action_to_menu(self, action: QAction, menu: QMenu, insert_sorted: bool):
        '''
        Adds action to menu - optionally in sorted order

        Parameters
        ----------
        action : QAction
        menu : QMenu
        insert_sorted : bool
        '''
        if insert_sorted:
            actions = menu.actions()
            if not actions:
                menu.addAction(action)
            else:
                actions = [act.text() for act in actions] + [action.text()]
                actions.sort()
                menu.insertAction(actions.index(action.text()), action)
        else:
            menu.addAction(action)
Exemplo n.º 4
0
class BaseList(CustomGroupBox):
    """Template for control of High Level Triggers."""

    _MIN_WIDs = {}
    _LABELS = {}
    _ALL_PROPS = tuple()

    def __init__(self,
                 name=None,
                 parent=None,
                 prefix='',
                 props=set(),
                 obj_names=list(),
                 has_search=True,
                 props2search=set()):
        """Initialize object."""
        super().__init__(name, parent)
        self.prefix = prefix
        self.props = props or set(self._ALL_PROPS)
        self.has_search = has_search
        self.props2search = set(props2search) or set()
        self.obj_names = obj_names
        self.setupUi()

    def setupUi(self):
        self.my_layout = QVBoxLayout(self)
        self.my_layout.setContentsMargins(6, 10, 6, 0)

        if self.has_search:
            hbl = QHBoxLayout()
            hbl.setSpacing(0)
            self.my_layout.addLayout(hbl)
            # Create search bar
            self.search_lineedit = QLineEdit(parent=self)
            hbl.addWidget(self.search_lineedit)
            self.search_lineedit.setPlaceholderText("Search...")
            self.search_lineedit.textEdited.connect(self.filter_lines)
            # Create search menu
            pbt = QPushButton('  ', self)
            pbt.setToolTip('Choose which columns to show')
            pbt.setObjectName('but')
            pbt.setIcon(qta.icon('mdi.view-column'))
            pbt.setStyleSheet("""
                #but{
                    min-width:35px; max-width:35px;
                    min-height:25px; max-height:25px;
                    icon-size:25px;
                }""")
            hbl.addWidget(pbt)
            self.search_menu = QMenu(pbt)
            self.search_menu.triggered.connect(self.filter_lines)
            pbt.setMenu(self.search_menu)
            for prop in self._ALL_PROPS:
                act = self.search_menu.addAction(prop)
                act.setCheckable(True)
                act.setChecked(prop in self.props)
                act.toggled.connect(self.filter_columns)

        # Create header
        header = QWidget()
        headerlay = QHBoxLayout(header)
        headerlay.setContentsMargins(0, 0, 0, 0)
        self.my_layout.addWidget(header, alignment=Qt.AlignLeft)
        objs = self.getLine(header=True)
        for prop, obj in objs:
            name = obj.objectName()
            obj.setStyleSheet("""
                #{0:s}{{
                    min-width:{1:.1f}em; max-width: {1:.1f}em;
                    min-height:1.8em; max-height:1.8em;
                }}""".format(name, self._MIN_WIDs[prop]))
            headerlay.addWidget(obj)

        # Create scrollarea
        sc_area = QScrollArea()
        sc_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        sc_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        sc_area.setWidgetResizable(True)
        sc_area.setFrameShape(QFrame.NoFrame)
        self.my_layout.addWidget(sc_area)

        # ScrollArea Widget
        wid = QWidget()
        wid.setObjectName('wid')
        wid.setStyleSheet('#wid {background-color: transparent;}')
        lay = QVBoxLayout(wid)
        lay.setSpacing(0)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setAlignment(Qt.AlignTop)
        sc_area.setWidget(wid)

        self.lines = dict()
        self.filtered_lines = set()
        for obj_name in self.obj_names:
            pref = _PVName(obj_name).substitute(prefix=self.prefix)
            objs = self.getLine(pref)
            self.lines[pref] = objs
            self.filtered_lines.add(pref)
            lwid = QWidget()
            hlay = QHBoxLayout(lwid)
            hlay.setContentsMargins(0, 0, 0, 0)
            for prop, obj in objs:
                name = obj.objectName()
                obj.setStyleSheet("""
                    #{0:s}{{
                        min-width:{1:.1f}em; max-width: {1:.1f}em;
                    }}""".format(name, self._MIN_WIDs[prop]))
                hlay.addWidget(obj)
            lay.addWidget(lwid, alignment=Qt.AlignLeft)

    def getLine(self, device=None, header=False):
        objects = list()
        for prop in self._ALL_PROPS:
            widget = self.getColumn(device, prop, header)
            if widget is not None:
                objects.append([prop, widget])
        return objects

    def getColumn(self, device, prop, header):
        widget = QWidget(self)
        widget.setObjectName(prop)
        widget.setVisible(prop in self.props)
        widget.setSizePolicy(QSzPol.Fixed, QSzPol.Fixed)
        lay = QVBoxLayout(widget)
        lay.setSpacing(6)
        lay.setContentsMargins(0, 6, 0, 6)
        lay.setAlignment(Qt.AlignCenter)
        fun = self._createObjs if not header else self._headerLabel
        for obj in fun(device, prop):
            lay.addWidget(obj)
            obj.setSizePolicy(QSzPol.MinimumExpanding, QSzPol.Maximum)
        return widget

    def filter_columns(self):
        txt = self.sender().text()
        visi = self.sender().isChecked()
        objs = self.findChildren(QWidget, txt)
        for obj in objs:
            objname = obj.objectName()
            if objname.startswith(txt):
                obj.setVisible(visi)

    def filter_lines(self, text):
        """Filter lines according to the regexp filter."""
        text = self.search_lineedit.text()
        try:
            pattern = re.compile(text, re.I)
        except Exception:
            return

        self.filtered_lines.clear()
        for line, objs in self.lines.items():
            keep = False
            for prop, obj in objs:
                if keep:
                    self.filtered_lines.add(line)
                    break
                if prop not in self.props2search:
                    continue
                cnt = obj.layout().count()
                wid = obj.layout().itemAt(cnt - 1).widget()
                if hasattr(wid, 'text'):
                    keep |= bool(pattern.search(wid.text()))
                    continue
                elif hasattr(wid, 'enum_strings') and hasattr(wid, 'value'):
                    conds = wid.enum_strings is not None
                    if conds:
                        conds &= isinstance(wid.value, int)
                        conds &= wid.value < len(wid.enum_strings)
                    if conds:
                        enum = wid.enum_strings[wid.value]
                        keep |= bool(pattern.search(enum))
                        continue
        self._set_lines_visibility()

    def _set_lines_visibility(self):
        props = {a.text() for a in self.search_menu.actions() if a.isChecked()}
        for key, objs in self.lines.items():
            if key in self.filtered_lines:
                for _, wid in objs:
                    wid.setVisible(wid.objectName() in props)
            else:
                for _, wid in objs:
                    wid.setVisible(False)

    def _headerLabel(self, device, prop):
        lbl = QLabel('<h4>' + self._LABELS[prop] + '</h4>', self)
        lbl.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
        return (lbl, )

    def _createObjs(self, device, prop):
        return tuple()  # return tuple of widgets
Exemplo n.º 5
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)
Exemplo n.º 6
0
class EnvironmentsTab(WidgetBase):
    """
    This tab holds the list of named and application environments in the local
    machine.

    Available options include, `create`, `clone` and `remove` and package
    management.
    """
    BLACKLIST = ['anaconda-navigator']  # Do not show in package manager.

    sig_status_updated = Signal(object, object, object, object)

    def __init__(self, parent=None):
        super(EnvironmentsTab, self).__init__(parent)

        self.api = AnacondaAPI()
        self.last_env_prefix = None
        self.last_env_name = None
        self.previous_environments = None
        self.tracker = GATracker()
        self.metadata = {}

        active_channels = CONF.get('main',  'conda_active_channels', tuple())
        channels = CONF.get('main',  'conda_channels', tuple())
        conda_url = CONF.get('main',  'conda_url',
                             'https:/conda.anaconda.org')
        conda_api_url = CONF.get('main',  'anaconda_api_url',
                                 'https://api.anaconda.org')

        # Widgets
        self.button_clone = ButtonEnvironmentPrimary("Clone")
        self.button_create = ButtonEnvironmentPrimary("Create")
        self.button_remove = ButtonEnvironmentCancel("Remove")
        self.frame_environments = FrameEnvironments(self)
        self.frame_environments_list = FrameEnvironmentsList(self)
        self.frame_environments_list_buttons = FrameEnvironmentsListButtons(self)
        self.frame_environments_packages = FrameEnvironmentsPackages(self)
        self.list_environments = ListWidgetEnvironment()
        self.packages_widget = CondaPackagesWidget(
            self,
            setup=False,
            active_channels=active_channels,
            channels=channels,
            data_directory=CHANNELS_PATH,
            conda_api_url=conda_api_url,
            conda_url=conda_url)
        self.menu_list = QMenu()
        self.text_search = LineEditSearch()
        self.timer_environments = QTimer()

        # Widgets setup
        self.list_environments.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.list_environments.setContextMenuPolicy(Qt.CustomContextMenu)
        self.packages_widget.textbox_search.setAttribute(
            Qt.WA_MacShowFocusRect, False)
        self.packages_widget.textbox_search.set_icon_visibility(False)
        self.text_search.setPlaceholderText("Search Environments")
        self.text_search.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.timer_environments.setInterval(5000)

        # Layouts
        environments_layout = QVBoxLayout()
        environments_layout.addWidget(self.text_search)

        buttons_layout = QHBoxLayout()
        buttons_layout.addWidget(self.button_create)
        buttons_layout.addWidget(self.button_clone)
        buttons_layout.addWidget(self.button_remove)
        buttons_layout.setContentsMargins(0, 0, 0, 0)

        list_buttons_layout = QVBoxLayout()
        list_buttons_layout.addWidget(self.list_environments)
        list_buttons_layout.addLayout(buttons_layout)
        self.frame_environments_list_buttons.setLayout(list_buttons_layout)
        list_buttons_layout.setContentsMargins(0, 0, 0, 0)
        environments_layout.addWidget(self.frame_environments_list_buttons)

        self.frame_environments_list.setLayout(environments_layout)

        packages_layout = QHBoxLayout()
        packages_layout.addWidget(self.packages_widget)
        packages_layout.setContentsMargins(0, 0, 0, 0)
        self.frame_environments_packages.setLayout(packages_layout)

        main_layout = QHBoxLayout()
        main_layout.addWidget(self.frame_environments_list, 1)
        main_layout.addWidget(self.frame_environments_packages, 3)
        main_layout.setContentsMargins(0, 0, 0, 0)
        self.frame_environments.setLayout(main_layout)

        layout = QHBoxLayout()
        layout.addWidget(self.frame_environments)
        self.setLayout(layout)

        # Signals
        self.button_clone.clicked.connect(self.clone_environment)
        self.button_create.clicked.connect(self.create_environment)
        self.button_remove.clicked.connect(self.remove_environment)
        self.list_environments.sig_item_selected.connect(
            self.load_environment)
        self.packages_widget.sig_packages_ready.connect(self.refresh)
        self.packages_widget.sig_channels_updated.connect(self.update_channels)
#        self.packages_widget.sig_environment_cloned.connect(
#            self._environment_created)
#        self.packages_widget.sig_environment_created.connect(
#            self._environment_created)
#        self.packages_widget.sig_environment_removed.connect(
#            self._environment_removed)
        self.text_search.textChanged.connect(self.filter_environments)
        self.timer_environments.timeout.connect(self.refresh_environments)
        self.packages_widget.sig_process_cancelled.connect(
            lambda: self.update_visibility(True))

    # --- Helpers
    # -------------------------------------------------------------------------
    def update_visibility(self, enabled=True):
        self.button_create.setDisabled(not enabled)
        self.button_remove.setDisabled(not enabled)
        self.button_clone.setDisabled(not enabled)
        self.list_environments.setDisabled(not enabled)
        update_pointer()

    def update_style_sheet(self, style_sheet=None):
        if style_sheet is None:
            style_sheet = load_style_sheet()

        self.setStyleSheet(style_sheet)
        self.menu_list.setStyleSheet(style_sheet)
        self.list_environments.setFrameStyle(QFrame.NoFrame)
        self.list_environments.setFrameShape(QFrame.NoFrame)
        self.packages_widget.table.setFrameStyle(QFrame.NoFrame)
        self.packages_widget.table.setFrameShape(QFrame.NoFrame)
        self.packages_widget.layout().setContentsMargins(0, 0, 0, 0)

        size = QSize(16, 16)

        palette = {
            'icon.action.not_installed': QIcon(images.CONDA_MANAGER_NOT_INSTALLED).pixmap(size),
            'icon.action.installed': QIcon(images.CONDA_MANAGER_INSTALLED).pixmap(size),
            'icon.action.remove': QIcon(images.CONDA_MANAGER_REMOVE).pixmap(size),
            'icon.action.add': QIcon(images.CONDA_MANAGER_ADD).pixmap(size),
            'icon.action.upgrade': QIcon(images.CONDA_MANAGER_UPGRADE).pixmap(size),
            'icon.action.downgrade': QIcon(images.CONDA_MANAGER_DOWNGRADE).pixmap(size),
            'icon.upgrade.arrow': QIcon(images.CONDA_MANAGER_UPGRADE_ARROW).pixmap(size),
            'background.remove': QColor(0, 0, 0, 0),
            'background.install': QColor(0, 0, 0, 0),
            'background.upgrade': QColor(0, 0, 0, 0),
            'background.downgrade': QColor(0, 0, 0, 0),
            'foreground.not.installed': QColor("#666"),
            'foreground.upgrade': QColor("#0071a0"),
            }

        self.packages_widget.update_style_sheet(
            style_sheet=style_sheet,
            extra_dialogs={'cancel_dialog': ClosePackageManagerDialog,
                           'apply_actions_dialog': ActionsDialog,
                           'message_box_error': MessageBoxError,
                           },
            palette=palette,
            )

    def get_environments(self):
        """
        Return an ordered dictionary of all existing named environments as
        keys and the prefix as items.

        The dictionary includes the root environment as the first entry.
        """
        environments = OrderedDict()
        environments_prefix = sorted(self.api.conda_get_envs())
        environments['root'] = self.api.ROOT_PREFIX

        for prefix in environments_prefix:
            name = os.path.basename(prefix)
            environments[name] = prefix

        return environments

    def refresh_environments(self):
        """
        Check every `timer_refresh_envs` amount of miliseconds for newly
        created environments and update the list if new ones are found.
        """
        environments = self.get_environments()
        if self.previous_environments is None:
            self.previous_environments = environments.copy()

        if self.previous_environments != environments:
            self.previous_environments = environments.copy()
            self.setup_tab()

    def open_environment_in(self, which):
        environment_prefix = self.list_environments.currentItem().prefix()
        environment_name = self.list_environments.currentItem().text()
        logger.debug("%s, %s", which, environment_prefix)

        if environment_name == 'root':
            environment_prefix = None

        if which == 'terminal':
            launch.console(environment_prefix)
        else:
            launch.py_in_console(environment_prefix, which)

    def set_last_active_prefix(self):
        current_item = self.list_environments.currentItem()
        if current_item:
            self.last_env_prefix = getattr(current_item, '_prefix')
        else:
            self.last_env_prefix = self.api.ROOT_PREFIX
        CONF.set('main', 'last_active_prefix', self.last_env_prefix)

    def setup_tab(self, metadata={}, load_environment=True):
        if metadata:
            self.metadata = metadata

        # show_apps = CONF.get('main', 'show_application_environments')
        envs = self.get_environments()
        self.timer_environments.start()
        self.menu_list.clear()
        menu_item = self.menu_list.addAction('Open Terminal')
        menu_item.triggered.connect(
            lambda: self.open_environment_in('terminal'))

        for word in ['Python', 'IPython', 'Jupyter Notebook']:
            menu_item = self.menu_list.addAction("Open with " + word)
            menu_item.triggered.connect(
                lambda x, w=word: self.open_environment_in(w.lower()))

        def select(value=None, position=None):
            current_item = self.list_environments.currentItem()
            prefix = current_item.prefix()

            if isinstance(position, bool) or position is None:
                width = current_item.button_options.width()
                position = QPoint(width, 0)

#            parent_position = self.list_environments.mapToGlobal(QPoint(0, 0))
            point = QPoint(0, 0)
            parent_position = current_item.button_options.mapToGlobal(point)
            self.menu_list.move(parent_position + position)
            self.menu_list.actions()[2].setEnabled(
                launch.check_prog('ipython', prefix))
            self.menu_list.actions()[3].setEnabled(
                launch.check_prog('notebook', prefix))
            self.menu_list.exec_()

        self.set_last_active_prefix()
        self.list_environments.clear()

#        if show_apps:
#            separator_item = ListItemSeparator('My environments:')
#            self.list_environments.addItem(separator_item)

        for env in envs:
            prefix = envs[env]
            item = ListItemEnvironment(env, prefix=prefix)
            item.button_options.clicked.connect(select)
            self.list_environments.addItem(item)

#        if show_apps:
#            application_envs = self.api.get_application_environments()
#            separator_item = ListItemSeparator('Application environments:')
#            self.list_environments.addItem(separator_item)
#            for app in application_envs:
#                env_prefix = application_envs[app]
#                item = ListItemEnvironment(name=app, prefix=env_prefix)
#                item.button_options.clicked.connect(select)
#                self.list_environments.addItem(item)

        if load_environment:
            self.load_environment()
        else:
            return

        # Adjust Tab Order
        self.setTabOrder(self.text_search,
                         self.list_environments._items[0].widget)
        for i in range(len(self.list_environments._items) - 1):
            self.setTabOrder(self.list_environments._items[i].widget,
                             self.list_environments._items[i+1].widget)
        self.setTabOrder(self.list_environments._items[-1].button_name,
                         self.button_create)
        self.setTabOrder(self.button_create, self.button_clone)
        self.setTabOrder(self.button_clone, self.button_remove)
        self.setTabOrder(self.button_remove,
                         self.packages_widget.combobox_filter)
        self.setTabOrder(self.packages_widget.combobox_filter,
                         self.packages_widget.button_channels)
        self.setTabOrder(self.packages_widget.button_channels,
                         self.packages_widget.button_update)
        self.setTabOrder(self.packages_widget.button_update,
                         self.packages_widget.textbox_search)
        self.setTabOrder(self.packages_widget.textbox_search,
                         self.packages_widget.table_first_row)
        self.setTabOrder(self.packages_widget.table_last_row,
                         self.packages_widget.button_apply)
        self.setTabOrder(self.packages_widget.button_apply,
                         self.packages_widget.button_clear)
        self.setTabOrder(self.packages_widget.button_clear,
                         self.packages_widget.button_cancel)

    def filter_environments(self):
        """
        Filter displayed environments by matching search text.
        """
        text = self.text_search.text().lower()

        for i in range(self.list_environments.count()):
            item = self.list_environments.item(i)
            item.setHidden(text not in item.text().lower())

            if not item.widget.isVisible():
                item.widget.repaint()

    def load_environment(self, item=None):
        self.update_visibility(False)
        if item is None:
            item = self.list_environments.currentItem()

        if item is None or not isinstance(item, ListItemEnvironment):
            prefix = self.api.ROOT_PREFIX
            index = 0
        elif item and isinstance(item, ListItemEnvironment):
            prefix = item.prefix()
        else:
            prefix = self.last_env_prefix if self.last_env_prefix else None

        index = [i for i, it in enumerate(self.list_environments._items)
                 if prefix in it.prefix()]
        index = index[0] if len(index) else 0

        self.list_environments.setCurrentRow(index)
        self.packages_widget.set_environment(prefix=prefix)
        self.packages_widget.setup(check_updates=False,
                                   blacklist=self.BLACKLIST,
                                   metadata=self.metadata)
        self.list_environments.setDisabled(True)
        self.update_visibility(False)
        self.set_last_active_prefix()
#        update_pointer(Qt.BusyCursor)

    def refresh(self):
        self.update_visibility(True)
        self.list_environments.setDisabled(False)
        item = self.list_environments.currentItem()

        try:
            item.set_loading(False)
        except RuntimeError:
            pass
            # C/C++ object not found

        is_root = item.text() == 'root'

        self.button_remove.setDisabled(is_root)
        self.button_clone.setDisabled(is_root)

    def update_channels(self, channels, active_channels):
        """
        Save updated channels to the CONF.
        """
        CONF.set('main', 'conda_active_channels', active_channels)
        CONF.set('main', 'conda_channels', channels)

    # --- Callbacks
    # -------------------------------------------------------------------------
    def _environment_created(self, worker, output, error):
        if error:
            logger.error(str(error))

        self.update_visibility(False)
        for row, environment in enumerate(self.get_environments()):
            if worker.name == environment:
                break

        self.last_env_prefix = self.api.conda_get_prefix_envname(environment)
        self.setup_tab(load_environment=False)
        self.list_environments.setCurrentRow(row)
        self.load_environment()
        self.refresh()
        self.update_visibility(True)
        update_pointer()

    def _environment_removed(self, worker, output, error):
        self.update_visibility(True)
        if error:
            logger.error(str(error))

        self.setup_tab()
        self.list_environments.setCurrentRow(0)

    # --- Public API
    # -------------------------------------------------------------------------
    def update_domains(self, anaconda_api_url, conda_url):
        self.packages_widget.update_domains(
            anaconda_api_url=anaconda_api_url,
            conda_url=conda_url,
            )

    def create_environment(self):
        """
        Create new basic environment with selectable python version.

        Actually makes new env on disc, in directory within the project
        whose name depends on the env name. New project state is saved.
        Should also sync to spec file.
        """
        dlg = CreateEnvironmentDialog(parent=self,
                                      environments=self.get_environments())
        self.tracker.track_page('/environments/create',
                                pagetitle='Create new environment dialog')

        if dlg.exec_():
            name = dlg.text_name.text().strip()
            pyver = dlg.combo_version.currentText()

            if name:
                logger.debug(str('{0}, {1}'.format(name, pyver)))

                self.update_visibility(False)
                update_pointer(Qt.BusyCursor)

                if pyver:
                    pkgs = ['python=' + pyver, 'jupyter']
                else:
                    pkgs = ['jupyter']

                channels = self.packages_widget._active_channels
                logger.debug(str((name, pkgs, channels)))
                self.update_visibility(False)
                worker = self.packages_widget.create_environment(name=name, 
                                                                 packages=pkgs)
#                worker = self.api.conda_create(name=name, pkgs=pkgs,
#                                               channels=channels)
                worker.name = name
                worker.sig_finished.connect(self._environment_created)
        self.tracker.track_page('/environments')

    def remove_environment(self):
        """
        Clone currently selected environment.
        """
        current_item = self.list_environments.currentItem()
        if current_item is not None:
            name = current_item.text()

            if name == 'root':
                return

            dlg = RemoveEnvironmentDialog(environment=name)
            self.tracker.track_page('/environments/remove',
                                    pagetitle='Remove environment dialog')
            if dlg.exec_():
                logger.debug(str(name))
                self.update_visibility(False)
                update_pointer(Qt.BusyCursor)
                worker = self.packages_widget.remove_environment(name=name)
#                worker = self.api.conda_remove(name=name, all_=True)
                worker.sig_finished.connect(self._environment_removed)
#                self.sig_status_updated.emit('Deleting environment '
#                                             '"{0}"'.format(name),
#                                             0, -1, -1)
            self.tracker.track_page('/environments')

    def clone_environment(self):
        """
        Clone currently selected environment.
        """
        current_item = self.list_environments.currentItem()
        if current_item is not None:
            current_name = current_item.text()
            dlg = CloneEnvironmentDialog(parent=self,
                                         environments=self.get_environments())
            self.tracker.track_page('/environments/clone',
                                    pagetitle='Clone environment dialog')

            if dlg.exec_():
                name = dlg.text_name.text().strip()

                if name and current_name:
                    logger.debug(str("{0}, {1}".format(current_name, name)))

                    self.update_visibility(False)
                    update_pointer(Qt.BusyCursor)
                    worker = self.packages_widget.clone_environment(clone=current_name,
                                                                    name=name)
#                    worker = self.api.conda_clone(current_name, name=name)
                    worker.name = name
                    worker.sig_finished.connect(self._environment_created)
            self.tracker.track_page('/environments')

    def import_environment(self):
        """
Exemplo n.º 7
0
class EnvironmentsTab(WidgetBase):
    """Conda environments tab."""
    BLACKLIST = ['anaconda-navigator', '_license']  # Hide in package manager

    # --- Signals
    # -------------------------------------------------------------------------
    sig_ready = Signal()

    # name, prefix, sender
    sig_item_selected = Signal(object, object, object)

    # sender, func_after_dlg_accept, func_callback_on_finished
    sig_create_requested = Signal()
    sig_clone_requested = Signal()
    sig_import_requested = Signal()
    sig_remove_requested = Signal()

    # button_widget, sender_constant
    sig_channels_requested = Signal(object, object)

    # sender_constant
    sig_update_index_requested = Signal(object)
    sig_cancel_requested = Signal(object)

    # conda_packages_action_dict, pip_packages_action_dict
    sig_packages_action_requested = Signal(object, object)

    def __init__(self, parent=None):
        """Conda environments tab."""
        super(EnvironmentsTab, self).__init__(parent)

        # Variables
        self.api = AnacondaAPI()
        self.current_prefix = None
        self.style_sheet = None

        # Widgets
        self.frame_header_left = FrameTabHeader()
        self.frame_list = FrameEnvironmentsList(self)
        self.frame_widget = FrameEnvironmentsPackages(self)
        self.text_search = LineEditSearch()
        self.list = ListWidgetEnv()
        self.menu_list = QMenu()
        self.button_create = ButtonToolNormal(text="Create")
        self.button_clone = ButtonToolNormal(text="Clone")
        self.button_import = ButtonToolNormal(text="Import")
        self.button_remove = ButtonToolNormal(text="Remove")
        self.button_toggle_collapse = ButtonToggleCollapse()
        self.widget = CondaPackagesWidget(parent=self)

        # Widgets setup
        self.frame_list.is_expanded = True
        self.text_search.setPlaceholderText("Search Environments")
        self.list.setContextMenuPolicy(Qt.CustomContextMenu)
        self.button_create.setObjectName("create")  # Needed for QSS selectors
        self.button_clone.setObjectName("clone")
        self.button_import.setObjectName("import")
        self.button_remove.setObjectName("remove")
        self.widget.textbox_search.set_icon_visibility(False)

        # Layouts
        layout_header_left = QVBoxLayout()
        layout_header_left.addWidget(self.text_search)
        self.frame_header_left.setLayout(layout_header_left)

        layout_buttons = QHBoxLayout()
        layout_buttons.addWidget(self.button_create)
        layout_buttons.addWidget(self.button_clone)
        layout_buttons.addWidget(self.button_import)
        layout_buttons.addWidget(self.button_remove)

        layout_list_buttons = QVBoxLayout()
        layout_list_buttons.addWidget(self.frame_header_left)
        layout_list_buttons.addWidget(self.list)
        layout_list_buttons.addLayout(layout_buttons)
        self.frame_list.setLayout(layout_list_buttons)

        layout_widget = QHBoxLayout()
        layout_widget.addWidget(self.widget)
        self.frame_widget.setLayout(layout_widget)

        layout_main = QHBoxLayout()
        layout_main.addWidget(self.frame_list, 10)
        layout_main.addWidget(self.button_toggle_collapse, 1)
        layout_main.addWidget(self.frame_widget, 30)

        self.setLayout(layout_main)

        # Signals for buttons and boxes
        self.button_toggle_collapse.clicked.connect(self.expand_collapse)
        self.button_create.clicked.connect(self.sig_create_requested)
        self.button_clone.clicked.connect(self.sig_clone_requested)
        self.button_import.clicked.connect(self.sig_import_requested)
        self.button_remove.clicked.connect(self.sig_remove_requested)
        self.text_search.textChanged.connect(self.filter_list)

        # Signals for list
        self.list.sig_item_selected.connect(self._item_selected)

        # Signals for packages widget
        self.widget.sig_ready.connect(self.sig_ready)
        self.widget.sig_channels_requested.connect(self.sig_channels_requested)
        self.widget.sig_update_index_requested.connect(
            self.sig_update_index_requested)
        self.widget.sig_cancel_requested.connect(self.sig_cancel_requested)
        self.widget.sig_packages_action_requested.connect(
            self.sig_packages_action_requested)

    # --- Setup methods
    # -------------------------------------------------------------------------
    def setup(self, conda_data):
        """Setup tab content and populates the list of environments."""
        self.set_widgets_enabled(False)
        conda_processed_info = conda_data.get('processed_info')
        environments = conda_processed_info.get('__environments')
        packages = conda_data.get('packages')
        self.current_prefix = conda_processed_info.get('default_prefix')
        self.set_environments(environments)
        self.set_packages(packages)

    def set_environments(self, environments):
        """Populate the list of environments."""
        self.list.clear()
        selected_item_row = 0
        for i, (env_prefix, env_name) in enumerate(environments.items()):
            item = ListItemEnv(prefix=env_prefix, name=env_name)
            item.button_options.clicked.connect(self.show_environment_menu)
            if env_prefix == self.current_prefix:
                selected_item_row = i
            self.list.addItem(item)

        self.list.setCurrentRow(selected_item_row, loading=True)
        self.filter_list()

    def _set_packages(self, worker, output, error):
        """Set packages callback."""
        packages, model_data = output
        self.widget.setup(packages, model_data)
        self.set_widgets_enabled(True)
        self.set_loading(prefix=self.current_prefix, value=False)

    def set_packages(self, packages):
        """Set packages widget content."""
        worker = self.api.process_packages(packages,
                                           prefix=self.current_prefix,
                                           blacklist=self.BLACKLIST)
        worker.sig_chain_finished.connect(self._set_packages)

    def show_environment_menu(self, value=None, position=None):
        """Show the environment actions menu."""
        self.menu_list.clear()
        menu_item = self.menu_list.addAction('Open Terminal')
        menu_item.triggered.connect(
            lambda: self.open_environment_in('terminal'))

        for word in ['Python', 'IPython', 'Jupyter Notebook']:
            menu_item = self.menu_list.addAction("Open with " + word)
            menu_item.triggered.connect(
                lambda x, w=word: self.open_environment_in(w.lower()))

        current_item = self.list.currentItem()
        prefix = current_item.prefix

        if isinstance(position, bool) or position is None:
            width = current_item.button_options.width()
            position = QPoint(width, 0)

        point = QPoint(0, 0)
        parent_position = current_item.button_options.mapToGlobal(point)
        self.menu_list.move(parent_position + position)

        # Disabled actions depending on the environment installed packages
        actions = self.menu_list.actions()
        actions[2].setEnabled(launch.check_prog('ipython', prefix))
        actions[3].setEnabled(launch.check_prog('notebook', prefix))

        self.menu_list.exec_()

    def open_environment_in(self, which):
        """Open selected environment in console terminal."""
        prefix = self.list.currentItem().prefix
        logger.debug("%s, %s", which, prefix)

        if which == 'terminal':
            launch.console(prefix)
        else:
            launch.py_in_console(prefix, which)

    # --- Common Helpers (# FIXME: factor out to common base widget)
    # -------------------------------------------------------------------------
    def _item_selected(self, item):
        """Callback to emit signal as user selects an item from the list."""
        self.set_loading(prefix=item.prefix)
        self.sig_item_selected.emit(item.name, item.prefix, C.TAB_ENVIRONMENT)

    def add_temporal_item(self, name):
        """Creates a temporal item on list while creation becomes effective."""
        item_names = [item.name for item in self.list.items()]
        item_names.append(name)
        index = list(sorted(item_names)).index(name) + 1
        item = ListItemEnv(name=name)
        self.list.insertItem(index, item)
        self.list.setCurrentRow(index)
        self.list.scrollToItem(item)
        item.set_loading(True)

    def expand_collapse(self):
        """Expand or collapse the list selector."""
        if self.frame_list.is_expanded:
            self.frame_list.hide()
            self.frame_list.is_expanded = False
        else:
            self.frame_list.show()
            self.frame_list.is_expanded = True

    def filter_list(self, text=None):
        """Filter items in list by name."""
        text = self.text_search.text().lower()
        for i in range(self.list.count()):
            item = self.list.item(i)
            item.setHidden(text not in item.name.lower())

            if not item.widget.isVisible():
                item.widget.repaint()

    def ordered_widgets(self, next_widget=None):
        """Return a list of the ordered widgets."""
        if next_widget is not None:
            self.widget.table_last_row.add_focus_widget(next_widget)

        ordered_widgets = [
            self.text_search,
        ]
        ordered_widgets += self.list.ordered_widgets()
        ordered_widgets += [
            self.button_create,
            self.button_clone,
            self.button_import,
            self.button_remove,
            self.widget.combobox_filter,
            self.widget.button_channels,
            self.widget.button_update,
            self.widget.textbox_search,
            # self.widget.table_first_row,
            self.widget.table,
            self.widget.table_last_row,
            self.widget.button_apply,
            self.widget.button_clear,
            self.widget.button_cancel,
        ]
        return ordered_widgets

    def refresh(self):
        """Refresh the enabled/disabled status of the widget and subwidgets."""
        is_root = self.current_prefix == self.api.ROOT_PREFIX
        self.button_clone.setDisabled(is_root)
        self.button_remove.setDisabled(is_root)

    def set_loading(self, prefix=None, value=True):
        """Set the item given by `prefix` to loading state."""
        for row, item in enumerate(self.list.items()):
            if item.prefix == prefix:
                item.set_loading(value)
                self.list.setCurrentRow(row)
                break

    def set_widgets_enabled(self, value):
        """Change the enabled status of widgets and subwidgets."""
        self.list.setEnabled(value)
        self.button_create.setEnabled(value)
        self.button_clone.setEnabled(value)
        self.button_import.setEnabled(value)
        self.button_remove.setEnabled(value)
        self.widget.set_widgets_enabled(value)
        if value:
            self.refresh()

    def update_status(self, action='', message='', value=None, max_value=None):
        """Update widget status and progress bar."""
        self.widget.update_status(action=action,
                                  message=message,
                                  value=value,
                                  max_value=max_value)

    def update_style_sheet(self, style_sheet=None):
        """Update custom CSS stylesheet."""
        if style_sheet is None:
            self.style_sheet = load_style_sheet()
        else:
            self.style_sheet = style_sheet

        self.setStyleSheet(self.style_sheet)
        self.list.update_style_sheet(self.style_sheet)
        self.menu_list.setStyleSheet(self.style_sheet)
Exemplo n.º 8
0
class GcodeTextEdit(QPlainTextEdit):
    """G-code Text Edit

    QPlainTextEdit based G-code editor with syntax heightening.
    """
    focusLine = Signal(int)

    def __init__(self, parent=None):
        super(GcodeTextEdit, self).__init__(parent)

        self.setCenterOnScroll(True)
        self.setGeometry(50, 50, 800, 640)
        self.setWordWrapMode(QTextOption.NoWrap)

        self.block_number = None
        self.focused_line = 1
        self.current_line_background = QColor(self.palette().alternateBase())

        self.old_docs = []
        # set the custom margin
        self.margin = NumberMargin(self)

        # set the syntax highlighter
        self.gCodeHighlighter = GcodeSyntaxHighlighter(self)

        # context menu
        self.menu = QMenu(self)
        self.menu.addAction(
            self.tr("Run from line {}".format(self.focused_line)),
            self.runFromHere)
        self.menu.addSeparator()
        self.menu.addAction(self.tr('Cut'), self.cut)
        self.menu.addAction(self.tr('Copy'), self.copy)
        self.menu.addAction(self.tr('Paste'), self.paste)

        # FixMe: Picks the first action run from here, should not be by index
        self.run_action = self.menu.actions()[0]
        self.run_action.setEnabled(program_actions.run_from_line.ok())
        program_actions.run_from_line.bindOk(self.run_action)

        # connect signals
        self.cursorPositionChanged.connect(self.onCursorChanged)

        # connect status signals
        STATUS.file.notify(self.loadProgramFile)
        STATUS.motion_line.onValueChanged(self.setCurrentLine)

    def keyPressEvent(self, event):
        # keep the cursor centered
        if event.key() == Qt.Key_Up:
            self.moveCursor(QTextCursor.Up)
            self.centerCursor()

        elif event.key() == Qt.Key_Down:
            self.moveCursor(QTextCursor.Down)
            self.centerCursor()

        else:
            super(GcodeTextEdit, self).keyPressEvent(event)

    def changeEvent(self, event):
        if event.type() == QEvent.FontChange:
            # Update syntax highlighter with new font
            self.gCodeHighlighter = GcodeSyntaxHighlighter(self)
        super(GcodeTextEdit, self).changeEvent(event)

    def setPlainText(self, p_str):
        # FixMe: Keep a reference to old QTextDocuments form previously loaded
        # files. This is needed to prevent garbage collection which results in a
        # seg fault if the document is discarded while still being highlighted.
        self.old_docs.append(self.document())

        doc = QTextDocument()
        doc.setDocumentLayout(QPlainTextDocumentLayout(doc))
        doc.setPlainText(p_str)

        self.setDocument(doc)
        self.margin.updateWidth()

        # start syntax heightening
        self.gCodeHighlighter = GcodeSyntaxHighlighter(self)

    @Slot(bool)
    def EditorReadOnly(self, state):
        """Set to Read Only to disable editing"""
        if state:
            self.setReadOnly(True)
        else:
            self.setReadOnly(False)

    @Property(QColor)
    def currentLineBackground(self):
        return self.current_line_background

    @currentLineBackground.setter
    def currentLineBackground(self, color):
        self.current_line_background = color
        # Hack to get background to update
        self.setCurrentLine(2)
        self.setCurrentLine(1)

    @Property(QColor)
    def marginBackground(self):
        return self.margin.background

    @marginBackground.setter
    def marginBackground(self, color):
        self.margin.background = color
        self.margin.update()

    @Property(QColor)
    def marginCurrentLineBackground(self):
        return self.margin.highlight_background

    @marginCurrentLineBackground.setter
    def marginCurrentLineBackground(self, color):
        self.margin.highlight_background = color
        self.margin.update()

    @Property(QColor)
    def marginColor(self):
        return self.margin.color

    @marginColor.setter
    def marginColor(self, color):
        self.margin.color = color
        self.margin.update()

    @Property(QColor)
    def marginCurrentLineColor(self):
        return self.margin.highlight_color

    @marginCurrentLineColor.setter
    def marginCurrentLineColor(self, color):
        self.margin.highlight_color = color
        self.margin.update()

    @Slot(str)
    @Slot(object)
    def loadProgramFile(self, fname=None):
        if fname:
            with open(fname) as f:
                gcode = f.read()
            self.setPlainText(gcode)

    @Slot(int)
    @Slot(object)
    def setCurrentLine(self, line):
        cursor = QTextCursor(self.document().findBlockByLineNumber(line - 1))
        self.setTextCursor(cursor)
        self.centerCursor()

    def getCurrentLine(self):
        return self.textCursor().blockNumber() + 1

    def onCursorChanged(self):
        # highlights current line, find a way not to use QTextEdit
        block_number = self.textCursor().blockNumber()
        if block_number != self.block_number:
            self.block_number = block_number
            selection = QTextEdit.ExtraSelection()
            selection.format.setBackground(self.current_line_background)
            selection.format.setProperty(QTextFormat.FullWidthSelection, True)
            selection.cursor = self.textCursor()
            selection.cursor.clearSelection()
            self.setExtraSelections([selection])

        # emit signals for backplot etc.
        self.focused_line = block_number + 1
        self.focusLine.emit(self.focused_line)

    def contextMenuEvent(self, event):
        self.run_action.setText("Run from line {}".format(self.focused_line))
        self.menu.popup(event.globalPos())
        event.accept()

    def runFromHere(self, *args, **kwargs):
        line = self.getCurrentLine()
        program_actions.run(line)

    def resizeEvent(self, *e):
        cr = self.contentsRect()
        rec = QRect(cr.left(), cr.top(), self.margin.getWidth(), cr.height())
        self.margin.setGeometry(rec)
        QPlainTextEdit.resizeEvent(self, *e)
Exemplo n.º 9
0
    def _context_menu(self, point):
        '''
        Opens a context menu generated from the user settings.
        '''
        menu_item_settings = self._settings['context_menu']

        # Don't do anything if there are no defined menu items.
        if len(menu_item_settings) == 0:
            return

        # Get all the selected file paths.
        selected_items = [self._model.filePath(index) for index in self._view.selectedIndexes()]

        # Get the current directory the user is in.
        current_directory = self.current_directory()

        # Create the menu.
        menu = QMenu(self)
        for menu_item_setting in menu_item_settings:
            command = menu_item_setting['command']

            # Get the highest field number in the command string, as well as a set of field names.
            field_names = set()
            highest_field_number = None
            for parse_record in string.Formatter().parse(command):
                field_name = parse_record[1]
                if field_name is None:
                    continue

                field_names.add(field_name)

                try:
                    field_number = int(field_name)
                except ValueError:
                    pass
                else:
                    if highest_field_number is None or field_number > highest_field_number:
                        highest_field_number = field_number

            # If field numbers were used, then the menu item will be disabled if the number of
            # selected items does not equal the highest field number + 1.
            enabled = True
            if highest_field_number is not None and len(selected_items) != highest_field_number + 1:
                enabled = False

            # Disable the menu item if the command uses {selected}, but there is nothing selected.
            if enabled and 'selected' in field_names and len(selected_items) == 0:
                enabled = False

            # Disable the menu item if the command uses {current_directory}, but there is no
            # current directory.
            if enabled and 'current_directory' in field_names and current_directory is None:
                enabled = False

            # Disable the menu item if any of the selected items don't match at least one of the
            # given regex patterns. Note that if this is specified at least one item must be
            # selected.
            if enabled and 'require' in menu_item_setting:
                require_filters = regex_tools.FastListMatcher(menu_item_setting['require'])

                if not selected_items:
                    enabled = False
                else:
                    for path in selected_items:
                        if not require_filters.fullmatch(path):
                            enabled = False
                            break

            # Disable the menu item if any of the selected items matches any of the given regex
            # patterns
            if enabled and 'exclude' in menu_item_setting:
                exclude_filters = regex_tools.FastListMatcher(menu_item_setting['exclude'])

                for path in selected_items:
                    if exclude_filters.fullmatch(path):
                        enabled = False
                        break

            if not enabled:
                # Only create a menu item if it is not hidden.
                if menu_item_setting.get('show_if_disabled', False):
                    action = SubprocessAction(menu_item_setting['label'], self)
                    action.setEnabled(False)
                    menu.addAction(action)

                # No need to setup the action command if it is disabled.
                continue

            action = SubprocessAction(menu_item_setting['label'], self)
            menu.addAction(action)

            # Set the menu item command. The item will be disabled if there is a field in the
            # command string that is not supported.
            escaped_items = ['"{}"'.format(item) for item in selected_items]
            selected = ' '.join(escaped_items)
            current_directory = '"{}"'.format(current_directory)
            try:
                command = menu_item_setting['command'].format(
                    *escaped_items,
                    selected=selected,
                    current_directory=current_directory)
            except KeyError:
                action.setEnabled(False)
            else:
                action.command = command

        # Show the menu if it has entries.
        if len(menu.actions()) != 0:
            menu.popup(self._view.mapToGlobal(point))
Exemplo n.º 10
0
class BaseDeviceButton(QPushButton):
    """Base class for QPushButton to show devices"""
    _OPEN_ALL = "Open All"

    def __init__(self, title, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.title = title
        # References for created screens
        self._device_displays = {}
        self._suite = None
        # Setup Menu
        self.setContextMenuPolicy(Qt.PreventContextMenu)
        self.device_menu = QMenu()
        self.device_menu.aboutToShow.connect(self._menu_shown)

    def show_device(self, device):
        if device.name not in self._device_displays:
            widget = display_for_device(device)
            widget.setParent(self)
            self._device_displays[device.name] = widget
        return self._device_displays[device.name]

    def show_all(self):
        if len(self.devices) == 0:
            return None
        """Create a widget for contained devices"""
        if not self._suite:
            self._suite = suite_for_devices(self.devices,
                                            parent=self,
                                            pin=True)
        else:
            # Check that any devices that have been added since our last show
            # request have been added to the TyphosSuite
            for device in self.devices:
                if device not in self._suite.devices:
                    self._suite.add_device(device)
        return self._suite

    def _devices_shown(self, shown):
        """Implemeted by subclass"""
        pass

    def _menu_shown(self):
        # Current menu options
        menu_devices = [action.text() for action in self.device_menu.actions()]
        if self._OPEN_ALL not in menu_devices:
            show_all_devices = self._show_all_wrapper()
            self.device_menu.addAction(self._OPEN_ALL, show_all_devices)
            self.device_menu.addSeparator()
        # Add devices
        for device in self.devices:
            if device.name not in menu_devices:
                # Add to device menu
                show_device = self._show_device_wrapper(device)
                self.device_menu.addAction(device.name, show_device)

    def _show_all_wrapper(self):
        return lucid.LucidMainWindow.in_dock(self.show_all,
                                             title=self.title,
                                             active_slot=self._devices_shown)

    def _show_device_wrapper(self, device):
        return lucid.LucidMainWindow.in_dock(partial(self.show_device, device),
                                             title=device.name)

    def eventFilter(self, obj, event):
        """
        QWidget.eventFilter to be installed on child indicators

        This is required to display the :meth:`.contextMenuEvent` even if an
        indicator is pressed.
        """
        # Filter child widgets events to show context menu
        if event.type() == QEvent.MouseButtonPress:
            if event.button() == Qt.RightButton:
                self._show_all_wrapper()()
                return True
            elif event.button() == Qt.LeftButton:
                if len(self.devices) == 1:
                    self._show_device_wrapper(self.devices[0])()
                else:
                    self.device_menu.exec_(self.mapToGlobal(event.pos()))
                return True
        return False
Exemplo n.º 11
0
class GcodeTextEdit(QPlainTextEdit):
    """G-code Text Edit

    QPlainTextEdit based G-code editor with syntax heightening.
    """
    focusLine = Signal(int)

    def __init__(self, parent=None):
        super(GcodeTextEdit, self).__init__(parent)

        self.parent = parent

        self.setCenterOnScroll(True)
        self.setGeometry(50, 50, 800, 640)
        self.setWordWrapMode(QTextOption.NoWrap)

        self.block_number = None
        self.focused_line = 1
        self.current_line_background = QColor(self.palette().alternateBase())
        self.readonly = False
        self.syntax_highlighting = False

        self.old_docs = []
        # set the custom margin
        self.margin = NumberMargin(self)

        # set the syntax highlighter # Fixme un needed init here
        self.gCodeHighlighter = None

        if parent is not None:

            self.find_case = None
            self.find_words = None

            self.search_term = ""
            self.replace_term = ""

        # context menu
        self.menu = QMenu(self)
        self.menu.addAction(
            self.tr("Run from line {}".format(self.focused_line)),
            self.runFromHere)
        self.menu.addSeparator()
        self.menu.addAction(self.tr('Cut'), self.cut)
        self.menu.addAction(self.tr('Copy'), self.copy)
        self.menu.addAction(self.tr('Paste'), self.paste)
        self.menu.addAction(self.tr('Find'), self.findForward)
        self.menu.addAction(self.tr('Find All'), self.findAll)
        self.menu.addAction(self.tr('Replace'), self.replace)
        self.menu.addAction(self.tr('Replace All'), self.replace)

        # FixMe: Picks the first action run from here, should not be by index
        self.run_action = self.menu.actions()[0]
        self.run_action.setEnabled(program_actions.run_from_line.ok())
        program_actions.run_from_line.bindOk(self.run_action)

        self.dialog = FindReplaceDialog(parent=self)

        # connect signals
        self.cursorPositionChanged.connect(self.onCursorChanged)

        # connect status signals
        STATUS.file.notify(self.loadProgramFile)
        STATUS.motion_line.onValueChanged(self.setCurrentLine)

    @Slot(str)
    def set_search_term(self, text):
        LOG.debug(f"Set search term :{text}")
        self.search_term = text

    @Slot(str)
    def set_replace_term(self, text):
        LOG.debug(f"Set replace term :{text}")
        self.replace_term = text

    @Slot()
    def findDialog(self):
        LOG.debug("Show find dialog")
        self.dialog.show()

    @Slot(bool)
    def findCase(self, enabled):
        LOG.debug(f"Find case sensitive :{enabled}")
        self.find_case = enabled

    @Slot(bool)
    def findWords(self, enabled):
        LOG.debug(f"Find whole words :{enabled}")
        self.find_words = enabled

    def findAllText(self, text):
        flags = QTextDocument.FindFlag(0)

        if self.find_case:
            flags |= QTextDocument.FindCaseSensitively
        if self.find_words:
            flags |= QTextDocument.FindWholeWords

        searching = True
        cursor = self.textCursor()

        while searching:
            found = self.find(text, flags)
            if found:
                cursor = self.textCursor()
            else:
                searching = False

        if cursor.hasSelection():
            self.setTextCursor(cursor)

    def findForwardText(self, text):
        flags = QTextDocument.FindFlag()

        if self.find_case:
            flags |= QTextDocument.FindCaseSensitively
        if self.find_words:
            flags |= QTextDocument.FindWholeWords

        found = self.find(text, flags)

        # if found:
        #     cursor = self.document().find(text, flags)
        #     if cursor.position() > 0:
        #         self.setTextCursor(cursor)

    def findBackwardText(self, text):
        flags = QTextDocument.FindFlag()
        flags |= QTextDocument.FindBackward

        if self.find_case:
            flags |= QTextDocument.FindCaseSensitively
        if self.find_words:
            flags |= QTextDocument.FindWholeWords

        found = self.find(text, flags)

        # if found:
        #     cursor = self.document().find(text, flags)
        #     if cursor.position() > 0:
        #         self.setTextCursor(cursor)

    def replaceText(self, search, replace):

        flags = QTextDocument.FindFlag()

        if self.find_case:
            flags |= QTextDocument.FindCaseSensitively
        if self.find_words:
            flags |= QTextDocument.FindWholeWords

        found = self.find(search, flags)
        if found:
            cursor = self.textCursor()
            cursor.beginEditBlock()
            if cursor.hasSelection():
                cursor.insertText(replace)
            cursor.endEditBlock()

    def replaceAllText(self, search, replace):

        flags = QTextDocument.FindFlag()

        if self.find_case:
            flags |= QTextDocument.FindCaseSensitively
        if self.find_words:
            flags |= QTextDocument.FindWholeWords

        searching = True
        while searching:
            found = self.find(search, flags)
            if found:
                cursor = self.textCursor()
                cursor.beginEditBlock()
                if cursor.hasSelection():
                    cursor.insertText(replace)
                cursor.endEditBlock()
            else:
                searching = False

    @Slot()
    def findAll(self):

        text = self.search_term
        LOG.debug(f"Find all text :{text}")
        self.findAllText(text)

    @Slot()
    def findForward(self):
        text = self.search_term
        LOG.debug(f"Find forward :{text}")
        self.findForwardText(text)

    @Slot()
    def findBackward(self):
        text = self.search_term
        LOG.debug(f"Find backwards :{text}")
        self.findBackwardText(text)

    @Slot()
    def replace(self):

        search_text = self.search_term
        replace_text = self.replace_term

        LOG.debug(f"Replace text :{search_text} with {replace_text}")

        self.replaceText(search_text, replace_text)

    @Slot()
    def replaceAll(self):

        search_text = self.search_term
        replace_text = self.replace_term

        LOG.debug(f"Replace all text :{search_text} with {replace_text}")

        self.replaceAllText(search_text, replace_text)

    @Slot()
    def saveFile(self, save_file_name=None):
        if save_file_name == None:
            save_file = QFile(str(STATUS.file))
        else:
            save_file = QFile(str(save_file_name))

        result = save_file.open(QFile.WriteOnly)
        if result:
            LOG.debug(f'---Save file: {save_file.fileName()}')
            save_stream = QTextStream(save_file)
            save_stream << self.toPlainText()
            save_file.close()
        else:
            LOG.debug("---save error")

    # simple input dialog for save as
    def save_as_dialog(self, filename):
        text, ok_pressed = QInputDialog.getText(self, "Save as", "New name:",
                                                QLineEdit.Normal, filename)

        if ok_pressed and text != '':
            return text
        else:
            return False

    @Slot()
    def saveFileAs(self):
        open_file = QFile(str(STATUS.file))
        if open_file == None:
            return

        save_file = self.save_as_dialog(open_file.fileName())
        self.saveFile(save_file)

    def keyPressEvent(self, event):
        # keep the cursor centered
        if event.key() == Qt.Key_Up:
            self.moveCursor(QTextCursor.Up)
            self.centerCursor()

        elif event.key() == Qt.Key_Down:
            self.moveCursor(QTextCursor.Down)
            self.centerCursor()

        else:
            super(GcodeTextEdit, self).keyPressEvent(event)

    def changeEvent(self, event):
        if event.type() == QEvent.FontChange:
            # Update syntax highlighter with new font
            self.gCodeHighlighter = GcodeSyntaxHighlighter(
                self.document(), self.font)

        super(GcodeTextEdit, self).changeEvent(event)

    @Slot(bool)
    def syntaxHighlightingOnOff(self, state):
        """Toggle syntax highlighting on/off"""
        pass

    @Property(bool)
    def syntaxHighlighting(self):
        return self.syntax_highlighting

    @syntaxHighlighting.setter
    def syntaxHighlighting(self, state):
        self.syntax_highlighting = state

    def setPlainText(self, p_str):
        # FixMe: Keep a reference to old QTextDocuments form previously loaded
        # files. This is needed to prevent garbage collection which results in a
        # seg fault if the document is discarded while still being highlighted.
        self.old_docs.append(self.document())

        doc = QTextDocument()
        doc.setDocumentLayout(QPlainTextDocumentLayout(doc))
        doc.setPlainText(p_str)

        # start syntax highlighting
        if self.syntax_highlighting == True:
            self.gCodeHighlighter = GcodeSyntaxHighlighter(doc, self.font)

        self.setDocument(doc)
        self.margin.updateWidth()

        # start syntax highlighting
        # self.gCodeHighlighter = GcodeSyntaxHighlighter(self)

    @Slot(bool)
    def EditorReadOnly(self, state):
        """Set to Read Only to disable editing"""

        if state:
            self.setReadOnly(True)
        else:
            self.setReadOnly(False)

        self.readonly = state

    @Slot(bool)
    def EditorReadWrite(self, state):
        """Set to Read Only to disable editing"""

        if state:
            self.setReadOnly(False)
        else:
            self.setReadOnly(True)

        self.readonly != state

    @Property(bool)
    def readOnly(self):
        return self.readonly

    @readOnly.setter
    def readOnly(self, state):

        if state:
            self.setReadOnly(True)
        else:
            self.setReadOnly(False)

        self.readonly = state

    @Property(QColor)
    def currentLineBackground(self):
        return self.current_line_background

    @currentLineBackground.setter
    def currentLineBackground(self, color):
        self.current_line_background = color
        # Hack to get background to update
        self.setCurrentLine(2)
        self.setCurrentLine(1)

    @Property(QColor)
    def marginBackground(self):
        return self.margin.background

    @marginBackground.setter
    def marginBackground(self, color):
        self.margin.background = color
        self.margin.update()

    @Property(QColor)
    def marginCurrentLineBackground(self):
        return self.margin.highlight_background

    @marginCurrentLineBackground.setter
    def marginCurrentLineBackground(self, color):
        self.margin.highlight_background = color
        self.margin.update()

    @Property(QColor)
    def marginColor(self):
        return self.margin.color

    @marginColor.setter
    def marginColor(self, color):
        self.margin.color = color
        self.margin.update()

    @Property(QColor)
    def marginCurrentLineColor(self):
        return self.margin.highlight_color

    @marginCurrentLineColor.setter
    def marginCurrentLineColor(self, color):
        self.margin.highlight_color = color
        self.margin.update()

    @Slot(str)
    @Slot(object)
    def loadProgramFile(self, fname=None):
        if fname:
            encodings = allEncodings()
            enc = None
            for enc in encodings:
                try:
                    with open(fname, 'r', encoding=enc) as f:
                        gcode = f.read()
                        break
                except Exception as e:
                    # LOG.debug(e)
                    LOG.info(
                        f"File encoding doesn't match {enc}, trying others")
            LOG.info(f"File encoding: {enc}")
            # set the syntax highlighter
            self.setPlainText(gcode)
            # self.gCodeHighlighter = GcodeSyntaxHighlighter(self.document(), self.font)

    @Slot(int)
    @Slot(object)
    def setCurrentLine(self, line):
        cursor = QTextCursor(self.document().findBlockByLineNumber(line - 1))
        self.setTextCursor(cursor)
        self.centerCursor()

    def getCurrentLine(self):
        return self.textCursor().blockNumber() + 1

    def onCursorChanged(self):
        # highlights current line, find a way not to use QTextEdit
        block_number = self.textCursor().blockNumber()
        if block_number != self.block_number:
            self.block_number = block_number
            selection = QTextEdit.ExtraSelection()
            selection.format.setBackground(self.current_line_background)
            selection.format.setProperty(QTextFormat.FullWidthSelection, True)
            selection.cursor = self.textCursor()
            selection.cursor.clearSelection()
            self.setExtraSelections([selection])

        # emit signals for backplot etc.
        self.focused_line = block_number + 1
        self.focusLine.emit(self.focused_line)

    def contextMenuEvent(self, event):
        self.run_action.setText("Run from line {}".format(self.focused_line))
        self.menu.popup(event.globalPos())
        event.accept()

    def runFromHere(self, *args, **kwargs):
        line = self.getCurrentLine()
        program_actions.run(line)

    def resizeEvent(self, *e):
        cr = self.contentsRect()
        rec = QRect(cr.left(), cr.top(), self.margin.getWidth(), cr.height())
        self.margin.setGeometry(rec)
        QPlainTextEdit.resizeEvent(self, *e)