def botlicense(qtbot): # First remove all licenses api = AnacondaAPI() api.remove_all_licenses() widget = LicenseManagerDialog() qtbot.addWidget(widget) widget.show() return qtbot, widget
def run_python_console(project_path, project=None, filename=""): """Execute python in env with console.""" from anaconda_navigator.api import AnacondaAPI api = AnacondaAPI() if project is None: project = api.load_project(project_path) cmd = os.sep.join([project.env_prefix(project_path), 'bin', 'python']) run_ex(cmd, filename)
def run_python_file(project_path, project=None, filename=""): """Execute python in environment.""" from anaconda_navigator.api import AnacondaAPI api = AnacondaAPI() if project is None: project = api.load_project(project_path) cmd = os.sep.join([project.env_prefix(project_path), 'bin', 'python']) logger.debug(",".join([cmd, filename])) subprocess.Popen([cmd, filename])
def botlicensefilled(qtbot): # First remove all licenses api = AnacondaAPI() api.remove_all_licenses() widget = LicenseManagerDialog() qtbot.addWidget(widget) widget.show() widget.add_license(path=EXPIRED_LICENSE_PATH) with qtbot.waitSignal(signal=widget.accepted, timeout=1000, raising=False): pass return qtbot, widget
def __init__(self): """Google analytics tracker.""" # Avoid blocking the UI self._queue = deque() self._threads = [] self._workers = [] self._timer = QTimer() self.api = AnacondaAPI() self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) self.setup()
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)
def check_prog(prog, prefix=None): """Check if program exists in prefix.""" api = AnacondaAPI() prefix = prefix or api.conda_get_prefix_envname(name='root') if prog in ['notebook', 'jupyter notebook']: pkgs = ['notebook', 'ipython-notebook', 'jupyter-notebook'] elif prog in ['ipython', 'jupyter console']: pkgs = ['ipython', 'jupyter'] else: pkgs = [prog] return any( api.conda_package_version(prefix=prefix, pkg=p) is not None for p in pkgs)
def local_test(): # pragma: no cover """Run local test.""" from anaconda_navigator.utils.qthelpers import qapplication from anaconda_navigator.utils.styles import load_style_sheet app = qapplication() api = AnacondaAPI() widget = DialogChannels(None) widget.update_style_sheet(load_style_sheet()) widget.show() worker = api.conda_config_sources() worker_2 = api.api_urls() worker.sig_chain_finished.connect(widget.setup) worker_2.sig_chain_finished.connect(widget.update_api) sys.exit(app.exec_())
def run_notebook(project_path, project=None, filename=""): """Start notebook server.""" from anaconda_navigator.api import AnacondaAPI api = AnacondaAPI() if project is None: project = api.load_project(project_path) kernels_folder = os.sep.join([os.path.expanduser('~'), ".ipython", "kernels"]) display_name = '{0} ({1})'.format(project.name, project_path) kernel_uuid = uuid.uuid1() kernel_path = os.sep.join([kernels_folder, "{name}", "kernel.json"]) pyexec = get_pyexec(project.env_prefix(project_path)) spec = { "argv": [pyexec, "-m", "IPython.kernel", "-f", "{connection_file}"], "display_name": display_name, "language": "python", } # Delete any other kernel sec mathching this name! kernels = os.listdir(kernels_folder) for kernel in kernels: path = os.sep.join([kernels_folder, kernel]) file_path = os.sep.join([path, 'kernel.json']) if os.path.isfile(file_path): with open(file_path, 'r') as f: data = json.loads(f.read()) name = data.get('display_name') if name is not None and project_path in name: shutil.rmtree(path) os.makedirs(os.path.split(kernel_path.format(name=kernel_uuid))[0]) with open(kernel_path.format(name=kernel_uuid), 'w') as f: f.write(json.dumps(spec)) # This is not working! cmd = ( 'jupyter notebook ' '--KernelSpecManager.whitelist={0}'.format(kernel_uuid) ) cmd = ('jupyter notebook') command = (cmd + ' ' + filename) logger.debug(",".join([command, project_path])) subprocess.Popen(command.split(), cwd=project_path)
def botchannels(qtbot, tmpfile): qtbot.api = AnacondaAPI() widget = DialogChannels() widget.update_style_sheet(load_style_sheet()) temp_rc_content = """channels: - chdoig - sean""" with open(tmpfile, 'w') as f: f.write(temp_rc_content) # Mock conda config data config_data = { 'config_sources': { tmpfile: { 'channels': ['chdoig', 'sean'] } } } api_info = {'conda_url': 'https://conda.beta.anaconda.org'} with qtbot.waitSignal( signal=widget.sig_setup_ready, timeout=5000, raising=True ): if random.randint(0, 1): widget.setup(None, config_data, None) widget.update_api(None, api_info, None) else: widget.update_api(None, api_info, None) widget.setup(None, config_data, None) widget.show() return qtbot, widget
def tmpjsonfile_production(request): """Return a temporal file path with downloaded content.""" content_base_uri = 'http://anaconda.com/api/{0}?items_per_page=all' url = content_base_uri.format(request.param) _, path = tempfile.mkstemp(suffix='json', text=True) api = AnacondaAPI() r = requests.get(url, proxies=api.conda_load_proxy_config()) with open(path, 'w') as f: f.write(r.text) def fin(): print("Finalizing production {0} check".format(request.param)) # shutil.rmtree(path) request.addfinalizer(fin) return path
def botlogin(qtbot, tmpconfig): tmpconfig.set('main', 'anaconda_api_url', 'https://api.anaconda.org/') widget = AuthenticationDialog(api=AnacondaAPI()) widget.config = tmpconfig # Patch with a temporal config file widget.setup() qtbot.addWidget(widget) widget.show() return qtbot, widget
def local_test(): # pragma: no cover """Run local test.""" from anaconda_navigator.utils.qthelpers import qapplication app = qapplication(test_time=3) load_fonts(app) widget = AuthenticationDialog(AnacondaAPI()) widget.update_style_sheet() widget.show() app.exec_()
def tmpconfig(request): """Return a temporal configuration object and delete after used.""" SUBFOLDER = tempfile.mkdtemp() CONF = UserConfig( 'anaconda-navigator-test', defaults=DEFAULTS, version=CONF_VERSION, subfolder=SUBFOLDER, raw_mode=True, ) CONF.reset_to_defaults() AnacondaAPI().client_set_api_url('https://api.anaconda.org') def fin(): shutil.rmtree(SUBFOLDER) request.addfinalizer(fin) return CONF
def local_test(): # pragma: no cover """Run local tests.""" from anaconda_navigator.utils.qthelpers import qapplication from anaconda_navigator.api.anaconda_api import AnacondaAPI app = qapplication() api = AnacondaAPI() worker = api.info() widget_create = CreateDialog(parent=None) worker.sig_chain_finished.connect(widget_create.setup) widget_create.show() worker = api.info() widget_import = ImportDialog(parent=None) worker.sig_chain_finished.connect(widget_import.setup) widget_import.show() worker = api.info() widget_clone = CloneDialog(parent=None, name='root') widget_clone.show() worker.sig_chain_finished.connect(widget_clone.setup) widget_remove = RemoveDialog(parent=None, name='root', prefix='/User/user/anaconda') widget_remove.show() worker = api.info() widget_create_conflict = ConflictDialog( parent=None, package='orange3', ) worker.sig_chain_finished.connect(widget_create_conflict.setup) widget_create_conflict.show() app.exec_()
def __init__(self, parent=None): super(ProjectsTab, self).__init__(parent) # Variables self.api = AnacondaAPI() self.current_project = None self.style_sheet = None self.projects = None # Widgets self.frame_list = FrameEnvironmentsList(self) self.frame_widget = FrameEnvironmentsPackages(self) self.frame_header_left = FrameTabHeader() self.frame_header_right = FrameTabHeader() self.button_create = ButtonToolNormal(text="Create") self.button_import = ButtonToolNormal(text="Import") self.button_remove = ButtonToolNormal(text="Remove") self.button_toggle_collapse = ButtonToggleCollapse() self.list = ListWidgetEnv() self.widget = ProjectsWidget() self.menu_list = QMenu() self.text_search = LineEditSearch() # Widgets setup self.frame_list.is_expanded = True self.list.setContextMenuPolicy(Qt.CustomContextMenu) self.text_search.setPlaceholderText("Search Projects") self.button_create.setObjectName("create") self.button_import.setObjectName("import") self.button_remove.setObjectName("remove") # Layouts layout_header_left = QVBoxLayout() layout_header_left.addWidget(self.text_search) self.frame_header_left.setLayout(layout_header_left) layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.button_create) layout_buttons.addWidget(self.button_import) layout_buttons.addWidget(self.button_remove) layout_list_buttons = QVBoxLayout() layout_list_buttons.addWidget(self.frame_header_left) layout_list_buttons.addWidget(self.list) layout_list_buttons.addLayout(layout_buttons) self.frame_list.setLayout(layout_list_buttons) layout_widget = QHBoxLayout() layout_widget.addWidget(self.widget) self.frame_widget.setLayout(layout_widget) layout_main = QHBoxLayout() layout_main.addWidget(self.frame_list, 10) layout_main.addWidget(self.button_toggle_collapse, 1) layout_main.addWidget(self.frame_widget, 30) self.setLayout(layout_main) # Signals self.button_toggle_collapse.clicked.connect(self.expand_collapse) self.button_create.clicked.connect(self.sig_create_requested) self.button_import.clicked.connect(self.sig_import_requested) self.button_remove.clicked.connect(self.sig_remove_requested) self.list.sig_item_selected.connect(self._item_selected) self.text_search.textChanged.connect(self.filter_list) self.widget.sig_login_requested.connect(self.sig_login_requested) self.refresh()
class LicenseManagerDialog(DialogBase): """License Manager main dialog.""" CONTACT_LINK = 'https://support.continuum.io/' # TODO: Centralize this? # Url, Sender sig_url_clicked = Signal(object, object) def __init__(self, parent=None): """License Manager main dialog.""" super(LicenseManagerDialog, self).__init__(parent=parent) self.api = AnacondaAPI() # Widgets self.message_box = None # For testing self.button_add = ButtonPrimary('Add license') self.button_ok = ButtonNormal('Close') self.button_remove = ButtonNormal('Remove license') self.button_contact = ButtonLink('Please contact us.') self.label_info = LabelBase('Manage your Continuum Analytics ' 'license keys.') self.label_contact = LabelBase('Got a problem with your license? ') self.proxy_model = QSortFilterProxyModel(parent=self) self.model = LicenseModel(parent=self) self.table = LicenseTableView(parent=self) self.delegate = BackgroundDelegate(self.table) # Widget setup self.proxy_model.setSourceModel(self.model) self.table.setItemDelegate(self.delegate) self.table.setModel(self.proxy_model) self.setWindowTitle('License Manager') # Layouts layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.label_info) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addStretch() layout_buttons.addWidget(self.button_add) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_remove) layout_buttons_bottom = QHBoxLayout() layout_buttons_bottom.addWidget(self.label_contact) layout_buttons_bottom.addWidget(self.button_contact) layout_buttons_bottom.addStretch() layout_buttons_bottom.addWidget(self.button_ok) layout = QVBoxLayout() layout.addLayout(layout_buttons) layout.addWidget(SpacerVertical()) layout.addWidget(self.table) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_buttons_bottom) self.setLayout(layout) # Signals self.button_add.clicked.connect(lambda: self.add_license()) self.button_remove.clicked.connect(self.remove_license) self.button_ok.clicked.connect(self.accept) self.button_contact.clicked.connect( lambda v=None: self.sig_url_clicked.emit(self.CONTACT_LINK, 'License Manager')) self.table.sig_dropped.connect(self.handle_drop) # Setup self.button_add.setFocus() self.load_licenses() def handle_drop(self, links): """Handle a drag and drop event.""" self.api.add_license(links) self.load_licenses() def _hide_columns(self): """Hide columns.""" for key, val in COL_MAP.items(): if val in HIDDEN_COLUMNS: self.table.setColumnHidden(key, True) def add_license(self, v=None, path=None): """Add license file.""" if path is None: filename, selected_filter = getopenfilename( self, 'Select license file', filters='License files (*.txt)', basedir=get_home_dir(), ) if filename: paths = [filename] else: paths = [] else: paths = [path] valid_licenses, invalid_licenses = self.api.add_license(paths) for path in invalid_licenses: text = ('File: <b>"{0}"</b>' '<br>is not a valid license file.').format(path) self.message_box = MessageBoxInformation( text=text, title="Invalid license file") self.message_box.exec_() if valid_licenses: self.load_licenses() def remove_license(self, row=None): """Remove license from file.""" if row is None: index = self.table.currentIndex() else: index = self.proxy_model.index(row, 0) model_index = self.proxy_model.mapToSource(index) row_data = self.model.row(model_index.row()) if row_data: text = ('Do you want to remove license for product:<br><br>' '<b>{product}</b> ({issued} - {end_date})') text = text.format(product=row_data.get('product'), end_date=row_data.get('end_date'), issued=row_data.get('issued')) self.message_box = MessageBoxRemove(title='Remove license', text=text) if self.message_box.exec_(): self.api.remove_license(row_data) self.load_licenses() def load_licenses(self): """Load license files.""" res = self.api.load_licenses() self.model.load_licenses(res) self.proxy_model.setSourceModel(self.model) self.table.resizeColumnsToContents() self._hide_columns() self.update_status() def count(self): """Return the number of items in the table.""" return self.table.model().rowCount() def update_status(self): """Update visible and enabled status for widgets based on actions.""" self.button_remove.setEnabled(bool(self.count()))
def __init__(self, parent=None): """Conda environments tab.""" super(EnvironmentsTab, self).__init__(parent) # Variables self.api = AnacondaAPI() self.current_prefix = None self.style_sheet = None # Widgets self.frame_header_left = FrameTabHeader() self.frame_list = FrameEnvironmentsList(self) self.frame_widget = FrameEnvironmentsPackages(self) self.text_search = LineEditSearch() self.list = ListWidgetEnv() self.menu_list = QMenu() self.button_create = ButtonToolNormal(text="Create") self.button_clone = ButtonToolNormal(text="Clone") self.button_import = ButtonToolNormal(text="Import") self.button_remove = ButtonToolNormal(text="Remove") self.button_toggle_collapse = ButtonToggleCollapse() self.widget = CondaPackagesWidget(parent=self) # Widgets setup self.frame_list.is_expanded = True self.text_search.setPlaceholderText("Search Environments") self.list.setContextMenuPolicy(Qt.CustomContextMenu) self.button_create.setObjectName("create") # Needed for QSS selectors self.button_clone.setObjectName("clone") self.button_import.setObjectName("import") self.button_remove.setObjectName("remove") self.widget.textbox_search.set_icon_visibility(False) # Layouts layout_header_left = QVBoxLayout() layout_header_left.addWidget(self.text_search) self.frame_header_left.setLayout(layout_header_left) layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.button_create) layout_buttons.addWidget(self.button_clone) layout_buttons.addWidget(self.button_import) layout_buttons.addWidget(self.button_remove) layout_list_buttons = QVBoxLayout() layout_list_buttons.addWidget(self.frame_header_left) layout_list_buttons.addWidget(self.list) layout_list_buttons.addLayout(layout_buttons) self.frame_list.setLayout(layout_list_buttons) layout_widget = QHBoxLayout() layout_widget.addWidget(self.widget) self.frame_widget.setLayout(layout_widget) layout_main = QHBoxLayout() layout_main.addWidget(self.frame_list, 10) layout_main.addWidget(self.button_toggle_collapse, 1) layout_main.addWidget(self.frame_widget, 30) self.setLayout(layout_main) # Signals for buttons and boxes self.button_toggle_collapse.clicked.connect(self.expand_collapse) self.button_create.clicked.connect(self.sig_create_requested) self.button_clone.clicked.connect(self.sig_clone_requested) self.button_import.clicked.connect(self.sig_import_requested) self.button_remove.clicked.connect(self.sig_remove_requested) self.text_search.textChanged.connect(self.filter_list) # Signals for list self.list.sig_item_selected.connect(self._item_selected) # Signals for packages widget self.widget.sig_ready.connect(self.sig_ready) self.widget.sig_channels_requested.connect(self.sig_channels_requested) self.widget.sig_update_index_requested.connect( self.sig_update_index_requested) self.widget.sig_cancel_requested.connect(self.sig_cancel_requested) self.widget.sig_packages_action_requested.connect( self.sig_packages_action_requested)
def __init__(self, parent=None): """Home applications tab.""" super(HomeTab, self).__init__(parent) # Variables self._parent = parent self.api = AnacondaAPI() self.applications = None self.style_sheet = None self.app_timers = None self.current_prefix = None # Widgets self.list = ListWidgetApplication() self.button_channels = ButtonHomeChannels('Channels') self.button_refresh = ButtonHomeRefresh('Refresh') self.combo = ComboHomeEnvironment() self.frame_top = FrameTabHeader(self) self.frame_body = FrameTabContent(self) self.frame_bottom = FrameTabFooter(self) self.label_home = LabelHome('Applications on') self.label_status_action = QLabel('') self.label_status = QLabel('') self.progress_bar = QProgressBar() self.first_widget = self.combo # Widget setup self.setObjectName('Tab') self.progress_bar.setTextVisible(False) self.list.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) # Layout layout_top = QHBoxLayout() layout_top.addWidget(self.label_home) layout_top.addWidget(SpacerHorizontal()) layout_top.addWidget(self.combo) layout_top.addWidget(SpacerHorizontal()) layout_top.addWidget(self.button_channels) layout_top.addWidget(SpacerHorizontal()) layout_top.addStretch() layout_top.addWidget(self.button_refresh) self.frame_top.setLayout(layout_top) layout_body = QVBoxLayout() layout_body.addWidget(self.list) self.frame_body.setLayout(layout_body) layout_bottom = QHBoxLayout() layout_bottom.addWidget(self.label_status_action) layout_bottom.addWidget(SpacerHorizontal()) layout_bottom.addWidget(self.label_status) layout_bottom.addStretch() layout_bottom.addWidget(self.progress_bar) self.frame_bottom.setLayout(layout_bottom) layout = QVBoxLayout() layout.addWidget(self.frame_top) layout.addWidget(self.frame_body) layout.addWidget(self.frame_bottom) self.setLayout(layout) # Signals self.list.sig_conda_action_requested.connect( self.sig_conda_action_requested) self.list.sig_url_clicked.connect(self.sig_url_clicked) self.list.sig_launch_action_requested.connect( self.sig_launch_action_requested) self.button_channels.clicked.connect(self.show_channels) self.button_refresh.clicked.connect(self.refresh_cards) self.progress_bar.setVisible(False)
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()
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)
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)
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 __init__(self, parent=None): """License Manager main dialog.""" super(LicenseManagerDialog, self).__init__(parent=parent) self.api = AnacondaAPI() # Widgets self.message_box = None # For testing self.button_add = ButtonPrimary('Add license') self.button_ok = ButtonNormal('Close') self.button_remove = ButtonNormal('Remove license') self.button_contact = ButtonLink('Please contact us.') self.label_info = LabelBase('Manage your Continuum Analytics ' 'license keys.') self.label_contact = LabelBase('Got a problem with your license? ') self.proxy_model = QSortFilterProxyModel(parent=self) self.model = LicenseModel(parent=self) self.table = LicenseTableView(parent=self) self.delegate = BackgroundDelegate(self.table) # Widget setup self.proxy_model.setSourceModel(self.model) self.table.setItemDelegate(self.delegate) self.table.setModel(self.proxy_model) self.setWindowTitle('License Manager') # Layouts layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.label_info) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addStretch() layout_buttons.addWidget(self.button_add) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_remove) layout_buttons_bottom = QHBoxLayout() layout_buttons_bottom.addWidget(self.label_contact) layout_buttons_bottom.addWidget(self.button_contact) layout_buttons_bottom.addStretch() layout_buttons_bottom.addWidget(self.button_ok) layout = QVBoxLayout() layout.addLayout(layout_buttons) layout.addWidget(SpacerVertical()) layout.addWidget(self.table) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_buttons_bottom) self.setLayout(layout) # Signals self.button_add.clicked.connect(lambda: self.add_license()) self.button_remove.clicked.connect(self.remove_license) self.button_ok.clicked.connect(self.accept) self.button_contact.clicked.connect( lambda v=None: self.sig_url_clicked.emit(self.CONTACT_LINK, 'License Manager')) self.table.sig_dropped.connect(self.handle_drop) # Setup self.button_add.setFocus() self.load_licenses()
def __init__( self, name=None, display_name=None, description=None, command=None, versions=None, image_path=None, prefix=None, needs_license=False, non_conda=False, ): """Item with custom widget for the applications list.""" super(ListItemApplication, self).__init__() self.api = AnacondaAPI() self.prefix = prefix self.name = name self.display_name = display_name if display_name else name self.url = '' self.expired = False self.needs_license = needs_license self.description = description self.command = command self.versions = versions self.image_path = image_path if image_path else ANACONDA_ICON_256_PATH self.style_sheet = None self.timeout = 2000 self.non_conda = non_conda self._vscode_version_value = None # Widgets self.button_install = ButtonApplicationInstall("Install") # or Try! self.button_launch = ButtonApplicationLaunch("Launch") self.button_options = ButtonApplicationOptions() self.label_license = LabelApplicationLicense('') self.button_license = ButtonApplicationLicense('') self.label_icon = LabelApplicationIcon() self.label_name = LabelApplicationName(self.display_name) self.label_description = LabelApplicationDescription(self.description) self.button_version = ButtonApplicationVersion( to_text_string(self.version)) self.menu_options = QMenu('Application options') self.menu_versions = QMenu('Install specific version') self.pixmap = QPixmap(self.image_path) self.timer = QTimer() self.widget = WidgetApplication() self.frame_spinner = FrameApplicationSpinner() self.spinner = NavigatorSpinner(self.widget, total_width=16) lay = QHBoxLayout() lay.addWidget(self.spinner) self.frame_spinner.setLayout(lay) # Widget setup self.button_version.setFocusPolicy(Qt.NoFocus) self.button_version.setEnabled(True) self.label_description.setAlignment(Qt.AlignCenter) self.timer.setInterval(self.timeout) self.timer.setSingleShot(True) self.label_icon.setPixmap(self.pixmap) self.label_icon.setScaledContents(True) # important on High DPI! self.label_icon.setMaximumWidth(self.ICON_SIZE) self.label_icon.setMaximumHeight(self.ICON_SIZE) self.label_icon.setAlignment(Qt.AlignCenter) self.label_name.setAlignment(Qt.AlignCenter) self.label_name.setWordWrap(True) self.label_description.setWordWrap(True) self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter) self.frame_spinner.setVisible(False) # Layouts layout_spinner = QHBoxLayout() layout_spinner.addWidget(self.button_version, 0, Qt.AlignCenter) layout_spinner.addWidget(self.frame_spinner, 0, Qt.AlignCenter) layout_license = QHBoxLayout() layout_license.addStretch() layout_license.addWidget(self.label_license, 0, Qt.AlignCenter) layout_license.addWidget(self.button_license, 0, Qt.AlignCenter) layout_license.addStretch() layout_main = QVBoxLayout() layout_main.addWidget(self.button_options, 0, Qt.AlignRight) layout_main.addWidget(self.label_icon, 0, Qt.AlignCenter) layout_main.addWidget(self.label_name, 0, Qt.AlignCenter) layout_main.addLayout(layout_spinner) layout_main.addLayout(layout_license) layout_main.addWidget(self.label_description, 0, Qt.AlignCenter) layout_main.addWidget(self.button_launch, 0, Qt.AlignCenter) layout_main.addWidget(self.button_install, 0, Qt.AlignCenter) self.widget.setLayout(layout_main) self.widget.setStyleSheet(load_style_sheet()) self.setSizeHint(self.widget_size()) # This might help with visual quirks on the home screen self.widget.setMinimumSize(self.widget_size()) # Signals self.button_install.clicked.connect(self.install_application) self.button_launch.clicked.connect(self.launch_application) self.button_options.clicked.connect(self.actions_menu_requested) self.button_license.clicked.connect(self.launch_url) self.timer.timeout.connect(self._application_launched) # Setup self.update_status()
class _GATracker(object): """Google analytics tracker.""" TRACKER_ID = 'UA-27761864-8' def __init__(self): """Google analytics tracker.""" # Avoid blocking the UI self._queue = deque() self._threads = [] self._workers = [] self._timer = QTimer() self.api = AnacondaAPI() self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) self.setup() def setup(self): """Setup the tracker initial configuration.""" # Set tracker config try: user = self.api.client_user().get('login', '@anonymous') except Exception: user = '******' domain = self.api.client_domain() normalized_user = domain + '/' + user self.tracker = Tracker.create( self.TRACKER_ID, client_id=normalized_user, user_id=normalized_user, ) pkg_version = self.api.conda_package_version # Set custom variables python_version = '.'.join([str(i) for i in sys.version_info[:3]]) operating_system = ";".join([platform.uname()[0]] + list(platform.uname()[2:]), ) pyqt_version = (pkg_version(pkg='pyqt5', name='root') or pkg_version(pkg='pyqt4', name='root') or pkg_version(pkg='pyqt', name='root')) conda_version = pkg_version(pkg='conda', name='root') # Track initial global data events self.track_event('application', 'python-version', label=python_version) self.track_event('application', 'language', label=self.get_language()) self.track_event('application', 'screen-resolution', label=self.get_screen_resolution()) self.track_event('application', 'operating-system', label=operating_system) self.track_event('application', 'version', label=app_version) self.track_event('application', 'platform', label=self.api.conda_platform()) self.info = { 'python': python_version, 'language': self.get_language(), 'os': operating_system, 'version': app_version, 'platform': self.api.conda_platform(), 'qt': QT_VERSION, 'pyqt': pyqt_version, 'conda': conda_version } def _clean(self): """Check for inactive workers and remove their references.""" if self._workers: for w in self._workers: if w.is_finished(): self._workers.remove(w) if self._threads: for t in self._threads: if t.isFinished(): self._threads.remove(t) else: self._timer.stop() def _start(self): """Start the next item in the worker queue.""" if len(self._queue) == 1: thread = self._queue.popleft() thread.start() self._timer.start() def _create_worker(self, method, *args, **kwargs): """Create a worker for this client to be run in a separate thread.""" provide_analytics = CONF.get('main', 'provide_analytics') thread = QThread() worker = AnalyticsWorker(method, args, kwargs) if provide_analytics: worker.moveToThread(thread) worker.sig_finished.connect(self._start) worker.sig_finished.connect(thread.quit) thread.started.connect(worker.start) self._queue.append(thread) self._threads.append(thread) self._workers.append(worker) self._start() return worker def track_event(self, category, action, label=None): """Track analytyics event.""" return self._create_worker( self.tracker.send, 'event', category=category, action=action, label=str(label), ) def track_page(self, page, pagetitle=None): """ Track analytyics page. Page in the context of Navigator is a tab, a dialog, a message box. """ if pagetitle is None: title = ' '.join([i for i in page.split('/') if i]) else: title = pagetitle title = title.capitalize() return self._create_worker( self.tracker.send, 'pageview', page=page, pagetitle=title, ) def set_client_id(self, client_id=None): """Set the client tracker ID.""" self.tracker = Tracker.create('UA-74661388-1', client_id=client_id, user_id=client_id) def get_ip(self): """Return the current ip based on ipify.org.""" try: response = requests.get('https://api.ipify.org/?format=json', proxies=self.api.conda_load_proxy_config()) ip = response.json()['ip'] except Exception as error: logger.error(str(error)) ip = None return ip @staticmethod def get_language(): """Return the locale language.""" # Process locale language try: lang = locale.getdefaultlocale()[0] except Exception: lang = None if not lang: lang = 'en' return lang @staticmethod def get_screen_resolution(): """Return the screen resolution of the primary screen.""" try: widget = QDesktopWidget() geometry = widget.availableGeometry(widget.primaryScreen()) value = "{0}x{1}".format(geometry.width(), geometry.height()) except Exception: value = None return value
class HomeTab(WidgetBase): """Home applications tab.""" # name, prefix, sender sig_item_selected = Signal(object, object, object) # button_widget, sender sig_channels_requested = Signal(object, object) # application_name, command, prefix, leave_path_alone, sender sig_launch_action_requested = Signal(object, object, bool, object, object) # action, application_name, version, sender sig_conda_action_requested = Signal(object, object, object, object) # url sig_url_clicked = Signal(object) # TODO: Connect these signals to have more granularity # [{'name': package_name, 'version': version}...], sender sig_install_action_requested = Signal(object, object) sig_remove_action_requested = Signal(object, object) def __init__(self, parent=None): """Home applications tab.""" super(HomeTab, self).__init__(parent) # Variables self._parent = parent self.api = AnacondaAPI() self.applications = None self.style_sheet = None self.app_timers = None self.current_prefix = None # Widgets self.list = ListWidgetApplication() self.button_channels = ButtonHomeChannels('Channels') self.button_refresh = ButtonHomeRefresh('Refresh') self.combo = ComboHomeEnvironment() self.frame_top = FrameTabHeader(self) self.frame_body = FrameTabContent(self) self.frame_bottom = FrameTabFooter(self) self.label_home = LabelHome('Applications on') self.label_status_action = QLabel('') self.label_status = QLabel('') self.progress_bar = QProgressBar() self.first_widget = self.combo # Widget setup self.setObjectName('Tab') self.progress_bar.setTextVisible(False) self.list.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) # Layout layout_top = QHBoxLayout() layout_top.addWidget(self.label_home) layout_top.addWidget(SpacerHorizontal()) layout_top.addWidget(self.combo) layout_top.addWidget(SpacerHorizontal()) layout_top.addWidget(self.button_channels) layout_top.addWidget(SpacerHorizontal()) layout_top.addStretch() layout_top.addWidget(self.button_refresh) self.frame_top.setLayout(layout_top) layout_body = QVBoxLayout() layout_body.addWidget(self.list) self.frame_body.setLayout(layout_body) layout_bottom = QHBoxLayout() layout_bottom.addWidget(self.label_status_action) layout_bottom.addWidget(SpacerHorizontal()) layout_bottom.addWidget(self.label_status) layout_bottom.addStretch() layout_bottom.addWidget(self.progress_bar) self.frame_bottom.setLayout(layout_bottom) layout = QVBoxLayout() layout.addWidget(self.frame_top) layout.addWidget(self.frame_body) layout.addWidget(self.frame_bottom) self.setLayout(layout) # Signals self.list.sig_conda_action_requested.connect( self.sig_conda_action_requested) self.list.sig_url_clicked.connect(self.sig_url_clicked) self.list.sig_launch_action_requested.connect( self.sig_launch_action_requested) self.button_channels.clicked.connect(self.show_channels) self.button_refresh.clicked.connect(self.refresh_cards) self.progress_bar.setVisible(False) # --- Setup methods # ------------------------------------------------------------------------- def setup(self, conda_data): """Setup the tab content.""" conda_processed_info = conda_data.get('processed_info') environments = conda_processed_info.get('__environments') applications = conda_data.get('applications') self.current_prefix = conda_processed_info.get('default_prefix') self.set_environments(environments) self.set_applications(applications) def set_environments(self, environments): """Setup the environments list.""" # Disconnect to avoid triggering the signal when updating the content try: self.combo.currentIndexChanged.disconnect() except TypeError: pass self.combo.clear() for i, (env_prefix, env_name) in enumerate(environments.items()): self.combo.addItem(env_name, env_prefix) self.combo.setItemData(i, env_prefix, Qt.ToolTipRole) index = 0 for i, (env_prefix, env_name) in enumerate(environments.items()): if self.current_prefix == env_prefix: index = i break self.combo.setCurrentIndex(index) self.combo.currentIndexChanged.connect(self._item_selected) def set_applications(self, applications): """Build the list of applications present in the current conda env.""" apps = self.api.process_apps(applications, prefix=self.current_prefix) all_applications = [] installed_applications = [] not_installed_applications = [] # Check if some installed applications are not on the apps dict # for example when the channel was removed. linked_apps = self.api.conda_linked_apps_info(self.current_prefix) missing_apps = [app for app in linked_apps if app not in apps] for app in missing_apps: apps[app] = linked_apps[app] for app_name in sorted(list(apps.keys())): app = apps[app_name] item = ListItemApplication(name=app['name'], description=app['description'], versions=app['versions'], command=app['command'], image_path=app['image_path'], prefix=self.current_prefix, needs_license=app.get( 'needs_license', False)) if item.installed: installed_applications.append(item) else: not_installed_applications.append(item) all_applications = installed_applications + not_installed_applications self.list.clear() for i in all_applications: self.list.addItem(i) self.list.update_style_sheet(self.style_sheet) self.set_widgets_enabled(True) self.update_status() # --- Other methods # ------------------------------------------------------------------------- def current_environment(self): """Return the current selected environment.""" env_name = self.combo.currentText() return self.api.conda_get_prefix_envname(env_name) def refresh_cards(self): """Refresh application widgets. List widget items sometimes are hidden on resize. This method tries to compensate for that refreshing and repainting on user demand. """ self.list.update_style_sheet(self.style_sheet) self.list.repaint() for item in self.list.items(): if not item.widget.isVisible(): item.widget.repaint() def show_channels(self): """Emit signal requesting the channels dialog editor.""" self.sig_channels_requested.emit(self.button_channels, C.TAB_HOME) def update_list(self, name=None, version=None): """Update applications list.""" self.set_applications() self.label_status.setVisible(False) self.label_status_action.setVisible(False) self.progress_bar.setVisible(False) def update_versions(self, apps=None): """Update applications versions.""" self.items = [] for i in range(self.list.count()): item = self.list.item(i) self.items.append(item) if isinstance(item, ListItemApplication): name = item.name meta = apps.get(name) if meta: versions = meta['versions'] version = self.api.get_dev_tool_version(item.path) item.update_versions(version, versions) # --- Common Helpers (# FIXME: factor out to common base widget) # ------------------------------------------------------------------------- def _item_selected(self, index): """Notify that the item in combo (environment) changed.""" name = self.combo.itemText(index) prefix = self.combo.itemData(index) self.sig_item_selected.emit(name, prefix, C.TAB_HOME) @property def last_widget(self): """Return the last element of the list to be used in tab ordering.""" if self.list.items(): return self.list.items()[-1].widget def ordered_widgets(self, next_widget=None): """Return a list of the ordered widgets.""" ordered_widgets = [ self.combo, self.button_channels, self.button_refresh, ] ordered_widgets += self.list.ordered_widgets() return ordered_widgets def set_widgets_enabled(self, value): """Enable or disable widgets.""" self.combo.setEnabled(value) self.button_channels.setEnabled(value) self.button_refresh.setEnabled(value) for item in self.list.items(): item.button_install.setEnabled(value) item.button_options.setEnabled(value) if value: item.set_loading(not value) def update_items(self): """Update status of items in list.""" if self.list: for item in self.list.items(): item.update_status() def update_status(self, action='', message='', value=None, max_value=None): """Update the application action status.""" # Elide if too big width = QApplication.desktop().availableGeometry().width() max_status_length = round(width * (2.0 / 3.0), 0) msg_percent = 0.70 fm = self.label_status_action.fontMetrics() action = fm.elidedText(action, Qt.ElideRight, round(max_status_length * msg_percent, 0)) message = fm.elidedText( message, Qt.ElideRight, round(max_status_length * (1 - msg_percent), 0)) self.label_status_action.setText(action) 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) def update_style_sheet(self, style_sheet=None): """Update custom CSS style sheet.""" if style_sheet is None: self.style_sheet = load_style_sheet() else: self.style_sheet = style_sheet self.list.update_style_sheet(style_sheet=self.style_sheet) self.setStyleSheet(self.style_sheet)
class EnvironmentsTab(WidgetBase): """Conda environments tab.""" BLACKLIST = ['anaconda-navigator', '_license'] # Hide in package manager # --- Signals # ------------------------------------------------------------------------- sig_ready = Signal() # name, prefix, sender sig_item_selected = Signal(object, object, object) # sender, func_after_dlg_accept, func_callback_on_finished sig_create_requested = Signal() sig_clone_requested = Signal() sig_import_requested = Signal() sig_remove_requested = Signal() # button_widget, sender_constant sig_channels_requested = Signal(object, object) # sender_constant sig_update_index_requested = Signal(object) sig_cancel_requested = Signal(object) # conda_packages_action_dict, pip_packages_action_dict sig_packages_action_requested = Signal(object, object) def __init__(self, parent=None): """Conda environments tab.""" super(EnvironmentsTab, self).__init__(parent) # Variables self.api = AnacondaAPI() self.current_prefix = None self.style_sheet = None # Widgets self.frame_header_left = FrameTabHeader() self.frame_list = FrameEnvironmentsList(self) self.frame_widget = FrameEnvironmentsPackages(self) self.text_search = LineEditSearch() self.list = ListWidgetEnv() self.menu_list = QMenu() self.button_create = ButtonToolNormal(text="Create") self.button_clone = ButtonToolNormal(text="Clone") self.button_import = ButtonToolNormal(text="Import") self.button_remove = ButtonToolNormal(text="Remove") self.button_toggle_collapse = ButtonToggleCollapse() self.widget = CondaPackagesWidget(parent=self) # Widgets setup self.frame_list.is_expanded = True self.text_search.setPlaceholderText("Search Environments") self.list.setContextMenuPolicy(Qt.CustomContextMenu) self.button_create.setObjectName("create") # Needed for QSS selectors self.button_clone.setObjectName("clone") self.button_import.setObjectName("import") self.button_remove.setObjectName("remove") self.widget.textbox_search.set_icon_visibility(False) # Layouts layout_header_left = QVBoxLayout() layout_header_left.addWidget(self.text_search) self.frame_header_left.setLayout(layout_header_left) layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.button_create) layout_buttons.addWidget(self.button_clone) layout_buttons.addWidget(self.button_import) layout_buttons.addWidget(self.button_remove) layout_list_buttons = QVBoxLayout() layout_list_buttons.addWidget(self.frame_header_left) layout_list_buttons.addWidget(self.list) layout_list_buttons.addLayout(layout_buttons) self.frame_list.setLayout(layout_list_buttons) layout_widget = QHBoxLayout() layout_widget.addWidget(self.widget) self.frame_widget.setLayout(layout_widget) layout_main = QHBoxLayout() layout_main.addWidget(self.frame_list, 10) layout_main.addWidget(self.button_toggle_collapse, 1) layout_main.addWidget(self.frame_widget, 30) self.setLayout(layout_main) # Signals for buttons and boxes self.button_toggle_collapse.clicked.connect(self.expand_collapse) self.button_create.clicked.connect(self.sig_create_requested) self.button_clone.clicked.connect(self.sig_clone_requested) self.button_import.clicked.connect(self.sig_import_requested) self.button_remove.clicked.connect(self.sig_remove_requested) self.text_search.textChanged.connect(self.filter_list) # Signals for list self.list.sig_item_selected.connect(self._item_selected) # Signals for packages widget self.widget.sig_ready.connect(self.sig_ready) self.widget.sig_channels_requested.connect(self.sig_channels_requested) self.widget.sig_update_index_requested.connect( self.sig_update_index_requested) self.widget.sig_cancel_requested.connect(self.sig_cancel_requested) self.widget.sig_packages_action_requested.connect( self.sig_packages_action_requested) # --- Setup methods # ------------------------------------------------------------------------- def setup(self, conda_data): """Setup tab content and populates the list of environments.""" self.set_widgets_enabled(False) conda_processed_info = conda_data.get('processed_info') environments = conda_processed_info.get('__environments') packages = conda_data.get('packages') self.current_prefix = conda_processed_info.get('default_prefix') self.set_environments(environments) self.set_packages(packages) def set_environments(self, environments): """Populate the list of environments.""" self.list.clear() selected_item_row = 0 for i, (env_prefix, env_name) in enumerate(environments.items()): item = ListItemEnv(prefix=env_prefix, name=env_name) item.button_options.clicked.connect(self.show_environment_menu) if env_prefix == self.current_prefix: selected_item_row = i self.list.addItem(item) self.list.setCurrentRow(selected_item_row, loading=True) self.filter_list() def _set_packages(self, worker, output, error): """Set packages callback.""" packages, model_data = output self.widget.setup(packages, model_data) self.set_widgets_enabled(True) self.set_loading(prefix=self.current_prefix, value=False) def set_packages(self, packages): """Set packages widget content.""" worker = self.api.process_packages(packages, prefix=self.current_prefix, blacklist=self.BLACKLIST) worker.sig_chain_finished.connect(self._set_packages) def show_environment_menu(self, value=None, position=None): """Show the environment actions menu.""" self.menu_list.clear() menu_item = self.menu_list.addAction('Open Terminal') menu_item.triggered.connect( lambda: self.open_environment_in('terminal')) for word in ['Python', 'IPython', 'Jupyter Notebook']: menu_item = self.menu_list.addAction("Open with " + word) menu_item.triggered.connect( lambda x, w=word: self.open_environment_in(w.lower())) current_item = self.list.currentItem() prefix = current_item.prefix if isinstance(position, bool) or position is None: width = current_item.button_options.width() position = QPoint(width, 0) point = QPoint(0, 0) parent_position = current_item.button_options.mapToGlobal(point) self.menu_list.move(parent_position + position) # Disabled actions depending on the environment installed packages actions = self.menu_list.actions() actions[2].setEnabled(launch.check_prog('ipython', prefix)) actions[3].setEnabled(launch.check_prog('notebook', prefix)) self.menu_list.exec_() def open_environment_in(self, which): """Open selected environment in console terminal.""" prefix = self.list.currentItem().prefix logger.debug("%s, %s", which, prefix) if which == 'terminal': launch.console(prefix) else: launch.py_in_console(prefix, which) # --- Common Helpers (# FIXME: factor out to common base widget) # ------------------------------------------------------------------------- def _item_selected(self, item): """Callback to emit signal as user selects an item from the list.""" self.set_loading(prefix=item.prefix) self.sig_item_selected.emit(item.name, item.prefix, C.TAB_ENVIRONMENT) def add_temporal_item(self, name): """Creates a temporal item on list while creation becomes effective.""" item_names = [item.name for item in self.list.items()] item_names.append(name) index = list(sorted(item_names)).index(name) + 1 item = ListItemEnv(name=name) self.list.insertItem(index, item) self.list.setCurrentRow(index) self.list.scrollToItem(item) item.set_loading(True) def expand_collapse(self): """Expand or collapse the list selector.""" if self.frame_list.is_expanded: self.frame_list.hide() self.frame_list.is_expanded = False else: self.frame_list.show() self.frame_list.is_expanded = True def filter_list(self, text=None): """Filter items in list by name.""" text = self.text_search.text().lower() for i in range(self.list.count()): item = self.list.item(i) item.setHidden(text not in item.name.lower()) if not item.widget.isVisible(): item.widget.repaint() def ordered_widgets(self, next_widget=None): """Return a list of the ordered widgets.""" if next_widget is not None: self.widget.table_last_row.add_focus_widget(next_widget) ordered_widgets = [ self.text_search, ] ordered_widgets += self.list.ordered_widgets() ordered_widgets += [ self.button_create, self.button_clone, self.button_import, self.button_remove, self.widget.combobox_filter, self.widget.button_channels, self.widget.button_update, self.widget.textbox_search, # self.widget.table_first_row, self.widget.table, self.widget.table_last_row, self.widget.button_apply, self.widget.button_clear, self.widget.button_cancel, ] return ordered_widgets def refresh(self): """Refresh the enabled/disabled status of the widget and subwidgets.""" is_root = self.current_prefix == self.api.ROOT_PREFIX self.button_clone.setDisabled(is_root) self.button_remove.setDisabled(is_root) def set_loading(self, prefix=None, value=True): """Set the item given by `prefix` to loading state.""" for row, item in enumerate(self.list.items()): if item.prefix == prefix: item.set_loading(value) self.list.setCurrentRow(row) break def set_widgets_enabled(self, value): """Change the enabled status of widgets and subwidgets.""" self.list.setEnabled(value) self.button_create.setEnabled(value) self.button_clone.setEnabled(value) self.button_import.setEnabled(value) self.button_remove.setEnabled(value) self.widget.set_widgets_enabled(value) if value: self.refresh() def update_status(self, action='', message='', value=None, max_value=None): """Update widget status and progress bar.""" self.widget.update_status(action=action, message=message, value=value, max_value=max_value) def update_style_sheet(self, style_sheet=None): """Update custom CSS stylesheet.""" if style_sheet is None: self.style_sheet = load_style_sheet() else: self.style_sheet = style_sheet self.setStyleSheet(self.style_sheet) self.list.update_style_sheet(self.style_sheet) self.menu_list.setStyleSheet(self.style_sheet)
def __init__(self, parent, config=CONF): """Conda Packages Widget.""" super(CondaPackagesWidget, self).__init__(parent) self._parent = parent self._current_model_index = None self._current_action_name = '' self._current_table_scroll = None self._hide_widgets = False self.api = AnacondaAPI() self.prefix = None self.style_sheet = None self.message = '' self.config = config # Widgets self.bbox = QDialogButtonBox(Qt.Horizontal) self.button_cancel = ButtonPackageCancel('Cancel') self.button_channels = ButtonPackageChannels(_('Channels')) self.button_ok = ButtonPackageOk(_('Ok')) self.button_update = ButtonPackageUpdate(_('Update index...')) self.button_apply = ButtonPackageApply(_('Apply')) self.button_clear = ButtonPackageClear(_('Clear')) self.combobox_filter = ComboBoxPackageFilter(self) self.frame_top = FrameTabHeader() self.frame_bottom = FrameTabFooter() self.progress_bar = ProgressBarPackage(self) self.label_status = LabelPackageStatus(self) self.label_status_action = LabelPackageStatusAction(self) self.table = TableCondaPackages(self) self.textbox_search = LineEditSearch(self) self.widgets = [ self.button_update, self.button_channels, self.combobox_filter, self.textbox_search, self.table, self.button_ok, self.button_apply, self.button_clear ] self.table_first_row = FirstRowWidget( widget_before=self.textbox_search) self.table_last_row = LastRowWidget(widgets_after=[ self.button_apply, self.button_clear, self.button_cancel, ]) # Widgets setup max_height = self.label_status.fontMetrics().height() max_width = self.textbox_search.fontMetrics().width('M' * 23) self.bbox.addButton(self.button_ok, QDialogButtonBox.ActionRole) self.button_ok.setMaximumSize(QSize(0, 0)) self.button_ok.setVisible(False) self.button_channels.setCheckable(True) combo_items = [k for k in C.COMBOBOX_VALUES_ORDERED] self.combobox_filter.addItems(combo_items) self.combobox_filter.setMinimumWidth(120) self.progress_bar.setMaximumHeight(max_height * 1.2) self.progress_bar.setMaximumWidth(max_height * 12) self.progress_bar.setTextVisible(False) self.progress_bar.setVisible(False) self.setMinimumSize(QSize(480, 300)) self.setWindowTitle(_("Conda Package Manager")) self.label_status.setFixedHeight(max_height * 1.5) self.textbox_search.setMaximumWidth(max_width) self.textbox_search.setPlaceholderText('Search Packages') self.table_first_row.setMaximumHeight(0) self.table_last_row.setMaximumHeight(0) self.table_last_row.setVisible(False) self.table_first_row.setVisible(False) # Layout top_layout = QHBoxLayout() top_layout.addWidget(self.combobox_filter, 0, Qt.AlignCenter) top_layout.addWidget(SpacerHorizontal()) top_layout.addWidget(self.button_channels, 0, Qt.AlignCenter) top_layout.addWidget(SpacerHorizontal()) top_layout.addWidget(self.button_update, 0, Qt.AlignCenter) top_layout.addWidget(SpacerHorizontal()) top_layout.addWidget(self.textbox_search, 0, Qt.AlignCenter) top_layout.addStretch() self.frame_top.setLayout(top_layout) middle_layout = QVBoxLayout() middle_layout.addWidget(self.table_first_row) middle_layout.addWidget(self.table) middle_layout.addWidget(self.table_last_row) bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.label_status_action) bottom_layout.addWidget(SpacerHorizontal()) bottom_layout.addWidget(self.label_status) bottom_layout.addStretch() bottom_layout.addWidget(self.progress_bar) bottom_layout.addWidget(SpacerHorizontal()) bottom_layout.addWidget(self.button_cancel) bottom_layout.addWidget(SpacerHorizontal()) bottom_layout.addWidget(self.button_apply) bottom_layout.addWidget(SpacerHorizontal()) bottom_layout.addWidget(self.button_clear) self.frame_bottom.setLayout(bottom_layout) layout = QVBoxLayout(self) layout.addWidget(self.frame_top) layout.addLayout(middle_layout) layout.addWidget(self.frame_bottom) self.setLayout(layout) # Signals and slots self.button_cancel.clicked.connect( lambda: self.sig_cancel_requested.emit(C.TAB_ENVIRONMENT)) self.combobox_filter.currentTextChanged.connect(self.filter_package) self.button_apply.clicked.connect(self.apply_multiple_actions) self.button_clear.clicked.connect(self.clear_actions) self.button_channels.clicked.connect(self.show_channels) self.button_update.clicked.connect(self.update_package_index) self.textbox_search.textChanged.connect(self.search_package) self.table.sig_actions_updated.connect(self.update_actions) self.table.sig_status_updated.connect(self.update_status) self.table.sig_next_focus.connect(self.table_last_row.handle_tab) self.table.sig_previous_focus.connect( lambda: self.table_first_row.widget_before.setFocus()) self.table_first_row.sig_enter_first.connect(self._handle_tab_focus) self.table_last_row.sig_enter_last.connect(self._handle_backtab_focus) self.button_cancel.setVisible(False)
class ListItemApplication(ListWidgetItemBase): """Item with custom widget for the applications list.""" ICON_SIZE = 64 def __init__( self, name=None, display_name=None, description=None, command=None, versions=None, image_path=None, prefix=None, needs_license=False, non_conda=False, ): """Item with custom widget for the applications list.""" super(ListItemApplication, self).__init__() self.api = AnacondaAPI() self.prefix = prefix self.name = name self.display_name = display_name if display_name else name self.url = '' self.expired = False self.needs_license = needs_license self.description = description self.command = command self.versions = versions self.image_path = image_path if image_path else ANACONDA_ICON_256_PATH self.style_sheet = None self.timeout = 2000 self.non_conda = non_conda self._vscode_version_value = None # Widgets self.button_install = ButtonApplicationInstall("Install") # or Try! self.button_launch = ButtonApplicationLaunch("Launch") self.button_options = ButtonApplicationOptions() self.label_license = LabelApplicationLicense('') self.button_license = ButtonApplicationLicense('') self.label_icon = LabelApplicationIcon() self.label_name = LabelApplicationName(self.display_name) self.label_description = LabelApplicationDescription(self.description) self.button_version = ButtonApplicationVersion( to_text_string(self.version)) self.menu_options = QMenu('Application options') self.menu_versions = QMenu('Install specific version') self.pixmap = QPixmap(self.image_path) self.timer = QTimer() self.widget = WidgetApplication() self.frame_spinner = FrameApplicationSpinner() self.spinner = NavigatorSpinner(self.widget, total_width=16) lay = QHBoxLayout() lay.addWidget(self.spinner) self.frame_spinner.setLayout(lay) # Widget setup self.button_version.setFocusPolicy(Qt.NoFocus) self.button_version.setEnabled(True) self.label_description.setAlignment(Qt.AlignCenter) self.timer.setInterval(self.timeout) self.timer.setSingleShot(True) self.label_icon.setPixmap(self.pixmap) self.label_icon.setScaledContents(True) # important on High DPI! self.label_icon.setMaximumWidth(self.ICON_SIZE) self.label_icon.setMaximumHeight(self.ICON_SIZE) self.label_icon.setAlignment(Qt.AlignCenter) self.label_name.setAlignment(Qt.AlignCenter) self.label_name.setWordWrap(True) self.label_description.setWordWrap(True) self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter) self.frame_spinner.setVisible(False) # Layouts layout_spinner = QHBoxLayout() layout_spinner.addWidget(self.button_version, 0, Qt.AlignCenter) layout_spinner.addWidget(self.frame_spinner, 0, Qt.AlignCenter) layout_license = QHBoxLayout() layout_license.addStretch() layout_license.addWidget(self.label_license, 0, Qt.AlignCenter) layout_license.addWidget(self.button_license, 0, Qt.AlignCenter) layout_license.addStretch() layout_main = QVBoxLayout() layout_main.addWidget(self.button_options, 0, Qt.AlignRight) layout_main.addWidget(self.label_icon, 0, Qt.AlignCenter) layout_main.addWidget(self.label_name, 0, Qt.AlignCenter) layout_main.addLayout(layout_spinner) layout_main.addLayout(layout_license) layout_main.addWidget(self.label_description, 0, Qt.AlignCenter) layout_main.addWidget(self.button_launch, 0, Qt.AlignCenter) layout_main.addWidget(self.button_install, 0, Qt.AlignCenter) self.widget.setLayout(layout_main) self.widget.setStyleSheet(load_style_sheet()) self.setSizeHint(self.widget_size()) # This might help with visual quirks on the home screen self.widget.setMinimumSize(self.widget_size()) # Signals self.button_install.clicked.connect(self.install_application) self.button_launch.clicked.connect(self.launch_application) self.button_options.clicked.connect(self.actions_menu_requested) self.button_license.clicked.connect(self.launch_url) self.timer.timeout.connect(self._application_launched) # Setup self.update_status() # --- Callbacks # ------------------------------------------------------------------------- def _application_launched(self): self.button_launch.setDisabled(False) update_pointer() # --- Helpers # ------------------------------------------------------------------------- def update_style_sheet(self, style_sheet=None): """Update custom CSS stylesheet.""" if style_sheet: self.style_sheet = style_sheet else: self.style_sheet = load_style_sheet() self.menu_options.setStyleSheet(self.style_sheet) self.menu_versions.setStyleSheet(self.style_sheet) def ordered_widgets(self): """Return a list of the ordered widgets.""" return [ self.button_license, self.button_install, self.button_launch, self.button_options ] @staticmethod def widget_size(): """Return the size defined in the SASS file.""" return QSize(SASS_VARIABLES.WIDGET_APPLICATION_TOTAL_WIDTH, SASS_VARIABLES.WIDGET_APPLICATION_TOTAL_HEIGHT) def launch_url(self): """Launch signal for url click.""" self.widget.sig_url_clicked.emit(self.url) def actions_menu_requested(self): """Create and display menu for the currently selected application.""" self.menu_options.clear() self.menu_versions.clear() # Add versions menu versions = self.versions if self.versions else [] version_actions = [] for version in reversed(versions): action = create_action(self.widget, version, triggered=lambda value, version=version: self.install_application(version=version)) action.setCheckable(True) if self.version == version and self.installed: action.setChecked(True) action.setDisabled(True) version_actions.append(action) install_action = create_action( self.widget, 'Install application', triggered=lambda: self.install_application()) install_action.setEnabled(not self.installed) update_action = create_action( self.widget, 'Update application', triggered=lambda: self.update_application()) if versions and versions[-1] == self.version: update_action.setDisabled(True) else: update_action.setDisabled(False) if self.non_conda and self.name == GLOBAL_VSCODE_APP: update_action.setDisabled(True) remove_action = create_action( self.widget, 'Remove application', triggered=lambda: self.remove_application()) remove_action.setEnabled(self.installed) actions = [ install_action, update_action, remove_action, None, self.menu_versions ] add_actions(self.menu_options, actions) add_actions(self.menu_versions, version_actions) offset = QPoint(self.button_options.width(), 0) position = self.button_options.mapToGlobal(QPoint(0, 0)) self.menu_versions.setEnabled(len(versions) > 1) self.menu_options.move(position + offset) self.menu_options.exec_() def update_status(self): """Update status.""" # License check license_label_text = '' license_url_text = '' self.url = '' self.expired = False button_label = 'Install' if self.needs_license: # TODO: Fix this method to use the api license_info = self.api.get_package_license(self.name) license_days = self.api.get_days_left(license_info) end_date = license_info.get('end_date', '') self.expired = license_days == 0 plural = 's' if license_days != 1 else '' is_trial = license_info.get('type', '').lower() == 'trial' if self.installed and license_info: if is_trial and not self.expired: license_label_text = ('Trial, {days} day{plural} ' 'remaining'.format(days=license_days, plural=plural)) self.url = '' elif is_trial and self.expired: license_label_text = 'Trial expired, ' license_url_text = 'contact us' self.url = 'mailto:[email protected]' elif not is_trial and not self.expired: license_label_text = 'License expires {}'.format(end_date) self.url = '' elif not is_trial and self.expired: license_url_text = 'Renew license' self.url = 'mailto:[email protected]' elif self.installed and not bool(license_info): # Installed but no license found! license_url_text = 'No license found' self.url = 'mailto:[email protected]' else: if not self.expired: button_label = 'Install' else: button_label = 'Try' self.button_license.setText(license_url_text) self.button_license.setVisible(bool(self.url)) self.label_license.setText(license_label_text) self.label_license.setVisible(bool(license_label_text)) # Version and version updates if (self.versions and self.version != self.versions[-1] and self.installed): # The property is used with CSS to display updatable packages. self.button_version.setProperty('pressed', True) self.button_version.setToolTip('Version {0} available'.format( self.versions[-1])) else: self.button_version.setProperty('pressed', False) # For VScode app do not display if new updates are available # See: https://github.com/ContinuumIO/navigator/issues/1504 if self.non_conda and self.name == GLOBAL_VSCODE_APP: self.button_version.setProperty('pressed', False) self.button_version.setToolTip('') if not self.needs_license: self.button_install.setText(button_label) self.button_install.setVisible(not self.installed) self.button_launch.setVisible(self.installed) else: self.button_install.setText('Try' if self.expired else 'Install') self.button_launch.setVisible(not self.expired) self.button_install.setVisible(self.expired) self.button_launch.setEnabled(True) def update_versions(self, version=None, versions=None): """Update button visibility depending on update availability.""" logger.debug(str((self.name, self.dev_tool, self.installed))) if self.installed and version: self.button_options.setVisible(True) self.button_version.setText(version) self.button_version.setVisible(True) elif not self.installed and versions: self.button_install.setEnabled(True) self.button_version.setText(versions[-1]) self.button_version.setVisible(True) self.versions = versions self.version = version self.update_status() def set_loading(self, value): """Set loading status.""" self.button_install.setDisabled(value) self.button_options.setDisabled(value) self.button_launch.setDisabled(value) self.button_license.setDisabled(value) if value: self.spinner.start() else: self.spinner.stop() if self.version is None and self.versions is not None: version = self.versions[-1] else: version = self.version self.button_version.setText(version) self.button_launch.setDisabled(self.expired) self.frame_spinner.setVisible(value) self.button_version.setVisible(not value) # --- Helpers using api # ------------------------------------------------------------------------- def _vscode_version(self): """Query the vscode version for the default installation path.""" version = None if self._vscode_version_value is None: cmd = [self.api.vscode_executable(), '--version'] # print(cmd) import subprocess try: output = subprocess.check_output(cmd) if PY3: output = output.decode() output = [o for o in output.split('\n') if o and '.' in o] # print(output) if output: version = output[0] except Exception: pass # print(e) self._vscode_version_value = version else: version = self._vscode_version_value return version @property def installed(self): """Return the installed status of the package.""" version = None if self.non_conda and self.name == GLOBAL_VSCODE_APP: # TODO: Vscode program location, check existence version = self._vscode_version() elif self.prefix: version = self.api.conda_package_version(prefix=self.prefix, pkg=self.name, build=False) return bool(version) @property def version(self): """Return the current installed version or the highest version.""" version = None if self.non_conda and self.name == GLOBAL_VSCODE_APP: version = self._vscode_version() elif self.prefix: version = self.api.conda_package_version(prefix=self.prefix, pkg=self.name, build=False) if not version: version = self.versions[-1] return version # --- Application actions # ------------------------------------------------------------------------ def install_application(self, value=None, version=None, install=True): """ Update the application on the defined prefix environment. This is used for both normal install and specific version install. """ if not version: version = self.versions[-1] action = C.APPLICATION_INSTALL if install else C.APPLICATION_UPDATE self.widget.sig_conda_action_requested.emit( action, self.name, version, C.TAB_HOME, self.non_conda, ) self.set_loading(True) def remove_application(self): """Remove the application from the defined prefix environment.""" self.widget.sig_conda_action_requested.emit( C.APPLICATION_REMOVE, self.name, None, C.TAB_HOME, self.non_conda, ) self.set_loading(True) def update_application(self): """Update the application on the defined prefix environment.""" self.install_application(version=self.versions[-1], install=False) def launch_application(self): """Launch application installed in prefix environment.""" leave_path_alone = False if self.command is not None: if self.non_conda and self.name == GLOBAL_VSCODE_APP: leave_path_alone = True args = [self.command] else: args = self.command.split(' ') leave_path_alone = True self.button_launch.setDisabled(True) self.timer.setInterval(self.timeout) self.timer.start() update_pointer(Qt.BusyCursor) self.widget.sig_launch_action_requested.emit( self.name, args, leave_path_alone, self.prefix, C.TAB_HOME, self.non_conda, )