class ProjectsTab(WidgetBase): """ Projects management tab. """ # sig_apps_changed = Signal(str) sig_apps_updated = Signal() sig_project_updated = Signal() sig_status_updated = Signal(str, int, int, int) def __init__(self, parent=None): super(ProjectsTab, self).__init__(parent) self.api = AnacondaAPI() self.current_project_item = None self.dev_tool_visible = False self.new_project_name_template = "New project {0}" self.project_counter = 1 # Counter for new projects self.projects_dict = {} # Widgets self.button_add = QPushButton("Import") self.button_create = QPushButton("Create") self.button_delete = QPushButton("Delete") self.button_duplicate = QPushButton("Duplicate") self.label_search = QLabel('') self.list_projects = ProjectsListWidget() self.project_widget = ProjectWidget(parent=self) self.text_search = QLineEdit() search_icon = qta.icon('fa.search') # Widgets setup self.label_search.setPixmap(search_icon.pixmap(16, 16)) self.text_search.setPlaceholderText("Search Projects") self.setObjectName("Tab") self.list_projects.setObjectName("ListProjects") # Layouts projects_layout = QVBoxLayout() search_layout = QHBoxLayout() search_layout.addWidget(self.text_search) search_layout.addWidget(self.label_search) projects_layout.addLayout(search_layout) projects_layout.addWidget(self.list_projects) project_buttons_layout = QHBoxLayout() project_buttons_layout.addWidget(self.button_create) project_buttons_layout.addWidget(self.button_add) project_buttons_layout.addWidget(self.button_duplicate) project_buttons_layout.addWidget(self.button_delete) projects_layout.addLayout(project_buttons_layout) main_layout = QHBoxLayout() main_layout.addSpacing(20) main_layout.addLayout(projects_layout, 1) main_layout.addSpacing(20) main_layout.addWidget(self.project_widget, 2) self.setLayout(main_layout) # Signals self.button_add.clicked.connect(lambda: self.add_project(path=None)) self.button_create.clicked.connect( lambda: self.create_new_project(path=None)) self.button_delete.clicked.connect(self.delete_project) self.button_duplicate.clicked.connect(self.duplicate_project) self.list_projects.itemChanged.connect(self.set_app) self.list_projects.itemSelectionChanged.connect( self.load_project_information) self.project_widget.sig_apps_updated.connect(self.sig_apps_updated) self.project_widget.sig_project_updated.connect( self.sig_project_updated) self.project_widget.sig_project_name_updated.connect( self.update_project_name) self.project_widget.sig_project_icon_updated.connect( self.update_project_icon) self.project_widget.sig_project_commands_updated.connect( self.update_project_commands) self.text_search.textChanged.connect(self.filter_projects) # Setup self.button_duplicate.setDisabled(True) self.button_duplicate.setVisible(False) self.button_delete.setDisabled(True) self.update_style_sheet() def update_style_sheet(self, style_sheet=None): """ """ if style_sheet is None: style_sheet = load_style_sheet() self.list_projects.setFrameStyle(QFrame.NoFrame) self.list_projects.setFrameShape(QFrame.NoFrame) self.list_projects.setStyleSheet(style_sheet) for i in range(self.list_projects.count()): item = self.list_projects.item(i) item.setSizeHint(QSize(60, 60)) self.list_projects.repaint() def setup_tab(self): """ self.update_visibility(disabled=True) """ self.load_projects() self.project_widget.setDisabled(True) self.toogle_dev_tools(visible=self.dev_tool_visible) self.load_last_active_project() # --- Helpers # ------------------------------------------------------------------------- def _update_project_numbering(self): """ Update the new project count number based on existing project names. """ new_project_numbering = [0] for path in self.projects_dict: project = self.projects_dict[path] if self.new_project_name_template[:-3] in project.name: try: number = int(project.name.split(' ')[-1]) except Exception: number = 0 new_project_numbering.append(number) self.project_counter = max(new_project_numbering) + 1 def set_app(self, item): """ Check box changed, set project's is_app property. """ item.project.is_app = item.checkState() == Qt.Checked item.project.save(item.path) self.sig_apps_updated.emit() def filter_projects(self): """ Filter displayed projects by matching search text. """ text = self.text_search.text().lower() for i in range(self.list_projects.count()): item = self.list_projects.item(i) item.setHidden(text not in item.text().lower()) self.toogle_dev_tools(visible=self.dev_tool_visible) def load_projects(self): """ Load all projects available in the configuration folder and settings. """ paths = get_projects_path() self.projects_dict = self.api.load_projects(paths) self._update_project_numbering() self.load_project_list() def load_last_active_project(self): """ Make sure project selection ("current") persists. """ active_path = CONF.get('main', 'current_project_path', DEFAULT_PROJECT_PATH) for i in range(self.list_projects.count()): item = self.list_projects.item(i) if item.path == active_path: self.list_projects.setCurrentItem(item) self.current_project_item = item self.load_project_information() def load_project_information(self): """ Update the project widget after a project has been selected. """ # Project Widget update item = self.list_projects.currentItem() details = item.project path = item.path self.project_widget.load_project_information(path) self.current_project_item = item CONF.set('main', 'current_project_path', item.path) self.button_delete.setDisabled(details.is_default) self.button_duplicate.setDisabled(details.is_default) self.project_widget.setDisabled(False) def load_project_list(self): """ Populate the project selection list. """ self.list_projects.clear() for path in self.projects_dict: project = self.projects_dict[path] name = project.name item = QListWidgetItem(name) if getattr(project, 'icon', None): icon = self.api.load_icon(path, project) else: icon = qta.icon('fa.cog') item.setIcon(icon) item.project = project item.path = path if project.commands: item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) if project.is_app: item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) self.list_projects.addItem(item) self.toogle_dev_tools(visible=self.dev_tool_visible) for i in range(self.list_projects.count()): item = self.list_projects.item(i) item.setSizeHint(QSize(item.sizeHint().width(), self._height())) def _height(self): """ Get the height for the row in the widget based on OS font metrics. """ return self.fontMetrics().height() * 2 def toogle_dev_tools(self, visible=None): """ Display/Hide development tools project folders. """ if visible is not None: self.dev_tool_visible = visible else: self.dev_tool_visible = not self.dev_tool_visible for i in range(self.list_projects.count()): item = self.list_projects.item(i) is_conda_app = item.project.is_conda_app if is_conda_app: item.setHidden(not self.dev_tool_visible) def update_visibility(self, disabled=True): """ Enable/Disabled widgets during operations. """ self.list_projects.setDisabled(disabled) self.button_add.setDisabled(disabled) self.button_create.setDisabled(disabled) self.button_delete.setDisabled(disabled) self.button_duplicate.setDisabled(disabled) self.project_widget.setDisabled(disabled) def update_status(self, message='', timeout=0): """ Update project tab status. """ self.sig_status_updated.emit(message, timeout, -1, -1) def update_project_name(self, path, old_name, new_name): """ Update project name item only. Avoid reloading projects. """ item = self.get_item_by_path(path) item.setText(new_name) def update_project_icon(self, path, old_image_path, new_image_path): """ Update project item icon only. Avoid reloading projects. """ self.api.save_icon(new_image_path, path) icon = QIcon(QPixmap(new_image_path)) item = self.get_item_by_path(path) item.setIcon(icon) def get_item_by_path(self, path): """ """ for i in range(self.list_projects.count()): item = self.list_projects.item(i) if item.path == path: return item def update_project_commands(self, path, commands): """ Update project checkbox icon only. Avoid reloading projects. """ # FIXME: There should be a better way! item = self.get_item_by_path(path) item.project.commands = commands if len(commands) != 0: item.setData(Qt.CheckStateRole, item.checkState()) else: item.setData(Qt.CheckStateRole, None) # --- Main project tab actions # ------------------------------------------------------------------------- def add_project(self, path=None, new_path=None): """ Add/import existing project given by `path` into `new_path`. """ if path is None: path = getexistingdirectory(caption="Select origin project folder " "to import", basedir=get_home_dir()) if path: self.update_status('Adding project at {0}'.format(path)) self.update_visibility(disabled=True) update_pointer(Qt.BusyCursor) # shutil.copytree does not work if the destionation folder exists # os.removedirs(new_path) # shutil.copytree(path, new_path) worker = self.api.add_project(path) worker.path = path worker.sig_finished.connect(self._project_added) def create_new_project(self, path=None): """ Create a new project. If `path` is provided no dialog is openned. """ if path is None: path = getexistingdirectory(caption='Select new project folder ' 'location', basedir=get_home_dir()) # Check that project.yaml does not exists, otherwise warn the user and # prompt for action? if path: name = self.new_project_name_template.format(self.project_counter) self.update_status('Creating project "{0}"'.format(name)) worker = self.api.create_new_project(path=path, name=name) worker.sig_finished.connect(self._project_created) worker.path = path self.update_visibility(disabled=True) self.project_counter += 1 update_pointer(Qt.BusyCursor) def delete_project(self): """ Remove project from listing and optionally delete all project files. """ project = self.list_projects.currentItem().project # The default project cant be deleted. Enforce extra security for that if project.is_default: return msgbox = MessageCheckBox.warning( self, "Delete project", "Do you really want to delete project <strong>{0}</strong>?" "".format(project.name), QMessageBox.Yes | QMessageBox.No, checkbox_text="Delete all project files") reply = msgbox.exec_() if reply == MessageCheckBox.Yes: self.sig_status_updated.emit( 'Deleting project ' '"{0}"'.format(project.name), 0, -1, -1) item = self.list_projects.takeItem(self.list_projects.currentRow()) projects_path = list(set(CONF.get('main', 'projects_path', []))) projects_path = list(projects_path) projects_path.remove(item.path) CONF.set('main', 'projects_path', projects_path) if msgbox.is_checked(): if os.path.isdir(item.path): shutil.rmtree(item.path) # Select the default project after deletion of a project default_item = self.list_projects.item(0) self.list_projects.itemClicked.emit(default_item) self.sig_status_updated.emit('', 0, -1, -1) self.sig_apps_updated.emit() def duplicate_project(self, path=None): """ Copy selected project directory into `path`. """ source_path = self.list_projects.currentItem().path if path is None: path = getexistingdirectory(caption="Select folder destination " "for project duplication", basedir=get_home_dir()) if path: shutil.copy2(source_path, path) # --- Callbacks # ------------------------------------------------------------------------- def _project_created(self, worker, output, error): """ Callback after worker has finished creating a new project. """ self._update_project_config(path=worker.path) self.update_visibility(disabled=False) # Select the project after created for i in range(self.list_projects.count()): item = self.list_projects.item(i) if worker.path == item.path: self.list_projects.setCurrentRow(i) self.list_projects.itemClicked.emit(item) update_pointer() def _project_added(self, worker, output, error): """ Callback after worker has finished adding project. """ self._project_created(worker, output, error) def _update_project_config(self, path): """ Add given path to the CONF list of known projects. """ projects_path = list(set(CONF.get('main', 'projects_path', []))) projects_path = list(projects_path) projects_path.append(path) CONF.set('main', 'projects_path', projects_path) self.load_projects() self.update_status('')
def __init__(self, parent=None): super(HomeTab, self).__init__(parent) # Variables self._parent = parent self.api = AnacondaAPI() self.setObjectName('Tab') # Widgetsugh main_icon = QLabel() self.list_applications = ListWidgetApplication() self.apps_model = None self.apps = None self.app_timers = None self.button_refresh = ButtonHomeRefresh('Refresh') self.frame_home = FrameHome(self) self.frame_home_top = FrameHomeTop(self) self.frame_home_bottom = FrameHomeTop(self) self.label_home = LabelHome('') self.label_status = QLabel('') self.timer_refresh = QTimer() # Widget setup pixmap = QPixmap(images.ANACONDA_ICON_128_PATH) main_icon.setPixmap(pixmap) main_text = ('My Applications') self.label_home.setText(main_text) self.list_applications.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) # Layout home_layout_top = QHBoxLayout() home_layout_top.addWidget(self.label_home, 10, Qt.AlignLeft) home_layout_top.addWidget(self.button_refresh, 1, Qt.AlignRight | Qt.AlignBottom) home_layout_top.setSpacing(0) self.frame_home_top.setLayout(home_layout_top) home_layout_bottom = QVBoxLayout() home_layout_bottom.addWidget(self.list_applications) home_layout_bottom.addWidget(self.label_status) home_layout_bottom.setSpacing(0) self.frame_home_bottom.setLayout(home_layout_bottom) home_layout = QVBoxLayout() home_layout.addWidget(self.frame_home_top) home_layout.addWidget(self.frame_home_bottom) home_layout.setSpacing(0) self.frame_home.setLayout(home_layout) layout = QHBoxLayout() layout.addWidget(self.frame_home) layout.setSpacing(0) self.setLayout(layout) # Signals self.list_applications.sig_application_updated.connect( self.sig_application_updated) self.list_applications.sig_application_updated.connect( self.update_list) self.list_applications.sig_status_updated.connect(self.update_status) self.button_refresh.clicked.connect(self.refresh_cards)
class CommunityTab(WidgetBase): sig_video_started = Signal(str, int) sig_status_updated = Signal(object, int, int, int) instances = [] VIDEOS_LIMIT = 20 WEBINARS_LIMIT = 20 EVENTS_LIMIT = 20 def __init__(self, parent=None, tags=None, content_urls=None): super(CommunityTab, self).__init__(parent) self._parent = parent self._downloaded_urls = [] self._downloaded_filepaths = [] self.api = AnacondaAPI() self.content_urls = content_urls self.content_info = [] self.step = 0 self.step_size = 1 self.tags = tags self.timer_load = QTimer() self.pixmaps = {} self.default_pixmap = QPixmap(VIDEO_ICON_PATH).scaled( 100, 60, Qt.KeepAspectRatio, Qt.FastTransformation) # Widgets self.text_filter = LineEditSearch() self.frame_community = FrameCommunity() self.list = ListWidgetContent() # Widget setup self.timer_load.setInterval(333) self.list.setAttribute(Qt.WA_MacShowFocusRect, False) self.text_filter.setPlaceholderText('Search') self.text_filter.setAttribute(Qt.WA_MacShowFocusRect, False) self.setObjectName("Tab") self.list.setMinimumHeight(200) fm = self.text_filter.fontMetrics() self.text_filter.setMaximumWidth(fm.width('M'*23)) # Layouts hlayout = QHBoxLayout() self.filters_layout = QHBoxLayout() layout = QVBoxLayout() layout.addLayout(hlayout) controls_layout = QHBoxLayout() controls_layout.addLayout(self.filters_layout) controls_layout.addStretch() controls_layout.addWidget(self.text_filter) layout.addLayout(controls_layout) layout.addWidget(self.list) self.frame_community.setLayout(layout) layout = QHBoxLayout() layout.addWidget(self.frame_community) self.setLayout(layout) # Signals self.timer_load.timeout.connect(self.set_content_list) self.text_filter.textChanged.connect(self.filter_content) def setup_tab(self): self.download_content() def _json_downloaded(self, worker, output, error): url = worker.url if url in self._downloaded_urls: self._downloaded_urls.remove(url) if not self._downloaded_urls: self.load_content() def download_content(self): self._downloaded_urls = [] self._downloaded_filepaths = [] if self.content_urls: for url in self.content_urls: url = url.lower() # Enforce lowecase... just in case fname = url.split('/')[-1] + '.json' filepath = os.sep.join([CONTENT_PATH, fname]) self._downloaded_urls.append(url) self._downloaded_filepaths.append(filepath) worker = self.api.download_async(url, filepath) worker.url = url worker.sig_finished.connect(self._json_downloaded) else: self.load_content() def load_content(self, paths=None): """ Load downloaded and bundled content. """ content = [] # Load downloaded content for filepath in self._downloaded_filepaths: fname = filepath.split(os.sep)[-1] items = [] if os.path.isfile(filepath): with open(filepath, 'r') as f: data = f.read() try: items = json.loads(data) except Exception as error: logger.error(str((filepath, error))) else: items = [] if 'video' in fname: for item in items: item['tags'] = ['video'] item['uri'] = item['video'] uri = item['uri'].split('watch?v=')[-1] url = 'http://img.youtube.com/vi/{0}/0.jpg'.format(uri) item['banner'] = url item['date'] = item.get('date_start', '') item['image_file_path'] = uri + '.jpg' items = items[:self.VIDEOS_LIMIT] elif 'event' in fname: for item in items: item['tags'] = ['event'] + item.get('categories', '').split(', ') item['uri'] = item['url'] item['image_file_path'] = item['banner'].split('/')[-1] items = items[:self.EVENTS_LIMIT] elif 'webinar' in fname: for item in items: item['tags'] = ['webinar'] item['uri'] = item['url'] item['banner'] = item['image']['src'] item['image_file_path'] = item['banner'].split('/')[-1] items = items[:self.WEBINARS_LIMIT] if items: content.extend(items) # Load bundled content with open(LINKS_INFO_PATH, 'r') as f: data = f.read() items = [] try: items = json.loads(data) except Exception as error: logger.error(str(filepath, error)) content.extend(items) # Add the omage path to get the full path for item in content: filename = item['image_file_path'] item['image_file_path'] = os.path.sep.join([IMAGE_DATA_PATH, filename]) self.content_info = content # Save loaded data in a single file with open(CONTENT_JSON_PATH, 'w') as f: json.dump(content, f) self.make_tag_filters() self.timer_load.start(random.randint(25, 35)) def make_tag_filters(self): if not self.tags: self.tags = set() for content_item in self.content_info: tags = content_item.get('tags', []) for tag in tags: if tag: self.tags.add(tag) logger.debug("TAGS: %s", self.tags) self.filter_widgets = [] for tag in self.tags: item = QCheckBox(tag.capitalize()) item.setObjectName(tag.lower()) item.setChecked(True) item.clicked.connect(self.filter_content) self.filter_widgets.append(item) self.filters_layout.addWidget(item) def filter_content(self, text=None): """ Filter content by a search string on all the fields of the item. Using comma allows the use of several keywords, e.g. Peter,2015 """ text = self.text_filter.text().lower() text = [t for t in re.split('\W', text) if t] sel_tags = [i.text().lower() for i in self.filter_widgets if i.isChecked()] for i in range(self.list.count()): item = self.list.item(i) all_checks = [] for t in text: t = t.strip() checks = (t in item.title.lower() or t in item.venue.lower() or t in ' '.join(item.authors).lower() or t in item.summary.lower()) all_checks.append(checks) all_checks.append(any(tag.lower() in sel_tags for tag in item.tags)) if all(all_checks): item.setHidden(False) else: item.setHidden(True) def set_content_list(self): """ Add items to the list, gradually. Called by a timer. """ for i in range(self.step, self.step + self.step_size): if i < len(self.content_info): item = self.content_info[i] banner = item.get('banner', '') path = item.get('image_file_path', '') content_item = ListItemContent( title=item['title'], subtitle=item.get('subtitle', "") or "", uri=item['uri'], date=item.get('date', '') or "", summary=item.get('summary', '') or "", tags=item.get('tags', []), banner=banner, path=path, pixmap=self.default_pixmap ) self.list.addItem(content_item) content_item.pixmaps = self.pixmaps self.download_thumbnail(content_item, banner, path) else: self.timer_load.stop() break self.step += self.step_size self.filter_content() def download_thumbnail(self, item, url, path): """ Download all the video thumbnails. """ worker = self.api.download_async(url, path) worker.url = url worker.item = item worker.sig_finished.connect(self.convert_image) logger.debug('Fetching thumbnail %s', url) def convert_image(self, worker, output, error): """ Load an image using PIL, and converts it to a QPixmap. This was needed as some image libraries are not found in some OS. """ # Needs to come after qtpy imports path = output if path in self.pixmaps: return if path: if os.path.isfile(path): try: if sys.platform == 'darwin': from PIL.ImageQt import ImageQt from PIL import Image image = Image.open(path) image = ImageQt(image) qt_image = QImage(image) pixmap = QPixmap.fromImage(qt_image) else: extension = path.split('.')[-1].upper() if extension in ['PNG', 'JPEG', 'JPG']: pixmap = QPixmap(path, format=extension) else: pixmap = QPixmap(path) self.pixmaps[path] = pixmap except (IOError, OSError) as error: logger.error(str(error)) def update_style_sheet(self, style_sheet=None): 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)
class PackagesTab(QWidget): """ Packages, environments and commands tab widget. """ sig_status_updated = Signal(object, object, object, object) sig_updated = Signal(object, object) sig_project_commands_updated = Signal(object, object) sig_apps_updated = Signal() def __init__(self): super(PackagesTab, self).__init__() self.api = AnacondaAPI() self.path = None self.project = None # Widgets self.list_environments = EditableList(title='Environments') self.list_commands = EditableList(title='Commands', min_items=0) self.package_manager = CondaPackagesWidget( self, setup=False, data_directory=CHANNELS_PATH, ) # Widget setup self.list_environments.button_add.clicked.disconnect() self.list_environments.button_edit.setVisible(False) # Layout top_layout = QHBoxLayout() top_layout.addWidget(self.list_environments) top_layout.addStretch() top_layout.addWidget(self.list_commands) top_layout.addStretch() main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addWidget(self.package_manager) self.setLayout(main_layout) # Signals self.list_commands.sig_item_removed.connect(self.edit_commands) self.list_commands.sig_item_edited.connect(self.add_command) self.list_commands.sig_item_selected.connect(self.select_command) self.list_environments.button_add.clicked.connect(self.add_environment) self.list_environments.sig_item_removed.connect( self.remove_environment) self.list_environments.sig_item_selected.connect( self.choose_environment) self.package_manager.sig_channels_updated.connect(self.set_channels) def load_information(self, path, project=None): """ """ self.path = path if project is None: project = self.api.load_project(path) logger.debug(str((path, project.name))) self.project = project # Load commands commands = project.commands self.list_commands.clear() for command in commands: item = QListWidgetItem(command) self.list_commands.add(item) self.list_commands.setCurrentRow(0) # Load environments environments = project.environments self.list_environments.clear() selected_row = 0 for i, environment in enumerate(environments): item = QListWidgetItem(environment) self.list_environments.add(item) if environment == project.default_environment: selected_row = i self.list_environments.setCurrentRow(selected_row) # Load channels self.package_manager.set_environment(prefix=project.env_prefix(path)) self.package_manager.update_channels(project.default_channels, project.default_channels) def set_channels(self, channels, active_channels): """ """ logger.debug(channels) self.project.default_channels = list(active_channels) self.project.save(self.path) # --- Environments # ------------------------------------------------------------------------- def add_environment(self, text=None, pyver=None): """ Create new basic environment with selectable python version. Actually makes new env on disc, in directory within the project whose name depends on the env name. New project state is saved. Should also sync to spec file. """ dlg = CreateEnvironmentDialog(parent=self, envs=self.project.environments) if dlg.exec_(): text = dlg.text_name.text().strip() pyver = dlg.combo_version.currentText() if text: self.setEnabled(False) QApplication.setOverrideCursor(Qt.BusyCursor) # FIXME: This does not seem like a very safe way. What if it fails? self.project.default_environment = text self.project.environments[text] = "{0}.yaml".format(text) prefix = self.project.env_prefix(self.path) if pyver: pkgs = ['python=' + pyver, 'jupyter'] else: pkgs = ['jupyter'] channels = self.project.default_channels logger.debug(str((prefix, pkgs, channels))) worker = self.api.conda_create(prefix=prefix, pkgs=pkgs, channels=channels) worker.text = text worker.sig_finished.connect(self._environment_added) def _environment_added(self, worker, output, error): """ Callback for worker after envrionemnt creation has finished. """ text = worker.text item = QListWidgetItem(text) self.list_environments.add(item) self.list_environments.setCurrentRow( len(self.project.environments) - 1) self.setEnabled(True) self.project.save(self.path) self.choose_environment() QApplication.restoreOverrideCursor() def remove_environment(self, item): """ Delete environment from project and project directory. """ text = item.text() self.project.default_environment = text path = self.project.env_prefix(self.path) logger.debug(str((path, text))) shutil.rmtree(path) self.project.environments.pop(text) self.project.save(self.path) self.choose_environment() def choose_environment(self): """ The user has picked a different environment. Refresh the package list and save the new state of the project. """ current_item_text = self.list_environments.currentItem().text() logger.debug(current_item_text) self.project.default_environment = current_item_text self.project.save(self.path) prefix = self.project.env_prefix(self.path) self.package_manager.set_environment(prefix=prefix) self.package_manager.setup() # --- Commands # ------------------------------------------------------------------------- def add_command(self, command=None): """ Add a new command in the commands list. Adds to the bottom of the list. """ if command: item = QListWidgetItem(command) self.list_commands.add(item) commands = [ self.list_commands.item(i).text() for i in range(self.list_commands.count()) ] self.project.commands = commands self.project.save(self.path) self.sig_project_commands_updated.emit(self.path, self.project.commands) logger.debug(self.project.commands) def edit_commands(self): """ """ commands = [ self.list_commands.item(i).text() for i in range(self.list_commands.count()) ] self.project.commands = commands self.project.save(self.path) self.sig_project_commands_updated.emit(self.path, self.project.commands) logger.debug(self.project.commands) def select_command(self): """ """ self.sig_apps_updated.emit() logger.debug(self.project.commands)
class HomeTab(WidgetBase): sig_pointer_updated = Signal(object, object) sig_status_updated = Signal(object, object, object, object) sig_application_updated = Signal(object, object) def __init__(self, parent=None): super(HomeTab, self).__init__(parent) # Variables self._parent = parent self.api = AnacondaAPI() self.setObjectName('Tab') # Widgetsugh main_icon = QLabel() self.list_applications = ListWidgetApplication() self.apps_model = None self.apps = None self.app_timers = None self.button_refresh = ButtonHomeRefresh('Refresh') self.frame_home = FrameHome(self) self.frame_home_top = FrameHomeTop(self) self.frame_home_bottom = FrameHomeTop(self) self.label_home = LabelHome('') self.label_status = QLabel('') self.timer_refresh = QTimer() # Widget setup pixmap = QPixmap(images.ANACONDA_ICON_128_PATH) main_icon.setPixmap(pixmap) main_text = ('My Applications') self.label_home.setText(main_text) self.list_applications.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) # Layout home_layout_top = QHBoxLayout() home_layout_top.addWidget(self.label_home, 10, Qt.AlignLeft) home_layout_top.addWidget(self.button_refresh, 1, Qt.AlignRight | Qt.AlignBottom) home_layout_top.setSpacing(0) self.frame_home_top.setLayout(home_layout_top) home_layout_bottom = QVBoxLayout() home_layout_bottom.addWidget(self.list_applications) home_layout_bottom.addWidget(self.label_status) home_layout_bottom.setSpacing(0) self.frame_home_bottom.setLayout(home_layout_bottom) home_layout = QVBoxLayout() home_layout.addWidget(self.frame_home_top) home_layout.addWidget(self.frame_home_bottom) home_layout.setSpacing(0) self.frame_home.setLayout(home_layout) layout = QHBoxLayout() layout.addWidget(self.frame_home) layout.setSpacing(0) self.setLayout(layout) # Signals self.list_applications.sig_application_updated.connect( self.sig_application_updated) self.list_applications.sig_application_updated.connect( self.update_list) self.list_applications.sig_status_updated.connect(self.update_status) self.button_refresh.clicked.connect(self.refresh_cards) def update_list(self, name=None, version=None): self.set_applications() def refresh_cards(self): """ List widget items sometimes are hidden on resize. This method tries to compensate for that refreshing and repainting periodically. """ self.list_applications.update_style_sheet() self.list_applications.repaint() for item in self.list_applications.items(): if not item.widget.isVisible(): item.widget.repaint() def update_status(self, text): self.label_status.setText(text) for item in self.list_applications._items: item.widget.setVisible(True) item.setHidden(False) item.widget.repaint() def setup_tab(self, apps): self.apps_packages = apps self.set_applications() def set_applications(self): """ Build the list of applications present in the current conda environment """ paths = get_projects_path() project_dict = self.api.load_applications(paths) APPS_DESCRIPTIONS = { 'glueviz': 'Multidimensional data visualization across files. ' 'Explore relationships within and among related ' 'datasets.', 'notebook': 'Web-based, interactive computing notebook ' 'environment. Edit and run human-readable docs while ' 'describing the data analysis.', 'orange-app': 'Component based data mining framework. Data ' 'visualization and data analysis for novice and ' 'expert. Interactive workflows with a large ' 'toolbox.', 'qtconsole': 'PyQt GUI that supports inline figures, proper ' 'multiline editing with syntax highlighting, ' 'graphical calltips, and more.', 'spyder': 'Scientific PYthon Development EnviRonment. Powerful ' 'Python IDE with advanced editing, interactive ' 'testing, debugging and introspection features', } # Add applications all_applications = [] project_installed = [] project_not_installed = [] for project_path in project_dict: project = project_dict[project_path] prefix = self.api.ROOT_PREFIX version = self.api.conda_package_version(prefix, pkg=project.name) if version: project_installed.append(project_path) else: project_not_installed.append(project_path) for project_path in project_installed + project_not_installed: project = project_dict[project_path] if project.icon: pixmap = self.api.load_icon(project_path, project, as_pixmap=True) else: pixmap = None if project.commands: command = project.commands[0] else: command = None # Temporal FIX, only install APPS on ROOT # prefix = project.env_prefix(project_path) prefix = self.api.ROOT_PREFIX name = project.name if project.is_conda_app: version = self.api.conda_package_version(prefix, pkg=name) else: version = project.version description = APPS_DESCRIPTIONS.get(name, '') item = ListItemApplication(name=name, description=description, path=project_path, dev_tool=project.dev_tool, pixmap=pixmap, version=version, versions=None, command=command, prefix=prefix, is_conda_app=project.is_conda_app) all_applications.append(item) self.list_applications.clear() for i in all_applications: self.list_applications.addItem(i) empty = ListItemEmpty() self.list_applications.addItem(empty) self.update_versions(self.apps_packages) self.timer_refresh.start() def update_versions(self, apps=None): self.items = [] for i in range(self.list_applications.count()): item = self.list_applications.item(i) self.items.append(item) if isinstance(item, ListItemApplication): name = item.name meta = apps.get(name, None) if meta: versions = meta['versions'] version = self.api.get_dev_tool_version(item.path) item.update_versions(version, versions) def update_style_sheet(self, style_sheet=None): if style_sheet is None: style_sheet = load_style_sheet() self.list_applications.update_style_sheet(style_sheet=style_sheet) self.setStyleSheet(style_sheet)
def __init__(self, parent=None): super(ProjectWidget, self).__init__(parent) # Variables self.api = AnacondaAPI() self._parent = parent self.path = None self.project = None self.timer = QTimer() self.timeout = 6000 # Widgets self.text_name = EditableLineEdit('Name', '') self.text_location = EditablePathEdit('Location', '', caption="Select new project " "directory", basedir=get_home_dir()) self.text_icon = EditablePathEdit( 'Icon', '', fileselect=True, caption='Select icon image file', filters="Image files (*.png *.jpg *.jpeg)", basedir=get_home_dir()) self.button_launch = QPushButton('Launch') self.button_upload = QPushButton('Upload to Anaconda Cloud') self.tabs = QTabWidget(parent=self) self.tab_packages = PackagesTab() # self.tab_advanced = AdvancedTab() self.tab_explorer = ExplorerTab() # Widget Setup self.setWindowTitle('Project Editor') self.button_upload.setObjectName('ButtonUpdate') self.button_launch.setObjectName('ButtonUpdate') self.tabs.addTab(self.tab_explorer, 'Files') self.tabs.addTab(self.tab_packages, 'Packages') # self.tabs.addTab(self.tab_advanced, 'Advanced options') self.text_icon._text.setVisible(False) # Layouts project_buttons = QHBoxLayout() project_buttons.addWidget(self.button_upload) # project_buttons.addWidget(self.button_launch) project_buttons.addStretch() information_layout = QHBoxLayout() information_layout.addWidget(self.text_name, 2) information_layout.addWidget(self.text_location, 4) information_layout.addWidget(self.text_icon, 1) main_layout = QVBoxLayout() # main_layout.addLayout(project_buttons, 1) # main_layout.addSpacing(6) main_layout.addLayout(information_layout, 1) main_layout.addWidget(self.tabs, 10) main_layout.addWidget(self.button_launch, 0, Qt.AlignRight) self.setLayout(main_layout) # Signals self.button_launch.clicked.connect(self.launch) self.tab_explorer.explorer.sig_home_clicked.connect( self.set_explorer_path) self.tab_explorer.treewidget.sig_edit.connect(self.edit_file) self.tab_explorer.treewidget.sig_open_py.connect(self.run_python_file) self.tab_explorer.treewidget.sig_open_py_con.connect( self.run_python_console) self.tab_explorer.treewidget.sig_open_notebook.connect( self.run_notebook) self.tab_explorer.treewidget.sig_add_endpoint.connect( self.make_command_from_file) self.tab_packages.sig_project_commands_updated.connect( self.sig_project_commands_updated) self.tab_packages.sig_apps_updated.connect(self.sig_apps_updated) self.timer.timeout.connect(self._launched) self.text_location.sig_text_changed.connect(self.update_location) self.text_name.sig_text_changed.connect(self.update_name) self.text_icon.sig_text_changed.connect(self.update_icon) # Setup self.button_upload.setDisabled(True) self.button_upload.setVisible(False)
class ProjectWidget(QWidget): """ Project information widget. """ sig_apps_updated = Signal() sig_project_updated = Signal() sig_project_commands_updated = Signal(object, object) sig_project_name_updated = Signal(object, object, object) sig_project_icon_updated = Signal(object, object, object) sig_status_updated = Signal(object, object, object, object) def __init__(self, parent=None): super(ProjectWidget, self).__init__(parent) # Variables self.api = AnacondaAPI() self._parent = parent self.path = None self.project = None self.timer = QTimer() self.timeout = 6000 # Widgets self.text_name = EditableLineEdit('Name', '') self.text_location = EditablePathEdit('Location', '', caption="Select new project " "directory", basedir=get_home_dir()) self.text_icon = EditablePathEdit( 'Icon', '', fileselect=True, caption='Select icon image file', filters="Image files (*.png *.jpg *.jpeg)", basedir=get_home_dir()) self.button_launch = QPushButton('Launch') self.button_upload = QPushButton('Upload to Anaconda Cloud') self.tabs = QTabWidget(parent=self) self.tab_packages = PackagesTab() # self.tab_advanced = AdvancedTab() self.tab_explorer = ExplorerTab() # Widget Setup self.setWindowTitle('Project Editor') self.button_upload.setObjectName('ButtonUpdate') self.button_launch.setObjectName('ButtonUpdate') self.tabs.addTab(self.tab_explorer, 'Files') self.tabs.addTab(self.tab_packages, 'Packages') # self.tabs.addTab(self.tab_advanced, 'Advanced options') self.text_icon._text.setVisible(False) # Layouts project_buttons = QHBoxLayout() project_buttons.addWidget(self.button_upload) # project_buttons.addWidget(self.button_launch) project_buttons.addStretch() information_layout = QHBoxLayout() information_layout.addWidget(self.text_name, 2) information_layout.addWidget(self.text_location, 4) information_layout.addWidget(self.text_icon, 1) main_layout = QVBoxLayout() # main_layout.addLayout(project_buttons, 1) # main_layout.addSpacing(6) main_layout.addLayout(information_layout, 1) main_layout.addWidget(self.tabs, 10) main_layout.addWidget(self.button_launch, 0, Qt.AlignRight) self.setLayout(main_layout) # Signals self.button_launch.clicked.connect(self.launch) self.tab_explorer.explorer.sig_home_clicked.connect( self.set_explorer_path) self.tab_explorer.treewidget.sig_edit.connect(self.edit_file) self.tab_explorer.treewidget.sig_open_py.connect(self.run_python_file) self.tab_explorer.treewidget.sig_open_py_con.connect( self.run_python_console) self.tab_explorer.treewidget.sig_open_notebook.connect( self.run_notebook) self.tab_explorer.treewidget.sig_add_endpoint.connect( self.make_command_from_file) self.tab_packages.sig_project_commands_updated.connect( self.sig_project_commands_updated) self.tab_packages.sig_apps_updated.connect(self.sig_apps_updated) self.timer.timeout.connect(self._launched) self.text_location.sig_text_changed.connect(self.update_location) self.text_name.sig_text_changed.connect(self.update_name) self.text_icon.sig_text_changed.connect(self.update_icon) # Setup self.button_upload.setDisabled(True) self.button_upload.setVisible(False) # --- Global methods # ------------------------------------------------------------------------- def project_updated(self, environments, commands): """ """ self.sig_project_updated.emit() def update_status(self, message='', timeout=0): """ Update project tab status. """ self.sig_status_updated.emit(message, timeout, -1, -1) def load_project_information(self, path, project=None): """ Load project information on project widget. """ if project is None: project = self.api.load_project(path) self.path = path self.project = project self.text_name.setText(project.name) self.text_location.setText(path) self.set_explorer_path(path) self.tab_packages.load_information(path) self.text_location.button_edit.setVisible(False) # FIXME: Temporal! if self.is_default(): self.button_launch.setDisabled(True) self.text_name.button_edit.setDisabled(True) self.text_icon.button_edit.setDisabled(True) self.text_location.button_edit.setDisabled(True) self.tab_packages.list_commands.setVisible(False) self.tab_packages.list_environments.setVisible(False) else: if self.tab_packages.list_commands.count(): self.button_launch.setDisabled(False) else: self.button_launch.setDisabled(True) self.text_icon.button_edit.setDisabled(False) self.text_name.button_edit.setDisabled(False) self.text_location.button_edit.setDisabled(True) self.tab_packages.list_commands.setVisible(True) self.tab_packages.list_environments.setVisible(True) def update_name(self, old_name, new_name): """ Update project name. """ if self.project is not None: self.project.name = new_name logger.debug(new_name) self.project.save(self.path) self.sig_project_name_updated.emit(self.path, old_name, new_name) def update_location(self, old_path, new_path): """ FIXME: To be implemented """ path = getexistingdirectory(caption="New location", basedir=self.path) logger.debug(path) if path: try: if os.path.exists(path): os.rmdir(path) shutil.move(self.path, path) projects_path = CONF.get('main', 'projects_path', []) projects_path.remove(self.path) projects_path.append(path) CONF.set('main', 'projects_path', projects_path) self.project.path = path self.path = path self.sig_project_updated.emit() except Exception as e: print(e) def update_icon(self, old_image_path, new_image_path): """ Update project icon/image. """ self.project.icon = 'icon.png' logger.debug(new_image_path) self.project.save(self.path) self.sig_project_icon_updated.emit(self.path, old_image_path, new_image_path) def launch(self): """ Execute the selected command in the current project. """ list_commands = self.tab_packages.list_commands if list_commands.count() != 0: self.button_launch.setDisabled(True) self.timer.setInterval(self.timeout) self.timer.start() update_pointer(Qt.BusyCursor) command = list_commands.currentItem().text() logger.debug(str((self.path, command))) launch(self.path, command) def _launched(self): """ """ self.button_launch.setDisabled(False) update_pointer() def select_icon(self): """ """ out = getopenfilename( caption='Select icon', filters='PNG Image (*.png)', ) logger.debug(str(out)) if out[0]: with open(out[0], 'rb') as f: image = f.read() self.project.icon = base64.b64encode(image) self.project.save(self.path) self.load_icon() def is_default(self): """ Check if current project is the default project. """ return (self.text_name.text() == 'default' and self.path == DEFAULT_PROJECT_PATH) # --- Explorer Commands # ------------------------------------------------------------------------- def set_explorer_path(self, path=None): """ """ path = path or self.path self.tab_explorer.treewidget.set_current_folder(path) def edit_file(self, filename=""): """ Open file with spyder. """ logger.debug(filename) subprocess.Popen(['spyder', filename]) def run_notebook(self, filename=""): """ Start notebook server. """ logger.debug(filename) run_notebook(self.path, self.project, filename) def run_python_file(self, filename=""): """ Execute python in environment. """ logger.debug(filename) run_python_file(self.path, self.project, filename) def run_python_console(self, filename=""): """ Execute python in env with console. """ logger.debug(filename) run_python_console(self.path, self.project, filename) def make_command_from_file(self, filename): """ Add a command in the commands list based on the current file. """ logger.debug(filename) bin_path = os.sep.join([self.project.env_prefix(self.path), 'bin']) if filename.endswith('.py'): cmd = 'python ' + filename.replace(self.path, '{PREFIX}') elif filename.endswith('.ipynb'): cmd = 'jupyter notebook ' + filename.replace(self.path, '{PREFIX}') elif (os.path.isfile(filename) and os.access(filename, os.X_OK) and bin_path in filename): new_bin_path = os.sep.join( [self.project.env_prefix(self.path), 'bin']) cmd = filename.replace(new_bin_path, '') else: return self.add_command(text=cmd) def upload_notebook(self, filepath): """ """ pass # --- Commands # ------------------------------------------------------------------------- def add_command(self, text): """ New endpoint: go into edit mode. """ self.tab_packages.add_command(text) logger.debug(text)
class PreferencesDialog(DialogBase): sig_urls_updated = Signal(str, str) sig_show_application_environments = Signal(bool) def __init__(self, *args, **kwargs): super(PreferencesDialog, self).__init__(*args, **kwargs) self.api = AnacondaAPI() self.widgets_changed = set() self.widgets = [] # Widgets self.button_ok = QPushButton('Ok') self.button_cancel = ButtonCancel('Cancel') self.button_reset = QPushButton('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_ok) buttons_layout.addWidget(self.button_cancel) main_layout = QVBoxLayout() main_layout.addLayout(self.grid_layout) main_layout.addSpacing(40) 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): return CONF.get('main', option, None) def set_option(self, option, value, default=False): CONF.set('main', option, value) def set_option_default(self, option): default = CONF.get_default('main', option) self.set_option(option, default) def create_widget(self, widget=None, label=None, option=None, hint=None, check=None): if hint: widget.setPlaceholderText(hint) config_value = self.get_option(option) 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) 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.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, regex=None, check=None): widget = QLineEdit() widget.setAttribute(Qt.WA_MacShowFocusRect, False) widget.setMinimumWidth(250) if regex: regex_validator = QRegExpValidator(QRegExp(regex)) widget.setValidator(regex_validator) 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, hint=None): 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)) widget.stateChanged.connect( lambda v=None, w=widget: self.options_changed(widget=w)) self.create_widget(widget=widget, option=option, label=label, hint=hint) def options_changed(self, value=None, widget=None): 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))) # --- API # ------------------------------------------------------------------------- def setup(self): self.create_textbox('Anaconda API domain', 'anaconda_api_url', check=self.is_valid_api) self.create_textbox('Conda domain', 'conda_url', check=self.is_valid_url) self.create_checkbox('Provide analytics', 'provide_analytics') self.create_checkbox("Hide quit dialog", 'hide_quit_dialog') # self.create_checkbox('Show application<br>environments', # 'show_application_environments') def warn(self, widget, text=None): """ """ label = widget.label_information if text: pixmap = QPixmap(WARNING_ICON) label.setPixmap( pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)) label.setToolTip(str(text)) else: label.setPixmap(QPixmap()) label.setToolTip('') # --- Checkers # ------------------------------------------------------------------------- def is_valid_url(self, url): """ Chek 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 url' return output, error def is_valid_api(self, url): """ Chek if a given URL is a valid anaconda api endpoint. """ output = self.api.download_is_valid_api_url(url, non_blocking=False) error = '' if not output: error = 'Invalid Anaconda API url.' return output, error def reset_to_defaults(self): """ """ # ASK! for widget in self.widgets: option = widget.option self.set_option_default(option) value = self.get_option(option) widget.set_value(value) def accept(self): """ Qt override. """ sig_updated = False for widget in self.widgets_changed: option = widget.option value = widget.get_value() check, error = widget.check_value(value) if check: self.set_option(option, value) self.warn(widget) else: self.button_ok.setDisabled(True) widget.set_warning() self.warn(widget, error) return if widget.option == 'conda_url': sig_updated = True if widget.option == 'anaconda_api_url': sig_updated = True if widget.option == 'show_application_environments': self.sig_show_application_environments.emit(value) for widget in self.widgets: if widget.option == 'anaconda_api_url': anaconda_api_url = widget.get_value() elif widget.option == 'conda_url': conda_url = widget.get_value() if sig_updated: self.sig_urls_updated.emit(anaconda_api_url, conda_url) super(PreferencesDialog, self).accept()
def __init__(self, name=None, description=None, command=None, pixmap=None, version=None, versions=None, path=None, dev_tool=True, prefix=None, is_conda_app=False, packages_widget=None): super(ListItemApplication, self).__init__() self.api = AnacondaAPI() self.command = command self.dev_tool = dev_tool self.installed = False self.is_conda_app = is_conda_app self.name = name self.path = path self.pixmap = pixmap if pixmap else QPixmap(ANACONDA_ICON_64_PATH) self.prefix = prefix self.timeout = 10000 # In miliseconds self.version = version self.versions = versions self.packages_widget = packages_widget # Widgets self.button_install = ButtonApplicationInstall("Install") self.button_launch = ButtonApplicationLaunch("Launch") self.button_options = ButtonApplicationOptions() self.label_icon = LabelApplicationIcon() self.label_name = LabelApplicationName(name) self.label_description = LabelApplicationDescription(description) # self.label_update = LabelApplicationUpdate() self.button_version = ButtonApplicationVersion(to_text_string(version)) self.label_spinner = LabelApplicationSpinner() # self.label_version = LabelApplicationVersion(to_text_string(version)) self.menu_options = QMenu('Application options') self.menu_versions = QMenu('Install specific version') self.movie_spinner = QMovie(SPINNER_WHITE_16_PATH) self.timer = QTimer() self.widget = WidgetApplication() # Widget setup self.button_version.setFocusPolicy(Qt.NoFocus) self.label_name.setToolTip(description) self.label_description.setAlignment(Qt.AlignCenter) self.movie_spinner.start() self.timer.setInterval(self.timeout) self.timer.setSingleShot(True) self.label_icon.setToolTip(description) self.label_icon.setPixmap( self.pixmap.scaled(self.ICON_SIZE, self.ICON_SIZE, Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.label_icon.setAlignment(Qt.AlignCenter) self.label_name.setAlignment(Qt.AlignCenter) self.label_name.setWordWrap(True) self.label_description.setWordWrap(True) self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter) self.label_spinner.setVisible(False) self.label_spinner.setMinimumWidth(16) self.label_spinner.setMinimumHeight(16) # Layouts layout = QVBoxLayout() layout.addWidget(self.button_options, 0, Qt.AlignRight) layout.addWidget(self.label_icon, 0, Qt.AlignCenter) layout.addWidget(self.label_name, 0, Qt.AlignCenter) layout.addWidget(self.label_description, 0, Qt.AlignCenter) # hlayout = QHBoxLayout() # hlayout.addWidget(self.label_update) # hlayout.addWidget(self.label_version) # layout.addLayout(hlayout) # layout.addWidget(self.label_version, 0, Qt.AlignCenter) layout.addWidget(self.button_version, 0, Qt.AlignCenter) layout.addWidget(self.label_spinner, 0, Qt.AlignCenter) layout.addWidget(self.button_launch, 0, Qt.AlignCenter) layout.addWidget(self.button_install, 0, Qt.AlignCenter) self.widget.setLayout(layout) self.widget.setStyleSheet(load_style_sheet()) self.setSizeHint(self.widget.sizeHint()) # Signals self.button_install.clicked.connect(self.install_application) self.button_launch.clicked.connect(self.launch_application) self.button_options.clicked.connect(self.actions_menu_requested) self.timer.timeout.connect(self._application_launched) # Setup self.update_status()
class ListItemApplication(QListWidgetItem): """ Item with custom widget for the applications list. """ ICON_SIZE = 48 def __init__(self, name=None, description=None, command=None, pixmap=None, version=None, versions=None, path=None, dev_tool=True, prefix=None, is_conda_app=False, packages_widget=None): super(ListItemApplication, self).__init__() self.api = AnacondaAPI() self.command = command self.dev_tool = dev_tool self.installed = False self.is_conda_app = is_conda_app self.name = name self.path = path self.pixmap = pixmap if pixmap else QPixmap(ANACONDA_ICON_64_PATH) self.prefix = prefix self.timeout = 10000 # In miliseconds self.version = version self.versions = versions self.packages_widget = packages_widget # Widgets self.button_install = ButtonApplicationInstall("Install") self.button_launch = ButtonApplicationLaunch("Launch") self.button_options = ButtonApplicationOptions() self.label_icon = LabelApplicationIcon() self.label_name = LabelApplicationName(name) self.label_description = LabelApplicationDescription(description) # self.label_update = LabelApplicationUpdate() self.button_version = ButtonApplicationVersion(to_text_string(version)) self.label_spinner = LabelApplicationSpinner() # self.label_version = LabelApplicationVersion(to_text_string(version)) self.menu_options = QMenu('Application options') self.menu_versions = QMenu('Install specific version') self.movie_spinner = QMovie(SPINNER_WHITE_16_PATH) self.timer = QTimer() self.widget = WidgetApplication() # Widget setup self.button_version.setFocusPolicy(Qt.NoFocus) self.label_name.setToolTip(description) self.label_description.setAlignment(Qt.AlignCenter) self.movie_spinner.start() self.timer.setInterval(self.timeout) self.timer.setSingleShot(True) self.label_icon.setToolTip(description) self.label_icon.setPixmap( self.pixmap.scaled(self.ICON_SIZE, self.ICON_SIZE, Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.label_icon.setAlignment(Qt.AlignCenter) self.label_name.setAlignment(Qt.AlignCenter) self.label_name.setWordWrap(True) self.label_description.setWordWrap(True) self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter) self.label_spinner.setVisible(False) self.label_spinner.setMinimumWidth(16) self.label_spinner.setMinimumHeight(16) # Layouts layout = QVBoxLayout() layout.addWidget(self.button_options, 0, Qt.AlignRight) layout.addWidget(self.label_icon, 0, Qt.AlignCenter) layout.addWidget(self.label_name, 0, Qt.AlignCenter) layout.addWidget(self.label_description, 0, Qt.AlignCenter) # hlayout = QHBoxLayout() # hlayout.addWidget(self.label_update) # hlayout.addWidget(self.label_version) # layout.addLayout(hlayout) # layout.addWidget(self.label_version, 0, Qt.AlignCenter) layout.addWidget(self.button_version, 0, Qt.AlignCenter) layout.addWidget(self.label_spinner, 0, Qt.AlignCenter) layout.addWidget(self.button_launch, 0, Qt.AlignCenter) layout.addWidget(self.button_install, 0, Qt.AlignCenter) self.widget.setLayout(layout) self.widget.setStyleSheet(load_style_sheet()) self.setSizeHint(self.widget.sizeHint()) # Signals self.button_install.clicked.connect(self.install_application) self.button_launch.clicked.connect(self.launch_application) self.button_options.clicked.connect(self.actions_menu_requested) self.timer.timeout.connect(self._application_launched) # Setup self.update_status() # --- Callbacks # ------------------------------------------------------------------------- def _application_launched(self): """ """ self.button_launch.setDisabled(False) update_pointer() def _application_installed(self, worker, output, error): """ """ self.handle_action_finished(worker, output, error) def _application_updated(self, worker, output, error): """ """ self.handle_action_finished(worker, output, error) def _application_removed(self, worker, output, error): """ """ self.handle_action_finished(worker, output, error) # --- Helpers # ------------------------------------------------------------------------- def _partial_output_ready(self, worker, output, error): """ """ message = None progress = (0, 0) if isinstance(output, dict): progress = (output.get('progress', None), output.get('maxval', None)) name = output.get('name', None) fetch = output.get('fetch', None) if fetch: message = "Downloading <b>{0}</b>...".format(fetch) if name: self._current_action_name = name message = "Linking <b>{0}</b>...".format(name) logger.debug(message) self.widget.sig_status_updated.emit(message) def update_style_sheet(self, style_sheet=None): if style_sheet is None: style_sheet = load_style_sheet() self.menu_options.setStyleSheet(style_sheet) self.menu_versions.setStyleSheet(style_sheet) def actions_menu_requested(self): """ Create and display options menu for the currently selected application. """ self.menu_options.clear() self.menu_versions.clear() # Add versions menu versions = self.versions if self.versions else [] version_actions = [] for version in reversed(versions): action = create_action(self.widget, version, triggered=lambda value, version=version: self.install_application(version=version)) action.setCheckable(True) if self.version == version: action.setChecked(True) action.setDisabled(True) version_actions.append(action) update_action = create_action( self.widget, 'Update application', triggered=lambda: self.update_application()) if versions and versions[-1] == self.version: update_action.setDisabled(True) else: update_action.setDisabled(False) remove_action = create_action( self.widget, 'Remove application', triggered=lambda: self.remove_application()) remove_action.setEnabled(self.installed) actions = [update_action, remove_action, None, self.menu_versions] add_actions(self.menu_options, actions) add_actions(self.menu_versions, version_actions) offset = QPoint(self.button_options.width(), 0) position = self.button_options.mapToGlobal(QPoint(0, 0)) self.menu_versions.setEnabled(bool(versions)) self.menu_options.move(position + offset) self.menu_options.exec_() def update_status(self): if self.prefix: self.version = self.api.conda_package_version(self.prefix, pkg=self.name) self.installed = bool(self.version) if (self.versions and self.version != self.versions[-1] and self.installed): self.button_version.setIcon(QIcon(CONDA_MANAGER_UPGRADE_ARROW)) self.button_version.setStyleSheet( "ButtonApplicationVersion {color: #0071a0;}") self.button_version.setToolTip('Version {0} available'.format( self.versions[-1])) else: self.button_version.setIcon(QIcon()) self.button_version.setStyleSheet( "ButtonApplicationVersion {color: black;}") self.button_install.setVisible(not self.installed) self.button_launch.setVisible(self.installed) def set_loading(self, value): self.button_launch.setDisabled(value) self.button_install.setDisabled(value) self.button_options.setDisabled(value) if value: self.label_spinner.setMovie(self.movie_spinner) else: self.label_spinner.setMovie(None) if self.version is None: version = self.versions[-1] else: version = self.version self.button_version.setText(version) self.label_spinner.setVisible(value) self.button_version.setVisible(not value) def handle_action_finished(self, worker, output, error): if not isinstance(output, dict): output = {} success = output.get('success', True) if error or not success: # Error might be harmless if no decoding was possible... # Success deserves some sort of messagebox logger.error(error) self.widget.sig_application_updated.emit(self.name, self.version) self.update_status() self.set_loading(False) def update_versions(self, version=None, versions=None): """ Update button visibility depending on update availability. """ update = versions[-1] != version logger.debug(str((self.name, self.dev_tool, self.installed))) if self.installed and version: self.button_options.setVisible(True) self.button_version.setText(version) self.button_version.setVisible(True) elif not self.installed and versions: self.button_install.setEnabled(True) self.button_version.setText(versions[-1]) self.button_version.setVisible(True) self.versions = versions self.version = version self.update_status() # --- Public API # ------------------------------------------------------------------------ def install_application(self, value=None, version=None): """ Update the application on the defined prefix environment. This is used for both normal install and specific version install. """ if version: self.version = version else: self.version = self.versions[-1] version = self.versions[-1] pkg = '{0}={1}'.format(self.name, version) pkgs = [pkg] logger.debug(str((pkg, self.dev_tool))) # Check if environment exists and then create or install # is_installed = self.api.conda_package_version(prefix=self.prefix, # pkg=self.name) # pkgs = [pkg] + self.BASIC_PACKAGES # if is_installed: # worker = self.api.conda_install(prefix=self.prefix, pkgs=pkgs) # else: # worker = self.api.conda_create(prefix=self.prefix, pkgs=pkgs) worker = self.api.conda_install(prefix=self.prefix, pkgs=pkgs) worker.sig_finished.connect(self._application_installed) worker.sig_partial.connect(self._partial_output_ready) self.set_loading(True) self.widget.sig_status_updated.emit('Installing application ' '<b>{0}</b>'.format(self.name)) def launch_application(self): """ Launch application installed in prefix environment. """ if self.command is not None: if self.command.startswith('open'): command = self.command.replace("${PREFIX}", self.prefix) elif self.prefix: command = os.sep.join([self.prefix, 'bin', self.command]) else: command = self.command self.button_launch.setDisabled(True) self.timer.setInterval(self.timeout) self.timer.start() update_pointer(Qt.BusyCursor) launch(self.path, command) def remove_application(self): """ Remove the application from the defined prefix environment. """ pkg = self.name pkgs = [pkg] logger.debug(str((self.name, self.dev_tool))) worker = self.api.conda_remove(prefix=self.prefix, pkgs=pkgs) worker.sig_finished.connect(self._application_removed) worker.sig_partial.connect(self._partial_output_ready) self.set_loading(True) self.widget.sig_status_updated.emit('Removing application ' '<b>{0}</b>'.format(self.name)) def update_application(self): """ Update the application on the defined prefix environment. """ logger.debug(str((self.name, self.dev_tool, self.installed))) self.install_application(version=self.versions[-1]) self.widget.sig_status_updated.emit('Updating application ' '<b>{0}</b>'.format(self.name))