def _convert_items_to_indices( self, items: Iterable[KnechtItem], proxy_model: QSortFilterProxyModel = None ) -> Iterator[QModelIndex]: for item in items: index = self.model.get_index_from_item(item) proxy_index = None if proxy_model: proxy_index = proxy_model.mapFromSource(index) if proxy_index: yield proxy_index continue if index: yield index
class SearchBarEditor(QTableView): """A Google-like search bar, implemented as a QTableView with a _CustomLineEditDelegate in the first row. """ data_committed = Signal() def __init__(self, parent, tutor=None): """Initializes instance. Args: parent (QWidget): parent widget tutor (QWidget, NoneType): another widget used for positioning. """ super().__init__(parent) self._tutor = tutor self._base_size = QSize() self._base_offset = QPoint() self._original_text = None self._orig_pos = None self.first_index = QModelIndex() self.model = QStandardItemModel(self) self.proxy_model = QSortFilterProxyModel(self) self.proxy_model.setSourceModel(self.model) self.proxy_model.filterAcceptsRow = self._proxy_model_filter_accepts_row self.setModel(self.proxy_model) self.verticalHeader().hide() self.horizontalHeader().hide() self.setShowGrid(False) self.setMouseTracking(True) self.setTabKeyNavigation(False) delegate = _CustomLineEditDelegate(self) delegate.text_edited.connect(self._handle_delegate_text_edited) self.setItemDelegateForRow(0, delegate) def set_data(self, current, items): """Populates model. Args: current (str) items (Sequence(str)) """ item_list = [QStandardItem(current)] for item in items: qitem = QStandardItem(item) item_list.append(qitem) qitem.setFlags(~Qt.ItemIsEditable) self.model.invisibleRootItem().appendRows(item_list) self.first_index = self.proxy_model.mapFromSource( self.model.index(0, 0)) def set_base_size(self, size): self._base_size = size def set_base_offset(self, offset): self._base_offset = offset def update_geometry(self): """Updates geometry. """ self.horizontalHeader().setDefaultSectionSize(self._base_size.width()) self.verticalHeader().setDefaultSectionSize(self._base_size.height()) self._orig_pos = self.pos() + self._base_offset if self._tutor: self._orig_pos += self._tutor.mapTo(self.parent(), self._tutor.rect().topLeft()) self.refit() def refit(self): self.move(self._orig_pos) table_height = self.verticalHeader().length() size = QSize(self._base_size.width(), table_height + 2).boundedTo(self.parent().size()) self.resize(size) # Adjust position if widget is outside parent's limits bottom_right = self.mapToGlobal(self.rect().bottomRight()) parent_bottom_right = self.parent().mapToGlobal( self.parent().rect().bottomRight()) x_offset = max(0, bottom_right.x() - parent_bottom_right.x()) y_offset = max(0, bottom_right.y() - parent_bottom_right.y()) self.move(self.pos() - QPoint(x_offset, y_offset)) def data(self): return self.first_index.data(Qt.EditRole) @Slot("QString") def _handle_delegate_text_edited(self, text): """Filters model as the first row is being edited.""" self._original_text = text self.proxy_model.setFilterRegExp("^" + text) self.proxy_model.setData(self.first_index, text) self.refit() def _proxy_model_filter_accepts_row(self, source_row, source_parent): """Always accept first row. """ if source_row == 0: return True return QSortFilterProxyModel.filterAcceptsRow(self.proxy_model, source_row, source_parent) def keyPressEvent(self, event): """Sets data from current index into first index as the user navigates through the table using the up and down keys. """ super().keyPressEvent(event) event.accept( ) # Important to avoid unhandled behavior when trying to navigate outside view limits # Initialize original text. TODO: Is there a better place for this? if self._original_text is None: self.proxy_model.setData(self.first_index, event.text()) self._handle_delegate_text_edited(event.text()) # Set data from current index in model if event.key() in (Qt.Key_Up, Qt.Key_Down): current = self.currentIndex() if current.row() == 0: self.proxy_model.setData(self.first_index, self._original_text) else: self.proxy_model.setData(self.first_index, current.data()) def currentChanged(self, current, previous): super().currentChanged(current, previous) self.edit_first_index() def edit_first_index(self): """Edits first index if valid and not already being edited. """ if not self.first_index.isValid(): return if self.isPersistentEditorOpen(self.first_index): return self.edit(self.first_index) def mouseMoveEvent(self, event): """Sets the current index to the one hovered by the mouse.""" if not self.currentIndex().isValid(): return index = self.indexAt(event.pos()) if index.row() == 0: return self.setCurrentIndex(index) def mousePressEvent(self, event): """Commits data.""" index = self.indexAt(event.pos()) if index.row() == 0: return self.proxy_model.setData(self.first_index, index.data(Qt.EditRole)) self.data_committed.emit()
class SmartSyncDialog(object): def __init__(self, parent): self._dialog = QDialog(parent) self._dialog.setWindowIcon(QIcon(':/images/icon.png')) self._ui = Ui_Dialog() self._ui.setupUi(self._dialog) self._ui.centralWidget.setFrameShape(QFrame.NoFrame) self._ui.centralWidget.setLineWidth(1) self._model = None self._proxy_model = QSortFilterProxyModel() self._view = self._ui.folder_list_view self._view.setModel(self._proxy_model) self._view.expanded.connect(self.on_item_expanded) self._offline_paths = None # for frameless window moving self._x_coord = 0 self._y_coord = 0 self._dialog.mousePressEvent = self.on_mouse_press_event self._dialog.mouseMoveEvent = self.on_mouse_move_event self._loader_movie = QMovie(":/images/loader.gif") self._ui.loader_label.setMovie(self._loader_movie) def on_mouse_press_event(self, ev): self._x_coord = ev.x() self._y_coord = ev.y() def on_mouse_move_event(self, ev): self._dialog.move( ev.globalX() - self._x_coord, ev.globalY() - self._y_coord) def on_item_expanded(self, index): if self._model: self._model.on_item_expanded(self._proxy_model.mapToSource(index)) def show(self, root_path, hide_dotted=False): if LOGGING_ENABLED: logger.info( "Opening smart sync dialog for path '%s'...", root_path) self._model = TreeModel( root_path, hide_dotted=hide_dotted) self._proxy_model.setSourceModel(self._model) self.show_cursor_loading(show_movie=True) # Execute dialog result = self._dialog.exec_() if result == QDialog.Accepted: offline_dirs = self._model.get_added_to_offline_paths() new_online = list(self._model.get_removed_from_offline_paths()) new_offline = list(offline_dirs - self._offline_paths) if LOGGING_ENABLED: logger.debug("new offline dirs %s, new online dirs %s", new_offline, new_online) return new_offline, new_online else: return [], [] def set_offline_paths(self, offline_paths): self._offline_paths = offline_paths logger.debug("offline paths %s", offline_paths) self._model.set_offline_dirs(offline_paths) self.show_cursor_normal() self._view.expand(self._proxy_model.mapFromSource( self._model.get_root_path_index())) self._proxy_model.sort(0, Qt.AscendingOrder) def show_cursor_loading(self, show_movie=False): if show_movie: self._ui.stackedWidget.setCurrentIndex(1) self._loader_movie.start() else: self._dialog.setCursor(Qt.WaitCursor) def show_cursor_normal(self): self._dialog.setCursor(Qt.ArrowCursor) if self._loader_movie.state() == QMovie.Running: self._loader_movie.stop() self._ui.stackedWidget.setCurrentIndex(0)