diff options
author | Florian Bruhin <git@the-compiler.org> | 2015-07-31 13:16:10 +0200 |
---|---|---|
committer | Florian Bruhin <git@the-compiler.org> | 2015-07-31 13:16:10 +0200 |
commit | accd2399edbec63d363dd1e2f6c2c38c46a1eedf (patch) | |
tree | 8fb6816b49658492c8e17f22d24464f56ed5cad7 | |
parent | 907a4b0e5e350056681c9bbfcaaa60e3999642dc (diff) | |
parent | 57e79db136ec68bbfd32db449d47425f32a1ff5d (diff) | |
download | qutebrowser-accd2399edbec63d363dd1e2f6c2c38c46a1eedf.tar.gz qutebrowser-accd2399edbec63d363dd1e2f6c2c38c46a1eedf.zip |
Merge branch 'antoyo-master'
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | CHANGELOG.asciidoc | 9 | ||||
-rw-r--r-- | README.asciidoc | 2 | ||||
-rw-r--r-- | doc/help/commands.asciidoc | 43 | ||||
-rw-r--r-- | qutebrowser/app.py | 7 | ||||
-rw-r--r-- | qutebrowser/browser/commands.py | 38 | ||||
-rw-r--r-- | qutebrowser/browser/quickmarks.py | 170 | ||||
-rw-r--r-- | qutebrowser/browser/urlmarks.py | 296 | ||||
-rw-r--r-- | qutebrowser/completion/completer.py | 13 | ||||
-rw-r--r-- | qutebrowser/completion/completiondelegate.py | 3 | ||||
-rw-r--r-- | qutebrowser/completion/models/base.py | 15 | ||||
-rw-r--r-- | qutebrowser/completion/models/instances.py | 26 | ||||
-rw-r--r-- | qutebrowser/completion/models/miscmodels.py | 27 | ||||
-rw-r--r-- | qutebrowser/completion/models/sortfilter.py | 26 | ||||
-rw-r--r-- | qutebrowser/completion/models/urlmodel.py | 100 | ||||
-rw-r--r-- | qutebrowser/config/configdata.py | 5 | ||||
-rw-r--r-- | qutebrowser/utils/usertypes.py | 4 |
17 files changed, 548 insertions, 238 deletions
diff --git a/.gitignore b/.gitignore index d71a7a16b..cc23379f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__ *.pyc +*.swp /build /dist /qutebrowser.egg-info @@ -26,3 +27,4 @@ __pycache__ /.cache /.testmondata /.hypothesis +TODO diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 385cb1ce0..165916389 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -17,6 +17,15 @@ This project adheres to http://semver.org/[Semantic Versioning]. v0.4.0 (unreleased) ------------------- +Added +~~~~~ + +- New bookmark functionality (similar to quickmarks without a name). + * New command `:bookmark-add` to bookmark the current page (bound to `M`). + * New command `:bookmark-load` to load a bookmark (bound to `gb`/`gB`/`wB`). +- New (hidden) command `:completion-item-del` (bound to `<Ctrl-D>`) to delete + the current item in the completion (for quickmarks/bookmarks). + Changed ~~~~~~~ diff --git a/README.asciidoc b/README.asciidoc index 2c11d315c..fc8eeb196 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -135,6 +135,7 @@ Contributors, sorted by the number of commits in descending order: // QUTE_AUTHORS_START * Florian Bruhin * Bruno Oliveira +* Antoni Boucher * Raphael Pierzina * Joel Torstensson * Martin Tournoij @@ -142,7 +143,6 @@ Contributors, sorted by the number of commits in descending order: * Lamar Pavel * Austin Anderson * Artur Shaik -* Antoni Boucher * ZDarian * Peter Vilim * John ShaggyTwoDope Jenkins diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index f12b3ba99..a898b9698 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -8,6 +8,9 @@ |<<adblock-update,adblock-update>>|Update the adblock block lists. |<<back,back>>|Go back in the history of the current tab. |<<bind,bind>>|Bind a key to a command. +|<<bookmark-add,bookmark-add>>|Save the current page as a bookmark. +|<<bookmark-del,bookmark-del>>|Delete a bookmark. +|<<bookmark-load,bookmark-load>>|Load a bookmark. |<<close,close>>|Close the current window. |<<download,download>>|Download a given URL, or current page if no URL given. |<<download-cancel,download-cancel>>|Cancel the last/[count]th download. @@ -99,6 +102,41 @@ Bind a key to a command. * This command does not split arguments after the last argument and handles quotes literally. * With this command, +;;+ is interpreted literally instead of splitting off a second command. +[[bookmark-add]] +=== bookmark-add +Save the current page as a bookmark. + +[[bookmark-del]] +=== bookmark-del +Syntax: +:bookmark-del 'url'+ + +Delete a bookmark. + +==== positional arguments +* +'url'+: The URL of the bookmark to delete. + +==== note +* This command does not split arguments after the last argument and handles quotes literally. +* With this command, +;;+ is interpreted literally instead of splitting off a second command. + +[[bookmark-load]] +=== bookmark-load +Syntax: +:bookmark-load [*--tab*] [*--bg*] [*--window*] 'url'+ + +Load a bookmark. + +==== positional arguments +* +'url'+: The url of the bookmark to load. + +==== optional arguments +* +*-t*+, +*--tab*+: Load the bookmark in a new tab. +* +*-b*+, +*--bg*+: Load the bookmark in a new background tab. +* +*-w*+, +*--window*+: Load the bookmark in a new window. + +==== note +* This command does not split arguments after the last argument and handles quotes literally. +* With this command, +;;+ is interpreted literally instead of splitting off a second command. + [[close]] === close Close the current window. @@ -724,6 +762,7 @@ How many steps to zoom out. |<<command-accept,command-accept>>|Execute the command currently in the commandline. |<<command-history-next,command-history-next>>|Go forward in the commandline history. |<<command-history-prev,command-history-prev>>|Go back in the commandline history. +|<<completion-item-del,completion-item-del>>|Delete the current completion item. |<<completion-item-next,completion-item-next>>|Select the next completion item. |<<completion-item-prev,completion-item-prev>>|Select the previous completion item. |<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled. @@ -790,6 +829,10 @@ Go forward in the commandline history. === command-history-prev Go back in the commandline history. +[[completion-item-del]] +=== completion-item-del +Delete the current completion item. + [[completion-item-next]] === completion-item-next Select the next completion item. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 0774fdfd7..da0a4ef6d 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -44,7 +44,7 @@ import qutebrowser.resources # pylint: disable=unused-import from qutebrowser.completion.models import instances as completionmodels from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.config import style, config, websettings, configexc -from qutebrowser.browser import quickmarks, cookies, cache, adblock, history +from qutebrowser.browser import urlmarks, cookies, cache, adblock, history from qutebrowser.browser.network import qutescheme, proxy, networkmanager from qutebrowser.mainwindow import mainwindow from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal @@ -413,8 +413,11 @@ def _init_modules(args, crash_handler): host_blocker.read_hosts() objreg.register('host-blocker', host_blocker) log.init.debug("Initializing quickmarks...") - quickmark_manager = quickmarks.QuickmarkManager(qApp) + quickmark_manager = urlmarks.QuickmarkManager(qApp) objreg.register('quickmark-manager', quickmark_manager) + log.init.debug("Initializing bookmarks...") + bookmark_manager = urlmarks.BookmarkManager(qApp) + objreg.register('bookmark-manager', bookmark_manager) log.init.debug("Initializing proxy...") proxy.init() log.init.debug("Initializing cookies...") diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 8cd1eaf6a..dfbb0592b 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -38,7 +38,7 @@ import pygments.formatters from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.config import config, configexc -from qutebrowser.browser import webelem, inspector +from qutebrowser.browser import webelem, inspector, urlmarks from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils) @@ -100,6 +100,10 @@ class CommandDispatcher: msg += "!" raise cmdexc.CommandError(msg) + def _current_title(self): + """Convenience method to get the current title.""" + return self._current_widget().title() + def _current_widget(self): """Get the currently active widget from a command.""" widget = self._tabbed_browser.currentWidget() @@ -1050,7 +1054,37 @@ class CommandDispatcher: bg: Load the quickmark in a new background tab. window: Load the quickmark in a new window. """ - url = objreg.get('quickmark-manager').get(name) + try: + url = objreg.get('quickmark-manager').get(name) + except urlmarks.Error as e: + raise cmdexc.CommandError(str(e)) + self._open(url, tab, bg, window) + + @cmdutils.register(instance='command-dispatcher', scope='window') + def bookmark_add(self): + """Save the current page as a bookmark.""" + bookmark_manager = objreg.get('bookmark-manager') + try: + bookmark_manager.add(self._current_url(), self._current_title()) + except urlmarks.Error as e: + raise cmdexc.CommandError(str(e)) + + @cmdutils.register(instance='command-dispatcher', scope='window', + maxsplit=0, + completion=[usertypes.Completion.bookmark_by_url]) + def bookmark_load(self, url, tab=False, bg=False, window=False): + """Load a bookmark. + + Args: + url: The url of the bookmark to load. + tab: Load the bookmark in a new tab. + bg: Load the bookmark in a new background tab. + window: Load the bookmark in a new window. + """ + try: + url = urlutils.fuzzy_url(url) + except urlutils.FuzzyUrlError as e: + raise cmdexc.CommandError(e) self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', hide=True, diff --git a/qutebrowser/browser/quickmarks.py b/qutebrowser/browser/quickmarks.py deleted file mode 100644 index cf2a36b89..000000000 --- a/qutebrowser/browser/quickmarks.py +++ /dev/null @@ -1,170 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org> -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. - -"""Manager for quickmarks. - -Note we violate our general QUrl rule by storing url strings in the marks -OrderedDict. This is because we read them from a file at start and write them -to a file on shutdown, so it makes sense to keep them as strings here. -""" - -import os.path -import functools -import collections - -from PyQt5.QtCore import pyqtSignal, QUrl, QObject - -from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg -from qutebrowser.commands import cmdexc, cmdutils -from qutebrowser.misc import lineparser - - -class QuickmarkManager(QObject): - - """Manager for quickmarks. - - Attributes: - marks: An OrderedDict of all quickmarks. - _lineparser: The LineParser used for the quickmarks, or None - (when qutebrowser is started with -c ''). - - Signals: - changed: Emitted when anything changed. - added: Emitted when a new quickmark was added. - arg 0: The name of the quickmark. - arg 1: The URL of the quickmark, as string. - removed: Emitted when an existing quickmark was removed. - arg 0: The name of the quickmark. - """ - - changed = pyqtSignal() - added = pyqtSignal(str, str) - removed = pyqtSignal(str) - - def __init__(self, parent=None): - """Initialize and read quickmarks.""" - super().__init__(parent) - - self.marks = collections.OrderedDict() - - if standarddir.config() is None: - self._lineparser = None - else: - self._lineparser = lineparser.LineParser( - standarddir.config(), 'quickmarks', parent=self) - for line in self._lineparser: - if not line.strip(): - # Ignore empty or whitespace-only lines. - continue - try: - key, url = line.rsplit(maxsplit=1) - except ValueError: - message.error('current', "Invalid quickmark '{}'".format( - line)) - else: - self.marks[key] = url - filename = os.path.join(standarddir.config(), 'quickmarks') - objreg.get('save-manager').add_saveable( - 'quickmark-manager', self.save, self.changed, - filename=filename) - - def save(self): - """Save the quickmarks to disk.""" - if self._lineparser is not None: - self._lineparser.data = [' '.join(tpl) - for tpl in self.marks.items()] - self._lineparser.save() - - def prompt_save(self, win_id, url): - """Prompt for a new quickmark name to be added and add it. - - Args: - win_id: The current window ID. - url: The quickmark url as a QUrl. - """ - if not url.isValid(): - urlutils.invalid_url_error(win_id, url, "save quickmark") - return - urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) - message.ask_async( - win_id, "Add quickmark:", usertypes.PromptMode.text, - functools.partial(self.quickmark_add, win_id, urlstr)) - - @cmdutils.register(instance='quickmark-manager', win_id='win_id') - def quickmark_add(self, win_id, url, name): - """Add a new quickmark. - - Args: - win_id: The window ID to display the errors in. - url: The url to add as quickmark. - name: The name for the new quickmark. - """ - # We don't raise cmdexc.CommandError here as this can be called async - # via prompt_save. - if not name: - message.error(win_id, "Can't set mark with empty name!") - return - if not url: - message.error(win_id, "Can't set mark with empty URL!") - return - - def set_mark(): - """Really set the quickmark.""" - self.marks[name] = url - self.changed.emit() - self.added.emit(name, url) - - if name in self.marks: - message.confirm_async( - win_id, "Override existing quickmark?", set_mark, default=True) - else: - set_mark() - - @cmdutils.register(instance='quickmark-manager', maxsplit=0, - completion=[usertypes.Completion.quickmark_by_name]) - def quickmark_del(self, name): - """Delete a quickmark. - - Args: - name: The name of the quickmark to delete. - """ - try: - del self.marks[name] - except KeyError: - raise cmdexc.CommandError("Quickmark '{}' not found!".format(name)) - else: - self.changed.emit() - self.removed.emit(name) - - def get(self, name): - """Get the URL of the quickmark named name as a QUrl.""" - if name not in self.marks: - raise cmdexc.CommandError( - "Quickmark '{}' does not exist!".format(name)) - urlstr = self.marks[name] - try: - url = urlutils.fuzzy_url(urlstr, do_search=False) - except urlutils.FuzzyUrlError as e: - if e.url is None or not e.url.errorString(): - errstr = '' - else: - errstr = ' ({})'.format(e.url.errorString()) - raise cmdexc.CommandError("Invalid URL for quickmark {}: " - "{}{}".format(name, urlstr, errstr)) - return url diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py new file mode 100644 index 000000000..735f05e13 --- /dev/null +++ b/qutebrowser/browser/urlmarks.py @@ -0,0 +1,296 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# Copyright 2015 Antoni Boucher <bouanto@zoho.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/>. + +"""Managers for bookmarks and quickmarks. + +Note we violate our general QUrl rule by storing url strings in the marks +OrderedDict. This is because we read them from a file at start and write them +to a file on shutdown, so it makes sense to keep them as strings here. +""" + +import os +import os.path +import functools +import collections + +from PyQt5.QtCore import pyqtSignal, QUrl, QObject + +from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg +from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.misc import lineparser + + +class Error(Exception): + + """Base class for all errors in this module.""" + + pass + + +class InvalidUrlError(Error): + + """Exception emitted when a URL is invalid.""" + + pass + + +class DoesNotExistError(Error): + + """Exception emitted when a given URL does not exist.""" + + pass + + +class AlreadyExistsError(Error): + + """Exception emitted when a given URL does already exist.""" + + pass + + +class UrlMarkManager(QObject): + + """Base class for BookmarkManager and QuickmarkManager. + + Attributes: + marks: An OrderedDict of all quickmarks/bookmarks. + _lineparser: The LineParser used for the marks, or None + (when qutebrowser is started with -c ''). + + Signals: + changed: Emitted when anything changed. + added: Emitted when a new quickmark/bookmark was added. + removed: Emitted when an existing quickmark/bookmark was removed. + """ + + changed = pyqtSignal() + added = pyqtSignal(str, str) + removed = pyqtSignal(str) + + def __init__(self, parent=None): + """Initialize and read quickmarks.""" + super().__init__(parent) + + self.marks = collections.OrderedDict() + self._lineparser = None + + if standarddir.config() is None: + return + + self._init_lineparser() + for line in self._lineparser: + if not line.strip(): + # Ignore empty or whitespace-only lines. + continue + self._parse_line(line) + self._init_savemanager(objreg.get('save-manager')) + + def _init_lineparser(self): + raise NotImplementedError + + def _parse_line(self, line): + raise NotImplementedError + + def _init_savemanager(self, _save_manager): + raise NotImplementedError + + def save(self): + """Save the marks to disk.""" + if self._lineparser is not None: + self._lineparser.data = [' '.join(tpl) + for tpl in self.marks.items()] + self._lineparser.save() + + def delete(self, key): + """Delete a quickmark/bookmark. + + Args: + key: The key to delete (name for quickmarks, URL for bookmarks.) + """ + del self.marks[key] + self.changed.emit() + self.removed.emit(key) + + +class QuickmarkManager(UrlMarkManager): + + """Manager for quickmarks. + + The primary key for quickmarks is their *name*, this means: + + - self.marks maps names to URLs. + - changed gets emitted with the name as first argument and the URL as + second argument. + - removed gets emitted with the name as argument. + """ + + def _init_lineparser(self): + self._lineparser = lineparser.LineParser( + standarddir.config(), 'quickmarks', parent=self) + + def _init_savemanager(self, save_manager): + filename = os.path.join(standarddir.config(), 'quickmarks') + save_manager.add_saveable('quickmark-manager', self.save, self.changed, + filename=filename) + + def _parse_line(self, line): + try: + key, url = line.rsplit(maxsplit=1) + except ValueError: + message.error('current', "Invalid quickmark '{}'".format( + line)) + else: + self.marks[key] = url + + def prompt_save(self, win_id, url): + """Prompt for a new quickmark name to be added and add it. + + Args: + win_id: The current window ID. + url: The quickmark url as a QUrl. + """ + if not url.isValid(): + urlutils.invalid_url_error(win_id, url, "save quickmark") + return + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) + message.ask_async( + win_id, "Add quickmark:", usertypes.PromptMode.text, + functools.partial(self.quickmark_add, win_id, urlstr)) + + @cmdutils.register(instance='quickmark-manager', win_id='win_id') + def quickmark_add(self, win_id, url, name): + """Add a new quickmark. + + Args: + win_id: The window ID to display the errors in. + url: The url to add as quickmark. + name: The name for the new quickmark. + """ + # We don't raise cmdexc.CommandError here as this can be called async + # via prompt_save. + if not name: + message.error(win_id, "Can't set mark with empty name!") + return + if not url: + message.error(win_id, "Can't set mark with empty URL!") + return + + def set_mark(): + """Really set the quickmark.""" + self.marks[name] = url + self.changed.emit() + self.added.emit(name, url) + + if name in self.marks: + message.confirm_async( + win_id, "Override existing quickmark?", set_mark, default=True) + else: + set_mark() + + @cmdutils.register(instance='quickmark-manager', maxsplit=0, + completion=[usertypes.Completion.quickmark_by_name]) + def quickmark_del(self, name): + """Delete a quickmark. + + Args: + name: The name of the quickmark to delete. + """ + try: + self.delete(name) + except KeyError: + raise cmdexc.CommandError("Quickmark '{}' not found!".format(name)) + + def get(self, name): + """Get the URL of the quickmark named name as a QUrl.""" + if name not in self.marks: + raise DoesNotExistError( + "Quickmark '{}' does not exist!".format(name)) + urlstr = self.marks[name] + try: + url = urlutils.fuzzy_url(urlstr, do_search=False) + except urlutils.FuzzyUrlError as e: + raise InvalidUrlError( + "Invalid URL for quickmark {}: {}".format(name, str(e))) + return url + + +class BookmarkManager(UrlMarkManager): + + """Manager for bookmarks. + + The primary key for bookmarks is their *url*, this means: + + - self.marks maps URLs to titles. + - changed gets emitted with the URL as first argument and the title as + second argument. + - removed gets emitted with the URL as argument. + """ + + def _init_lineparser(self): + bookmarks_directory = os.path.join(standarddir.config(), 'bookmarks') + if not os.path.isdir(bookmarks_directory): + os.makedirs(bookmarks_directory) + self._lineparser = lineparser.LineParser( + standarddir.config(), 'bookmarks/urls', parent=self) + + def _init_savemanager(self, save_manager): + filename = os.path.join(standarddir.config(), 'bookmarks/urls') + save_manager.add_saveable('bookmark-manager', self.save, self.changed, + filename=filename) + + def _parse_line(self, line): + parts = line.split(maxsplit=1) + if len(parts) == 2: + self.marks[parts[0]] = parts[1] + elif len(parts) == 1: + self.marks[parts[0]] = '' + + def add(self, url, title): + """Add a new bookmark. + + Args: + url: The url to add as bookmark. + title: The title for the new bookmark. + """ + if not url.isValid(): + errstr = urlutils.get_errstring(url) + raise InvalidUrlError(errstr) + + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) + + if urlstr in self.marks: + raise AlreadyExistsError("Bookmark already exists!") + else: + self.marks[urlstr] = title + self.changed.emit() + self.added.emit(title, urlstr) + + @cmdutils.register(instance='bookmark-manager', maxsplit=0, + completion=[usertypes.Completion.bookmark_by_url]) + def bookmark_del(self, url): + """Delete a bookmark. + + Args: + url: The URL of the bookmark to delete. + """ + try: + self.delete(url) + except KeyError: + raise cmdexc.CommandError("Bookmark '{}' not found!".format(url)) diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 2d4165bbf..bc1a9daa0 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -22,7 +22,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer from qutebrowser.config import config -from qutebrowser.commands import cmdutils, runners +from qutebrowser.commands import cmdexc, cmdutils, runners from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.completion.models import instances @@ -481,3 +481,14 @@ class Completer(QObject): """Select the next completion item.""" self._open_completion_if_needed() self.next_prev_item.emit(False) + + @cmdutils.register(instance='completion', hide=True, + modes=[usertypes.KeyMode.command], scope='window') + def completion_item_del(self): + """Delete the current completion item.""" + completion = objreg.get('completion', scope='window', + window=self._win_id) + try: + self.model().srcmodel.delete_cur_item(completion) + except NotImplementedError: + raise cmdexc.CommandError("Cannot delete this item.") diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index 3fe7e00ba..f2f256a40 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -195,7 +195,8 @@ class CompletionItemDelegate(QStyledItemDelegate): if index.parent().isValid(): pattern = index.model().pattern - if index.column() == 0 and pattern: + columns_to_filter = index.model().srcmodel.columns_to_filter + if index.column() in columns_to_filter and pattern: repl = r'<span class="highlight">\g<0></span>' text = re.sub(re.escape(pattern), repl, self._opt.text, flags=re.IGNORECASE) diff --git a/qutebrowser/completion/models/base.py b/qutebrowser/completion/models/base.py index b56444013..0d4bf7fca 100644 --- a/qutebrowser/completion/models/base.py +++ b/qutebrowser/completion/models/base.py @@ -44,6 +44,7 @@ class BaseCompletionModel(QStandardItemModel): def __init__(self, parent=None): super().__init__(parent) self.setColumnCount(3) + self.columns_to_filter = [0] def new_category(self, name, sort=None): """Add a new category to the model. @@ -95,6 +96,10 @@ class BaseCompletionModel(QStandardItemModel): nameitem.setData(userdata, Role.userdata) return nameitem, descitem, miscitem + def delete_cur_item(self, win_id): + """Delete the selected item.""" + raise NotImplementedError + def flags(self, index): """Return the item flags for index. @@ -121,3 +126,13 @@ class BaseCompletionModel(QStandardItemModel): Override QAbstractItemModel::sort. """ raise NotImplementedError + + def custom_filter(self, pattern, row, parent): + """Custom filter. + + Args: + pattern: The current filter pattern. + row: The row to accept or reject in the filter. + parent: The parent item QModelIndex. + """ + raise NotImplementedError diff --git a/qutebrowser/completion/models/instances.py b/qutebrowser/completion/models/instances.py index e39ca1692..0fc683488 100644 --- a/qutebrowser/completion/models/instances.py +++ b/qutebrowser/completion/models/instances.py @@ -97,17 +97,26 @@ def init_quickmark_completions(): """Initialize quickmark completion models.""" log.completion.debug("Initializing quickmark completion.") try: - _instances[usertypes.Completion.quickmark_by_url].deleteLater() _instances[usertypes.Completion.quickmark_by_name].deleteLater() except KeyError: pass - model = _init_model(miscmodels.QuickmarkCompletionModel, 'url') - _instances[usertypes.Completion.quickmark_by_url] = model - model = _init_model(miscmodels.QuickmarkCompletionModel, 'name') + model = _init_model(miscmodels.QuickmarkCompletionModel) _instances[usertypes.Completion.quickmark_by_name] = model @pyqtSlot() +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 = _init_model(miscmodels.BookmarkCompletionModel) + _instances[usertypes.Completion.bookmark_by_url] = model + + +@pyqtSlot() def init_session_completion(): """Initialize session completion model.""" log.completion.debug("Initializing session completion.") @@ -126,8 +135,8 @@ INITIALIZERS = { usertypes.Completion.section: _init_setting_completions, usertypes.Completion.option: _init_setting_completions, usertypes.Completion.value: _init_setting_completions, - usertypes.Completion.quickmark_by_url: init_quickmark_completions, usertypes.Completion.quickmark_by_name: init_quickmark_completions, + usertypes.Completion.bookmark_by_url: init_bookmark_completions, usertypes.Completion.sessions: init_session_completion, } @@ -163,8 +172,11 @@ def init(): """Initialize completions. Note this only connects signals.""" quickmark_manager = objreg.get('quickmark-manager') quickmark_manager.changed.connect( - functools.partial(update, [usertypes.Completion.quickmark_by_url, - usertypes.Completion.quickmark_by_name])) + 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( diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index f9af679ce..2a1d9cfef 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -96,19 +96,26 @@ class QuickmarkCompletionModel(base.BaseCompletionModel): # pylint: disable=abstract-method - def __init__(self, match_field='url', parent=None): + def __init__(self, parent=None): super().__init__(parent) cat = self.new_category("Quickmarks") quickmarks = objreg.get('quickmark-manager').marks.items() - if match_field == 'url': - for qm_name, qm_url in quickmarks: - self.new_item(cat, qm_url, qm_name) - elif match_field == 'name': - for qm_name, qm_url in quickmarks: - self.new_item(cat, qm_name, qm_url) - else: - raise ValueError("Invalid value '{}' for match_field!".format( - match_field)) + for qm_name, qm_url in quickmarks: + self.new_item(cat, qm_name, qm_url) + + +class BookmarkCompletionModel(base.BaseCompletionModel): + + """A CompletionModel filled with all 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() + for bm_url, bm_title in bookmarks: + self.new_item(cat, bm_url, bm_title) class SessionCompletionModel(base.BaseCompletionModel): diff --git a/qutebrowser/completion/models/sortfilter.py b/qutebrowser/completion/models/sortfilter.py index 19802fa96..3c247a0a2 100644 --- a/qutebrowser/completion/models/sortfilter.py +++ b/qutebrowser/completion/models/sortfilter.py @@ -130,19 +130,23 @@ 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(): + if parent == QModelIndex() or not self.pattern: return True - idx = self.srcmodel.index(row, 0, parent) - if not idx.isValid(): - # No entries in parent model - return False - data = self.srcmodel.data(idx) - # TODO more sophisticated filtering - if not self.pattern: - return True - if not data: + + try: + return self.srcmodel.custom_filter(self.pattern, row, parent) + except NotImplementedError: + for col in self.srcmodel.columns_to_filter: + idx = self.srcmodel.index(row, col, parent) + if not idx.isValid(): + # No entries in parent model + continue + data = self.srcmodel.data(idx) + if not data: + continue + elif self.pattern.casefold() in data.casefold(): + return True return False - return self.pattern.casefold() in data.casefold() def intelligentLessThan(self, lindex, rindex): """Custom sorting implementation. diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index 1eb7e13d2..03f55e3ac 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -23,32 +23,48 @@ import datetime from PyQt5.QtCore import pyqtSlot, Qt -from qutebrowser.utils import objreg, utils +from qutebrowser.utils import objreg, utils, qtutils from qutebrowser.completion.models import base from qutebrowser.config import config class UrlCompletionModel(base.BaseCompletionModel): - """A model which combines quickmarks and web history URLs. + """A model which combines bookmarks, quickmarks and web history URLs. Used for the `open` command.""" # pylint: disable=abstract-method + URL_COLUMN = 0 + TEXT_COLUMN = 1 + TIME_COLUMN = 2 + 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._add_quickmark_entry(qm_name, qm_url) - quickmark_manager.added.connect(self.on_quickmark_added) + 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) + bookmark_manager = objreg.get('bookmark-manager') + bookmarks = bookmark_manager.marks.items() + for bm_url, bm_title in bookmarks: + self.new_item(self._bookmark_cat, bm_url, bm_title) + bookmark_manager.added.connect( + lambda name, url: self.new_item(self._bookmark_cat, url, name)) + bookmark_manager.removed.connect(self.on_bookmark_removed) + self._history = objreg.get('web-history') max_history = config.get('completion', 'web-history-max-items') history = utils.newest_slice(self._history, max_history) @@ -72,47 +88,42 @@ class UrlCompletionModel(base.BaseCompletionModel): self._fmt_atime(entry.atime), sort=int(entry.atime), userdata=entry.url) - def _add_quickmark_entry(self, name, url): - """Add a new quickmark entry to the completion. - - Args: - name: The name of the new quickmark. - url: The URL of the new quickmark. - """ - self.new_item(self._quickmark_cat, url, name) - @config.change_filter('completion', 'timestamp-format') def reformat_timestamps(self): """Reformat the timestamps if the config option was changed.""" for i in range(self._history_cat.rowCount()): - name_item = self._history_cat.child(i, 0) - atime_item = self._history_cat.child(i, 2) - atime = name_item.data(base.Role.sort) + url_item = self._history_cat.child(i, self.URL_COLUMN) + atime_item = self._history_cat.child(i, self.TIME_COLUMN) + atime = url_item.data(base.Role.sort) atime_item.setText(self._fmt_atime(atime)) @pyqtSlot(object) def on_history_item_added(self, entry): """Slot called when a new history item was added.""" for i in range(self._history_cat.rowCount()): - name_item = self._history_cat.child(i, 0) - atime_item = self._history_cat.child(i, 2) - url = name_item.data(base.Role.userdata) + url_item = self._history_cat.child(i, self.URL_COLUMN) + atime_item = self._history_cat.child(i, self.TIME_COLUMN) + url = url_item.data(base.Role.userdata) if url == entry.url: atime_item.setText(self._fmt_atime(entry.atime)) - name_item.setData(int(entry.atime), base.Role.sort) + url_item.setData(int(entry.atime), base.Role.sort) break else: self._add_history_entry(entry) - @pyqtSlot(str, str) - def on_quickmark_added(self, name, url): - """Called when a quickmark has been added by the user. + def _remove_item(self, data, category, column): + """Helper function for on_quickmark_removed and on_bookmark_removed. Args: - name: The name of the new quickmark. - url: The url of the new quickmark, as string. + data: The item to search for. + category: The category to search in. + column: The column to use for matching. """ - self._add_quickmark_entry(name, url) + for i in range(category.rowCount()): + item = category.child(i, column) + if item.data(Qt.DisplayRole) == data: + category.removeRow(i) + break @pyqtSlot(str) def on_quickmark_removed(self, name): @@ -121,8 +132,35 @@ class UrlCompletionModel(base.BaseCompletionModel): Args: name: The name of the quickmark which has been removed. """ - for i in range(self._quickmark_cat.rowCount()): - name_item = self._quickmark_cat.child(i, 1) - if name_item.data(Qt.DisplayRole) == name: - self._quickmark_cat.removeRow(i) - break + self._remove_item(name, self._quickmark_cat, self.TEXT_COLUMN) + + @pyqtSlot(str) + def on_bookmark_removed(self, url): + """Called when a bookmark has been removed by the user. + + Args: + url: The url of the bookmark which has been removed. + """ + self._remove_item(url, self._bookmark_cat, self.URL_COLUMN) + + def delete_cur_item(self, completion): + """Delete the selected item. + + Args: + completion: The Completion object to use. + """ + index = completion.currentIndex() + qtutils.ensure_valid(index) + url = index.data() + category = index.parent() + qtutils.ensure_valid(category) + + if category.data() == 'Bookmarks': + bookmark_manager = objreg.get('bookmark-manager') + bookmark_manager.delete(url) + elif category.data() == 'Quickmarks': + quickmark_manager = objreg.get('quickmark-manager') + sibling = index.sibling(index.row(), self.TEXT_COLUMN) + qtutils.ensure_valid(sibling) + name = sibling.data() + quickmark_manager.quickmark_del(name) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2a976be4f..06ddad079 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1274,6 +1274,10 @@ KEY_DATA = collections.OrderedDict([ ('set-cmd-text -s :quickmark-load', ['b']), ('set-cmd-text -s :quickmark-load -t', ['B']), ('set-cmd-text -s :quickmark-load -w', ['wb']), + ('bookmark-add', ['M']), + ('set-cmd-text -s :bookmark-load', ['gb']), + ('set-cmd-text -s :bookmark-load -t', ['gB']), + ('set-cmd-text -s :bookmark-load -w', ['wB']), ('save', ['sf']), ('set-cmd-text -s :set', ['ss']), ('set-cmd-text -s :set -t', ['sl']), @@ -1336,6 +1340,7 @@ KEY_DATA = collections.OrderedDict([ ('command-history-next', ['<Ctrl-N>']), ('completion-item-prev', ['<Shift-Tab>', '<Up>']), ('completion-item-next', ['<Tab>', '<Down>']), + ('completion-item-del', ['<Ctrl-D>']), ('command-accept', RETURN_KEYS), ])), diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index d1d6883a8..42ce0b93a 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -236,8 +236,8 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', # Available command completions Completion = enum('Completion', ['command', 'section', 'option', 'value', - 'helptopic', 'quickmark_by_url', - 'quickmark_by_name', 'url', 'sessions']) + 'helptopic', 'quickmark_by_name', + 'bookmark_by_url', 'url', 'sessions']) # Exit statuses for errors. Needs to be an int for sys.exit. |