Merge branch 'master' of https://github.com/antoyo/qutebrowser into antoyo-master

Conflicts:
      .gitignore
This commit is contained in:
Florian Bruhin 2015-07-26 15:08:58 +02:00
commit b52a41ac6f
14 changed files with 318 additions and 10 deletions

2
.gitignore vendored
View File

@ -1,5 +1,6 @@
__pycache__
*.pyc
*.swp
/build
/dist
/qutebrowser.egg-info
@ -26,3 +27,4 @@ __pycache__
/.cache
/.testmondata
/.hypothesis
TODO

View File

@ -44,7 +44,8 @@ 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 (bookmarks, quickmarks, 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
@ -415,6 +416,9 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing quickmarks...")
quickmark_manager = quickmarks.QuickmarkManager(qApp)
objreg.register('quickmark-manager', quickmark_manager)
log.init.debug("Initializing bookmarks...")
bookmark_manager = bookmarks.BookmarkManager(qApp)
objreg.register('bookmark-manager', bookmark_manager)
log.init.debug("Initializing proxy...")
proxy.init()
log.init.debug("Initializing cookies...")

View File

@ -0,0 +1,124 @@
# 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/>.
"""Manager for bookmarks.
Note we violate our general QUrl rule by storing url strings in the bookmarks
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 collections
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from qutebrowser.utils import message, standarddir, objreg
from qutebrowser.misc import lineparser
class BookmarkManager(QObject):
"""Manager for bookmarks.
Attributes:
bookmarks: An OrderedDict of all bookmarks.
_lineparser: The LineParser used for the bookmarks, or None
(when qutebrowser is started with -c '').
Signals:
changed: Emitted when anything changed.
added: Emitted when a new bookmark was added.
arg 0: The title of the bookmark.
arg 1: The URL of the bookmark, as string.
removed: Emitted when an existing bookmark was removed.
arg 0: The title of the bookmark.
"""
changed = pyqtSignal()
added = pyqtSignal(str, str)
removed = pyqtSignal(str)
def __init__(self, parent=None):
"""Initialize and read bookmarks."""
super().__init__(parent)
self.bookmarks = collections.OrderedDict()
if standarddir.config() is None:
self._lineparser = None
else:
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)
for line in self._lineparser:
if not line.strip():
# Ignore empty or whitespace-only lines.
continue
parts = line.split(maxsplit=1)
if len(parts) == 2:
self.bookmarks[parts[0]] = parts[1]
elif len(parts) == 1:
self.bookmarks[parts[0]] = ''
filename = os.path.join(standarddir.config(), 'bookmarks/urls')
objreg.get('save-manager').add_saveable(
'bookmark-manager', self.save, self.changed,
filename=filename)
def save(self):
"""Save the bookmarks to disk."""
if self._lineparser is not None:
self._lineparser.data = [' '.join(tpl)
for tpl in self.bookmarks.items()]
self._lineparser.save()
def add(self, win_id, url, title):
"""Add a new bookmark.
Args:
win_id: The window ID to display the errors in.
url: The url to add as bookmark.
title: The title for the new bookmark.
"""
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr in self.bookmarks:
message.error(win_id, "Bookmark already exists!")
else:
self.bookmarks[urlstr] = title
self.changed.emit()
self.added.emit(title, urlstr)
message.info(win_id, "Bookmark added")
def delete(self, url):
"""Delete a bookmark.
Args:
url: The url of the bookmark to delete.
"""
del self.bookmarks[url]
self.changed.emit()
self.removed.emit(url)

View File

@ -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()
@ -1053,6 +1057,27 @@ class CommandDispatcher:
url = objreg.get('quickmark-manager').get(name)
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')
bookmark_manager.add(self._win_id, self._current_url(),
self._current_title())
@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.
"""
self._open(QUrl(url), tab, bg, window)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def follow_selected(self, tab=False):

View File

@ -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,12 @@ 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."""
try:
self.model().srcmodel.delete_cur_item(self._win_id)
except NotImplementedError:
raise cmdexc.CommandError("Cannot delete this item.")

View File

@ -195,7 +195,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
if index.parent().isValid():
pattern = index.model().pattern
if index.column() == 0 and pattern:
if(index.column() in index.model().srcmodel.columns_to_highlight
and pattern):
repl = r'<span class="highlight">\g<0></span>'
text = re.sub(re.escape(pattern), repl, self._opt.text,
flags=re.IGNORECASE)

View File

@ -44,6 +44,7 @@ class BaseCompletionModel(QStandardItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self.setColumnCount(3)
self.columns_to_highlight = []
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

View File

@ -34,6 +34,7 @@ class SettingSectionCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
cat = self.new_category("Sections")
for name in configdata.DATA.keys():
desc = configdata.SECTION_DESC[name].splitlines()[0].strip()
@ -53,6 +54,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
def __init__(self, section, parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
cat = self.new_category(section)
sectdata = configdata.DATA[section]
self._misc_items = {}
@ -106,6 +108,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
def __init__(self, section, option, parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
self._section = section
self._option = option
objreg.get('config').changed.connect(self.update_current_value)

View File

@ -107,6 +107,18 @@ def init_quickmark_completions():
_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, 'url')
_instances[usertypes.Completion.bookmark_by_url] = model
@pyqtSlot()
def init_session_completion():
"""Initialize session completion model."""
@ -128,6 +140,7 @@ INITIALIZERS = {
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,
}
@ -166,6 +179,10 @@ def init():
functools.partial(update, [usertypes.Completion.quickmark_by_url,
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]))

View File

@ -33,6 +33,7 @@ class CommandCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
@ -56,6 +57,7 @@ class HelpCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
self._init_commands()
self._init_settings()
@ -98,6 +100,7 @@ class QuickmarkCompletionModel(base.BaseCompletionModel):
def __init__(self, match_field='url', parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
cat = self.new_category("Quickmarks")
quickmarks = objreg.get('quickmark-manager').marks.items()
if match_field == 'url':
@ -111,6 +114,28 @@ class QuickmarkCompletionModel(base.BaseCompletionModel):
match_field))
class BookmarkCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all bookmarks."""
# pylint: disable=abstract-method
def __init__(self, match_field='url', parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
cat = self.new_category("Bookmarks")
bookmarks = objreg.get('bookmark-manager').bookmarks.items()
if match_field == 'url':
for bm_url, bm_title in bookmarks:
self.new_item(cat, bm_url, bm_title)
elif match_field == 'title':
for bm_url, bm_title in bookmarks:
self.new_item(cat, bm_title, bm_url)
else:
raise ValueError("Invalid value '{}' for match_field!".format(
match_field))
class SessionCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with session names."""
@ -119,6 +144,7 @@ class SessionCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
cat = self.new_category("Sessions")
try:
for name in objreg.get('session-manager').list_sessions():

View File

@ -137,12 +137,15 @@ class CompletionFilterModel(QSortFilterProxyModel):
# 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:
return False
return self.pattern.casefold() in data.casefold()
try:
return self.srcmodel.custom_filter(self.pattern, row, parent)
except NotImplementedError:
if not data:
return False
return self.pattern.casefold() in data.casefold()
def intelligentLessThan(self, lindex, rindex):
"""Custom sorting implementation.

View File

@ -23,14 +23,14 @@ import datetime
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.utils import objreg, utils
from qutebrowser.utils import message, objreg, utils
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."""
@ -39,7 +39,11 @@ class UrlCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None):
super().__init__(parent)
self.columns_to_highlight.append(0)
self.columns_to_highlight.append(1)
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')
@ -49,6 +53,13 @@ class UrlCompletionModel(base.BaseCompletionModel):
quickmark_manager.added.connect(self.on_quickmark_added)
quickmark_manager.removed.connect(self.on_quickmark_removed)
bookmark_manager = objreg.get('bookmark-manager')
bookmarks = bookmark_manager.bookmarks.items()
for bm_url, bm_title in bookmarks:
self._add_bookmark_entry(bm_title, bm_url)
bookmark_manager.added.connect(self.on_bookmark_added)
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)
@ -81,6 +92,24 @@ class UrlCompletionModel(base.BaseCompletionModel):
"""
self.new_item(self._quickmark_cat, url, name)
def _add_bookmark_entry(self, title, url):
"""Add a new bookmark entry to the completion.
Args:
title: The title of the new bookmark.
url: The URL of the new bookmark.
"""
self.new_item(self._bookmark_cat, url, title)
def custom_filter(self, pattern, row, parent):
"""Filter by url and title."""
index0 = self.index(row, 0, parent)
index1 = self.index(row, 1, parent)
url = self.data(index0) or ''
title = self.data(index1) or ''
return (pattern.casefold() in url.casefold() or pattern.casefold() in
title.casefold())
@config.change_filter('completion', 'timestamp-format')
def reformat_timestamps(self):
"""Reformat the timestamps if the config option was changed."""
@ -126,3 +155,50 @@ class UrlCompletionModel(base.BaseCompletionModel):
if name_item.data(Qt.DisplayRole) == name:
self._quickmark_cat.removeRow(i)
break
@pyqtSlot(str, str)
def on_bookmark_added(self, title, url):
"""Called when a bookmark has been added by the user.
Args:
title: The title of the new bookmark.
url: The url of the new bookmark, as string.
"""
self._add_bookmark_entry(title, url)
@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.
"""
for i in range(self._bookmark_cat.rowCount()):
url_item = self._bookmark_cat.child(i, 0)
if url_item.data(Qt.DisplayRole) == url:
self._bookmark_cat.removeRow(i)
break
def delete_cur_item(self, win_id):
"""Delete the selected item.
Args:
win_id: The current windows id.
"""
completion = objreg.get('completion', scope='window',
window=win_id)
index = completion.currentIndex()
model = completion.model()
url = model.data(index)
category = index.parent()
if category.isValid():
if category.data() == 'Bookmarks':
bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.delete(url)
message.info(win_id, "Bookmarks deleted")
elif category.data() == 'Quickmarks':
quickmark_manager = objreg.get('quickmark-manager')
name = model.data(index.sibling(index.row(),
index.column() + 1))
quickmark_manager.quickmark_del(name)
message.info(win_id, "Quickmarks deleted")

View File

@ -1274,6 +1274,7 @@ 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']),
('save', ['sf']),
('set-cmd-text -s :set', ['ss']),
('set-cmd-text -s :set -t', ['sl']),
@ -1336,6 +1337,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),
])),

View File

@ -237,7 +237,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'])
'quickmark_by_name', 'bookmark_by_url',
'url', 'sessions'])
# Exit statuses for errors. Needs to be an int for sys.exit.