Implement completion models as functions.

First step of Completion Model/View revamping (#74). Rewrite the
completion models as functions that each return an instance of a
CompletionModel class.

Caching is removed from all models except the UrlModel. Models other
than the UrlModel can be generated very quickly so caching just adds
needless complexity and can lead to incorrect results if one forgets to
wire up a signal.
This commit is contained in:
Ryan Roden-Corrent 2016-09-16 15:41:54 -04:00
parent bef372e5f5
commit 08bb3f4f19
13 changed files with 431 additions and 814 deletions

View File

@ -72,20 +72,7 @@ class Completer(QObject):
Return: Return:
A completion model or None. A completion model or None.
""" """
if completion == usertypes.Completion.option: model = instances.get(completion)(*pos_args)
section = pos_args[0]
model = instances.get(completion).get(section)
elif completion == usertypes.Completion.value:
section = pos_args[0]
option = pos_args[1]
try:
model = instances.get(completion)[section][option]
except KeyError:
# No completion model for this section/option.
model = None
else:
model = instances.get(completion)
if model is None: if model is None:
return None return None
else: else:
@ -109,7 +96,7 @@ class Completer(QObject):
log.completion.debug("After removing flags: {}".format(before_cursor)) log.completion.debug("After removing flags: {}".format(before_cursor))
if not before_cursor: if not before_cursor:
# '|' or 'set|' # '|' or 'set|'
model = instances.get(usertypes.Completion.command) model = instances.get(usertypes.Completion.command)()
return sortfilter.CompletionFilterModel(source=model, parent=self) return sortfilter.CompletionFilterModel(source=model, parent=self)
try: try:
cmd = cmdutils.cmd_dict[before_cursor[0]] cmd = cmdutils.cmd_dict[before_cursor[0]]

View File

@ -28,7 +28,6 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config, style from qutebrowser.config import config, style
from qutebrowser.completion import completiondelegate from qutebrowser.completion import completiondelegate
from qutebrowser.completion.models import base
from qutebrowser.utils import utils, usertypes, objreg from qutebrowser.utils import utils, usertypes, objreg
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
@ -112,7 +111,7 @@ class CompletionView(QTreeView):
# objreg.get('config').changed.connect(self.init_command_completion) # objreg.get('config').changed.connect(self.init_command_completion)
objreg.get('config').changed.connect(self._on_config_changed) objreg.get('config').changed.connect(self._on_config_changed)
self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS self._column_widths = (30, 70, 0)
self._active = False self._active = False
self._delegate = completiondelegate.CompletionItemDelegate(self) self._delegate = completiondelegate.CompletionItemDelegate(self)
@ -300,7 +299,7 @@ class CompletionView(QTreeView):
if pattern is not None: if pattern is not None:
model.set_pattern(pattern) model.set_pattern(pattern)
self._column_widths = model.srcmodel.COLUMN_WIDTHS self._column_widths = model.srcmodel.column_widths
self._resize_columns() self._resize_columns()
self._maybe_update_geometry() self._maybe_update_geometry()

View File

@ -33,26 +33,27 @@ Role = usertypes.enum('Role', ['sort', 'userdata'], start=Qt.UserRole,
is_int=True) is_int=True)
class BaseCompletionModel(QStandardItemModel): class CompletionModel(QStandardItemModel):
"""A simple QStandardItemModel adopted for completions. """A simple QStandardItemModel adopted for completions.
Used for showing completions later in the CompletionView. Supports setting Used for showing completions later in the CompletionView. Supports setting
marks and adding new categories/items easily. marks and adding new categories/items easily.
Class Attributes: Attributes:
COLUMN_WIDTHS: The width percentages of the columns used in the column_widths: The width percentages of the columns used in the
completion view. completion view.
DUMB_SORT: the dumb sorting used by the model dumb_sort: the dumb sorting used by the model
""" """
COLUMN_WIDTHS = (30, 70, 0) def __init__(self, dumb_sort=None, column_widths=(30, 70, 0),
DUMB_SORT = None columns_to_filter=None, delete_cur_item=None, parent=None):
def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setColumnCount(3) self.setColumnCount(3)
self.columns_to_filter = [0] self.columns_to_filter = columns_to_filter or [0]
self.dumb_sort = dumb_sort
self.column_widths = column_widths
self.delete_cur_item = delete_cur_item
def new_category(self, name, sort=None): def new_category(self, name, sort=None):
"""Add a new category to the model. """Add a new category to the model.
@ -103,10 +104,6 @@ class BaseCompletionModel(QStandardItemModel):
nameitem.setData(userdata, Role.userdata) nameitem.setData(userdata, Role.userdata)
return nameitem, descitem, miscitem return nameitem, descitem, miscitem
def delete_cur_item(self, completion):
"""Delete the selected item."""
raise NotImplementedError
def flags(self, index): def flags(self, index):
"""Return the item flags for index. """Return the item flags for index.

View File

