Docstringify ALL the things

This commit is contained in:
Florian Bruhin 2014-01-29 15:30:19 +01:00
parent 46660b11ef
commit e56099e0ec
18 changed files with 548 additions and 127 deletions

View File

@ -1 +1,14 @@
"""A vim like browser based on Qt""" """A vim like browser based on Qt.
Files:
__init__.py - This file.
__main__.py - Entry point for qutebrowser, to use\
'python -m qutebrowser'.
app.py - Main qutebrowser application>
simplebrowser.py - Simple browser for testing purposes.
Subpackages:
commands - Handling of commands and key parsing.
utils - Misc utility code.
widgets - Qt widgets displayed on the screen.
"""

View File

@ -1,3 +1,5 @@
"""Entry point for qutebrowser. Simply execute qutebrowser."""
from qutebrowser.app import QuteBrowser from qutebrowser.app import QuteBrowser
import sys import sys

View File

@ -1,3 +1,5 @@
""" Initialization of qutebrowser and application-wide things """
import sys import sys
import logging import logging
import faulthandler import faulthandler
@ -15,7 +17,13 @@ from qutebrowser.utils.appdirs import AppDirs
class QuteBrowser(QApplication): class QuteBrowser(QApplication):
"""Main object for QuteBrowser""" """Main object for qutebrowser.
Can be used like this:
>>> app = QuteBrowser()
>>> sys.exit(app.exec_())
"""
dirs = None # AppDirs - config/cache directories dirs = None # AppDirs - config/cache directories
config = None # Config(Parser) object config = None # Config(Parser) object
mainwindow = None mainwindow = None
@ -27,13 +35,13 @@ class QuteBrowser(QApplication):
def __init__(self): def __init__(self):
super().__init__(sys.argv) super().__init__(sys.argv)
# Exit on exceptions # Exit on exceptions
sys.excepthook = self.tmp_exception_hook sys.excepthook = self._tmp_exception_hook
# Handle segfaults # Handle segfaults
faulthandler.enable() faulthandler.enable()
self.parseopts() self._parseopts()
self.initlog() self._initlog()
self.dirs = AppDirs('qutebrowser') self.dirs = AppDirs('qutebrowser')
if self.args.confdir is None: if self.args.confdir is None:
@ -46,7 +54,7 @@ class QuteBrowser(QApplication):
self.commandparser = cmdutils.CommandParser() self.commandparser = cmdutils.CommandParser()
self.keyparser = KeyParser(self.mainwindow) self.keyparser = KeyParser(self.mainwindow)
self.init_cmds() self._init_cmds()
self.mainwindow = MainWindow() self.mainwindow = MainWindow()
self.aboutToQuit.connect(config.config.save) self.aboutToQuit.connect(config.config.save)
@ -62,15 +70,23 @@ class QuteBrowser(QApplication):
self.mainwindow.status.txt.set_keystring) self.mainwindow.status.txt.set_keystring)
self.mainwindow.show() self.mainwindow.show()
self.python_hacks() self._python_hacks()
def tmp_exception_hook(self, exctype, value, traceback): def _tmp_exception_hook(self, exctype, value, traceback):
"""Exception hook while initializing, simply exit""" """Handle exceptions while initializing by simply exiting.
This is only temporary and will get replaced by exception_hook later.
It's necessary because PyQt seems to ignore exceptions by default.
"""
sys.__excepthook__(exctype, value, traceback) sys.__excepthook__(exctype, value, traceback)
self.exit(1) self.exit(1)
def exception_hook(self, exctype, value, traceback): def _exception_hook(self, exctype, value, traceback):
"""Try very hard to write open tabs to a file and exit gracefully""" """Handle uncaught python exceptions.
It'll try very hard to write all open tabs to a file, and then exit
gracefully.
"""
# pylint: disable=broad-except # pylint: disable=broad-except
sys.__excepthook__(exctype, value, traceback) sys.__excepthook__(exctype, value, traceback)
try: try:
@ -84,22 +100,21 @@ class QuteBrowser(QApplication):
pass pass
self.exit(1) self.exit(1)
def python_hacks(self): def _python_hacks(self):
"""Gets around some PyQt-oddities by evil hacks""" """Get around some PyQt-oddities by evil hacks.
## Make python exceptions work
sys.excepthook = self.exception_hook
## Quit on SIGINT This sets up the uncaught exception hook, quits with an appropriate
exit status, and handles Ctrl+C properly by passing control to the
Python interpreter once all 500ms.
"""
sys.excepthook = self._exception_hook
signal(SIGINT, lambda *args: self.exit(128 + SIGINT)) signal(SIGINT, lambda *args: self.exit(128 + SIGINT))
## hack to make Ctrl+C work by passing control to the Python
## interpreter once all 500ms (lambda to ignore args)
self.timer = QTimer() self.timer = QTimer()
self.timer.start(500) self.timer.start(500)
self.timer.timeout.connect(lambda: None) self.timer.timeout.connect(lambda: None)
def parseopts(self): def _parseopts(self):
"""Parse command line options""" """Parse command line options."""
parser = ArgumentParser("usage: %(prog)s [options]") parser = ArgumentParser("usage: %(prog)s [options]")
parser.add_argument('-l', '--log', dest='loglevel', parser.add_argument('-l', '--log', dest='loglevel',
help='Set loglevel', default='info') help='Set loglevel', default='info')
@ -107,8 +122,8 @@ class QuteBrowser(QApplication):
'(empty for no config storage)') '(empty for no config storage)')
self.args = parser.parse_args() self.args = parser.parse_args()
def initlog(self): def _initlog(self):
"""Initialisation of the log""" """Initialisation of the logging output."""
loglevel = self.args.loglevel loglevel = self.args.loglevel
numeric_level = getattr(logging, loglevel.upper(), None) numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int): if not isinstance(numeric_level, int):
@ -119,8 +134,11 @@ class QuteBrowser(QApplication):
'[%(module)s:%(funcName)s:%(lineno)s] %(message)s', '[%(module)s:%(funcName)s:%(lineno)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S') datefmt='%Y-%m-%d %H:%M:%S')
def init_cmds(self): def _init_cmds(self):
"""Initialisation of the qutebrowser commands""" """Initialisation of the qutebrowser commands.
Registers all commands, connects its signals, and sets up keyparser.
"""
cmdutils.register_all() cmdutils.register_all()
for cmd in cmdutils.cmd_dict.values(): for cmd in cmdutils.cmd_dict.values():
cmd.signal.connect(self.cmd_handler) cmd.signal.connect(self.cmd_handler)
@ -130,8 +148,10 @@ class QuteBrowser(QApplication):
pass pass
def cmd_handler(self, tpl): def cmd_handler(self, tpl):
"""Handler which gets called from all commands and delegates the """Handle commands and delegate the specific actions.
specific actions.
This gets called as a slot from all commands, and then calls the
appropriate command handler.
tpl -- A tuple in the form (count, argv) where argv is [cmd, arg, ...] tpl -- A tuple in the form (count, argv) where argv is [cmd, arg, ...]
@ -163,12 +183,17 @@ class QuteBrowser(QApplication):
handler = handlers[cmd] handler = handlers[cmd]
if self.sender().count: if self.sender().count:
handler(*args, count=count) return handler(*args, count=count)
else: else:
handler(*args) return handler(*args)
def pyeval(self, s): def pyeval(self, s):
"""Evaluates a python string, handler for the pyeval command""" """Evaluate a python string and display the results as a webpage.
s -- The string to evaluate.
:pyeval command handler.
"""
try: try:
r = eval(s) r = eval(s)
out = repr(r) out = repr(r)

