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
import sys

View File

@ -1,3 +1,5 @@
""" Initialization of qutebrowser and application-wide things """
import sys
import logging
import faulthandler
@ -15,7 +17,13 @@ from qutebrowser.utils.appdirs import AppDirs
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
config = None # Config(Parser) object
mainwindow = None
@ -27,13 +35,13 @@ class QuteBrowser(QApplication):
def __init__(self):
super().__init__(sys.argv)
# Exit on exceptions
sys.excepthook = self.tmp_exception_hook
sys.excepthook = self._tmp_exception_hook
# Handle segfaults
faulthandler.enable()
self.parseopts()
self.initlog()
self._parseopts()
self._initlog()
self.dirs = AppDirs('qutebrowser')
if self.args.confdir is None:
@ -46,7 +54,7 @@ class QuteBrowser(QApplication):
self.commandparser = cmdutils.CommandParser()
self.keyparser = KeyParser(self.mainwindow)
self.init_cmds()
self._init_cmds()
self.mainwindow = MainWindow()
self.aboutToQuit.connect(config.config.save)
@ -62,15 +70,23 @@ class QuteBrowser(QApplication):
self.mainwindow.status.txt.set_keystring)
self.mainwindow.show()
self.python_hacks()
self._python_hacks()
def tmp_exception_hook(self, exctype, value, traceback):
"""Exception hook while initializing, simply exit"""
def _tmp_exception_hook(self, exctype, value, traceback):
"""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)
self.exit(1)
def exception_hook(self, exctype, value, traceback):
"""Try very hard to write open tabs to a file and exit gracefully"""
def _exception_hook(self, exctype, value, traceback):
"""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
sys.__excepthook__(exctype, value, traceback)
try:
@ -84,22 +100,21 @@ class QuteBrowser(QApplication):
pass
self.exit(1)
def python_hacks(self):
"""Gets around some PyQt-oddities by evil hacks"""
## Make python exceptions work
sys.excepthook = self.exception_hook
def _python_hacks(self):
"""Get around some PyQt-oddities by evil hacks.
## 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))
## 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.start(500)
self.timer.timeout.connect(lambda: None)
def parseopts(self):
"""Parse command line options"""
def _parseopts(self):
"""Parse command line options."""
parser = ArgumentParser("usage: %(prog)s [options]")
parser.add_argument('-l', '--log', dest='loglevel',
help='Set loglevel', default='info')
@ -107,8 +122,8 @@ class QuteBrowser(QApplication):
'(empty for no config storage)')
self.args = parser.parse_args()
def initlog(self):
"""Initialisation of the log"""
def _initlog(self):
"""Initialisation of the logging output."""
loglevel = self.args.loglevel
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
@ -119,8 +134,11 @@ class QuteBrowser(QApplication):
'[%(module)s:%(funcName)s:%(lineno)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
def init_cmds(self):
"""Initialisation of the qutebrowser commands"""
def _init_cmds(self):
"""Initialisation of the qutebrowser commands.
Registers all commands, connects its signals, and sets up keyparser.
"""
cmdutils.register_all()
for cmd in cmdutils.cmd_dict.values():
cmd.signal.connect(self.cmd_handler)
@ -130,8 +148,10 @@ class QuteBrowser(QApplication):
pass
def cmd_handler(self, tpl):
"""Handler which gets called from all commands and delegates the
specific actions.
"""Handle commands and delegate the 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, ...]
@ -163,12 +183,17 @@ class QuteBrowser(QApplication):
handler = handlers[cmd]
if self.sender().count:
handler(*args, count=count)
return handler(*args, count=count)
else:
handler(*args)
return handler(*args)
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:
r = eval(s)
out = repr(r)

View File

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

View File

@ -1,3 +1,6 @@
"""Parse keypresses/keychains in the main window."""
import logging
import re
@ -8,7 +11,7 @@ from qutebrowser.commands.utils import (CommandParser, ArgumentCountError,
class KeyParser(QObject):
"""Parser for vim-like key sequences"""
"""Parser for vim-like key sequences."""
keystring = '' # The currently entered key sequence
# Signal emitted when the statusbar should set a partial command
set_cmd_text = pyqtSignal(str)
@ -27,8 +30,9 @@ class KeyParser(QObject):
self.commandparser = CommandParser()
def from_config_sect(self, sect):
"""Loads keybindings from a ConfigParser section, in the config format
key = command, e.g.
"""Load keybindings from a ConfigParser section.
Config format: key = command, e.g.:
gg = scrollstart
"""
for (key, cmd) in sect.items():
@ -36,13 +40,17 @@ class KeyParser(QObject):
self.bindings[key] = cmd
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.keystring_updated.emit(self.keystring)
def _handle(self, e):
"""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
"""
logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text()))
@ -98,7 +106,10 @@ class KeyParser(QObject):
return
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:
cmdstr_hay = self.bindings[cmdstr_needle]
return (self.MATCH_DEFINITIVE, cmdstr_hay)

