diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py
index f403354c3..94c7f9352 100644
--- a/qutebrowser/completion/completer.py
+++ b/qutebrowser/completion/completer.py
@@ -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
diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py
index 1d5dfadf0..776b2164c 100644
--- a/qutebrowser/completion/completiondelegate.py
+++ b/qutebrowser/completion/completiondelegate.py
@@ -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'\g<0>'
text = re.sub(re.escape(pattern).replace(r'\ ', r'|'),
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
index 3d2aef27e..f6a980612 100644
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -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)
diff --git a/qutebrowser/completion/models/base.py b/qutebrowser/completion/models/base.py
deleted file mode 100644
index f5148ff9c..000000000
--- a/qutebrowser/completion/models/base.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2014-2017 Florian Bruhin (The Compiler)
-#
-# 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 .
-
-"""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
diff --git a/qutebrowser/completion/models/sqlmodel.py b/qutebrowser/completion/models/completionmodel.py
similarity index 63%
rename from qutebrowser/completion/models/sqlmodel.py
rename to qutebrowser/completion/models/completionmodel.py
index e3e5a5c5f..b8dcc4601 100644
--- a/qutebrowser/completion/models/sqlmodel.py
+++ b/qutebrowser/completion/models/completionmodel.py
@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-# Copyright 2016-2017 Ryan Roden-Corrent (rcorre)
+# Copyright 2017 Ryan Roden-Corrent (rcorre)
#
# 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 .
-"""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
diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py
index 7c7e3a0c6..3cb46d42d 100644
--- a/qutebrowser/completion/models/configmodel.py
+++ b/qutebrowser/completion/models/configmodel.py
@@ -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
diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py
new file mode 100644
index 000000000..7b222c3c0
--- /dev/null
+++ b/qutebrowser/completion/models/listcategory.py
@@ -0,0 +1,66 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2017 Ryan Roden-Corrent (rcorre)
+#
+# 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 .
+
+"""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
diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py
index f5ed72f5d..d07ffaa88 100644
--- a/qutebrowser/completion/models/miscmodels.py
+++ b/qutebrowser/completion/models/miscmodels.py
@@ -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
diff --git a/qutebrowser/completion/models/sortfilter.py b/qutebrowser/completion/models/sortfilter.py
index 92d9aeaf1..fb79d002c 100644
--- a/qutebrowser/completion/models/sortfilter.py
+++ b/qutebrowser/completion/models/sortfilter.py
@@ -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
diff --git a/qutebrowser/completion/models/sqlcategory.py b/qutebrowser/completion/models/sqlcategory.py
new file mode 100644
index 000000000..12e0534c1
--- /dev/null
+++ b/qutebrowser/completion/models/sqlcategory.py
@@ -0,0 +1,64 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2017 Ryan Roden-Corrent (rcorre)
+#
+# 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 .
+
+"""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)
diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py
index 0b357c3d7..a38e107d2 100644
--- a/qutebrowser/completion/models/urlmodel.py
+++ b/qutebrowser/completion/models/urlmodel.py
@@ -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')
diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py
new file mode 100644
index 000000000..4c1241d12
--- /dev/null
+++ b/tests/unit/completion/test_completionmodel.py
@@ -0,0 +1,146 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2017 Ryan Roden-Corrent (rcorre)
+#
+# 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 .
+
+"""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
diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py
index 2c8d96713..3db4dca5a 100644
--- a/tests/unit/completion/test_completionwidget.py
+++ b/tests/unit/completion/test_completionwidget.py
@@ -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
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index 224d7ed59..6d68cc8ab 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -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),
]
})
diff --git a/tests/unit/completion/test_sortfilter.py b/tests/unit/completion/test_sortfilter.py
index 92d644810..7dadaa9dd 100644
--- a/tests/unit/completion/test_sortfilter.py
+++ b/tests/unit/completion/test_sortfilter.py
@@ -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', '', '')]],