View File

@ -5,8 +5,10 @@ Defined here to avoid circular dependency hell.
class NoSuchCommandError(ValueError): class NoSuchCommandError(ValueError):
"""Raised when a command wasn't found."""
pass pass
class ArgumentCountError(TypeError): class ArgumentCountError(TypeError):
"""Raised when a command was called with an invalid count of arguments."""
pass pass

View File

@ -1,3 +1,6 @@
"""Parse keypresses/keychains in the main window."""
import logging import logging
import re import re
@ -8,7 +11,7 @@ from qutebrowser.commands.utils import (CommandParser, ArgumentCountError,
class KeyParser(QObject): class KeyParser(QObject):
"""Parser for vim-like key sequences""" """Parser for vim-like key sequences."""
keystring = '' # The currently entered key sequence keystring = '' # The currently entered key sequence
# Signal emitted when the statusbar should set a partial command # Signal emitted when the statusbar should set a partial command
set_cmd_text = pyqtSignal(str) set_cmd_text = pyqtSignal(str)
@ -27,8 +30,9 @@ class KeyParser(QObject):
self.commandparser = CommandParser() self.commandparser = CommandParser()
def from_config_sect(self, sect): def from_config_sect(self, sect):
"""Loads keybindings from a ConfigParser section, in the config format """Load keybindings from a ConfigParser section.
key = command, e.g.
Config format: key = command, e.g.:
gg = scrollstart gg = scrollstart
""" """
for (key, cmd) in sect.items(): for (key, cmd) in sect.items():
@ -36,13 +40,17 @@ class KeyParser(QObject):
self.bindings[key] = cmd self.bindings[key] = cmd
def handle(self, e): def handle(self, e):
"""Wrapper for _handle to emit keystring_updated after _handle""" """Wrap _handle to emit keystring_updated after _handle."""
self._handle(e) self._handle(e)
self.keystring_updated.emit(self.keystring) self.keystring_updated.emit(self.keystring)
def _handle(self, e): def _handle(self, e):
"""Handle a new keypress. """Handle a new keypress.
Separates the keypress into count/command, then checks if it matches
any possible command, and either runs the command, ignores it, or
displays an error.
e -- the KeyPressEvent from Qt e -- the KeyPressEvent from Qt
""" """
logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text())) logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text()))
@ -98,7 +106,10 @@ class KeyParser(QObject):
return return
def _match_key(self, cmdstr_needle): def _match_key(self, cmdstr_needle):
"""Tries to match a given cmdstr with any defined command""" """Try to match a given keystring with any bound keychain.
cmdstr_needle: The command string to find.
"""
try: try:
cmdstr_hay = self.bindings[cmdstr_needle] cmdstr_hay = self.bindings[cmdstr_needle]
return (self.MATCH_DEFINITIVE, cmdstr_hay) return (self.MATCH_DEFINITIVE, cmdstr_hay)

View File

@ -1,3 +1,6 @@
"""Contains the Command class, a skeleton for a command."""
import logging import logging
from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtCore import QObject, pyqtSignal
@ -6,8 +9,9 @@ from qutebrowser.commands.exceptions import ArgumentCountError
class Command(QObject): class Command(QObject):
"""Base skeleton for a command. See the module help for """Base skeleton for a command.
qutebrowser.commands.commands for details.
See the module documentation for qutebrowser.commands.commands for details.
""" """
# FIXME: # FIXME:

View File

@ -1,4 +1,5 @@
"""Various command utils and the Command base class""" """Contains various command utils, and the CommandParser."""
import inspect import inspect
import shlex import shlex
@ -10,6 +11,7 @@ from qutebrowser.commands.exceptions import (ArgumentCountError,
NoSuchCommandError) NoSuchCommandError)
from qutebrowser.utils.completion import CompletionModel from qutebrowser.utils.completion import CompletionModel
# A mapping from command-strings to command objects.
cmd_dict = {} cmd_dict = {}
@ -28,13 +30,17 @@ def register_all():
class CommandParser(QObject): class CommandParser(QObject):
"""Parser for qutebrowser commandline commands""" """Parse qutebrowser commandline commands."""
text = '' text = ''
cmd = '' cmd = ''
args = [] args = []
error = pyqtSignal(str) # Emitted if there's an error error = pyqtSignal(str) # Emitted if there's an error
def _parse(self, text): def _parse(self, text):
"""Split the commandline text into command and arguments.
Raise NoSuchCommandError if a command wasn't found.
"""
self.text = text self.text = text
parts = self.text.strip().split(maxsplit=1) parts = self.text.strip().split(maxsplit=1)
cmdstr = parts[0] cmdstr = parts[0]
@ -53,19 +59,22 @@ class CommandParser(QObject):
self.args = args self.args = args
def _check(self): def _check(self):
"""Check if the argument count for the command is correct."""
self.cmd.check(self.args) self.cmd.check(self.args)
def _run(self, count=None): def _run(self, count=None):
"""Run a command with an optional count."""
if count is not None: if count is not None:
self.cmd.run(self.args, count=count) self.cmd.run(self.args, count=count)
else: else:
self.cmd.run(self.args) self.cmd.run(self.args)
def run(self, text, count=None, ignore_exc=True): def run(self, text, count=None, ignore_exc=True):
"""Parses a command from a line of text. """Parse a command from a line of text.
If ignore_exc is True, ignores exceptions and returns True/False
instead. If ignore_exc is True, ignore exceptions and return True/False.
Raises NoSuchCommandError if a command wasn't found, and
Raise NoSuchCommandError if a command wasn't found, and
ArgumentCountError if a command was called with the wrong count of ArgumentCountError if a command was called with the wrong count of
arguments. arguments.
""" """
@ -89,7 +98,11 @@ class CommandParser(QObject):
class CommandCompletionModel(CompletionModel): class CommandCompletionModel(CompletionModel):
"""A CompletionModel filled with all commands and descriptions."""
# pylint: disable=abstract-method # pylint: disable=abstract-method
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
assert cmd_dict assert cmd_dict

View File

@ -1,4 +1,4 @@
"""Simple browser for testing purposes""" """Very simple browser for testing purposes."""
import sys import sys

View File