View File

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

View File

@ -1,4 +1,4 @@
"""Simple browser for testing purposes"""
"""Very simple browser for testing purposes."""
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 PyQt5.QtCore import (QAbstractItemModel, Qt, QModelIndex, QVariant,
@ -5,6 +14,12 @@ from PyQt5.QtCore import (QAbstractItemModel, Qt, QModelIndex, QVariant,
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):
super().__init__(parent)
self._data = OrderedDict()
@ -12,22 +27,40 @@ class CompletionModel(QAbstractItemModel):
self.root = CompletionItem([""] * 2)
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)
node.children.pop(position)
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():
return index.internalPointer()
else:
return self.root
def columnCount(self, parent=QModelIndex()):
"""Return the column count in the model.
Overrides QAbstractItemModel::columnCount.
"""
# pylint: disable=unused-argument
return self.root.column_count()
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():
return QVariant()
item = index.internalPointer()
@ -37,6 +70,11 @@ class CompletionModel(QAbstractItemModel):
return QVariant()
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
# tries to select them
if not index.isValid():
@ -48,11 +86,21 @@ class CompletionModel(QAbstractItemModel):
return flags | Qt.ItemIsSelectable
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:
return QVariant(self.root.data(section))
return None
return QVariant()
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():
return False
item = index.internalPointer()
@ -64,6 +112,11 @@ class CompletionModel(QAbstractItemModel):
return True
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
0 <= column < self.columnCount(parent)):
pass
@ -82,6 +135,11 @@ class CompletionModel(QAbstractItemModel):
return QModelIndex()
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():
return QModelIndex()
item = index.internalPointer().parent
@ -90,6 +148,11 @@ class CompletionModel(QAbstractItemModel):
return self.createIndex(item.row(), 0, item)
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:
return 0
@ -101,9 +164,15 @@ class CompletionModel(QAbstractItemModel):
return len(pitem.children)
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
def init_data(self):
"""Initialize the Qt model based on the data in self._data."""
for (cat, items) in self._data.items():
newcat = CompletionItem([cat], self.root)
self.root.children.append(newcat)
@ -112,6 +181,10 @@ class CompletionModel(QAbstractItemModel):
newcat.children.append(newitem)
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()):
cat = self.index(i, 0)
for k in range(self.rowCount(cat)):
@ -121,6 +194,7 @@ class CompletionModel(QAbstractItemModel):
self.setData(idx, marks, Qt.UserRole)
def _get_marks(self, needle, haystack):
"""Return the marks for needle in haystack."""
pos1 = pos2 = 0
marks = []
if not needle:
@ -135,18 +209,29 @@ class CompletionModel(QAbstractItemModel):
class CompletionItem():
"""An item (row) in a CompletionModel."""
parent = None
_data = None
children = None
_data = None
_marks = 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._data = data
self.children = []
self._marks = []
def data(self, column, role=Qt.DisplayRole):
"""Get the data for role/column.
Raise ValueError if the role is invalid.
"""
if role == Qt.DisplayRole:
return self._data[column]
elif role == Qt.UserRole:
@ -155,6 +240,10 @@ class CompletionItem():
raise ValueError
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:
self._data[column] = value
elif role == Qt.UserRole:
@ -163,33 +252,53 @@ class CompletionItem():
raise ValueError
def column_count(self):
"""Return the column count in the item."""
return len(self._data)
def row(self):
"""Return the row index (int) of the item, or 0 if it's a root item."""
if self.parent:
return self.parent.children.index(self)
return 0
class CompletionFilterModel(QSortFilterProxyModel):
_pattern = None
"""Subclass of QSortFilterProxyModel with custom sorting/filtering."""
pattern_changed = pyqtSignal(str)
@property
def pattern(self):
return self._pattern
@pattern.setter
def pattern(self, val):
self._pattern = val
self.invalidate()
self.pattern_changed.emit(val)
_pattern = None
def __init__(self, parent=None):
super().__init__(parent)
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):
"""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():
return True
idx = self.sourceModel().index(row, 0, parent)
@ -200,6 +309,14 @@ class CompletionFilterModel(QSortFilterProxyModel):
return self.pattern in data
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()
right = self.sourceModel().data(rindex).value()
@ -216,9 +333,11 @@ class CompletionFilterModel(QSortFilterProxyModel):
return left < right
def first_item(self):
"""Returns the first item in the model."""
cat = self.index(0, 0)
return self.index(0, 0, cat)
def last_item(self):
"""Returns the last item in the model."""
cat = self.index(self.rowCount() - 1, 0)
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
import logging
@ -61,6 +69,7 @@ MONOSPACE = ', '.join(_MONOSPACE)
def init(confdir):
"""Initialize the global objects based on the config in configdir."""
global config, colordict
config = Config(confdir)
try:
@ -70,11 +79,26 @@ def init(confdir):
def get_stylesheet(template):
"""Return a formatted stylesheet based on a template."""
return template.strip().format(color=colordict, monospace=MONOSPACE)
class ColorDict(dict):
"""A dict aimed at Qt stylesheet colors."""
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:
val = super().__getitem__(key)
except KeyError:
@ -87,6 +111,10 @@ class ColorDict(dict):
return val
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:
return super().__getitem__(key)
except KeyError:
@ -94,12 +122,16 @@ class ColorDict(dict):
class Config(ConfigParser):
"""Our own ConfigParser"""
"""Our own ConfigParser subclass."""
configdir = None
FNAME = 'config'
def __init__(self, configdir):
"""configdir: directory to store the config in"""
"""Config constructor.
configdir -- directory to store the config in.
"""
super().__init__()
self.optionxform = lambda opt: opt # be case-insensitive
self.configdir = configdir
@ -113,6 +145,7 @@ class Config(ConfigParser):
self.read(self.configfile)
def init_config(self):
"""Initialize Config from default_config and save it."""
logging.info("Initializing default config.")
if self.configdir is None:
self.read_dict(default_config)
@ -126,6 +159,7 @@ class Config(ConfigParser):
cp.write(f)
def save(self):
"""Save the config file."""
if self.configdir is None:
return
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
from PyQt5.QtWidgets import QShortcut
@ -11,7 +18,12 @@ from qutebrowser.widgets.tabbar import 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_load_started = pyqtSignal() # Current tab started loading
@ -33,7 +45,10 @@ class TabbedBrowser(TabWidget):
space.activated.connect(self.space_scroll)
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)
tab = BrowserTab(self)
tab.openurl(url)
@ -60,16 +75,26 @@ class TabbedBrowser(TabWidget):
tab.open_tab.connect(self.tabopen)
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)
def undo_close(self):
"""Undos closing a tab"""
"""Undo closing a tab.
Command handler for :undo.
"""
if self._url_stack:
self.tabopen(self._url_stack.pop())
def cur_close(self):
"""Closes the current tab"""
"""Close the current tab.
Command handler for :close.
"""
if self.count() > 1:
idx = self.currentIndex()
tab = self.currentWidget()
@ -81,32 +106,50 @@ class TabbedBrowser(TabWidget):
pass
def cur_reload(self):
"""Reloads the current tab"""
"""Reload the current tab.
Command handler for :reload.
"""
self.currentWidget().reload()
def cur_stop(self):
"""Stops loading in the current tab"""
"""Stop loading in the current tab.
Command handler for :stop.
"""
self.currentWidget().stop()
def cur_print(self):
"""Prints the current tab"""
"""Print the current tab.
Command handler for :print.
"""
# FIXME that does not what I expect
preview = QPrintPreviewDialog()
preview.paintRequested.connect(self.currentWidget().print)
preview.exec_()
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
self.currentWidget().back()
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
self.currentWidget().forward()
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:
count = 1
dx = int(count) * int(dx)
@ -114,18 +157,23 @@ class TabbedBrowser(TabWidget):
self.currentWidget().page().mainFrame().scroll(dx, dy)
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.
Command handler for :scroll_perc_x.
"""
self._cur_scroll_percent(perc, count, Qt.Horizontal)
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.
Command handler for :scroll_perc_y
"""
self._cur_scroll_percent(perc, count, Qt.Vertical)
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:
perc = 100
elif perc is None:
@ -138,15 +186,11 @@ class TabbedBrowser(TabWidget):
return
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):
"""Switches to the previous tab"""
"""Switch to the previous tab.
Command handler for :tabprev.
"""
idx = self.currentIndex()
if idx > 0:
self.setCurrentIndex(idx - 1)
@ -155,7 +199,10 @@ class TabbedBrowser(TabWidget):
pass
def switch_next(self):
"""Switches to the next tab"""
"""Switch to the next tab.
Command handler for :tabnext.
"""
idx = self.currentIndex()
if idx < self.count() - 1:
self.setCurrentIndex(idx + 1)
@ -163,37 +210,69 @@ class TabbedBrowser(TabWidget):
# FIXME
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):
"""Extend TabWidget (QWidget)'s keyPressEvent to emit a signal."""
self.keypress.emit(e)
super().keyPressEvent(e)
def _titleChanged_handler(self, text):
"""Set the title of a tab.
Slot for the titleChanged signal of any tab.
"""
if text:
self.setTabText(self.indexOf(self.sender()), text)
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()
self.setTabText(self.indexOf(s), s.url().toString())
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.
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(
signal.signal, ','.join([str(e) for e in args]))
if self.currentWidget() == self.sender():
logging.debug('{} - emitting'.format(dbgstr))
signal.emit(*args)
return signal.emit(*args)
else:
logging.debug('{} - ignoring'.format(dbgstr))
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)
self.cur_progress.emit(tab.progress)
def _scroll_pos_changed_handler(self, x, y):
"""Gets the new position from a BrowserTab. If it's the current tab, it
calculates the percentage and emits cur_scroll_perc_changed.
"""Get the new position from a BrowserTab. If it's the current tab,
calculate the percentage and emits cur_scroll_perc_changed.
Slot for the scroll_pos_changed signal of any tab.
"""
sender = self.sender()
if sender != self.currentWidget():
@ -207,12 +286,15 @@ class TabbedBrowser(TabWidget):
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
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')
_scroll_pos = (-1, -1)
_open_new_tab = False # open new tab for the next action
def __init__(self, parent):
super().__init__(parent)
@ -224,16 +306,32 @@ class BrowserTab(QWebView):
self.show()
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))
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)
else:
self.openurl(url)
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
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
be requested when scrolling, and if the scroll position actually
changed, we emit a signal.
watched -- The watched Qt object.
e -- The new event.
"""
if watched == self and e.type() == QEvent.Paint:
frame = self.page().mainFrame()
@ -254,10 +355,16 @@ class BrowserTab(QWebView):
return super().eventFilter(watched, e)
def event(self, e):
"""Another hack to see when a link was pressed with the middle
button
"""Check if a link was clicked with the middle button or Ctrl.
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]:
self.open_new_tab = (e.button() == Qt.MidButton or
self._open_new_tab = (e.button() == Qt.MidButton or
e.modifiers() & Qt.ControlModifier)
return super().event(e)

View File

@ -157,27 +157,41 @@ class CompletionView(QTreeView):
self.show()
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:
# No completion running at the moment, ignore keypress
return
idx = self._next_idx(shift)
self.ignore_next = True
self.selectionModel().setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect)
data = self.model.data(idx)
if data is not None:
self.ignore_next = True
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()
if not idx.isValid():
# No item selected yet
return self.model.first_item()
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
if not idx.isValid() and shift:
if not idx.isValid() and upwards:
return self.model.last_item()
elif not idx.isValid() and not shift:
elif not idx.isValid() and not upwards:
return self.model.first_item()
elif idx.parent().isValid():
# Item is a real item, not a category header -> success
@ -185,11 +199,21 @@ class CompletionView(QTreeView):
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
style = None
painter = None
def paint(self, painter, option, index):
"""Overrides the QStyledItemDelegate paint function."""
painter.save()
self.painter = painter
@ -205,10 +229,12 @@ class CompletionItemDelegate(QStyledItemDelegate):
painter.restore()
def _draw_background(self):
"""Draw the background of an ItemViewItem"""
self.style.drawPrimitive(self.style.PE_PanelItemViewItem, self.opt,
self.painter, self.opt.widget)
def _draw_icon(self):
"""Draw the icon of an ItemViewItem"""
icon_rect = self.style.subElementRect(
self.style.SE_ItemViewItemDecoration, self.opt, self.opt.widget)
@ -222,6 +248,13 @@ class CompletionItemDelegate(QStyledItemDelegate):
self.opt.decorationAlignment, mode, state)
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:
return
@ -258,6 +291,11 @@ class CompletionItemDelegate(QStyledItemDelegate):
self.painter.restore()
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
# qcommonstyle.cpp:viewItemDrawText
clip = QRectF(0, 0, text_rect.width(), text_rect.height())
@ -298,6 +336,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
doc.drawContents(self.painter, clip)
def _draw_focus_rect(self):
"""Draws the focus rectangle of an ItemViewItem"""
state = self.opt.state
if not state & QStyle.State_HasFocus:
return

