class CondaPackagesWidget(QWidget): """Conda Packages Widget.""" sig_ready = Signal() sig_next_focus = Signal() # conda_packages_action_dict, pip_packages_action_dict sig_packages_action_requested = Signal(object, object) # button_widget, sender sig_channels_requested = Signal(object, object) # sender sig_update_index_requested = Signal(object) sig_cancel_requested = Signal(object) 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) # --- Helpers # ------------------------------------------------------------------------- def _handle_tab_focus(self): self.table.setFocus() if self.table.proxy_model: index = self.table.proxy_model.index(0, 0) self.table.setCurrentIndex(index) def _handle_backtab_focus(self): self.table.setFocus() if self.table.proxy_model: row = self.table.proxy_model.rowCount() - 1 index = self.table.proxy_model.index(row, 0) self.table.setCurrentIndex(index) # --- Setup # ------------------------------------------------------------------------- def setup(self, packages=None, model_data=None, prefix=None): """ Setup packages. Populate the table with `packages` information. Parameters ---------- packages: dict Grouped package information by package name. blacklist: list of str List of conda package names to be excluded from the actual package manager view. """ self.table.setup_model(packages, model_data) combobox_text = self.combobox_filter.currentText() self.combobox_filter.setCurrentText(combobox_text) self.filter_package(combobox_text) self.table.setFocus() self.sig_ready.emit() # --- Other methods # ------------------------------------------------------------------------- def apply_multiple_actions(self): """Apply multiple actions on packages.""" logger.debug('') actions = self.table.get_actions() pip_actions = actions[C.PIP_PACKAGE] conda_actions = actions[C.CONDA_PACKAGE] self.sig_packages_action_requested.emit(conda_actions, pip_actions) def clear_actions(self): """Clear the table actions.""" self.table.clear_actions() def filter_package(self, value): """Filter packages by type.""" self.table.filter_status_changed(value) def search_package(self, text): """Search and filter packages by text.""" self.table.search_string_changed(text) def show_channels(self): """Show channel dialog.""" self.sig_channels_requested.emit( self.button_channels, C.TAB_ENVIRONMENT, ) def update_actions(self, number_of_actions): """Update visibility of buttons based on actions.""" self.button_apply.setVisible(bool(number_of_actions)) self.button_clear.setVisible(bool(number_of_actions)) def update_package_index(self): """Update pacakge index.""" self.sig_update_index_requested.emit(C.ENVIRONMENT_PACKAGE_MANAGER) # --- Common methods # ------------------------------------------------------------------------- def ordered_widgets(self): pass def set_widgets_enabled(self, value): """Set the enabled status of widgets and subwidgets.""" self.table.setEnabled(value) self.button_clear.setEnabled(value) self.button_apply.setEnabled(value) self.button_cancel.setEnabled(not value) self.button_cancel.setVisible(not value) def update_status(self, action='', message='', value=None, max_value=None): """ Update status of package widget. - progress == None and max_value == None -> Not Visible - progress == 0 and max_value == 0 -> Busy - progress == n and max_value == m -> Progress values """ 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.setStyleSheet(self.style_sheet)
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)
class ProjectsTab(WidgetBase): """Projects management tab.""" # name, path, sender sig_item_selected = Signal(object, object, object) sig_create_requested = Signal() sig_import_requested = Signal() sig_remove_requested = Signal() sig_upload_requested = Signal() sig_login_requested = Signal() sig_ready = Signal() # 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) # 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() # --- Setup methods # ------------------------------------------------------------------------- def setup(self, projects): """Setup tab content and populates the list of projects.""" self.set_projects(projects=projects) def set_projects(self, projects, current_project=None): """Populate the list of projects.""" self.projects = projects if current_project is None: for (proj_path, proj_name) in projects.items(): current_project = proj_path break self.list.clear() self.current_project = current_project selected_item_row = 0 for i, (proj_path, proj_name) in enumerate(projects.items()): item = ListItemEnv(prefix=proj_path, name=proj_name) if proj_path == self.current_project: selected_item_row = i self.list.addItem(item) loading = False self.list.setCurrentRow(selected_item_row, loading=loading) self.set_project_widget(self.current_project) self.filter_list() def set_project_widget(self, project_path): """Set the project widget.""" if project_path is None: # Disabled widget pass else: self.widget.load_project(project_path) self.refresh() self.sig_ready.emit() def before_delete(self): """Prerpare widget before delete.""" self.widget.before_delete() def update_brand(self, brand): """Update service brand.""" self.widget.update_brand(brand) # --- 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.""" prefix = item.prefix() self.current_project = prefix self.set_loading(prefix) self.sig_item_selected.emit(item.name, prefix, C.TAB_PROJECTS) 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.""" ordered_widgets = [self.text_search] ordered_widgets += self.list.ordered_widgets() ordered_widgets += [ self.button_create, self.button_import, self.button_remove ] ordered_widgets += self.widget.ordered_widgets() return ordered_widgets def refresh(self): """Refresh the enabled/disabled status of the widget and subwidgets.""" projects = self.projects active = bool(projects) if not active: self.widget.clear() self.widget.setVisible(active) self.button_remove.setEnabled(active) self.widget.setEnabled(active) 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_import.setEnabled(value) self.button_remove.setEnabled(value) self.widget_projects.set_widgets_enabled(value) if value: self.refresh() @staticmethod def update_status(action=None, message=None, value=0, max_value=0): """Update status bar.""" # TODO:! 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.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