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 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 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 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))