View File

@ -1,3 +1,5 @@
"""The main window of QuteBrowser."""
from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QWidget
from qutebrowser.widgets.statusbar import StatusBar
@ -6,7 +8,11 @@ from qutebrowser.widgets.completion import CompletionView
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
vbox = None
tabs = None

View File

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

View File

@ -1,3 +1,4 @@
"""The commandline part of the statusbar."""
import logging
from PyQt5.QtWidgets import QLineEdit, QShortcut
@ -6,7 +7,7 @@ from PyQt5.QtGui import QValidator, QKeySequence
class Command(QLineEdit):
"""The commandline part of the statusbar"""
"""The commandline part of the statusbar."""
# Emitted when a command is triggered by the user
got_cmd = pyqtSignal(str)
statusbar = None # The status bar object
@ -30,18 +31,20 @@ class Command(QLineEdit):
self.returnPressed.connect(self.process_cmd)
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_Down, self.key_down_handler),
(Qt.Key_Tab | Qt.SHIFT, self.key_stab_handler),
(Qt.Key_Tab, self.key_tab_handler)]:
(Qt.Key_Tab | Qt.SHIFT, lambda: self.tab_pressed.emit(True)),
(Qt.Key_Tab, lambda: self.tab_pressed.emit(False))
]:
sc = QShortcut(self)
sc.setKey(QKeySequence(key))
sc.setContext(Qt.WidgetWithChildrenShortcut)
sc.activated.connect(handler)
def process_cmd(self):
"""Handle the command in the status bar"""
"""Handle the command in the status bar."""
self._histbrowse_stop()
text = self.text().lstrip(':')
if not self.history or text != self.history[-1]:
@ -50,18 +53,18 @@ class Command(QLineEdit):
self.got_cmd.emit(text)
def set_cmd(self, text):
"""Preset the statusbar to some text"""
"""Preset the statusbar to some text."""
self.setText(':' + text)
self.setFocus()
def append_cmd(self, text):
"""Append text to the commandline"""
"""Append text to the commandline."""
# FIXME do the right thing here
self.setText(':' + text)
self.setFocus()
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,
Qt.BacktabFocusReason, Qt.OtherFocusReason]:
self.setText('')
@ -70,11 +73,18 @@ class Command(QLineEdit):
super().focusOutEvent(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()
super().focusInEvent(e)
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(':')
logging.debug('Preset text: "{}"'.format(pre))
if pre:
@ -84,9 +94,11 @@ class Command(QLineEdit):
self._histpos = len(self._tmphist) - 1
def _histbrowse_stop(self):
"""Stop browsing the history."""
self._histpos = None
def key_up_handler(self):
"""Handle Up presses (go back in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos))
if self._histpos is None:
self._histbrowse_start()
@ -101,6 +113,7 @@ class Command(QLineEdit):
self.set_cmd(self._tmphist[self._histpos])
def key_down_handler(self):
"""Handle Down presses (go forward in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos,
self._tmphist, len(self._tmphist), self._histpos))
if (self._histpos is None or
@ -112,16 +125,20 @@ class Command(QLineEdit):
self._tmphist, len(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):
"""Validator to prevent the : from getting deleted"""
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(':'):
return (QValidator.Acceptable, string, pos)
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
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
@ -5,7 +7,7 @@ from PyQt5.QtCore import QSize
class Progress(QProgressBar):
""" The progress bar part of the status bar"""
"""The progress bar part of the status bar."""
statusbar = None
color = None
_stylesheet = """
@ -30,21 +32,28 @@ class Progress(QProgressBar):
self.hide()
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)
if name == 'color' and value is not None:
config.colordict['statusbar.progress.bg.__cur__'] = value
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
def minimumSizeHint(self):
"""Return the size of the progress widget."""
status_size = self.statusbar.size()
return QSize(100, status_size.height())
def sizeHint(self):
"""Return the size of the progress widget.
Simply copied from minimumSizeHint because the SizePolicy is fixed.
"""
return self.minimumSizeHint()
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?
if prog == 100:
self.setValue(prog)
@ -56,6 +65,12 @@ class Progress(QProgressBar):
self.show()
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:
self.hide()
else:

View File

@ -1,9 +1,16 @@
"""The text part of the statusbar."""
import logging
from PyQt5.QtWidgets import 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 = ''
error = ''
text = ''
@ -18,11 +25,11 @@ class Text(QLabel):
self.update()
def set_keystring(self, s):
"""Setter to be used as a Qt slot"""
"""Setter to be used as a Qt slot."""
self.keystring = s
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
if y == 0:
self.scrollperc = '[top]'
@ -32,10 +39,11 @@ class Text(QLabel):
self.scrollperc = '[{}%]'.format(y)
def set_text(self, text):
"""Setter to be used as a Qt slot."""
logging.debug('Setting text to "{}"'.format(text))
self.text = text
def update(self):
"""Update the text displayed"""
"""Update the text displayed."""
self.setText(' '.join([self.keystring, self.error, self.text,
self.scrollperc]))

View File

@ -1,3 +1,5 @@
"""The tab widget used for TabbedBrowser from browser.py."""
from PyQt5.QtWidgets import QTabWidget
from PyQt5.QtCore import Qt
@ -5,7 +7,7 @@ import qutebrowser.utils.config as config
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
# background-color: grey for QTabBar...