@ -17,53 +17,34 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""CompletionModels for the config.""" """Functions that return config-related completion models."""
from PyQt5.QtCore import pyqtSlot, Qt from qutebrowser.config import config, configdata, configexc
from qutebrowser.config import config, configdata
from qutebrowser.utils import log, qtutils, objreg
from qutebrowser.completion.models import base from qutebrowser.completion.models import base
class SettingSectionCompletionModel(base.BaseCompletionModel): def section():
"""A CompletionModel filled with settings sections.""" """A CompletionModel filled with settings sections."""
model = base.CompletionModel(column_widths=(20, 70, 10))
# https://github.com/qutebrowser/qutebrowser/issues/545 cat = model.new_category("Sections")
# pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 70, 10)
def __init__(self, parent=None):
super().__init__(parent)
cat = self.new_category("Sections")
for name in configdata.DATA: for name in configdata.DATA:
desc = configdata.SECTION_DESC[name].splitlines()[0].strip() desc = configdata.SECTION_DESC[name].splitlines()[0].strip()
self.new_item(cat, name, desc) model.new_item(cat, name, desc)
return model
class SettingOptionCompletionModel(base.BaseCompletionModel): def option(sectname):
"""A CompletionModel filled with settings and their descriptions. """A CompletionModel filled with settings and their descriptions.
Attributes: Args:
_misc_items: A dict of the misc. column items which will be set later. sectname: The name of the config section this model shows.
_section: The config section this model shows.
""" """
model = base.CompletionModel(column_widths=(20, 70, 10))
# https://github.com/qutebrowser/qutebrowser/issues/545 cat = model.new_category(sectname)
# pylint: disable=abstract-method try:
sectdata = configdata.DATA[sectname]
COLUMN_WIDTHS = (20, 70, 10) except KeyError:
return None
def __init__(self, section, parent=None):
super().__init__(parent)
cat = self.new_category(section)
sectdata = configdata.DATA[section]
self._misc_items = {}
self._section = section
objreg.get('config').changed.connect(self.update_misc_column)
for name in sectdata: for name in sectdata:
try: try:
desc = sectdata.descriptions[name] desc = sectdata.descriptions[name]
@ -73,86 +54,38 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
desc = "" desc = ""
else: else:
desc = desc.splitlines()[0] desc = desc.splitlines()[0]
value = config.get(section, name, raw=True) val = config.get(sectname, name, raw=True)
_valitem, _descitem, miscitem = self.new_item(cat, name, desc, model.new_item(cat, name, desc, val)
value) return model
self._misc_items[name] = miscitem
@pyqtSlot(str, str)
def update_misc_column(self, section, option):
"""Update misc column when config changed."""
if section != self._section:
return
try:
item = self._misc_items[option]
except KeyError:
log.completion.debug("Couldn't get item {}.{} from model!".format(
section, option))
# changed before init
return
val = config.get(section, option, raw=True)
idx = item.index()
qtutils.ensure_valid(idx)
ok = self.setData(idx, val, Qt.DisplayRole)
if not ok:
raise ValueError("Setting data failed! (section: {}, option: {}, "
"value: {})".format(section, option, val))
class SettingValueCompletionModel(base.BaseCompletionModel): def value(sectname, optname):
"""A CompletionModel filled with setting values. """A CompletionModel filled with setting values.
Attributes: Args:
_section: The config section this model shows. sectname: The name of the config section this model shows.
_option: The config option this model shows. optname: The name of the config option this model shows.
""" """
model = base.CompletionModel(column_widths=(20, 70, 10))
# https://github.com/qutebrowser/qutebrowser/issues/545 cur_cat = model.new_category("Current/Default", sort=0)
# pylint: disable=abstract-method try:
val = config.get(sectname, optname, raw=True) or '""'
COLUMN_WIDTHS = (20, 70, 10) except (configexc.NoSectionError, configexc.NoOptionError):
return None
def __init__(self, section, option, parent=None): model.new_item(cur_cat, val, "Current value")
super().__init__(parent) default_value = configdata.DATA[sectname][optname].default() or '""'
self._section = section model.new_item(cur_cat, default_value, "Default value")
self._option = option if hasattr(configdata.DATA[sectname], 'valtype'):
objreg.get('config').changed.connect(self.update_current_value)
cur_cat = self.new_category("Current/Default", sort=0)
value = config.get(section, option, raw=True)
if not value:
value = '""'
self.cur_item, _descitem, _miscitem = self.new_item(cur_cat, value,
"Current value")
default_value = configdata.DATA[section][option].default()
if not default_value:
default_value = '""'
self.new_item(cur_cat, default_value, "Default value")
if hasattr(configdata.DATA[section], 'valtype'):
# Same type for all values (ValueList) # Same type for all values (ValueList)
vals = configdata.DATA[section].valtype.complete() vals = configdata.DATA[sectname].valtype.complete()
else: else:
if option is None: if optname is None:
raise ValueError("option may only be None for ValueList " raise ValueError("optname may only be None for ValueList "
"sections, but {} is not!".format(section)) "sections, but {} is not!".format(sectname))
# Different type for each value (KeyValue) # Different type for each value (KeyValue)
vals = configdata.DATA[section][option].typ.complete() vals = configdata.DATA[sectname][optname].typ.complete()
if vals is not None: if vals is not None:
cat = self.new_category("Completions", sort=1) cat = model.new_category("Completions", sort=1)
for (val, desc) in vals: for (val, desc) in vals:
self.new_item(cat, val, desc) model.new_item(cat, val, desc)
return model
@pyqtSlot(str, str)
def update_current_value(self, section, option):
"""Update current value when config changed."""
if (section, option) != (self._section, self._option):
return
value = config.get(section, option, raw=True)
if not value:
value = '""'
idx = self.cur_item.index()
qtutils.ensure_valid(idx)
ok = self.setData(idx, value, Qt.DisplayRole)
if not ok:
raise ValueError("Setting data failed! (section: {}, option: {}, "
"value: {})".format(section, option, value))

View File

@ -17,180 +17,37 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Global instances of the completion models. """Global instances of the completion models."""
Module attributes:
_instances: A dict of available completions.
INITIALIZERS: A {usertypes.Completion: callable} dict of functions to
initialize completions.
"""
import functools
from qutebrowser.utils import usertypes
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
from qutebrowser.utils import objreg, usertypes, log, debug
from qutebrowser.config import configdata, config
_instances = {}
def _init_command_completion():
"""Initialize the command completion model."""
log.completion.debug("Initializing command completion.")
model = miscmodels.CommandCompletionModel()
_instances[usertypes.Completion.command] = model
def _init_helptopic_completion():
"""Initialize the helptopic completion model."""
log.completion.debug("Initializing helptopic completion.")
model = miscmodels.HelpCompletionModel()
_instances[usertypes.Completion.helptopic] = model
def _init_url_completion():
"""Initialize the URL completion model."""
log.completion.debug("Initializing URL completion.")
with debug.log_time(log.completion, 'URL completion init'):
model = urlmodel.UrlCompletionModel()
_instances[usertypes.Completion.url] = model
def _init_tab_completion():
"""Initialize the tab completion model."""
log.completion.debug("Initializing tab completion.")
with debug.log_time(log.completion, 'tab completion init'):
model = miscmodels.TabCompletionModel()
_instances[usertypes.Completion.tab] = model
def _init_setting_completions():
"""Initialize setting completion models."""
log.completion.debug("Initializing setting completion.")
_instances[usertypes.Completion.section] = (
configmodel.SettingSectionCompletionModel())
_instances[usertypes.Completion.option] = {}
_instances[usertypes.Completion.value] = {}
for sectname in configdata.DATA:
opt_model = configmodel.SettingOptionCompletionModel(sectname)
_instances[usertypes.Completion.option][sectname] = opt_model
_instances[usertypes.Completion.value][sectname] = {}
for opt in configdata.DATA[sectname]:
val_model = configmodel.SettingValueCompletionModel(sectname, opt)
_instances[usertypes.Completion.value][sectname][opt] = val_model
def init_quickmark_completions():
"""Initialize quickmark completion models."""
log.completion.debug("Initializing quickmark completion.")
try:
_instances[usertypes.Completion.quickmark_by_name].deleteLater()
except KeyError:
pass
model = miscmodels.QuickmarkCompletionModel()
_instances[usertypes.Completion.quickmark_by_name] = model
def init_bookmark_completions():
"""Initialize bookmark completion models."""
log.completion.debug("Initializing bookmark completion.")
try:
_instances[usertypes.Completion.bookmark_by_url].deleteLater()
except KeyError:
pass
model = miscmodels.BookmarkCompletionModel()
_instances[usertypes.Completion.bookmark_by_url] = model
def init_session_completion():
"""Initialize session completion model."""
log.completion.debug("Initializing session completion.")
try:
_instances[usertypes.Completion.sessions].deleteLater()
except KeyError:
pass
model = miscmodels.SessionCompletionModel()
_instances[usertypes.Completion.sessions] = model
def _init_bind_completion():
"""Initialize the command completion model."""
log.completion.debug("Initializing bind completion.")
model = miscmodels.BindCompletionModel()
_instances[usertypes.Completion.bind] = model
INITIALIZERS = {
usertypes.Completion.command: _init_command_completion,
usertypes.Completion.helptopic: _init_helptopic_completion,
usertypes.Completion.url: _init_url_completion,
usertypes.Completion.tab: _init_tab_completion,
usertypes.Completion.section: _init_setting_completions,
usertypes.Completion.option: _init_setting_completions,
usertypes.Completion.value: _init_setting_completions,
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
usertypes.Completion.bookmark_by_url: init_bookmark_completions,
usertypes.Completion.sessions: init_session_completion,
usertypes.Completion.bind: _init_bind_completion,
}
def get(completion): def get(completion):
"""Get a certain completion. Initializes the completion if needed.""" """Get a certain completion. Initializes the completion if needed."""
try: if completion == usertypes.Completion.command:
return _instances[completion] return miscmodels.command
except KeyError: if completion == usertypes.Completion.helptopic:
if completion in INITIALIZERS: return miscmodels.helptopic
INITIALIZERS[completion]() if completion == usertypes.Completion.tab:
return _instances[completion] return miscmodels.buffer
else: if completion == usertypes.Completion.quickmark_by_name:
raise return miscmodels.quickmark
if completion == usertypes.Completion.bookmark_by_url:
return miscmodels.bookmark
def update(completions): if completion == usertypes.Completion.sessions:
"""Update an already existing completion. return miscmodels.session
if completion == usertypes.Completion.bind:
Args: return miscmodels.bind
completions: An iterable of usertypes.Completions. if completion == usertypes.Completion.section:
""" return configmodel.section
did_run = [] if completion == usertypes.Completion.option:
for completion in completions: return configmodel.option
if completion in _instances: if completion == usertypes.Completion.value:
func = INITIALIZERS[completion] return configmodel.value
if func not in did_run: if completion == usertypes.Completion.url:
func() return urlmodel.url
did_run.append(func)
@config.change_filter('aliases', function=True)
def _update_aliases():
"""Update completions that include command aliases."""
update([usertypes.Completion.command])
def init(): def init():
"""Initialize completions. Note this only connects signals.""" pass
quickmark_manager = objreg.get('quickmark-manager')
quickmark_manager.changed.connect(
functools.partial(update, [usertypes.Completion.quickmark_by_name]))
bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.changed.connect(
functools.partial(update, [usertypes.Completion.bookmark_by_url]))
session_manager = objreg.get('session-manager')
session_manager.update_completion.connect(
functools.partial(update, [usertypes.Completion.sessions]))
history = objreg.get('web-history')
history.async_read_done.connect(
functools.partial(update, [usertypes.Completion.url]))
keyconf = objreg.get('key-config')
keyconf.changed.connect(
functools.partial(update, [usertypes.Completion.command]))
keyconf.changed.connect(
functools.partial(update, [usertypes.Completion.bind]))
objreg.get('config').changed.connect(_update_aliases)

