def setupTabs(self): """ Setup the various tabs in the AddressWidget. """ groups = ["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"] for group in groups: proxyModel = QSortFilterProxyModel(self) proxyModel.setSourceModel(self.tableModel) proxyModel.setDynamicSortFilter(True) tableView = QTableView() tableView.setModel(proxyModel) tableView.setSortingEnabled(True) tableView.setSelectionBehavior(QAbstractItemView.SelectRows) tableView.horizontalHeader().setStretchLastSection(True) tableView.verticalHeader().hide() tableView.setEditTriggers(QAbstractItemView.NoEditTriggers) tableView.setSelectionMode(QAbstractItemView.SingleSelection) # This here be the magic: we use the group name (e.g. "ABC") to # build the regex for the QSortFilterProxyModel for the group's # tab. The regex will end up looking like "^[ABC].*", only # allowing this tab to display items where the name starts with # "A", "B", or "C". Notice that we set it to be case-insensitive. re = QRegularExpression("^[{}].*".format(group)) assert re.isValid() re.setPatternOptions(QRegularExpression.CaseInsensitiveOption) proxyModel.setFilterRegularExpression(re) proxyModel.setFilterKeyColumn(0) # Filter on the "name" column proxyModel.sort(0, Qt.AscendingOrder) # This prevents an application crash (see: http://www.qtcentre.org/threads/58874-QListView-SelectionModel-selectionChanged-Crash) viewselectionmodel = tableView.selectionModel() tableView.selectionModel().selectionChanged.connect( self.selectionChanged) self.addTab(tableView, group)
class PlaylistView(QTableView): current_index_changed = Signal(QModelIndex) playlist_double_clicked = Signal() filtering = Signal(str) unfiltered = Signal(str) next = Slot(int) previous = Slot(int) @property def mime_Index(self): return 'application/x-original_index' @property def mime_URLS(self): return 'application/x-file-urls' @property def mime_url_count(self): return 'application/x-urls-count' @property def url_delimiter(self): return '\n' @property def open_file_filter(self): return '*.mp4 *.m4v *.mov *.mpg *.mpeg *. mp3 *.m4a *.wmv *.aiff *.wav' def __init__(self, parent=None): super(PlaylistView, self).__init__(parent) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setDragEnabled(True) self.setAcceptDrops(True) self.setDragDropMode(QAbstractItemView.DragDrop) self.setDropIndicatorShown(True) self.proxy_model = QSortFilterProxyModel(self) self.proxy_model.setSourceModel(PlaylistModel(self)) self.setModel(self.proxy_model) # self.setModel(PlaylistModel()) self.setShowGrid(False) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.verticalHeader().setDefaultSectionSize(16) self.verticalHeader().hide() self.horizontalHeader().setMinimumSectionSize(30) self.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) self.current_index = QModelIndex() self.previousIndex = QModelIndex() self.rubberBand: QRubberBand = QRubberBand(QRubberBand.Rectangle, self) self.isDragging = False self.wasSelected = False self.context_menu = QMenu(self) self.create_context_menu() # self.current_index_changed.connect(self.proxy_model.sourceModel().set_current_index) self.current_index_changed.connect( self.proxy_model.sourceModel().set_current_index) def create_context_menu(self): add_file = createAction(self, 'Add File(s)', self.open) delete_selected = createAction(self, 'Delete selected', self.delete_items) sort_action = createAction(self, 'Sort', self.proxy_model.sourceModel().sort) pickup_dup_action = createAction(self, 'Filter by same title', self.filter_same_title) stop_filtering_action = createAction(self, 'Stop Filtering', self.unfilter) self.context_menu.addActions([ add_file, delete_selected, sort_action, pickup_dup_action, stop_filtering_action ]) def contextMenuEvent(self, event): self.context_menu.exec_(event.globalPos()) def count(self): return self.proxy_model.sourceModel().rowCount() def open(self): list, _ = QFileDialog.getOpenFileNames(self, 'Open File', QDir.homePath()) for path in list: if path[-3:] == 'm3u': self.load(path) else: self.add_item(path) def open_directory(self): directory_url = QFileDialog.getExistingDirectory( self, '0Open directory', QDir.homePath()) dir = QDir(directory_url) filters = [ '*.mp4', '*.m4v', '*.mov', '*.mpg', '*.mpeg', '*.mp3', '*.m4a', '*.wmv', '*.wav', '*.aiff' ] dir.setNameFilters(filters) file_list = dir.entryList() path = dir.absolutePath() + '/' for file in file_list: self.add_item(path + file) def save(self, path=None): """プレイリストを保存する。 :param file :QFile 出力するようのファイル fileが指定されていれば、fileに内容を書き込み、 指定がなければ、ダイアログで名前を指定してそこにファイルを保存。 """ if path is None: return with open(path, 'wt') as fout: for i in range(self.proxy_model.sourceModel().rowCount()): index = self.proxy_model.sourceModel().index(i, 0) print(self.url(index).toLocalFile(), file=fout) return True def load(self, path=None): """プレイリストを読み込む pathが与えられた場合は、そこから読み込み、 ない場合は、何も読み込まない。""" with open(path, 'rt') as fin: for line in fin: self.add_item(line[:-1]) # 最後の改行文字を取り除く def add_item(self, path): if is_media(path): self.proxy_model.sourceModel().add(QUrl.fromLocalFile(path)) return True return False def current_url(self): return self.url(self.current_index) def current_title(self): return self.title(self.current_index) def next(self, step=1): if self.current_row() + step < self.count(): self.set_current_index_from_row(self.current_row() + step) return self.url(self.current_index) else: return None def previous(self, step=1): if self.current_row() - step >= 0: self.set_current_index_from_row(self.current_row() - step) return self.url(self.current_index) else: return None def selected(self): selected_indexes = self.selectedIndexes() if len(selected_indexes) > 0: return selected_indexes[0] else: return None def current_row(self): return self.current_index.row() def url(self, index): if isinstance(index, int): row = index if 0 <= row < self.count(): index = self.proxy_model.sourceModel().index(row, 0) if isinstance(index, QModelIndex): return self.proxy_model.sourceModel().data(index) else: return None def title(self, index): if isinstance(index, int): row = index if 0 <= row < self.count(): index = self.proxy_model.sourceModel().index(row, 0) if isinstance(index, QModelIndex): return self.proxy_model.sourceModel().data(index, Qt.DisplayRole) else: return None def set_current_index_from_row(self, row): new_index = self.proxy_model.sourceModel().index(row, 0) return self.set_current_index(new_index) def set_current_index(self, new_index: QModelIndex): self.current_index = new_index self.current_index_changed.emit(new_index) def deactivate(self): self.set_current_index(QModelIndex()) def auto_resize_header(self): """auto resize Header width on table view. """ width = self.viewport().width() duration_width = 120 self.horizontalHeader().resizeSection(0, width - duration_width) self.horizontalHeader().resizeSection(1, duration_width) def mousePressEvent(self, event): """左クリックされたらカーソル下にある要素を選択し、ドラッグを認識するために現在の位置を保存する。 :param event: QMousePressEvent :return: nothing """ self.isDragging = False if Qt.LeftButton == event.button(): self.dragStartPosition = event.pos() index = self.indexAt(self.dragStartPosition) if index in self.selectedIndexes(): self.isDragging = True self.wasSelected = True return self.rubberBand.setGeometry(QRect(self.dragStartPosition, QSize())) self.rubberBand.show() super(PlaylistView, self).mousePressEvent(event) def mouseMoveEvent(self, event): """start Drag and prepare for Drop. :type event: QMoveEvent マウスを動かした嶺がQApplication.startDragDistance()を超えると、Drag開始されたと認識し、 そのための準備を行う。QMimeDataを使って、データをやりとりする。 """ if not (event.buttons() & Qt.LeftButton): return if (event.pos() - self.dragStartPosition).manhattanLength() \ < QApplication.startDragDistance(): return if self.isDragging: indexes = self.selectedIndexes() urls = self.url_list(indexes) mimeData = QMimeData() # mimeData.setData(self.mime_URLS, convert_to_bytearray(urls)) mimeData.setUrls(urls) file_icon = self.style().standardIcon(QStyle.SP_FileIcon) pixmap = file_icon.pixmap(32, 32) drag = QDrag(self) drag.setMimeData(mimeData) drag.setPixmap(pixmap) drag.setHotSpot(QPoint(0, 0)) dropAction = drag.exec_(Qt.CopyAction | Qt.MoveAction, Qt.CopyAction) if dropAction == Qt.MoveAction: pass else: self.rubberBand.setGeometry( QRect(self.dragStartPosition, event.pos()).normalized()) super(PlaylistView, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): '''マウスを離したときにQRubberBandを隠す。 左クリックをpress と release がだいたい同じ位置であれば、その要素を1つだけ選択する。 :param event: ''' self.rubberBand.hide() if Qt.LeftButton == event.button( ) and Qt.NoModifier == event.modifiers(): if self.indexAt(event.pos()).row() == -1 and \ self.indexAt(self.dragStartPosition).row() == -1: self.clearSelection() elif len(self.selectedIndexes() ) / 2 == 1 and self.wasSelected == True: self.clearSelection() elif (event.pos() - self.dragStartPosition).manhattanLength() \ < QApplication.startDragDistance(): self.setCurrentIndex(self.indexAt(event.pos())) self.wasSelected = False super(PlaylistView, self).mouseReleaseEvent(event) def dragEnterEvent(self, event): """ドラッグした状態でWidgetに入った縁で呼ばれる関数。 :param event: QDragEvent :return: nothing イベントが発生元と発生しているWidgetが同一の場合はMoveActionにする。それ以外はCopyAction。 その二つの場合は受け入れられるように、accept()もしくはacceptProposedAction()を呼ぶ。 """ if event.mimeData().hasUrls() or event.mimeData().hasFormat( self.mime_URLS): if event.source() is self: event.setDropAction(Qt.MoveAction) event.accept() else: event.acceptProposedAction() else: event.ignore() def dragMoveEvent(self, event): """ドラッグした状態でWidget内を移動したときに呼ばれる。 :param event: QDragMoveEvent :return: nothing ドラッグしている要素の背景の色を変えて、どこにファイルがDropされるかをグラデーションした背景で 表現する。 """ if event.mimeData().hasUrls() or event.mimeData().hasFormat( self.mime_URLS): self.rubberBand.setGeometry( self.rectForDropIndicator( self.index_for_dropping_pos(event.pos()))) self.rubberBand.show() self.previousIndex = self.indexAt(event.pos()) if event.source() is self: event.setDropAction(Qt.MoveAction) event.accept() else: event.acceptProposedAction() else: event.ignore() def dragLeaveEvent(self, event): """ドラッグしたままWidget内を出たときにドラッグ下にあった要素の背景色の色を元に戻す。 :param event: QDragLeaveEvent :return: nothing """ self.rubberBand.hide() def dropEvent(self, event): """Dropされたらデータを取り出して、新たに登録する。 :param event: QDropEvent :return: nothing ファイルへのパスと移動前に登録してあった要素のindexを取り出す。 """ self.rubberBand.hide() if event.mimeData().hasUrls() or event.mimeData().hasFormat( self.mime_URLS): if event.mimeData().hasUrls(): urls = event.mimeData().urls() else: urls = convert_from_bytearray(event.mimeData().data( self.mime_URLS)) index = self.index_for_dropping_pos(event.pos()) if event.source() is self: self.move_items(self.selectedIndexes(), index) event.setDropAction(Qt.MoveAction) event.accept() else: self.add_items(urls) event.acceptProposedAction() else: event.ignore() def mouseDoubleClickEvent(self, event): if event.button() == Qt.LeftButton: new_index = self.indexAt(event.pos()) if not new_index.isValid(): return self.selectRow(new_index.row()) self.playlist_double_clicked.emit() def add_items(self, items: [QUrl], start: int = -1): """渡された要素をmodelに追加する。 :param items: 追加する項目 :param start: 追加するindexを表す。初期値は-1 start に −1を渡すと一番後ろに追加する。 """ if isinstance(items, QUrl): self.proxy_model.sourceModel().add(items) elif start == -1: for item in items: self.proxy_model.sourceModel().add(item) else: for item, i in items, range(start, len(items)): self.proxy_model.sourceModel().add(item, i) def delete_items(self): """渡されたインデックスを順番に消していく。 :param indexes: 消すためのインデックス """ indexes = self.selectedIndexes() if indexes: self.proxy_model.sourceModel().remove_items(indexes) else: return def move_items(self, indexes: [QModelIndex], dest: QModelIndex): self.proxy_model.sourceModel().move(indexes, dest.row()) def filter_same_title(self): dup = self.proxy_model.sourceModel().pickup_same_title() re = QRegularExpression('|'.join(dup)) self.proxy_model.setFilterRegularExpression(re) self.filtering.emit(' - filtered') def unfilter(self): self.proxy_model.setFilterWildcard('*') self.unfiltered.emit('') def index_for_dropping_pos(self, pos: QPoint) -> QModelIndex: """dropした場所のindexを返す。ただし、要素の高さ半分より下にある場合は、下の要素を返す。 :param pos: :return: posから導き出されたindex 挿入や移動のために、要素の間を意識している。 """ index = self.indexAt(pos) if index.row() < 0: new_index = self.proxy_model.sourceModel().index( self.proxy_model.sourceModel().rowCount(), 0) return new_index item_rect = self.visualRect(index) pos_in_rect = pos.y() - item_rect.top() if pos_in_rect < (item_rect.height() / 2): return index else: return self.proxy_model.sourceModel().index(index.row() + 1, 0) def rectForDropIndicator(self, index: QModelIndex) -> QRect: """QRubberBand を DropIndicatorとして表示するためのQRectを返す。 Geometryに渡されるので、表示位置となるようにQRectを作成する。 幅が表示領域、縦1pixelの棒で表示する。 """ item_rect = self.visualRect(index) top_left = item_rect.topLeft() size = QSize(item_rect.width(), 3) return QRect(top_left, size) def url_list(self, indexes): urls = [] for index in indexes: urls.append(self.proxy_model.sourceModel().data(index)) return sorted(set(urls), key=urls.index)
class Window(QWidget): def __init__(self): super(Window, self).__init__() self.proxyModel = QSortFilterProxyModel() self.proxyModel.setDynamicSortFilter(True) self.sourceGroupBox = QGroupBox("Original Model") self.proxyGroupBox = QGroupBox("Sorted/Filtered Model") self.sourceView = QTreeView() self.sourceView.setRootIsDecorated(False) self.sourceView.setAlternatingRowColors(True) self.proxyView = QTreeView() self.proxyView.setRootIsDecorated(False) self.proxyView.setAlternatingRowColors(True) self.proxyView.setModel(self.proxyModel) self.proxyView.setSortingEnabled(True) self.sortCaseSensitivityCheckBox = QCheckBox("Case sensitive sorting") self.filterCaseSensitivityCheckBox = QCheckBox("Case sensitive filter") self.filterPatternLineEdit = QLineEdit() self.filterPatternLineEdit.setClearButtonEnabled(True) self.filterPatternLabel = QLabel("&Filter pattern:") self.filterPatternLabel.setBuddy(self.filterPatternLineEdit) self.filterSyntaxComboBox = QComboBox() self.filterSyntaxComboBox.addItem("Regular expression", REGULAR_EXPRESSION) self.filterSyntaxComboBox.addItem("Wildcard", WILDCARD) self.filterSyntaxComboBox.addItem("Fixed string", FIXED_STRING) self.filterSyntaxLabel = QLabel("Filter &syntax:") self.filterSyntaxLabel.setBuddy(self.filterSyntaxComboBox) self.filterColumnComboBox = QComboBox() self.filterColumnComboBox.addItem("Subject") self.filterColumnComboBox.addItem("Sender") self.filterColumnComboBox.addItem("Date") self.filterColumnLabel = QLabel("Filter &column:") self.filterColumnLabel.setBuddy(self.filterColumnComboBox) self.filterPatternLineEdit.textChanged.connect(self.filterRegExpChanged) self.filterSyntaxComboBox.currentIndexChanged.connect(self.filterRegExpChanged) self.filterColumnComboBox.currentIndexChanged.connect(self.filterColumnChanged) self.filterCaseSensitivityCheckBox.toggled.connect(self.filterRegExpChanged) self.sortCaseSensitivityCheckBox.toggled.connect(self.sortChanged) sourceLayout = QHBoxLayout() sourceLayout.addWidget(self.sourceView) self.sourceGroupBox.setLayout(sourceLayout) proxyLayout = QGridLayout() proxyLayout.addWidget(self.proxyView, 0, 0, 1, 3) proxyLayout.addWidget(self.filterPatternLabel, 1, 0) proxyLayout.addWidget(self.filterPatternLineEdit, 1, 1, 1, 2) proxyLayout.addWidget(self.filterSyntaxLabel, 2, 0) proxyLayout.addWidget(self.filterSyntaxComboBox, 2, 1, 1, 2) proxyLayout.addWidget(self.filterColumnLabel, 3, 0) proxyLayout.addWidget(self.filterColumnComboBox, 3, 1, 1, 2) proxyLayout.addWidget(self.filterCaseSensitivityCheckBox, 4, 0, 1, 2) proxyLayout.addWidget(self.sortCaseSensitivityCheckBox, 4, 2) self.proxyGroupBox.setLayout(proxyLayout) mainLayout = QVBoxLayout() mainLayout.addWidget(self.sourceGroupBox) mainLayout.addWidget(self.proxyGroupBox) self.setLayout(mainLayout) self.setWindowTitle("Basic Sort/Filter Model") self.resize(500, 450) self.proxyView.sortByColumn(1, Qt.AscendingOrder) self.filterColumnComboBox.setCurrentIndex(1) self.filterPatternLineEdit.setText("Andy|Grace") self.filterCaseSensitivityCheckBox.setChecked(True) self.sortCaseSensitivityCheckBox.setChecked(True) def setSourceModel(self, model): self.proxyModel.setSourceModel(model) self.sourceView.setModel(model) def filterRegExpChanged(self): syntax_nr = self.filterSyntaxComboBox.currentData() pattern = self.filterPatternLineEdit.text() if syntax_nr == WILDCARD: pattern = QRegularExpression.wildcardToRegularExpression(pattern) elif syntax_nr == FIXED_STRING: pattern = QRegularExpression.escape(pattern) regExp = QRegularExpression(pattern) if not self.filterCaseSensitivityCheckBox.isChecked(): options = regExp.patternOptions() options |= QRegularExpression.CaseInsensitiveOption regExp.setPatternOptions(options) self.proxyModel.setFilterRegularExpression(regExp) def filterColumnChanged(self): self.proxyModel.setFilterKeyColumn(self.filterColumnComboBox.currentIndex()) def sortChanged(self): if self.sortCaseSensitivityCheckBox.isChecked(): caseSensitivity = Qt.CaseSensitive else: caseSensitivity = Qt.CaseInsensitive self.proxyModel.setSortCaseSensitivity(caseSensitivity)
class Completer(QWidget): """docstring for ClassName Attributes: delegate (CompleterDelegate): the delegate use by the view model (CompleterModel): the model proxy_model (QSortFilterProxyModel ): the proxy model used to filter model panel (QLabel): The description widget view (QListView): the view Signals: activated (str): return the keyword selected """ activated = Signal(str) def __init__(self, parent=None): super().__init__(parent) self._target = None self._completion_prefix = "" self.setWindowFlag(Qt.Popup) self.setFocusPolicy(Qt.NoFocus) # create model self.model = CompleterModel() self.proxy_model = QSortFilterProxyModel() self.proxy_model.setSourceModel(self.model) self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive) # create delegate self.delegate = CompleterDelegate() # create view self.view = QListView() self.view.setSelectionMode(QAbstractItemView.SingleSelection) self.view.setFocusPolicy(Qt.NoFocus) self.view.installEventFilter(self) self.view.setModel(self.proxy_model) self.view.setItemDelegate(self.delegate) self.view.setMinimumWidth(200) self.view.setUniformItemSizes(True) self.view.setSpacing(0) self.view.selectionModel().currentRowChanged.connect( self._on_row_changed) self.setFocusProxy(self.view) # create panel info self.panel = QLabel() self.panel.setAlignment(Qt.AlignTop) self.panel.setMinimumWidth(300) self.panel.setWordWrap(True) self.panel.setFrameShape(QFrame.StyledPanel) # Create layout vlayout = QHBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) vlayout.setSpacing(0) vlayout.addWidget(self.view) vlayout.addWidget(self.panel) self.setLayout(vlayout) def set_target(self, target): """Set CodeEdit Args: target (CodeEdit): The CodeEdit """ self._target = target self.installEventFilter(self._target) def eventFilter(self, obj: QObject, event: QEvent) -> bool: """Filter event from CodeEdit and QListView Args: obj (QObject): Description event (QEvent): Description Returns: bool """ # Intercept CodeEdit event if obj == self._target: if event.type() == QEvent.FocusOut: # Ignore lost focus! return True else: obj.event(event) return True # Intercept QListView event if obj == self.view: # Redirect event to QTextExit if event.type() == QEvent.KeyPress and self._target: current = self.view.selectionModel().currentIndex() # emit signal when user press return if event.key() == Qt.Key_Return: word = current.data() self.activated.emit(word) self.hide() event.ignore() return True # use tab to move down/up in the list if event.key() == Qt.Key_Tab: if current.row() < self.proxy_model.rowCount() - 1: self.view.setCurrentIndex( self.proxy_model.index(current.row() + 1, 0)) if event.key() == Qt.Key_Backtab: if current.row() > 0: self.view.setCurrentIndex( self.proxy_model.index(current.row() - 1, 0)) # Route other key event to the target ! This make possible to write text when completer is visible self._target.event(event) return super().eventFilter(obj, event) def complete(self, rect: QRect): """Show completer as popup Args: rect (QRect): the area where to display the completer """ if self.proxy_model.rowCount() == 0: self.hide() return if self._target: pos = self._target.mapToGlobal(rect.bottomRight()) self.move(pos) self.setFocus() if not self.isVisible(): width = 400 #height = self.view.sizeHintForRow(0) * self.proxy_model.rowCount() + 3 # HACK.. TODO better ! #height = min(self._target.height() / 2, height) #self.resize(width, height) self.adjustSize() self.show() def set_completion_prefix(self, prefix: str): """Set prefix and filter model Args: prefix (str): A prefix keyword used to filter model """ self.view.clearSelection() self._completion_prefix = prefix self.proxy_model.setFilterRegularExpression( QRegularExpression(f"^{prefix}.*", QRegularExpression.CaseInsensitiveOption)) if self.proxy_model.rowCount() > 0: self.select_row(0) def select_row(self, row: int): """Select a row in the model Args: row (int): a row number """ index = self.proxy_model.index(row, 0) self.view.selectionModel().setCurrentIndex(index, QItemSelectionModel.Select) def completion_prefix(self) -> str: """getter of completion_prefix TODO: use getter / setter Returns: str: Return the completion_prefix """ return self._completion_prefix def hide(self): """Override from QWidget Hide the completer """ self.set_completion_prefix("") super().hide() def _on_row_changed(self, current: QModelIndex, previous: QModelIndex): """Slot received when user select a new item in the list. This is used to update the panel Args: current (QModelIndex): the selection index previous (QModelIndex): UNUSED """ description = current.data(Qt.ToolTipRole) self.panel.setText(description)