class WorkflowableROI(ROI): # FIXME: do we still want this for our (e.g.) CorrelationStage process_actions??? def __init__(self, *args, **kwargs): super(WorkflowableROI, self).__init__(*args, **kwargs) self.operation = ROIOperation(self) self._param = None def parameter(self) -> Parameter: raise NotImplementedError def getMenu(self): if self.menu is None: self.menu = QMenu() self.menu.setTitle("ROI") if self.removable: # FIXME: if the removable attr is changed, the menu will not react and remAct won't show remAct = QAction("Remove ROI", self.menu) remAct.triggered.connect(self.removeClicked) self.menu.addAction(remAct) self.menu.remAct = remAct editAct = QAction("Edit ROI", self.menu) editAct.triggered.connect(self.edit_parameters) self.menu.addAction(editAct) self.menu.editAct = editAct self.menu.setEnabled(True) return self.menu def contextMenuEnabled(self): return True def edit_parameters(self): class DefocusParameterTree(QWidget): def __init__(self, *args, **kwargs): super(DefocusParameterTree, self).__init__(*args, **kwargs) self.setLayout(QVBoxLayout()) self.parameter_tree = ParameterTree() self.layout().addWidget(self.parameter_tree) self.layout().setContentsMargins(0, 0, 0, 0) def setParameters(self, *args, **kwargs): self.parameter_tree.setParameters(*args, **kwargs) # self.parameter_tree = DefocusParameterTree() self.parameter_tree = DefocusParameterTree() self.parameter_tree.setParameters(self.parameter()) self.parameter_tree.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup) # self.parameter_tree = QLabel('blah') self.parameter_tree.show() self.parameter_tree.activateWindow() self.parameter_tree.raise_() self.parameter_tree.move(QCursor().pos()) self.parameter_tree.setFocus(Qt.PopupFocusReason) self.parameter_tree.resize(QSize(300, 400))
class ListItemApplication(ListWidgetItemBase): """Item with custom widget for the applications list.""" ICON_SIZE = 64 def __init__( self, name=None, display_name=None, description=None, command=None, versions=None, image_path=None, prefix=None, needs_license=False, non_conda=False, ): """Item with custom widget for the applications list.""" super(ListItemApplication, self).__init__() self.api = AnacondaAPI() self.prefix = prefix self.name = name self.display_name = display_name if display_name else name self.url = '' self.expired = False self.needs_license = needs_license self.description = description self.command = command self.versions = versions self.image_path = image_path if image_path else ANACONDA_ICON_256_PATH self.style_sheet = None self.timeout = 2000 self.non_conda = non_conda self._vscode_version_value = None # Widgets self.button_install = ButtonApplicationInstall("Install") # or Try! self.button_launch = ButtonApplicationLaunch("Launch") self.button_options = ButtonApplicationOptions() self.label_license = LabelApplicationLicense('') self.button_license = ButtonApplicationLicense('') self.label_icon = LabelApplicationIcon() self.label_name = LabelApplicationName(self.display_name) self.label_description = LabelApplicationDescription(self.description) self.button_version = ButtonApplicationVersion( to_text_string(self.version)) self.menu_options = QMenu('Application options') self.menu_versions = QMenu('Install specific version') self.pixmap = QPixmap(self.image_path) self.timer = QTimer() self.widget = WidgetApplication() self.frame_spinner = FrameApplicationSpinner() self.spinner = NavigatorSpinner(self.widget, total_width=16) lay = QHBoxLayout() lay.addWidget(self.spinner) self.frame_spinner.setLayout(lay) # Widget setup self.button_version.setFocusPolicy(Qt.NoFocus) self.button_version.setEnabled(True) self.label_description.setAlignment(Qt.AlignCenter) self.timer.setInterval(self.timeout) self.timer.setSingleShot(True) self.label_icon.setPixmap(self.pixmap) self.label_icon.setScaledContents(True) # important on High DPI! self.label_icon.setMaximumWidth(self.ICON_SIZE) self.label_icon.setMaximumHeight(self.ICON_SIZE) self.label_icon.setAlignment(Qt.AlignCenter) self.label_name.setAlignment(Qt.AlignCenter) self.label_name.setWordWrap(True) self.label_description.setWordWrap(True) self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter) self.frame_spinner.setVisible(False) # Layouts layout_spinner = QHBoxLayout() layout_spinner.addWidget(self.button_version, 0, Qt.AlignCenter) layout_spinner.addWidget(self.frame_spinner, 0, Qt.AlignCenter) layout_license = QHBoxLayout() layout_license.addStretch() layout_license.addWidget(self.label_license, 0, Qt.AlignCenter) layout_license.addWidget(self.button_license, 0, Qt.AlignCenter) layout_license.addStretch() layout_main = QVBoxLayout() layout_main.addWidget(self.button_options, 0, Qt.AlignRight) layout_main.addWidget(self.label_icon, 0, Qt.AlignCenter) layout_main.addWidget(self.label_name, 0, Qt.AlignCenter) layout_main.addLayout(layout_spinner) layout_main.addLayout(layout_license) layout_main.addWidget(self.label_description, 0, Qt.AlignCenter) layout_main.addWidget(self.button_launch, 0, Qt.AlignCenter) layout_main.addWidget(self.button_install, 0, Qt.AlignCenter) self.widget.setLayout(layout_main) self.widget.setStyleSheet(load_style_sheet()) self.setSizeHint(self.widget_size()) # This might help with visual quirks on the home screen self.widget.setMinimumSize(self.widget_size()) # Signals self.button_install.clicked.connect(self.install_application) self.button_launch.clicked.connect(self.launch_application) self.button_options.clicked.connect(self.actions_menu_requested) self.button_license.clicked.connect(self.launch_url) self.timer.timeout.connect(self._application_launched) # Setup self.update_status() # --- Callbacks # ------------------------------------------------------------------------- def _application_launched(self): self.button_launch.setDisabled(False) update_pointer() # --- Helpers # ------------------------------------------------------------------------- def update_style_sheet(self, style_sheet=None): """Update custom CSS stylesheet.""" if style_sheet: self.style_sheet = style_sheet else: self.style_sheet = load_style_sheet() self.menu_options.setStyleSheet(self.style_sheet) self.menu_versions.setStyleSheet(self.style_sheet) def ordered_widgets(self): """Return a list of the ordered widgets.""" return [ self.button_license, self.button_install, self.button_launch, self.button_options ] @staticmethod def widget_size(): """Return the size defined in the SASS file.""" return QSize(SASS_VARIABLES.WIDGET_APPLICATION_TOTAL_WIDTH, SASS_VARIABLES.WIDGET_APPLICATION_TOTAL_HEIGHT) def launch_url(self): """Launch signal for url click.""" self.widget.sig_url_clicked.emit(self.url) def actions_menu_requested(self): """Create and display menu for the currently selected application.""" self.menu_options.clear() self.menu_versions.clear() # Add versions menu versions = self.versions if self.versions else [] version_actions = [] for version in reversed(versions): action = create_action(self.widget, version, triggered=lambda value, version=version: self.install_application(version=version)) action.setCheckable(True) if self.version == version and self.installed: action.setChecked(True) action.setDisabled(True) version_actions.append(action) install_action = create_action( self.widget, 'Install application', triggered=lambda: self.install_application()) install_action.setEnabled(not self.installed) update_action = create_action( self.widget, 'Update application', triggered=lambda: self.update_application()) if versions and versions[-1] == self.version: update_action.setDisabled(True) else: update_action.setDisabled(False) if self.non_conda and self.name == GLOBAL_VSCODE_APP: update_action.setDisabled(True) remove_action = create_action( self.widget, 'Remove application', triggered=lambda: self.remove_application()) remove_action.setEnabled(self.installed) actions = [ install_action, update_action, remove_action, None, self.menu_versions ] add_actions(self.menu_options, actions) add_actions(self.menu_versions, version_actions) offset = QPoint(self.button_options.width(), 0) position = self.button_options.mapToGlobal(QPoint(0, 0)) self.menu_versions.setEnabled(len(versions) > 1) self.menu_options.move(position + offset) self.menu_options.exec_() def update_status(self): """Update status.""" # License check license_label_text = '' license_url_text = '' self.url = '' self.expired = False button_label = 'Install' if self.needs_license: # TODO: Fix this method to use the api license_info = self.api.get_package_license(self.name) license_days = self.api.get_days_left(license_info) end_date = license_info.get('end_date', '') self.expired = license_days == 0 plural = 's' if license_days != 1 else '' is_trial = license_info.get('type', '').lower() == 'trial' if self.installed and license_info: if is_trial and not self.expired: license_label_text = ('Trial, {days} day{plural} ' 'remaining'.format(days=license_days, plural=plural)) self.url = '' elif is_trial and self.expired: license_label_text = 'Trial expired, ' license_url_text = 'contact us' self.url = 'mailto:[email protected]' elif not is_trial and not self.expired: license_label_text = 'License expires {}'.format(end_date) self.url = '' elif not is_trial and self.expired: license_url_text = 'Renew license' self.url = 'mailto:[email protected]' elif self.installed and not bool(license_info): # Installed but no license found! license_url_text = 'No license found' self.url = 'mailto:[email protected]' else: if not self.expired: button_label = 'Install' else: button_label = 'Try' self.button_license.setText(license_url_text) self.button_license.setVisible(bool(self.url)) self.label_license.setText(license_label_text) self.label_license.setVisible(bool(license_label_text)) # Version and version updates if (self.versions and self.version != self.versions[-1] and self.installed): # The property is used with CSS to display updatable packages. self.button_version.setProperty('pressed', True) self.button_version.setToolTip('Version {0} available'.format( self.versions[-1])) else: self.button_version.setProperty('pressed', False) # For VScode app do not display if new updates are available # See: https://github.com/ContinuumIO/navigator/issues/1504 if self.non_conda and self.name == GLOBAL_VSCODE_APP: self.button_version.setProperty('pressed', False) self.button_version.setToolTip('') if not self.needs_license: self.button_install.setText(button_label) self.button_install.setVisible(not self.installed) self.button_launch.setVisible(self.installed) else: self.button_install.setText('Try' if self.expired else 'Install') self.button_launch.setVisible(not self.expired) self.button_install.setVisible(self.expired) self.button_launch.setEnabled(True) def update_versions(self, version=None, versions=None): """Update button visibility depending on update availability.""" logger.debug(str((self.name, self.dev_tool, self.installed))) if self.installed and version: self.button_options.setVisible(True) self.button_version.setText(version) self.button_version.setVisible(True) elif not self.installed and versions: self.button_install.setEnabled(True) self.button_version.setText(versions[-1]) self.button_version.setVisible(True) self.versions = versions self.version = version self.update_status() def set_loading(self, value): """Set loading status.""" self.button_install.setDisabled(value) self.button_options.setDisabled(value) self.button_launch.setDisabled(value) self.button_license.setDisabled(value) if value: self.spinner.start() else: self.spinner.stop() if self.version is None and self.versions is not None: version = self.versions[-1] else: version = self.version self.button_version.setText(version) self.button_launch.setDisabled(self.expired) self.frame_spinner.setVisible(value) self.button_version.setVisible(not value) # --- Helpers using api # ------------------------------------------------------------------------- def _vscode_version(self): """Query the vscode version for the default installation path.""" version = None if self._vscode_version_value is None: cmd = [self.api.vscode_executable(), '--version'] # print(cmd) import subprocess try: output = subprocess.check_output(cmd) if PY3: output = output.decode() output = [o for o in output.split('\n') if o and '.' in o] # print(output) if output: version = output[0] except Exception: pass # print(e) self._vscode_version_value = version else: version = self._vscode_version_value return version @property def installed(self): """Return the installed status of the package.""" version = None if self.non_conda and self.name == GLOBAL_VSCODE_APP: # TODO: Vscode program location, check existence version = self._vscode_version() elif self.prefix: version = self.api.conda_package_version(prefix=self.prefix, pkg=self.name, build=False) return bool(version) @property def version(self): """Return the current installed version or the highest version.""" version = None if self.non_conda and self.name == GLOBAL_VSCODE_APP: version = self._vscode_version() elif self.prefix: version = self.api.conda_package_version(prefix=self.prefix, pkg=self.name, build=False) if not version: version = self.versions[-1] return version # --- Application actions # ------------------------------------------------------------------------ def install_application(self, value=None, version=None, install=True): """ Update the application on the defined prefix environment. This is used for both normal install and specific version install. """ if not version: version = self.versions[-1] action = C.APPLICATION_INSTALL if install else C.APPLICATION_UPDATE self.widget.sig_conda_action_requested.emit( action, self.name, version, C.TAB_HOME, self.non_conda, ) self.set_loading(True) def remove_application(self): """Remove the application from the defined prefix environment.""" self.widget.sig_conda_action_requested.emit( C.APPLICATION_REMOVE, self.name, None, C.TAB_HOME, self.non_conda, ) self.set_loading(True) def update_application(self): """Update the application on the defined prefix environment.""" self.install_application(version=self.versions[-1], install=False) def launch_application(self): """Launch application installed in prefix environment.""" leave_path_alone = False if self.command is not None: if self.non_conda and self.name == GLOBAL_VSCODE_APP: leave_path_alone = True args = [self.command] else: args = self.command.split(' ') leave_path_alone = True self.button_launch.setDisabled(True) self.timer.setInterval(self.timeout) self.timer.start() update_pointer(Qt.BusyCursor) self.widget.sig_launch_action_requested.emit( self.name, args, leave_path_alone, self.prefix, C.TAB_HOME, self.non_conda, )
class LineEditChannel(LineEditBase): """ Custom line edit that uses different validators for text and url. More info: http://conda.pydata.org/docs/config.html#channel-locations-channels Valid entries: - defaults <- Special case - <some-channel-name> - https://conda.anaconda.org/<channel>/<package> - https://conda.anaconda.org/t/<token>/<package> - http://<some.custom.url>/<channel> - https://<some.custom.url>/<channel> - file:///<some-local-directory> """ VALID_RE = QRegExp('^[A-Za-z][A-Za-z0-9_-]+$|' '^https?://.*|' '^file:///.*') sig_return_pressed = Signal() sig_escape_pressed = Signal() sig_copied = Signal() def __init__(self, *args, **kwargs): """Custom line edit that uses different validators for text and url.""" super(LineEditChannel, self).__init__(*args, **kwargs) self._validator = QRegExpValidator(self.VALID_RE) self.menu = QMenu(parent=self) self.setValidator(self._validator) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def event(self, event): """Override Qt method.""" if (event.type() == QEvent.MouseButtonPress and event.buttons() & Qt.RightButton and not self.isEnabled()): self.show_menu(event.pos()) return True else: return super(LineEditChannel, self).event(event) def keyPressEvent(self, event): """Override Qt method.""" key = event.key() # Display a copy menu in case the widget is disabled. if event.matches(QKeySequence.Paste): clipboard = QApplication.clipboard() text = clipboard.text() if self.VALID_RE.exactMatch(text): self.setText(text) return else: if key in [Qt.Key_Return, Qt.Key_Enter]: self.sig_return_pressed.emit() elif key in [Qt.Key_Escape]: self.sig_escape_pressed.emit() super(LineEditChannel, self).keyPressEvent(event) def show_menu(self, pos): """Show copy menu for channel item.""" self.menu.clear() copy = QAction("&Copy", self.menu) copy.triggered.connect(self.copy_text) self.menu.addAction(copy) self.menu.setEnabled(True) self.menu.exec_(self.mapToGlobal(pos)) def copy_text(self): """Copy channel text to clipboard.""" clipboard = QApplication.clipboard() clipboard.setText(self.text()) self.sig_copied.emit()
class MenuNode(object): _currentMenuContext = None """docstring for MenuNode""" def __init__(self, option, parent, menubar=None): if isinstance(option, str): blobs = option.split('|') _option = {'label': blobs[0]} l = len(blobs) if l > 1: _option['shortcut'] = blobs[1] if l > 2: _option['help'] = blobs[2] option = _option self.qtmenubar = menubar self.qtaction = None self.qtmenu = None # self.qtaction = None self.owner = None self.parent = parent self.groupName = None signal = option.get('signal', None) self.setSignal(signal) self.mgr = parent and parent.mgr self.owner = parent and parent.owner self.children = [] self.actionGroups = {} self.label = option.get('label', 'UNNAMED') self.name = option.get('name', self.label.replace('&', '').replace(' ', '_')) self.name = self.name.lower() self.shortcut = option.get('shortcut', False) self.help = option.get('help', '') self.priority = option.get('priority', 0) self.itemType = option.get('type', False) self.onClick = option.get('on_click', None) self.cmd = option.get('command', None) self.cmdArgs = option.get('command_args', None) self.link = None self.groupName = option.get('group', None) self.menuType = self.qtmenubar and 'menubar' or 'item' children = option.get('children', None) link = option.get('link', None) if children or self.itemType == 'menu': if self.menuType != 'menubar': self.menuType = 'menu' self.itemType = False elif link: self.link = link if self.menuType != 'menubar': self.menuType = 'link' elif parent and parent.menuType == 'menubar': self.menuType = 'menu' if self.menuType == 'menu': self.qtmenu = QMenu(self.label) if not parent or parent.menuType == 'root': return parent.addChildControl(self) if self.itemType == 'check': checked = option.get('checked', False) self.setValue(checked or False) if children: for data in children: self.addChild(data) # self.mgr.addNodeIndex(self) def getFullName(self): if parent: return parent.getFullName() + '/' + self.name return self.name def addChild(self, option, owner=None): if option == '----': if self.qtmenu: self.qtmenu.addSeparator() elif isinstance(option, list): output = [] for data in option: n = self.addChild(data) if n: output.append(n) if owner: n.owner = owner return output else: node = MenuNode(option, self) if owner: node.owner = owner self.children.append(node) return node def affirmQtActionGroup(self, name): group = self.actionGroups.get(name, None) if not group: group = QtWidgets.QActionGroup(self.qtmenu) self.actionGroups[name] = group return group def addChildControl(self, child): childType = child.menuType selfType = self.menuType if selfType == 'menu': if childType == 'menu': child.qtaction = self.qtmenu.addMenu(child.qtmenu) elif child.link: qtmenu = child.link.qtmenu child.qtaction = self.qtmenu.addMenu(qtmenu) else: action = QtWidgets.QAction(child.label, None, shortcut=child.shortcut, statusTip=child.help, checkable=child.itemType == 'check', triggered=child.handleEvent) self.qtmenu.addAction(action) child.qtaction = action if child.groupName: self.affirmQtActionGroup(child.groupName).addAction(action) elif selfType == 'menubar': if childType == 'menu': self.qtmenubar.addMenu(child.qtmenu) child.qtaction = child.qtmenu.menuAction() else: logging.warning('attempt to add menuitem/link to a menubar') return else: logging.warning('menuitem has no child') def setEnabled(self, enabled): #todo: set state of linked item selfType = self.menuType if selfType == 'menubar': self.qtmenubar.setEnable(enabled) return if self.qtmenu: self.qtmenu.setEnabled(enabled) else: self.qtaction.setEnabled(enabled) def remove(self): self.clear() self.parent.children.remove(self) selfType = self.menuType if not self.parent: return if selfType == 'menubar': return parentType = self.parent.menuType if parentType == 'menu': self.parent.qtmenu.removeAction(self.qtaction) elif parentType == 'menubar': self.parent.qtmenubar.removeAction(self.qtaction) logging.info('remove menunode:' + self.name) def clear(self): if self.menuType in ['menu', 'menubar']: for node in self.children[:]: node.remove() def findChild(self, name): name = name.lower() for c in self.children: if c.name == name: return c return None def getValue(self): if self.itemType in ('check', 'radio'): return self.qtaction.isChecked() return True def setValue(self, v): if self.itemType in ('check', 'radio'): self.qtaction.setChecked(v and True or False) def setSignal(self, signal): if isinstance(signal, str): signal = signals.get(signal) self.signal = signal def popUp(self, **option): if self.qtmenu: context = option.get('context', None) MenuNode._currentMenuContext = context self.qtmenu.exec_(QtGui.QCursor.pos()) def getContext(self): return MenuNode._currentMenuContext def setOnClick(self, onClick): self.onClick = onClick def handleEvent(self): itemtype = self.itemType value = self.getValue() logging.debug('menu event:' + self.name) if self.owner: if hasattr(self.owner, 'onMenu'): self.owner.onMenu(self) if self.signal: self.signal(value) if self.onClick != None: self.onClick(value) if self.cmd: args = self.cmdArgs or {} app.doCommand(self.cmd, **args) MenuNode._currentMenuContext = None
class LineEditEnvironment(LineEditBase): """ Custom line edit to handle regex for naming an environment. """ if WIN: VALID_RE = QRegExp('^[A-Za-z][A-Za-z0-9 _-]{0,30}$') else: VALID_RE = QRegExp('^[A-Za-z][A-Za-z0-9_ -]{0,30}$') sig_return_pressed = Signal() sig_escape_pressed = Signal() sig_copied = Signal() def __init__(self, *args, **kwargs): """Custom line edit for naming an environment.""" super(LineEditEnvironment, self).__init__(*args, **kwargs) self._validator = QRegExpValidator(self.VALID_RE) self.menu = QMenu(parent=self) self.setValidator(self._validator) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def event(self, event): """Override Qt method.""" if (event.type() == QEvent.MouseButtonPress and event.buttons() & Qt.RightButton and not self.isEnabled()): self.show_menu(event.pos()) return True else: return super(LineEditEnvironment, self).event(event) def keyPressEvent(self, event): """Override Qt method.""" key = event.key() # Display a copy menu in case the widget is disabled. if event.matches(QKeySequence.Paste): clipboard = QApplication.clipboard() text = clipboard.text() if self.VALID_RE.exactMatch(text): self.setText(text) return else: if key in [Qt.Key_Return, Qt.Key_Enter]: self.sig_return_pressed.emit() elif key in [Qt.Key_Escape]: self.sig_escape_pressed.emit() super(LineEditEnvironment, self).keyPressEvent(event) def show_menu(self, pos): """Show copy menu for channel item.""" self.menu.clear() copy = QAction("&Copy", self.menu) copy.triggered.connect(self.copy_text) self.menu.addAction(copy) self.menu.setEnabled(True) self.menu.exec_(self.mapToGlobal(pos)) def copy_text(self): """Copy environment text to clipboard.""" clipboard = QApplication.clipboard() clipboard.setText(self.text()) self.sig_copied.emit()
def context_menu_requested(self, event, right_click=False): """ Custom context menu. """ if self.proxy_model is None: return self._menu = QMenu(self) index = self.currentIndex() model_index = self.proxy_model.mapToSource(index) row_data = self.source_model.row(model_index.row()) column = model_index.column() name = row_data[const.COL_NAME] # package_type = row_data[const.COL_PACKAGE_TYPE] versions = self.source_model.get_package_versions(name) current_version = self.source_model.get_package_version(name) # if column in [const.COL_ACTION, const.COL_VERSION, const.COL_NAME]: if column in [const.COL_ACTION] and not right_click: is_installable = self.source_model.is_installable(model_index) is_removable = self.source_model.is_removable(model_index) is_upgradable = self.source_model.is_upgradable(model_index) action_status = self.source_model.action_status(model_index) actions = [] action_unmark = create_action( self, _('Unmark'), triggered=lambda: self.set_action_status(model_index, const.ACTION_NONE, current_version)) action_install = create_action( self, _('Mark for installation'), triggered=lambda: self.set_action_status(model_index, const.ACTION_INSTALL, versions[-1])) action_upgrade = create_action( self, _('Mark for upgrade'), triggered=lambda: self.set_action_status(model_index, const.ACTION_UPGRADE, versions[-1])) action_remove = create_action( self, _('Mark for removal'), triggered=lambda: self.set_action_status(model_index, const.ACTION_REMOVE, current_version)) version_actions = [] for version in reversed(versions): def trigger(model_index=model_index, action=const.ACTION_INSTALL, version=version): return lambda: self.set_action_status(model_index, status=action, version=version) if version == current_version: version_action = create_action( self, version, icon=QIcon(), triggered=trigger(model_index, const.ACTION_INSTALL, version)) if not is_installable: version_action.setCheckable(True) version_action.setChecked(True) version_action.setDisabled(True) elif version != current_version: if ((version in versions and versions.index(version)) > (current_version in versions and versions.index(current_version))): upgrade_or_downgrade_action = const.ACTION_UPGRADE else: upgrade_or_downgrade_action = const.ACTION_DOWNGRADE if is_installable: upgrade_or_downgrade_action = const.ACTION_INSTALL version_action = create_action( self, version, icon=QIcon(), triggered=trigger(model_index, upgrade_or_downgrade_action, version)) version_actions.append(version_action) install_versions_menu = QMenu('Mark for specific version ' 'installation', self) add_actions(install_versions_menu, version_actions) actions = [action_unmark, action_install, action_upgrade, action_remove] actions += [None, install_versions_menu] install_versions_menu.setEnabled(len(version_actions) > 1) if action_status is const.ACTION_NONE: action_unmark.setDisabled(True) action_install.setDisabled(not is_installable) action_upgrade.setDisabled(not is_upgradable) action_remove.setDisabled(not is_removable) install_versions_menu.setDisabled(False) else: action_unmark.setDisabled(False) action_install.setDisabled(True) action_upgrade.setDisabled(True) action_remove.setDisabled(True) install_versions_menu.setDisabled(True) elif right_click: license_ = row_data[const.COL_LICENSE] metadata = self.metadata_links.get(name, {}) pypi = metadata.get('pypi', '') home = metadata.get('home', '') dev = metadata.get('dev', '') docs = metadata.get('docs', '') q_pypi = QIcon(get_image_path('python.png')) q_home = QIcon(get_image_path('home.png')) q_docs = QIcon(get_image_path('conda_docs.png')) if 'git' in dev: q_dev = QIcon(get_image_path('conda_github.png')) elif 'bitbucket' in dev: q_dev = QIcon(get_image_path('conda_bitbucket.png')) else: q_dev = QIcon() if 'mit' in license_.lower(): lic = 'http://opensource.org/licenses/MIT' elif 'bsd' == license_.lower(): lic = 'http://opensource.org/licenses/BSD-3-Clause' else: lic = None actions = [] if license_ != '': actions.append(create_action(self, _('License: ' + license_), icon=QIcon(), triggered=lambda: self.open_url(lic))) actions.append(None) if pypi != '': actions.append(create_action(self, _('Python Package Index'), icon=q_pypi, triggered=lambda: self.open_url(pypi))) if home != '': actions.append(create_action(self, _('Homepage'), icon=q_home, triggered=lambda: self.open_url(home))) if docs != '': actions.append(create_action(self, _('Documentation'), icon=q_docs, triggered=lambda: self.open_url(docs))) if dev != '': actions.append(create_action(self, _('Development'), icon=q_dev, triggered=lambda: self.open_url(dev))) if actions and len(actions) > 1: # self._menu = QMenu(self) add_actions(self._menu, actions) if event.type() == QEvent.KeyRelease: rect = self.visualRect(index) global_pos = self.viewport().mapToGlobal(rect.bottomRight()) else: pos = QPoint(event.x(), event.y()) global_pos = self.viewport().mapToGlobal(pos) self._menu.popup(global_pos)
class ListItemApplication(QListWidgetItem): """ Item with custom widget for the applications list. """ ICON_SIZE = 48 def __init__(self, name=None, description=None, command=None, pixmap=None, version=None, versions=None, path=None, dev_tool=True, prefix=None, is_conda_app=False, packages_widget=None): super(ListItemApplication, self).__init__() self.api = AnacondaAPI() self.command = command self.dev_tool = dev_tool self.installed = False self.is_conda_app = is_conda_app self.name = name self.path = path self.pixmap = pixmap if pixmap else QPixmap(ANACONDA_ICON_64_PATH) self.prefix = prefix self.timeout = 10000 # In miliseconds self.version = version self.versions = versions self.packages_widget = packages_widget # Widgets self.button_install = ButtonApplicationInstall("Install") self.button_launch = ButtonApplicationLaunch("Launch") self.button_options = ButtonApplicationOptions() self.label_icon = LabelApplicationIcon() self.label_name = LabelApplicationName(name) self.label_description = LabelApplicationDescription(description) # self.label_update = LabelApplicationUpdate() self.button_version = ButtonApplicationVersion(to_text_string(version)) self.label_spinner = LabelApplicationSpinner() # self.label_version = LabelApplicationVersion(to_text_string(version)) self.menu_options = QMenu('Application options') self.menu_versions = QMenu('Install specific version') self.movie_spinner = QMovie(SPINNER_WHITE_16_PATH) self.timer = QTimer() self.widget = WidgetApplication() # Widget setup self.button_version.setFocusPolicy(Qt.NoFocus) self.label_name.setToolTip(description) self.label_description.setAlignment(Qt.AlignCenter) self.movie_spinner.start() self.timer.setInterval(self.timeout) self.timer.setSingleShot(True) self.label_icon.setToolTip(description) self.label_icon.setPixmap( self.pixmap.scaled(self.ICON_SIZE, self.ICON_SIZE, Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.label_icon.setAlignment(Qt.AlignCenter) self.label_name.setAlignment(Qt.AlignCenter) self.label_name.setWordWrap(True) self.label_description.setWordWrap(True) self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter) self.label_spinner.setVisible(False) self.label_spinner.setMinimumWidth(16) self.label_spinner.setMinimumHeight(16) # Layouts layout = QVBoxLayout() layout.addWidget(self.button_options, 0, Qt.AlignRight) layout.addWidget(self.label_icon, 0, Qt.AlignCenter) layout.addWidget(self.label_name, 0, Qt.AlignCenter) layout.addWidget(self.label_description, 0, Qt.AlignCenter) # hlayout = QHBoxLayout() # hlayout.addWidget(self.label_update) # hlayout.addWidget(self.label_version) # layout.addLayout(hlayout) # layout.addWidget(self.label_version, 0, Qt.AlignCenter) layout.addWidget(self.button_version, 0, Qt.AlignCenter) layout.addWidget(self.label_spinner, 0, Qt.AlignCenter) layout.addWidget(self.button_launch, 0, Qt.AlignCenter) layout.addWidget(self.button_install, 0, Qt.AlignCenter) self.widget.setLayout(layout) self.widget.setStyleSheet(load_style_sheet()) self.setSizeHint(self.widget.sizeHint()) # Signals self.button_install.clicked.connect(self.install_application) self.button_launch.clicked.connect(self.launch_application) self.button_options.clicked.connect(self.actions_menu_requested) self.timer.timeout.connect(self._application_launched) # Setup self.update_status() # --- Callbacks # ------------------------------------------------------------------------- def _application_launched(self): """ """ self.button_launch.setDisabled(False) update_pointer() def _application_installed(self, worker, output, error): """ """ self.handle_action_finished(worker, output, error) def _application_updated(self, worker, output, error): """ """ self.handle_action_finished(worker, output, error) def _application_removed(self, worker, output, error): """ """ self.handle_action_finished(worker, output, error) # --- Helpers # ------------------------------------------------------------------------- def _partial_output_ready(self, worker, output, error): """ """ message = None progress = (0, 0) if isinstance(output, dict): progress = (output.get('progress', None), output.get('maxval', None)) name = output.get('name', None) fetch = output.get('fetch', None) if fetch: message = "Downloading <b>{0}</b>...".format(fetch) if name: self._current_action_name = name message = "Linking <b>{0}</b>...".format(name) logger.debug(message) self.widget.sig_status_updated.emit(message) def update_style_sheet(self, style_sheet=None): if style_sheet is None: style_sheet = load_style_sheet() self.menu_options.setStyleSheet(style_sheet) self.menu_versions.setStyleSheet(style_sheet) def actions_menu_requested(self): """ Create and display options menu for the currently selected application. """ self.menu_options.clear() self.menu_versions.clear() # Add versions menu versions = self.versions if self.versions else [] version_actions = [] for version in reversed(versions): action = create_action(self.widget, version, triggered=lambda value, version=version: self.install_application(version=version)) action.setCheckable(True) if self.version == version: action.setChecked(True) action.setDisabled(True) version_actions.append(action) update_action = create_action( self.widget, 'Update application', triggered=lambda: self.update_application()) if versions and versions[-1] == self.version: update_action.setDisabled(True) else: update_action.setDisabled(False) remove_action = create_action( self.widget, 'Remove application', triggered=lambda: self.remove_application()) remove_action.setEnabled(self.installed) actions = [update_action, remove_action, None, self.menu_versions] add_actions(self.menu_options, actions) add_actions(self.menu_versions, version_actions) offset = QPoint(self.button_options.width(), 0) position = self.button_options.mapToGlobal(QPoint(0, 0)) self.menu_versions.setEnabled(bool(versions)) self.menu_options.move(position + offset) self.menu_options.exec_() def update_status(self): if self.prefix: self.version = self.api.conda_package_version(self.prefix, pkg=self.name) self.installed = bool(self.version) if (self.versions and self.version != self.versions[-1] and self.installed): self.button_version.setIcon(QIcon(CONDA_MANAGER_UPGRADE_ARROW)) self.button_version.setStyleSheet( "ButtonApplicationVersion {color: #0071a0;}") self.button_version.setToolTip('Version {0} available'.format( self.versions[-1])) else: self.button_version.setIcon(QIcon()) self.button_version.setStyleSheet( "ButtonApplicationVersion {color: black;}") self.button_install.setVisible(not self.installed) self.button_launch.setVisible(self.installed) def set_loading(self, value): self.button_launch.setDisabled(value) self.button_install.setDisabled(value) self.button_options.setDisabled(value) if value: self.label_spinner.setMovie(self.movie_spinner) else: self.label_spinner.setMovie(None) if self.version is None: version = self.versions[-1] else: version = self.version self.button_version.setText(version) self.label_spinner.setVisible(value) self.button_version.setVisible(not value) def handle_action_finished(self, worker, output, error): if not isinstance(output, dict): output = {} success = output.get('success', True) if error or not success: # Error might be harmless if no decoding was possible... # Success deserves some sort of messagebox logger.error(error) self.widget.sig_application_updated.emit(self.name, self.version) self.update_status() self.set_loading(False) def update_versions(self, version=None, versions=None): """ Update button visibility depending on update availability. """ update = versions[-1] != version logger.debug(str((self.name, self.dev_tool, self.installed))) if self.installed and version: self.button_options.setVisible(True) self.button_version.setText(version) self.button_version.setVisible(True) elif not self.installed and versions: self.button_install.setEnabled(True) self.button_version.setText(versions[-1]) self.button_version.setVisible(True) self.versions = versions self.version = version self.update_status() # --- Public API # ------------------------------------------------------------------------ def install_application(self, value=None, version=None): """ Update the application on the defined prefix environment. This is used for both normal install and specific version install. """ if version: self.version = version else: self.version = self.versions[-1] version = self.versions[-1] pkg = '{0}={1}'.format(self.name, version) pkgs = [pkg] logger.debug(str((pkg, self.dev_tool))) # Check if environment exists and then create or install # is_installed = self.api.conda_package_version(prefix=self.prefix, # pkg=self.name) # pkgs = [pkg] + self.BASIC_PACKAGES # if is_installed: # worker = self.api.conda_install(prefix=self.prefix, pkgs=pkgs) # else: # worker = self.api.conda_create(prefix=self.prefix, pkgs=pkgs) worker = self.api.conda_install(prefix=self.prefix, pkgs=pkgs) worker.sig_finished.connect(self._application_installed) worker.sig_partial.connect(self._partial_output_ready) self.set_loading(True) self.widget.sig_status_updated.emit('Installing application ' '<b>{0}</b>'.format(self.name)) def launch_application(self): """ Launch application installed in prefix environment. """ if self.command is not None: if self.command.startswith('open'): command = self.command.replace("${PREFIX}", self.prefix) elif self.prefix: command = os.sep.join([self.prefix, 'bin', self.command]) else: command = self.command self.button_launch.setDisabled(True) self.timer.setInterval(self.timeout) self.timer.start() update_pointer(Qt.BusyCursor) launch(self.path, command) def remove_application(self): """ Remove the application from the defined prefix environment. """ pkg = self.name pkgs = [pkg] logger.debug(str((self.name, self.dev_tool))) worker = self.api.conda_remove(prefix=self.prefix, pkgs=pkgs) worker.sig_finished.connect(self._application_removed) worker.sig_partial.connect(self._partial_output_ready) self.set_loading(True) self.widget.sig_status_updated.emit('Removing application ' '<b>{0}</b>'.format(self.name)) def update_application(self): """ Update the application on the defined prefix environment. """ logger.debug(str((self.name, self.dev_tool, self.installed))) self.install_application(version=self.versions[-1]) self.widget.sig_status_updated.emit('Updating application ' '<b>{0}</b>'.format(self.name))
class PMTableView(QTableView): """ 基类,用于显示数据。输入数据类型为列表。 """ INSERT_ROW = 0 DELETE_ROW = 1 INSERT_COLUMN = 2 DELETE_COLUMN = 3 signal_need_save = Signal(bool) def __init__(self, data=None): super().__init__() self.translator = create_translator( path=os.path.join(os.path.dirname(__file__), 'translations', 'qt_{0}.qm'.format(QLocale.system().name()))) # translator self.data = None self.menu = QMenu() self.action_insert_row = self.menu.addAction(QCoreApplication.translate('PMTableView','Insert Row')) self.action_insert_row.triggered.connect(lambda: self.on_change_row_col(self.INSERT_ROW)) self.action_delete_row = self.menu.addAction(QCoreApplication.translate('PMTableView','Delete Row')) self.action_delete_row.triggered.connect(lambda: self.on_change_row_col(self.DELETE_ROW)) self.action_insert_col = self.menu.addAction(QCoreApplication.translate('PMTableView','Insert Column')) self.action_insert_col.triggered.connect(lambda: self.on_change_row_col(self.INSERT_COLUMN)) self.action_delete_col = self.menu.addAction(QCoreApplication.translate('PMTableView','Delete Column')) self.action_delete_col.triggered.connect(lambda: self.on_change_row_col(self.DELETE_COLUMN)) # self.menu.addAction("aaaaaa") if data is not None: self.set_data(data) def on_change_row_col(self, operation: int): """ The slot for editting row or columns Args: operation: Returns: """ import pandas as pd import numpy as np pd_data: pd.DataFrame = self.model._data current_index = self.currentIndex() row, column = current_index.row(), current_index.column() if operation == self.INSERT_ROW: prev = pd_data.iloc[:row] lat = pd_data.iloc[row:] self.model._data = pd.concat([prev, pd.DataFrame([[]]), lat]) elif operation == self.DELETE_ROW: self.model._data = pd_data.drop(index=[row], axis=1) elif operation == self.INSERT_COLUMN: col_name, _ = QInputDialog.getText(self, QCoreApplication.translate('PMTableView','Input Column Title'), QCoreApplication.translate('PMTableView','Title')) if _: pd_data.insert(column, col_name, np.nan) elif operation == self.DELETE_COLUMN: self.model._data = pd_data.drop(columns=[column], axis=0) else: raise NotImplementedError self.model.layoutChanged.emit() self.signal_need_save.emit(True) def set_data(self, data): self.data = data self.show_data(data) def get_data(self): return self.model._data def show_data(self, data): import pandas as pd import numpy as np if isinstance(data, pd.DataFrame): self.model = TableModelForPandasDataframe(data, self.data) elif isinstance(data, np.ndarray): self.model = TableModelForNumpyArray(data) self.menu.setEnabled(False) elif isinstance(data, list): self.model = TableModelForList(data) self.menu.setEnabled(True) else: raise Exception("data type %s is not supported in PMTableView.\ \n Supported Types are: numpy.array,list and pandas.DataFrame." % type(data)) self.setModel(self.model) def get_default_slicing_statement(self): return self.model.default_slicing_statement def mouseDoubleClickEvent(self, event: 'QMouseEvent') -> None: """ TODO:编辑功能无效,暂时需要屏蔽掉。 Args: event: Returns: """ super().mouseDoubleClickEvent(event) self.show_edit_dialog(self.currentIndex().row(), self.currentIndex().column()) def keyPressEvent(self, event: QKeyEvent) -> None: super(PMTableView, self).keyPressEvent(event) if event.key() == Qt.Key_Return: self.show_edit_dialog(self.currentIndex().row(), self.currentIndex().column()) def show_edit_dialog(self, row, col): import pandas as pd data = self.model._data if isinstance(data, pd.DataFrame): def on_edited(text): from pandas import Timestamp, Period, Interval try: result = eval(text) data.iloc[row, col] = result self.signal_need_save.emit(True) except: import traceback QMessageBox.warning(self, QCoreApplication.translate('PMTableView','Warning'), traceback.format_exc()) return def on_move_current_cell(direction: int): target_row = row + direction if 0 <= target_row < self.model.rowCount(col): self.setCurrentIndex(self.model.index(target_row, col)) self.show_edit_dialog(target_row, col) original_data = data.iloc[row, col] dlg = InputValueDialog(self) dlg.setWindowTitle(QCoreApplication.translate('PMTableView','Input New Value')) dlg.edit.setText(repr(original_data)) dlg.signal_edit_finished.connect(on_edited) dlg.signal_move_cursor.connect(on_move_current_cell) global_pos = self.mapToGlobal( QPoint(self.columnViewportPosition(col) + 50, self.rowViewportPosition(row) + 50)) dlg.setGeometry(global_pos.x(), global_pos.y(), dlg.width(), dlg.height()) dlg.exec_() # QInputDialog.getText(self, QCoreApplication.translate('PMTableView','Input New Value'), '', QLineEdit.Normal, # text=repr(original_data)) def contextMenuEvent(self, event: QContextMenuEvent): import pandas as pd if isinstance(self.model._data, pd.DataFrame): self.menu.exec_(event.globalPos()) def on_goto_index(self, row: int, col: int = 0): import pandas as pd import numpy as np if isinstance(self.data, (pd.DataFrame, np.ndarray)): assert 0 <= row <= self.model.rowCount(None) self.setCurrentIndex(self.model.index(row, col))