View File

@ -17,60 +17,37 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Misc. CompletionModels.""" """Functions that return miscellaneous completion models."""
from PyQt5.QtCore import Qt, QTimer, pyqtSlot from PyQt5.QtCore import Qt
from qutebrowser.browser import browsertab
from qutebrowser.config import config, configdata from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, qtutils from qutebrowser.utils import objreg, log, qtutils
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base from qutebrowser.completion.models import base
class CommandCompletionModel(base.BaseCompletionModel): def command():
"""A CompletionModel filled with non-hidden commands and descriptions.""" """A CompletionModel filled with non-hidden commands and descriptions."""
model = base.CompletionModel(column_widths=(20, 60, 20))
# https://github.com/qutebrowser/qutebrowser/issues/545 cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False)
# pylint: disable=abstract-method cat = model.new_category("Commands")
COLUMN_WIDTHS = (20, 60, 20)
def __init__(self, parent=None):
super().__init__(parent)
cmdlist = _get_cmd_completions(include_aliases=True,
include_hidden=False)
cat = self.new_category("Commands")
for (name, desc, misc) in cmdlist: for (name, desc, misc) in cmdlist:
self.new_item(cat, name, desc, misc) model.new_item(cat, name, desc, misc)
return model
class HelpCompletionModel(base.BaseCompletionModel): def helptopic():
"""A CompletionModel filled with help topics.""" """A CompletionModel filled with help topics."""
model = base.CompletionModel()
# https://github.com/qutebrowser/qutebrowser/issues/545 cmdlist = _get_cmd_completions(include_aliases=False, include_hidden=True,
# pylint: disable=abstract-method prefix=':')
cat = model.new_category("Commands")
COLUMN_WIDTHS = (20, 60, 20)
def __init__(self, parent=None):
super().__init__(parent)
self._init_commands()
self._init_settings()
def _init_commands(self):
"""Fill completion with :command entries."""
cmdlist = _get_cmd_completions(include_aliases=False,
include_hidden=True, prefix=':')
cat = self.new_category("Commands")
for (name, desc, misc) in cmdlist: for (name, desc, misc) in cmdlist:
self.new_item(cat, name, desc, misc) model.new_item(cat, name, desc, misc)
def _init_settings(self): cat = model.new_category("Settings")
"""Fill completion with section->option entries."""
cat = self.new_category("Settings")
for sectname, sectdata in configdata.DATA.items(): for sectname, sectdata in configdata.DATA.items():
for optname in sectdata: for optname in sectdata:
try: try:
@ -82,187 +59,97 @@ class HelpCompletionModel(base.BaseCompletionModel):
else: else:
desc = desc.splitlines()[0] desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname) name = '{}->{}'.format(sectname, optname)
self.new_item(cat, name, desc) model.new_item(cat, name, desc)
return model
class QuickmarkCompletionModel(base.BaseCompletionModel): def quickmark():
"""A CompletionModel filled with all quickmarks.""" """A CompletionModel filled with all quickmarks."""
model = base.CompletionModel()
# https://github.com/qutebrowser/qutebrowser/issues/545 cat = model.new_category("Quickmarks")
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
cat = self.new_category("Quickmarks")
quickmarks = objreg.get('quickmark-manager').marks.items() quickmarks = objreg.get('quickmark-manager').marks.items()
for qm_name, qm_url in quickmarks: for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_name, qm_url) model.new_item(cat, qm_name, qm_url)
return model
class BookmarkCompletionModel(base.BaseCompletionModel): def bookmark():
"""A CompletionModel filled with all bookmarks.""" """A CompletionModel filled with all bookmarks."""
model = base.CompletionModel()
# https://github.com/qutebrowser/qutebrowser/issues/545 cat = model.new_category("Bookmarks")
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
cat = self.new_category("Bookmarks")
bookmarks = objreg.get('bookmark-manager').marks.items() bookmarks = objreg.get('bookmark-manager').marks.items()
for bm_url, bm_title in bookmarks: for bm_url, bm_title in bookmarks:
self.new_item(cat, bm_url, bm_title) model.new_item(cat, bm_url, bm_title)
return model
class SessionCompletionModel(base.BaseCompletionModel): def session():
"""A CompletionModel filled with session names.""" """A CompletionModel filled with session names."""
model = base.CompletionModel()
# https://github.com/qutebrowser/qutebrowser/issues/545 cat = model.new_category("Sessions")
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
cat = self.new_category("Sessions")
try: try:
for name in objreg.get('session-manager').list_sessions(): for name in objreg.get('session-manager').list_sessions():
if not name.startswith('_'): if not name.startswith('_'):
self.new_item(cat, name) model.new_item(cat, name)
except OSError: except OSError:
log.completion.exception("Failed to list sessions!") log.completion.exception("Failed to list sessions!")
return model
class TabCompletionModel(base.BaseCompletionModel): def buffer():
"""A model to complete on open tabs across all windows. """A model to complete on open tabs across all windows.
Used for switching the buffer command. Used for switching the buffer command.
""" """
idx_column = 0
url_column = 1
text_column = 2
IDX_COLUMN = 0 def delete_buffer(completion):
URL_COLUMN = 1 """Close the selected tab."""
TEXT_COLUMN = 2
COLUMN_WIDTHS = (6, 40, 54)
DUMB_SORT = Qt.DescendingOrder
def __init__(self, parent=None):
super().__init__(parent)
self.columns_to_filter = [self.IDX_COLUMN, self.URL_COLUMN,
self.TEXT_COLUMN]
for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
for i in range(tabbed_browser.count()):
tab = tabbed_browser.widget(i)
tab.url_changed.connect(self.rebuild)
tab.title_changed.connect(self.rebuild)
tab.shutting_down.connect(self.delayed_rebuild)
tabbed_browser.new_tab.connect(self.on_new_tab)
tabbed_browser.tabBar().tabMoved.connect(self.rebuild)
objreg.get("app").new_window.connect(self.on_new_window)
self.rebuild()
def on_new_window(self, window):
"""Add hooks to new windows."""
window.tabbed_browser.new_tab.connect(self.on_new_tab)
@pyqtSlot(browsertab.AbstractTab)
def on_new_tab(self, tab):
"""Add hooks to new tabs."""
tab.url_changed.connect(self.rebuild)
tab.title_changed.connect(self.rebuild)
tab.shutting_down.connect(self.delayed_rebuild)
self.rebuild()
@pyqtSlot()
def delayed_rebuild(self):
"""Fire a rebuild indirectly so widgets get a chance to update."""
QTimer.singleShot(0, self.rebuild)
@pyqtSlot()
def rebuild(self):
"""Rebuild completion model from current tabs.
Very lazy method of keeping the model up to date. We could connect to
signals for new tab, tab url/title changed, tab close, tab moved and
make sure we handled background loads too ... but iterating over a
few/few dozen/few hundred tabs doesn't take very long at all.
"""
window_count = 0
for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if not tabbed_browser.shutting_down:
window_count += 1
if window_count < self.rowCount():
self.removeRows(window_count, self.rowCount() - window_count)
for i, win_id in enumerate(objreg.window_registry):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if tabbed_browser.shutting_down:
continue
if i >= self.rowCount():
c = self.new_category("{}".format(win_id))
else:
c = self.item(i, 0)
c.setData("{}".format(win_id), Qt.DisplayRole)
if tabbed_browser.count() < c.rowCount():
c.removeRows(tabbed_browser.count(),
c.rowCount() - tabbed_browser.count())
for idx in range(tabbed_browser.count()):
tab = tabbed_browser.widget(idx)
if idx >= c.rowCount():
self.new_item(c, "{}/{}".format(win_id, idx + 1),
tab.url().toDisplayString(),
tabbed_browser.page_title(idx))
else:
c.child(idx, 0).setData("{}/{}".format(win_id, idx + 1),
Qt.DisplayRole)
c.child(idx, 1).setData(tab.url().toDisplayString(),
Qt.DisplayRole)
c.child(idx, 2).setData(tabbed_browser.page_title(idx),
Qt.DisplayRole)
def delete_cur_item(self, completion):
"""Delete the selected item.
Args:
completion: The Completion object to use.
"""
index = completion.currentIndex() index = completion.currentIndex()
qtutils.ensure_valid(index) qtutils.ensure_valid(index)
category = index.parent() category = index.parent()
qtutils.ensure_valid(category) qtutils.ensure_valid(category)
index = category.child(index.row(), self.IDX_COLUMN) index = category.child(index.row(), idx_column)
win_id, tab_index = index.data().split('/') win_id, tab_index = index.data().split('/')
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=int(win_id)) window=int(win_id))
tabbed_browser.on_tab_close_requested(int(tab_index) - 1) tabbed_browser.on_tab_close_requested(int(tab_index) - 1)
model = base.CompletionModel(
column_widths=(6, 40, 54),
dumb_sort=Qt.DescendingOrder,
delete_cur_item=delete_buffer,
columns_to_filter=[idx_column, url_column, text_column])
class BindCompletionModel(base.BaseCompletionModel): for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if tabbed_browser.shutting_down:
continue
c = model.new_category("{}".format(win_id))
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))
return model
"""A CompletionModel filled with all bindable commands and descriptions."""
# https://github.com/qutebrowser/qutebrowser/issues/545 def bind(_):
# pylint: disable=abstract-method """A CompletionModel filled with all bindable commands and descriptions.
COLUMN_WIDTHS = (20, 60, 20) Args:
_: the key being bound.
def __init__(self, parent=None): """
super().__init__(parent) # TODO: offer a 'Current binding' completion based on the key.
cmdlist = _get_cmd_completions(include_hidden=True, model = base.CompletionModel(column_widths=(20, 60, 20))
include_aliases=True) cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)
cat = self.new_category("Commands") cat = model.new_category("Commands")
for (name, desc, misc) in cmdlist: for (name, desc, misc) in cmdlist:
self.new_item(cat, name, desc, misc) model.new_item(cat, name, desc, misc)
return model
def _get_cmd_completions(include_hidden, include_aliases, prefix=''): def _get_cmd_completions(include_hidden, include_aliases, prefix=''):

View File

@ -50,7 +50,7 @@ class CompletionFilterModel(QSortFilterProxyModel):
self.pattern = '' self.pattern = ''
self.pattern_re = None self.pattern_re = None
dumb_sort = self.srcmodel.DUMB_SORT dumb_sort = self.srcmodel.dumb_sort
if dumb_sort is None: if dumb_sort is None:
# pylint: disable=invalid-name # pylint: disable=invalid-name
self.lessThan = self.intelligentLessThan self.lessThan = self.intelligentLessThan

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""CompletionModels for URLs.""" """Function to return the url completion model for the `open` command."""
import datetime import datetime
@ -27,116 +27,80 @@ from qutebrowser.utils import objreg, utils, qtutils, log
from qutebrowser.completion.models import base from qutebrowser.completion.models import base
from qutebrowser.config import config from qutebrowser.config import config
_URL_COLUMN = 0
_TEXT_COLUMN = 1
_TIME_COLUMN = 2
_model = None
_history_cat = None
_quickmark_cat = None
_bookmark_cat = None
class UrlCompletionModel(base.BaseCompletionModel):
"""A model which combines bookmarks, quickmarks and web history URLs.
Used for the `open` command.
"""
URL_COLUMN = 0
TEXT_COLUMN = 1
TIME_COLUMN = 2
COLUMN_WIDTHS = (40, 50, 10)
DUMB_SORT = Qt.DescendingOrder
def __init__(self, parent=None):
super().__init__(parent)
self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN]
self._quickmark_cat = self.new_category("Quickmarks")
self._bookmark_cat = self.new_category("Bookmarks")
self._history_cat = self.new_category("History")
quickmark_manager = objreg.get('quickmark-manager')
quickmarks = quickmark_manager.marks.items()
for qm_name, qm_url in quickmarks:
self.new_item(self._quickmark_cat, qm_url, qm_name)
quickmark_manager.added.connect(
lambda name, url: self.new_item(self._quickmark_cat, url, name))
quickmark_manager.removed.connect(self.on_quickmark_removed)
def _delete_url(completion):
index = completion.currentIndex()
qtutils.ensure_valid(index)
category = index.parent()
index = category.child(index.row(), _URL_COLUMN)
qtutils.ensure_valid(category)
if category.data() == 'Bookmarks':
bookmark_manager = objreg.get('bookmark-manager') bookmark_manager = objreg.get('bookmark-manager')
bookmarks = bookmark_manager.marks.items() bookmark_manager.delete(index.data())
for bm_url, bm_title in bookmarks: elif category.data() == 'Quickmarks':
self.new_item(self._bookmark_cat, bm_url, bm_title) quickmark_manager = objreg.get('quickmark-manager')
bookmark_manager.added.connect( sibling = index.sibling(index.row(), _TEXT_COLUMN)
lambda name, url: self.new_item(self._bookmark_cat, url, name)) qtutils.ensure_valid(sibling)
bookmark_manager.removed.connect(self.on_bookmark_removed) name = sibling.data()
quickmark_manager.delete(name)
self._history = objreg.get('web-history')
self._max_history = config.get('completion', 'web-history-max-items')
history = utils.newest_slice(self._history, self._max_history)
for entry in history:
if not entry.redirect:
self._add_history_entry(entry)
self._history.add_completion_item.connect(self.on_history_item_added)
self._history.cleared.connect(self.on_history_cleared)
objreg.get('config').changed.connect(self.reformat_timestamps) def _remove_oldest_history():
def _fmt_atime(self, atime):
"""Format an atime to a human-readable string."""
fmt = config.get('completion', 'timestamp-format')
if fmt is None:
return ''
try:
dt = datetime.datetime.fromtimestamp(atime)
except (ValueError, OSError, OverflowError):
# Different errors which can occur for too large values...
log.misc.error("Got invalid timestamp {}!".format(atime))
return '(invalid)'
else:
return dt.strftime(fmt)
def _remove_oldest_history(self):
"""Remove the oldest history entry.""" """Remove the oldest history entry."""
self._history_cat.removeRow(0) _history_cat.removeRow(0)
def _add_history_entry(self, entry):
def _add_history_entry(entry):
"""Add a new history entry to the completion.""" """Add a new history entry to the completion."""
self.new_item(self._history_cat, entry.url.toDisplayString(), _model.new_item(_history_cat, entry.url.toDisplayString(),
entry.title, entry.title, _fmt_atime(entry.atime),
self._fmt_atime(entry.atime), sort=int(entry.atime), sort=int(entry.atime), userdata=entry.url)
userdata=entry.url)
if (self._max_history != -1 and max_history = config.get('completion', 'web-history-max-items')
self._history_cat.rowCount() > self._max_history): if max_history != -1 and _history_cat.rowCount() > max_history:
self._remove_oldest_history() _remove_oldest_history()
@config.change_filter('completion', 'timestamp-format')
def reformat_timestamps(self): @config.change_filter('completion', 'timestamp-format')
def _reformat_timestamps():
"""Reformat the timestamps if the config option was changed.""" """Reformat the timestamps if the config option was changed."""
for i in range(self._history_cat.rowCount()): for i in range(_history_cat.rowCount()):
url_item = self._history_cat.child(i, self.URL_COLUMN) url_item = _history_cat.child(i, _URL_COLUMN)
atime_item = self._history_cat.child(i, self.TIME_COLUMN) atime_item = _history_cat.child(i, _TIME_COLUMN)
atime = url_item.data(base.Role.sort) atime = url_item.data(base.Role.sort)
atime_item.setText(self._fmt_atime(atime)) atime_item.setText(_fmt_atime(atime))
@pyqtSlot(object)
def on_history_item_added(self, entry): @pyqtSlot(object)
def _on_history_item_added(entry):
"""Slot called when a new history item was added.""" """Slot called when a new history item was added."""
for i in range(self._history_cat.rowCount()): for i in range(_history_cat.rowCount()):
url_item = self._history_cat.child(i, self.URL_COLUMN) url_item = _history_cat.child(i, _URL_COLUMN)
atime_item = self._history_cat.child(i, self.TIME_COLUMN) atime_item = _history_cat.child(i, _TIME_COLUMN)
title_item = self._history_cat.child(i, self.TEXT_COLUMN) title_item = _history_cat.child(i, _TEXT_COLUMN)
url = url_item.data(base.Role.userdata) if url_item.data(base.Role.userdata) == entry.url:
if url == entry.url: atime_item.setText(_fmt_atime(entry.atime))
atime_item.setText(self._fmt_atime(entry.atime))
title_item.setText(entry.title) title_item.setText(entry.title)
url_item.setData(int(entry.atime), base.Role.sort) url_item.setData(int(entry.atime), base.Role.sort)
break break
else: else:
self._add_history_entry(entry) _add_history_entry(entry)
@pyqtSlot()
def on_history_cleared(self):
self._history_cat.removeRows(0, self._history_cat.rowCount())
def _remove_item(self, data, category, column): @pyqtSlot()
def _on_history_cleared():
_history_cat.removeRows(0, _history_cat.rowCount())
def _remove_item(data, category, column):
"""Helper function for on_quickmark_removed and on_bookmark_removed. """Helper function for on_quickmark_removed and on_bookmark_removed.
Args: Args:
@ -150,43 +114,85 @@ class UrlCompletionModel(base.BaseCompletionModel):
category.removeRow(i) category.removeRow(i)
break break
@pyqtSlot(str)
def on_quickmark_removed(self, name): @pyqtSlot(str)
def _on_quickmark_removed(name):
"""Called when a quickmark has been removed by the user. """Called when a quickmark has been removed by the user.
Args: Args:
name: The name of the quickmark which has been removed. name: The name of the quickmark which has been removed.
""" """
self._remove_item(name, self._quickmark_cat, self.TEXT_COLUMN) _remove_item(name, _quickmark_cat, _TEXT_COLUMN)
@pyqtSlot(str)
def on_bookmark_removed(self, url): @pyqtSlot(str)
def _on_bookmark_removed(urltext):
"""Called when a bookmark has been removed by the user. """Called when a bookmark has been removed by the user.
Args: Args:
url: The url of the bookmark which has been removed. urltext: The url of the bookmark which has been removed.
""" """
self._remove_item(url, self._bookmark_cat, self.URL_COLUMN) _remove_item(urltext, _bookmark_cat, _URL_COLUMN)
def delete_cur_item(self, completion):
"""Delete the selected item.
Args: def _fmt_atime(atime):
completion: The Completion object to use. """Format an atime to a human-readable string."""
""" fmt = config.get('completion', 'timestamp-format')
index = completion.currentIndex() if fmt is None:
qtutils.ensure_valid(index) return ''
category = index.parent() try:
index = category.child(index.row(), self.URL_COLUMN) dt = datetime.datetime.fromtimestamp(atime)
url = index.data() except (ValueError, OSError, OverflowError):
qtutils.ensure_valid(category) # Different errors which can occur for too large values...
log.misc.error("Got invalid timestamp {}!".format(atime))
return '(invalid)'
else:
return dt.strftime(fmt)
def _init():
global _model, _quickmark_cat, _bookmark_cat, _history_cat
_model = base.CompletionModel(column_widths=(40, 50, 10),
dumb_sort=Qt.DescendingOrder,
delete_cur_item=_delete_url,
columns_to_filter=[_URL_COLUMN,
_TEXT_COLUMN])
_quickmark_cat = _model.new_category("Quickmarks")
_bookmark_cat = _model.new_category("Bookmarks")
_history_cat = _model.new_category("History")
if category.data() == 'Bookmarks':
bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.delete(url)
elif category.data() == 'Quickmarks':
quickmark_manager = objreg.get('quickmark-manager') quickmark_manager = objreg.get('quickmark-manager')
sibling = index.sibling(index.row(), self.TEXT_COLUMN) quickmarks = quickmark_manager.marks.items()
qtutils.ensure_valid(sibling) for qm_name, qm_url in quickmarks:
name = sibling.data() _model.new_item(_quickmark_cat, qm_url, qm_name)
quickmark_manager.delete(name) quickmark_manager.added.connect(
lambda name, url: _model.new_item(_quickmark_cat, url, name))
quickmark_manager.removed.connect(_on_quickmark_removed)
bookmark_manager = objreg.get('bookmark-manager')
bookmarks = bookmark_manager.marks.items()
for bm_url, bm_title in bookmarks:
_model.new_item(_bookmark_cat, bm_url, bm_title)
bookmark_manager.added.connect(
lambda name, url: _model.new_item(_bookmark_cat, url, name))
bookmark_manager.removed.connect(_on_bookmark_removed)
history = objreg.get('web-history')
max_history = config.get('completion', 'web-history-max-items')
for entry in utils.newest_slice(history, max_history):
if not entry.redirect:
_add_history_entry(entry)
history.add_completion_item.connect(_on_history_item_added)
history.cleared.connect(_on_history_cleared)
objreg.get('config').changed.connect(_reformat_timestamps)
def url():
"""A _model which combines bookmarks, quickmarks and web history URLs.
Used for the `open` command.
"""
if not _model:
_init()
return _model