@ -1,3 +1,12 @@
"""The base data models for completion in the commandline.
Contains:
CompletionModel -- A simple tree model based on Python data.
CompletionItem -- One item in the CompletionModel.
CompletionFilterModel -- A QSortFilterProxyModel subclass for completions.
"""
from collections import OrderedDict from collections import OrderedDict
from PyQt5.QtCore import (QAbstractItemModel, Qt, QModelIndex, QVariant, from PyQt5.QtCore import (QAbstractItemModel, Qt, QModelIndex, QVariant,
@ -5,6 +14,12 @@ from PyQt5.QtCore import (QAbstractItemModel, Qt, QModelIndex, QVariant,
class CompletionModel(QAbstractItemModel): class CompletionModel(QAbstractItemModel):
"""A simple tree model based on Python OrderdDict containing tuples.
Used for showing completions later in the CompletionView.
"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._data = OrderedDict() self._data = OrderedDict()
@ -12,22 +27,40 @@ class CompletionModel(QAbstractItemModel):
self.root = CompletionItem([""] * 2) self.root = CompletionItem([""] * 2)
def removeRows(self, position=0, count=1, parent=QModelIndex()): def removeRows(self, position=0, count=1, parent=QModelIndex()):
node = self.node(parent) """Remove rows from the model.
Overrides QAbstractItemModel::removeRows.
"""
node = self._node(parent)
self.beginRemoveRows(parent, position, position + count - 1) self.beginRemoveRows(parent, position, position + count - 1)
node.children.pop(position) node.children.pop(position)
self.endRemoveRows() self.endRemoveRows()
def node(self, index): def _node(self, index):
"""Return the interal data representation for index.
Returns the CompletionItem for index, or the root CompletionItem if the
index was invalid.
"""
if index.isValid(): if index.isValid():
return index.internalPointer() return index.internalPointer()
else: else:
return self.root return self.root
def columnCount(self, parent=QModelIndex()): def columnCount(self, parent=QModelIndex()):
"""Return the column count in the model.
Overrides QAbstractItemModel::columnCount.
"""
# pylint: disable=unused-argument # pylint: disable=unused-argument
return self.root.column_count() return self.root.column_count()
def data(self, index, role=Qt.DisplayRole): def data(self, index, role=Qt.DisplayRole):
"""Return the data for role/index as QVariant.
Returns an invalid QVariant on error.
Overrides QAbstractItemModel::data.
"""
if not index.isValid(): if not index.isValid():
return QVariant() return QVariant()
item = index.internalPointer() item = index.internalPointer()
@ -37,6 +70,11 @@ class CompletionModel(QAbstractItemModel):
return QVariant() return QVariant()
def flags(self, index): def flags(self, index):
"""Return the item flags for index.
Returns Qt.NoItemFlags on error.
Overrides QAbstractItemModel::flags.
"""
# FIXME categories are not selectable, but moving via arrow keys still # FIXME categories are not selectable, but moving via arrow keys still
# tries to select them # tries to select them
if not index.isValid(): if not index.isValid():
@ -48,11 +86,21 @@ class CompletionModel(QAbstractItemModel):
return flags | Qt.ItemIsSelectable return flags | Qt.ItemIsSelectable
def headerData(self, section, orientation, role=Qt.DisplayRole): def headerData(self, section, orientation, role=Qt.DisplayRole):
"""Return the header data for role/index as QVariant.
Returns an invalid QVariant on error.
Overrides QAbstractItemModel::headerData.
"""
if orientation == Qt.Horizontal and role == Qt.DisplayRole: if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.root.data(section)) return QVariant(self.root.data(section))
return None return QVariant()
def setData(self, index, value, role=Qt.EditRole): def setData(self, index, value, role=Qt.EditRole):
"""Set the data for role/index to value.
Returns True on success, False on failure.
Overrides QAbstractItemModel::setData.
"""
if not index.isValid(): if not index.isValid():
return False return False
item = index.internalPointer() item = index.internalPointer()
@ -64,6 +112,11 @@ class CompletionModel(QAbstractItemModel):
return True return True
def index(self, row, column, parent=QModelIndex()): def index(self, row, column, parent=QModelIndex()):
"""Return the QModelIndex for row/column/parent.
Returns an invalid QModelIndex on failure.
Overrides QAbstractItemModel::index.
"""
if (0 <= row < self.rowCount(parent) and if (0 <= row < self.rowCount(parent) and
0 <= column < self.columnCount(parent)): 0 <= column < self.columnCount(parent)):
pass pass
@ -82,6 +135,11 @@ class CompletionModel(QAbstractItemModel):
return QModelIndex() return QModelIndex()
def parent(self, index): def parent(self, index):
"""Return the QModelIndex of the parent of the object behind index.
Returns an invalid QModelIndex on failure.
Overrides QAbstractItemModel::parent.
"""
if not index.isValid(): if not index.isValid():
return QModelIndex() return QModelIndex()
item = index.internalPointer().parent item = index.internalPointer().parent
@ -90,6 +148,11 @@ class CompletionModel(QAbstractItemModel):
return self.createIndex(item.row(), 0, item) return self.createIndex(item.row(), 0, item)
def rowCount(self, parent=QModelIndex()): def rowCount(self, parent=QModelIndex()):
"""Return the rowCount (children count) for a parent.
Uses the root frame if parent is invalid.
Overrides QAbstractItemModel::data.
"""
if parent.column() > 0: if parent.column() > 0:
return 0 return 0
@ -101,9 +164,15 @@ class CompletionModel(QAbstractItemModel):
return len(pitem.children) return len(pitem.children)
def sort(self, column, order=Qt.AscendingOrder): def sort(self, column, order=Qt.AscendingOrder):
"""Sort the data in column according to order.
Raises NotImplementedError, should be overwritten in a superclass.
Overrides QAbstractItemModel::sort.
"""
raise NotImplementedError raise NotImplementedError
def init_data(self): def init_data(self):
"""Initialize the Qt model based on the data in self._data."""
for (cat, items) in self._data.items(): for (cat, items) in self._data.items():
newcat = CompletionItem([cat], self.root) newcat = CompletionItem([cat], self.root)
self.root.children.append(newcat) self.root.children.append(newcat)
@ -112,6 +181,10 @@ class CompletionModel(QAbstractItemModel):
newcat.children.append(newitem) newcat.children.append(newitem)
def mark_all_items(self, needle): def mark_all_items(self, needle):
"""Mark a string in all items (children of root-children).
needle -- The string to mark.
"""
for i in range(self.rowCount()): for i in range(self.rowCount()):
cat = self.index(i, 0) cat = self.index(i, 0)
for k in range(self.rowCount(cat)): for k in range(self.rowCount(cat)):
@ -121,6 +194,7 @@ class CompletionModel(QAbstractItemModel):
self.setData(idx, marks, Qt.UserRole) self.setData(idx, marks, Qt.UserRole)
def _get_marks(self, needle, haystack): def _get_marks(self, needle, haystack):
"""Return the marks for needle in haystack."""
pos1 = pos2 = 0 pos1 = pos2 = 0
marks = [] marks = []
if not needle: if not needle:
@ -135,18 +209,29 @@ class CompletionModel(QAbstractItemModel):
class CompletionItem(): class CompletionItem():
"""An item (row) in a CompletionModel."""
parent = None parent = None
_data = None
children = None children = None
_data = None
_marks = None _marks = None
def __init__(self, data, parent=None): def __init__(self, data, parent=None):
"""Constructor for CompletionItem.
data -- The data for the model, as tuple (columns).
parent -- An optional parent item.
"""
self.parent = parent self.parent = parent
self._data = data self._data = data
self.children = [] self.children = []
self._marks = [] self._marks = []
def data(self, column, role=Qt.DisplayRole): def data(self, column, role=Qt.DisplayRole):
"""Get the data for role/column.
Raise ValueError if the role is invalid.
"""
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
return self._data[column] return self._data[column]
elif role == Qt.UserRole: elif role == Qt.UserRole:
@ -155,6 +240,10 @@ class CompletionItem():
raise ValueError raise ValueError
def setdata(self, column, value, role=Qt.DisplayRole): def setdata(self, column, value, role=Qt.DisplayRole):
"""Set the data for column/role to value.
Raise ValueError if the role is invalid.
"""
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
self._data[column] = value self._data[column] = value
elif role == Qt.UserRole: elif role == Qt.UserRole:
@ -163,33 +252,53 @@ class CompletionItem():
raise ValueError raise ValueError
def column_count(self): def column_count(self):
"""Return the column count in the item."""
return len(self._data) return len(self._data)
def row(self): def row(self):
"""Return the row index (int) of the item, or 0 if it's a root item."""
if self.parent: if self.parent:
return self.parent.children.index(self) return self.parent.children.index(self)
return 0 return 0
class CompletionFilterModel(QSortFilterProxyModel): class CompletionFilterModel(QSortFilterProxyModel):
_pattern = None
"""Subclass of QSortFilterProxyModel with custom sorting/filtering."""
pattern_changed = pyqtSignal(str) pattern_changed = pyqtSignal(str)
_pattern = None
@property
def pattern(self):
return self._pattern
@pattern.setter
def pattern(self, val):
self._pattern = val
self.invalidate()
self.pattern_changed.emit(val)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.pattern = '' self.pattern = ''
@property
def pattern(self):
"""Getter for pattern."""
return self._pattern
@pattern.setter
def pattern(self, val):
"""Setter for pattern.
Invalidates the filter and emits pattern_changed.
"""
self._pattern = val
self.invalidate()
self.pattern_changed.emit(val)
def filterAcceptsRow(self, row, parent): def filterAcceptsRow(self, row, parent):
"""Custom filter implementation.
Overrides QSortFilterProxyModel::filterAcceptsRow.
row -- The row of the item.
parent -- The parent item QModelIndex.
Returns True if self.pattern is contained in item, or if it's a root
item (category). Else returns False.
"""
if parent == QModelIndex(): if parent == QModelIndex():
return True return True
idx = self.sourceModel().index(row, 0, parent) idx = self.sourceModel().index(row, 0, parent)
@ -200,6 +309,14 @@ class CompletionFilterModel(QSortFilterProxyModel):
return self.pattern in data return self.pattern in data
def lessThan(self, lindex, rindex): def lessThan(self, lindex, rindex):
"""Custom sorting implementation.
lindex -- The QModelIndex of the left item (*left* < right)
rindex -- The QModelIndex of the right item (left < *right*)
Prefers all items which start with self.pattern. Other than that, uses
normal Python string sorting.
"""
left = self.sourceModel().data(lindex).value() left = self.sourceModel().data(lindex).value()
right = self.sourceModel().data(rindex).value() right = self.sourceModel().data(rindex).value()
@ -216,9 +333,11 @@ class CompletionFilterModel(QSortFilterProxyModel):
return left < right return left < right
def first_item(self): def first_item(self):
"""Returns the first item in the model."""
cat = self.index(0, 0) cat = self.index(0, 0)
return self.index(0, 0, cat) return self.index(0, 0, cat)
def last_item(self): def last_item(self):
"""Returns the last item in the model."""
cat = self.index(self.rowCount() - 1, 0) cat = self.index(self.rowCount() - 1, 0)
return self.index(self.rowCount(cat) - 1, 0, cat) return self.index(self.rowCount(cat) - 1, 0, cat)

View File

@ -1,3 +1,11 @@
"""Configuration storage and config-related utilities.
config -- The main Config object.
colordict -- All configured colors.
default_config -- The default config as dict.
MONOSPACE -- A list of suitable monospace fonts.
"""
import os.path import os.path
import os import os
import logging import logging
@ -61,6 +69,7 @@ MONOSPACE = ', '.join(_MONOSPACE)
def init(confdir): def init(confdir):
"""Initialize the global objects based on the config in configdir."""
global config, colordict global config, colordict
config = Config(confdir) config = Config(confdir)
try: try:
@ -70,11 +79,26 @@ def init(confdir):
def get_stylesheet(template): def get_stylesheet(template):
"""Return a formatted stylesheet based on a template."""
return template.strip().format(color=colordict, monospace=MONOSPACE) return template.strip().format(color=colordict, monospace=MONOSPACE)
class ColorDict(dict): class ColorDict(dict):
"""A dict aimed at Qt stylesheet colors."""
def __getitem__(self, key): def __getitem__(self, key):
"""Override dict __getitem__.
If a value wasn't found, return an empty string.
(Color not defined, so no output in the stylesheet)
If the key has a .fg. element in it, return color: X;.
If the key has a .bg. element in it, return background-color: X;.
In all other cases, return the plain value.
"""
try: try:
val = super().__getitem__(key) val = super().__getitem__(key)
except KeyError: except KeyError:
@ -87,6 +111,10 @@ class ColorDict(dict):
return val return val
def getraw(self, key): def getraw(self, key):
"""Get a value without the transformations done in __getitem__.
Returns a value, or None if the value wasn't found.
"""
try: try:
return super().__getitem__(key) return super().__getitem__(key)
except KeyError: except KeyError:
@ -94,12 +122,16 @@ class ColorDict(dict):
class Config(ConfigParser): class Config(ConfigParser):
"""Our own ConfigParser""" """Our own ConfigParser subclass."""
configdir = None configdir = None
FNAME = 'config' FNAME = 'config'
def __init__(self, configdir): def __init__(self, configdir):
"""configdir: directory to store the config in""" """Config constructor.
configdir -- directory to store the config in.
"""
super().__init__() super().__init__()
self.optionxform = lambda opt: opt # be case-insensitive self.optionxform = lambda opt: opt # be case-insensitive
self.configdir = configdir self.configdir = configdir
@ -113,6 +145,7 @@ class Config(ConfigParser):
self.read(self.configfile) self.read(self.configfile)
def init_config(self): def init_config(self):
"""Initialize Config from default_config and save it."""
logging.info("Initializing default config.") logging.info("Initializing default config.")
if self.configdir is None: if self.configdir is None:
self.read_dict(default_config) self.read_dict(default_config)
@ -126,6 +159,7 @@ class Config(ConfigParser):
cp.write(f) cp.write(f)
def save(self): def save(self):
"""Save the config file."""
if self.configdir is None: if self.configdir is None:
return return
if not os.path.exists(self.configdir): if not os.path.exists(self.configdir):

View File

@ -1,3 +1,10 @@
"""The main browser widget.
Defines BrowserTab (our own QWebView subclass) and TabbedBrowser (a TabWidget
containing BrowserTabs).
"""
import logging import logging
from PyQt5.QtWidgets import QShortcut from PyQt5.QtWidgets import QShortcut
@ -11,7 +18,12 @@ from qutebrowser.widgets.tabbar import TabWidget
class TabbedBrowser(TabWidget): class TabbedBrowser(TabWidget):
"""A TabWidget with QWebViews inside""" """A TabWidget with QWebViews inside.
Provides methods to manage tabs, convenience methods to interact with the
current tab (cur_*) and filters signals to re-emit them when they occured
in the currently visible tab.
"""
cur_progress = pyqtSignal(int) # Progress of the current tab changed cur_progress = pyqtSignal(int) # Progress of the current tab changed
cur_load_started = pyqtSignal() # Current tab started loading cur_load_started = pyqtSignal() # Current tab started loading
@ -33,7 +45,10 @@ class TabbedBrowser(TabWidget):
space.activated.connect(self.space_scroll) space.activated.connect(self.space_scroll)
def tabopen(self, url): def tabopen(self, url):
"""Opens a new tab with a given url""" """Open a new tab with a given url.
Also connect all the signals we need to _filter_signals.
"""
url = utils.qurl(url) url = utils.qurl(url)
tab = BrowserTab(self) tab = BrowserTab(self)
tab.openurl(url) tab.openurl(url)
@ -60,16 +75,26 @@ class TabbedBrowser(TabWidget):
tab.open_tab.connect(self.tabopen) tab.open_tab.connect(self.tabopen)
def openurl(self, url): def openurl(self, url):
"""Opens an url in the current tab""" """Open an url in the current tab.
Command handler for :open.
url -- The URL to open.
"""
self.currentWidget().openurl(url) self.currentWidget().openurl(url)
def undo_close(self): def undo_close(self):
"""Undos closing a tab""" """Undo closing a tab.
Command handler for :undo.
"""
if self._url_stack: if self._url_stack:
self.tabopen(self._url_stack.pop()) self.tabopen(self._url_stack.pop())
def cur_close(self): def cur_close(self):
"""Closes the current tab""" """Close the current tab.
Command handler for :close.
"""
if self.count() > 1: if self.count() > 1:
idx = self.currentIndex() idx = self.currentIndex()
tab = self.currentWidget() tab = self.currentWidget()
@ -81,32 +106,50 @@ class TabbedBrowser(TabWidget):
pass pass
def cur_reload(self): def cur_reload(self):
"""Reloads the current tab""" """Reload the current tab.
Command handler for :reload.
"""
self.currentWidget().reload() self.currentWidget().reload()
def cur_stop(self): def cur_stop(self):
"""Stops loading in the current tab""" """Stop loading in the current tab.
Command handler for :stop.
"""
self.currentWidget().stop() self.currentWidget().stop()
def cur_print(self): def cur_print(self):
"""Prints the current tab""" """Print the current tab.
Command handler for :print.
"""
# FIXME that does not what I expect # FIXME that does not what I expect
preview = QPrintPreviewDialog() preview = QPrintPreviewDialog()
preview.paintRequested.connect(self.currentWidget().print) preview.paintRequested.connect(self.currentWidget().print)
preview.exec_() preview.exec_()
def cur_back(self): def cur_back(self):
"""Goes back in the history of the current tab""" """Go back in the history of the current tab.
Command handler for :back.
"""
# FIXME display warning if beginning of history # FIXME display warning if beginning of history
self.currentWidget().back() self.currentWidget().back()
def cur_forward(self): def cur_forward(self):
"""Goes forward in the history of the current tab""" """Go forward in the history of the current tab.
Command handler for :forward.
"""
# FIXME display warning if end of history # FIXME display warning if end of history
self.currentWidget().forward() self.currentWidget().forward()
def cur_scroll(self, dx, dy, count=None): def cur_scroll(self, dx, dy, count=None):
"""Scrolls the current tab by count * dx/dy""" """Scroll the current tab by count * dx/dy
Command handler for :scroll.
"""
if count is None: if count is None:
count = 1 count = 1
dx = int(count) * int(dx) dx = int(count) * int(dx)
@ -114,18 +157,23 @@ class TabbedBrowser(TabWidget):
self.currentWidget().page().mainFrame().scroll(dx, dy) self.currentWidget().page().mainFrame().scroll(dx, dy)
def cur_scroll_percent_x(self, perc=None, count=None): def cur_scroll_percent_x(self, perc=None, count=None):
"""Scrolls the current tab to a specific percent of the page. """Scroll the current tab to a specific percent of the page.
Accepts percentage either as argument, or as count. Accepts percentage either as argument, or as count.
Command handler for :scroll_perc_x.
""" """
self._cur_scroll_percent(perc, count, Qt.Horizontal) self._cur_scroll_percent(perc, count, Qt.Horizontal)
def cur_scroll_percent_y(self, perc=None, count=None): def cur_scroll_percent_y(self, perc=None, count=None):
"""Scrolls the current tab to a specific percent of the page """Scroll the current tab to a specific percent of the page
Accepts percentage either as argument, or as count. Accepts percentage either as argument, or as count.
Command handler for :scroll_perc_y
""" """
self._cur_scroll_percent(perc, count, Qt.Vertical) self._cur_scroll_percent(perc, count, Qt.Vertical)
def _cur_scroll_percent(self, perc=None, count=None, orientation=None): def _cur_scroll_percent(self, perc=None, count=None, orientation=None):
"""Inner logic for cur_scroll_percent_(x|y)."""
if perc is None and count is None: if perc is None and count is None:
perc = 100 perc = 100
elif perc is None: elif perc is None:
@ -138,15 +186,11 @@ class TabbedBrowser(TabWidget):
return return
frame.setScrollBarValue(orientation, int(m * perc / 100)) frame.setScrollBarValue(orientation, int(m * perc / 100))
def space_scroll(self):
try:
amount = config.config['general']['space_scroll']
except KeyError:
amount = 200
self.cur_scroll(0, amount)
def switch_prev(self): def switch_prev(self):
"""Switches to the previous tab""" """Switch to the previous tab.
Command handler for :tabprev.
"""
idx = self.currentIndex() idx = self.currentIndex()
if idx > 0: if idx > 0:
self.setCurrentIndex(idx - 1) self.setCurrentIndex(idx - 1)
@ -155,7 +199,10 @@ class TabbedBrowser(TabWidget):
pass pass
def switch_next(self): def switch_next(self):
"""Switches to the next tab""" """Switch to the next tab.
Command handler for :tabnext.
"""
idx = self.currentIndex() idx = self.currentIndex()
if idx < self.count() - 1: if idx < self.count() - 1:
self.setCurrentIndex(idx + 1) self.setCurrentIndex(idx + 1)
@ -163,37 +210,69 @@ class TabbedBrowser(TabWidget):
# FIXME # FIXME
pass pass
def space_scroll(self):
"""Scroll when space is pressed.
This gets called from the space QShortcut in __init__.
"""
try:
amount = config.config['general']['space_scroll']
except KeyError:
amount = 200
self.cur_scroll(0, amount)
def keyPressEvent(self, e): def keyPressEvent(self, e):
"""Extend TabWidget (QWidget)'s keyPressEvent to emit a signal."""
self.keypress.emit(e) self.keypress.emit(e)
super().keyPressEvent(e) super().keyPressEvent(e)
def _titleChanged_handler(self, text): def _titleChanged_handler(self, text):
"""Set the title of a tab.
Slot for the titleChanged signal of any tab.
"""
if text: if text:
self.setTabText(self.indexOf(self.sender()), text) self.setTabText(self.indexOf(self.sender()), text)
def _loadStarted_handler(self): def _loadStarted_handler(self):
"""Set url as the title of a tab after it loaded.
Slot for the loadStarted signal of any tab.
"""
s = self.sender() s = self.sender()
self.setTabText(self.indexOf(s), s.url().toString()) self.setTabText(self.indexOf(s), s.url().toString())
def _filter_signals(self, signal, *args): def _filter_signals(self, signal, *args):
"""Filters signals, and triggers TabbedBrowser signals if the signal """Filter signals and trigger TabbedBrowser signals if the signal
was sent from the _current_ tab and not from any other one. was sent from the _current_ tab and not from any other one.
The original signal does not matter, since we get the new signal and
all args.
signal -- The signal to emit if the sender was the current widget.
*args -- The args to pass to the signal.
""" """
dbgstr = "{} ({})".format( dbgstr = "{} ({})".format(
signal.signal, ','.join([str(e) for e in args])) signal.signal, ','.join([str(e) for e in args]))
if self.currentWidget() == self.sender(): if self.currentWidget() == self.sender():
logging.debug('{} - emitting'.format(dbgstr)) logging.debug('{} - emitting'.format(dbgstr))
signal.emit(*args) return signal.emit(*args)
else: else:
logging.debug('{} - ignoring'.format(dbgstr)) logging.debug('{} - ignoring'.format(dbgstr))
def _currentChanged_handler(self, idx): def _currentChanged_handler(self, idx):
"""Update status bar values when a tab was changed.
Slot for the currentChanged signal of any tab.
"""
tab = self.widget(idx) tab = self.widget(idx)
self.cur_progress.emit(tab.progress) self.cur_progress.emit(tab.progress)
def _scroll_pos_changed_handler(self, x, y): def _scroll_pos_changed_handler(self, x, y):
"""Gets the new position from a BrowserTab. If it's the current tab, it """Get the new position from a BrowserTab. If it's the current tab,
calculates the percentage and emits cur_scroll_perc_changed. calculate the percentage and emits cur_scroll_perc_changed.
Slot for the scroll_pos_changed signal of any tab.
""" """
sender = self.sender() sender = self.sender()
if sender != self.currentWidget(): if sender != self.currentWidget():
@ -207,12 +286,15 @@ class TabbedBrowser(TabWidget):
class BrowserTab(QWebView): class BrowserTab(QWebView):
"""One browser tab in TabbedBrowser""" """One browser tab in TabbedBrowser.
Our own subclass of a QWebView with some added bells and whistles.
"""
progress = 0 progress = 0
scroll_pos_changed = pyqtSignal(int, int) scroll_pos_changed = pyqtSignal(int, int)
_scroll_pos = (-1, -1)
open_new_tab = False # open new tab for the next action
open_tab = pyqtSignal('QUrl') open_tab = pyqtSignal('QUrl')
_scroll_pos = (-1, -1)
_open_new_tab = False # open new tab for the next action
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
@ -224,16 +306,32 @@ class BrowserTab(QWebView):
self.show() self.show()
def openurl(self, url): def openurl(self, url):
"""Opens an URL in the browser""" """Open an URL in the browser.
url -- The URL to load, as string or QUrl.
"""
return self.load(utils.qurl(url)) return self.load(utils.qurl(url))
def link_handler(self, url): def link_handler(self, url):
if self.open_new_tab: """Handle a link.
Called from the linkClicked signal. Checks if it should open it in a
tab (middle-click or control) or not, and does so.
url -- The url to handle, as string or QUrl.
"""
if self._open_new_tab:
self.open_tab.emit(url) self.open_tab.emit(url)
else: else:
self.openurl(url) self.openurl(url)
def set_progress(self, prog): def set_progress(self, prog):
"""Update the progress property if the loading progress changed.
Slot for the loadProgress signal.
prog -- New progress.
"""
self.progress = prog self.progress = prog
def eventFilter(self, watched, e): def eventFilter(self, watched, e):
@ -242,6 +340,9 @@ class BrowserTab(QWebView):
We listen to repaint requests here, in the hope a repaint will always We listen to repaint requests here, in the hope a repaint will always
be requested when scrolling, and if the scroll position actually be requested when scrolling, and if the scroll position actually
changed, we emit a signal. changed, we emit a signal.
watched -- The watched Qt object.
e -- The new event.
""" """
if watched == self and e.type() == QEvent.Paint: if watched == self and e.type() == QEvent.Paint:
frame = self.page().mainFrame() frame = self.page().mainFrame()
@ -254,10 +355,16 @@ class BrowserTab(QWebView):
return super().eventFilter(watched, e) return super().eventFilter(watched, e)
def event(self, e): def event(self, e):
"""Another hack to see when a link was pressed with the middle """Check if a link was clicked with the middle button or Ctrl.
button
Extends the superclass event().
This also is a bit of a hack, but it seems it's the only possible way.
Set the _open_new_tab attribute accordingly.
e -- The arrived event
""" """
if e.type() in [QEvent.MouseButtonPress, QEvent.MouseButtonDblClick]: if e.type() in [QEvent.MouseButtonPress, QEvent.MouseButtonDblClick]:
self.open_new_tab = (e.button() == Qt.MidButton or self._open_new_tab = (e.button() == Qt.MidButton or
e.modifiers() & Qt.ControlModifier) e.modifiers() & Qt.ControlModifier)
return super().event(e) return super().event(e)

View File

@ -157,27 +157,41 @@ class CompletionView(QTreeView):
self.show() self.show()
def tab_handler(self, shift): def tab_handler(self, shift):
"""Handle a tab press for the CompletionView.
Selects the previous/next item and writes the new text to the
statusbar. Called by key_(s)tab_handler in statusbar.command.
shift -- Whether shift is pressed or not.
"""
if not self.completing: if not self.completing:
# No completion running at the moment, ignore keypress
return return
idx = self._next_idx(shift) idx = self._next_idx(shift)
self.ignore_next = True
self.selectionModel().setCurrentIndex( self.selectionModel().setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect) idx, QItemSelectionModel.ClearAndSelect)
data = self.model.data(idx) data = self.model.data(idx)
if data is not None: if data is not None:
self.ignore_next = True
self.append_cmd_text.emit(self.model.data(idx) + ' ') self.append_cmd_text.emit(self.model.data(idx) + ' ')
def _next_idx(self, shift): def _next_idx(self, upwards):
"""Get the previous/next QModelIndex displayed in the view.
Used by tab_handler.
upwards -- Get previous item, not next.
"""
idx = self.selectionModel().currentIndex() idx = self.selectionModel().currentIndex()
if not idx.isValid(): if not idx.isValid():
# No item selected yet # No item selected yet
return self.model.first_item() return self.model.first_item()
while True: while True:
idx = self.indexAbove(idx) if shift else self.indexBelow(idx) idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
# wrap around if we arrived at beginning/end # wrap around if we arrived at beginning/end
if not idx.isValid() and shift: if not idx.isValid() and upwards:
return self.model.last_item() return self.model.last_item()
elif not idx.isValid() and not shift: elif not idx.isValid() and not upwards:
return self.model.first_item() return self.model.first_item()
elif idx.parent().isValid(): elif idx.parent().isValid():
# Item is a real item, not a category header -> success # Item is a real item, not a category header -> success
@ -185,11 +199,21 @@ class CompletionView(QTreeView):
class CompletionItemDelegate(QStyledItemDelegate): class CompletionItemDelegate(QStyledItemDelegate):
"""Delegate used by CompletionView to draw individual items.
Mainly a cleaned up port of Qt's way to draw a TreeView item, except it
uses a QTextDocument to draw the text and add marking.
Original implementation:
qt/src/gui/styles/qcommonstyle.cpp:drawControl:2153
"""
opt = None opt = None
style = None style = None
painter = None painter = None
def paint(self, painter, option, index): def paint(self, painter, option, index):
"""Overrides the QStyledItemDelegate paint function."""
painter.save() painter.save()
self.painter = painter self.painter = painter
@ -205,10 +229,12 @@ class CompletionItemDelegate(QStyledItemDelegate):
painter.restore() painter.restore()
def _draw_background(self): def _draw_background(self):
"""Draw the background of an ItemViewItem"""
self.style.drawPrimitive(self.style.PE_PanelItemViewItem, self.opt, self.style.drawPrimitive(self.style.PE_PanelItemViewItem, self.opt,
self.painter, self.opt.widget) self.painter, self.opt.widget)
def _draw_icon(self): def _draw_icon(self):
"""Draw the icon of an ItemViewItem"""
icon_rect = self.style.subElementRect( icon_rect = self.style.subElementRect(
self.style.SE_ItemViewItemDecoration, self.opt, self.opt.widget) self.style.SE_ItemViewItemDecoration, self.opt, self.opt.widget)
@ -222,6 +248,13 @@ class CompletionItemDelegate(QStyledItemDelegate):
self.opt.decorationAlignment, mode, state) self.opt.decorationAlignment, mode, state)
def _draw_text(self, index): def _draw_text(self, index):
"""Draw the text of an ItemViewItem.
This is the main part where we differ from the original implementation
in Qt: We use a QTextDocument to draw text.
index -- The QModelIndex of the item to draw.
"""
if not self.opt.text: if not self.opt.text:
return return
@ -258,6 +291,11 @@ class CompletionItemDelegate(QStyledItemDelegate):
self.painter.restore() self.painter.restore()
def _draw_textdoc(self, index, text_rect): def _draw_textdoc(self, index, text_rect):
"""Draw the QTextDocument of an item.
index -- The QModelIndex of the item to draw.
text_rect -- The QRect to clip the drawing to.
"""
# FIXME we probably should do eliding here. See # FIXME we probably should do eliding here. See
# qcommonstyle.cpp:viewItemDrawText # qcommonstyle.cpp:viewItemDrawText
clip = QRectF(0, 0, text_rect.width(), text_rect.height()) clip = QRectF(0, 0, text_rect.width(), text_rect.height())
@ -298,6 +336,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
doc.drawContents(self.painter, clip) doc.drawContents(self.painter, clip)
def _draw_focus_rect(self): def _draw_focus_rect(self):
"""Draws the focus rectangle of an ItemViewItem"""
state = self.opt.state state = self.opt.state
if not state & QStyle.State_HasFocus: if not state & QStyle.State_HasFocus:
return return

