diff --git a/TODO b/TODO
index 5318f5708..21bc33653 100644
--- a/TODO
+++ b/TODO
@@ -38,7 +38,6 @@ Internationalization
Marks
show infos in statusline, temporary/longer
set settings/colors/bindings via commandline
-write default config with comments
more completions (URLs, ...)
SSL handling
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index bd0663eed..150a24a9f 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -22,8 +22,10 @@ import sys
import logging
import functools
import subprocess
+import configparser
from signal import signal, SIGINT
from argparse import ArgumentParser
+from base64 import b64encode
# Print a nice traceback on segfault -- only available on Python 3.3+, but if
# it's unavailable, it doesn't matter much.
@@ -62,39 +64,43 @@ class QuteBrowser(QApplication):
>>> app = QuteBrowser()
>>> sys.exit(app.exec_())
- """
+ Attributes:
+ mainwindow: The MainWindow QWidget.
+ commandparser: The main CommandParser instance.
+ keyparser: The main KeyParser instance.
+ searchparser: The main SearchParser instance.
+ _dirs: AppDirs instance for config/cache directories.
+ _args: ArgumentParser instance.
+ _timers: List of used QTimers so they don't get GCed.
+ _shutting_down: True if we're currently shutting down.
+ _quit_status: The current quitting status.
- dirs = None # AppDirs - config/cache directories
- config = None # Config(Parser) object
- mainwindow = None
- commandparser = None
- keyparser = None
- args = None # ArgumentParser
- timers = None
- shutting_down = False
- _quit_status = None
+ """
def __init__(self):
super().__init__(sys.argv)
self._quit_status = {}
+ self._timers = []
+ self._shutting_down = False
+
sys.excepthook = self._exception_hook
- self._parseopts()
+ self._args = self._parseopts()
self._initlog()
self._initmisc()
- self.dirs = AppDirs('qutebrowser')
- if self.args.confdir is None:
- confdir = self.dirs.user_config_dir
- elif self.args.confdir == '':
+ self._dirs = AppDirs('qutebrowser')
+ if self._args.confdir is None:
+ confdir = self._dirs.user_config_dir
+ elif self._args.confdir == '':
confdir = None
else:
- confdir = self.args.confdir
+ confdir = self._args.confdir
config.init(confdir)
self.commandparser = cmdutils.CommandParser()
self.searchparser = cmdutils.SearchParser()
- self.keyparser = KeyParser(self.mainwindow)
+ self.keyparser = KeyParser(self)
self._init_cmds()
self.mainwindow = MainWindow()
@@ -102,9 +108,9 @@ class QuteBrowser(QApplication):
self.lastWindowClosed.connect(self.shutdown)
self.mainwindow.tabs.keypress.connect(self.keyparser.handle)
self.keyparser.set_cmd_text.connect(
- self.mainwindow.status.cmd.on_set_cmd_text)
+ self.mainwindow.status.cmd.set_cmd_text)
self.mainwindow.tabs.set_cmd_text.connect(
- self.mainwindow.status.cmd.on_set_cmd_text)
+ self.mainwindow.status.cmd.set_cmd_text)
self.mainwindow.tabs.quit.connect(self.shutdown)
self.mainwindow.status.cmd.got_cmd.connect(self.commandparser.run)
self.mainwindow.status.cmd.got_search.connect(self.searchparser.search)
@@ -123,7 +129,55 @@ class QuteBrowser(QApplication):
self.mainwindow.show()
self._python_hacks()
timer = QTimer.singleShot(0, self._process_init_args)
- self.timers.append(timer)
+ self._timers.append(timer)
+
+ 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')
+ parser.add_argument('-c', '--confdir', help='Set config directory '
+ '(empty for no config storage)')
+ parser.add_argument('-d', '--debug', help='Turn on debugging options.',
+ action='store_true')
+ parser.add_argument('command', nargs='*', help='Commands to execute '
+ 'on startup.', metavar=':command')
+ # URLs will actually be in command
+ parser.add_argument('url', nargs='*', help='URLs to open on startup.')
+ return parser.parse_args()
+
+ def _initlog(self):
+ """Initialisation of the logging output."""
+ loglevel = 'debug' if self._args.debug else self._args.loglevel
+ numeric_level = getattr(logging, loglevel.upper(), None)
+ if not isinstance(numeric_level, int):
+ raise ValueError('Invalid log level: {}'.format(loglevel))
+ logging.basicConfig(
+ level=numeric_level,
+ format='%(asctime)s [%(levelname)s] '
+ '[%(module)s:%(funcName)s:%(lineno)s] %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S')
+
+ def _initmisc(self):
+ """Initialize misc things."""
+ if self._args.debug:
+ os.environ['QT_FATAL_WARNINGS'] = '1'
+ self.setApplicationName("qutebrowser")
+ self.setApplicationVersion(qutebrowser.__version__)
+
+ 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)
+ try:
+ self.keyparser.from_config_sect(config.config['keybind'])
+ except KeyError:
+ pass
def _process_init_args(self):
"""Process initial positional args.
@@ -137,7 +191,7 @@ class QuteBrowser(QApplication):
QEventLoop.ExcludeSocketNotifiers)
opened_urls = False
- for e in self.args.command:
+ for e in self._args.command:
if e.startswith(':'):
logging.debug('Startup cmd {}'.format(e))
self.commandparser.run(e.lstrip(':'))
@@ -152,6 +206,20 @@ class QuteBrowser(QApplication):
for url in config.config.get('general', 'startpage').split(','):
self.mainwindow.tabs.tabopen(url)
+ def _python_hacks(self):
+ """Get around some PyQt-oddities by evil hacks.
+
+ 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.
+
+ """
+ signal(SIGINT, lambda *args: self.exit(128 + SIGINT))
+ timer = QTimer()
+ timer.start(500)
+ timer.timeout.connect(lambda: None)
+ self._timers.append(timer)
+
def _recover_pages(self):
"""Try to recover all open pages.
@@ -173,6 +241,15 @@ class QuteBrowser(QApplication):
pass
return pages
+ def _save_geometry(self):
+ """Save the window geometry to the state config."""
+ geom = b64encode(bytes(self.mainwindow.saveGeometry())).decode('ASCII')
+ try:
+ config.state.add_section('geometry')
+ except configparser.DuplicateSectionError:
+ pass
+ config.state['geometry']['mainwindow'] = geom
+
def _exception_hook(self, exctype, excvalue, tb):
"""Handle uncaught python exceptions.
@@ -237,99 +314,9 @@ class QuteBrowser(QApplication):
logging.debug("maybe_quit called from {}, quit status {}".format(
sender, self._quit_status))
if all(self._quit_status.values()):
+ logging.debug("maybe_quit quitting.")
self.quit()
- def _python_hacks(self):
- """Get around some PyQt-oddities by evil hacks.
-
- 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.
-
- """
- signal(SIGINT, lambda *args: self.exit(128 + SIGINT))
- timer = QTimer()
- timer.start(500)
- timer.timeout.connect(lambda: None)
- self.timers.append(timer)
-
- 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')
- parser.add_argument('-c', '--confdir', help='Set config directory '
- '(empty for no config storage)')
- parser.add_argument('-d', '--debug', help='Turn on debugging options.',
- action='store_true')
- parser.add_argument('command', nargs='*', help='Commands to execute '
- 'on startup.', metavar=':command')
- # URLs will actually be in command
- parser.add_argument('url', nargs='*', help='URLs to open on startup.')
- self.args = parser.parse_args()
-
- def _initlog(self):
- """Initialisation of the logging output."""
- loglevel = 'debug' if self.args.debug else self.args.loglevel
- numeric_level = getattr(logging, loglevel.upper(), None)
- if not isinstance(numeric_level, int):
- raise ValueError('Invalid log level: {}'.format(loglevel))
- logging.basicConfig(
- level=numeric_level,
- format='%(asctime)s [%(levelname)s] '
- '[%(module)s:%(funcName)s:%(lineno)s] %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S')
-
- def _initmisc(self):
- """Initialize misc things."""
- if self.args.debug:
- os.environ['QT_FATAL_WARNINGS'] = '1'
- self.setApplicationName("qutebrowser")
- self.setApplicationVersion(qutebrowser.__version__)
- self.timers = []
-
- 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)
- try:
- self.keyparser.from_config_sect(config.config['keybind'])
- except KeyError:
- pass
-
- @pyqtSlot()
- def shutdown(self, do_quit=True):
- """Try to shutdown everything cleanly.
-
- For some reason lastWindowClosing sometimes seem to get emitted twice,
- so we make sure we only run once here.
-
- quit -- Whether to quit after shutting down.
-
- """
- if self.shutting_down:
- return
- self.shutting_down = True
- logging.debug("Shutting down... (do_quit={})".format(do_quit))
- if config.config is not None:
- config.config.save()
- try:
- if do_quit:
- self.mainwindow.tabs.shutdown_complete.connect(self.quit)
- else:
- self.mainwindow.tabs.shutdown_complete.connect(
- functools.partial(self._maybe_quit, 'shutdown'))
- self.mainwindow.tabs.shutdown()
- except AttributeError: # mainwindow or tabs could still be None
- logging.debug("No mainwindow/tabs to shut down.")
- if do_quit:
- self.quit()
-
@pyqtSlot(tuple)
def cmd_handler(self, tpl):
"""Handle commands and delegate the specific actions.
@@ -345,32 +332,33 @@ class QuteBrowser(QApplication):
(count, argv) = tpl
cmd = argv[0]
args = argv[1:]
+ browser = self.mainwindow.tabs
handlers = {
- 'open': self.mainwindow.tabs.openurl,
- 'opencur': self.mainwindow.tabs.opencur,
- 'tabopen': self.mainwindow.tabs.tabopen,
- 'tabopencur': self.mainwindow.tabs.tabopencur,
+ 'open': browser.openurl,
+ 'opencur': browser.opencur,
+ 'tabopen': browser.tabopen,
+ 'tabopencur': browser.tabopencur,
'quit': self.shutdown,
- 'tabclose': self.mainwindow.tabs.cur_close,
- 'tabprev': self.mainwindow.tabs.switch_prev,
- 'tabnext': self.mainwindow.tabs.switch_next,
- 'reload': self.mainwindow.tabs.cur_reload,
- 'stop': self.mainwindow.tabs.cur_stop,
- 'back': self.mainwindow.tabs.cur_back,
- 'forward': self.mainwindow.tabs.cur_forward,
- 'print': self.mainwindow.tabs.cur_print,
- 'scroll': self.mainwindow.tabs.cur_scroll,
- 'scroll_page': self.mainwindow.tabs.cur_scroll_page,
- 'scroll_perc_x': self.mainwindow.tabs.cur_scroll_percent_x,
- 'scroll_perc_y': self.mainwindow.tabs.cur_scroll_percent_y,
- 'undo': self.mainwindow.tabs.undo_close,
+ 'tabclose': browser.cur_close,
+ 'tabprev': browser.switch_prev,
+ 'tabnext': browser.switch_next,
+ 'reload': browser.cur_reload,
+ 'stop': browser.cur_stop,
+ 'back': browser.cur_back,
+ 'forward': browser.cur_forward,
+ 'print': browser.cur_print,
+ 'scroll': browser.cur_scroll,
+ 'scroll_page': browser.cur_scroll_page,
+ 'scroll_perc_x': browser.cur_scroll_percent_x,
+ 'scroll_perc_y': browser.cur_scroll_percent_y,
+ 'undo': browser.undo_close,
'pyeval': self.pyeval,
'nextsearch': self.searchparser.nextsearch,
- 'yank': self.mainwindow.tabs.cur_yank,
- 'yanktitle': self.mainwindow.tabs.cur_yank_title,
- 'paste': self.mainwindow.tabs.paste,
- 'tabpaste': self.mainwindow.tabs.tabpaste,
+ 'yank': browser.cur_yank,
+ 'yanktitle': browser.cur_yank_title,
+ 'paste': browser.paste,
+ 'tabpaste': browser.tabpaste,
'crash': self.crash,
}
@@ -404,3 +392,49 @@ class QuteBrowser(QApplication):
"""
raise Exception
+
+ @pyqtSlot()
+ def shutdown(self, do_quit=True):
+ """Try to shutdown everything cleanly.
+
+ For some reason lastWindowClosing sometimes seem to get emitted twice,
+ so we make sure we only run once here.
+
+ quit -- Whether to quit after shutting down.
+
+ """
+ if self._shutting_down:
+ return
+ self._shutting_down = True
+ logging.debug("Shutting down... (do_quit={})".format(do_quit))
+ try:
+ config.config.save()
+ except AttributeError:
+ logging.exception("Could not save config.")
+ try:
+ self._save_geometry()
+ config.state.save()
+ except AttributeError:
+ logging.exception("Could not save window geometry.")
+ try:
+ if do_quit:
+ self.mainwindow.tabs.shutdown_complete.connect(
+ self.on_tab_shutdown_complete)
+ else:
+ self.mainwindow.tabs.shutdown_complete.connect(
+ functools.partial(self._maybe_quit, 'shutdown'))
+ self.mainwindow.tabs.shutdown()
+ except AttributeError: # mainwindow or tabs could still be None
+ logging.exception("No mainwindow/tabs to shut down.")
+ if do_quit:
+ self.quit()
+
+ @pyqtSlot()
+ def on_tab_shutdown_complete(self):
+ """Quit application after a shutdown.
+
+ Gets called when all tabs finished shutting down after shutdown().
+
+ """
+ logging.debug("Shutdown complete, quitting.")
+ self.quit()
diff --git a/qutebrowser/commands/keys.py b/qutebrowser/commands/keys.py
index 7ba79c3a5..0ed29624e 100644
--- a/qutebrowser/commands/keys.py
+++ b/qutebrowser/commands/keys.py
@@ -32,17 +32,24 @@ startchars = ":/?"
class KeyParser(QObject):
- """Parser for vim-like key sequences."""
+ """Parser for vim-like key sequences.
+
+ Attributes:
+ commandparser: Commandparser instance.
+ _keystring: The currently entered key sequence
+ _bindings: Bound keybindings
+ _modifier_bindings: Bound modifier bindings.
+
+ Signals:
+ set_cmd_text: Emitted when the statusbar should set a partial command.
+ arg: Text to set.
+ keystring_updated: Emitted when the keystring is updated.
+ arg: New keystring.
+
+ """
- keystring = '' # The currently entered key sequence
- # Signal emitted when the statusbar should set a partial command
set_cmd_text = pyqtSignal(str)
- # Signal emitted when the keystring is updated
keystring_updated = pyqtSignal(str)
- # Keybindings
- bindings = {}
- modifier_bindings = {}
- commandparser = None
MATCH_PARTIAL = 0
MATCH_DEFINITIVE = 1
@@ -51,35 +58,9 @@ class KeyParser(QObject):
def __init__(self, mainwindow):
super().__init__(mainwindow)
self.commandparser = CommandParser()
-
- def from_config_sect(self, sect):
- """Load keybindings from a ConfigParser section.
-
- Config format: key = command, e.g.:
- gg = scrollstart
-
- """
- for (key, cmd) in sect.items():
- if key.startswith('@') and key.endswith('@'):
- # normalize keystring
- keystr = self._normalize_keystr(key.strip('@'))
- logging.debug('registered mod key: {} -> {}'.format(keystr,
- cmd))
- self.modifier_bindings[keystr] = cmd
- else:
- logging.debug('registered key: {} -> {}'.format(key, cmd))
- self.bindings[key] = cmd
-
- def handle(self, e):
- """Handle a new keypress and call the respective handlers.
-
- e -- the KeyPressEvent from Qt
-
- """
- handled = self._handle_modifier_key(e)
- if not handled:
- self._handle_single_key(e)
- self.keystring_updated.emit(self.keystring)
+ self._keystring = ''
+ self._bindings = {}
+ self._modifier_bindings = {}
def _handle_modifier_key(self, e):
"""Handle a new keypress with modifiers.
@@ -108,7 +89,7 @@ class KeyParser(QObject):
modstr += s + '+'
keystr = QKeySequence(e.key()).toString()
try:
- cmdstr = self.modifier_bindings[modstr + keystr]
+ cmdstr = self._modifier_bindings[modstr + keystr]
except KeyError:
logging.debug('No binding found for {}.'.format(modstr + keystr))
return True
@@ -133,15 +114,15 @@ class KeyParser(QObject):
logging.debug('Ignoring, no text')
return
- self.keystring += txt
+ self._keystring += txt
- if any(self.keystring == c for c in startchars):
- self.set_cmd_text.emit(self.keystring)
- self.keystring = ''
+ if any(self._keystring == c for c in startchars):
+ self.set_cmd_text.emit(self._keystring)
+ self._keystring = ''
return
(countstr, cmdstr_needle) = re.match(r'^(\d*)(.*)',
- self.keystring).groups()
+ self._keystring).groups()
if not cmdstr_needle:
return
@@ -157,16 +138,16 @@ class KeyParser(QObject):
if match == self.MATCH_DEFINITIVE:
pass
elif match == self.MATCH_PARTIAL:
- logging.debug('No match for "{}" (added {})'.format(self.keystring,
- txt))
+ logging.debug('No match for "{}" (added {})'.format(
+ self._keystring, txt))
return
elif match == self.MATCH_NONE:
logging.debug('Giving up with "{}", no matches'.format(
- self.keystring))
- self.keystring = ''
+ self._keystring))
+ self._keystring = ''
return
- self.keystring = ''
+ self._keystring = ''
count = int(countstr) if countstr else None
self._run_or_fill(cmdstr_hay, count=count, ignore_exc=False)
return
@@ -178,11 +159,11 @@ class KeyParser(QObject):
"""
try:
- cmdstr_hay = self.bindings[cmdstr_needle]
+ cmdstr_hay = self._bindings[cmdstr_needle]
return (self.MATCH_DEFINITIVE, cmdstr_hay)
except KeyError:
# No definitive match, check if there's a chance of a partial match
- for hay in self.bindings:
+ for hay in self._bindings:
try:
if cmdstr_needle[-1] == hay[len(cmdstr_needle) - 1]:
return (self.MATCH_PARTIAL, None)
@@ -229,3 +210,32 @@ class KeyParser(QObject):
cmdstr))
self.set_cmd_text.emit(':{} '.format(cmdstr))
return
+
+ def from_config_sect(self, sect):
+ """Load keybindings from a ConfigParser section.
+
+ Config format: key = command, e.g.:
+ gg = scrollstart
+
+ """
+ for (key, cmd) in sect.items():
+ if key.startswith('@') and key.endswith('@'):
+ # normalize keystring
+ keystr = self._normalize_keystr(key.strip('@'))
+ logging.debug('registered mod key: {} -> {}'.format(keystr,
+ cmd))
+ self._modifier_bindings[keystr] = cmd
+ else:
+ logging.debug('registered key: {} -> {}'.format(key, cmd))
+ self._bindings[key] = cmd
+
+ def handle(self, e):
+ """Handle a new keypress and call the respective handlers.
+
+ e -- the KeyPressEvent from Qt
+
+ """
+ handled = self._handle_modifier_key(e)
+ if not handled:
+ self._handle_single_key(e)
+ self.keystring_updated.emit(self._keystring)
diff --git a/qutebrowser/commands/template.py b/qutebrowser/commands/template.py
index b99c79a9c..7126fe017 100644
--- a/qutebrowser/commands/template.py
+++ b/qutebrowser/commands/template.py
@@ -30,6 +30,10 @@ class Command(QObject):
See the module documentation for qutebrowser.commands.commands for details.
+ Signals:
+ signal: Emitted when the command was executed.
+ arg: A tuple (command, [args])
+
"""
# FIXME:
diff --git a/qutebrowser/commands/utils.py b/qutebrowser/commands/utils.py
index 0aeb8a000..f63c3f11d 100644
--- a/qutebrowser/commands/utils.py
+++ b/qutebrowser/commands/utils.py
@@ -49,12 +49,45 @@ def register_all():
class SearchParser(QObject):
- """Parse qutebrowser searches."""
+ """Parse qutebrowser searches.
+
+ Attributes:
+ _text: The text from the last search.
+ _flags: The flags from the last search.
+
+ Signals:
+ do_search: Emitted when a search should be started.
+ arg 1: Search string.
+ arg 2: Flags to use.
+
+ """
- text = None
- flags = 0
do_search = pyqtSignal(str, 'QWebPage::FindFlags')
+ def __init__(self, parent=None):
+ self._text = None
+ self._flags = 0
+ super().__init__(parent)
+
+ def _search(self, text, rev=False):
+ """Search for a text on the current page.
+
+ text -- The text to search for.
+ rev -- Search direction.
+
+ """
+ if self._text is not None and self._text != text:
+ self.do_search.emit('', 0)
+ self._text = text
+ self._flags = 0
+ if config.config.getboolean('general', 'ignorecase', fallback=True):
+ self._flags |= QWebPage.FindCaseSensitively
+ if config.config.getboolean('general', 'wrapsearch', fallback=True):
+ self._flags |= QWebPage.FindWrapsAroundDocument
+ if rev:
+ self._flags |= QWebPage.FindBackward
+ self.do_search.emit(self._text, self._flags)
+
@pyqtSlot(str)
def search(self, text):
"""Search for a text on a website.
@@ -73,40 +106,33 @@ class SearchParser(QObject):
"""
self._search(text, rev=True)
- def _search(self, text, rev=False):
- """Search for a text on the current page.
-
- text -- The text to search for.
- rev -- Search direction.
-
- """
- if self.text != text:
- self.do_search.emit('', 0)
- self.text = text
- self.flags = 0
- if config.config.getboolean('general', 'ignorecase', fallback=True):
- self.flags |= QWebPage.FindCaseSensitively
- if config.config.getboolean('general', 'wrapsearch', fallback=True):
- self.flags |= QWebPage.FindWrapsAroundDocument
- if rev:
- self.flags |= QWebPage.FindBackward
- self.do_search.emit(self.text, self.flags)
-
def nextsearch(self, count=1):
"""Continue the search to the ([count]th) next term."""
- if self.text is not None:
+ if self._text is not None:
for i in range(count): # pylint: disable=unused-variable
- self.do_search.emit(self.text, self.flags)
+ self.do_search.emit(self._text, self._flags)
class CommandParser(QObject):
- """Parse qutebrowser commandline commands."""
+ """Parse qutebrowser commandline commands.
- text = ''
- cmd = ''
- args = []
- error = pyqtSignal(str) # Emitted if there's an error
+ Attributes:
+ _cmd: The command which was parsed.
+ _args: The arguments which were parsed.
+
+ Signals:
+ error: Emitted if there was an error.
+ arg: The error message.
+
+ """
+
+ error = pyqtSignal(str)
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._cmd = None
+ self._args = []
def _parse(self, text):
"""Split the commandline text into command and arguments.
@@ -114,8 +140,7 @@ class CommandParser(QObject):
Raise NoSuchCommandError if a command wasn't found.
"""
- self.text = text
- parts = self.text.strip().split(maxsplit=1)
+ parts = text.strip().split(maxsplit=1)
if not parts:
raise NoSuchCommandError
cmdstr = parts[0]
@@ -130,19 +155,19 @@ class CommandParser(QObject):
args = shlex.split(parts[1])
else:
args = [parts[1]]
- self.cmd = cmd
- self.args = args
+ self._cmd = cmd
+ self._args = args
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):
"""Run a command with an optional count."""
if count is not None:
- self.cmd.run(self.args, count=count)
+ self._cmd.run(self._args, count=count)
else:
- self.cmd.run(self.args)
+ self._cmd.run(self._args)
@pyqtSlot(str, int, bool)
def run(self, text, count=None, ignore_exc=True):
@@ -155,13 +180,17 @@ class CommandParser(QObject):
arguments.
"""
+ if ';;' in text:
+ for sub in text.split(';;'):
+ self.run(sub, count, ignore_exc)
+ return
try:
self._parse(text)
self._check()
except ArgumentCountError:
if ignore_exc:
self.error.emit("{}: invalid argument count".format(
- self.cmd.mainname))
+ self._cmd.mainname))
return False
else:
raise
diff --git a/qutebrowser/models/commandcompletion.py b/qutebrowser/models/commandcompletion.py
index 25bb6e819..d8e4f7d72 100644
--- a/qutebrowser/models/commandcompletion.py
+++ b/qutebrowser/models/commandcompletion.py
@@ -17,6 +17,8 @@
"""A CompletionModel filled with all commands and descriptions."""
+from collections import OrderedDict
+
from qutebrowser.commands.utils import cmd_dict
from qutebrowser.models.completion import CompletionModel
@@ -35,5 +37,6 @@ class CommandCompletionModel(CompletionModel):
if not obj.hide:
doc = obj.__doc__.splitlines()[0].strip().rstrip('.')
cmdlist.append([obj.mainname, doc])
- self._data['Commands'] = sorted(cmdlist)
- self.init_data()
+ data = OrderedDict()
+ data['Commands'] = sorted(cmdlist)
+ self.init_data(data)
diff --git a/qutebrowser/models/completion.py b/qutebrowser/models/completion.py
index 1e4ed488b..695e980a7 100644
--- a/qutebrowser/models/completion.py
+++ b/qutebrowser/models/completion.py
@@ -15,15 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see .
-"""The base completion model for completion in the command line.
-
-Contains:
- CompletionModel -- A simple tree model based on Python data.
- CompletionItem -- One item in the CompletionModel.
-
-"""
-
-from collections import OrderedDict
+"""The base completion model for completion in the command line."""
from PyQt5.QtCore import Qt, QVariant, QAbstractItemModel, QModelIndex
@@ -34,26 +26,18 @@ class CompletionModel(QAbstractItemModel):
Used for showing completions later in the CompletionView.
+ Attributes:
+ _id_map: A mapping from Python object IDs (from id()) to objects, to be
+ used as internalIndex for the model.
+ _root: The root item.
+
"""
def __init__(self, parent=None):
super().__init__(parent)
- self._data = OrderedDict()
- self.parents = []
- self.id_map = {}
- self.root = CompletionItem([""] * 2)
- self.id_map[id(self.root)] = self.root
-
- def removeRows(self, position=0, count=1, parent=QModelIndex()):
- """Remove rows from the model.
-
- Override QAbstractItemModel::removeRows.
-
- """
- node = self._node(parent)
- self.beginRemoveRows(parent, position, position + count - 1)
- node.children.pop(position)
- self.endRemoveRows()
+ self._id_map = {}
+ self._root = CompletionItem([""] * 2)
+ self._id_map[id(self._root)] = self._root
def _node(self, index):
"""Return the interal data representation for index.
@@ -63,172 +47,9 @@ class CompletionModel(QAbstractItemModel):
"""
if index.isValid():
- return self.id_map[index.internalId()]
+ return self._id_map[index.internalId()]
else:
- return self.root
-
- def columnCount(self, parent=QModelIndex()):
- """Return the column count in the model.
-
- Override 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.
-
- Return an invalid QVariant on error.
- Override QAbstractItemModel::data.
-
- """
- if not index.isValid():
- return QVariant()
- try:
- item = self.id_map[index.internalId()]
- except KeyError:
- return QVariant()
- try:
- return QVariant(item.data(index.column(), role))
- except (IndexError, ValueError):
- return QVariant()
-
- def flags(self, index):
- """Return the item flags for index.
-
- Return Qt.NoItemFlags on error.
- Override QAbstractItemModel::flags.
-
- """
- # FIXME categories are not selectable, but moving via arrow keys still
- # tries to select them
- if not index.isValid():
- return Qt.NoItemFlags
- flags = Qt.ItemIsEnabled
- if len(self.id_map[index.internalId()].children) > 0:
- return flags
- else:
- return flags | Qt.ItemIsSelectable
-
- def headerData(self, section, orientation, role=Qt.DisplayRole):
- """Return the header data for role/index as QVariant.
-
- Return an invalid QVariant on error.
- Override QAbstractItemModel::headerData.
-
- """
- if orientation == Qt.Horizontal and role == Qt.DisplayRole:
- return QVariant(self.root.data(section))
- return QVariant()
-
- def setData(self, index, value, role=Qt.EditRole):
- """Set the data for role/index to value.
-
- Return True on success, False on failure.
- Override QAbstractItemModel::setData.
-
- """
- if not index.isValid():
- return False
- item = self.id_map[index.internalId()]
- try:
- item.setdata(index.column(), value, role)
- except (IndexError, ValueError):
- return False
- self.dataChanged.emit(index, index)
- return True
-
- def index(self, row, column, parent=QModelIndex()):
- """Return the QModelIndex for row/column/parent.
-
- Return an invalid QModelIndex on failure.
- Override QAbstractItemModel::index.
-
- """
- if (0 <= row < self.rowCount(parent) and
- 0 <= column < self.columnCount(parent)):
- pass
- else:
- return QModelIndex()
-
- if not parent.isValid():
- parent_item = self.root
- else:
- parent_item = self.id_map[parent.internalId()]
-
- child_item = parent_item.children[row]
- if child_item:
- index = self.createIndex(row, column, id(child_item))
- self.id_map.setdefault(index.internalId(), child_item)
- return index
- else:
- return QModelIndex()
-
- def parent(self, index):
- """Return the QModelIndex of the parent of the object behind index.
-
- Return an invalid QModelIndex on failure.
- Override QAbstractItemModel::parent.
-
- """
- if not index.isValid():
- return QModelIndex()
- item = self.id_map[index.internalId()].parent
- if item == self.root or item is None:
- return QModelIndex()
- return self.createIndex(item.row(), 0, id(item))
-
- def rowCount(self, parent=QModelIndex()):
- """Return the children count of an item.
-
- Use the root frame if parent is invalid.
- Override QAbstractItemModel::rowCount.
-
- """
- if parent.column() > 0:
- return 0
-
- if not parent.isValid():
- pitem = self.root
- else:
- pitem = self.id_map[parent.internalId()]
-
- return len(pitem.children)
-
- def sort(self, column, order=Qt.AscendingOrder):
- """Sort the data in column according to order.
-
- Raise NotImplementedError, should be overwritten in a superclass.
- Override 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.id_map[id(newcat)] = newcat
- self.root.children.append(newcat)
- for item in items:
- newitem = CompletionItem(item, newcat)
- self.id_map[id(newitem)] = newitem
- 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)):
- idx = self.index(k, 0, cat)
- old = self.data(idx).value()
- marks = self._get_marks(needle, old)
- self.setData(idx, marks, Qt.UserRole)
+ return self._root
def _get_marks(self, needle, haystack):
"""Return the marks for needle in haystack."""
@@ -244,15 +65,196 @@ class CompletionModel(QAbstractItemModel):
marks.append((pos1, pos2))
return marks
+ 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)):
+ idx = self.index(k, 0, cat)
+ old = self.data(idx).value()
+ marks = self._get_marks(needle, old)
+ self.setData(idx, marks, Qt.UserRole)
+
+ def init_data(self, data):
+ """Initialize the Qt model based on the data given.
+
+ data -- dict of data to process.
+
+ """
+ for (cat, items) in data.items():
+ newcat = CompletionItem([cat], self._root)
+ self._id_map[id(newcat)] = newcat
+ self._root.children.append(newcat)
+ for item in items:
+ newitem = CompletionItem(item, newcat)
+ self._id_map[id(newitem)] = newitem
+ newcat.children.append(newitem)
+
+ def removeRows(self, position=0, count=1, parent=QModelIndex()):
+ """Remove rows from the model.
+
+ Override QAbstractItemModel::removeRows.
+
+ """
+ node = self._node(parent)
+ self.beginRemoveRows(parent, position, position + count - 1)
+ node.children.pop(position)
+ self.endRemoveRows()
+
+ def columnCount(self, parent=QModelIndex()):
+ """Return the column count in the model.
+
+ Override QAbstractItemModel::columnCount.
+
+ """
+ # pylint: disable=unused-argument
+ return self._root.column_count()
+
+ def rowCount(self, parent=QModelIndex()):
+ """Return the children count of an item.
+
+ Use the root frame if parent is invalid.
+ Override QAbstractItemModel::rowCount.
+
+ """
+ if parent.column() > 0:
+ return 0
+
+ if not parent.isValid():
+ pitem = self._root
+ else:
+ pitem = self._id_map[parent.internalId()]
+
+ return len(pitem.children)
+
+ def data(self, index, role=Qt.DisplayRole):
+ """Return the data for role/index as QVariant.
+
+ Return an invalid QVariant on error.
+ Override QAbstractItemModel::data.
+
+ """
+ if not index.isValid():
+ return QVariant()
+ try:
+ item = self._id_map[index.internalId()]
+ except KeyError:
+ return QVariant()
+ try:
+ return QVariant(item.data(index.column(), role))
+ except (IndexError, ValueError):
+ return QVariant()
+
+ def headerData(self, section, orientation, role=Qt.DisplayRole):
+ """Return the header data for role/index as QVariant.
+
+ Return an invalid QVariant on error.
+ Override QAbstractItemModel::headerData.
+
+ """
+ if orientation == Qt.Horizontal and role == Qt.DisplayRole:
+ return QVariant(self._root.data(section))
+ return QVariant()
+
+ def setData(self, index, value, role=Qt.EditRole):
+ """Set the data for role/index to value.
+
+ Return True on success, False on failure.
+ Override QAbstractItemModel::setData.
+
+ """
+ if not index.isValid():
+ return False
+ item = self._id_map[index.internalId()]
+ try:
+ item.setdata(index.column(), value, role)
+ except (IndexError, ValueError):
+ return False
+ self.dataChanged.emit(index, index)
+ return True
+
+ def flags(self, index):
+ """Return the item flags for index.
+
+ Return Qt.NoItemFlags on error.
+ Override QAbstractItemModel::flags.
+
+ """
+ # FIXME categories are not selectable, but moving via arrow keys still
+ # tries to select them
+ if not index.isValid():
+ return Qt.NoItemFlags
+ flags = Qt.ItemIsEnabled
+ if len(self._id_map[index.internalId()].children) > 0:
+ return flags
+ else:
+ return flags | Qt.ItemIsSelectable
+
+ def index(self, row, column, parent=QModelIndex()):
+ """Return the QModelIndex for row/column/parent.
+
+ Return an invalid QModelIndex on failure.
+ Override QAbstractItemModel::index.
+
+ """
+ if (0 <= row < self.rowCount(parent) and
+ 0 <= column < self.columnCount(parent)):
+ pass
+ else:
+ return QModelIndex()
+
+ if not parent.isValid():
+ parent_item = self._root
+ else:
+ parent_item = self._id_map[parent.internalId()]
+
+ child_item = parent_item.children[row]
+ if child_item:
+ index = self.createIndex(row, column, id(child_item))
+ self._id_map.setdefault(index.internalId(), child_item)
+ return index
+ else:
+ return QModelIndex()
+
+ def parent(self, index):
+ """Return the QModelIndex of the parent of the object behind index.
+
+ Return an invalid QModelIndex on failure.
+ Override QAbstractItemModel::parent.
+
+ """
+ if not index.isValid():
+ return QModelIndex()
+ item = self._id_map[index.internalId()].parent
+ if item == self._root or item is None:
+ return QModelIndex()
+ return self.createIndex(item.row(), 0, id(item))
+
+ def sort(self, column, order=Qt.AscendingOrder):
+ """Sort the data in column according to order.
+
+ Raise NotImplementedError, should be overwritten in a superclass.
+ Override QAbstractItemModel::sort.
+
+ """
+ raise NotImplementedError
+
class CompletionItem():
- """An item (row) in a CompletionModel."""
+ """An item (row) in a CompletionModel.
- parent = None
- children = None
- _data = None
- _marks = None
+ Attributes:
+ parent: The parent of this item.
+ children: The children of this item.
+ _data: The data of this item.
+ _marks: The marks of this item.
+
+ """
def __init__(self, data, parent=None):
"""Constructor for CompletionItem.
@@ -262,8 +264,8 @@ class CompletionItem():
"""
self.parent = parent
- self._data = data
self.children = []
+ self._data = data
self._marks = []
def data(self, column, role=Qt.DisplayRole):
diff --git a/qutebrowser/models/completionfilter.py b/qutebrowser/models/completionfilter.py
index 8d2c50bb8..6052bb045 100644
--- a/qutebrowser/models/completionfilter.py
+++ b/qutebrowser/models/completionfilter.py
@@ -27,13 +27,17 @@ from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
class CompletionFilterModel(QSortFilterProxyModel):
- """Subclass of QSortFilterProxyModel with custom sorting/filtering."""
+ """Subclass of QSortFilterProxyModel with custom sorting/filtering.
- _pattern = None
- srcmodel = None
+ Attributes:
+ _pattern: The pattern to filter with, used in pattern property.
+ _srcmodel: The source model, accessed via the srcmodel property.
+
+ """
def __init__(self, parent=None):
super().__init__(parent)
+ self._srcmodel = None
self._pattern = ''
@property
@@ -41,16 +45,6 @@ class CompletionFilterModel(QSortFilterProxyModel):
"""Getter for pattern."""
return self._pattern
- def setsrc(self, model):
- """Set a new source model and clear the pattern.
-
- model -- The new source model.
-
- """
- self.setSourceModel(model)
- self.srcmodel = model
- self.pattern = ''
-
@pattern.setter
def pattern(self, val):
"""Setter for pattern.
@@ -71,6 +65,33 @@ class CompletionFilterModel(QSortFilterProxyModel):
self.sort(sortcol)
self.invalidate()
+ @property
+ def srcmodel(self):
+ """Getter for srcmodel."""
+ return self._srcmodel
+
+ @srcmodel.setter
+ def srcmodel(self, model):
+ """Set a new source model and clear the pattern.
+
+ model -- The new source model.
+
+ """
+ # FIXME change this to a property
+ self.setSourceModel(model)
+ self._srcmodel = model
+ self.pattern = ''
+
+ def first_item(self):
+ """Return the first item in the model."""
+ cat = self.index(0, 0)
+ return self.index(0, 0, cat)
+
+ def last_item(self):
+ """Return the last item in the model."""
+ cat = self.index(self.rowCount() - 1, 0)
+ return self.index(self.rowCount(cat) - 1, 0, cat)
+
def filterAcceptsRow(self, row, parent):
"""Custom filter implementation.
@@ -116,13 +137,3 @@ class CompletionFilterModel(QSortFilterProxyModel):
return False
else:
return left < right
-
- def first_item(self):
- """Return the first item in the model."""
- cat = self.index(0, 0)
- return self.index(0, 0, cat)
-
- def last_item(self):
- """Return 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/qutebrowser.conf b/qutebrowser/qutebrowser.conf
new file mode 100644
index 000000000..43e30ba5c
--- /dev/null
+++ b/qutebrowser/qutebrowser.conf
@@ -0,0 +1,188 @@
+# vim: ft=dosini
+
+# Configfile for qutebrowser.
+#
+# This configfile is parsed by python's configparser in extended interpolation
+# mode. The format is very INI-like, so there are categories like [general]
+# with "key = value"-pairs.
+#
+# Comments start with ; or # and may only start at the beginning of a line.
+#
+# Interpolation looks like ${value} or ${section:value} and will be replaced
+# by the respective value.
+#
+# This is the default config, so if you want to remove anything from here (as
+# opposed to change/add), for example a keybinding, set it to an empty value.
+
+[general]
+# show_completion: bool, whether to show the autocompletion window or not.
+# ignorecase: bool, whether to do case-insensitive searching.
+# wrapsearch: bool, whether to wrap search to the top when arriving at the end.
+# startpage: The default pages to open at the start, multiple pages can be
+# separated with commas.
+# auto_search: Whether to start a search automatically when something
+# which is not an url is entered.
+# true/naive: Use simple/naive check
+# dns: Use DNS matching (might be slow)
+# false: Never search automatically
+show_completion = true
+ignorecase = true
+wrapsearch = true
+startpage = http://www.duckduckgo.com/
+auto_search = naive
+
+[tabbar]
+# movable: bool, whether tabs should be movable
+# closebuttons: bool, whether tabs should have a close button
+# scrollbuttons: bool, whether there should be scroll buttons if there are too
+# many tabs open.
+# position: Position of the tab bar, either north, south, east or west.
+# select_on_remove: Which tab to select when the focused tab is removed. Either
+# 'previous', 'left' or 'right'.
+# last_close; Behavour when the last tab is closed - 'ignore' (don't do
+# anything), 'blank' (load about:blank) or 'quit' (quit
+# qutebrowser).
+movable = true
+closebuttons = false
+scrollbuttons = false
+position = north
+select_on_remove = previous
+last_close = quit
+
+[searchengines]
+# Definitions of search engines which can be used via the address bar.
+# The searchengine named DEFAULT is used when general.auto_search is true and
+# something else than an URL was entered to be opened.
+# Other search engines can be used via the bang-syntax, e.g.
+# "qutebrowser !google". The string "{}" will be replaced by the search term,
+# use "{{" and "}}" for literal {/} signs.
+DEFAULT = ${duckduckgo}
+duckduckgo = https://duckduckgo.com/?q={}
+ddg = ${duckduckgo}
+google = https://encrypted.google.com/search?q={}
+g = ${google}
+wikipedia = http://en.wikipedia.org/w/index.php?title=Special:Search&search={}
+wiki = ${wikipedia}
+
+[keybind]
+# Bindings from a key(chain) to a command. For special keys (can't be part of a
+# keychain), enclose them in @-signs. For modifiers, you can use either - or +
+# as delimiters, and these names:
+# Control: Control, Ctrl
+# Meta: Meta, Windows, Mod4
+# Alt: Alt, Mod1
+# Shift: Shift
+# For simple keys (no @ signs), a capital letter means the key is pressed with
+# Shift. For modifier keys (with @ signs), you need to explicitely add "Shift-"
+# to match a key pressed with shift.
+# You can bind multiple commands by separating them with ";;".
+o = open
+go = opencur
+O = tabopen
+gO = tabopencur
+ga = tabopen about:blank
+d = tabclose
+J = tabnext
+K = tabprev
+r = reload
+H = back
+L = forward
+h = scroll -50 0
+j = scroll 0 50
+k = scroll 0 -50
+l = scroll 50 0
+u = undo
+gg = scroll_perc_y 0
+G = scroll_perc_y
+n = nextsearch
+yy = yank
+yY = yank sel
+yt = yanktitle
+yT = yanktitle sel
+pp = paste
+pP = paste sel
+Pp = tabpaste
+PP = tabpaste sel
+@Ctrl-Q@ = quit
+@Ctrl-Shift-T@ = undo
+@Ctrl-W@ = tabclose
+@Ctrl-T@ = tabopen about:blank
+@Ctrl-F@ = scroll_page 0 1
+@Ctrl-B@ = scroll_page 0 -1
+@Ctrl-D@ = scroll_page 0 0.5
+@Ctrl-U@ = scroll_page 0 -0.5
+
+[colors]
+# Colors used in the UI. A value can be in one of the following format:
+# - #RGB/#RRGGBB/#RRRGGGBBB/#RRRRGGGGBBBB
+# - A SVG color name as specified in [1].
+# - transparent (no color)
+# - rgb(r, g, b) / rgba(r, g, b, a) (values 0-255 or percentages)
+# - hsv(h, s, v) / hsva(h, s, v, a) (values 0-255, hue 0-359)
+# - A gradient as explained at [2] under "Gradient"
+# [1] http://www.w3.org/TR/SVG/types.html#ColorKeywords
+# [2] http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types
+#
+## Completion widget
+# Text color
+completion.fg = #333333
+# Background of a normal item
+completion.item.bg = white
+# Background of a category header
+completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1,
+ stop:0 #e4e4e4, stop:1 #dbdbdb)
+# Borders of a category header
+completion.category.border.top = #808080
+completion.category.border.bottom = #bbbbbb
+# Text/background color for the currently selected item
+completion.item.selected.fg = #333333
+completion.item.selected.bg = #ffec8b
+# Borders for the selected item
+completion.item.selected.border.top = #f2f2c0
+completion.item.selected.border.bottom = #e6e680
+# Matched text in a completion
+completion.match.fg = red
+## Statusbar widget
+# Normal colors for the statusbar.
+statusbar.bg = black
+statusbar.fg = white
+# Statusbar colors when there is an error
+statusbar.bg.error = red
+# Colors of the progress bar
+statusbar.progress.bg = white
+# Colors of the URL shown in the statusbar
+# Unknown/default status
+statusbar.url.fg = ${statusbar.fg}
+# Page loaded successfully
+statusbar.url.fg.success = lime
+# Error while loading page
+statusbar.url.fg.error = orange
+# Warning while loading page
+statusbar.url.fg.warn = yellow
+# Link under the mouse cursor
+statusbar.url.fg.hover = aqua
+## Tabbar
+# Tabbar colors
+tab.fg = white
+tab.bg = grey
+# Color of the selected tab
+tab.bg.selected = black
+# Seperator between tabs
+tab.seperator = white
+
+[fonts]
+# Fonts used for the UI, with optional style/weight/size.
+# Style: normal/italic/oblique
+# Weight: normal, bold, 100..900
+# Size: Number + px/pt
+#
+# Default monospace fonts
+_monospace = Monospace, "DejaVu Sans Mono", Consolas, Monaco,
+ "Bitstream Vera Sans Mono", "Andale Mono", "Liberation Mono",
+ "Courier New", Courier, monospace, Fixed, Terminal
+# Font used in the completion widget.
+completion = 8pt ${_monospace}
+# Font used in the tabbar
+tabbar = 8pt ${_monospace}
+# Font used in the statusbar.
+statusbar = 8pt ${_monospace}
diff --git a/qutebrowser/utils/config.py b/qutebrowser/utils/config.py
index 2c2e1346d..c286e56b4 100644
--- a/qutebrowser/utils/config.py
+++ b/qutebrowser/utils/config.py
@@ -15,131 +15,32 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see .
-"""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.
-
-"""
+"""Configuration storage and config-related utilities."""
import os
import io
import os.path
import logging
-from configparser import ConfigParser, ExtendedInterpolation
+from configparser import (ConfigParser, ExtendedInterpolation, NoSectionError,
+ NoOptionError)
+
+from qutebrowser.utils.misc import read_file
config = None
+state = None
colordict = {}
fontdict = {}
-default_config = """
-[general]
-show_completion = true
-ignorecase = true
-wrapsearch = true
-startpage = http://www.duckduckgo.com/
-addressbar_dns_lookup = false
-auto_search = true
-
-[tabbar]
-movable = true
-closebuttons = false
-scrollbuttons = false
-# north, south, east, west
-position = north
-# previous, left, right
-select_on_remove = previous
-# ignore, blank, quit
-last_close = quit
-
-[searchengines]
-DEFAULT = ${duckduckgo}
-duckduckgo = https://duckduckgo.com/?q={}
-ddg = ${duckduckgo}
-google = https://encrypted.google.com/search?q={}
-g = ${google}
-wikipedia = http://en.wikipedia.org/w/index.php?title=Special:Search&search={}
-wiki = ${wikipedia}
-
-[keybind]
-o = open
-go = opencur
-O = tabopen
-gO = tabopencur
-ga = tabopen about:blank
-d = tabclose
-J = tabnext
-K = tabprev
-r = reload
-H = back
-L = forward
-h = scroll -50 0
-j = scroll 0 50
-k = scroll 0 -50
-l = scroll 50 0
-u = undo
-gg = scroll_perc_y 0
-G = scroll_perc_y
-n = nextsearch
-yy = yank
-yY = yank sel
-yt = yanktitle
-yT = yanktitle sel
-pp = paste
-pP = paste sel
-Pp = tabpaste
-PP = tabpaste sel
-@Ctrl-Q@ = quit
-@Ctrl-Shift-T@ = undo
-@Ctrl-W@ = tabclose
-@Ctrl-T@ = tabopen about:blank
-@Ctrl-F@ = scroll_page 0 1
-@Ctrl-B@ = scroll_page 0 -1
-@Ctrl-D@ = scroll_page 0 0.5
-@Ctrl-U@ = scroll_page 0 -0.5
-
-[colors]
-completion.fg = #333333
-completion.item.bg = white
-completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1,
- stop:0 #e4e4e4, stop:1 #dbdbdb)
-completion.category.border.top = #808080
-completion.category.border.bottom = #bbbbbb
-completion.item.selected.fg = #333333
-completion.item.selected.bg = #ffec8b
-completion.item.selected.border.top = #f2f2c0
-completion.item.selected.border.bottom = #e6e680
-completion.match.fg = red
-statusbar.progress.bg = white
-statusbar.bg = black
-statusbar.fg = white
-statusbar.bg.error = red
-statusbar.url.fg = ${statusbar.fg}
-statusbar.url.fg.success = lime
-statusbar.url.fg.error = orange
-statusbar.url.fg.warn = yellow
-statusbar.url.fg.hover = aqua
-tab.bg = grey
-tab.bg.selected = black
-tab.fg = white
-tab.seperator = white
-
-[fonts]
-_monospace = Monospace, "DejaVu Sans Mono", Consolas, Monaco,
- "Bitstream Vera Sans Mono", "Andale Mono", "Liberation Mono",
- "Courier New", Courier, monospace, Fixed, Terminal
-completion = 8pt ${_monospace}
-tabbar = 8pt ${_monospace}
-statusbar = 8pt ${_monospace}
-"""
+# Special value for an unset fallback, so None can be passed as fallback.
+_UNSET = object()
def init(confdir):
"""Initialize the global objects based on the config in configdir."""
- global config, colordict, fontdict
- config = Config(confdir)
+ global config, state, colordict, fontdict
+ logging.debug("Config init, confdir {}".format(confdir))
+ config = Config(confdir, 'qutebrowser.conf', read_file('qutebrowser.conf'))
+ state = Config(confdir, 'state', always_save=True)
try:
colordict = ColorDict(config['colors'])
except KeyError:
@@ -229,33 +130,44 @@ class FontDict(dict):
class Config(ConfigParser):
- """Our own ConfigParser subclass."""
+ """Our own ConfigParser subclass.
- configdir = None
- FNAME = 'config'
- default_cp = None
- config_loaded = False
+ Attributes:
+ _configdir: The dictionary to save the config in.
+ _default_cp: The ConfigParser instance supplying the default values.
+ _config_loaded: Whether the config was loaded successfully.
- def __init__(self, configdir):
+ """
+
+ def __init__(self, configdir, fname, default_config=None,
+ always_save=False):
"""Config constructor.
configdir -- directory to store the config in.
+ fname -- Filename of the config file.
+ default_config -- Default config as string.
+ always_save -- Whether to always save the config, even when it wasn't
+ loaded.
"""
super().__init__(interpolation=ExtendedInterpolation())
- self.default_cp = ConfigParser(interpolation=ExtendedInterpolation())
- self.default_cp.optionxform = lambda opt: opt # be case-insensitive
- self.default_cp.read_string(default_config)
- if not self.configdir:
+ self._config_loaded = False
+ self.always_save = always_save
+ self._configdir = configdir
+ self._default_cp = ConfigParser(interpolation=ExtendedInterpolation())
+ self._default_cp.optionxform = lambda opt: opt # be case-insensitive
+ if default_config is not None:
+ self._default_cp.read_string(default_config)
+ if not self._configdir:
return
self.optionxform = lambda opt: opt # be case-insensitive
- self.configdir = configdir
- self.configfile = os.path.join(self.configdir, self.FNAME)
+ self._configdir = configdir
+ self.configfile = os.path.join(self._configdir, fname)
if not os.path.isfile(self.configfile):
return
logging.debug("Reading config from {}".format(self.configfile))
self.read(self.configfile)
- self.config_loaded = True
+ self._config_loaded = True
def __getitem__(self, key):
"""Get an item from the configparser or default dict.
@@ -266,25 +178,42 @@ class Config(ConfigParser):
try:
return super().__getitem__(key)
except KeyError:
- return self.default_cp[key]
+ return self._default_cp[key]
- def get(self, *args, **kwargs):
+ def get(self, *args, raw=False, vars=None, fallback=_UNSET):
"""Get an item from the configparser or default dict.
Extend ConfigParser's get().
+ This is a bit of a hack, but it (hopefully) works like this:
+ - Get value from original configparser.
+ - If that's not available, try the default_cp configparser
+ - If that's not available, try the fallback given as kwarg
+ - If that's not available, we're doomed.
+
"""
- if 'fallback' in kwargs:
- del kwargs['fallback']
- fallback = self.default_cp.get(*args, **kwargs)
- return super().get(*args, fallback=fallback, **kwargs)
+ # pylint: disable=redefined-builtin
+ try:
+ return super().get(*args, raw=raw, vars=vars)
+ except (NoSectionError, NoOptionError):
+ pass
+ try:
+ return self._default_cp.get(*args, raw=raw, vars=vars)
+ except (NoSectionError, NoOptionError):
+ if fallback is _UNSET:
+ raise
+ else:
+ return fallback
def save(self):
"""Save the config file."""
- if self.configdir is None or not self.config_loaded:
+ if self._configdir is None or (not self._config_loaded and
+ not self.always_save):
+ logging.error("Not saving config (dir {}, loaded {})".format(
+ self._configdir, self._config_loaded))
return
- if not os.path.exists(self.configdir):
- os.makedirs(self.configdir, 0o755)
+ if not os.path.exists(self._configdir):
+ os.makedirs(self._configdir, 0o755)
logging.debug("Saving config to {}".format(self.configfile))
with open(self.configfile, 'w') as f:
self.write(f)
diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py
index a7420bf35..829cd4c11 100644
--- a/qutebrowser/utils/misc.py
+++ b/qutebrowser/utils/misc.py
@@ -17,6 +17,7 @@
"""Other utilities which don't fit anywhere else."""
+import sys
import os.path
from PyQt5.QtCore import pyqtRemoveInputHook
@@ -50,3 +51,16 @@ def read_file(filename):
fn = os.path.join(qutebrowser.basedir, filename)
with open(fn, 'r', encoding='UTF-8') as f:
return f.read()
+
+
+def trace_lines(do_trace):
+ """Turn on/off printing each executed line."""
+ def trace(frame, event, _):
+ """Trace function passed to sys.settrace."""
+ print("{}, {}:{}".format(event, frame.f_code.co_filename,
+ frame.f_lineno))
+ return trace
+ if do_trace:
+ sys.settrace(trace)
+ else:
+ sys.settrace(None)
diff --git a/qutebrowser/utils/signals.py b/qutebrowser/utils/signals.py
index a124f3cae..69ca4ab24 100644
--- a/qutebrowser/utils/signals.py
+++ b/qutebrowser/utils/signals.py
@@ -42,10 +42,13 @@ def dbg_signal(sig, args):
class SignalCache(QObject):
- """Cache signals emitted by an object, and re-emit them later."""
+ """Cache signals emitted by an object, and re-emit them later.
- uncached = None
- signal_dict = None
+ Attributes:
+ _uncached: A list of signals which should not be cached.
+ _signal_dict: The internal mapping of signals we got.
+
+ """
def __init__(self, uncached=None):
"""Create a new SignalCache.
@@ -56,10 +59,14 @@ class SignalCache(QObject):
"""
super().__init__()
if uncached is None:
- self.uncached = []
+ self._uncached = []
else:
- self.uncached = uncached
- self.signal_dict = OrderedDict()
+ self._uncached = uncached
+ self._signal_dict = OrderedDict()
+
+ def _signal_needs_caching(self, signal):
+ """Return True if a signal should be cached, false otherwise."""
+ return not signal_name(signal) in self._uncached
def add(self, sig, args):
"""Add a new signal to the signal cache.
@@ -71,21 +78,17 @@ class SignalCache(QObject):
"""
if not self._signal_needs_caching(sig):
return
- had_signal = sig.signal in self.signal_dict
- self.signal_dict[sig.signal] = (sig, args)
+ had_signal = sig.signal in self._signal_dict
+ self._signal_dict[sig.signal] = (sig, args)
if had_signal:
- self.signal_dict.move_to_end(sig.signal)
+ self._signal_dict.move_to_end(sig.signal)
def clear(self):
"""Clear/purge the signal cache."""
- self.signal_dict.clear()
+ self._signal_dict.clear()
def replay(self):
"""Replay all cached signals."""
- for (signal, args) in self.signal_dict.values():
+ for (signal, args) in self._signal_dict.values():
logging.debug('emitting {}'.format(dbg_signal(signal, args)))
signal.emit(*args)
-
- def _signal_needs_caching(self, signal):
- """Return True if a signal should be cached, false otherwise."""
- return not signal_name(signal) in self.uncached
diff --git a/qutebrowser/utils/style.py b/qutebrowser/utils/style.py
index 344ab959c..4b7a654b4 100644
--- a/qutebrowser/utils/style.py
+++ b/qutebrowser/utils/style.py
@@ -38,6 +38,9 @@ class Style(QCommonStyle):
http://stackoverflow.com/a/17294081
https://code.google.com/p/makehuman/source/browse/trunk/makehuman/lib/qtgui.py # noqa # pylint: disable=line-too-long
+ Attributes:
+ _style: The base/"parent" style.
+
"""
def __init__(self, style):
diff --git a/qutebrowser/utils/url.py b/qutebrowser/utils/url.py
index b48a97ece..acc5f154c 100644
--- a/qutebrowser/utils/url.py
+++ b/qutebrowser/utils/url.py
@@ -27,6 +27,52 @@ from PyQt5.QtCore import QUrl
import qutebrowser.utils.config as config
+def _get_search_url(txt):
+ """Return a search engine URL (QUrl) for a text."""
+ logging.debug('Finding search engine for "{}"'.format(txt))
+ r = re.compile(r'(^|\s+)!(\w+)($|\s+)')
+ m = r.search(txt)
+ if m:
+ engine = m.group(2)
+ # FIXME why doesn't fallback work?!
+ template = config.config.get('searchengines', engine, fallback=None)
+ term = r.sub('', txt)
+ logging.debug('engine {}, term "{}"'.format(engine, term))
+ else:
+ template = config.config.get('searchengines', 'DEFAULT',
+ fallback=None)
+ term = txt
+ logging.debug('engine: default, term "{}"'.format(txt))
+ if template is None or not term:
+ raise ValueError
+ # pylint: disable=maybe-no-member
+ return QUrl.fromUserInput(template.format(urllib.parse.quote(term)))
+
+
+def _is_url_naive(url):
+ """Naive check if given url (QUrl) is really an url."""
+ PROTOCOLS = ['http://', 'https://']
+ u = urlstring(url)
+ return (any(u.startswith(proto) for proto in PROTOCOLS) or '.' in u or
+ u == 'localhost')
+
+
+def _is_url_dns(url):
+ """Check if an url (QUrl) is really an url via DNS."""
+ # FIXME we could probably solve this in a nicer way by attempting to open
+ # the page in the webview, and then open the search if that fails.
+ host = url.host()
+ logging.debug("DNS request for {}".format(host))
+ if not host:
+ return False
+ try:
+ socket.gethostbyname(host)
+ except socket.gaierror:
+ return False
+ else:
+ return True
+
+
def qurl(url):
"""Get a QUrl from an url string."""
return url if isinstance(url, QUrl) else QUrl(url)
@@ -49,7 +95,7 @@ def fuzzy_url(url):
"""
u = qurl(url)
urlstr = urlstring(url)
- if (not config.config.getboolean('general', 'auto_search')) or is_url(u):
+ if is_url(u):
# probably an address
logging.debug("url is a fuzzy address")
newurl = QUrl.fromUserInput(urlstr)
@@ -64,27 +110,6 @@ def fuzzy_url(url):
return newurl
-def _get_search_url(txt):
- """Return a search engine URL (QUrl) for a text."""
- logging.debug('Finding search engine for "{}"'.format(txt))
- r = re.compile(r'(^|\s+)!(\w+)($|\s+)')
- m = r.search(txt)
- if m:
- engine = m.group(2)
- # FIXME why doesn't fallback work?!
- template = config.config.get('searchengines', engine, fallback=None)
- term = r.sub('', txt)
- logging.debug('engine {}, term "{}"'.format(engine, term))
- else:
- template = config.config.get('searchengines', 'DEFAULT',
- fallback=None)
- term = txt
- logging.debug('engine: default, term "{}"'.format(txt))
- if template is None or not term:
- raise ValueError
- return QUrl.fromUserInput(template.format(urllib.parse.quote(term)))
-
-
def is_about_url(url):
"""Return True if url (QUrl) is an about:... or other special URL."""
return urlstring(url).replace('http://', '').startswith('about:')
@@ -93,38 +118,37 @@ def is_about_url(url):
def is_url(url):
"""Return True if url (QUrl) seems to be a valid URL."""
urlstr = urlstring(url)
- logging.debug('Checking if "{}" is an URL'.format(urlstr))
+
+ try:
+ autosearch = config.config.getboolean('general', 'auto_search')
+ except ValueError:
+ autosearch = config.config.get('general', 'auto_search')
+ else:
+ if autosearch:
+ autosearch = 'naive'
+ else:
+ autosearch = None
+
+ logging.debug('Checking if "{}" is an URL (autosearch={}).'.format(
+ urlstr, autosearch))
+
+ if autosearch is None:
+ # no autosearch, so everything is an URL.
+ return True
+
if ' ' in urlstr:
# An URL will never contain a space
logging.debug('Contains space -> no url')
return False
- elif config.config.getboolean('general', 'addressbar_dns_lookup'):
+ elif is_about_url(url):
+ # About URLs are always URLs, even with autosearch=False
+ logging.debug('Is an about URL.')
+ return True
+ elif autosearch == 'dns':
logging.debug('Checking via DNS')
return _is_url_dns(QUrl.fromUserInput(urlstr))
- else:
+ elif autosearch == 'naive':
logging.debug('Checking via naive check')
return _is_url_naive(url)
-
-
-def _is_url_naive(url):
- """Naive check if given url (QUrl) is really an url."""
- PROTOCOLS = ['http://', 'https://']
- u = urlstring(url)
- return (any(u.startswith(proto) for proto in PROTOCOLS) or '.' in u or
- is_about_url(url) or u == 'localhost')
-
-
-def _is_url_dns(url):
- """Check if an url (QUrl) is really an url via DNS."""
- # FIXME we could probably solve this in a nicer way by attempting to open
- # the page in the webview, and then open the search if that fails.
- host = url.host()
- logging.debug("DNS request for {}".format(host))
- if not host:
- return False
- try:
- socket.gethostbyname(host)
- except socket.gaierror:
- return False
else:
- return True
+ raise ValueError("Invalid autosearch value")
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 93a5fd147..3b518205d 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -28,6 +28,29 @@ from PyQt5.QtWebKit import qWebKitVersion
import qutebrowser
+def _git_str():
+ """Try to find out git version and return a string if possible.
+
+ Return None if there was an error or we're not in a git repo.
+
+ """
+ if hasattr(sys, "frozen"):
+ return None
+ try:
+ gitpath = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ os.path.pardir, os.path.pardir)
+ except NameError:
+ return None
+ if not os.path.isdir(os.path.join(gitpath, ".git")):
+ return None
+ try:
+ return subprocess.check_output(
+ ['git', 'describe', '--tags', '--dirty', '--always'],
+ cwd=gitpath).decode('UTF-8').strip()
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return None
+
+
def version():
"""Return a string with various version informations."""
if sys.platform == 'linux':
@@ -56,26 +79,3 @@ def version():
lines.append('\nGit commit: {}'.format(gitver))
return ''.join(lines)
-
-
-def _git_str():
- """Try to find out git version and return a string if possible.
-
- Return None if there was an error or we're not in a git repo.
-
- """
- if hasattr(sys, "frozen"):
- return None
- try:
- gitpath = os.path.join(os.path.dirname(os.path.realpath(__file__)),
- os.path.pardir, os.path.pardir)
- except NameError:
- return None
- if not os.path.isdir(os.path.join(gitpath, ".git")):
- return None
- try:
- return subprocess.check_output(
- ['git', 'describe', '--tags', '--dirty', '--always'],
- cwd=gitpath).decode('UTF-8').strip()
- except (subprocess.CalledProcessError, FileNotFoundError):
- return None
diff --git a/qutebrowser/widgets/browser.py b/qutebrowser/widgets/browser.py
index fc9e07256..0d90abf34 100644
--- a/qutebrowser/widgets/browser.py
+++ b/qutebrowser/widgets/browser.py
@@ -56,23 +56,40 @@ class TabbedBrowser(TabWidget):
- the signal gets filtered with _filter_signals and self.cur_* gets
emitted if the signal occured in the current tab.
+ Attributes:
+ _url_stack: Stack of URLs of closed tabs.
+ _space: Space QShortcut to avoid garbage collection
+ _tabs: A list of open tabs.
+
+ Signals:
+ cur_progress: Progress of the current tab changed (loadProgress).
+ cur_load_started: Current tab started loading (loadStarted)
+ cur_load_finished: Current tab finished loading (loadFinished)
+ cur_statusbar_message: Current tab got a statusbar message
+ (statusBarMessage)
+ cur_url_changed: Current URL changed (urlChanged)
+ cur_link_hovered: Link hovered in current tab (linkHovered)
+ cur_scroll_perc_changed: Scroll percentage of current tab changed.
+ arg 1: x-position in %.
+ arg 2: y-position in %.
+ keypress: A key was pressed.
+ arg: The QKeyEvent leading to the keypress.
+ shutdown_complete: The shuttdown is completed.
+ quit: The last tab was closed, quit application.
+
"""
- cur_progress = pyqtSignal(int) # Progress of the current tab changed
- cur_load_started = pyqtSignal() # Current tab started loading
- cur_load_finished = pyqtSignal(bool) # Current tab finished loading
- cur_statusbar_message = pyqtSignal(str) # Status bar message
- cur_url_changed = pyqtSignal('QUrl') # Current URL changed
- cur_link_hovered = pyqtSignal(str, str, str) # Link hovered in cur tab
- # Current tab changed scroll position
+ cur_progress = pyqtSignal(int)
+ cur_load_started = pyqtSignal()
+ cur_load_finished = pyqtSignal(bool)
+ cur_statusbar_message = pyqtSignal(str)
+ cur_url_changed = pyqtSignal('QUrl')
+ cur_link_hovered = pyqtSignal(str, str, str)
cur_scroll_perc_changed = pyqtSignal(int, int)
- set_cmd_text = pyqtSignal(str) # Set commandline to a given text
+ set_cmd_text = pyqtSignal(str)
keypress = pyqtSignal('QKeyEvent')
- shutdown_complete = pyqtSignal() # All tabs have been shut down.
- quit = pyqtSignal() # Last tab closed, quit application.
- _url_stack = [] # Stack of URLs of closed tabs
- _space = None # Space QShortcut
- _tabs = None
+ shutdown_complete = pyqtSignal()
+ quit = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
@@ -171,9 +188,10 @@ class TabbedBrowser(TabWidget):
try:
self._tabs.remove(tab)
except ValueError:
- logging.error("tab {} could not be removed from tabs {}.".format(
- tab, self._tabs))
+ logging.exception("tab {} could not be removed".format(tab))
+ logging.debug("Tabs after removing: {}".format(self._tabs))
if not self._tabs: # all tabs shut down
+ logging.debug("Tab shutdown complete.")
self.shutdown_complete.emit()
def cur_reload(self, count=None):
@@ -205,7 +223,7 @@ class TabbedBrowser(TabWidget):
# FIXME that does not what I expect
tab = self._widget(count)
if tab is not None:
- preview = QPrintPreviewDialog()
+ preview = QPrintPreviewDialog(self)
preview.paintRequested.connect(tab.print)
preview.exec_()
@@ -451,12 +469,12 @@ class TabbedBrowser(TabWidget):
except TypeError:
pass
tabcount = self.count()
- logging.debug("Shutting down {} tabs...".format(tabcount))
if tabcount == 0:
+ logging.debug("No tabs -> shutdown complete")
self.shutdown_complete.emit()
return
for tabidx in range(tabcount):
- logging.debug("shutdown {}".format(tabidx))
+ logging.debug("Shutting down tab {}/{}".format(tabidx, tabcount))
tab = self.widget(tabidx)
tab.shutdown(callback=functools.partial(self._cb_tab_shutdown,
tab))
@@ -468,30 +486,39 @@ class BrowserTab(QWebView):
Our own subclass of a QWebView with some added bells and whistles.
+ Attributes:
+ page_: The QWebPage behind the view
+ signal_cache: The signal cache associated with the view.
+ _scroll_pos: The old scroll position.
+ _shutdown_callback: Callback to be called after shutdown.
+ _open_new_tab: Whether to open a new tab for the next action.
+ _shutdown_callback: The callback to call after shutting down.
+ _destroyed: Dict of all items to be destroyed on shtudown.
+ Signals:
+ scroll_pos_changed: Scroll percentage of current tab changed.
+ arg 1: x-position in %.
+ arg 2: y-position in %.
+ open_tab: A new tab should be opened.
+ arg: The address to open
+ linkHovered: QWebPages linkHovered signal exposed.
+
"""
- progress = 0
scroll_pos_changed = pyqtSignal(int, int)
open_tab = pyqtSignal('QUrl')
linkHovered = pyqtSignal(str, str, str)
- _scroll_pos = (-1, -1)
- _shutdown_callback = None # callback to be called after shutdown
- _open_new_tab = False # open new tab for the next action
- _destroyed = None # Dict of all items to be destroyed.
- page_ = None # QWebPage
- # dict of tab specific signals, and the values we last got from them.
- signal_cache = None
def __init__(self, parent=None):
super().__init__(parent)
+ self._scroll_pos = (-1, -1)
+ self._shutdown_callback = None
+ self._open_new_tab = False
self._destroyed = {}
self.page_ = BrowserPage(self)
self.setPage(self.page_)
self.signal_cache = SignalCache(uncached=['linkHovered'])
- self.loadProgress.connect(self.on_load_progress)
self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page_.linkHovered.connect(self.linkHovered)
- self.installEventFilter(self)
self.linkClicked.connect(self.on_link_clicked)
# FIXME find some way to hide scrollbars without setScrollBarPolicy
@@ -531,17 +558,6 @@ class BrowserTab(QWebView):
else:
self.openurl(url)
- @pyqtSlot(int)
- def on_load_progress(self, prog):
- """Update the progress property if the loading progress changed.
-
- Slot for the loadProgress signal.
-
- prog -- New progress.
-
- """
- self.progress = prog
-
def shutdown(self, callback=None):
"""Shut down the tab cleanly and remove it.
@@ -561,55 +577,57 @@ class BrowserTab(QWebView):
self.settings().setAttribute(QWebSettings.JavascriptEnabled, False)
self._destroyed[self.page_] = False
- self.page_.destroyed.connect(functools.partial(self.on_destroyed,
+ self.page_.destroyed.connect(functools.partial(self._on_destroyed,
self.page_))
self.page_.deleteLater()
self._destroyed[self] = False
- self.destroyed.connect(functools.partial(self.on_destroyed, self))
+ self.destroyed.connect(functools.partial(self._on_destroyed, self))
self.deleteLater()
netman = self.page_.network_access_manager
self._destroyed[netman] = False
netman.abort_requests()
- netman.destroyed.connect(functools.partial(self.on_destroyed, netman))
+ netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
netman.deleteLater()
- logging.debug("Shutdown scheduled")
+ logging.debug("Tab shutdown scheduled")
- def on_destroyed(self, sender):
+ def _on_destroyed(self, sender):
"""Called when a subsystem has been destroyed during shutdown."""
self._destroyed[sender] = True
+ dbgout = '\n'.join(['{}: {}'.format(k.__class__.__name__, v)
+ for (k, v) in self._destroyed.items()])
+ logging.debug("{} has been destroyed, new status:\n{}".format(
+ sender.__class__.__name__, dbgout))
if all(self._destroyed.values()):
if self._shutdown_callback is not None:
+ logging.debug("Everything destroyed, calling callback")
self._shutdown_callback()
- def eventFilter(self, watched, e):
- """Dirty hack to emit a signal if the scroll position changed.
+ def paintEvent(self, e):
+ """Extend paintEvent to emit a signal if the scroll position changed.
- 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.
+ This is a bit of a hack: 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.
+ e -- The QPaintEvent.
"""
- if watched == self and e.type() == QEvent.Paint:
+ frame = self.page_.mainFrame()
+ new_pos = (frame.scrollBarValue(Qt.Horizontal),
+ frame.scrollBarValue(Qt.Vertical))
+ if self._scroll_pos != new_pos:
+ self._scroll_pos = new_pos
+ logging.debug("Updating scroll position")
frame = self.page_.mainFrame()
- new_pos = (frame.scrollBarValue(Qt.Horizontal),
- frame.scrollBarValue(Qt.Vertical))
- if self._scroll_pos != new_pos:
- self._scroll_pos = new_pos
- logging.debug("Updating scroll position")
- frame = self.page_.mainFrame()
- m = (frame.scrollBarMaximum(Qt.Horizontal),
- frame.scrollBarMaximum(Qt.Vertical))
- perc = (round(100 * new_pos[0] / m[0]) if m[0] != 0 else 0,
- round(100 * new_pos[1] / m[1]) if m[1] != 0 else 0)
- self.scroll_pos_changed.emit(*perc)
- # we're not actually filtering something, let superclass handle the
- # event
- return super().eventFilter(watched, e)
+ m = (frame.scrollBarMaximum(Qt.Horizontal),
+ frame.scrollBarMaximum(Qt.Vertical))
+ perc = (round(100 * new_pos[0] / m[0]) if m[0] != 0 else 0,
+ round(100 * new_pos[1] / m[1]) if m[1] != 0 else 0)
+ self.scroll_pos_changed.emit(*perc)
+ # Let superclass handle the event
+ return super().paintEvent(e)
def event(self, e):
"""Check if a link was clicked with the middle button or Ctrl.
@@ -630,10 +648,13 @@ class BrowserTab(QWebView):
class BrowserPage(QWebPage):
- """Our own QWebPage with advanced features."""
+ """Our own QWebPage with advanced features.
- _extension_handlers = None
- network_access_manager = None
+ Attributes:
+ _extension_handlers: Mapping of QWebPage extensions to their handlers.
+ network_access_manager: The QNetworkAccessManager used.
+
+ """
def __init__(self, parent=None):
super().__init__(parent)
@@ -677,9 +698,12 @@ class BrowserPage(QWebPage):
class NetworkManager(QNetworkAccessManager):
- """Our own QNetworkAccessManager."""
+ """Our own QNetworkAccessManager.
- _requests = None
+ Attributes:
+ _requests: Pending requests.
+
+ """
def __init__(self, parent=None):
self._requests = {}
diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py
index 951d378ad..9035f7569 100644
--- a/qutebrowser/widgets/completion.py
+++ b/qutebrowser/widgets/completion.py
@@ -45,9 +45,22 @@ class CompletionView(QTreeView):
Highlights completions based on marks in the UserRole.
+ Attributes:
+ _STYLESHEET: The stylesheet template for the CompletionView.
+ _completion_models: dict of available completion models.
+ _ignore_next: Whether to ignore the next cmd_text_changed signal.
+ _enabled: Whether showing the CompletionView is enabled.
+ _completing: Whether we're currently completing something.
+ _height: The height to use for the CompletionView.
+ _delegate: The item delegate used.
+
+ Signals:
+ append_cmd_text: Command text which should be appended to the
+ statusbar.
+
"""
- _stylesheet = """
+ _STYLESHEET = """
QTreeView {{
{font[completion]}
{color[completion.fg]}
@@ -77,25 +90,24 @@ class CompletionView(QTreeView):
# like one anymore
# FIXME somehow only the first column is yellow, even with
# setAllColumnsShowFocus
- completion_models = {}
append_cmd_text = pyqtSignal(str)
- ignore_next = False
- enabled = True
- completing = False
- height = QPoint(0, 200)
- _delegate = None
def __init__(self, parent=None):
super().__init__(parent)
- self.enabled = config.config.getboolean('general', 'show_completion')
- self.completion_models[''] = None
- self.completion_models['command'] = CommandCompletionModel()
+ self._height = QPoint(0, 200) # FIXME make that configurable
+ self._enabled = config.config.getboolean('general', 'show_completion')
+ self._completion_models = {}
+ self._completion_models[''] = None
+ self._completion_models['command'] = CommandCompletionModel()
+ self._ignore_next = False
+ self._completing = False
+
self.model = CompletionFilterModel()
self.setModel(self.model)
self.setmodel('command')
- self._delegate = CompletionItemDelegate(self)
+ self._delegate = _CompletionItemDelegate(self)
self.setItemDelegate(self._delegate)
- self.setStyleSheet(config.get_stylesheet(self._stylesheet))
+ self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.setHeaderHidden(True)
self.setIndentation(0)
@@ -117,7 +129,7 @@ class CompletionView(QTreeView):
model -- A QAbstractItemModel with available completions.
"""
- self.model.setsrc(self.completion_models[model])
+ self.model.srcmodel = self._completion_models[model]
self.expandAll()
self.resizeColumnToContents(0)
@@ -131,7 +143,7 @@ class CompletionView(QTreeView):
"""
bottomleft = geom.topLeft()
bottomright = geom.topRight()
- topleft = bottomleft - self.height
+ topleft = bottomleft - self._height
assert topleft.x() < bottomright.x()
assert topleft.y() < bottomright.y()
self.setGeometry(QRect(topleft, bottomright))
@@ -144,7 +156,7 @@ class CompletionView(QTreeView):
pos -- A QPoint containing the statusbar position.
"""
- self.move(pos - self.height)
+ self.move(pos - self._height)
@pyqtSlot(str)
def on_cmd_text_changed(self, text):
@@ -154,22 +166,22 @@ class CompletionView(QTreeView):
text -- The new text
"""
- if self.ignore_next:
+ if self._ignore_next:
# Text changed by a completion, so we don't have to complete again.
- self.ignore_next = False
+ self._ignore_next = False
return
# FIXME more sophisticated completions
if ' ' in text or not text.startswith(':'):
self.hide()
- self.completing = False
+ self._completing = False
return
- self.completing = True
+ self._completing = True
self.setmodel('command')
text = text.lstrip(':')
self.model.pattern = text
self.model.srcmodel.mark_all_items(text)
- if self.enabled:
+ if self._enabled:
self.show()
@pyqtSlot(bool)
@@ -182,7 +194,7 @@ class CompletionView(QTreeView):
shift -- Whether shift is pressed or not.
"""
- if not self.completing:
+ if not self._completing:
# No completion running at the moment, ignore keypress
return
idx = self._next_idx(shift)
@@ -190,7 +202,7 @@ class CompletionView(QTreeView):
idx, QItemSelectionModel.ClearAndSelect)
data = self.model.data(idx)
if data is not None:
- self.ignore_next = True
+ self._ignore_next = True
self.append_cmd_text.emit(self.model.data(idx) + ' ')
def _next_idx(self, upwards):
@@ -217,7 +229,7 @@ class CompletionView(QTreeView):
return idx
-class CompletionItemDelegate(QStyledItemDelegate):
+class _CompletionItemDelegate(QStyledItemDelegate):
"""Delegate used by CompletionView to draw individual items.
@@ -227,12 +239,20 @@ class CompletionItemDelegate(QStyledItemDelegate):
Original implementation:
qt/src/gui/styles/qcommonstyle.cpp:drawControl:2153
+ Attributes:
+ _opt: The QStyleOptionViewItem which is used.
+ _style: The style to be used.
+ _painter: The QPainter to be used.
+ _doc: The QTextDocument to be used.
+
"""
- opt = None
- style = None
- painter = None
- doc = None
+ def __init__(self, parent=None):
+ self._painter = None
+ self._opt = None
+ self._doc = None
+ self._style = None
+ super().__init__(parent)
def sizeHint(self, option, index):
"""Override sizeHint of QStyledItemDelegate.
@@ -244,49 +264,48 @@ class CompletionItemDelegate(QStyledItemDelegate):
value = index.data(Qt.SizeHintRole)
if value is not None:
return value
- self.opt = QStyleOptionViewItem(option)
- self.initStyleOption(self.opt, index)
- self.style = self.opt.widget.style()
+ self._opt = QStyleOptionViewItem(option)
+ self.initStyleOption(self._opt, index)
+ self._style = self._opt.widget.style()
self._get_textdoc(index)
- docsize = self.doc.size().toSize()
- size = self.style.sizeFromContents(QStyle.CT_ItemViewItem, self.opt,
- docsize, self.opt.widget)
+ docsize = self._doc.size().toSize()
+ size = self._style.sizeFromContents(QStyle.CT_ItemViewItem, self._opt,
+ docsize, self._opt.widget)
return size + QSize(10, 1)
def paint(self, painter, option, index):
"""Override the QStyledItemDelegate paint function."""
- painter.save()
-
- self.painter = painter
- self.opt = QStyleOptionViewItem(option)
- self.initStyleOption(self.opt, index)
- self.style = self.opt.widget.style()
+ self._painter = painter
+ self._painter.save()
+ self._opt = QStyleOptionViewItem(option)
+ self.initStyleOption(self._opt, index)
+ self._style = self._opt.widget.style()
self._draw_background()
self._draw_icon()
self._draw_text(index)
self._draw_focus_rect()
- painter.restore()
+ self._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)
+ 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)
+ icon_rect = self._style.subElementRect(
+ self._style.SE_ItemViewItemDecoration, self._opt, self._opt.widget)
mode = QIcon.Normal
- if not self.opt.state & QStyle.State_Enabled:
+ if not self._opt.state & QStyle.State_Enabled:
mode = QIcon.Disabled
- elif self.opt.state & QStyle.State_Selected:
+ elif self._opt.state & QStyle.State_Selected:
mode = QIcon.Selected
- state = QIcon.On if self.opt.state & QStyle.State_Open else QIcon.Off
- self.opt.icon.paint(self.painter, icon_rect,
- self.opt.decorationAlignment, mode, state)
+ state = QIcon.On if self._opt.state & QStyle.State_Open else QIcon.Off
+ self._opt.icon.paint(self._painter, icon_rect,
+ self._opt.decorationAlignment, mode, state)
def _draw_text(self, index):
"""Draw the text of an ItemViewItem.
@@ -297,13 +316,13 @@ class CompletionItemDelegate(QStyledItemDelegate):
index of the item of the item -- The QModelIndex of the item to draw.
"""
- if not self.opt.text:
+ if not self._opt.text:
return
- text_rect_ = self.style.subElementRect(self.style.SE_ItemViewItemText,
- self.opt, self.opt.widget)
- margin = self.style.pixelMetric(QStyle.PM_FocusFrameHMargin, self.opt,
- self.opt.widget) + 1
+ text_rect_ = self._style.subElementRect(
+ self._style.SE_ItemViewItemText, self._opt, self._opt.widget)
+ margin = self._style.pixelMetric(QStyle.PM_FocusFrameHMargin,
+ self._opt, self._opt.widget) + 1
# remove width padding
text_rect = text_rect_.adjusted(margin, 0, -margin, 0)
# move text upwards a bit
@@ -311,8 +330,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
text_rect.adjust(0, -1, 0, -1)
else:
text_rect.adjust(0, -2, 0, -2)
- self.painter.save()
- state = self.opt.state
+ self._painter.save()
+ state = self._opt.state
if state & QStyle.State_Enabled and state & QStyle.State_Active:
cg = QPalette.Normal
elif state & QStyle.State_Enabled:
@@ -321,22 +340,22 @@ class CompletionItemDelegate(QStyledItemDelegate):
cg = QPalette.Disabled
if state & QStyle.State_Selected:
- self.painter.setPen(self.opt.palette.color(
+ self._painter.setPen(self._opt.palette.color(
cg, QPalette.HighlightedText))
# FIXME this is a dirty fix for the text jumping by one pixel...
# we really should do this properly somehow
text_rect.adjust(0, -1, 0, 0)
else:
- self.painter.setPen(self.opt.palette.color(cg, QPalette.Text))
+ self._painter.setPen(self._opt.palette.color(cg, QPalette.Text))
if state & QStyle.State_Editing:
- self.painter.setPen(self.opt.palette.color(cg, QPalette.Text))
- self.painter.drawRect(text_rect_.adjusted(0, 0, -1, -1))
+ self._painter.setPen(self._opt.palette.color(cg, QPalette.Text))
+ self._painter.drawRect(text_rect_.adjusted(0, 0, -1, -1))
- self.painter.translate(text_rect.left(), text_rect.top())
+ self._painter.translate(text_rect.left(), text_rect.top())
self._get_textdoc(index)
self._draw_textdoc(text_rect)
- self.painter.restore()
+ self._painter.restore()
def _draw_textdoc(self, text_rect):
"""Draw the QTextDocument of an item.
@@ -345,7 +364,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
"""
clip = QRectF(0, 0, text_rect.width(), text_rect.height())
- self.doc.drawContents(self.painter, clip)
+ self._doc.drawContents(self._painter, clip)
def _get_textdoc(self, index):
"""Create the QTextDocument of an item.
@@ -356,32 +375,32 @@ class CompletionItemDelegate(QStyledItemDelegate):
# FIXME we probably should do eliding here. See
# qcommonstyle.cpp:viewItemDrawText
text_option = QTextOption()
- if self.opt.features & QStyleOptionViewItem.WrapText:
+ if self._opt.features & QStyleOptionViewItem.WrapText:
text_option.setWrapMode(QTextOption.WordWrap)
else:
text_option.setWrapMode(QTextOption.ManualWrap)
- text_option.setTextDirection(self.opt.direction)
+ text_option.setTextDirection(self._opt.direction)
text_option.setAlignment(QStyle.visualAlignment(
- self.opt.direction, self.opt.displayAlignment))
+ self._opt.direction, self._opt.displayAlignment))
- self.doc = QTextDocument()
+ self._doc = QTextDocument(self)
if index.parent().isValid():
- self.doc.setPlainText(self.opt.text)
+ self._doc.setPlainText(self._opt.text)
else:
- self.doc.setHtml('{}'.format(html.escape(self.opt.text)))
- self.doc.setDefaultFont(self.opt.font)
- self.doc.setDefaultTextOption(text_option)
- self.doc.setDefaultStyleSheet(config.get_stylesheet("""
+ self._doc.setHtml('{}'.format(html.escape(self._opt.text)))
+ self._doc.setDefaultFont(self._opt.font)
+ self._doc.setDefaultTextOption(text_option)
+ self._doc.setDefaultStyleSheet(config.get_stylesheet("""
.highlight {{
{color[completion.match.fg]}
}}
"""))
- self.doc.setDocumentMargin(2)
+ self._doc.setDocumentMargin(2)
if index.column() == 0:
marks = index.data(Qt.UserRole)
for mark in marks:
- cur = QTextCursor(self.doc)
+ cur = QTextCursor(self._doc)
cur.setPosition(mark[0])
cur.setPosition(mark[1], QTextCursor.KeepAnchor)
txt = cur.selectedText()
@@ -391,12 +410,12 @@ class CompletionItemDelegate(QStyledItemDelegate):
def _draw_focus_rect(self):
"""Draw the focus rectangle of an ItemViewItem."""
- state = self.opt.state
+ state = self._opt.state
if not state & QStyle.State_HasFocus:
return
- o = self.opt
- o.rect = self.style.subElementRect(self.style.SE_ItemViewItemFocusRect,
- self.opt, self.opt.widget)
+ o = self._opt
+ o.rect = self._style.subElementRect(
+ self._style.SE_ItemViewItemFocusRect, self._opt, self._opt.widget)
o.state |= QStyle.State_KeyboardFocusChange | QStyle.State_Item
if state & QStyle.State_Enabled:
cg = QPalette.Normal
@@ -406,6 +425,6 @@ class CompletionItemDelegate(QStyledItemDelegate):
role = QPalette.Highlight
else:
role = QPalette.Window
- o.backgroundColor = self.opt.palette.color(cg, role)
- self.style.drawPrimitive(QStyle.PE_FrameFocusRect, o, self.painter,
- self.opt.widget)
+ o.backgroundColor = self._opt.palette.color(cg, role)
+ self._style.drawPrimitive(QStyle.PE_FrameFocusRect, o, self._painter,
+ self._opt.widget)
diff --git a/qutebrowser/widgets/crash.py b/qutebrowser/widgets/crash.py
index 450505d0c..b5da98dbf 100644
--- a/qutebrowser/widgets/crash.py
+++ b/qutebrowser/widgets/crash.py
@@ -29,14 +29,18 @@ from qutebrowser.utils.version import version
class CrashDialog(QDialog):
- """Dialog which gets shown after there was a crash."""
+ """Dialog which gets shown after there was a crash.
- vbox = None
- lbl = None
- txt = None
- hbox = None
- btn_quit = None
- btn_restore = None
+ Attributes:
+ These are just here to have a static reference to avoid GCing.
+ _vbox: The main QVBoxLayout
+ _lbl: The QLabel with the static text
+ _txt: The QTextEdit with the crash information
+ _hbox: The QHboxLayout containing the buttons
+ _btn_quit: The quit button
+ _btn_restore: the restore button
+
+ """
def __init__(self, pages, cmdhist, exc):
super().__init__()
@@ -44,8 +48,8 @@ class CrashDialog(QDialog):
self.setWindowTitle('Whoops!')
self.setModal(True)
- self.vbox = QVBoxLayout()
- self.lbl = QLabel(self)
+ self._vbox = QVBoxLayout(self)
+ self._lbl = QLabel()
text = ('Argh! qutebrowser crashed unexpectedly.
'
'Please review the info below to remove sensitive data and '
'then submit it to '
@@ -53,29 +57,29 @@ class CrashDialog(QDialog):
if pages:
text += ('You can click "Restore tabs" to attempt to reopen your '
'open tabs.')
- self.lbl.setText(text)
- self.lbl.setWordWrap(True)
- self.vbox.addWidget(self.lbl)
+ self._lbl.setText(text)
+ self._lbl.setWordWrap(True)
+ self._vbox.addWidget(self._lbl)
- self.txt = QTextEdit(self)
- self.txt.setReadOnly(True)
- self.txt.setText(self._crash_info(pages, cmdhist, exc))
- self.vbox.addWidget(self.txt)
- self.setLayout(self.vbox)
+ self._txt = QTextEdit()
+ self._txt.setReadOnly(True)
+ self._txt.setText(self._crash_info(pages, cmdhist, exc))
+ self._vbox.addWidget(self._txt)
- self.hbox = QHBoxLayout()
- self.btn_quit = QPushButton(self)
- self.btn_quit.setText('Quit')
- self.btn_quit.clicked.connect(self.reject)
- self.hbox.addWidget(self.btn_quit)
+ self._hbox = QHBoxLayout()
+ self._hbox.addStretch()
+ self._btn_quit = QPushButton()
+ self._btn_quit.setText('Quit')
+ self._btn_quit.clicked.connect(self.reject)
+ self._hbox.addWidget(self._btn_quit)
if pages:
- self.btn_restore = QPushButton(self)
- self.btn_restore.setText('Restore tabs')
- self.btn_restore.clicked.connect(self.accept)
- self.btn_restore.setDefault(True)
- self.hbox.addWidget(self.btn_restore)
+ self._btn_restore = QPushButton()
+ self._btn_restore.setText('Restore tabs')
+ self._btn_restore.clicked.connect(self.accept)
+ self._btn_restore.setDefault(True)
+ self._hbox.addWidget(self._btn_restore)
- self.vbox.addLayout(self.hbox)
+ self._vbox.addLayout(self._hbox)
def _crash_info(self, pages, cmdhist, exc):
"""Gather crash information to display."""
diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py
index 5674775ab..72da4f74e 100644
--- a/qutebrowser/widgets/mainwindow.py
+++ b/qutebrowser/widgets/mainwindow.py
@@ -17,11 +17,16 @@
"""The main window of QuteBrowser."""
+import binascii
+from base64 import b64decode
+
+from PyQt5.QtCore import QRect
from PyQt5.QtWidgets import QWidget, QVBoxLayout
from qutebrowser.widgets.statusbar import StatusBar
from qutebrowser.widgets.browser import TabbedBrowser
from qutebrowser.widgets.completion import CompletionView
+import qutebrowser.utils.config as config
class MainWindow(QWidget):
@@ -31,30 +36,41 @@ class MainWindow(QWidget):
Adds all needed components to a vbox, initializes subwidgets and connects
signals.
- """
+ Attributes:
+ tabs: The TabbedBrowser widget.
+ status: The StatusBar widget.
+ _vbox: The main QVBoxLayout.
- vbox = None
- tabs = None
- status = None
+ """
def __init__(self):
super().__init__()
self.setWindowTitle('qutebrowser')
- # FIXME maybe store window position/size on exit
- self.resize(800, 600)
+ try:
+ geom = b64decode(config.state['geometry']['mainwindow'],
+ validate=True)
+ except binascii.Error:
+ self._set_default_geometry()
+ else:
+ try:
+ ok = self.restoreGeometry(geom)
+ except KeyError:
+ self._set_default_geometry()
+ if not ok:
+ self._set_default_geometry()
- self.vbox = QVBoxLayout(self)
- self.vbox.setContentsMargins(0, 0, 0, 0)
- self.vbox.setSpacing(0)
+ self._vbox = QVBoxLayout(self)
+ self._vbox.setContentsMargins(0, 0, 0, 0)
+ self._vbox.setSpacing(0)
self.tabs = TabbedBrowser()
- self.vbox.addWidget(self.tabs)
+ self._vbox.addWidget(self.tabs)
self.completion = CompletionView(self)
self.status = StatusBar()
- self.vbox.addWidget(self.status)
+ self._vbox.addWidget(self.status)
self.status.resized.connect(self.completion.resize_to_bar)
self.status.moved.connect(self.completion.move_to_bar)
@@ -80,3 +96,7 @@ class MainWindow(QWidget):
#self.retranslateUi(MainWindow)
#self.tabWidget.setCurrentIndex(0)
#QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+ def _set_default_geometry(self):
+ """Set some sensible default geometry."""
+ self.setGeometry(QRect(50, 50, 800, 600))
diff --git a/qutebrowser/widgets/statusbar.py b/qutebrowser/widgets/statusbar.py
index 3eb02ef9d..d3b6fdb1e 100644
--- a/qutebrowser/widgets/statusbar.py
+++ b/qutebrowser/widgets/statusbar.py
@@ -21,8 +21,7 @@ import logging
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt
from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel,
- QHBoxLayout, QSizePolicy, QShortcut, QStyle,
- QStyleOption)
+ QHBoxLayout, QSizePolicy, QShortcut)
from PyQt5.QtGui import QPainter, QKeySequence, QValidator
import qutebrowser.utils.config as config
@@ -32,20 +31,33 @@ from qutebrowser.utils.url import urlstring
class StatusBar(QWidget):
- """The statusbar at the bottom of the mainwindow."""
+ """The statusbar at the bottom of the mainwindow.
+
+ Attributes:
+ cmd: The Command widget in the statusbar.
+ txt: The Text widget in the statusbar.
+ keystring: The KeyString widget in the statusbar.
+ percentage: The Percentage widget in the statusbar.
+ url: The Url widget in the statusbar.
+ prog: The Progress widget in the statusbar.
+ _hbox: The main QHBoxLayout.
+ _error: If there currently is an error, accessed through the error
+ property.
+ _STYLESHEET: The stylesheet template.
+
+ Signals:
+ resized: Emitted when the statusbar has resized, so the completion
+ widget can adjust its size to it.
+ arg: The new size.
+ moved: Emitted when the statusbar has moved, so the completion widget
+ can move the the right position.
+ arg: The new position.
+
+ """
- hbox = None
- cmd = None
- txt = None
- keystring = None
- percentage = None
- url = None
- prog = None
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
- _error = False
- _option = None
- _stylesheet = """
+ _STYLESHEET = """
QWidget#StatusBar[error="false"] {{
{color[statusbar.bg]}
}}
@@ -60,36 +72,39 @@ class StatusBar(QWidget):
}}
"""
- # TODO: the statusbar should be a bit smaller
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName(self.__class__.__name__)
- self.setStyleSheet(config.get_stylesheet(self._stylesheet))
+ self.setAttribute(Qt.WA_StyledBackground)
+ self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
- self.hbox = QHBoxLayout(self)
- self.hbox.setContentsMargins(0, 0, 0, 0)
- self.hbox.setSpacing(5)
+ self._error = False
+ self._option = None
- self.cmd = Command(self)
- self.hbox.addWidget(self.cmd)
+ self._hbox = QHBoxLayout(self)
+ self._hbox.setContentsMargins(0, 0, 0, 0)
+ self._hbox.setSpacing(5)
- self.txt = Text(self)
- self.hbox.addWidget(self.txt)
- self.hbox.addStretch()
+ self.cmd = _Command(self)
+ self._hbox.addWidget(self.cmd)
- self.keystring = KeyString(self)
- self.hbox.addWidget(self.keystring)
+ self.txt = _Text(self)
+ self._hbox.addWidget(self.txt)
+ self._hbox.addStretch()
- self.url = Url(self)
- self.hbox.addWidget(self.url)
+ self.keystring = _KeyString(self)
+ self._hbox.addWidget(self.keystring)
- self.percentage = Percentage(self)
- self.hbox.addWidget(self.percentage)
+ self.url = _Url(self)
+ self._hbox.addWidget(self.url)
- self.prog = Progress(self)
- self.hbox.addWidget(self.prog)
+ self.percentage = _Percentage(self)
+ self._hbox.addWidget(self.percentage)
+
+ self.prog = _Progress(self)
+ self._hbox.addWidget(self.prog)
@pyqtProperty(bool)
def error(self):
@@ -106,20 +121,11 @@ class StatusBar(QWidget):
"""
self._error = val
- self.setStyleSheet(config.get_stylesheet(self._stylesheet))
-
- def paintEvent(self, e):
- """Override QWIidget.paintEvent to handle stylesheets."""
- # pylint: disable=unused-argument
- self._option = QStyleOption()
- self._option.initFrom(self)
- painter = QPainter(self)
- self.style().drawPrimitive(QStyle.PE_Widget, self._option,
- painter, self)
+ self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
@pyqtSlot(str)
def disp_error(self, text):
- """Displaysan error in the statusbar."""
+ """Display an error in the statusbar."""
self.error = True
self.txt.set_error(text)
@@ -147,24 +153,40 @@ class StatusBar(QWidget):
self.moved.emit(e.pos())
-class Command(QLineEdit):
+class _Command(QLineEdit):
- """The commandline part of the statusbar."""
+ """The commandline part of the statusbar.
+
+ Attributes:
+ history: The command history, with newer commands at the bottom.
+ _statusbar: The statusbar (parent) QWidget.
+ _shortcuts: Defined QShortcuts to prevent GCing.
+ _tmphist: The temporary history for history browsing
+ _histpos: The current position inside _tmphist
+ _validator: The current command validator.
+
+ Signals:
+ got_cmd: Emitted when a command is triggered by the user.
+ arg: The command string.
+ got_search: Emitted when the user started a new search.
+ arg: The search term.
+ got_rev_search: Emitted when the user started a new reverse search.
+ arg: The search term.
+ esc_pressed: Emitted when the escape key was pressed.
+ tab_pressed: Emitted when the tab key was pressed.
+ arg: Whether shift has been pressed.
+ hide_completion: Emitted when the completion widget should be hidden.
+
+ """
+
+ # FIXME we should probably use a proper model for the command history.
- # Emitted when a command is triggered by the user
got_cmd = pyqtSignal(str)
- # Emitted for searches triggered by the user
got_search = pyqtSignal(str)
got_search_rev = pyqtSignal(str)
- statusbar = None # The status bar object
- esc_pressed = pyqtSignal() # Emitted when escape is pressed
- tab_pressed = pyqtSignal(bool) # Emitted when tab is pressed (arg: shift)
- hide_completion = pyqtSignal() # Hide completion window
- history = [] # The command history, with newer commands at the bottom
- _shortcuts = []
- _tmphist = []
- _histpos = None
- _validator = None # CommandValidator
+ esc_pressed = pyqtSignal()
+ tab_pressed = pyqtSignal(bool)
+ hide_completion = pyqtSignal()
# FIXME won't the tab key switch to the next widget?
# See [0] for a possible fix.
@@ -173,7 +195,9 @@ class Command(QLineEdit):
def __init__(self, statusbar):
super().__init__(statusbar)
# FIXME
- self.statusbar = statusbar
+ self._statusbar = statusbar
+ self._histpos = None
+ self._tmphist = []
self.setStyleSheet("""
QLineEdit {
border: 0px;
@@ -181,16 +205,18 @@ class Command(QLineEdit):
background-color: transparent;
}
""")
- self._validator = CommandValidator(self)
+ self._validator = _CommandValidator(self)
self.setValidator(self._validator)
- self.returnPressed.connect(self.on_return_pressed)
+ self.returnPressed.connect(self._on_return_pressed)
self.textEdited.connect(self._histbrowse_stop)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
+ self.history = []
+ self._shortcuts = []
for (key, handler) in [
(Qt.Key_Escape, self.esc_pressed),
- (Qt.Key_Up, self.on_key_up_pressed),
- (Qt.Key_Down, self.on_key_down_pressed),
+ (Qt.Key_Up, self._on_key_up_pressed),
+ (Qt.Key_Down, self._on_key_down_pressed),
(Qt.Key_Tab | Qt.SHIFT, lambda: self.tab_pressed.emit(True)),
(Qt.Key_Tab, lambda: self.tab_pressed.emit(False))
]:
@@ -201,7 +227,7 @@ class Command(QLineEdit):
self._shortcuts.append(sc)
@pyqtSlot()
- def on_return_pressed(self):
+ def _on_return_pressed(self):
"""Handle the command in the status bar."""
signals = {
':': self.got_cmd,
@@ -217,7 +243,7 @@ class Command(QLineEdit):
signals[text[0]].emit(text.lstrip(text[0]))
@pyqtSlot(str)
- def on_set_cmd_text(self, text):
+ def set_cmd_text(self, text):
"""Preset the statusbar to some text."""
self.setText(text)
self.setFocus()
@@ -240,7 +266,7 @@ class Command(QLineEdit):
def focusInEvent(self, e):
"""Clear error message when the statusbar is focused."""
- self.statusbar.clear_error()
+ self._statusbar.clear_error()
super().focusInEvent(e)
def _histbrowse_start(self):
@@ -264,7 +290,7 @@ class Command(QLineEdit):
self._histpos = None
@pyqtSlot()
- def on_key_up_pressed(self):
+ def _on_key_up_pressed(self):
"""Handle Up presses (go back in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos))
if self._histpos is None:
@@ -277,10 +303,10 @@ class Command(QLineEdit):
return
logging.debug("history up: {} / len {} / pos {}".format(
self._tmphist, len(self._tmphist), self._histpos))
- self.set_cmd(self._tmphist[self._histpos])
+ self.set_cmd_text(self._tmphist[self._histpos])
@pyqtSlot()
- def on_key_down_pressed(self):
+ def _on_key_down_pressed(self):
"""Handle Down presses (go forward in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos,
self._tmphist, len(self._tmphist), self._histpos))
@@ -291,10 +317,10 @@ class Command(QLineEdit):
self._histpos += 1
logging.debug("history up: {} / len {} / pos {}".format(
self._tmphist, len(self._tmphist), self._histpos))
- self.set_cmd(self._tmphist[self._histpos])
+ self.set_cmd_text(self._tmphist[self._histpos])
-class CommandValidator(QValidator):
+class _CommandValidator(QValidator):
"""Validator to prevent the : from getting deleted."""
@@ -313,13 +339,17 @@ class CommandValidator(QValidator):
return (QValidator.Invalid, string, pos)
-class Progress(QProgressBar):
+class _Progress(QProgressBar):
- """The progress bar part of the status bar."""
+ """The progress bar part of the status bar.
+
+ Attributes:
+ _STYLESHEET: The stylesheet template.
+
+ """
- statusbar = None
# FIXME for some reason, margin-left is not shown
- _stylesheet = """
+ _STYLESHEET = """
QProgressBar {{
border-radius: 0px;
border: 2px solid transparent;
@@ -332,10 +362,9 @@ class Progress(QProgressBar):
}}
"""
- def __init__(self, statusbar):
- super().__init__(statusbar)
- self.statusbar = statusbar
- self.setStyleSheet(config.get_stylesheet(self._stylesheet))
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)
self.setTextVisible(False)
self.hide()
@@ -356,15 +385,17 @@ class TextBase(QLabel):
Eliding is loosly based on
http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html
- """
+ Attributes:
+ _elidemode: Where to elide the text.
+ _elided_text: The current elided text.
- elidemode = None
- _elided_text = None
+ """
def __init__(self, bar, elidemode=Qt.ElideRight):
super().__init__(bar)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
- self.elidemode = elidemode
+ self._elidemode = elidemode
+ self._elided_text = ''
def setText(self, txt):
"""Extend QLabel::setText to update the elided text afterwards."""
@@ -383,11 +414,11 @@ class TextBase(QLabel):
"""
self._elided_text = self.fontMetrics().elidedText(
- self.text(), self.elidemode, width, Qt.TextShowMnemonic)
+ self.text(), self._elidemode, width, Qt.TextShowMnemonic)
def paintEvent(self, e):
"""Override QLabel::paintEvent to draw elided text."""
- if self.elidemode == Qt.ElideNone:
+ if self._elidemode == Qt.ElideNone:
super().paintEvent(e)
else:
painter = QPainter(self)
@@ -396,30 +427,37 @@ class TextBase(QLabel):
self._elided_text)
-class Text(TextBase):
+class _Text(TextBase):
- """Text displayed in the statusbar."""
+ """Text displayed in the statusbar.
- old_text = ''
+ Attributes:
+ _old_text: The text displayed before the temporary error message.
+
+ """
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._old_text = ''
def set_error(self, text):
"""Display an error message and save current text in old_text."""
- self.old_text = self.text()
+ self._old_text = self.text()
self.setText(text)
def clear_error(self):
"""Clear a displayed error message."""
- self.setText(self.old_text)
+ self.setText(self._old_text)
-class KeyString(TextBase):
+class _KeyString(TextBase):
"""Keychain string displayed in the statusbar."""
pass
-class Percentage(TextBase):
+class _Percentage(TextBase):
"""Reading percentage displayed in the statusbar."""
@@ -434,15 +472,20 @@ class Percentage(TextBase):
self.setText('[{:2}%]'.format(y))
-class Url(TextBase):
+class _Url(TextBase):
- """URL displayed in the statusbar."""
+ """URL displayed in the statusbar.
- _old_url = None
- _old_urltype = None
- _urltype = None # 'normal', 'ok', 'error', 'warn, 'hover'
+ Attributes:
+ _old_url: The URL displayed before the hover URL.
+ _old_urltype: The type of the URL displayed before the hover URL.
+ _urltype: The current URL type. One of normal/ok/error/warn/hover.
+ Accessed via the urltype property.
+ _STYLESHEET: The stylesheet template.
- _stylesheet = """
+ """
+
+ _STYLESHEET = """
QLabel#Url[urltype="normal"] {{
{color[statusbar.url.fg]}
}}
@@ -468,7 +511,10 @@ class Url(TextBase):
"""Override TextBase::__init__ to elide in the middle by default."""
super().__init__(bar, elidemode)
self.setObjectName(self.__class__.__name__)
- self.setStyleSheet(config.get_stylesheet(self._stylesheet))
+ self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
+ self._urltype = None
+ self._old_urltype = None
+ self._old_url = None
@pyqtProperty(str)
def urltype(self):
@@ -480,7 +526,7 @@ class Url(TextBase):
def urltype(self, val):
"""Setter for self.urltype, so it can be used as Qt property."""
self._urltype = val
- self.setStyleSheet(config.get_stylesheet(self._stylesheet))
+ self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
@pyqtSlot(bool)
def on_loading_finished(self, ok):
diff --git a/qutebrowser/widgets/tabbar.py b/qutebrowser/widgets/tabbar.py
index 9a5c75a62..3aa50c1b0 100644
--- a/qutebrowser/widgets/tabbar.py
+++ b/qutebrowser/widgets/tabbar.py
@@ -26,12 +26,17 @@ from qutebrowser.utils.style import Style
class TabWidget(QTabWidget):
- """The tabwidget used for TabbedBrowser."""
+ """The tabwidget used for TabbedBrowser.
+
+ Attributes:
+ _STYLESHEET: The stylesheet template to be used.
+
+ """
# FIXME there is still some ugly 1px white stripe from somewhere if we do
# background-color: grey for QTabBar...
- _stylesheet = """
+ _STYLESHEET = """
QTabWidget::pane {{
position: absolute;
top: 0px;
@@ -63,7 +68,7 @@ class TabWidget(QTabWidget):
super().__init__(parent)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setStyle(Style(self.style()))
- self.setStyleSheet(config.get_stylesheet(self._stylesheet))
+ self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self.setDocumentMode(True)
self.setElideMode(Qt.ElideRight)
self._init_config()