View File

@ -1,50 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2017 Alexander Cogneau <alexander.cogneau@gmail.com>
#
# 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 qutebrowser.completion.models column widths."""
import pytest
from qutebrowser.completion.models.base import BaseCompletionModel
from qutebrowser.completion.models.configmodel import (
SettingOptionCompletionModel, SettingSectionCompletionModel,
SettingValueCompletionModel)
from qutebrowser.completion.models.miscmodels import (
CommandCompletionModel, HelpCompletionModel, QuickmarkCompletionModel,
BookmarkCompletionModel, SessionCompletionModel)
from qutebrowser.completion.models.urlmodel import UrlCompletionModel
CLASSES = [BaseCompletionModel, SettingOptionCompletionModel,
SettingOptionCompletionModel, SettingSectionCompletionModel,
SettingValueCompletionModel, CommandCompletionModel,
HelpCompletionModel, QuickmarkCompletionModel,
BookmarkCompletionModel, SessionCompletionModel, UrlCompletionModel]
@pytest.mark.parametrize("model", CLASSES)
def test_list_size(model):
"""Test if there are 3 items in the COLUMN_WIDTHS property."""
assert len(model.COLUMN_WIDTHS) == 3
@pytest.mark.parametrize("model", CLASSES)
def test_column_width_sum(model):
"""Test if the sum of the widths asserts to 100."""
assert sum(model.COLUMN_WIDTHS) == 100