View File

@ -1,3 +1,5 @@
"""The main window of QuteBrowser."""
from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QWidget from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QWidget
from qutebrowser.widgets.statusbar import StatusBar from qutebrowser.widgets.statusbar import StatusBar
@ -6,7 +8,11 @@ from qutebrowser.widgets.completion import CompletionView
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
"""The main window of QuteBrowser""" """The main window of QuteBrowser.
Adds all needed components to a vbox, initializes subwidgets and connects
signals.
"""
cwidget = None cwidget = None
vbox = None vbox = None
tabs = None tabs = None

View File

@ -69,5 +69,9 @@ class StatusBar(QWidget):
self.txt.error = '' self.txt.error = ''
def resizeEvent(self, e): def resizeEvent(self, e):
"""Override resizeEvent of QWidget to emit a resized signal afterwards.
e -- The QResizeEvent.
"""
super().resizeEvent(e) super().resizeEvent(e)
self.resized.emit(self.geometry()) self.resized.emit(self.geometry())

View File

@ -1,3 +1,4 @@
"""The commandline part of the statusbar."""
import logging import logging
from PyQt5.QtWidgets import QLineEdit, QShortcut from PyQt5.QtWidgets import QLineEdit, QShortcut
@ -6,7 +7,7 @@ from PyQt5.QtGui import QValidator, QKeySequence
class Command(QLineEdit): class Command(QLineEdit):
"""The commandline part of the statusbar""" """The commandline part of the statusbar."""
# Emitted when a command is triggered by the user # Emitted when a command is triggered by the user
got_cmd = pyqtSignal(str) got_cmd = pyqtSignal(str)
statusbar = None # The status bar object statusbar = None # The status bar object
@ -30,18 +31,20 @@ class Command(QLineEdit):
self.returnPressed.connect(self.process_cmd) self.returnPressed.connect(self.process_cmd)
self.textEdited.connect(self._histbrowse_stop) self.textEdited.connect(self._histbrowse_stop)
for (key, handler) in [(Qt.Key_Escape, self.esc_pressed), for (key, handler) in [
(Qt.Key_Escape, self.esc_pressed),
(Qt.Key_Up, self.key_up_handler), (Qt.Key_Up, self.key_up_handler),
(Qt.Key_Down, self.key_down_handler), (Qt.Key_Down, self.key_down_handler),
(Qt.Key_Tab | Qt.SHIFT, self.key_stab_handler), (Qt.Key_Tab | Qt.SHIFT, lambda: self.tab_pressed.emit(True)),
(Qt.Key_Tab, self.key_tab_handler)]: (Qt.Key_Tab, lambda: self.tab_pressed.emit(False))
]:
sc = QShortcut(self) sc = QShortcut(self)
sc.setKey(QKeySequence(key)) sc.setKey(QKeySequence(key))
sc.setContext(Qt.WidgetWithChildrenShortcut) sc.setContext(Qt.WidgetWithChildrenShortcut)
sc.activated.connect(handler) sc.activated.connect(handler)
def process_cmd(self): def process_cmd(self):
"""Handle the command in the status bar""" """Handle the command in the status bar."""
self._histbrowse_stop() self._histbrowse_stop()
text = self.text().lstrip(':') text = self.text().lstrip(':')
if not self.history or text != self.history[-1]: if not self.history or text != self.history[-1]:
@ -50,18 +53,18 @@ class Command(QLineEdit):
self.got_cmd.emit(text) self.got_cmd.emit(text)
def set_cmd(self, text): def set_cmd(self, text):
"""Preset the statusbar to some text""" """Preset the statusbar to some text."""
self.setText(':' + text) self.setText(':' + text)
self.setFocus() self.setFocus()
def append_cmd(self, text): def append_cmd(self, text):
"""Append text to the commandline""" """Append text to the commandline."""
# FIXME do the right thing here # FIXME do the right thing here
self.setText(':' + text) self.setText(':' + text)
self.setFocus() self.setFocus()
def focusOutEvent(self, e): def focusOutEvent(self, e):
"""Clear the statusbar text if it's explicitely unfocused""" """Clear the statusbar text if it's explicitely unfocused."""
if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason, if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason,
Qt.BacktabFocusReason, Qt.OtherFocusReason]: Qt.BacktabFocusReason, Qt.OtherFocusReason]:
self.setText('') self.setText('')
@ -70,11 +73,18 @@ class Command(QLineEdit):
super().focusOutEvent(e) super().focusOutEvent(e)
def focusInEvent(self, e): def focusInEvent(self, e):
"""Clear error message when the statusbar is focused""" """Clear error message when the statusbar is focused."""
self.statusbar.clear_error() self.statusbar.clear_error()
super().focusInEvent(e) super().focusInEvent(e)
def _histbrowse_start(self): def _histbrowse_start(self):
"""Start browsing to the history.
Called when the user presses the up/down key and wasn't browsing the
history already.
"""
pre = self.text().strip().lstrip(':') pre = self.text().strip().lstrip(':')
logging.debug('Preset text: "{}"'.format(pre)) logging.debug('Preset text: "{}"'.format(pre))
if pre: if pre:
@ -84,9 +94,11 @@ class Command(QLineEdit):
self._histpos = len(self._tmphist) - 1 self._histpos = len(self._tmphist) - 1
def _histbrowse_stop(self): def _histbrowse_stop(self):
"""Stop browsing the history."""
self._histpos = None self._histpos = None
def key_up_handler(self): def key_up_handler(self):
"""Handle Up presses (go back in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos)) logging.debug("history up [pre]: pos {}".format(self._histpos))
if self._histpos is None: if self._histpos is None:
self._histbrowse_start() self._histbrowse_start()
@ -101,6 +113,7 @@ class Command(QLineEdit):
self.set_cmd(self._tmphist[self._histpos]) self.set_cmd(self._tmphist[self._histpos])
def key_down_handler(self): def key_down_handler(self):
"""Handle Down presses (go forward in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos, logging.debug("history up [pre]: pos {}".format(self._histpos,
self._tmphist, len(self._tmphist), self._histpos)) self._tmphist, len(self._tmphist), self._histpos))
if (self._histpos is None or if (self._histpos is None or
@ -112,16 +125,20 @@ class Command(QLineEdit):
self._tmphist, len(self._tmphist), self._histpos)) self._tmphist, len(self._tmphist), self._histpos))
self.set_cmd(self._tmphist[self._histpos]) self.set_cmd(self._tmphist[self._histpos])
def key_tab_handler(self):
self.tab_pressed.emit(False)
def key_stab_handler(self):
self.tab_pressed.emit(True)
class Validator(QValidator): class Validator(QValidator):
"""Validator to prevent the : from getting deleted""" """Validator to prevent the : from getting deleted"""
def validate(self, string, pos): def validate(self, string, pos):
"""Overrides QValidator::validate.
string -- The string to validate.
pos -- The current curser position.
Returns a tuple (status, string, pos) as a QValidator should.\
"""
if string.startswith(':'): if string.startswith(':'):
return (QValidator.Acceptable, string, pos) return (QValidator.Acceptable, string, pos)
else: else:

