summaryrefslogtreecommitdiff
path: root/qutebrowser/completion/models/completionmodel.py
blob: 236b2553346a97a86ce9cf8f83e5a1c80898733f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2017-2021 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.

"""A model that proxies access to one or more completion categories."""

from typing import MutableSequence

from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel

from qutebrowser.utils import log, qtutils, utils
from qutebrowser.api import cmdutils


class CompletionModel(QAbstractItemModel):

    """A model that proxies access to one or more completion categories.

    Top level indices represent categories.
    Child indices represent rows of those tables.

    Attributes:
        column_widths: The width percentages of the columns used in the
                       completion view.
        _categories: The sub-categories.
    """

    def __init__(self, *, column_widths=(30, 70, 0), parent=None):
        super().__init__(parent)
        self.column_widths = column_widths
        self._categories: MutableSequence[QAbstractItemModel] = []

    def _cat_from_idx(self, index):
        """Return the category pointed to by the given index.

        Args:
            idx: A QModelIndex
        Returns:
            A category if the index points at one, else None
        """
        # items hold an index to the parent category in their internalPointer
        # categories have an empty internalPointer
        if index.isValid() and not index.internalPointer():
            return self._categories[index.row()]
        return None

    def add_category(self, cat):
        """Add a completion category to the model."""
        self._categories.append(cat)

    def data(self, index, role=Qt.DisplayRole):
        """Return the item data for index.

        Override QAbstractItemModel::data.

        Args:
            index: The QModelIndex to get item flags for.
            role: The Qt ItemRole to get the data for.

        Return: The item data, or None on an invalid index.
        """
        if role != Qt.DisplayRole:
            return None
        cat = self._cat_from_idx(index)
        if cat:
            # category header
            if index.column() == 0:
                return self._categories[index.row()].name
            return None
        # item
        cat = self._cat_from_idx(index.parent())
        if not cat:
            return None
        idx = cat.index(index.row(), index.column())
        return cat.data(idx)

    def flags(self, index):
        """Return the item flags for index.

        Override QAbstractItemModel::flags.

        Return: The item flags, or Qt.NoItemFlags on error.
        """
        if not index.isValid():
            return Qt.NoItemFlags
        if index.parent().isValid():
            # item
            return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
                    Qt.ItemNeverHasChildren)
        else:
            # category
            return Qt.NoItemFlags

    def index(self, row, col, parent=QModelIndex()):
        """Get an index into the model.

        Override QAbstractItemModel::index.

        Return: A QModelIndex.
        """
        if (row < 0 or row >= self.rowCount(parent) or
                col < 0 or col >= self.columnCount(parent)):
            return QModelIndex()
        if parent.isValid():
            if parent.column() != 0:
                return QModelIndex()
            # store a pointer to the parent category in internalPointer
            return self.createIndex(row, col, self._categories[parent.row()])
        return self.createIndex(row, col, None)

    def parent(self, index):
        """Get an index to the parent of the given index.

        Override QAbstractItemModel::parent.

        Args:
            index: The QModelIndex to get the parent index for.
        """
        parent_cat = index.internalPointer()
        if not parent_cat:
            # categories have no parent
            return QModelIndex()
        row = self._categories.index(parent_cat)
        return self.createIndex(row, 0, None)

    def rowCount(self, parent=QModelIndex()):
        """Override QAbstractItemModel::rowCount."""
        if not parent.isValid():
            # top-level
            return len(self._categories)
        cat = self._cat_from_idx(parent)
        if not cat or parent.column() != 0:
            # item or nonzero category column (only first col has children)
            return 0
        else:
            # category
            return cat.rowCount()

    def columnCount(self, parent=QModelIndex()):
        """Override QAbstractItemModel::columnCount."""
        utils.unused(parent)
        return len(self.column_widths)

    def canFetchMore(self, parent):
        """Override to forward the call to the categories."""
        cat = self._cat_from_idx(parent)
        if cat:
            return cat.canFetchMore(QModelIndex())
        return False

    def fetchMore(self, parent):
        """Override to forward the call to the categories."""
        cat = self._cat_from_idx(parent)
        if cat:
            cat.fetchMore(QModelIndex())

    def count(self):
        """Return the count of non-category items."""
        return sum(t.rowCount() for t in self._categories)

    def set_pattern(self, pattern):
        """Set the filter pattern for all categories.

        Args:
            pattern: The filter pattern to set.
        """
        log.completion.debug("Setting completion pattern '{}'".format(pattern))
        self.layoutAboutToBeChanged.emit()  # type: ignore[attr-defined]
        for cat in self._categories:
            cat.set_pattern(pattern)
        self.layoutChanged.emit()  # type: ignore[attr-defined]

    def first_item(self):
        """Return the index of the first child (non-category) in the model."""
        for row, cat in enumerate(self._categories):
            if cat.rowCount() > 0:
                parent = self.index(row, 0)
                index = self.index(0, 0, parent)
                qtutils.ensure_valid(index)
                return index
        return QModelIndex()

    def last_item(self):
        """Return the index of the last child (non-category) in the model."""
        for row, cat in reversed(list(enumerate(self._categories))):
            childcount = cat.rowCount()
            if childcount > 0:
                parent = self.index(row, 0)
                index = self.index(childcount - 1, 0, parent)
                qtutils.ensure_valid(index)
                return index
        return QModelIndex()

    def columns_to_filter(self, index):
        """Return the column indices the filter pattern applies to.

        Args:
            index: index of the item to check.

        Return: A list of integers.
        """
        cat = self._cat_from_idx(index.parent())
        return cat.columns_to_filter if cat else []

    def delete_cur_item(self, index):
        """Delete the row at the given index."""
        qtutils.ensure_valid(index)
        parent = index.parent()
        cat = self._cat_from_idx(parent)
        assert cat, "CompletionView sent invalid index for deletion"
        if not cat.delete_func:
            raise cmdutils.CommandError("Cannot delete this item.")

        data = [cat.data(cat.index(index.row(), i))
                for i in range(cat.columnCount())]
        cat.delete_func(data)

        self.beginRemoveRows(parent, index.row(), index.row())
        cat.removeRow(index.row(), QModelIndex())
        self.endRemoveRows()