View File

@ -34,11 +34,11 @@ class FakeCompletionModel(QStandardItemModel):
"""Stub for a completion model.""" """Stub for a completion model."""
DUMB_SORT = None def __init__(self, kind, *pos_args, parent=None):
def __init__(self, kind, parent=None):
super().__init__(parent) super().__init__(parent)
self.kind = kind self.kind = kind
self.pos_args = [*pos_args]
self.dumb_sort = None
class CompletionWidgetStub(QObject): class CompletionWidgetStub(QObject):
@ -74,17 +74,8 @@ def instances(monkeypatch):
"""Mock the instances module so get returns a fake completion model.""" """Mock the instances module so get returns a fake completion model."""
# populate a model for each completion type, with a nested structure for # populate a model for each completion type, with a nested structure for
# option and value completion # option and value completion
instances = {kind: FakeCompletionModel(kind) get = lambda kind: lambda *args: FakeCompletionModel(kind, *args)
for kind in usertypes.Completion} monkeypatch.setattr(completer, 'instances', get)
instances[usertypes.Completion.option] = {
'general': FakeCompletionModel(usertypes.Completion.option),
}
instances[usertypes.Completion.value] = {
'general': {
'editor': FakeCompletionModel(usertypes.Completion.value),
}
}
monkeypatch.setattr(completer, 'instances', instances)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -140,47 +131,52 @@ def _set_cmd_prompt(cmd, txt):
cmd.setCursorPosition(txt.index('|')) cmd.setCursorPosition(txt.index('|'))
@pytest.mark.parametrize('txt, kind, pattern', [ @pytest.mark.parametrize('txt, kind, pattern, pos_args', [
(':nope|', usertypes.Completion.command, 'nope'), (':nope|', usertypes.Completion.command, 'nope', []),
(':nope |', None, ''), (':nope |', None, '', []),
(':set |', usertypes.Completion.section, ''), (':set |', usertypes.Completion.section, '', []),
(':set gen|', usertypes.Completion.section, 'gen'), (':set gen|', usertypes.Completion.section, 'gen', []),
(':set general |', usertypes.Completion.option, ''), (':set general |', usertypes.Completion.option, '', ['general']),
(':set what |', None, ''), (':set what |', usertypes.Completion.option, '', ['what']),
(':set general editor |', usertypes.Completion.value, ''), (':set general editor |', usertypes.Completion.value, '',
(':set general editor gv|', usertypes.Completion.value, 'gv'), ['general', 'editor']),
(':set general editor "gvim -f"|', usertypes.Completion.value, 'gvim -f'), (':set general editor gv|', usertypes.Completion.value, 'gv',
(':set general editor "gvim |', usertypes.Completion.value, 'gvim'), ['general', 'editor']),
(':set general huh |', None, ''), (':set general editor "gvim -f"|', usertypes.Completion.value, 'gvim -f',
(':help |', usertypes.Completion.helptopic, ''), ['general', 'editor']),
(':help |', usertypes.Completion.helptopic, ''), (':set general editor "gvim |', usertypes.Completion.value, 'gvim',
(':open |', usertypes.Completion.url, ''), ['general', 'editor']),
(':bind |', None, ''), (':set general huh |', usertypes.Completion.value, '', ['general', 'huh']),
(':bind <c-x> |', usertypes.Completion.command, ''), (':help |', usertypes.Completion.helptopic, '', []),
(':bind <c-x> foo|', usertypes.Completion.command, 'foo'), (':help |', usertypes.Completion.helptopic, '', []),
(':bind <c-x>| foo', None, '<c-x>'), (':open |', usertypes.Completion.url, '', []),
(':set| general ', usertypes.Completion.command, 'set'), (':bind |', None, '', []),
(':|set general ', usertypes.Completion.command, 'set'), (':bind <c-x> |', usertypes.Completion.command, '', ['<c-x>']),
(':set gene|ral ignore-case', usertypes.Completion.section, 'general'), (':bind <c-x> foo|', usertypes.Completion.command, 'foo', ['<c-x>']),
(':|', usertypes.Completion.command, ''), (':bind <c-x>| foo', None, '<c-x>', []),
(': |', usertypes.Completion.command, ''), (':set| general ', usertypes.Completion.command, 'set', []),
('/|', None, ''), (':|set general ', usertypes.Completion.command, 'set', []),
(':open -t|', None, ''), (':set gene|ral ignore-case', usertypes.Completion.section, 'general', []),
(':open --tab|', None, ''), (':|', usertypes.Completion.command, '', []),
(':open -t |', usertypes.Completion.url, ''), (': |', usertypes.Completion.command, '', []),
(':open --tab |', usertypes.Completion.url, ''), ('/|', None, '', []),
(':open | -t', usertypes.Completion.url, ''), (':open -t|', None, '', []),
(':tab-detach |', None, ''), (':open --tab|', None, '', []),
(':bind --mode=caret <c-x> |', usertypes.Completion.command, ''), (':open -t |', usertypes.Completion.url, '', []),
(':open --tab |', usertypes.Completion.url, '', []),
(':open | -t', usertypes.Completion.url, '', []),
(':tab-detach |', None, '', []),
(':bind --mode=caret <c-x> |', usertypes.Completion.command, '',
['<c-x>']),
pytest.param(':bind --mode caret <c-x> |', usertypes.Completion.command, pytest.param(':bind --mode caret <c-x> |', usertypes.Completion.command,
'', marks=pytest.mark.xfail(reason='issue #74')), '', [], marks=pytest.mark.xfail(reason='issue #74')),
(':set -t -p |', usertypes.Completion.section, ''), (':set -t -p |', usertypes.Completion.section, '', []),
(':open -- |', None, ''), (':open -- |', None, '', []),
(':gibberish nonesense |', None, ''), (':gibberish nonesense |', None, '', []),
('/:help|', None, ''), ('/:help|', None, '', []),
('::bind|', usertypes.Completion.command, ':bind'), ('::bind|', usertypes.Completion.command, ':bind'),
]) ])
def test_update_completion(txt, kind, pattern, status_command_stub, def test_update_completion(txt, kind, pattern, pos_args, status_command_stub,
completer_obj, completion_widget_stub): completer_obj, completion_widget_stub):
"""Test setting the completion widget's model based on command text.""" """Test setting the completion widget's model based on command text."""
# this test uses | as a placeholder for the current cursor position # this test uses | as a placeholder for the current cursor position
@ -192,7 +188,9 @@ def test_update_completion(txt, kind, pattern, status_command_stub,
if kind is None: if kind is None:
assert args[0] is None assert args[0] is None
else: else:
assert args[0].srcmodel.kind == kind model = args[0].srcmodel
assert model.kind == kind
assert model.pos_args == pos_args
assert args[1] == pattern assert args[1] == pattern

View File

@ -71,7 +71,7 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
def test_set_model(completionview): def test_set_model(completionview):
"""Ensure set_model actually sets the model and expands all categories.""" """Ensure set_model actually sets the model and expands all categories."""
model = base.BaseCompletionModel() model = base.CompletionModel()
filtermodel = sortfilter.CompletionFilterModel(model) filtermodel = sortfilter.CompletionFilterModel(model)
for i in range(3): for i in range(3):
model.appendRow(QStandardItem(str(i))) model.appendRow(QStandardItem(str(i)))
@ -82,7 +82,7 @@ def test_set_model(completionview):
def test_set_pattern(completionview): def test_set_pattern(completionview):
model = sortfilter.CompletionFilterModel(base.BaseCompletionModel()) model = sortfilter.CompletionFilterModel(base.CompletionModel())
model.set_pattern = unittest.mock.Mock() model.set_pattern = unittest.mock.Mock()
completionview.set_model(model, 'foo') completionview.set_model(model, 'foo')
model.set_pattern.assert_called_with('foo') model.set_pattern.assert_called_with('foo')
@ -148,7 +148,7 @@ def test_completion_item_focus(which, tree, expected, completionview, qtbot):
successive movement. None implies no signal should be successive movement. None implies no signal should be
emitted. emitted.
""" """
model = base.BaseCompletionModel() model = base.CompletionModel()
for catdata in tree: for catdata in tree:
cat = QStandardItem() cat = QStandardItem()
model.appendRow(cat) model.appendRow(cat)
@ -176,7 +176,7 @@ def test_completion_item_focus_no_model(which, completionview, qtbot):
""" """
with qtbot.assertNotEmitted(completionview.selection_changed): with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which) completionview.completion_item_focus(which)
model = base.BaseCompletionModel() model = base.CompletionModel()
filtermodel = sortfilter.CompletionFilterModel(model, filtermodel = sortfilter.CompletionFilterModel(model,
parent=completionview) parent=completionview)
completionview.set_model(filtermodel) completionview.set_model(filtermodel)
@ -200,7 +200,7 @@ def test_completion_show(show, rows, quick_complete, completionview,
config_stub.data['completion']['show'] = show config_stub.data['completion']['show'] = show
config_stub.data['completion']['quick-complete'] = quick_complete config_stub.data['completion']['quick-complete'] = quick_complete
model = base.BaseCompletionModel() model = base.CompletionModel()
for name in rows: for name in rows:
cat = QStandardItem() cat = QStandardItem()
model.appendRow(cat) model.appendRow(cat)

View File

@ -56,6 +56,9 @@ def _check_completions(model, expected):
misc = actual_cat.child(j, 2) misc = actual_cat.child(j, 2)
actual_item = (name.text(), desc.text(), misc.text()) actual_item = (name.text(), desc.text(), misc.text())
assert actual_item in expected_cat assert actual_item in expected_cat
# sanity-check the column_widths
assert len(model.column_widths) == 3
assert sum(model.column_widths) == 100
def _patch_cmdutils(monkeypatch, stubs, symbol): def _patch_cmdutils(monkeypatch, stubs, symbol):
@ -184,7 +187,7 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub,
key_config_stub.set_bindings_for('normal', {'s': 'stop', key_config_stub.set_bindings_for('normal', {'s': 'stop',
'rr': 'roll', 'rr': 'roll',
'ro': 'rock'}) 'ro': 'rock'})
model = miscmodels.CommandCompletionModel() model = miscmodels.command()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -212,7 +215,7 @@ def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub):
key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'}) key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'})
_patch_cmdutils(monkeypatch, stubs, module + '.cmdutils') _patch_cmdutils(monkeypatch, stubs, module + '.cmdutils')
_patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA')
model = miscmodels.HelpCompletionModel() model = miscmodels.helptopic()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -236,7 +239,7 @@ def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub):
def test_quickmark_completion(qtmodeltester, quickmarks): def test_quickmark_completion(qtmodeltester, quickmarks):
"""Test the results of quickmark completion.""" """Test the results of quickmark completion."""
model = miscmodels.QuickmarkCompletionModel() model = miscmodels.quickmark()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -251,7 +254,7 @@ def test_quickmark_completion(qtmodeltester, quickmarks):
def test_bookmark_completion(qtmodeltester, bookmarks): def test_bookmark_completion(qtmodeltester, bookmarks):
"""Test the results of bookmark completion.""" """Test the results of bookmark completion."""
model = miscmodels.BookmarkCompletionModel() model = miscmodels.bookmark()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -275,7 +278,7 @@ def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks,
""" """
config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d',
'web-history-max-items': 2} 'web-history-max-items': 2}
model = urlmodel.UrlCompletionModel() model = urlmodel.url()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -303,7 +306,7 @@ def test_url_completion_delete_bookmark(qtmodeltester, config_stub,
"""Test deleting a bookmark from the url completion model.""" """Test deleting a bookmark from the url completion model."""
config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d',
'web-history-max-items': 2} 'web-history-max-items': 2}
model = urlmodel.UrlCompletionModel() model = urlmodel.url()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -321,7 +324,7 @@ def test_url_completion_delete_quickmark(qtmodeltester, config_stub,
"""Test deleting a bookmark from the url completion model.""" """Test deleting a bookmark from the url completion model."""
config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d',
'web-history-max-items': 2} 'web-history-max-items': 2}
model = urlmodel.UrlCompletionModel() model = urlmodel.url()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -335,7 +338,7 @@ def test_url_completion_delete_quickmark(qtmodeltester, config_stub,
def test_session_completion(qtmodeltester, session_manager_stub): def test_session_completion(qtmodeltester, session_manager_stub):
session_manager_stub.sessions = ['default', '1', '2'] session_manager_stub.sessions = ['default', '1', '2']
model = miscmodels.SessionCompletionModel() model = miscmodels.session()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -354,7 +357,7 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
tabbed_browser_stubs[1].tabs = [ tabbed_browser_stubs[1].tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
] ]
model = miscmodels.TabCompletionModel() model = miscmodels.buffer()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -381,7 +384,7 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, qtbot, app_stub,
tabbed_browser_stubs[1].tabs = [ tabbed_browser_stubs[1].tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
] ]
model = miscmodels.TabCompletionModel() model = miscmodels.buffer()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -398,7 +401,7 @@ def test_setting_section_completion(qtmodeltester, monkeypatch, stubs):
_patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA')
_patch_config_section_desc(monkeypatch, stubs, _patch_config_section_desc(monkeypatch, stubs,
module + '.configdata.SECTION_DESC') module + '.configdata.SECTION_DESC')
model = configmodel.SettingSectionCompletionModel() model = configmodel.section()
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -418,7 +421,7 @@ def test_setting_option_completion(qtmodeltester, monkeypatch, stubs,
config_stub.data = {'ui': {'gesture': 'off', config_stub.data = {'ui': {'gesture': 'off',
'mind': 'on', 'mind': 'on',
'voice': 'sometimes'}} 'voice': 'sometimes'}}
model = configmodel.SettingOptionCompletionModel('ui') model = configmodel.option('ui')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -440,7 +443,7 @@ def test_setting_option_completion_valuelist(qtmodeltester, monkeypatch, stubs,
'DEFAULT': 'https://duckduckgo.com/?q={}' 'DEFAULT': 'https://duckduckgo.com/?q={}'
} }
} }
model = configmodel.SettingOptionCompletionModel('searchengines') model = configmodel.option('searchengines')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -454,7 +457,7 @@ def test_setting_value_completion(qtmodeltester, monkeypatch, stubs,
module = 'qutebrowser.completion.models.configmodel' module = 'qutebrowser.completion.models.configmodel'
_patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA')
config_stub.data = {'general': {'volume': '0'}} config_stub.data = {'general': {'volume': '0'}}
model = configmodel.SettingValueCompletionModel('general', 'volume') model = configmodel.value('general', 'volume')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -486,7 +489,7 @@ def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub,
key_config_stub.set_bindings_for('normal', {'s': 'stop', key_config_stub.set_bindings_for('normal', {'s': 'stop',
'rr': 'roll', 'rr': 'roll',
'ro': 'rock'}) 'ro': 'rock'})
model = miscmodels.BindCompletionModel() model = miscmodels.bind('s')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)