View File

@ -1,3 +1,5 @@
"""Widget to show the percentage of the page load in the statusbar."""
import qutebrowser.utils.config as config import qutebrowser.utils.config as config
from PyQt5.QtWidgets import QProgressBar, QSizePolicy from PyQt5.QtWidgets import QProgressBar, QSizePolicy
@ -5,7 +7,7 @@ from PyQt5.QtCore import QSize
class Progress(QProgressBar): class Progress(QProgressBar):
""" The progress bar part of the status bar""" """The progress bar part of the status bar."""
statusbar = None statusbar = None
color = None color = None
_stylesheet = """ _stylesheet = """
@ -30,21 +32,28 @@ class Progress(QProgressBar):
self.hide() self.hide()
def __setattr__(self, name, value): def __setattr__(self, name, value):
"""Update the stylesheet if relevant attributes have been changed""" """Update the stylesheet if relevant attributes have been changed."""
super().__setattr__(name, value) super().__setattr__(name, value)
if name == 'color' and value is not None: if name == 'color' and value is not None:
config.colordict['statusbar.progress.bg.__cur__'] = value config.colordict['statusbar.progress.bg.__cur__'] = value
self.setStyleSheet(config.get_stylesheet(self._stylesheet)) self.setStyleSheet(config.get_stylesheet(self._stylesheet))
def minimumSizeHint(self): def minimumSizeHint(self):
"""Return the size of the progress widget."""
status_size = self.statusbar.size() status_size = self.statusbar.size()
return QSize(100, status_size.height()) return QSize(100, status_size.height())
def sizeHint(self): def sizeHint(self):
"""Return the size of the progress widget.
Simply copied from minimumSizeHint because the SizePolicy is fixed.
"""
return self.minimumSizeHint() return self.minimumSizeHint()
def set_progress(self, prog): def set_progress(self, prog):
"""Sets the progress of the bar and shows/hides it if necessary""" """Set the progress of the bar and show/hide it if necessary."""
# TODO display failed loading in some meaningful way? # TODO display failed loading in some meaningful way?
if prog == 100: if prog == 100:
self.setValue(prog) self.setValue(prog)
@ -56,6 +65,12 @@ class Progress(QProgressBar):
self.show() self.show()
def load_finished(self, ok): def load_finished(self, ok):
"""Hide the progress bar or color it red, depending on ok.
Slot for the loadFinished signal of a QWebView.
"""
if ok: if ok:
self.hide() self.hide()
else: else:

