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, version=None, versions=None, image_path=None, prefix=None, needs_license=False, non_conda=False, installed=False, summary=None, ): """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 if description else summary self.command = command self.version = version 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.installed = installed # 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) if self.non_conda: install_action.setDisabled(True) 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 = [ install_action, update_action, remove_action, None, self.menu_versions ] if self.non_conda: # we're not going to support messing with vscode/pycharm via navigator for now update_action.setDisabled(True) remove_action.setDisabled(True) install_action.setDisabled(True) versions = [] self.menu_versions.setDisabled(True) 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: 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: 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) # --- 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. """ 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.""" # version = None is equivalent to saying "most recent version that is compatible with my env" self.install_application(version=None, 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: leave_path_alone = True # args = [self.command] args = external_apps[self.name]( config=self.api.config, process_api=self.api._process_api, conda_api=self.api._conda_api ).executable else: args = self.command 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, )
def context_menu_requested(self, event, right_click=False): """Custom context menu.""" if self.proxy_model is None: return self._menu = QMenu(self) left_click = not right_click 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[C.COL_NAME] # package_type = row_data[C.COL_PACKAGE_TYPE] versions = self.source_model.get_package_versions(name) current_version = self.source_model.get_package_version(name) action_version = row_data[C.COL_ACTION_VERSION] package_status = row_data[C.COL_STATUS] package_type = row_data[C.COL_PACKAGE_TYPE] remove_actions = bool(self.source_model.count_remove_actions()) install_actions = bool(self.source_model.count_install_actions()) update_actions = bool(self.source_model.count_update_actions()) if column in [C.COL_ACTION] and left_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, C.ACTION_NONE, current_version)) action_install = create_action( self, _('Mark for installation'), toggled=lambda: self.set_action_status(model_index, C. ACTION_INSTALL)) action_update = create_action( self, _('Mark for update'), toggled=lambda: self.set_action_status(model_index, C. ACTION_UPDATE, None)) action_remove = create_action( self, _('Mark for removal'), toggled=lambda: self.set_action_status( model_index, C.ACTION_REMOVE, current_version)) version_actions = [] for version in reversed(versions): def trigger(model_index=model_index, action=C.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, C.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 = C.ACTION_UPGRADE else: upgrade_or_downgrade_action = C.ACTION_DOWNGRADE if is_installable: upgrade_or_downgrade_action = C.ACTION_INSTALL version_action = create_action( self, version, icon=QIcon(), triggered=trigger(model_index, upgrade_or_downgrade_action, version)) if action_version == version: version_action.setCheckable(True) version_action.setChecked(True) 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_update, action_remove ] actions += [None, install_versions_menu] # Disable firing of signals, while setting the checked status for ac in actions + version_actions: if ac: ac.blockSignals(True) if action_status == C.ACTION_NONE: action_unmark.setEnabled(False) action_install.setEnabled(is_installable) action_update.setEnabled(is_upgradable) action_remove.setEnabled(is_removable) if install_actions: # Invalidate remove and update if install actions selected action_update.setDisabled(True) action_remove.setDisabled(True) elif remove_actions: # Invalidate install/update if remove actions already action_install.setDisabled(True) action_update.setDisabled(True) elif update_actions: # Invalidate install/update if remove actions already action_install.setDisabled(True) action_remove.setDisabled(True) install_versions_menu.setDisabled(False) elif action_status == C.ACTION_INSTALL: action_unmark.setEnabled(True) action_install.setEnabled(False) action_install.setChecked(True) action_update.setEnabled(False) action_remove.setEnabled(False) elif action_status == C.ACTION_REMOVE: action_unmark.setEnabled(True) action_install.setEnabled(False) action_update.setEnabled(False) action_remove.setEnabled(False) action_remove.setChecked(True) elif action_status == C.ACTION_UPDATE: action_unmark.setEnabled(True) action_install.setEnabled(False) action_update.setEnabled(False) action_update.setChecked(True) action_remove.setEnabled(False) elif action_status in [C.ACTION_UPGRADE, C.ACTION_DOWNGRADE]: action_unmark.setEnabled(True) action_install.setEnabled(False) action_update.setEnabled(False) action_update.setChecked(False) action_remove.setEnabled(False) install_versions_menu.setEnabled(False) if package_status == C.NOT_INSTALLED: action_remove.setEnabled(False) action_update.setEnabled(False) if package_type == C.PIP_PACKAGE: action_unmark.setEnabled(False) action_install.setEnabled(False) action_update.setEnabled(False) action_remove.setEnabled(False) # Enable firing of signals, while setting the checked status for ac in actions + version_actions: if ac: ac.blockSignals(False) install_versions_menu.setDisabled(True) install_versions_menu.setEnabled( len(version_actions) > 1 and not remove_actions and not update_actions) elif right_click: license_ = row_data[C.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)
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)