View File

@ -33,7 +33,7 @@ def _create_model(data):
tuple in the sub-list represents an item, and each value in the tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column tuple represents the item data for that column
""" """
model = base.BaseCompletionModel() model = base.CompletionModel()
for catdata in data: for catdata in data:
cat = model.new_category('') cat = model.new_category('')
for itemdata in catdata: for itemdata in catdata:
@ -74,7 +74,7 @@ def _extract_model_data(model):
('4', 'blah', False), ('4', 'blah', False),
]) ])
def test_filter_accepts_row(pattern, data, expected): def test_filter_accepts_row(pattern, data, expected):
source_model = base.BaseCompletionModel() source_model = base.CompletionModel()
cat = source_model.new_category('test') cat = source_model.new_category('test')
source_model.new_item(cat, data) source_model.new_item(cat, data)
@ -119,8 +119,8 @@ def test_first_last_item(tree, first, last):
def test_set_source_model(): def test_set_source_model():
"""Ensure setSourceModel sets source_model and clears the pattern.""" """Ensure setSourceModel sets source_model and clears the pattern."""
model1 = base.BaseCompletionModel() model1 = base.CompletionModel()
model2 = base.BaseCompletionModel() model2 = base.CompletionModel()
filter_model = sortfilter.CompletionFilterModel(model1) filter_model = sortfilter.CompletionFilterModel(model1)
filter_model.set_pattern('foo') filter_model.set_pattern('foo')
# sourceModel() is cached as srcmodel, so make sure both match # sourceModel() is cached as srcmodel, so make sure both match
@ -202,7 +202,7 @@ def test_count(tree, expected):
def test_set_pattern(pattern, dumb_sort, filter_cols, before, after): def test_set_pattern(pattern, dumb_sort, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern.""" """Validate the filtering and sorting results of set_pattern."""
model = _create_model(before) model = _create_model(before)
model.DUMB_SORT = dumb_sort model.dumb_sort = dumb_sort
model.columns_to_filter = filter_cols model.columns_to_filter = filter_cols
filter_model = sortfilter.CompletionFilterModel(model) filter_model = sortfilter.CompletionFilterModel(model)
filter_model.set_pattern(pattern) filter_model.set_pattern(pattern)