View File

@ -1,9 +1,16 @@
"""The text part of the statusbar."""
import logging import logging
from PyQt5.QtWidgets import QLabel from PyQt5.QtWidgets import QLabel
class Text(QLabel): class Text(QLabel):
"""The text part of the status bar, composed of several 'widgets'"""
"""The text part of the status bar.
Contains several parts (keystring, error, text, scrollperc) which are later
joined and displayed."""
keystring = '' keystring = ''
error = '' error = ''
text = '' text = ''
@ -18,11 +25,11 @@ class Text(QLabel):
self.update() self.update()
def set_keystring(self, s): def set_keystring(self, s):
"""Setter to be used as a Qt slot""" """Setter to be used as a Qt slot."""
self.keystring = s self.keystring = s
def set_perc(self, x, y): def set_perc(self, x, y):
"""Setter to be used as a Qt slot""" """Setter to be used as a Qt slot."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
if y == 0: if y == 0:
self.scrollperc = '[top]' self.scrollperc = '[top]'
@ -32,10 +39,11 @@ class Text(QLabel):
self.scrollperc = '[{}%]'.format(y) self.scrollperc = '[{}%]'.format(y)
def set_text(self, text): def set_text(self, text):
"""Setter to be used as a Qt slot."""
logging.debug('Setting text to "{}"'.format(text)) logging.debug('Setting text to "{}"'.format(text))
self.text = text self.text = text
def update(self): def update(self):
"""Update the text displayed""" """Update the text displayed."""
self.setText(' '.join([self.keystring, self.error, self.text, self.setText(' '.join([self.keystring, self.error, self.text,
self.scrollperc])) self.scrollperc]))

View File

@ -1,3 +1,5 @@
"""The tab widget used for TabbedBrowser from browser.py."""
from PyQt5.QtWidgets import QTabWidget from PyQt5.QtWidgets import QTabWidget
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@ -5,7 +7,7 @@ import qutebrowser.utils.config as config
class TabWidget(QTabWidget): class TabWidget(QTabWidget):
"""The tabwidget used for TabbedBrowser""" """The tabwidget used for TabbedBrowser."""
# FIXME there is still some ugly 1px white stripe from somewhere if we do # FIXME there is still some ugly 1px white stripe from somewhere if we do
# background-color: grey for QTabBar... # background-color: grey for QTabBar...