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 CommunityTab(WidgetBase): """Community tab.""" # Qt Signals sig_video_started = Signal(str, int) sig_status_updated = Signal(object, int, int, int) sig_ready = Signal(object) # Sender # Class variables instances = [] # Maximum item count for different content type VIDEOS_LIMIT = 25 WEBINARS_LIMIT = 25 EVENTS_LIMIT = 25 # Google analytics campaigns UTM_MEDIUM = 'in-app' UTM_SOURCE = 'navigator' def __init__(self, parent=None, tags=None, content_urls=None, content_path=CONTENT_PATH, image_path=IMAGE_DATA_PATH, config=CONF, bundle_path=LINKS_INFO_PATH, saved_content_path=CONTENT_JSON_PATH, tab_name=''): """Community tab.""" super(CommunityTab, self).__init__(parent=parent) self._tab_name = '' self.content_path = content_path self.image_path = image_path self.bundle_path = bundle_path self.saved_content_path = saved_content_path self.config = config self._parent = parent self._downloaded_thumbnail_urls = [] 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.filter_widgets = [] self.default_pixmap = QPixmap(VIDEO_ICON_PATH).scaled( 100, 60, Qt.KeepAspectRatio, Qt.FastTransformation) # Widgets self.text_filter = LineEditSearch() self.list = ListWidgetContent() self.frame_header = FrameTabHeader() self.frame_content = FrameTabContent() # 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 self.filters_layout = QHBoxLayout() layout_header = QHBoxLayout() layout_header.addLayout(self.filters_layout) layout_header.addStretch() layout_header.addWidget(self.text_filter) self.frame_header.setLayout(layout_header) layout_content = QHBoxLayout() layout_content.addWidget(self.list) self.frame_content.setLayout(layout_content) layout = QVBoxLayout() layout.addWidget(self.frame_header) layout.addWidget(self.frame_content) self.setLayout(layout) # Signals self.timer_load.timeout.connect(self.set_content_list) self.text_filter.textChanged.connect(self.filter_content) def setup(self): """Setup tab content.""" self.download_content() def _json_downloaded(self, worker, output, error): """Callbacl for download_content.""" 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): """Download content to display in cards.""" 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([self.content_path, fname]) self._downloaded_urls.append(url) self._downloaded_filepaths.append(filepath) worker = self.api.download(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: try: item['tags'] = ['video'] item['uri'] = item.get('video', '') if item['uri']: item['banner'] = item.get('thumbnail') image_path = item['banner'].split('/')[-1] item['image_file'] = image_path else: url = '' item['image_file'] = '' item['banner'] = url item['date'] = item.get('date_start', '') except Exception: logger.debug("Video parse failed: {0}".format(item)) items = items[:self.VIDEOS_LIMIT] elif 'event' in fname: for item in items: try: item['tags'] = ['event'] item['uri'] = item.get('url', '') if item['banner']: image_path = item['banner'].split('/')[-1] item['image_file'] = image_path else: item['banner'] = '' except Exception: logger.debug('Event parse failed: {0}'.format(item)) items = items[:self.EVENTS_LIMIT] elif 'webinar' in fname: for item in items: try: item['tags'] = ['webinar'] uri = item.get('url', '') utm_campaign = item.get('utm_campaign', '') item['uri'] = self.add_campaign(uri, utm_campaign) image = item.get('image', '') if image and isinstance(image, dict): item['banner'] = image.get('src', '') if item['banner']: image_path = item['banner'].split('/')[-1] item['image_file'] = image_path else: item['image_file'] = '' else: item['banner'] = '' item['image_file_path'] = '' except Exception: logger.debug('Webinar parse failed: {0}'.format(item)) items = items[:self.WEBINARS_LIMIT] if items: content.extend(items) # Load bundled content with open(self.bundle_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 image path to get the full path for i, item in enumerate(content): uri = item['uri'] item['uri'] = uri.replace(' ', '%20') filename = item.get('image_file', '') item['image_file_path'] = os.path.sep.join( [self.image_path, filename]) # if 'video' in item['tags']: # print(i, item['uri']) # print(item['banner']) # print(item['image_file_path']) # print('') # Make sure items of the same type/tag are contiguous in the list content = sorted(content, key=lambda i: i.get('tags')) # But also make sure sticky content appears first sticky_content = [] for i, item in enumerate(content[:]): sticky = item.get('sticky') if isinstance(sticky, str): is_sticky = sticky == 'true' elif sticky is None: is_sticky = False # print(i, sticky, is_sticky, item.get('title')) if is_sticky: sticky_content.append(item) content.remove(item) content = sticky_content + content self.content_info = content # Save loaded data in a single file with open(self.saved_content_path, 'w') as f: json.dump(content, f) self.make_tag_filters() self.timer_load.start(random.randint(25, 35)) def add_campaign(self, uri, utm_campaign): """Add tracking analytics campaing to url in content items.""" if uri and utm_campaign: parameters = parse.urlencode({ 'utm_source': self.UTM_SOURCE, 'utm_medium': self.UTM_MEDIUM, 'utm_campaign': utm_campaign }) uri = '{0}?{1}'.format(uri, parameters) return uri def make_tag_filters(self): """Create tag filtering checkboxes based on available content tags.""" 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) # Get count tag_count = {tag: 0 for tag in self.tags} for tag in self.tags: for content_item in self.content_info: item_tags = content_item.get('tags', []) if tag in item_tags: tag_count[tag] += 1 logger.debug("TAGS: {0}".format(self.tags)) self.filter_widgets = [] for tag in sorted(self.tags): count = tag_count[tag] tag_text = "{0} ({1})".format(tag.capitalize(), count).strip() item = ButtonToggle(tag_text) item.setObjectName(tag.lower()) item.setChecked(self.config.get('checkboxes', tag.lower(), True)) item.clicked.connect(self.filter_content) self.filter_widgets.append(item) self.filters_layout.addWidget(item) self.filters_layout.addWidget(SpacerHorizontal()) 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] selected_tags = [] for item in self.filter_widgets: tag_parts = item.text().lower().split() tag = tag_parts[0] # tag_count = tag_parts[-1] if item.isChecked(): selected_tags.append(tag) self.config.set('checkboxes', tag, True) else: self.config.set('checkboxes', tag, False) 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 selected_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) # self.update_style_sheet(self.style_sheet) # This allows the content to look for the pixmap content_item.pixmaps = self.pixmaps # Use images shipped with Navigator, if no image try the # download image_file = item.get('image_file', 'NaN') local_image = os.path.join(LOGO_PATH, image_file) if os.path.isfile(local_image): self.pixmaps[path] = QPixmap(local_image) else: self.download_thumbnail(content_item, banner, path) else: self.timer_load.stop() self.sig_ready.emit(self._tab_name) break self.step += self.step_size self.filter_content() def download_thumbnail(self, item, url, path): """Download all the video thumbnails.""" # Check url is not an empty string or not already downloaded if url and url not in self._downloaded_thumbnail_urls: self._downloaded_thumbnail_urls.append(url) # For some content the app segfaults (with big files) so # we dont use chunks worker = self.api.download(url, path, chunked=True) worker.url = url worker.item = item worker.path = path worker.sig_finished.connect(self.convert_image) logger.debug('Fetching thumbnail {}'.format(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. """ path = output if path in self.pixmaps: return try: if sys.platform == 'darwin' and PYQT4: from PIL.ImageQt import ImageQt from PIL import Image if path: image = Image.open(path) image = ImageQt(image) qt_image = QImage(image) pixmap = QPixmap.fromImage(qt_image) else: pixmap = QPixmap() else: if path and os.path.isfile(path): extension = path.split('.')[-1].upper() if extension in ['PNG', 'JPEG', 'JPG']: pixmap = QPixmap(path, format=extension) else: pixmap = QPixmap(path) else: pixmap = QPixmap() self.pixmaps[path] = pixmap except (IOError, OSError) as error: logger.error(str(error)) 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) def ordered_widgets(self, next_widget=None): """Fix tab order of UI widgets.""" ordered_widgets = [] ordered_widgets += self.filter_widgets ordered_widgets += [self.text_filter] ordered_widgets += self.list.ordered_widgets() return ordered_widgets