Implement a hybrid list/sql completion model.
Now all completion models are of a single type called CompletionModel. This model combines one or more categories. A category can either be a ListCategory or a SqlCategory. This simplifies the API, and will allow the use of models that combine simple list-based and sql sources. This is important for two reasons: - Adding searchengines to url completion - Using an on-disk sqlite database for history, while keeping bookmarks and quickmars as text files.
This commit is contained in:
parent
921211bbaa
commit
e3a33ca427
@ -24,7 +24,7 @@ from PyQt5.QtCore import pyqtSlot, QObject, QTimer
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
from qutebrowser.utils import log, utils
|
||||
from qutebrowser.completion.models import sortfilter, miscmodels
|
||||
from qutebrowser.completion.models import miscmodels
|
||||
|
||||
|
||||
class Completer(QObject):
|
||||
@ -62,22 +62,6 @@ class Completer(QObject):
|
||||
completion = self.parent()
|
||||
return completion.model()
|
||||
|
||||
def _get_completion_model(self, completion, pos_args):
|
||||
"""Get a completion model based on an enum member.
|
||||
|
||||
Args:
|
||||
completion: A usertypes.Completion member.
|
||||
pos_args: The positional args entered before the cursor.
|
||||
|
||||
Return:
|
||||
A completion model or None.
|
||||
"""
|
||||
model = completion(*pos_args)
|
||||
if model is None or hasattr(model, 'set_pattern'):
|
||||
return model
|
||||
else:
|
||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||
|
||||
def _get_new_completion(self, before_cursor, under_cursor):
|
||||
"""Get a new completion.
|
||||
|
||||
@ -96,9 +80,8 @@ class Completer(QObject):
|
||||
log.completion.debug("After removing flags: {}".format(before_cursor))
|
||||
if not before_cursor:
|
||||
# '|' or 'set|'
|
||||
model = miscmodels.command()
|
||||
log.completion.debug('Starting command completion')
|
||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||
return miscmodels.command()
|
||||
try:
|
||||
cmd = cmdutils.cmd_dict[before_cursor[0]]
|
||||
except KeyError:
|
||||
@ -113,7 +96,8 @@ class Completer(QObject):
|
||||
return None
|
||||
if completion is None:
|
||||
return None
|
||||
model = self._get_completion_model(completion, before_cursor[1:])
|
||||
|
||||
model = completion(*before_cursor[1:])
|
||||
log.completion.debug('Starting {} completion'
|
||||
.format(completion.__name__))
|
||||
return model
|
||||
|
@ -197,7 +197,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
|
||||
if index.parent().isValid():
|
||||
pattern = index.model().pattern
|
||||
columns_to_filter = index.model().srcmodel.columns_to_filter
|
||||
columns_to_filter = index.model().columns_to_filter
|
||||
if index.column() in columns_to_filter and pattern:
|
||||
repl = r'<span class="highlight">\g<0></span>'
|
||||
text = re.sub(re.escape(pattern).replace(r'\ ', r'|'),
|
||||
|
@ -299,7 +299,7 @@ class CompletionView(QTreeView):
|
||||
if pattern is not None:
|
||||
model.set_pattern(pattern)
|
||||
|
||||
self._column_widths = model.srcmodel.column_widths
|
||||
self._column_widths = model.column_widths
|
||||
self._resize_columns()
|
||||
self._maybe_update_geometry()
|
||||
|
||||
@ -368,7 +368,7 @@ class CompletionView(QTreeView):
|
||||
"""Delete the current completion item."""
|
||||
if not self.currentIndex().isValid():
|
||||
raise cmdexc.CommandError("No item selected!")
|
||||
if self.model().srcmodel.delete_cur_item is None:
|
||||
if self.model().delete_cur_item is None:
|
||||
raise cmdexc.CommandError("Cannot delete this item.")
|
||||
else:
|
||||
self.model().srcmodel.delete_cur_item(self)
|
||||
self.model().delete_cur_item(self)
|
||||
|
@ -1,105 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""The base completion model for completion in the command line.
|
||||
|
||||
Module attributes:
|
||||
Role: An enum of user defined model roles.
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QStandardItemModel, QStandardItem
|
||||
|
||||
|
||||
class CompletionModel(QStandardItemModel):
|
||||
|
||||
"""A simple QStandardItemModel adopted for completions.
|
||||
|
||||
Used for showing completions later in the CompletionView. Supports setting
|
||||
marks and adding new categories/items easily.
|
||||
|
||||
Attributes:
|
||||
column_widths: The width percentages of the columns used in the
|
||||
completion view.
|
||||
"""
|
||||
|
||||
def __init__(self, *, column_widths=(30, 70, 0), columns_to_filter=None,
|
||||
delete_cur_item=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setColumnCount(3)
|
||||
self.columns_to_filter = columns_to_filter or [0]
|
||||
self.column_widths = column_widths
|
||||
self.delete_cur_item = delete_cur_item
|
||||
|
||||
def new_category(self, name):
|
||||
"""Add a new category to the model.
|
||||
|
||||
Args:
|
||||
name: The name of the category to add.
|
||||
|
||||
Return:
|
||||
The created QStandardItem.
|
||||
"""
|
||||
cat = QStandardItem(name)
|
||||
self.appendRow(cat)
|
||||
return cat
|
||||
|
||||
def new_item(self, cat, name, desc='', misc=None):
|
||||
"""Add a new item to a category.
|
||||
|
||||
Args:
|
||||
cat: The parent category.
|
||||
name: The name of the item.
|
||||
desc: The description of the item.
|
||||
misc: Misc text to display.
|
||||
|
||||
Return:
|
||||
A (nameitem, descitem, miscitem) tuple.
|
||||
"""
|
||||
assert not isinstance(name, int)
|
||||
assert not isinstance(desc, int)
|
||||
assert not isinstance(misc, int)
|
||||
|
||||
nameitem = QStandardItem(name)
|
||||
descitem = QStandardItem(desc)
|
||||
miscitem = QStandardItem(misc)
|
||||
|
||||
cat.appendRow([nameitem, descitem, miscitem])
|
||||
|
||||
def flags(self, index):
|
||||
"""Return the item flags for index.
|
||||
|
||||
Override QAbstractItemModel::flags.
|
||||
|
||||
Args:
|
||||
index: The QModelIndex to get item flags for.
|
||||
|
||||
Return:
|
||||
The item flags, or Qt.NoItemFlags on error.
|
||||
"""
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
if index.parent().isValid():
|
||||
# item
|
||||
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
|
||||
Qt.ItemNeverHasChildren)
|
||||
else:
|
||||
# category
|
||||
return Qt.NoItemFlags
|
@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
|
||||
# Copyright 2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@ -17,65 +17,29 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""A completion model backed by SQL tables."""
|
||||
"""A model that proxies access to one or more completion categories."""
|
||||
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel
|
||||
from PyQt5.QtSql import QSqlQueryModel
|
||||
|
||||
from qutebrowser.utils import log, qtutils
|
||||
from qutebrowser.misc import sql
|
||||
from qutebrowser.completion.models import sortfilter, listcategory, sqlcategory
|
||||
|
||||
|
||||
class _SqlCompletionCategory(QSqlQueryModel):
|
||||
"""Wraps a SqlQuery for use as a completion category."""
|
||||
class CompletionModel(QAbstractItemModel):
|
||||
|
||||
def __init__(self, name, *, sort_by, sort_order, select, where,
|
||||
columns_to_filter, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.tablename = name
|
||||
"""A model that proxies access to one or more completion categories.
|
||||
|
||||
query = sql.run_query('select * from {} limit 1'.format(name))
|
||||
self._fields = [query.record().fieldName(i) for i in columns_to_filter]
|
||||
|
||||
querystr = 'select {} from {} where ('.format(select, name)
|
||||
# the incoming pattern will have literal % and _ escaped with '\'
|
||||
# we need to tell sql to treat '\' as an escape character
|
||||
querystr += ' or '.join("{} like ? escape '\\'".format(f)
|
||||
for f in self._fields)
|
||||
querystr += ')'
|
||||
if where:
|
||||
querystr += ' and ' + where
|
||||
|
||||
if sort_by:
|
||||
assert sort_order == 'asc' or sort_order == 'desc'
|
||||
querystr += ' order by {} {}'.format(sort_by, sort_order)
|
||||
|
||||
self._querystr = querystr
|
||||
self.set_pattern('%')
|
||||
|
||||
def set_pattern(self, pattern):
|
||||
query = sql.run_query(self._querystr, [pattern for _ in self._fields])
|
||||
self.setQuery(query)
|
||||
|
||||
|
||||
class SqlCompletionModel(QAbstractItemModel):
|
||||
|
||||
"""A sqlite-based model that provides data for the CompletionView.
|
||||
|
||||
This model is a wrapper around one or more sql tables. The tables are all
|
||||
stored in a single database in qutebrowser's cache directory.
|
||||
|
||||
Top level indices represent categories, each of which is backed by a single
|
||||
table. Child indices represent rows of those tables.
|
||||
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.
|
||||
columns_to_filter: A list of indices of columns to apply the filter to.
|
||||
pattern: Current filter pattern, used for highlighting.
|
||||
_categories: The category tables.
|
||||
_categories: The sub-categories.
|
||||
"""
|
||||
|
||||
def __init__(self, *, column_widths=(30, 70, 0), columns_to_filter=None,
|
||||
@ -84,7 +48,6 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
self.columns_to_filter = columns_to_filter or [0]
|
||||
self.column_widths = column_widths
|
||||
self._categories = []
|
||||
self.srcmodel = self # TODO: dummy for compat with old API
|
||||
self.pattern = ''
|
||||
self.delete_cur_item = delete_cur_item
|
||||
|
||||
@ -94,7 +57,7 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
Args:
|
||||
idx: A QModelIndex
|
||||
Returns:
|
||||
A _SqlCompletionCategory if the index points at one, else None
|
||||
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
|
||||
@ -102,7 +65,19 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
return self._categories[index.row()]
|
||||
return None
|
||||
|
||||
def new_category(self, name, *, select='*', where=None, sort_by=None,
|
||||
def add_list(self, name, items):
|
||||
"""Add a list of items as a completion category.
|
||||
|
||||
Args:
|
||||
name: Title of the category.
|
||||
items: List of tuples.
|
||||
"""
|
||||
cat = listcategory.ListCategory(name, items, parent=self)
|
||||
filtermodel = sortfilter.CompletionFilterModel(cat,
|
||||
self.columns_to_filter)
|
||||
self._categories.append(filtermodel)
|
||||
|
||||
def add_sqltable(self, name, *, select='*', where=None, sort_by=None,
|
||||
sort_order=None):
|
||||
"""Create a new completion category and add it to this model.
|
||||
|
||||
@ -112,13 +87,11 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
where: An optional clause to filter out some rows.
|
||||
sort_by: The name of the field to sort by, or None for no sorting.
|
||||
sort_order: Either 'asc' or 'desc', if sort_by is non-None
|
||||
|
||||
Return: A new CompletionCategory.
|
||||
"""
|
||||
cat = _SqlCompletionCategory(name, parent=self, sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
select=select, where=where,
|
||||
columns_to_filter=self.columns_to_filter)
|
||||
cat = sqlcategory.SqlCategory(name, parent=self, sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
select=select, where=where,
|
||||
columns_to_filter=self.columns_to_filter)
|
||||
self._categories.append(cat)
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
@ -135,11 +108,11 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
return None
|
||||
if not index.parent().isValid():
|
||||
if index.column() == 0:
|
||||
return self._categories[index.row()].tablename
|
||||
return self._categories[index.row()].name
|
||||
else:
|
||||
table = self._categories[index.parent().row()]
|
||||
idx = table.index(index.row(), index.column())
|
||||
return table.data(idx)
|
||||
cat = self._categories[index.parent().row()]
|
||||
idx = cat.index(index.row(), index.column())
|
||||
return cat.data(idx)
|
||||
|
||||
def flags(self, index):
|
||||
"""Return the item flags for index.
|
||||
@ -171,7 +144,7 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
if parent.isValid():
|
||||
if parent.column() != 0:
|
||||
return QModelIndex()
|
||||
# store a pointer to the parent table in internalPointer
|
||||
# store a pointer to the parent category in internalPointer
|
||||
return self.createIndex(row, col, self._categories[parent.row()])
|
||||
return self.createIndex(row, col, None)
|
||||
|
||||
@ -183,11 +156,11 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
Args:
|
||||
index: The QModelIndex to get the parent index for.
|
||||
"""
|
||||
parent_table = index.internalPointer()
|
||||
if not parent_table:
|
||||
parent_cat = index.internalPointer()
|
||||
if not parent_cat:
|
||||
# categories have no parent
|
||||
return QModelIndex()
|
||||
row = self._categories.index(parent_table)
|
||||
row = self._categories.index(parent_cat)
|
||||
return self.createIndex(row, 0, None)
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
@ -209,24 +182,24 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
return 3
|
||||
|
||||
def canFetchMore(self, parent):
|
||||
"""Override to forward the call to the tables."""
|
||||
"""Override to forward the call to the categories."""
|
||||
cat = self._cat_from_idx(parent)
|
||||
if cat:
|
||||
return cat.canFetchMore()
|
||||
return cat.canFetchMore(parent)
|
||||
return False
|
||||
|
||||
def fetchMore(self, parent):
|
||||
"""Override to forward the call to the tables."""
|
||||
"""Override to forward the call to the categories."""
|
||||
cat = self._cat_from_idx(parent)
|
||||
if cat:
|
||||
cat.fetchMore()
|
||||
cat.fetchMore(parent)
|
||||
|
||||
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 category tables.
|
||||
"""Set the filter pattern for all categories.
|
||||
|
||||
This will apply to the fields indicated in columns_to_filter.
|
||||
|
||||
@ -236,19 +209,13 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
log.completion.debug("Setting completion pattern '{}'".format(pattern))
|
||||
# TODO: should pattern be saved in the view layer instead?
|
||||
self.pattern = pattern
|
||||
# escape to treat a user input % or _ as a literal, not a wildcard
|
||||
pattern = pattern.replace('%', '\\%')
|
||||
pattern = pattern.replace('_', '\\_')
|
||||
# treat spaces as wildcards to match any of the typed words
|
||||
pattern = re.sub(r' +', '%', pattern)
|
||||
pattern = '%{}%'.format(pattern)
|
||||
for cat in self._categories:
|
||||
cat.set_pattern(pattern)
|
||||
|
||||
def first_item(self):
|
||||
"""Return the index of the first child (non-category) in the model."""
|
||||
for row, table in enumerate(self._categories):
|
||||
if table.rowCount() > 0:
|
||||
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)
|
||||
@ -257,18 +224,11 @@ class SqlCompletionModel(QAbstractItemModel):
|
||||
|
||||
def last_item(self):
|
||||
"""Return the index of the last child (non-category) in the model."""
|
||||
for row, table in reversed(list(enumerate(self._categories))):
|
||||
childcount = table.rowCount()
|
||||
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()
|
||||
|
||||
|
||||
class SqlException(Exception):
|
||||
|
||||
"""Raised on an error interacting with the SQL database."""
|
||||
|
||||
pass
|
@ -20,17 +20,16 @@
|
||||
"""Functions that return config-related completion models."""
|
||||
|
||||
from qutebrowser.config import configdata, configexc
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.completion.models import completionmodel
|
||||
from qutebrowser.utils import objreg
|
||||
|
||||
|
||||
def section():
|
||||
"""A CompletionModel filled with settings sections."""
|
||||
model = base.CompletionModel(column_widths=(20, 70, 10))
|
||||
cat = model.new_category("Sections")
|
||||
for name in configdata.DATA:
|
||||
desc = configdata.SECTION_DESC[name].splitlines()[0].strip()
|
||||
model.new_item(cat, name, desc)
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||
sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip())
|
||||
for name in configdata.DATA)
|
||||
model.add_list("Sections", sections)
|
||||
return model
|
||||
|
||||
|
||||
@ -40,12 +39,12 @@ def option(sectname):
|
||||
Args:
|
||||
sectname: The name of the config section this model shows.
|
||||
"""
|
||||
model = base.CompletionModel(column_widths=(20, 70, 10))
|
||||
cat = model.new_category(sectname)
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||
try:
|
||||
sectdata = configdata.DATA[sectname]
|
||||
except KeyError:
|
||||
return None
|
||||
options = []
|
||||
for name in sectdata:
|
||||
try:
|
||||
desc = sectdata.descriptions[name]
|
||||
@ -57,7 +56,8 @@ def option(sectname):
|
||||
desc = desc.splitlines()[0]
|
||||
config = objreg.get('config')
|
||||
val = config.get(sectname, name, raw=True)
|
||||
model.new_item(cat, name, desc, val)
|
||||
options.append((name, desc, val))
|
||||
model.add_list(sectname, options)
|
||||
return model
|
||||
|
||||
|
||||
@ -68,16 +68,16 @@ def value(sectname, optname):
|
||||
sectname: The name of the config section this model shows.
|
||||
optname: The name of the config option this model shows.
|
||||
"""
|
||||
model = base.CompletionModel(column_widths=(20, 70, 10))
|
||||
cur_cat = model.new_category("Current/Default")
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||
config = objreg.get('config')
|
||||
|
||||
try:
|
||||
val = config.get(sectname, optname, raw=True) or '""'
|
||||
current = config.get(sectname, optname, raw=True) or '""'
|
||||
except (configexc.NoSectionError, configexc.NoOptionError):
|
||||
return None
|
||||
model.new_item(cur_cat, val, "Current value")
|
||||
default_value = configdata.DATA[sectname][optname].default() or '""'
|
||||
model.new_item(cur_cat, default_value, "Default value")
|
||||
|
||||
default = configdata.DATA[sectname][optname].default() or '""'
|
||||
|
||||
if hasattr(configdata.DATA[sectname], 'valtype'):
|
||||
# Same type for all values (ValueList)
|
||||
vals = configdata.DATA[sectname].valtype.complete()
|
||||
@ -87,8 +87,9 @@ def value(sectname, optname):
|
||||
"sections, but {} is not!".format(sectname))
|
||||
# Different type for each value (KeyValue)
|
||||
vals = configdata.DATA[sectname][optname].typ.complete()
|
||||
|
||||
model.add_list("Current/Default", [(current, "Current value"),
|
||||
(default, "Default value")])
|
||||
if vals is not None:
|
||||
cat = model.new_category("Completions")
|
||||
for (val, desc) in vals:
|
||||
model.new_item(cat, val, desc)
|
||||
model.add_list("Completions", vals)
|
||||
return model
|
||||
|
66
qutebrowser/completion/models/listcategory.py
Normal file
66
qutebrowser/completion/models/listcategory.py
Normal file
@ -0,0 +1,66 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""The base completion model for completion in the command line.
|
||||
|
||||
Module attributes:
|
||||
Role: An enum of user defined model roles.
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
|
||||
class ListCategory(QStandardItemModel):
|
||||
|
||||
"""Expose a list of items as a category for the CompletionModel."""
|
||||
|
||||
def __init__(self, name, items, parent=None):
|
||||
super().__init__(parent)
|
||||
self.name = name
|
||||
# self.setColumnCount(3) TODO needed?
|
||||
# TODO: batch insert?
|
||||
# TODO: can I just insert a tuple instead of a list?
|
||||
for item in items:
|
||||
self.appendRow([QStandardItem(x) for x in item])
|
||||
|
||||
def flags(self, index):
|
||||
"""Return the item flags for index.
|
||||
|
||||
Override QAbstractItemModel::flags.
|
||||
|
||||
Args:
|
||||
index: The QModelIndex to get item flags for.
|
||||
|
||||
Return:
|
||||
The item flags, or Qt.NoItemFlags on error.
|
||||
"""
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
if index.parent().isValid():
|
||||
# item
|
||||
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
|
||||
Qt.ItemNeverHasChildren)
|
||||
else:
|
||||
# category
|
||||
return Qt.NoItemFlags
|
||||
|
||||
def set_pattern(self, pattern):
|
||||
pass
|
@ -22,30 +22,24 @@
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import objreg, log, qtutils
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.completion.models import base, sqlmodel
|
||||
from qutebrowser.completion.models import completionmodel
|
||||
|
||||
|
||||
def command():
|
||||
"""A CompletionModel filled with non-hidden commands and descriptions."""
|
||||
model = base.CompletionModel(column_widths=(20, 60, 20))
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
|
||||
cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False)
|
||||
cat = model.new_category("Commands")
|
||||
for (name, desc, misc) in cmdlist:
|
||||
model.new_item(cat, name, desc, misc)
|
||||
model.add_list("Commands", cmdlist)
|
||||
return model
|
||||
|
||||
|
||||
def helptopic():
|
||||
"""A CompletionModel filled with help topics."""
|
||||
model = base.CompletionModel()
|
||||
model = completionmodel.CompletionModel()
|
||||
|
||||
cmdlist = _get_cmd_completions(include_aliases=False, include_hidden=True,
|
||||
prefix=':')
|
||||
cat = model.new_category("Commands")
|
||||
for (name, desc, misc) in cmdlist:
|
||||
model.new_item(cat, name, desc, misc)
|
||||
|
||||
cat = model.new_category("Settings")
|
||||
settings = []
|
||||
for sectname, sectdata in configdata.DATA.items():
|
||||
for optname in sectdata:
|
||||
try:
|
||||
@ -57,34 +51,35 @@ def helptopic():
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
name = '{}->{}'.format(sectname, optname)
|
||||
model.new_item(cat, name, desc)
|
||||
settings.append((name, desc))
|
||||
|
||||
model.add_list("Commands", cmdlist)
|
||||
model.add_list("Settings", settings)
|
||||
return model
|
||||
|
||||
|
||||
def quickmark():
|
||||
"""A CompletionModel filled with all quickmarks."""
|
||||
model = base.CompletionModel()
|
||||
model = sqlmodel.SqlCompletionModel(column_widths=(30, 70, 0))
|
||||
model.new_category('Quickmarks')
|
||||
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
|
||||
model.add_sqltable('Quickmarks')
|
||||
return model
|
||||
|
||||
|
||||
def bookmark():
|
||||
"""A CompletionModel filled with all bookmarks."""
|
||||
model = base.CompletionModel()
|
||||
model = sqlmodel.SqlCompletionModel(column_widths=(30, 70, 0))
|
||||
model.new_category('Bookmarks')
|
||||
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
|
||||
model.add_sqltable('Bookmarks')
|
||||
return model
|
||||
|
||||
|
||||
def session():
|
||||
"""A CompletionModel filled with session names."""
|
||||
model = base.CompletionModel()
|
||||
cat = model.new_category("Sessions")
|
||||
model = completionmodel.CompletionModel()
|
||||
try:
|
||||
for name in objreg.get('session-manager').list_sessions():
|
||||
if not name.startswith('_'):
|
||||
model.new_item(cat, name)
|
||||
manager = objreg.get('session-manager')
|
||||
sessions = ((name,) for name in manager.list_sessions()
|
||||
if not name.startswith('_'))
|
||||
model.add_list("Sessions", sessions)
|
||||
except OSError:
|
||||
log.completion.exception("Failed to list sessions!")
|
||||
return model
|
||||
@ -111,7 +106,7 @@ def buffer():
|
||||
window=int(win_id))
|
||||
tabbed_browser.on_tab_close_requested(int(tab_index) - 1)
|
||||
|
||||
model = base.CompletionModel(
|
||||
model = completionmodel.CompletionModel(
|
||||
column_widths=(6, 40, 54),
|
||||
delete_cur_item=delete_buffer,
|
||||
columns_to_filter=[idx_column, url_column, text_column])
|
||||
@ -121,12 +116,13 @@ def buffer():
|
||||
window=win_id)
|
||||
if tabbed_browser.shutting_down:
|
||||
continue
|
||||
c = model.new_category("{}".format(win_id))
|
||||
tabs = []
|
||||
for idx in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(idx)
|
||||
model.new_item(c, "{}/{}".format(win_id, idx + 1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.page_title(idx))
|
||||
tabs.append(("{}/{}".format(win_id, idx + 1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.page_title(idx)))
|
||||
model.add_list("{}".format(win_id), tabs)
|
||||
return model
|
||||
|
||||
|
||||
@ -137,11 +133,9 @@ def bind(_key):
|
||||
_key: the key being bound.
|
||||
"""
|
||||
# TODO: offer a 'Current binding' completion based on the key.
|
||||
model = base.CompletionModel(column_widths=(20, 60, 20))
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
|
||||
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)
|
||||
cat = model.new_category("Commands")
|
||||
for (name, desc, misc) in cmdlist:
|
||||
model.new_item(cat, name, desc, misc)
|
||||
model.add_list("Commands", cmdlist)
|
||||
return model
|
||||
|
||||
|
||||
|
@ -41,12 +41,14 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
a long time for some reason.
|
||||
"""
|
||||
|
||||
def __init__(self, source, parent=None):
|
||||
def __init__(self, source, columns_to_filter, parent=None):
|
||||
super().__init__(parent)
|
||||
super().setSourceModel(source)
|
||||
self.srcmodel = source
|
||||
self.pattern = ''
|
||||
self.pattern_re = None
|
||||
self.columns_to_filter = columns_to_filter
|
||||
self.name = source.name
|
||||
|
||||
def set_pattern(self, val):
|
||||
"""Setter for pattern.
|
||||
@ -64,41 +66,6 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
sortcol = 0
|
||||
self.sort(sortcol)
|
||||
|
||||
def count(self):
|
||||
"""Get the count of non-toplevel items currently visible.
|
||||
|
||||
Note this only iterates one level deep, as we only need root items
|
||||
(categories) and children (items) in our model.
|
||||
"""
|
||||
count = 0
|
||||
for i in range(self.rowCount()):
|
||||
cat = self.index(i, 0)
|
||||
qtutils.ensure_valid(cat)
|
||||
count += self.rowCount(cat)
|
||||
return count
|
||||
|
||||
def first_item(self):
|
||||
"""Return the first item in the model."""
|
||||
for i in range(self.rowCount()):
|
||||
cat = self.index(i, 0)
|
||||
qtutils.ensure_valid(cat)
|
||||
if cat.model().hasChildren(cat):
|
||||
index = self.index(0, 0, cat)
|
||||
qtutils.ensure_valid(index)
|
||||
return index
|
||||
return QModelIndex()
|
||||
|
||||
def last_item(self):
|
||||
"""Return the last item in the model."""
|
||||
for i in range(self.rowCount() - 1, -1, -1):
|
||||
cat = self.index(i, 0)
|
||||
qtutils.ensure_valid(cat)
|
||||
if cat.model().hasChildren(cat):
|
||||
index = self.index(self.rowCount(cat) - 1, 0, cat)
|
||||
qtutils.ensure_valid(index)
|
||||
return index
|
||||
return QModelIndex()
|
||||
|
||||
def setSourceModel(self, model):
|
||||
"""Override QSortFilterProxyModel's setSourceModel to clear pattern."""
|
||||
log.completion.debug("Setting source model: {}".format(model))
|
||||
@ -119,10 +86,10 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
True if self.pattern is contained in item, or if it's a root item
|
||||
(category). False in all other cases
|
||||
"""
|
||||
if parent == QModelIndex() or not self.pattern:
|
||||
if not self.pattern:
|
||||
return True
|
||||
|
||||
for col in self.srcmodel.columns_to_filter:
|
||||
for col in self.columns_to_filter:
|
||||
idx = self.srcmodel.index(row, col, parent)
|
||||
if not idx.isValid(): # pragma: no cover
|
||||
# this is a sanity check not hit by any test case
|
||||
|
64
qutebrowser/completion/models/sqlcategory.py
Normal file
64
qutebrowser/completion/models/sqlcategory.py
Normal file
@ -0,0 +1,64 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""A completion model backed by SQL tables."""
|
||||
|
||||
import re
|
||||
|
||||
from PyQt5.QtSql import QSqlQueryModel
|
||||
|
||||
from qutebrowser.misc import sql
|
||||
|
||||
|
||||
class SqlCategory(QSqlQueryModel):
|
||||
"""Wraps a SqlQuery for use as a completion category."""
|
||||
|
||||
def __init__(self, name, *, sort_by, sort_order, select, where,
|
||||
columns_to_filter, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.name = name
|
||||
|
||||
query = sql.run_query('select * from {} limit 1'.format(name))
|
||||
self._fields = [query.record().fieldName(i) for i in columns_to_filter]
|
||||
|
||||
querystr = 'select {} from {} where ('.format(select, name)
|
||||
# the incoming pattern will have literal % and _ escaped with '\'
|
||||
# we need to tell sql to treat '\' as an escape character
|
||||
querystr += ' or '.join("{} like ? escape '\\'".format(f)
|
||||
for f in self._fields)
|
||||
querystr += ')'
|
||||
if where:
|
||||
querystr += ' and ' + where
|
||||
|
||||
if sort_by:
|
||||
assert sort_order == 'asc' or sort_order == 'desc'
|
||||
querystr += ' order by {} {}'.format(sort_by, sort_order)
|
||||
|
||||
self._querystr = querystr
|
||||
self.set_pattern('')
|
||||
|
||||
def set_pattern(self, pattern):
|
||||
# escape to treat a user input % or _ as a literal, not a wildcard
|
||||
pattern = pattern.replace('%', '\\%')
|
||||
pattern = pattern.replace('_', '\\_')
|
||||
# treat spaces as wildcards to match any of the typed words
|
||||
pattern = re.sub(r' +', '%', pattern)
|
||||
pattern = '%{}%'.format(pattern)
|
||||
query = sql.run_query(self._querystr, [pattern for _ in self._fields])
|
||||
self.setQuery(query)
|
@ -19,7 +19,7 @@
|
||||
|
||||
"""Function to return the url completion model for the `open` command."""
|
||||
|
||||
from qutebrowser.completion.models import sqlmodel
|
||||
from qutebrowser.completion.models import completionmodel
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import qtutils, log, objreg
|
||||
|
||||
@ -61,14 +61,16 @@ def url():
|
||||
|
||||
Used for the `open` command.
|
||||
"""
|
||||
model = sqlmodel.SqlCompletionModel(column_widths=(40, 50, 10),
|
||||
columns_to_filter=[_URLCOL, _TEXTCOL],
|
||||
delete_cur_item=_delete_url)
|
||||
model = completionmodel.CompletionModel(
|
||||
column_widths=(40, 50, 10),
|
||||
columns_to_filter=[_URLCOL, _TEXTCOL],
|
||||
delete_cur_item=_delete_url)
|
||||
|
||||
timefmt = config.get('completion', 'timestamp-format')
|
||||
select_time = "strftime('{}', atime, 'unixepoch')".format(timefmt)
|
||||
model.new_category('Quickmarks', select='url, name')
|
||||
model.new_category('Bookmarks')
|
||||
model.new_category('History',
|
||||
model.add_sqltable('Quickmarks', select='url, name')
|
||||
model.add_sqltable('Bookmarks')
|
||||
model.add_sqltable('History',
|
||||
sort_order='desc', sort_by='atime',
|
||||
select='url, title, {}'.format(select_time),
|
||||
where='not redirect')
|
||||
|
146
tests/unit/completion/test_completionmodel.py
Normal file
146
tests/unit/completion/test_completionmodel.py
Normal file
@ -0,0 +1,146 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Tests for CompletionModel."""
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.completion.models import completionmodel, sortfilter
|
||||
|
||||
|
||||
def _create_model(data, filter_cols=None):
|
||||
"""Create a completion model populated with the given data.
|
||||
|
||||
data: A list of lists, where each sub-list represents a category, each
|
||||
tuple in the sub-list represents an item, and each value in the
|
||||
tuple represents the item data for that column
|
||||
filter_cols: Columns to filter, or None for default.
|
||||
"""
|
||||
model = completionmodel.CompletionModel(columns_to_filter=filter_cols)
|
||||
for catdata in data:
|
||||
model.add_list('', catdata)
|
||||
return model
|
||||
|
||||
|
||||
def _extract_model_data(model):
|
||||
"""Express a model's data as a list for easier comparison.
|
||||
|
||||
Return: A list of lists, where each sub-list represents a category, each
|
||||
tuple in the sub-list represents an item, and each value in the
|
||||
tuple represents the item data for that column
|
||||
"""
|
||||
data = []
|
||||
for i in range(0, model.rowCount()):
|
||||
cat_idx = model.index(i, 0)
|
||||
row = []
|
||||
for j in range(0, model.rowCount(cat_idx)):
|
||||
row.append((model.data(cat_idx.child(j, 0)),
|
||||
model.data(cat_idx.child(j, 1)),
|
||||
model.data(cat_idx.child(j, 2))))
|
||||
data.append(row)
|
||||
return data
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, first, last', [
|
||||
([[('Aa',)]], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [('Ba',)]], 'Aa', 'Ba'),
|
||||
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]],
|
||||
'Aa', 'Ca'),
|
||||
([[], [('Ba',)]], 'Ba', 'Ba'),
|
||||
([[], [], [('Ca',)]], 'Ca', 'Ca'),
|
||||
([[], [], [('Ca',), ('Cb',)]], 'Ca', 'Cb'),
|
||||
([[('Aa',)], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [], [('Ca',)]], 'Aa', 'Ca'),
|
||||
([[], []], None, None),
|
||||
])
|
||||
def test_first_last_item(tree, first, last):
|
||||
"""Test that first() and last() return indexes to the first and last items.
|
||||
|
||||
Args:
|
||||
tree: Each list represents a completion category, with each string
|
||||
being an item under that category.
|
||||
first: text of the first item
|
||||
last: text of the last item
|
||||
"""
|
||||
model = _create_model(tree)
|
||||
assert model.data(model.first_item()) == first
|
||||
assert model.data(model.last_item()) == last
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, expected', [
|
||||
([[('Aa',)]], 1),
|
||||
([[('Aa',)], [('Ba',)]], 2),
|
||||
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]], 6),
|
||||
([[], [('Ba',)]], 1),
|
||||
([[], [], [('Ca',)]], 1),
|
||||
([[], [], [('Ca',), ('Cb',)]], 2),
|
||||
([[('Aa',)], []], 1),
|
||||
([[('Aa',)], []], 1),
|
||||
([[('Aa',)], [], []], 1),
|
||||
([[('Aa',)], [], [('Ca',)]], 2),
|
||||
])
|
||||
def test_count(tree, expected):
|
||||
model = _create_model(tree)
|
||||
assert model.count() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
|
||||
('foo', [0],
|
||||
[[('foo', '', ''), ('bar', '', '')]],
|
||||
[[('foo', '', '')]]),
|
||||
|
||||
('foo', [0],
|
||||
[[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]],
|
||||
[[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]),
|
||||
|
||||
('foo', [0],
|
||||
[[('foo', '', '')], [('bar', '', '')]],
|
||||
[[('foo', '', '')], []]),
|
||||
|
||||
# prefer foobar as it starts with the pattern
|
||||
('foo', [0],
|
||||
[[('barfoo', '', ''), ('foobar', '', '')]],
|
||||
[[('foobar', '', ''), ('barfoo', '', '')]]),
|
||||
|
||||
# however, don't rearrange categories
|
||||
('foo', [0],
|
||||
[[('barfoo', '', '')], [('foobar', '', '')]],
|
||||
[[('barfoo', '', '')], [('foobar', '', '')]]),
|
||||
|
||||
('foo', [1],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', '')]],
|
||||
[[('bar', 'foo', '')]]),
|
||||
|
||||
('foo', [0, 1],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', '')]]),
|
||||
|
||||
('foo', [0, 1, 2],
|
||||
[[('foo', '', ''), ('bar', '')]],
|
||||
[[('foo', '', '')]]),
|
||||
])
|
||||
def test_set_pattern(pattern, filter_cols, before, after):
|
||||
"""Validate the filtering and sorting results of set_pattern."""
|
||||
# TODO: just test that it calls the mock on its child categories
|
||||
model = _create_model(before, filter_cols)
|
||||
model.set_pattern(pattern)
|
||||
actual = _extract_model_data(model)
|
||||
assert actual == after
|
@ -25,7 +25,7 @@ import pytest
|
||||
from PyQt5.QtGui import QStandardItem, QColor
|
||||
|
||||
from qutebrowser.completion import completionwidget
|
||||
from qutebrowser.completion.models import base, sortfilter
|
||||
from qutebrowser.completion.models import completionmodel
|
||||
from qutebrowser.commands import cmdexc
|
||||
|
||||
|
||||
@ -72,28 +72,25 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
|
||||
|
||||
@pytest.fixture
|
||||
def simplemodel(completionview):
|
||||
"""A filter model wrapped around a completion model with one item."""
|
||||
model = base.CompletionModel()
|
||||
cat = QStandardItem()
|
||||
cat.appendRow(QStandardItem('foo'))
|
||||
model.appendRow(cat)
|
||||
return sortfilter.CompletionFilterModel(model, parent=completionview)
|
||||
"""A completion model with one item."""
|
||||
model = completionmodel.CompletionModel()
|
||||
model.add_list('', [('foo'),])
|
||||
return model
|
||||
|
||||
|
||||
def test_set_model(completionview):
|
||||
"""Ensure set_model actually sets the model and expands all categories."""
|
||||
model = base.CompletionModel()
|
||||
filtermodel = sortfilter.CompletionFilterModel(model)
|
||||
model = completionmodel.CompletionModel()
|
||||
for i in range(3):
|
||||
model.appendRow(QStandardItem(str(i)))
|
||||
completionview.set_model(filtermodel)
|
||||
assert completionview.model() is filtermodel
|
||||
model.add_list(str(i), [('foo',)])
|
||||
completionview.set_model(model)
|
||||
assert completionview.model() is model
|
||||
for i in range(model.rowCount()):
|
||||
assert completionview.isExpanded(filtermodel.index(i, 0))
|
||||
assert completionview.isExpanded(model.index(i, 0))
|
||||
|
||||
|
||||
def test_set_pattern(completionview):
|
||||
model = sortfilter.CompletionFilterModel(base.CompletionModel())
|
||||
model = completionmodel.CompletionModel()
|
||||
model.set_pattern = unittest.mock.Mock()
|
||||
completionview.set_model(model, 'foo')
|
||||
model.set_pattern.assert_called_with('foo')
|
||||
@ -159,15 +156,10 @@ def test_completion_item_focus(which, tree, expected, completionview, qtbot):
|
||||
successive movement. None implies no signal should be
|
||||
emitted.
|
||||
"""
|
||||
model = base.CompletionModel()
|
||||
model = completionmodel.CompletionModel()
|
||||
for catdata in tree:
|
||||
cat = QStandardItem()
|
||||
model.appendRow(cat)
|
||||
for name in catdata:
|
||||
cat.appendRow(QStandardItem(name))
|
||||
filtermodel = sortfilter.CompletionFilterModel(model,
|
||||
parent=completionview)
|
||||
completionview.set_model(filtermodel)
|
||||
model.add_list('', (x,) for x in catdata)
|
||||
completionview.set_model(model)
|
||||
for entry in expected:
|
||||
if entry is None:
|
||||
with qtbot.assertNotEmitted(completionview.selection_changed):
|
||||
@ -187,10 +179,8 @@ def test_completion_item_focus_no_model(which, completionview, qtbot):
|
||||
"""
|
||||
with qtbot.assertNotEmitted(completionview.selection_changed):
|
||||
completionview.completion_item_focus(which)
|
||||
model = base.CompletionModel()
|
||||
filtermodel = sortfilter.CompletionFilterModel(model,
|
||||
parent=completionview)
|
||||
completionview.set_model(filtermodel)
|
||||
model = completionmodel.CompletionModel()
|
||||
completionview.set_model(model)
|
||||
completionview.set_model(None)
|
||||
with qtbot.assertNotEmitted(completionview.selection_changed):
|
||||
completionview.completion_item_focus(which)
|
||||
@ -211,16 +201,12 @@ def test_completion_show(show, rows, quick_complete, completionview,
|
||||
config_stub.data['completion']['show'] = show
|
||||
config_stub.data['completion']['quick-complete'] = quick_complete
|
||||
|
||||
model = base.CompletionModel()
|
||||
model = completionmodel.CompletionModel()
|
||||
for name in rows:
|
||||
cat = QStandardItem()
|
||||
model.appendRow(cat)
|
||||
cat.appendRow(QStandardItem(name))
|
||||
filtermodel = sortfilter.CompletionFilterModel(model,
|
||||
parent=completionview)
|
||||
model.add_list('', [(name,)])
|
||||
|
||||
assert not completionview.isVisible()
|
||||
completionview.set_model(filtermodel)
|
||||
completionview.set_model(model)
|
||||
assert completionview.isVisible() == (show == 'always' and len(rows) > 0)
|
||||
completionview.completion_item_focus('next')
|
||||
expected = (show != 'never' and len(rows) > 0 and
|
||||
|
@ -228,12 +228,12 @@ def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub):
|
||||
(':hide', '', ''),
|
||||
],
|
||||
"Settings": [
|
||||
('general->time', 'Is an illusion.', ''),
|
||||
('general->volume', 'Goes to 11', ''),
|
||||
('ui->gesture', 'Waggle your hands to control qutebrowser', ''),
|
||||
('ui->mind', 'Enable mind-control ui (experimental)', ''),
|
||||
('ui->voice', 'Whether to respond to voice commands', ''),
|
||||
('searchengines->DEFAULT', '', ''),
|
||||
('general->time', 'Is an illusion.', None),
|
||||
('general->volume', 'Goes to 11', None),
|
||||
('ui->gesture', 'Waggle your hands to control qutebrowser', None),
|
||||
('ui->mind', 'Enable mind-control ui (experimental)', None),
|
||||
('ui->voice', 'Whether to respond to voice commands', None),
|
||||
('searchengines->DEFAULT', '', None),
|
||||
]
|
||||
})
|
||||
|
||||
@ -342,7 +342,9 @@ def test_session_completion(qtmodeltester, session_manager_stub):
|
||||
qtmodeltester.check(model)
|
||||
|
||||
_check_completions(model, {
|
||||
"Sessions": [('default', '', ''), ('1', '', ''), ('2', '', '')]
|
||||
"Sessions": [('default', None, None),
|
||||
('1', None, None),
|
||||
('2', None, None)]
|
||||
})
|
||||
|
||||
|
||||
@ -406,9 +408,9 @@ def test_setting_section_completion(qtmodeltester, monkeypatch, stubs):
|
||||
|
||||
_check_completions(model, {
|
||||
"Sections": [
|
||||
('general', 'General/miscellaneous options.', ''),
|
||||
('ui', 'General options related to the user interface.', ''),
|
||||
('searchengines', 'Definitions of search engines ...', ''),
|
||||
('general', 'General/miscellaneous options.', None),
|
||||
('ui', 'General options related to the user interface.', None),
|
||||
('searchengines', 'Definitions of search engines ...', None),
|
||||
]
|
||||
})
|
||||
|
||||
@ -462,12 +464,12 @@ def test_setting_value_completion(qtmodeltester, monkeypatch, stubs,
|
||||
|
||||
_check_completions(model, {
|
||||
"Current/Default": [
|
||||
('0', 'Current value', ''),
|
||||
('11', 'Default value', ''),
|
||||
('0', 'Current value', None),
|
||||
('11', 'Default value', None),
|
||||
],
|
||||
"Completions": [
|
||||
('0', '', ''),
|
||||
('11', '', ''),
|
||||
('0', '', None),
|
||||
('11', '', None),
|
||||
]
|
||||
})
|
||||
|
||||
|
@ -21,9 +21,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.completion.models import base, sortfilter
|
||||
from qutebrowser.completion.models import listcategory, sortfilter
|
||||
|
||||
|
||||
# TODO: merge listcategory and sortfilter
|
||||
def _create_model(data):
|
||||
"""Create a completion model populated with the given data.
|
||||
|
||||
@ -31,11 +32,9 @@ def _create_model(data):
|
||||
tuple in the sub-list represents an item, and each value in the
|
||||
tuple represents the item data for that column
|
||||
"""
|
||||
model = base.CompletionModel()
|
||||
model = completionmodel.CompletionModel()
|
||||
for catdata in data:
|
||||
cat = model.new_category('')
|
||||
for itemdata in catdata:
|
||||
model.new_item(cat, *itemdata)
|
||||
cat = model.add_list(itemdata)
|
||||
return model
|
||||
|
||||
|
||||
@ -72,7 +71,7 @@ def _extract_model_data(model):
|
||||
('4', 'blah', False),
|
||||
])
|
||||
def test_filter_accepts_row(pattern, data, expected):
|
||||
source_model = base.CompletionModel()
|
||||
source_model = completionmodel.CompletionModel()
|
||||
cat = source_model.new_category('test')
|
||||
source_model.new_item(cat, data)
|
||||
|
||||
@ -86,35 +85,6 @@ def test_filter_accepts_row(pattern, data, expected):
|
||||
assert row_count == (1 if expected else 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, first, last', [
|
||||
([[('Aa',)]], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [('Ba',)]], 'Aa', 'Ba'),
|
||||
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]],
|
||||
'Aa', 'Ca'),
|
||||
([[], [('Ba',)]], 'Ba', 'Ba'),
|
||||
([[], [], [('Ca',)]], 'Ca', 'Ca'),
|
||||
([[], [], [('Ca',), ('Cb',)]], 'Ca', 'Cb'),
|
||||
([[('Aa',)], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [], [('Ca',)]], 'Aa', 'Ca'),
|
||||
([[], []], None, None),
|
||||
])
|
||||
def test_first_last_item(tree, first, last):
|
||||
"""Test that first() and last() return indexes to the first and last items.
|
||||
|
||||
Args:
|
||||
tree: Each list represents a completion category, with each string
|
||||
being an item under that category.
|
||||
first: text of the first item
|
||||
last: text of the last item
|
||||
"""
|
||||
model = _create_model(tree)
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
assert filter_model.data(filter_model.first_item()) == first
|
||||
assert filter_model.data(filter_model.last_item()) == last
|
||||
|
||||
|
||||
def test_set_source_model():
|
||||
"""Ensure setSourceModel sets source_model and clears the pattern."""
|
||||
model1 = base.CompletionModel()
|
||||
@ -131,24 +101,6 @@ def test_set_source_model():
|
||||
assert not filter_model.pattern
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, expected', [
|
||||
([[('Aa',)]], 1),
|
||||
([[('Aa',)], [('Ba',)]], 2),
|
||||
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]], 6),
|
||||
([[], [('Ba',)]], 1),
|
||||
([[], [], [('Ca',)]], 1),
|
||||
([[], [], [('Ca',), ('Cb',)]], 2),
|
||||
([[('Aa',)], []], 1),
|
||||
([[('Aa',)], []], 1),
|
||||
([[('Aa',)], [], []], 1),
|
||||
([[('Aa',)], [], [('Ca',)]], 2),
|
||||
])
|
||||
def test_count(tree, expected):
|
||||
model = _create_model(tree)
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
assert filter_model.count() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
|
||||
('foo', [0],
|
||||
[[('foo', '', ''), ('bar', '', '')]],
|
||||
|
Loading…
Reference in New Issue
Block a user