예제 #1
0
    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)
예제 #2
0
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)
예제 #3
0
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)
예제 #4
0
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)