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)
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()
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)
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
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)
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): """
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)
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)
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))
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
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)