summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <git@the-compiler.org>2015-07-31 13:16:10 +0200
committerFlorian Bruhin <git@the-compiler.org>2015-07-31 13:16:10 +0200
commitaccd2399edbec63d363dd1e2f6c2c38c46a1eedf (patch)
tree8fb6816b49658492c8e17f22d24464f56ed5cad7
parent907a4b0e5e350056681c9bbfcaaa60e3999642dc (diff)
parent57e79db136ec68bbfd32db449d47425f32a1ff5d (diff)
downloadqutebrowser-accd2399edbec63d363dd1e2f6c2c38c46a1eedf.tar.gz
qutebrowser-accd2399edbec63d363dd1e2f6c2c38c46a1eedf.zip
Merge branch 'antoyo-master'
-rw-r--r--.gitignore2
-rw-r--r--CHANGELOG.asciidoc9
-rw-r--r--README.asciidoc2
-rw-r--r--doc/help/commands.asciidoc43
-rw-r--r--qutebrowser/app.py7
-rw-r--r--qutebrowser/browser/commands.py38
-rw-r--r--qutebrowser/browser/quickmarks.py170
-rw-r--r--qutebrowser/browser/urlmarks.py296
-rw-r--r--qutebrowser/completion/completer.py13
-rw-r--r--qutebrowser/completion/completiondelegate.py3
-rw-r--r--qutebrowser/completion/models/base.py15
-rw-r--r--qutebrowser/completion/models/instances.py26
-rw-r--r--qutebrowser/completion/models/miscmodels.py27
-rw-r--r--qutebrowser/completion/models/sortfilter.py26
-rw-r--r--qutebrowser/completion/models/urlmodel.py100
-rw-r--r--qutebrowser/config/configdata.py5
-rw-r--r--qutebrowser/utils/usertypes.py4
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.