diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 1a0ae0377..9fd897bf0 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -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. +""" diff --git a/qutebrowser/__main__.py b/qutebrowser/__main__.py index 37029ea1b..af0081084 100644 --- a/qutebrowser/__main__.py +++ b/qutebrowser/__main__.py @@ -1,3 +1,5 @@ +"""Entry point for qutebrowser. Simply execute qutebrowser.""" + from qutebrowser.app import QuteBrowser import sys diff --git a/qutebrowser/app.py b/qutebrowser/app.py index e8c90069d..d6a839ea6 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -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) diff --git a/qutebrowser/commands/exceptions.py b/qutebrowser/commands/exceptions.py index 7ce6d8386..b0ae89bcf 100644 --- a/qutebrowser/commands/exceptions.py +++ b/qutebrowser/commands/exceptions.py @@ -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 diff --git a/qutebrowser/commands/keys.py b/qutebrowser/commands/keys.py index 4c6cb505d..83acf3598 100644 --- a/qutebrowser/commands/keys.py +++ b/qutebrowser/commands/keys.py @@ -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) diff --git a/qutebrowser/commands/template.py b/qutebrowser/commands/template.py index c0a48c797..0524e7f61 100644 --- a/qutebrowser/commands/template.py +++ b/qutebrowser/commands/template.py @@ -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: diff --git a/qutebrowser/commands/utils.py b/qutebrowser/commands/utils.py index a3e7b26e9..f87cf7fa1 100644 --- a/qutebrowser/commands/utils.py +++ b/qutebrowser/commands/utils.py @@ -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 diff --git a/qutebrowser/simplebrowser.py b/qutebrowser/simplebrowser.py index fc763a546..a2a071a6b 100644 --- a/qutebrowser/simplebrowser.py +++ b/qutebrowser/simplebrowser.py @@ -1,4 +1,4 @@ -"""Simple browser for testing purposes""" +"""Very simple browser for testing purposes.""" import sys diff --git a/qutebrowser/utils/completion.py b/qutebrowser/utils/completion.py index 79835935d..0a5878524 100644 --- a/qutebrowser/utils/completion.py +++ b/qutebrowser/utils/completion.py @@ -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) diff --git a/qutebrowser/utils/config.py b/qutebrowser/utils/config.py index cfa47691f..e85151f2c 100644 --- a/qutebrowser/utils/config.py +++ b/qutebrowser/utils/config.py @@ -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): diff --git a/qutebrowser/widgets/browser.py b/qutebrowser/widgets/browser.py index 03c8b6cf8..e90186a75 100644 --- a/qutebrowser/widgets/browser.py +++ b/qutebrowser/widgets/browser.py @@ -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 - e.modifiers() & Qt.ControlModifier) + self._open_new_tab = (e.button() == Qt.MidButton or + e.modifiers() & Qt.ControlModifier) return super().event(e) diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py index e4e91fc3a..989338be4 100644 --- a/qutebrowser/widgets/completion.py +++ b/qutebrowser/widgets/completion.py @@ -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 diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 3f495d94c..908c635cd 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -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 diff --git a/qutebrowser/widgets/statusbar/__init__.py b/qutebrowser/widgets/statusbar/__init__.py index a661a7b90..661a84ad3 100644 --- a/qutebrowser/widgets/statusbar/__init__.py +++ b/qutebrowser/widgets/statusbar/__init__.py @@ -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()) diff --git a/qutebrowser/widgets/statusbar/command.py b/qutebrowser/widgets/statusbar/command.py index 2fec81250..a05239753 100644 --- a/qutebrowser/widgets/statusbar/command.py +++ b/qutebrowser/widgets/statusbar/command.py @@ -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), - (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)]: + 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, 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: diff --git a/qutebrowser/widgets/statusbar/progress.py b/qutebrowser/widgets/statusbar/progress.py index 2b9191112..293d4a9d8 100644 --- a/qutebrowser/widgets/statusbar/progress.py +++ b/qutebrowser/widgets/statusbar/progress.py @@ -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: diff --git a/qutebrowser/widgets/statusbar/text.py b/qutebrowser/widgets/statusbar/text.py index a8c3f5bb0..0e5c1e37e 100644 --- a/qutebrowser/widgets/statusbar/text.py +++ b/qutebrowser/widgets/statusbar/text.py @@ -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])) diff --git a/qutebrowser/widgets/tabbar.py b/qutebrowser/widgets/tabbar.py index c05071b59..8c05b1b1d 100644 --- a/qutebrowser/widgets/tabbar.py +++ b/qutebrowser/widgets/tabbar.py @@ -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...