class ProjectEditor(QWidget): sig_dirty_state = Signal(bool) sig_saved = Signal() def __init__(self, *args, **kwargs): super(ProjectEditor, self).__init__(*args, **kwargs) # Widgets self.editor = EditorBase(self) self.button_save = ButtonPrimary('Save') self.button_problems = ButtonProjectProblems('Problems') self.button_suggestions = ButtonProjectSuggestions('Suggestions') self.original_text = None self.problems = None self.suggestions = None # Layouts layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.button_save) layout_buttons.addStretch() layout_buttons.addWidget(self.button_problems) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_suggestions) layout = QVBoxLayout() layout.addLayout(layout_buttons) layout.addWidget(self.editor) self.setLayout(layout) # Signals self.editor.textChanged.connect(self.text_changed) self.button_save.clicked.connect(self.save) self.button_problems.clicked.connect(self.show_problems) self.button_suggestions.clicked.connect(self.show_suggestions) def show_problems(self): """Display problems in a dialog.""" dlg = DialogProblems(parent=self, problems=self.problems) geo_tl = self.button_problems.geometry().topRight() tl = self.button_problems.parentWidget().mapToGlobal(geo_tl) x = tl.x() - dlg.width() y = tl.y() + self.button_problems.height() dlg.move(x, y) dlg.show() def show_suggestions(self): """Display suggestions in a dialog.""" dlg = DialogProblems(parent=self, problems=self.suggestions) geo_tl = self.button_suggestions.geometry().topRight() tl = self.button_suggestions.parentWidget().mapToGlobal(geo_tl) x = tl.x() - dlg.width() y = tl.y() + self.button_suggestions.height() dlg.move(x, y) dlg.show() def save(self): """Save test to editor.""" text = self.editor.toPlainText() self.original_text = text self.button_save.setDisabled(True) self.sig_saved.emit() def text_changed(self): """Callback on text change.""" dirty = self.is_dirty() self.sig_dirty_state.emit(dirty) self.button_save.setEnabled(dirty) def is_dirty(self): """Return if the document is dirty.""" current_text = self.editor.toPlainText() return current_text != self.original_text def text(self): """Return current plain text from editor.""" return self.editor.toPlainText() def set_info(self, problems, suggestions): """Store problems and suggestions for display.""" self.button_problems.setVisible(False) self.button_suggestions.setVisible(False) self.problems = None self.suggetsions = None if problems: self.problems = problems self.button_problems.setVisible(True) if suggestions: self.suggestions = suggestions self.button_suggestions.setVisible(True) def set_text(self, text): """Set editor text.""" self.editor.setPlainText(text) self.original_text = text self.button_save.setDisabled(True) def scroll_value(self): """Get scroll value for vertical bar.""" return self.editor.verticalScrollBar().value() def set_scroll_value(self, value): """Set scroll value for vertical bar.""" return self.editor.verticalScrollBar().setValue(value) def ordered_widgets(self): """Return a list of the ordered widgets.""" ordered_widgets = [ self.button_save, self.button_problems, self.button_suggestions, self.editor ] return ordered_widgets
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 ProjectsWidget(WidgetBase): """Main projects widget.""" sig_saved = Signal() sig_login_requested = Signal() def __init__(self, *args, **kwargs): super(ProjectsWidget, self).__init__(*args, **kwargs) self.api = AnacondaAPI() self.timer = None self.timer_content_changed = QTimer() self.project_path = None self.original_content = None self.config = CONF self.timer = None # Widgets self.frame_projects_header = FrameProjectDetailsHeader() self.frame_projects_footer = FrameProjectDetailsFooter() self.button_upload = ButtonPrimary('Upload to Anaconda Cloud') self.button_cancel = ButtonDanger('Cancel') self.label_project_location = LabelProjectLocation( '<b>Project location</b>') self.label_status_message = LabelBase('') self.text_project_location = TextProjectLocation() self.tab_details = QTabWidget() self.file_explorer = ExplorerWidget() self.editor = ProjectEditor(parent=self) # Wigets setup tabbar = self.tab_details.tabBar() tabbar.setFocusPolicy(Qt.StrongFocus) self.tab_details.addTab(self.file_explorer, 'Files') self.tab_details.addTab(self.editor, 'Edit') self.timer_content_changed.setInterval(2000) self.timer_content_changed.timeout.connect(self.check_content_change) self.timer_content_changed.start() # Layouts layout_upload = QHBoxLayout() layout_upload.addWidget(SpacerHorizontal()) layout_upload.addWidget(SpacerHorizontal()) layout_upload.addWidget(self.label_status_message) layout_upload.addStretch() layout_upload.addWidget(self.button_cancel) layout_upload.addWidget(SpacerHorizontal()) layout_upload.addWidget(self.button_upload) layout_upload.addWidget(SpacerHorizontal()) layout_upload.addWidget(SpacerHorizontal()) layout_footer = QVBoxLayout() layout_footer.addWidget(SpacerVertical()) layout_footer.addWidget(self.tab_details) layout_footer.addLayout(layout_upload) layout_footer.addWidget(SpacerVertical()) layout_footer.addWidget(SpacerVertical()) self.frame_projects_footer.setLayout(layout_footer) layout = QVBoxLayout() layout.addWidget(self.frame_projects_footer) self.setLayout(layout) # Signals self.editor.sig_dirty_state.connect(self.set_dirty) self.editor.sig_saved.connect(self.save) self.button_upload.clicked.connect(self.upload) self.button_cancel.clicked.connect(self.cancel) self.file_explorer.sig_add_to_project.connect(self.add_to_project) self.button_cancel.setVisible(False) self.file_explorer.set_current_folder(HOME_PATH) def add_to_project(self, fname): """Add selected file to project.""" file_path = os.path.join( self.project_path, os.path.basename(fname), ) try: shutil.copyfile(fname, file_path) except Exception: pass def check_content_change(self): """Check if content of anaconda-project changed outside.""" if self.project_path: project_config_path = os.path.join(self.project_path, 'anaconda-project.yml') if os.path.isfile(project_config_path): current_content = self.editor.text() with open(project_config_path, 'r') as f: data = f.read() if (current_content != data and data != self.original_content): self.load_project(self.project_path) def set_dirty(self, state): """Set dirty state editor tab.""" text = 'Edit*' if state else 'Edit' self.tab_details.setTabText(1, text) def before_delete(self): """Before deleting a folder, ensure it is not the same as the cwd.""" self.file_explorer.set_current_folder(HOME_PATH) def clear(self): """Reset view for proect details.""" self.text_project_location.setText('') self.editor.set_text('') def cancel(self): """Cancel ongoing project process.""" # TODO: when running project. Cancel ongoing process! self.button_cancel.setVisible(False) self.button_upload.setEnabled(True) def _upload(self, worker, output, error): """Upload callback.""" error = error if error else '' errors = [] if output is not None: errors = output.errors # print(output.status_description) # print(output.logs) # print(errors) if error or errors: if errors: error_msg = error or '\n'.join(errors) elif error: error_msg = 'Upload failed!' self.update_status(error_msg) else: self.update_status('Project <b>{0}</b> upload successful'.format( worker.name)) self.timer = QTimer() self.timer.setSingleShot(True) self.timer.setInterval(10000) self.timer.timeout.connect(lambda: self.update_status('')) self.timer.start() self.button_upload.setEnabled(True) self.button_cancel.setVisible(False) def update_status(self, message): """Update Status Bar message.""" self.label_status_message.setText(message) def upload(self): """Upload project to Anaconda Cloud.""" # Check if is logged in? if not self.api.is_logged_in(): self.update_status('You need to log in to Anaconda Cloud') self.sig_login_requested.emit() self.update_status('') return project = self.api.project_load(self.project_path) name = project.name or os.path.basename(self.project_path) # Check if saved? if self.editor.is_dirty(): self.update_status('Saving project <b>{0}</b>'.format( project.name)) self.editor.save() project = self.api.project_load(self.project_path) if not project.problems: username, token = self.api.get_username_token() self.button_cancel.setVisible(True) worker = self.api.project_upload( project, username=username, token=token, ) worker.sig_finished.connect(self._upload) worker.name = project.name self.button_upload.setEnabled(False) msg = 'Uploading project <b>{0}</b> to Anaconda Cloud.'.format( project.name) self.update_status(msg) else: self.update_status( 'Problems must be fixed before uploading <b>{0}</b>' ''.format(name)) def save(self): """Save current edited project.""" project_config_path = os.path.join(self.project_path, 'anaconda-project.yml') data = self.editor.text() if os.path.isfile(project_config_path): with open(project_config_path, 'w') as f: data = f.write(data) self.load_project(self.project_path, overwrite=False) self.sig_saved.emit() def load_project(self, project_path, overwrite=True): """Load a conda project located at path.""" self.project_path = project_path project = self.api.project_load(project_path) self.project = project self.text_project_location.setText(project_path) self.file_explorer.set_current_folder(project_path) project_config_path = os.path.join(project_path, 'anaconda-project.yml') data = '' if os.path.isfile(project_config_path): with open(project_config_path, 'r') as f: data = f.read() self.original_content = data if overwrite: self.editor.set_text(data) self.set_dirty(False) self.file_explorer.set_home(project_path) self.update_error_status(project) self.update_status('') def ordered_widgets(self): """Return a list of the ordered widgets.""" tabbar = self.tab_details.tabBar() ordered_widgets = [tabbar] ordered_widgets += self.file_explorer.ordered_widgets() ordered_widgets += self.editor.ordered_widgets() ordered_widgets += [self.button_upload] return ordered_widgets def update_error_status(self, project): """Update problems and suggestions.""" if project: problems = project.problems suggestions = project.suggestions if problems or suggestions: icon = QIcon(WARNING_ICON) self.tab_details.setTabIcon(1, icon) else: self.tab_details.setTabIcon(1, QIcon()) self.editor.set_info(problems, suggestions)
class ProjectsPathDialog(DialogBase): """Select project path.""" def __init__(self, parent=None, default=DEFAULT_PROJECTS_PATH): """Select project folder.""" super(ProjectsPathDialog, self).__init__(parent=parent) # Widgets self.label_description = QLabel("If no path is selected, the default" " one will be used.") self.label_name = QLabel("Select the projects folder to use") self.label_path = QLabel("Projects path") self.label_info = QLabel('') self.text_path = QLineEdit() self.button_path = ButtonNormal("") self.button_default = ButtonNormal('Use default') self.button_ok = ButtonPrimary('Select') # Widgets setup self.button_path.setObjectName('import') self.default = default self.text_path.setPlaceholderText("projects folder path") self.text_path.setReadOnly(True) self.setMinimumWidth(580) self.setWindowTitle("Select Projects Path") # Layouts layout_grid = QGridLayout() layout_grid.addWidget(self.label_path, 0, 0) layout_grid.addWidget(SpacerHorizontal(), 0, 1) layout_grid.addWidget(self.text_path, 0, 2) layout_grid.addWidget(SpacerHorizontal(), 0, 3) layout_grid.addWidget(self.button_path, 0, 4) layout_grid.addWidget(SpacerVertical(), 1, 0, 1, 4) layout_grid.addWidget(self.label_info, 2, 2, 1, 2) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_default) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) layout = QVBoxLayout() layout.addWidget(self.label_description) 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_default.clicked.connect(self.use_default) self.button_path.clicked.connect(self.choose) # Setup self.refresh() @property def path(self): """Return the folder project path.""" return self.text_path.text().strip() or self.default def use_default(self): """Set the default projects path as the folder path to use.""" self.text_path.setText('') self.accept() def choose(self): """Display directory dialog to select project folder.""" path = getexistingdirectory( caption="Select Projects Folder", basedir=self.default, parent=self, ) # Enforce the correct slashes. # See Issue https://github.com/ContinuumIO/navigator/issues/1164 if WIN: path = path.replace('/', '\\') else: path = path.replace('\\', '/') if path: self.text_path.setText(path) self.refresh() return path def is_valid_path(self): """Check that entered path is valid.""" check = False error = '' path = self.text_path.text().strip() if bool(path) and os.path.isdir(path): if ' ' in path: error = '<b>Please select a path without spaces</b>' elif path == HOME_PATH: error = ('<b>Please select a path different to the home ' 'directory.</b>') elif not path_is_writable(path): error = ('<b>Please select a path that has write access.</b>') check = (' ' not in path and path != HOME_PATH and path_is_writable(path)) return check, error def refresh(self): """Refresh button status based on path.""" check, error = self.is_valid_path() self.button_ok.setEnabled(check) if check or not bool(self.text_path.text()): self.label_info.setText('<i>Default: {0}</i>'.format(self.default)) else: self.label_info.setText(error) def reject(self): """Override Qt method.""" self.use_default()
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 ConflictDialog(EnvironmentActionsDialog): """Create new environment dialog if navigator conflicts with deps.""" def __init__(self, parent=None, package=None, extra_message='', current_prefix=None): """Create new environment dialog if navigator conflicts with deps.""" super(ConflictDialog, self).__init__(parent=parent) parts = package.split('=') self.package = parts[0] if '=' in package else package self.package_version = parts[-1] if '=' in package else '' self.current_prefix = current_prefix base_message = ('<b>{0}</b> cannot be installed on this ' 'environment.').format(package) base_message = extra_message or base_message # Widgets self.label_info = LabelBase( base_message + '<br><br>' 'Do you want to install the package in an existing ' 'environment or <br>create a new environment?' ''.format(package)) self.label_name = LabelBase('Name:') self.label_prefix = LabelBase(' ' * 100) self.label_location = LabelBase('Location:') self.combo_name = ComboBoxBase() self.button_ok = ButtonPrimary('Create') self.button_cancel = ButtonNormal('Cancel') # Widgets setup self.align_labels([self.label_name, self.label_location]) self.combo_name.setEditable(True) self.combo_name.setCompleter(None) self.combo_name.setValidator(self.get_regex_validator()) self.setMinimumWidth(self.BASE_DIALOG_WIDTH) self.setWindowTitle("Create new environment for '{}'".format(package)) self.label_prefix.setObjectName('environment-location') self.combo_name.setObjectName('environment-selection') # Layouts grid_layout = QGridLayout() grid_layout.addWidget(self.label_name, 0, 0) grid_layout.addWidget(SpacerHorizontal(), 0, 1) grid_layout.addWidget(self.combo_name, 0, 2) grid_layout.addWidget(SpacerVertical(), 1, 0) grid_layout.addWidget(self.label_location, 2, 0) grid_layout.addWidget(SpacerHorizontal(), 2, 1) grid_layout.addWidget(self.label_prefix, 2, 2) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) main_layout = QVBoxLayout() main_layout.addWidget(self.label_info) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(SpacerVertical()) main_layout.addLayout(grid_layout) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(SpacerVertical()) main_layout.addLayout(layout_buttons) self.setLayout(main_layout) # Signals self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.combo_name.setCurrentText(self.package) self.combo_name.currentTextChanged.connect(self.refresh) self.button_ok.setDisabled(True) def new_env_name(self): """Generate a unique environment name.""" pkg_name_version = self.package + '-' + self.package_version if self.environments: if self.package not in self.environments.values(): env_name = self.package elif pkg_name_version not in self.environments.values(): env_name = pkg_name_version else: for i in range(1, 1000): new_pkg_name = pkg_name_version + '_' + str(i) if new_pkg_name not in self.environments.values(): env_name = new_pkg_name break else: env_name = self.package return env_name def setup(self, worker, info, error): """Setup the dialog conda information as a result of a worker.""" super(ConflictDialog, self).setup(worker, info, error) self.combo_name.blockSignals(True) self.combo_name.clear() new_env_name = self.new_env_name() for i, (env_prefix, env_name) in enumerate(self.environments.items()): # Do not include the env where the conflict was found! if self.current_prefix != env_prefix: self.combo_name.addItem(env_name, env_prefix) self.combo_name.setCurrentText(new_env_name) self.combo_name.setItemData(i, env_prefix, Qt.ToolTipRole) self.combo_name.setCurrentText(new_env_name) self.combo_name.blockSignals(False) self.refresh() def refresh(self): """Refresh state of buttons based on content.""" self.update_location() if self.environments: self.button_ok.setEnabled(bool(self.name)) @property def name(self): """Return the name of the environment.""" return self.combo_name.currentText()
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()