class PreferencesDialog(DialogBase): """Application preferences dialog.""" sig_urls_updated = Signal(str, str) sig_check_ready = Signal() sig_reset_ready = Signal() def __init__(self, config=CONF, **kwargs): """Application preferences dialog.""" super(PreferencesDialog, self).__init__(**kwargs) self.api = AnacondaAPI() self.widgets_changed = set() self.widgets = [] self.widgets_dic = {} self.config = config # Widgets self.button_ok = ButtonPrimary('Apply') self.button_cancel = ButtonNormal('Cancel') self.button_reset = ButtonNormal('Reset to defaults') self.row = 0 # Widget setup self.setWindowTitle("Preferences") # Layouts self.grid_layout = QGridLayout() buttons_layout = QHBoxLayout() buttons_layout.addWidget(self.button_reset) buttons_layout.addStretch() buttons_layout.addWidget(self.button_cancel) buttons_layout.addWidget(SpacerHorizontal()) buttons_layout.addWidget(self.button_ok) main_layout = QVBoxLayout() main_layout.addLayout(self.grid_layout) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(SpacerVertical()) main_layout.addLayout(buttons_layout) self.setLayout(main_layout) # Signals self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.button_reset.clicked.connect(self.reset_to_defaults) self.button_reset.clicked.connect( lambda: self.button_ok.setEnabled(True) ) # Setup self.grid_layout.setSpacing(0) self.setup() self.button_ok.setDisabled(True) self.widgets[0].setFocus() self.button_ok.setDefault(True) self.button_ok.setAutoDefault(True) # --- Helpers # ------------------------------------------------------------------------- def get_option(self, option): """Get configuration option from `main` section.""" return self.config.get('main', option, None) def set_option(self, option, value): """Set configuration option in `main` section.""" self.config.set('main', option, value) def get_option_default(self, option): """Get configuration option default value in `main` section.""" return self.config.get_default('main', option) def set_option_default(self, option): """Set configuration option default value in `main` section.""" self.set_option(option, self.get_option_default(option)) def create_widget( self, widget=None, label=None, option=None, hint=None, check=None, info=None, ): """Create preference option widget and add to layout.""" config_value = self.get_option(option) widget._text = label widget.label = QLabel(label) widget.option = option widget.set_value(config_value) widget.label_information = QLabel() widget.label_information.setMinimumWidth(16) widget.label_information.setMaximumWidth(16) form_widget = QWidget() h_layout = QHBoxLayout() h_layout.addSpacing(4) h_layout.addWidget(widget.label_information, 0, Qt.AlignRight) h_layout.addWidget(widget, 0, Qt.AlignLeft) h_layout.addWidget(QLabel(hint or ''), 0, Qt.AlignLeft) form_widget.setLayout(h_layout) if check: widget.check_value = lambda value: check(value) else: widget.check_value = lambda value: (True, '') if info: label = widget.label_information label = PreferencesDialog.update_icon(label, INFO_ICON) label.setToolTip(info) self.widgets.append(widget) self.widgets_dic[option] = widget self.grid_layout.addWidget( widget.label, self.row, 0, Qt.AlignRight | Qt.AlignCenter ) self.grid_layout.addWidget( form_widget, self.row, 1, Qt.AlignLeft | Qt.AlignCenter ) self.row += 1 def create_textbox(self, label, option, hint=None, check=None, info=None): """Create textbox (QLineEdit) preference option.""" widget = QLineEdit() widget.setAttribute(Qt.WA_MacShowFocusRect, False) widget.setMinimumWidth(250) widget.get_value = lambda w=widget: w.text() widget.set_value = lambda value, w=widget: w.setText(value) widget.set_warning = lambda w=widget: w.setSelection(0, 1000) widget.textChanged.connect( lambda v=None, w=widget: self.options_changed(widget=w) ) self.create_widget( widget=widget, option=option, label=label, hint=hint, check=check, info=info, ) def create_checkbox(self, label, option, check=None, hint=None, info=None): """Create checkbox preference option.""" widget = QCheckBox() widget.get_value = lambda w=widget: bool(w.checkState()) widget.set_value = lambda value, w=widget: bool( w.setCheckState(Qt.Checked if value else Qt.Unchecked) ) api_widget = self.widgets_dic['anaconda_api_url'] widget.set_warning = lambda w=widget: api_widget widget.stateChanged.connect( lambda v=None, w=widget: self.options_changed(widget=w) ) self.create_widget( widget=widget, option=option, label=label, hint=hint, check=check, info=info, ) def options_changed(self, value=None, widget=None): """Callback helper triggered on preference value change.""" config_value = self.get_option(widget.option) if config_value != widget.get_value(): self.widgets_changed.add(widget) else: if widget in self.widgets_changed: self.widgets_changed.remove(widget) self.button_ok.setDisabled(not bool(len(self.widgets_changed))) def widget_for_option(self, option): """Return the widget for the given option.""" return self.widgets_dic[option] # --- API # ------------------------------------------------------------------------- def set_initial_values(self): """ Set configuration values found in other config files. Some options of configuration are found in condarc or in anaconda-client configuration. """ self.config.set( 'main', 'anaconda_api_url', self.api.client_get_api_url() ) # See https://conda.io/docs/install/central.html # ssl_verify overloads True/False/<Path to certificate> # Navigator splits that into 2 separate options for clarity ssl_verify = self.api.client_get_ssl() if isinstance(ssl_verify, bool): self.config.set('main', 'ssl_verification', ssl_verify) self.config.set('main', 'ssl_certificate', None) else: self.config.set('main', 'ssl_verification', True) self.config.set('main', 'ssl_certificate', ssl_verify) def setup(self): """Setup the preferences dialog.""" def api_url_checker(value): """ Custom checker to use selected ssl option instead of stored one. This allows to set an unsafe api url directly on the preferences dialog. Without this, one would have to first disable, click accept, then open preferences again and change api url for it to work. """ # Ssl widget ssl_widget = self.widgets_dic.get('ssl_verification') verify = ssl_widget.get_value() if ssl_widget else True # Certificate path ssl_cert_widget = self.widgets_dic.get('ssl_certificate') if ssl_cert_widget: verify = ssl_cert_widget.get_value() # Offline mode offline_widget = self.widgets_dic.get('offline_mode') if ssl_widget or ssl_cert_widget: offline_mode = offline_widget.get_value() else: offline_mode = False if offline_mode: basic_check = ( False, 'API Domain cannot be modified when ' 'working in <b>offline mode</b>.<br>', ) else: basic_check = self.is_valid_api(value, verify=verify) return basic_check def ssl_checker(value): """Counterpart to api_url_checker.""" api_url_widget = self.widgets_dic.get('anaconda_api_url') api_url = api_url_widget.get_value() return self.is_valid_api(api_url, verify=value) def ssl_certificate_checker(value): """Check if certificate path is valid/exists.""" ssl_widget = self.widgets_dic.get('ssl_verification') verify = ssl_widget.get_value() if ssl_widget else True ssl_cert_widget = self.widgets_dic.get('ssl_certificate') path = ssl_cert_widget.get_value() return self.is_valid_cert_file(path, verify) self.set_initial_values() self.create_textbox( 'Anaconda API domain', 'anaconda_api_url', check=api_url_checker, ) self.create_checkbox( 'Enable SSL verification', 'ssl_verification', check=ssl_checker, hint=( '<i>Disabling this option is not <br>' 'recommended for security reasons</i>' ), ) self.create_textbox( 'SSL certificate path (Optional)', 'ssl_certificate', check=ssl_certificate_checker, ) info = '''To help us improve Anaconda Navigator, fix bugs, and make it even easier for everyone to use Python, we gather anonymized usage information, just like most web browsers and mobile apps.''' self.create_checkbox( 'Quality improvement reporting', 'provide_analytics', info=info, ) info_offline = DialogOfflineMode.MESSAGE_PREFERENCES extra = '<br><br>' if WIN7 else '' self.create_checkbox( 'Enable offline mode', 'offline_mode', info=info_offline + extra, ) self.create_checkbox('Hide offline mode dialog', 'hide_offline_dialog') self.create_checkbox('Hide quit dialog', 'hide_quit_dialog') self.create_checkbox( 'Hide update dialog on startup', 'hide_update_dialog' ) self.create_checkbox( 'Hide running applications dialog', 'hide_running_apps_dialog' ) self.create_checkbox( 'Enable high DPI scaling', 'enable_high_dpi_scaling' ) self.create_checkbox( 'Show application startup error messages', 'show_application_launch_errors' ) ssl_ver_widget = self.widgets_dic.get('ssl_verification') ssl_ver_widget.stateChanged.connect(self.enable_disable_cert) ssl_cert_widget = self.widgets_dic.get('ssl_certificate') ssl_cert_widget.setPlaceholderText( 'Certificate to verify SSL connections' ) # Refresh enabled/disabled status of certificate textbox self.enable_disable_cert() def enable_disable_cert(self, value=None): """Refresh enabled/disabled status of certificate textbox.""" ssl_cert_widget = self.widgets_dic.get('ssl_certificate') if value: value = bool(value) else: ssl_ver_widget = self.widgets_dic.get('ssl_verification') value = bool(ssl_ver_widget.checkState()) ssl_cert_widget.setEnabled(value) @staticmethod def update_icon(label, icon): """Update icon for information or warning.""" pixmap = QPixmap(icon) label.setScaledContents(True) label.setPixmap( pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation) ) return label @staticmethod def warn(widget, text=None): """Display warning for widget in preferences.""" label = widget.label_information if text: label = PreferencesDialog.update_icon(label, WARNING_ICON) label.setToolTip(str(text)) w = widget.label_information.width() / 2 h = widget.label_information.height() / 2 position = widget.label_information.mapToGlobal(QPoint(w, h)) QCursor.setPos(position) else: label.setPixmap(QPixmap()) label.setToolTip('') # --- Checkers # ------------------------------------------------------------------------- def is_valid_url(self, url): """Check if a given URL returns a 200 code.""" output = self.api.download_is_valid_url(url, non_blocking=False) error = '' if not output: error = 'Invalid api url.' return output, error def is_valid_cert_file(self, path, verify): """"Check if ssl certificate file in given path exists.""" output = True error = '' # Only validate if it is not empty and if ssl_verification is checked if path.strip() and verify: output = os.path.isfile(path) if not output: error = 'File not found.' return output, error def is_valid_api(self, url, verify=True): """Check if a given URL is a valid anaconda api endpoint.""" output = self.api.download_is_valid_api_url( url, non_blocking=False, verify=verify, ) error = '' if not output: url_api_1 = '' url_api_2 = '' if '/api' not in url and self.is_valid_url(url)[0]: url_api_1 = url.replace('https://', 'https://api.') url_api_1 = url_api_1.replace('http://', 'http://api.') if url.endswith('/'): url_api_2 = url + 'api' else: url_api_2 = url + '/api' error = ( 'Invalid Anaconda API url. <br>' '<br>Try using:<br><b>{0}</b> or <br>' '<b>{1}</b>'.format(url_api_1, url_api_2) ) else: error = ( 'Invalid Anaconda API url.<br><br>' 'Check the url is valid and corresponds to the api ' 'endpoint.' ) return output, error def run_checks(self): """ Run all check functions on configuration options. This method checks and warns but it does not change/set values. """ checks = [] for widget in self.widgets_changed: value = widget.get_value() check, error = widget.check_value(value) checks.append(check) if check: self.warn(widget) else: self.button_ok.setDisabled(True) widget.set_warning() self.warn(widget, error) break # Emit checks ready self.sig_check_ready.emit() return checks def reset_to_defaults(self): """Reset the preferences to the default values.""" for widget in self.widgets: option = widget.option default = self.get_option_default(option) widget.set_value(default) # Flag all values as updated self.options_changed(widget=widget, value=default) self.sig_reset_ready.emit() def accept(self): """Override Qt method.""" sig_updated = False anaconda_api_url = None checks = self.run_checks() # Update values if checks and all(checks): for widget in self.widgets_changed: value = widget.get_value() self.set_option(widget.option, value) # Settings not stored on Navigator config, but taken from # anaconda-client config if widget.option == 'anaconda_api_url': anaconda_api_url = value # Store it to be emitted self.api.client_set_api_url(value) sig_updated = True # ssl_verify/verify_ssl handles True/False/<Path to cert> # On navi it is split in 2 options for clarity if widget.option in ['ssl_certificate', 'ssl_verification']: ssl_veri = self.widgets_dic.get('ssl_verification') ssl_cert = self.widgets_dic.get('ssl_certificate') verify = ssl_veri.get_value() path = ssl_cert.get_value() if path.strip() and verify: value = path else: value = verify self.api.client_set_ssl(value) if sig_updated and anaconda_api_url: def _api_info(worker, output, error): conda_url = output.get('conda_url') try: self.sig_urls_updated.emit(anaconda_api_url, conda_url) super(PreferencesDialog, self).accept() except RuntimeError: # Some tests on appveyor/circleci fail pass worker = self.api.api_urls() worker.sig_chain_finished.connect(_api_info) super(PreferencesDialog, self).accept()
class ImportDialog(DialogBase): """Import project from folder or specification files.""" CONDA_ENV_FILES = 'Conda environment files (*.yaml *.yml)' CONDA_SPEC_FILES = 'Conda explicit specification files (*.txt)' PIP_REQUIREMENT_FILES = 'Pip requirement files (*.txt)' def __init__(self, parent=None, projects=None): """Import project from folder or environment files.""" super(ImportDialog, self).__init__(parent=parent) self.projects = projects if projects else {} self.selected_file_filter = None self._path = None # Widgets self.label_info = LabelSpecInfo('', parent=self) self.label_name = QLabel("Project name") self.label_path = QLabel("Specification File") self.text_name = QLineEdit() self.text_path = QLineEdit() self.button_path = ButtonNormal("") self.radio_folder = QRadioButton('From folder') self.radio_spec = QRadioButton('From specification file') self.button_cancel = ButtonNormal('Cancel') self.button_ok = ButtonPrimary('Import') # Widgets setup self.button_path.setObjectName('import') self.button_ok.setDefault(True) self.text_path.setPlaceholderText("File to import from") self.text_name.setPlaceholderText("New project name") self.setMinimumWidth(380) self.setWindowTitle("Import new project") self.text_name.setValidator(get_regex_validator()) # Layouts layout_radio = QHBoxLayout() layout_radio.addWidget(self.radio_folder) layout_radio.addWidget(SpacerHorizontal()) layout_radio.addWidget(self.radio_spec) layout_infile = QHBoxLayout() layout_infile.addWidget(self.text_path) layout_infile.addWidget(SpacerHorizontal()) layout_infile.addWidget(self.button_path) layout_grid = QGridLayout() layout_grid.addWidget(self.label_name, 0, 0, 1, 2) layout_grid.addWidget(SpacerHorizontal(), 0, 2) layout_grid.addWidget(self.text_name, 0, 3) layout_grid.addWidget(SpacerVertical(), 1, 0) layout_grid.addWidget(self.label_path, 2, 0) layout_grid.addWidget(self.label_info, 2, 1) layout_grid.addWidget(SpacerHorizontal(), 2, 2) layout_grid.addLayout(layout_infile, 2, 3) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) layout = QVBoxLayout() layout.addLayout(layout_radio) layout.addWidget(SpacerVertical()) layout.addLayout(layout_grid) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_buttons) self.setLayout(layout) # Signals self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.button_path.clicked.connect(self.choose) self.text_path.textChanged.connect(self.refresh) self.text_name.textChanged.connect(self.refresh) self.radio_folder.toggled.connect(self.refresh) self.radio_spec.toggled.connect(self.refresh) # Setup self.radio_folder.toggle() self.refresh() def refresh(self, text=''): """Update the status of buttons based on radio selection.""" if self.radio_folder.isChecked(): self.text_path.setPlaceholderText("Folder to import from") self.label_path.setText('Folder') self.label_info.setVisible(False) else: self.label_info.setVisible(True) self.label_path.setText('File ') self.text_path.setPlaceholderText("File to import from") text = self.text_name.text() path = self.text_path.text() if (text and path and os.path.exists(path) and is_valid_project_name(text, self.projects)): self.button_ok.setDisabled(False) self.button_ok.setDefault(True) else: self.button_ok.setDisabled(True) self.button_cancel.setDefault(True) def choose(self): """Display file dialog to select environment specification.""" selected_filter = None if self.radio_spec.isChecked(): path, selected_filter = getopenfilename( caption="Import Project", basedir=HOME_PATH, parent=None, filters="{0};;{1};;{2}".format(self.CONDA_ENV_FILES, self.CONDA_SPEC_FILES, self.PIP_REQUIREMENT_FILES)) else: path = getexistingdirectory( caption="Import Project", basedir=HOME_PATH, parent=None, ) if path: name = self.text_name.text() self.selected_file_filter = selected_filter self.text_path.setText(path) self.refresh(path) self.text_name.setText(name) @property def name(self): """Return the project name.""" return self.text_name.text().strip() @property def path(self): """Return the project path to import (file or folder).""" return self.text_path.text()
class AuthenticationDialog(DialogBase): """Login dialog.""" # See https://github.com/Anaconda-Platform/anaconda-server settings USER_RE = QRegExp('^[A-Za-z0-9_][A-Za-z0-9_-]+$') FORGOT_USERNAME_URL = 'account/forgot_username' FORGOT_PASSWORD_URL = 'account/forgot_password' sig_authentication_succeeded = Signal() sig_authentication_failed = Signal() sig_url_clicked = Signal(object) def __init__(self, api, parent=None): """Login dialog.""" super(AuthenticationDialog, self).__init__(parent) self._parent = parent self.config = CONF self.api = api self.token = None self.error = None self.tracker = GATracker() self.forgot_username_url = None self.forgot_password_url = None # Widgets self.label_username = QLabel('Username:'******'Password:'******'<hr><br><b>Already a member? ' 'Sign in!</b><br>') # For styling purposes the label next to a ButtonLink is also a button # so they align adequately self.button_register_text = ButtonLabel('You can register by ' 'visiting the ') self.button_register = ButtonLink('Anaconda Cloud') self.button_register_after_text = ButtonLabel('website.') self.label_information = QLabel(''' <strong>Anaconda Cloud</strong> is where packages, notebooks, and <br> environments are shared. It provides powerful <br> collaboration and package management for open <br> source and private projects.<br> ''') self.label_message = QLabel('') self.button_forgot_username = ButtonLink('I forgot my username') self.button_forgot_password = ButtonLink('I forgot my password') self.button_login = ButtonPrimary('Login') self.button_cancel = ButtonNormal('Cancel') # Widgets setup self.button_login.setDefault(True) username_validator = QRegExpValidator(self.USER_RE) self.text_username.setValidator(username_validator) self.setMinimumWidth(260) self.setWindowTitle('Sign in') # This allows to completely style the dialog with css using the frame self.text_password.setEchoMode(QLineEdit.Password) self.label_message.setVisible(False) # Layout grid_layout = QVBoxLayout() grid_layout.addWidget(self.label_username) # grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.text_username) grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.label_password) # grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.text_password) main_layout = QVBoxLayout() main_layout.addWidget(self.label_information) register_layout = QHBoxLayout() register_layout.addWidget(self.button_register_text) register_layout.addWidget(self.button_register) register_layout.addWidget(self.button_register_after_text) register_layout.addStretch() main_layout.addLayout(register_layout) main_layout.addWidget(self.label_signin_text) main_layout.addLayout(grid_layout) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(self.label_message) main_layout.addWidget(self.button_forgot_username, 0, Qt.AlignRight) main_layout.addWidget(self.button_forgot_password, 0, Qt.AlignRight) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_login) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(SpacerVertical()) main_layout.addLayout(layout_buttons) self.setLayout(main_layout) # Signals self.text_username.textEdited.connect(self.check_text) self.text_password.textEdited.connect(self.check_text) self.button_login.clicked.connect(self.login) self.button_cancel.clicked.connect(self.reject) # Setup self.check_text() self.update_style_sheet() self.text_username.setFocus() self.setup() def setup(self): """Setup login dialog.""" self.update_links() def update_links(self): """Update links.""" for button in [ self.button_forgot_username, self.button_forgot_password, self.button_register, ]: try: button.disconnect() except TypeError: # pragma: no cover pass # TODO, get this from anaconda client directly? # from binstar_client.utils import get_config, set_config # config = get_config() anaconda_api_url = self.config.get('main', 'anaconda_api_url', None) if anaconda_api_url: # Remove api if using a subdomain base_url = anaconda_api_url.lower().replace('//api.', '//') self.base_url = base_url # Remove api if not using a subdomain parts = base_url.lower().split('/') if parts[-1] == 'api': base_url = '/'.join(parts[:-1]) self.forgot_username_url = (base_url + '/' + self.FORGOT_USERNAME_URL) self.forgot_password_url = (base_url + '/' + self.FORGOT_PASSWORD_URL) self.button_register.clicked.connect( lambda: self.open_url(base_url)) self.button_forgot_username.clicked.connect( lambda: self.open_url(self.forgot_username_url)) self.button_forgot_password.clicked.connect( lambda: self.open_url(self.forgot_password_url)) @property def username(self): """Return the logged username.""" return self.text_username.text().lower() def update_style_sheet(self, style_sheet=None): """Update custom css style sheet.""" if style_sheet is None: style_sheet = load_style_sheet() self.setStyleSheet(style_sheet) def check_text(self): """Check that `username` and `password` are valid. If not empty and disable/enable buttons accordingly. """ username = self.text_username.text() password = self.text_password.text() if len(username) == 0 or len(password) == 0: self.button_login.setDisabled(True) else: self.button_login.setDisabled(False) def login(self): """Try to log the user in the specified anaconda api endpoint.""" self.button_login.setEnabled(False) self.text_username.setText(self.text_username.text().lower()) QApplication.setOverrideCursor(Qt.WaitCursor) self.label_message.setText('') worker = self.api.login(self.text_username.text().lower(), self.text_password.text()) worker.sig_finished.connect(self._finished) def _finished(self, worker, output, error): """Callback for the login procedure after worker has finished.""" token = output if token: self.token = token self.sig_authentication_succeeded.emit() self.accept() elif error: username = self.text_username.text().lower() bold_username = '******'.format(username) # The error might come in (error_message, http_error) format try: error_message = ast.literal_eval(str(error))[0] except Exception: # pragma: no cover error_message = str(error) error_message = error_message.lower().capitalize() error_message = error_message.split(', ')[0] error_text = '<i>{0}</i>'.format(error_message) error_text = error_text.replace(username, bold_username) self.label_message.setText(error_text) self.label_message.setVisible(True) if error_message: domain = self.api.client_domain() label = '{0}/{1}: {2}'.format(domain, username, error_message.lower()) self.tracker.track_event( 'authenticate', 'login failed', label=label, ) self.text_password.setFocus() self.text_password.selectAll() self.sig_authentication_failed.emit() self.button_login.setDisabled(False) self.check_text() QApplication.restoreOverrideCursor() def open_url(self, url): """Open given url in the default browser and log the action.""" self.tracker.track_event('content', 'click', url) self.sig_url_clicked.emit(url) QDesktopServices.openUrl(QUrl(url))
class DialogChannels(DialogBase): """Dialog to add delete and select active conda package channels.""" sig_channels_updated = Signal(object, object) # added, removed sig_setup_ready = Signal() sig_check_ready = Signal() WIDTH = 550 def __init__(self, parent=None): """Dialog to add delete and select active conda pacakge channels .""" super(DialogChannels, self).__init__(parent) self._parent = parent self._conda_url = 'https://conda.anaconda.org' self.api = AnacondaAPI() self.initial_sources = None self.config_sources = None self.style_sheet = None self._setup_ready = False self._conda_url_setup_ready = False # Widgets self.list = ListWidgetChannels(parent=self, api=self.api) self.label_info = LabelBase( 'Manage channels you want Navigator to include.') self.label_status = LabelBase('Collecting sources...') self.progress_bar = QProgressBar(self) self.button_add = ButtonNormal('Add...') self.button_cancel = ButtonNormal('Cancel') self.button_ok = ButtonPrimary('Update channels') # Widget setup self.frame_title_bar.setVisible(False) self.list.setFrameStyle(QFrame.NoFrame) self.list.setFrameShape(QFrame.NoFrame) self.setWindowFlags(self.windowFlags() | Qt.Popup) self.setWindowOpacity(0.96) self.setMinimumHeight(300) self.setMinimumWidth(self.WIDTH) self.setModal(True) # Layout layout_button = QHBoxLayout() layout_button.addWidget(self.label_info) layout_button.addStretch() layout_button.addWidget(self.button_add) layout_ok = QHBoxLayout() layout_ok.addWidget(self.label_status) layout_ok.addWidget(SpacerHorizontal()) layout_ok.addWidget(self.progress_bar) layout_ok.addWidget(SpacerHorizontal()) layout_ok.addStretch() layout_ok.addWidget(self.button_cancel) layout_ok.addWidget(SpacerHorizontal()) layout_ok.addWidget(self.button_ok) layout = QVBoxLayout() layout.addLayout(layout_button) layout.addWidget(SpacerVertical()) layout.addWidget(self.list) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_ok) self.setLayout(layout) # Signals self.button_add.clicked.connect(self.add_channel) self.button_ok.clicked.connect(self.update_channels) self.button_cancel.clicked.connect(self.reject) self.list.sig_status_updated.connect(self.update_status) self.list.sig_channel_added.connect( lambda v=None: self.set_tab_order()) self.list.sig_channel_added.connect( lambda v=None: self.button_ok.setFocus()) self.list.sig_channel_removed.connect( lambda v=None: self.set_tab_order()) self.list.sig_channel_removed.connect( lambda v=None: self.button_ok.setFocus()) self.list.sig_channel_checked.connect(self.sig_check_ready) self.list.sig_channel_status.connect(self.refresh) self.button_add.setDisabled(True) self.button_ok.setDisabled(True) self.button_cancel.setDisabled(True) self.update_status(action='Collecting sources...', value=0, max_value=0) @staticmethod def _group_sources_and_channels(sources): """ Flatten sources and channels dictionary to list of tuples. [(source, channel), (source, channel)...] """ grouped = [] for source, channels in sources.items(): for channel in channels: grouped.append((source, channel)) return grouped def keyPressEvent(self, event): """Override Qt method.""" key = event.key() if key in [Qt.Key_Escape]: if self.list.is_editing: self.refresh() self.list.is_editing = False else: self.reject() # --- Public API # ------------------------------------------------------------------------- def update_style_sheet(self, style_sheet=None): """Update custom css style sheets.""" if style_sheet is None: self.style_sheet = load_style_sheet() else: self.style_sheet = style_sheet self.setStyleSheet(self.style_sheet) self.setMinimumWidth(SASS_VARIABLES.WIDGET_CHANNEL_DIALOG_WIDTH) try: self.list.update_style_sheet(style_sheet) except Exception: pass def update_api(self, worker, api_info, error): """Update api info.""" self._conda_url = api_info.get('conda_url', 'https://conda.anaconda.org') self._conda_url_setup_ready = True if self._setup_ready: self.sig_setup_ready.emit() def setup(self, worker, conda_config_data, error): """Setup the channels widget.""" self.config_sources = conda_config_data.get('config_sources') self.button_add.setDisabled(False) for source, data in self.config_sources.items(): channels = data.get('channels', []) for channel in channels: item = ListWidgetItemChannel(channel=channel, location=source) item.set_editable(False) self.list.addItem(item) self.set_tab_order() self.button_add.setFocus() self.button_ok.setDefault(True) self.button_cancel.setEnabled(True) self.initial_sources = self.list.sources.copy() self.update_status() self._setup_ready = True if self._conda_url_setup_ready: self.sig_setup_ready.emit() def set_tab_order(self): """Fix the tab ordering in the list.""" if self.list._items: self.setTabOrder(self.button_add, self.list._items[0].button_remove) self.setTabOrder(self.list._items[-1].button_remove, self.button_cancel) self.setTabOrder(self.button_cancel, self.button_ok) self.refresh() def add_channel(self): """Add new conda channel.""" user_rc_path = self.api._conda_api.user_rc_path item = ListWidgetItemChannel(channel='', location=user_rc_path) self.list.addItem(item) self.refresh(False) def update_channels(self): """Update channels list and status.""" sources = self.list.sources original = self._group_sources_and_channels(self.initial_sources) updated = self._group_sources_and_channels(sources) if sorted(original) != sorted(updated): self.sig_channels_updated.emit(*self.sources) self.accept() else: self.reject() def refresh(self, channel_status=True): """Update enable/disable status based on item count.""" self.button_add.setEnabled(channel_status and bool(self.list.count)) self.button_ok.setEnabled(channel_status) self.button_cancel.setEnabled(True) if self.list.count() == 0: self.button_add.setEnabled(True) self.button_ok.setEnabled(False) def update_status(self, action='', message='', value=None, max_value=None): """Update the status and progress bar of the widget.""" visible = bool(action) self.label_status.setText(action) self.label_status.setVisible(visible) if value is not None and max_value is not None: self.progress_bar.setVisible(True) self.progress_bar.setRange(0, max_value) self.progress_bar.setValue(value) else: self.progress_bar.setVisible(False) @property def sources(self): """Return sources to add and remove from config.""" original = self._group_sources_and_channels(self.initial_sources) updated = self._group_sources_and_channels(self.list.sources) original = set(original) updated = set(updated) add = updated - original remove = original - updated return add, remove
class PackagesDialog(DialogBase): """Package dependencies dialog.""" sig_setup_ready = Signal() def __init__( self, parent=None, packages=None, pip_packages=None, remove_only=False, update_only=False, ): """About dialog.""" super(PackagesDialog, self).__init__(parent=parent) # Variables self.api = AnacondaAPI() self.actions = None self.packages = packages or [] self.pip_packages = pip_packages or [] # Widgets self.stack = QStackedWidget() self.table = QTableWidget() self.text = QTextEdit() self.label_description = LabelBase() self.label_status = LabelBase() self.progress_bar = QProgressBar() self.button_ok = ButtonPrimary('Apply') self.button_cancel = ButtonNormal('Cancel') # Widget setup self.text.setReadOnly(True) self.stack.addWidget(self.table) self.stack.addWidget(self.text) if remove_only: text = 'The following packages will be removed:<br>' else: text = 'The following packages will be modified:<br>' self.label_description.setText(text) self.label_description.setWordWrap(True) self.label_description.setWordWrap(True) self.label_status.setWordWrap(True) self.table.horizontalScrollBar().setVisible(False) self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setAlternatingRowColors(True) self.table.setSelectionMode(QAbstractItemView.NoSelection) self.table.setSortingEnabled(True) self._hheader = self.table.horizontalHeader() self._vheader = self.table.verticalHeader() self._hheader.setStretchLastSection(True) self._hheader.setDefaultAlignment(Qt.AlignLeft) self._hheader.setSectionResizeMode(self._hheader.Fixed) self._vheader.setSectionResizeMode(self._vheader.Fixed) self.button_ok.setMinimumWidth(70) self.button_ok.setDefault(True) self.base_minimum_width = 300 if remove_only else 420 if remove_only: self.setWindowTitle("Remove Packages") elif update_only: self.setWindowTitle("Update Packages") else: self.setWindowTitle("Install Packages") self.setMinimumWidth(self.base_minimum_width) # Layouts layout_progress = QHBoxLayout() layout_progress.addWidget(self.label_status) layout_progress.addWidget(SpacerHorizontal()) layout_progress.addWidget(self.progress_bar) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) layout = QVBoxLayout() layout.addWidget(self.label_description) layout.addWidget(SpacerVertical()) layout.addWidget(self.stack) layout.addWidget(SpacerVertical()) layout.addLayout(layout_progress) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_buttons) self.setLayout(layout) # Signals self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.button_ok.setDisabled(True) # Setup self.table.setDisabled(True) self.update_status('Solving package specifications', value=0, max_value=0) def setup(self, worker, output, error): """Setup the widget to include the list of dependencies.""" if not isinstance(output, dict): output = {} packages = sorted(pkg.split('==')[0] for pkg in self.packages) success = output.get('success') error = output.get('error', '') exception_name = output.get('exception_name', '') actions = output.get('actions', []) prefix = worker.prefix if exception_name: message = exception_name else: # All requested packages already installed message = output.get('message', ' ') navi_deps_error = self.api.check_navigator_dependencies( actions, prefix) description = self.label_description.text() if error: description = 'No packages will be modified.' self.stack.setCurrentIndex(1) self.button_ok.setDisabled(True) if self.api.is_offline(): error = ("Some of the functionality of Anaconda Navigator " "will be limited in <b>offline mode</b>. <br><br>" "Installation and upgrade actions will be subject to " "the packages currently available on your package " "cache.") self.text.setText(error) elif navi_deps_error: description = 'No packages will be modified.' error = ('Downgrading/removing these packages will modify ' 'Anaconda Navigator dependencies.') self.text.setText(error) self.stack.setCurrentIndex(1) message = 'NavigatorDependenciesError' self.button_ok.setDisabled(True) elif success and actions: self.stack.setCurrentIndex(0) # Conda 4.3.x if isinstance(actions, list): actions_link = actions[0].get('LINK', []) actions_unlink = actions[0].get('UNLINK', []) # Conda 4.4.x else: actions_link = actions.get('LINK', []) actions_unlink = actions.get('UNLINK', []) deps = set() deps = deps.union({p['name'] for p in actions_link}) deps = deps.union({p['name'] for p in actions_unlink}) deps = deps - set(packages) deps = sorted(list(deps)) count_total_packages = len(packages) + len(deps) plural_total = 's' if count_total_packages != 1 else '' plural_selected = 's' if len(packages) != 1 else '' self.table.setRowCount(count_total_packages) self.table.setColumnCount(4) if actions_link: description = '{0} package{1} will be installed'.format( count_total_packages, plural_total) self.table.showColumn(2) self.table.showColumn(3) elif actions_unlink and not actions_link: self.table.hideColumn(2) self.table.hideColumn(3) self.table.setHorizontalHeaderLabels( ['Name', 'Unlink', 'Link', 'Channel']) description = '{0} package{1} will be removed'.format( count_total_packages, plural_total) for row, pkg in enumerate(packages + deps): link_item = [p for p in actions_link if p['name'] == pkg] if not link_item: link_item = { 'version': '-'.center(len('link')), 'channel': '-'.center(len('channel')), } else: link_item = link_item[0] unlink_item = [p for p in actions_unlink if p['name'] == pkg] if not unlink_item: unlink_item = { 'version': '-'.center(len('link')), } else: unlink_item = unlink_item[0] unlink_version = str(unlink_item['version']) link_version = str(link_item['version']) item_unlink_v = QTableWidgetItem(unlink_version) item_link_v = QTableWidgetItem(link_version) item_link_c = QTableWidgetItem(link_item['channel']) if pkg in packages: item_name = QTableWidgetItem(pkg) else: item_name = QTableWidgetItem('*' + pkg) items = [item_name, item_unlink_v, item_link_v, item_link_c] for column, item in enumerate(items): item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self.table.setItem(row, column, item) if deps: message = ( '<b>*</b> indicates the package is a dependency of a ' 'selected package{0}<br>').format(plural_selected) self.button_ok.setEnabled(True) self.table.resizeColumnsToContents() unlink_width = self.table.columnWidth(1) if unlink_width < 60: self.table.setColumnWidth(1, 60) self.table.setHorizontalHeaderLabels( ['Name ', 'Unlink ', 'Link ', 'Channel ']) self.table.setEnabled(True) self.update_status(message=message) self.label_description.setText(description) # Adjust size after data has populated the table self.table.resizeColumnsToContents() width = sum( self.table.columnWidth(i) for i in range(self.table.columnCount())) delta = (self.width() - self.table.width() + self.table.verticalHeader().width() + 10) new_width = width + delta if new_width < self.base_minimum_width: new_width = self.base_minimum_width self.setMinimumWidth(new_width) self.setMaximumWidth(new_width) self.sig_setup_ready.emit() def update_status(self, message='', value=None, max_value=None): """Update status of packages dialog.""" self.label_status.setText(message) if max_value is None and value is None: self.progress_bar.setVisible(False) else: self.progress_bar.setVisible(True) self.progress_bar.setMaximum(max_value) self.progress_bar.setValue(value)
class ImportDialog(EnvironmentActionsDialog): """Import environment from environment specification dialog.""" CONDA_ENV_FILES = 'Conda environment files (*.yaml *.yml)' CONDA_SPEC_FILES = 'Conda explicit specification files (*.txt)' PIP_REQUIREMENT_FILES = 'Pip requirement files (*.txt)' def __init__(self, parent=None): """Import environment from environment specification dialog.""" super(ImportDialog, self).__init__(parent=parent) self.environments = None self.env_dirs = None self.selected_file_filter = None # Widgets self.label_name = LabelBase("Name:") self.label_location = LabelBase("Location:") self.label_path = LabelBase("Specification File") self.text_name = LineEditBase() self.label_prefix = LabelBase("") self.text_path = LineEditBase() self.button_path = ButtonNormal("") self.button_cancel = ButtonNormal('Cancel') self.button_ok = ButtonPrimary('Import') # Widgets setup self.align_labels( [self.label_name, self.label_location, self.label_path]) self.label_prefix.setObjectName('environment-location') self.button_path.setObjectName('import') self.button_ok.setDefault(True) self.text_path.setPlaceholderText("File to import from") self.text_name.setPlaceholderText("New environment name") self.setMinimumWidth(self.BASE_DIALOG_WIDTH) self.setWindowTitle("Import new environment") self.text_name.setValidator(self.get_regex_validator()) # Layouts layout_infile = QHBoxLayout() layout_infile.addWidget(self.text_path) layout_infile.addWidget(SpacerHorizontal()) layout_infile.addWidget(self.button_path) layout_grid = QGridLayout() layout_grid.addWidget(self.label_name, 0, 0) layout_grid.addWidget(SpacerHorizontal(), 0, 1) layout_grid.addWidget(self.text_name, 0, 2) layout_grid.addWidget(SpacerVertical(), 1, 0) layout_grid.addWidget(self.label_location, 2, 0) layout_grid.addWidget(SpacerHorizontal(), 2, 1) layout_grid.addWidget(self.label_prefix, 2, 2) layout_grid.addWidget(SpacerVertical(), 3, 0) layout_grid.addWidget(self.label_path, 4, 0) layout_grid.addWidget(SpacerHorizontal(), 4, 1) layout_grid.addLayout(layout_infile, 4, 2) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) layout = QVBoxLayout() layout.addLayout(layout_grid) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_buttons) self.setLayout(layout) # Signals self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.button_path.clicked.connect(self.choose) self.text_path.textChanged.connect(self.refresh) self.text_name.textChanged.connect(self.refresh) # Setup self.text_name.setFocus() self.refresh() def choose(self): """Display file dialog to select environment specification.""" path, selected_filter = getopenfilename( caption="Import Environment", basedir=os.path.expanduser('~'), parent=None, filters="{0};;{1};;{2}".format(self.CONDA_ENV_FILES, self.CONDA_SPEC_FILES, self.PIP_REQUIREMENT_FILES)) if path: name = self.name self.selected_file_filter = selected_filter self.text_path.setText(path) self.refresh(path) # Try to get the name key of the file if an environment.yaml file if selected_filter == self.CONDA_ENV_FILES: try: with open(path, 'r') as f: raw = f.read() data = yaml.load(raw) name = data.get('name', name) except Exception: pass self.text_name.setText(name) def refresh(self, text=''): """Update the status of buttons based data entered.""" name = self.name path = self.path self.update_location() if (name and path and os.path.exists(self.path) and self.is_valid_env_name(name)): self.button_ok.setDisabled(False) self.button_ok.setDefault(True) else: self.button_ok.setDisabled(True) self.button_cancel.setDefault(True) @property def path(self): """Return the content of the selected path if it exists.""" path = None if os.path.isfile(self.text_path.text()): path = self.text_path.text() return path
class PreferencesDialog(DialogBase): """Application preferences dialog.""" sig_urls_updated = Signal(str, str) sig_check_ready = Signal() sig_reset_ready = Signal() def __init__(self, config=CONF, **kwargs): """Application preferences dialog.""" super(PreferencesDialog, self).__init__(**kwargs) self.api = AnacondaAPI() self.widgets_changed = set() self.widgets = [] self.widgets_dic = {} self.config = config # Widgets self.button_ok = ButtonPrimary('Apply') self.button_cancel = ButtonNormal('Cancel') self.button_reset = ButtonNormal('Reset to defaults') self.row = 0 # Widget setup self.setWindowTitle("Preferences") # Layouts self.grid_layout = QGridLayout() buttons_layout = QHBoxLayout() buttons_layout.addWidget(self.button_reset) buttons_layout.addStretch() buttons_layout.addWidget(self.button_cancel) buttons_layout.addWidget(SpacerHorizontal()) buttons_layout.addWidget(self.button_ok) main_layout = QVBoxLayout() main_layout.addLayout(self.grid_layout) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(SpacerVertical()) main_layout.addLayout(buttons_layout) self.setLayout(main_layout) # Signals self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.button_reset.clicked.connect(self.reset_to_defaults) self.button_reset.clicked.connect( lambda: self.button_ok.setEnabled(True)) # Setup self.grid_layout.setSpacing(0) self.setup() self.button_ok.setDisabled(True) self.widgets[0].setFocus() self.button_ok.setDefault(True) self.button_ok.setAutoDefault(True) # --- Helpers # ------------------------------------------------------------------------- def get_option(self, option): """Get configuration option from `main` section.""" return self.config.get('main', option, None) def set_option(self, option, value): """Set configuration option in `main` section.""" self.config.set('main', option, value) def get_option_default(self, option): """Get configuration option default value in `main` section.""" return self.config.get_default('main', option) def set_option_default(self, option): """Set configuration option default value in `main` section.""" self.set_option(option, self.get_option_default(option)) def create_widget(self, widget=None, label=None, option=None, hint=None, check=None): """Create preference option widget and add to layout.""" config_value = self.get_option(option) widget._text = label widget.label = QLabel(label) widget.option = option widget.set_value(config_value) widget.label_information = QLabel() widget.label_information.setMinimumWidth(20) widget.label_information.setMaximumWidth(20) form_widget = QWidget() h_layout = QHBoxLayout() h_layout.addSpacing(4) h_layout.addWidget(widget.label_information, 0, Qt.AlignRight) h_layout.addWidget(widget, 0, Qt.AlignLeft) h_layout.addWidget(QLabel(hint or ''), 0, Qt.AlignLeft) form_widget.setLayout(h_layout) if check: widget.check_value = lambda value: check(value) else: widget.check_value = lambda value: (True, '') self.widgets.append(widget) self.widgets_dic[option] = widget self.grid_layout.addWidget(widget.label, self.row, 0, Qt.AlignRight | Qt.AlignCenter) self.grid_layout.addWidget(form_widget, self.row, 1, Qt.AlignLeft | Qt.AlignCenter) self.row += 1 def create_textbox(self, label, option, hint=None, check=None): """Create textbox (QLineEdit) preference option.""" widget = QLineEdit() widget.setAttribute(Qt.WA_MacShowFocusRect, False) widget.setMinimumWidth(250) widget.get_value = lambda w=widget: w.text() widget.set_value = lambda value, w=widget: w.setText(value) widget.set_warning = lambda w=widget: w.setSelection(0, 1000) widget.textChanged.connect( lambda v=None, w=widget: self.options_changed(widget=w)) self.create_widget( widget=widget, option=option, label=label, hint=hint, check=check, ) def create_checkbox(self, label, option, check=None, hint=None): """Create checkbox preference option.""" widget = QCheckBox() widget.get_value = lambda w=widget: bool(w.checkState()) widget.set_value = lambda value, w=widget: bool( w.setCheckState(Qt.Checked if value else Qt.Unchecked)) api_widget = self.widgets_dic['anaconda_api_url'] widget.set_warning = lambda w=widget: api_widget widget.stateChanged.connect( lambda v=None, w=widget: self.options_changed(widget=w)) self.create_widget( widget=widget, option=option, label=label, hint=hint, check=check, ) def options_changed(self, value=None, widget=None): """Callback helper triggered on preference value change.""" config_value = self.get_option(widget.option) if config_value != widget.get_value(): self.widgets_changed.add(widget) else: if widget in self.widgets_changed: self.widgets_changed.remove(widget) self.button_ok.setDisabled(not bool(len(self.widgets_changed))) def widget_for_option(self, option): """Return the widget for the given option.""" return self.widgets_dic[option] # --- API # ------------------------------------------------------------------------- def setup(self): """Setup the preferences dialog.""" # Set values for configuration located elsewhere (anaconda client) self.config.set('main', 'anaconda_api_url', self.api.client_get_api_url()) self.config.set('main', 'ssl_verification', self.api.client_get_ssl()) def api_url_checker(value): """ Custom checker to use selected ssl option instead of stored one. This allows to set an unsafe api url directly on the preferences dialog. Without this, one would have to first disable, click accept, then open preferences again and change api url for it to work. """ ssl_widget = self.widgets_dic.get('ssl_verification') verify = ssl_widget.get_value() if ssl_widget else True basic_check = self.is_valid_api(value, verify=verify) return basic_check def ssl_checker(value): """Counterpart to api_url_checker.""" api_url_widget = self.widgets_dic.get('anaconda_api_url') api_url = api_url_widget.get_value() return self.is_valid_api(api_url, verify=value) self.create_textbox( 'Anaconda API domain', 'anaconda_api_url', check=api_url_checker, ) self.create_checkbox( 'Enable SSL verification', 'ssl_verification', check=ssl_checker, hint=('<i>Disabling this option is not <br>' 'recommended for security reasons</i>'), ) self.create_checkbox('Provide analytics', 'provide_analytics') self.create_checkbox('Hide quit dialog', 'hide_quit_dialog') self.create_checkbox('Hide update dialog on startup', 'hide_update_dialog') self.create_checkbox('Hide running applications dialog', 'hide_running_apps_dialog') self.create_checkbox('Enable high DPI scaling', 'enable_high_dpi_scaling') @staticmethod def warn(widget, text=None): """Display warning for widget in preferences.""" label = widget.label_information if text: pixmap = QPixmap(WARNING_ICON) label.setPixmap( pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)) label.setToolTip(str(text)) w = widget.label_information.width() / 2 h = widget.label_information.height() / 2 position = widget.label_information.mapToGlobal(QPoint(w, h)) QCursor.setPos(position) else: label.setPixmap(QPixmap()) label.setToolTip('') # --- Checkers # ------------------------------------------------------------------------- def is_valid_url(self, url): """Check if a given URL returns a 200 code.""" output = self.api.download_is_valid_url(url, non_blocking=False) error = '' if not output: error = 'Invalid api url.' return output, error def is_valid_api(self, url, verify=True): """Check if a given URL is a valid anaconda api endpoint.""" output = self.api.download_is_valid_api_url( url, non_blocking=False, verify=verify, ) error = '' if not output: url_api_1 = '' url_api_2 = '' if '/api' not in url and self.is_valid_url(url)[0]: url_api_1 = url.replace('https://', 'https://api.') url_api_1 = url_api_1.replace('http://', 'http://api.') if url.endswith('/'): url_api_2 = url + 'api' else: url_api_2 = url + '/api' error = ('Invalid Anaconda API url. <br>' '<br>Try using:<br><b>{0}</b> or <br>' '<b>{1}</b>'.format(url_api_1, url_api_2)) else: error = ('Invalid Anaconda API url.<br><br>' 'Check the url is valid and corresponds to the api ' 'endpoint.') return output, error def reset_to_defaults(self): """Reset the preferences to the default values.""" for widget in self.widgets: option = widget.option default = self.get_option_default(option) widget.set_value(default) # Special case for anaconda client api if option in ['anaconda_api_url', 'ssl_verification']: self.set_option(option=option, value=None) # Flag all values as updated self.options_changed(widget=widget, value=default) self.sig_reset_ready.emit() def accept(self): """Override Qt method.""" sig_updated = False checks = [] for widget in self.widgets_changed: option = widget.option value = widget.get_value() check, error = widget.check_value(value) checks.append(check) if check: self.set_option(option, value) self.warn(widget) else: self.button_ok.setDisabled(True) widget.set_warning() self.warn(widget, error) break # Settings not stored on Navigator config, but taken from # anaconda-client config if widget.option == 'anaconda_api_url': self.api.client_set_api_url(value) sig_updated = True if widget.option == 'ssl_verification': self.api.client_set_ssl(value) # Emit checks ready self.sig_check_ready.emit() for widget in self.widgets: if widget.option == 'anaconda_api_url': anaconda_api_url = widget.get_value() if sig_updated: def _api_info(worker, output, error): conda_url = output['conda_url'] try: self.sig_urls_updated.emit(anaconda_api_url, conda_url) super(PreferencesDialog, self).accept() except RuntimeError: # Some tests on appveyor/circleci fail pass worker = self.api.api_urls() worker.sig_chain_finished.connect(_api_info) if checks and all(checks): super(PreferencesDialog, self).accept()
class PasswordDialog(DialogBase): """Password dialog.""" def __init__(self, *args, **kwargs): """About dialog.""" super(PasswordDialog, self).__init__(*args, **kwargs) self.wm = WorkerManager() # Widgets self.label_text = LabelBase( 'VSCode will be installed through your system <br> package ' 'manager.<br><br>' 'This action requires elevated privileges. Please <br>provide a ' 'password to forward to sudo') self.lineedit = PasswordEdit() self.label_info = LabelBase() self.button_cancel = ButtonNormal('Cancel') self.button_ok = ButtonPrimary('Ok') self.worker = None self._valid = False self._timer = QTimer() self._timer.setInterval(3000) self._timer.timeout.connect(self.check) # Widgets setup self.button_ok.setMinimumWidth(70) self.button_ok.setDefault(True) self.setWindowTitle("Privilege Elevation Required") self.lineedit.setEchoMode(LineEditBase.Password) # Layouts layout_content = QVBoxLayout() layout_content.addWidget(self.label_text) layout_content.addWidget(SpacerVertical()) layout_content.addWidget(self.lineedit, 0, Qt.AlignBottom) layout_content.addWidget(SpacerVertical()) layout_content.addWidget(self.label_info, 0, Qt.AlignTop) layout_content.addWidget(SpacerVertical()) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) layout_main = QVBoxLayout() layout_main.addLayout(layout_content) layout_main.addWidget(SpacerVertical()) layout_main.addWidget(SpacerVertical()) layout_main.addLayout(layout_buttons) self.setLayout(layout_main) # Signals self.button_ok.clicked.connect(self.accept2) self.button_cancel.clicked.connect(self.reject2) self.lineedit.textChanged.connect(self.refresh) # Setup self.lineedit.setFocus() self.refresh() def refresh(self): """Refresh state of buttons.""" self.button_ok.setEnabled(bool(self.password)) def _output(self, worker, output, error): """Callback.""" self._valid = True def check(self): """Check password.""" if self.worker._started and self._valid: self.lineedit.setEnabled(False) self.label_info.setText('') self.accept() elif self.worker._started: self._timer.stop() self.lineedit.setEnabled(True) self.button_ok.setEnabled(True) self.lineedit.setFocus() self.lineedit.selectAll() self.label_info.setText('<i>Invalid password</i>') def reject2(self): """Handle reject.""" if self.worker is not None: self.worker.terminate() self.reject() def accept2(self): """Handle accept.""" stdin = to_binary_string(self.password + '\n') if self.worker is not None: self.worker.terminate() self.worker = self.wm.create_process_worker(['sudo', '-kS', 'ls']) self.worker.sig_partial.connect(self._output) self.worker.sig_finished.connect(self._output) self._valid = False self._timer.start() self.worker.start() self.worker.write(stdin) self.lineedit.setEnabled(False) self.button_ok.setEnabled(False) @property def password(self): """Return password.""" return self.lineedit.text()