Merge branch 'master' of ssh://lupin/qutebrowser

This commit is contained in:
Florian Bruhin 2014-02-18 18:00:16 +01:00
commit 133d720de5
21 changed files with 1323 additions and 952 deletions

1
TODO
View File

@ -38,7 +38,6 @@ Internationalization
Marks Marks
show infos in statusline, temporary/longer show infos in statusline, temporary/longer
set settings/colors/bindings via commandline set settings/colors/bindings via commandline
write default config with comments
more completions (URLs, ...) more completions (URLs, ...)
SSL handling SSL handling

View File

@ -22,8 +22,10 @@ import sys
import logging import logging
import functools import functools
import subprocess import subprocess
import configparser
from signal import signal, SIGINT from signal import signal, SIGINT
from argparse import ArgumentParser from argparse import ArgumentParser
from base64 import b64encode
# Print a nice traceback on segfault -- only available on Python 3.3+, but if # Print a nice traceback on segfault -- only available on Python 3.3+, but if
# it's unavailable, it doesn't matter much. # it's unavailable, it doesn't matter much.
@ -62,39 +64,43 @@ class QuteBrowser(QApplication):
>>> app = QuteBrowser() >>> app = QuteBrowser()
>>> sys.exit(app.exec_()) >>> 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): def __init__(self):
super().__init__(sys.argv) super().__init__(sys.argv)
self._quit_status = {} self._quit_status = {}
self._timers = []
self._shutting_down = False
sys.excepthook = self._exception_hook sys.excepthook = self._exception_hook
self._parseopts() self._args = self._parseopts()
self._initlog() self._initlog()
self._initmisc() self._initmisc()
self.dirs = AppDirs('qutebrowser') self._dirs = AppDirs('qutebrowser')
if self.args.confdir is None: if self._args.confdir is None:
confdir = self.dirs.user_config_dir confdir = self._dirs.user_config_dir
elif self.args.confdir == '': elif self._args.confdir == '':
confdir = None confdir = None
else: else:
confdir = self.args.confdir confdir = self._args.confdir
config.init(confdir) config.init(confdir)
self.commandparser = cmdutils.CommandParser() self.commandparser = cmdutils.CommandParser()
self.searchparser = cmdutils.SearchParser() self.searchparser = cmdutils.SearchParser()
self.keyparser = KeyParser(self.mainwindow) self.keyparser = KeyParser(self)
self._init_cmds() self._init_cmds()
self.mainwindow = MainWindow() self.mainwindow = MainWindow()
@ -102,9 +108,9 @@ class QuteBrowser(QApplication):
self.lastWindowClosed.connect(self.shutdown) self.lastWindowClosed.connect(self.shutdown)
self.mainwindow.tabs.keypress.connect(self.keyparser.handle) self.mainwindow.tabs.keypress.connect(self.keyparser.handle)
self.keyparser.set_cmd_text.connect( 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.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.tabs.quit.connect(self.shutdown)
self.mainwindow.status.cmd.got_cmd.connect(self.commandparser.run) self.mainwindow.status.cmd.got_cmd.connect(self.commandparser.run)
self.mainwindow.status.cmd.got_search.connect(self.searchparser.search) self.mainwindow.status.cmd.got_search.connect(self.searchparser.search)
@ -123,7 +129,55 @@ class QuteBrowser(QApplication):
self.mainwindow.show() self.mainwindow.show()
self._python_hacks() self._python_hacks()
timer = QTimer.singleShot(0, self._process_init_args) 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): def _process_init_args(self):
"""Process initial positional args. """Process initial positional args.
@ -137,7 +191,7 @@ class QuteBrowser(QApplication):
QEventLoop.ExcludeSocketNotifiers) QEventLoop.ExcludeSocketNotifiers)
opened_urls = False opened_urls = False
for e in self.args.command: for e in self._args.command:
if e.startswith(':'): if e.startswith(':'):
logging.debug('Startup cmd {}'.format(e)) logging.debug('Startup cmd {}'.format(e))
self.commandparser.run(e.lstrip(':')) self.commandparser.run(e.lstrip(':'))
@ -152,6 +206,20 @@ class QuteBrowser(QApplication):
for url in config.config.get('general', 'startpage').split(','): for url in config.config.get('general', 'startpage').split(','):
self.mainwindow.tabs.tabopen(url) 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): def _recover_pages(self):
"""Try to recover all open pages. """Try to recover all open pages.
@ -173,6 +241,15 @@ class QuteBrowser(QApplication):
pass pass
return pages 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): def _exception_hook(self, exctype, excvalue, tb):
"""Handle uncaught python exceptions. """Handle uncaught python exceptions.
@ -237,97 +314,7 @@ class QuteBrowser(QApplication):
logging.debug("maybe_quit called from {}, quit status {}".format( logging.debug("maybe_quit called from {}, quit status {}".format(
sender, self._quit_status)) sender, self._quit_status))
if all(self._quit_status.values()): if all(self._quit_status.values()):
self.quit() logging.debug("maybe_quit quitting.")
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() self.quit()
@pyqtSlot(tuple) @pyqtSlot(tuple)
@ -345,32 +332,33 @@ class QuteBrowser(QApplication):
(count, argv) = tpl (count, argv) = tpl
cmd = argv[0] cmd = argv[0]
args = argv[1:] args = argv[1:]
browser = self.mainwindow.tabs
handlers = { handlers = {
'open': self.mainwindow.tabs.openurl, 'open': browser.openurl,
'opencur': self.mainwindow.tabs.opencur, 'opencur': browser.opencur,
'tabopen': self.mainwindow.tabs.tabopen, 'tabopen': browser.tabopen,
'tabopencur': self.mainwindow.tabs.tabopencur, 'tabopencur': browser.tabopencur,
'quit': self.shutdown, 'quit': self.shutdown,
'tabclose': self.mainwindow.tabs.cur_close, 'tabclose': browser.cur_close,
'tabprev': self.mainwindow.tabs.switch_prev, 'tabprev': browser.switch_prev,
'tabnext': self.mainwindow.tabs.switch_next, 'tabnext': browser.switch_next,
'reload': self.mainwindow.tabs.cur_reload, 'reload': browser.cur_reload,
'stop': self.mainwindow.tabs.cur_stop, 'stop': browser.cur_stop,
'back': self.mainwindow.tabs.cur_back, 'back': browser.cur_back,
'forward': self.mainwindow.tabs.cur_forward, 'forward': browser.cur_forward,
'print': self.mainwindow.tabs.cur_print, 'print': browser.cur_print,
'scroll': self.mainwindow.tabs.cur_scroll, 'scroll': browser.cur_scroll,
'scroll_page': self.mainwindow.tabs.cur_scroll_page, 'scroll_page': browser.cur_scroll_page,
'scroll_perc_x': self.mainwindow.tabs.cur_scroll_percent_x, 'scroll_perc_x': browser.cur_scroll_percent_x,
'scroll_perc_y': self.mainwindow.tabs.cur_scroll_percent_y, 'scroll_perc_y': browser.cur_scroll_percent_y,
'undo': self.mainwindow.tabs.undo_close, 'undo': browser.undo_close,
'pyeval': self.pyeval, 'pyeval': self.pyeval,
'nextsearch': self.searchparser.nextsearch, 'nextsearch': self.searchparser.nextsearch,
'yank': self.mainwindow.tabs.cur_yank, 'yank': browser.cur_yank,
'yanktitle': self.mainwindow.tabs.cur_yank_title, 'yanktitle': browser.cur_yank_title,
'paste': self.mainwindow.tabs.paste, 'paste': browser.paste,
'tabpaste': self.mainwindow.tabs.tabpaste, 'tabpaste': browser.tabpaste,
'crash': self.crash, 'crash': self.crash,
} }
@ -404,3 +392,49 @@ class QuteBrowser(QApplication):
""" """
raise Exception 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()

View File

@ -32,17 +32,24 @@ startchars = ":/?"
class KeyParser(QObject): 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) set_cmd_text = pyqtSignal(str)
# Signal emitted when the keystring is updated
keystring_updated = pyqtSignal(str) keystring_updated = pyqtSignal(str)
# Keybindings
bindings = {}
modifier_bindings = {}
commandparser = None
MATCH_PARTIAL = 0 MATCH_PARTIAL = 0
MATCH_DEFINITIVE = 1 MATCH_DEFINITIVE = 1
@ -51,35 +58,9 @@ class KeyParser(QObject):
def __init__(self, mainwindow): def __init__(self, mainwindow):
super().__init__(mainwindow) super().__init__(mainwindow)
self.commandparser = CommandParser() self.commandparser = CommandParser()
self._keystring = ''
def from_config_sect(self, sect): self._bindings = {}
"""Load keybindings from a ConfigParser section. self._modifier_bindings = {}
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)
def _handle_modifier_key(self, e): def _handle_modifier_key(self, e):
"""Handle a new keypress with modifiers. """Handle a new keypress with modifiers.
@ -108,7 +89,7 @@ class KeyParser(QObject):
modstr += s + '+' modstr += s + '+'
keystr = QKeySequence(e.key()).toString() keystr = QKeySequence(e.key()).toString()
try: try:
cmdstr = self.modifier_bindings[modstr + keystr] cmdstr = self._modifier_bindings[modstr + keystr]
except KeyError: except KeyError:
logging.debug('No binding found for {}.'.format(modstr + keystr)) logging.debug('No binding found for {}.'.format(modstr + keystr))
return True return True
@ -133,15 +114,15 @@ class KeyParser(QObject):
logging.debug('Ignoring, no text') logging.debug('Ignoring, no text')
return return
self.keystring += txt self._keystring += txt
if any(self.keystring == c for c in startchars): if any(self._keystring == c for c in startchars):
self.set_cmd_text.emit(self.keystring) self.set_cmd_text.emit(self._keystring)
self.keystring = '' self._keystring = ''
return return
(countstr, cmdstr_needle) = re.match(r'^(\d*)(.*)', (countstr, cmdstr_needle) = re.match(r'^(\d*)(.*)',
self.keystring).groups() self._keystring).groups()
if not cmdstr_needle: if not cmdstr_needle:
return return
@ -157,16 +138,16 @@ class KeyParser(QObject):
if match == self.MATCH_DEFINITIVE: if match == self.MATCH_DEFINITIVE:
pass pass
elif match == self.MATCH_PARTIAL: elif match == self.MATCH_PARTIAL:
logging.debug('No match for "{}" (added {})'.format(self.keystring, logging.debug('No match for "{}" (added {})'.format(
txt)) self._keystring, txt))
return return
elif match == self.MATCH_NONE: elif match == self.MATCH_NONE:
logging.debug('Giving up with "{}", no matches'.format( logging.debug('Giving up with "{}", no matches'.format(
self.keystring)) self._keystring))
self.keystring = '' self._keystring = ''
return return
self.keystring = '' self._keystring = ''
count = int(countstr) if countstr else None count = int(countstr) if countstr else None
self._run_or_fill(cmdstr_hay, count=count, ignore_exc=False) self._run_or_fill(cmdstr_hay, count=count, ignore_exc=False)
return return
@ -178,11 +159,11 @@ class KeyParser(QObject):
""" """
try: try:
cmdstr_hay = self.bindings[cmdstr_needle] cmdstr_hay = self._bindings[cmdstr_needle]
return (self.MATCH_DEFINITIVE, cmdstr_hay) return (self.MATCH_DEFINITIVE, cmdstr_hay)
except KeyError: except KeyError:
# No definitive match, check if there's a chance of a partial match # No definitive match, check if there's a chance of a partial match
for hay in self.bindings: for hay in self._bindings:
try: try:
if cmdstr_needle[-1] == hay[len(cmdstr_needle) - 1]: if cmdstr_needle[-1] == hay[len(cmdstr_needle) - 1]:
return (self.MATCH_PARTIAL, None) return (self.MATCH_PARTIAL, None)
@ -229,3 +210,32 @@ class KeyParser(QObject):
cmdstr)) cmdstr))
self.set_cmd_text.emit(':{} '.format(cmdstr)) self.set_cmd_text.emit(':{} '.format(cmdstr))
return 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)

View File

@ -30,6 +30,10 @@ class Command(QObject):
See the module documentation for qutebrowser.commands.commands for details. See the module documentation for qutebrowser.commands.commands for details.
Signals:
signal: Emitted when the command was executed.
arg: A tuple (command, [args])
""" """
# FIXME: # FIXME:

View File

@ -49,12 +49,45 @@ def register_all():
class SearchParser(QObject): 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') 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) @pyqtSlot(str)
def search(self, text): def search(self, text):
"""Search for a text on a website. """Search for a text on a website.
@ -73,40 +106,33 @@ class SearchParser(QObject):
""" """
self._search(text, rev=True) 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): def nextsearch(self, count=1):
"""Continue the search to the ([count]th) next term.""" """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 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): class CommandParser(QObject):
"""Parse qutebrowser commandline commands.""" """Parse qutebrowser commandline commands.
text = '' Attributes:
cmd = '' _cmd: The command which was parsed.
args = [] _args: The arguments which were parsed.
error = pyqtSignal(str) # Emitted if there's an error
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): def _parse(self, text):
"""Split the commandline text into command and arguments. """Split the commandline text into command and arguments.
@ -114,8 +140,7 @@ class CommandParser(QObject):
Raise NoSuchCommandError if a command wasn't found. Raise NoSuchCommandError if a command wasn't found.
""" """
self.text = text parts = text.strip().split(maxsplit=1)
parts = self.text.strip().split(maxsplit=1)
if not parts: if not parts:
raise NoSuchCommandError raise NoSuchCommandError
cmdstr = parts[0] cmdstr = parts[0]
@ -130,19 +155,19 @@ class CommandParser(QObject):
args = shlex.split(parts[1]) args = shlex.split(parts[1])
else: else:
args = [parts[1]] args = [parts[1]]
self.cmd = cmd self._cmd = cmd
self.args = args self._args = args
def _check(self): def _check(self):
"""Check if the argument count for the command is correct.""" """Check if the argument count for the command is correct."""
self.cmd.check(self.args) self._cmd.check(self._args)
def _run(self, count=None): def _run(self, count=None):
"""Run a command with an optional count.""" """Run a command with an optional count."""
if count is not None: if count is not None:
self.cmd.run(self.args, count=count) self._cmd.run(self._args, count=count)
else: else:
self.cmd.run(self.args) self._cmd.run(self._args)
@pyqtSlot(str, int, bool) @pyqtSlot(str, int, bool)
def run(self, text, count=None, ignore_exc=True): def run(self, text, count=None, ignore_exc=True):
@ -155,13 +180,17 @@ class CommandParser(QObject):
arguments. arguments.
""" """
if ';;' in text:
for sub in text.split(';;'):
self.run(sub, count, ignore_exc)
return
try: try:
self._parse(text) self._parse(text)
self._check() self._check()
except ArgumentCountError: except ArgumentCountError:
if ignore_exc: if ignore_exc:
self.error.emit("{}: invalid argument count".format( self.error.emit("{}: invalid argument count".format(
self.cmd.mainname)) self._cmd.mainname))
return False return False
else: else:
raise raise

View File

@ -17,6 +17,8 @@
"""A CompletionModel filled with all commands and descriptions.""" """A CompletionModel filled with all commands and descriptions."""
from collections import OrderedDict
from qutebrowser.commands.utils import cmd_dict from qutebrowser.commands.utils import cmd_dict
from qutebrowser.models.completion import CompletionModel from qutebrowser.models.completion import CompletionModel
@ -35,5 +37,6 @@ class CommandCompletionModel(CompletionModel):
if not obj.hide: if not obj.hide:
doc = obj.__doc__.splitlines()[0].strip().rstrip('.') doc = obj.__doc__.splitlines()[0].strip().rstrip('.')
cmdlist.append([obj.mainname, doc]) cmdlist.append([obj.mainname, doc])
self._data['Commands'] = sorted(cmdlist) data = OrderedDict()
self.init_data() data['Commands'] = sorted(cmdlist)
self.init_data(data)

View File

@ -15,15 +15,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""The base completion model for completion in the command line. """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
from PyQt5.QtCore import Qt, QVariant, QAbstractItemModel, QModelIndex from PyQt5.QtCore import Qt, QVariant, QAbstractItemModel, QModelIndex
@ -34,26 +26,18 @@ class CompletionModel(QAbstractItemModel):
Used for showing completions later in the CompletionView. 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): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._data = OrderedDict() self._id_map = {}
self.parents = [] self._root = CompletionItem([""] * 2)
self.id_map = {} self._id_map[id(self._root)] = self._root
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()
def _node(self, index): def _node(self, index):
"""Return the interal data representation for index. """Return the interal data representation for index.
@ -63,172 +47,9 @@ class CompletionModel(QAbstractItemModel):
""" """
if index.isValid(): if index.isValid():
return self.id_map[index.internalId()] return self._id_map[index.internalId()]
else: else:
return self.root 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)
def _get_marks(self, needle, haystack): def _get_marks(self, needle, haystack):
"""Return the marks for needle in haystack.""" """Return the marks for needle in haystack."""
@ -244,15 +65,196 @@ class CompletionModel(QAbstractItemModel):
marks.append((pos1, pos2)) marks.append((pos1, pos2))
return marks 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(): class CompletionItem():
"""An item (row) in a CompletionModel.""" """An item (row) in a CompletionModel.
parent = None Attributes:
children = None parent: The parent of this item.
_data = None children: The children of this item.
_marks = None _data: The data of this item.
_marks: The marks of this item.
"""
def __init__(self, data, parent=None): def __init__(self, data, parent=None):
"""Constructor for CompletionItem. """Constructor for CompletionItem.
@ -262,8 +264,8 @@ class CompletionItem():
""" """
self.parent = parent self.parent = parent
self._data = data
self.children = [] self.children = []
self._data = data
self._marks = [] self._marks = []
def data(self, column, role=Qt.DisplayRole): def data(self, column, role=Qt.DisplayRole):

View File

@ -27,13 +27,17 @@ from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
class CompletionFilterModel(QSortFilterProxyModel): class CompletionFilterModel(QSortFilterProxyModel):
"""Subclass of QSortFilterProxyModel with custom sorting/filtering.""" """Subclass of QSortFilterProxyModel with custom sorting/filtering.
_pattern = None Attributes:
srcmodel = None _pattern: The pattern to filter with, used in pattern property.
_srcmodel: The source model, accessed via the srcmodel property.
"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._srcmodel = None
self._pattern = '' self._pattern = ''
@property @property
@ -41,16 +45,6 @@ class CompletionFilterModel(QSortFilterProxyModel):
"""Getter for pattern.""" """Getter for pattern."""
return self._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 @pattern.setter
def pattern(self, val): def pattern(self, val):
"""Setter for pattern. """Setter for pattern.
@ -71,6 +65,33 @@ class CompletionFilterModel(QSortFilterProxyModel):
self.sort(sortcol) self.sort(sortcol)
self.invalidate() 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): def filterAcceptsRow(self, row, parent):
"""Custom filter implementation. """Custom filter implementation.
@ -116,13 +137,3 @@ class CompletionFilterModel(QSortFilterProxyModel):
return False return False
else: else:
return left < right 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)

View File

@ -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}

View File

@ -15,131 +15,32 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Configuration storage and config-related utilities. """Configuration storage and config-related utilities."""
config -- The main Config object.
colordict -- All configured colors.
default_config -- The default config as dict.
MONOSPACE -- A list of suitable monospace fonts.
"""
import os import os
import io import io
import os.path import os.path
import logging import logging
from configparser import ConfigParser, ExtendedInterpolation from configparser import (ConfigParser, ExtendedInterpolation, NoSectionError,
NoOptionError)
from qutebrowser.utils.misc import read_file
config = None config = None
state = None
colordict = {} colordict = {}
fontdict = {} fontdict = {}
default_config = """ # Special value for an unset fallback, so None can be passed as fallback.
[general] _UNSET = object()
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}
"""
def init(confdir): def init(confdir):
"""Initialize the global objects based on the config in configdir.""" """Initialize the global objects based on the config in configdir."""
global config, colordict, fontdict global config, state, colordict, fontdict
config = Config(confdir) logging.debug("Config init, confdir {}".format(confdir))
config = Config(confdir, 'qutebrowser.conf', read_file('qutebrowser.conf'))
state = Config(confdir, 'state', always_save=True)
try: try:
colordict = ColorDict(config['colors']) colordict = ColorDict(config['colors'])
except KeyError: except KeyError:
@ -229,33 +130,44 @@ class FontDict(dict):
class Config(ConfigParser): class Config(ConfigParser):
"""Our own ConfigParser subclass.""" """Our own ConfigParser subclass.
configdir = None Attributes:
FNAME = 'config' _configdir: The dictionary to save the config in.
default_cp = None _default_cp: The ConfigParser instance supplying the default values.
config_loaded = False _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. """Config constructor.
configdir -- directory to store the config in. 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()) super().__init__(interpolation=ExtendedInterpolation())
self.default_cp = ConfigParser(interpolation=ExtendedInterpolation()) self._config_loaded = False
self.default_cp.optionxform = lambda opt: opt # be case-insensitive self.always_save = always_save
self.default_cp.read_string(default_config) self._configdir = configdir
if not self.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 return
self.optionxform = lambda opt: opt # be case-insensitive self.optionxform = lambda opt: opt # be case-insensitive
self.configdir = configdir self._configdir = configdir
self.configfile = os.path.join(self.configdir, self.FNAME) self.configfile = os.path.join(self._configdir, fname)
if not os.path.isfile(self.configfile): if not os.path.isfile(self.configfile):
return return
logging.debug("Reading config from {}".format(self.configfile)) logging.debug("Reading config from {}".format(self.configfile))
self.read(self.configfile) self.read(self.configfile)
self.config_loaded = True self._config_loaded = True
def __getitem__(self, key): def __getitem__(self, key):
"""Get an item from the configparser or default dict. """Get an item from the configparser or default dict.
@ -266,25 +178,42 @@ class Config(ConfigParser):
try: try:
return super().__getitem__(key) return super().__getitem__(key)
except KeyError: 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. """Get an item from the configparser or default dict.
Extend ConfigParser's get(). 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: # pylint: disable=redefined-builtin
del kwargs['fallback'] try:
fallback = self.default_cp.get(*args, **kwargs) return super().get(*args, raw=raw, vars=vars)
return super().get(*args, fallback=fallback, **kwargs) 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): def save(self):
"""Save the config file.""" """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 return
if not os.path.exists(self.configdir): if not os.path.exists(self._configdir):
os.makedirs(self.configdir, 0o755) os.makedirs(self._configdir, 0o755)
logging.debug("Saving config to {}".format(self.configfile)) logging.debug("Saving config to {}".format(self.configfile))
with open(self.configfile, 'w') as f: with open(self.configfile, 'w') as f:
self.write(f) self.write(f)

View File

@ -17,6 +17,7 @@
"""Other utilities which don't fit anywhere else.""" """Other utilities which don't fit anywhere else."""
import sys
import os.path import os.path
from PyQt5.QtCore import pyqtRemoveInputHook from PyQt5.QtCore import pyqtRemoveInputHook
@ -50,3 +51,16 @@ def read_file(filename):
fn = os.path.join(qutebrowser.basedir, filename) fn = os.path.join(qutebrowser.basedir, filename)
with open(fn, 'r', encoding='UTF-8') as f: with open(fn, 'r', encoding='UTF-8') as f:
return f.read() 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)

View File

@ -42,10 +42,13 @@ def dbg_signal(sig, args):
class SignalCache(QObject): 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 Attributes:
signal_dict = None _uncached: A list of signals which should not be cached.
_signal_dict: The internal mapping of signals we got.
"""
def __init__(self, uncached=None): def __init__(self, uncached=None):
"""Create a new SignalCache. """Create a new SignalCache.
@ -56,10 +59,14 @@ class SignalCache(QObject):
""" """
super().__init__() super().__init__()
if uncached is None: if uncached is None:
self.uncached = [] self._uncached = []
else: else:
self.uncached = uncached self._uncached = uncached
self.signal_dict = OrderedDict() 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): def add(self, sig, args):
"""Add a new signal to the signal cache. """Add a new signal to the signal cache.
@ -71,21 +78,17 @@ class SignalCache(QObject):
""" """
if not self._signal_needs_caching(sig): if not self._signal_needs_caching(sig):
return return
had_signal = sig.signal in self.signal_dict had_signal = sig.signal in self._signal_dict
self.signal_dict[sig.signal] = (sig, args) self._signal_dict[sig.signal] = (sig, args)
if had_signal: if had_signal:
self.signal_dict.move_to_end(sig.signal) self._signal_dict.move_to_end(sig.signal)
def clear(self): def clear(self):
"""Clear/purge the signal cache.""" """Clear/purge the signal cache."""
self.signal_dict.clear() self._signal_dict.clear()
def replay(self): def replay(self):
"""Replay all cached signals.""" """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))) logging.debug('emitting {}'.format(dbg_signal(signal, args)))
signal.emit(*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

View File

@ -38,6 +38,9 @@ class Style(QCommonStyle):
http://stackoverflow.com/a/17294081 http://stackoverflow.com/a/17294081
https://code.google.com/p/makehuman/source/browse/trunk/makehuman/lib/qtgui.py # noqa # pylint: disable=line-too-long 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): def __init__(self, style):

View File

@ -27,6 +27,52 @@ from PyQt5.QtCore import QUrl
import qutebrowser.utils.config as config 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): def qurl(url):
"""Get a QUrl from an url string.""" """Get a QUrl from an url string."""
return url if isinstance(url, QUrl) else QUrl(url) return url if isinstance(url, QUrl) else QUrl(url)
@ -49,7 +95,7 @@ def fuzzy_url(url):
""" """
u = qurl(url) u = qurl(url)
urlstr = urlstring(url) urlstr = urlstring(url)
if (not config.config.getboolean('general', 'auto_search')) or is_url(u): if is_url(u):
# probably an address # probably an address
logging.debug("url is a fuzzy address") logging.debug("url is a fuzzy address")
newurl = QUrl.fromUserInput(urlstr) newurl = QUrl.fromUserInput(urlstr)
@ -64,27 +110,6 @@ def fuzzy_url(url):
return newurl 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): def is_about_url(url):
"""Return True if url (QUrl) is an about:... or other special URL.""" """Return True if url (QUrl) is an about:... or other special URL."""
return urlstring(url).replace('http://', '').startswith('about:') return urlstring(url).replace('http://', '').startswith('about:')
@ -93,38 +118,37 @@ def is_about_url(url):
def is_url(url): def is_url(url):
"""Return True if url (QUrl) seems to be a valid URL.""" """Return True if url (QUrl) seems to be a valid URL."""
urlstr = urlstring(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: if ' ' in urlstr:
# An URL will never contain a space # An URL will never contain a space
logging.debug('Contains space -> no url') logging.debug('Contains space -> no url')
return False 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') logging.debug('Checking via DNS')
return _is_url_dns(QUrl.fromUserInput(urlstr)) return _is_url_dns(QUrl.fromUserInput(urlstr))
else: elif autosearch == 'naive':
logging.debug('Checking via naive check') logging.debug('Checking via naive check')
return _is_url_naive(url) 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: else:
return True raise ValueError("Invalid autosearch value")

View File

@ -28,6 +28,29 @@ from PyQt5.QtWebKit import qWebKitVersion
import qutebrowser 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(): def version():
"""Return a string with various version informations.""" """Return a string with various version informations."""
if sys.platform == 'linux': if sys.platform == 'linux':
@ -56,26 +79,3 @@ def version():
lines.append('\nGit commit: {}'.format(gitver)) lines.append('\nGit commit: {}'.format(gitver))
return ''.join(lines) 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

View File

@ -56,23 +56,40 @@ class TabbedBrowser(TabWidget):
- the signal gets filtered with _filter_signals and self.cur_* gets - the signal gets filtered with _filter_signals and self.cur_* gets
emitted if the signal occured in the current tab. 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_progress = pyqtSignal(int)
cur_load_started = pyqtSignal() # Current tab started loading cur_load_started = pyqtSignal()
cur_load_finished = pyqtSignal(bool) # Current tab finished loading cur_load_finished = pyqtSignal(bool)
cur_statusbar_message = pyqtSignal(str) # Status bar message cur_statusbar_message = pyqtSignal(str)
cur_url_changed = pyqtSignal('QUrl') # Current URL changed cur_url_changed = pyqtSignal('QUrl')
cur_link_hovered = pyqtSignal(str, str, str) # Link hovered in cur tab cur_link_hovered = pyqtSignal(str, str, str)
# Current tab changed scroll position
cur_scroll_perc_changed = pyqtSignal(int, int) 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') keypress = pyqtSignal('QKeyEvent')
shutdown_complete = pyqtSignal() # All tabs have been shut down. shutdown_complete = pyqtSignal()
quit = pyqtSignal() # Last tab closed, quit application. quit = pyqtSignal()
_url_stack = [] # Stack of URLs of closed tabs
_space = None # Space QShortcut
_tabs = None
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -171,9 +188,10 @@ class TabbedBrowser(TabWidget):
try: try:
self._tabs.remove(tab) self._tabs.remove(tab)
except ValueError: except ValueError:
logging.error("tab {} could not be removed from tabs {}.".format( logging.exception("tab {} could not be removed".format(tab))
tab, self._tabs)) logging.debug("Tabs after removing: {}".format(self._tabs))
if not self._tabs: # all tabs shut down if not self._tabs: # all tabs shut down
logging.debug("Tab shutdown complete.")
self.shutdown_complete.emit() self.shutdown_complete.emit()
def cur_reload(self, count=None): def cur_reload(self, count=None):
@ -205,7 +223,7 @@ class TabbedBrowser(TabWidget):
# FIXME that does not what I expect # FIXME that does not what I expect
tab = self._widget(count) tab = self._widget(count)
if tab is not None: if tab is not None:
preview = QPrintPreviewDialog() preview = QPrintPreviewDialog(self)
preview.paintRequested.connect(tab.print) preview.paintRequested.connect(tab.print)
preview.exec_() preview.exec_()
@ -451,12 +469,12 @@ class TabbedBrowser(TabWidget):
except TypeError: except TypeError:
pass pass
tabcount = self.count() tabcount = self.count()
logging.debug("Shutting down {} tabs...".format(tabcount))
if tabcount == 0: if tabcount == 0:
logging.debug("No tabs -> shutdown complete")
self.shutdown_complete.emit() self.shutdown_complete.emit()
return return
for tabidx in range(tabcount): for tabidx in range(tabcount):
logging.debug("shutdown {}".format(tabidx)) logging.debug("Shutting down tab {}/{}".format(tabidx, tabcount))
tab = self.widget(tabidx) tab = self.widget(tabidx)
tab.shutdown(callback=functools.partial(self._cb_tab_shutdown, tab.shutdown(callback=functools.partial(self._cb_tab_shutdown,
tab)) tab))
@ -468,30 +486,39 @@ class BrowserTab(QWebView):
Our own subclass of a QWebView with some added bells and whistles. 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) scroll_pos_changed = pyqtSignal(int, int)
open_tab = pyqtSignal('QUrl') open_tab = pyqtSignal('QUrl')
linkHovered = pyqtSignal(str, str, str) 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): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._scroll_pos = (-1, -1)
self._shutdown_callback = None
self._open_new_tab = False
self._destroyed = {} self._destroyed = {}
self.page_ = BrowserPage(self) self.page_ = BrowserPage(self)
self.setPage(self.page_) self.setPage(self.page_)
self.signal_cache = SignalCache(uncached=['linkHovered']) self.signal_cache = SignalCache(uncached=['linkHovered'])
self.loadProgress.connect(self.on_load_progress)
self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page_.linkHovered.connect(self.linkHovered) self.page_.linkHovered.connect(self.linkHovered)
self.installEventFilter(self)
self.linkClicked.connect(self.on_link_clicked) self.linkClicked.connect(self.on_link_clicked)
# FIXME find some way to hide scrollbars without setScrollBarPolicy # FIXME find some way to hide scrollbars without setScrollBarPolicy
@ -531,17 +558,6 @@ class BrowserTab(QWebView):
else: else:
self.openurl(url) 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): def shutdown(self, callback=None):
"""Shut down the tab cleanly and remove it. """Shut down the tab cleanly and remove it.
@ -561,40 +577,43 @@ class BrowserTab(QWebView):
self.settings().setAttribute(QWebSettings.JavascriptEnabled, False) self.settings().setAttribute(QWebSettings.JavascriptEnabled, False)
self._destroyed[self.page_] = 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_))
self.page_.deleteLater() self.page_.deleteLater()
self._destroyed[self] = False self._destroyed[self] = False
self.destroyed.connect(functools.partial(self.on_destroyed, self)) self.destroyed.connect(functools.partial(self._on_destroyed, self))
self.deleteLater() self.deleteLater()
netman = self.page_.network_access_manager netman = self.page_.network_access_manager
self._destroyed[netman] = False self._destroyed[netman] = False
netman.abort_requests() netman.abort_requests()
netman.destroyed.connect(functools.partial(self.on_destroyed, netman)) netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
netman.deleteLater() 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.""" """Called when a subsystem has been destroyed during shutdown."""
self._destroyed[sender] = True 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 all(self._destroyed.values()):
if self._shutdown_callback is not None: if self._shutdown_callback is not None:
logging.debug("Everything destroyed, calling callback")
self._shutdown_callback() self._shutdown_callback()
def eventFilter(self, watched, e): def paintEvent(self, e):
"""Dirty hack to emit a signal if the scroll position changed. """Extend paintEvent to emit a signal if the scroll position changed.
We listen to repaint requests here, in the hope a repaint will always This is a bit of a hack: We listen to repaint requests here, in the
be requested when scrolling, and if the scroll position actually hope a repaint will always be requested when scrolling, and if the
changed, we emit a signal. scroll position actually changed, we emit a signal.
watched -- The watched Qt object. e -- The QPaintEvent.
e -- The new event.
""" """
if watched == self and e.type() == QEvent.Paint:
frame = self.page_.mainFrame() frame = self.page_.mainFrame()
new_pos = (frame.scrollBarValue(Qt.Horizontal), new_pos = (frame.scrollBarValue(Qt.Horizontal),
frame.scrollBarValue(Qt.Vertical)) frame.scrollBarValue(Qt.Vertical))
@ -607,9 +626,8 @@ class BrowserTab(QWebView):
perc = (round(100 * new_pos[0] / m[0]) if m[0] != 0 else 0, 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) round(100 * new_pos[1] / m[1]) if m[1] != 0 else 0)
self.scroll_pos_changed.emit(*perc) self.scroll_pos_changed.emit(*perc)
# we're not actually filtering something, let superclass handle the # Let superclass handle the event
# event return super().paintEvent(e)
return super().eventFilter(watched, e)
def event(self, e): def event(self, e):
"""Check if a link was clicked with the middle button or Ctrl. """Check if a link was clicked with the middle button or Ctrl.
@ -630,10 +648,13 @@ class BrowserTab(QWebView):
class BrowserPage(QWebPage): class BrowserPage(QWebPage):
"""Our own QWebPage with advanced features.""" """Our own QWebPage with advanced features.
_extension_handlers = None Attributes:
network_access_manager = None _extension_handlers: Mapping of QWebPage extensions to their handlers.
network_access_manager: The QNetworkAccessManager used.
"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -677,9 +698,12 @@ class BrowserPage(QWebPage):
class NetworkManager(QNetworkAccessManager): class NetworkManager(QNetworkAccessManager):
"""Our own QNetworkAccessManager.""" """Our own QNetworkAccessManager.
_requests = None Attributes:
_requests: Pending requests.
"""
def __init__(self, parent=None): def __init__(self, parent=None):
self._requests = {} self._requests = {}

View File

@ -45,9 +45,22 @@ class CompletionView(QTreeView):
Highlights completions based on marks in the UserRole. 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 {{ QTreeView {{
{font[completion]} {font[completion]}
{color[completion.fg]} {color[completion.fg]}
@ -77,25 +90,24 @@ class CompletionView(QTreeView):
# like one anymore # like one anymore
# FIXME somehow only the first column is yellow, even with # FIXME somehow only the first column is yellow, even with
# setAllColumnsShowFocus # setAllColumnsShowFocus
completion_models = {}
append_cmd_text = pyqtSignal(str) append_cmd_text = pyqtSignal(str)
ignore_next = False
enabled = True
completing = False
height = QPoint(0, 200)
_delegate = None
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.enabled = config.config.getboolean('general', 'show_completion') self._height = QPoint(0, 200) # FIXME make that configurable
self.completion_models[''] = None self._enabled = config.config.getboolean('general', 'show_completion')
self.completion_models['command'] = CommandCompletionModel() self._completion_models = {}
self._completion_models[''] = None
self._completion_models['command'] = CommandCompletionModel()
self._ignore_next = False
self._completing = False
self.model = CompletionFilterModel() self.model = CompletionFilterModel()
self.setModel(self.model) self.setModel(self.model)
self.setmodel('command') self.setmodel('command')
self._delegate = CompletionItemDelegate(self) self._delegate = _CompletionItemDelegate(self)
self.setItemDelegate(self._delegate) 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.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.setIndentation(0) self.setIndentation(0)
@ -117,7 +129,7 @@ class CompletionView(QTreeView):
model -- A QAbstractItemModel with available completions. model -- A QAbstractItemModel with available completions.
""" """
self.model.setsrc(self.completion_models[model]) self.model.srcmodel = self._completion_models[model]
self.expandAll() self.expandAll()
self.resizeColumnToContents(0) self.resizeColumnToContents(0)
@ -131,7 +143,7 @@ class CompletionView(QTreeView):
""" """
bottomleft = geom.topLeft() bottomleft = geom.topLeft()
bottomright = geom.topRight() bottomright = geom.topRight()
topleft = bottomleft - self.height topleft = bottomleft - self._height
assert topleft.x() < bottomright.x() assert topleft.x() < bottomright.x()
assert topleft.y() < bottomright.y() assert topleft.y() < bottomright.y()
self.setGeometry(QRect(topleft, bottomright)) self.setGeometry(QRect(topleft, bottomright))
@ -144,7 +156,7 @@ class CompletionView(QTreeView):
pos -- A QPoint containing the statusbar position. pos -- A QPoint containing the statusbar position.
""" """
self.move(pos - self.height) self.move(pos - self._height)
@pyqtSlot(str) @pyqtSlot(str)
def on_cmd_text_changed(self, text): def on_cmd_text_changed(self, text):
@ -154,22 +166,22 @@ class CompletionView(QTreeView):
text -- The new text 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. # Text changed by a completion, so we don't have to complete again.
self.ignore_next = False self._ignore_next = False
return return
# FIXME more sophisticated completions # FIXME more sophisticated completions
if ' ' in text or not text.startswith(':'): if ' ' in text or not text.startswith(':'):
self.hide() self.hide()
self.completing = False self._completing = False
return return
self.completing = True self._completing = True
self.setmodel('command') self.setmodel('command')
text = text.lstrip(':') text = text.lstrip(':')
self.model.pattern = text self.model.pattern = text
self.model.srcmodel.mark_all_items(text) self.model.srcmodel.mark_all_items(text)
if self.enabled: if self._enabled:
self.show() self.show()
@pyqtSlot(bool) @pyqtSlot(bool)
@ -182,7 +194,7 @@ class CompletionView(QTreeView):
shift -- Whether shift is pressed or not. shift -- Whether shift is pressed or not.
""" """
if not self.completing: if not self._completing:
# No completion running at the moment, ignore keypress # No completion running at the moment, ignore keypress
return return
idx = self._next_idx(shift) idx = self._next_idx(shift)
@ -190,7 +202,7 @@ class CompletionView(QTreeView):
idx, QItemSelectionModel.ClearAndSelect) idx, QItemSelectionModel.ClearAndSelect)
data = self.model.data(idx) data = self.model.data(idx)
if data is not None: if data is not None:
self.ignore_next = True self._ignore_next = True
self.append_cmd_text.emit(self.model.data(idx) + ' ') self.append_cmd_text.emit(self.model.data(idx) + ' ')
def _next_idx(self, upwards): def _next_idx(self, upwards):
@ -217,7 +229,7 @@ class CompletionView(QTreeView):
return idx return idx
class CompletionItemDelegate(QStyledItemDelegate): class _CompletionItemDelegate(QStyledItemDelegate):
"""Delegate used by CompletionView to draw individual items. """Delegate used by CompletionView to draw individual items.
@ -227,12 +239,20 @@ class CompletionItemDelegate(QStyledItemDelegate):
Original implementation: Original implementation:
qt/src/gui/styles/qcommonstyle.cpp:drawControl:2153 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 def __init__(self, parent=None):
style = None self._painter = None
painter = None self._opt = None
doc = None self._doc = None
self._style = None
super().__init__(parent)
def sizeHint(self, option, index): def sizeHint(self, option, index):
"""Override sizeHint of QStyledItemDelegate. """Override sizeHint of QStyledItemDelegate.
@ -244,49 +264,48 @@ class CompletionItemDelegate(QStyledItemDelegate):
value = index.data(Qt.SizeHintRole) value = index.data(Qt.SizeHintRole)
if value is not None: if value is not None:
return value return value
self.opt = QStyleOptionViewItem(option) self._opt = QStyleOptionViewItem(option)
self.initStyleOption(self.opt, index) self.initStyleOption(self._opt, index)
self.style = self.opt.widget.style() self._style = self._opt.widget.style()
self._get_textdoc(index) self._get_textdoc(index)
docsize = self.doc.size().toSize() docsize = self._doc.size().toSize()
size = self.style.sizeFromContents(QStyle.CT_ItemViewItem, self.opt, size = self._style.sizeFromContents(QStyle.CT_ItemViewItem, self._opt,
docsize, self.opt.widget) docsize, self._opt.widget)
return size + QSize(10, 1) return size + QSize(10, 1)
def paint(self, painter, option, index): def paint(self, painter, option, index):
"""Override the QStyledItemDelegate paint function.""" """Override the QStyledItemDelegate paint function."""
painter.save() self._painter = painter
self._painter.save()
self.painter = painter self._opt = QStyleOptionViewItem(option)
self.opt = QStyleOptionViewItem(option) self.initStyleOption(self._opt, index)
self.initStyleOption(self.opt, index) self._style = self._opt.widget.style()
self.style = self.opt.widget.style()
self._draw_background() self._draw_background()
self._draw_icon() self._draw_icon()
self._draw_text(index) self._draw_text(index)
self._draw_focus_rect() self._draw_focus_rect()
painter.restore() self._painter.restore()
def _draw_background(self): def _draw_background(self):
"""Draw the background of an ItemViewItem.""" """Draw the background of an ItemViewItem."""
self.style.drawPrimitive(self.style.PE_PanelItemViewItem, self.opt, self._style.drawPrimitive(self._style.PE_PanelItemViewItem, self._opt,
self.painter, self.opt.widget) self._painter, self._opt.widget)
def _draw_icon(self): def _draw_icon(self):
"""Draw the icon of an ItemViewItem.""" """Draw the icon of an ItemViewItem."""
icon_rect = self.style.subElementRect( icon_rect = self._style.subElementRect(
self.style.SE_ItemViewItemDecoration, self.opt, self.opt.widget) self._style.SE_ItemViewItemDecoration, self._opt, self._opt.widget)
mode = QIcon.Normal mode = QIcon.Normal
if not self.opt.state & QStyle.State_Enabled: if not self._opt.state & QStyle.State_Enabled:
mode = QIcon.Disabled mode = QIcon.Disabled
elif self.opt.state & QStyle.State_Selected: elif self._opt.state & QStyle.State_Selected:
mode = QIcon.Selected mode = QIcon.Selected
state = QIcon.On if self.opt.state & QStyle.State_Open else QIcon.Off state = QIcon.On if self._opt.state & QStyle.State_Open else QIcon.Off
self.opt.icon.paint(self.painter, icon_rect, self._opt.icon.paint(self._painter, icon_rect,
self.opt.decorationAlignment, mode, state) self._opt.decorationAlignment, mode, state)
def _draw_text(self, index): def _draw_text(self, index):
"""Draw the text of an ItemViewItem. """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. 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 return
text_rect_ = self.style.subElementRect(self.style.SE_ItemViewItemText, text_rect_ = self._style.subElementRect(
self.opt, self.opt.widget) self._style.SE_ItemViewItemText, self._opt, self._opt.widget)
margin = self.style.pixelMetric(QStyle.PM_FocusFrameHMargin, self.opt, margin = self._style.pixelMetric(QStyle.PM_FocusFrameHMargin,
self.opt.widget) + 1 self._opt, self._opt.widget) + 1
# remove width padding # remove width padding
text_rect = text_rect_.adjusted(margin, 0, -margin, 0) text_rect = text_rect_.adjusted(margin, 0, -margin, 0)
# move text upwards a bit # move text upwards a bit
@ -311,8 +330,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
text_rect.adjust(0, -1, 0, -1) text_rect.adjust(0, -1, 0, -1)
else: else:
text_rect.adjust(0, -2, 0, -2) text_rect.adjust(0, -2, 0, -2)
self.painter.save() self._painter.save()
state = self.opt.state state = self._opt.state
if state & QStyle.State_Enabled and state & QStyle.State_Active: if state & QStyle.State_Enabled and state & QStyle.State_Active:
cg = QPalette.Normal cg = QPalette.Normal
elif state & QStyle.State_Enabled: elif state & QStyle.State_Enabled:
@ -321,22 +340,22 @@ class CompletionItemDelegate(QStyledItemDelegate):
cg = QPalette.Disabled cg = QPalette.Disabled
if state & QStyle.State_Selected: if state & QStyle.State_Selected:
self.painter.setPen(self.opt.palette.color( self._painter.setPen(self._opt.palette.color(
cg, QPalette.HighlightedText)) cg, QPalette.HighlightedText))
# FIXME this is a dirty fix for the text jumping by one pixel... # FIXME this is a dirty fix for the text jumping by one pixel...
# we really should do this properly somehow # we really should do this properly somehow
text_rect.adjust(0, -1, 0, 0) text_rect.adjust(0, -1, 0, 0)
else: 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: if state & QStyle.State_Editing:
self.painter.setPen(self.opt.palette.color(cg, QPalette.Text)) self._painter.setPen(self._opt.palette.color(cg, QPalette.Text))
self.painter.drawRect(text_rect_.adjusted(0, 0, -1, -1)) 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._get_textdoc(index)
self._draw_textdoc(text_rect) self._draw_textdoc(text_rect)
self.painter.restore() self._painter.restore()
def _draw_textdoc(self, text_rect): def _draw_textdoc(self, text_rect):
"""Draw the QTextDocument of an item. """Draw the QTextDocument of an item.
@ -345,7 +364,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
""" """
clip = QRectF(0, 0, text_rect.width(), text_rect.height()) 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): def _get_textdoc(self, index):
"""Create the QTextDocument of an item. """Create the QTextDocument of an item.
@ -356,32 +375,32 @@ class CompletionItemDelegate(QStyledItemDelegate):
# FIXME we probably should do eliding here. See # FIXME we probably should do eliding here. See
# qcommonstyle.cpp:viewItemDrawText # qcommonstyle.cpp:viewItemDrawText
text_option = QTextOption() text_option = QTextOption()
if self.opt.features & QStyleOptionViewItem.WrapText: if self._opt.features & QStyleOptionViewItem.WrapText:
text_option.setWrapMode(QTextOption.WordWrap) text_option.setWrapMode(QTextOption.WordWrap)
else: else:
text_option.setWrapMode(QTextOption.ManualWrap) text_option.setWrapMode(QTextOption.ManualWrap)
text_option.setTextDirection(self.opt.direction) text_option.setTextDirection(self._opt.direction)
text_option.setAlignment(QStyle.visualAlignment( 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(): if index.parent().isValid():
self.doc.setPlainText(self.opt.text) self._doc.setPlainText(self._opt.text)
else: else:
self.doc.setHtml('<b>{}</b>'.format(html.escape(self.opt.text))) self._doc.setHtml('<b>{}</b>'.format(html.escape(self._opt.text)))
self.doc.setDefaultFont(self.opt.font) self._doc.setDefaultFont(self._opt.font)
self.doc.setDefaultTextOption(text_option) self._doc.setDefaultTextOption(text_option)
self.doc.setDefaultStyleSheet(config.get_stylesheet(""" self._doc.setDefaultStyleSheet(config.get_stylesheet("""
.highlight {{ .highlight {{
{color[completion.match.fg]} {color[completion.match.fg]}
}} }}
""")) """))
self.doc.setDocumentMargin(2) self._doc.setDocumentMargin(2)
if index.column() == 0: if index.column() == 0:
marks = index.data(Qt.UserRole) marks = index.data(Qt.UserRole)
for mark in marks: for mark in marks:
cur = QTextCursor(self.doc) cur = QTextCursor(self._doc)
cur.setPosition(mark[0]) cur.setPosition(mark[0])
cur.setPosition(mark[1], QTextCursor.KeepAnchor) cur.setPosition(mark[1], QTextCursor.KeepAnchor)
txt = cur.selectedText() txt = cur.selectedText()
@ -391,12 +410,12 @@ class CompletionItemDelegate(QStyledItemDelegate):
def _draw_focus_rect(self): def _draw_focus_rect(self):
"""Draw the focus rectangle of an ItemViewItem.""" """Draw the focus rectangle of an ItemViewItem."""
state = self.opt.state state = self._opt.state
if not state & QStyle.State_HasFocus: if not state & QStyle.State_HasFocus:
return return
o = self.opt o = self._opt
o.rect = self.style.subElementRect(self.style.SE_ItemViewItemFocusRect, o.rect = self._style.subElementRect(
self.opt, self.opt.widget) self._style.SE_ItemViewItemFocusRect, self._opt, self._opt.widget)
o.state |= QStyle.State_KeyboardFocusChange | QStyle.State_Item o.state |= QStyle.State_KeyboardFocusChange | QStyle.State_Item
if state & QStyle.State_Enabled: if state & QStyle.State_Enabled:
cg = QPalette.Normal cg = QPalette.Normal
@ -406,6 +425,6 @@ class CompletionItemDelegate(QStyledItemDelegate):
role = QPalette.Highlight role = QPalette.Highlight
else: else:
role = QPalette.Window role = QPalette.Window
o.backgroundColor = self.opt.palette.color(cg, role) o.backgroundColor = self._opt.palette.color(cg, role)
self.style.drawPrimitive(QStyle.PE_FrameFocusRect, o, self.painter, self._style.drawPrimitive(QStyle.PE_FrameFocusRect, o, self._painter,
self.opt.widget) self._opt.widget)

View File

@ -29,14 +29,18 @@ from qutebrowser.utils.version import version
class CrashDialog(QDialog): class CrashDialog(QDialog):
"""Dialog which gets shown after there was a crash.""" """Dialog which gets shown after there was a crash.
vbox = None Attributes:
lbl = None These are just here to have a static reference to avoid GCing.
txt = None _vbox: The main QVBoxLayout
hbox = None _lbl: The QLabel with the static text
btn_quit = None _txt: The QTextEdit with the crash information
btn_restore = None _hbox: The QHboxLayout containing the buttons
_btn_quit: The quit button
_btn_restore: the restore button
"""
def __init__(self, pages, cmdhist, exc): def __init__(self, pages, cmdhist, exc):
super().__init__() super().__init__()
@ -44,8 +48,8 @@ class CrashDialog(QDialog):
self.setWindowTitle('Whoops!') self.setWindowTitle('Whoops!')
self.setModal(True) self.setModal(True)
self.vbox = QVBoxLayout() self._vbox = QVBoxLayout(self)
self.lbl = QLabel(self) self._lbl = QLabel()
text = ('Argh! qutebrowser crashed unexpectedly.<br/>' text = ('Argh! qutebrowser crashed unexpectedly.<br/>'
'Please review the info below to remove sensitive data and ' 'Please review the info below to remove sensitive data and '
'then submit it to <a href="mailto:crash@qutebrowser.org">' 'then submit it to <a href="mailto:crash@qutebrowser.org">'
@ -53,29 +57,29 @@ class CrashDialog(QDialog):
if pages: if pages:
text += ('You can click "Restore tabs" to attempt to reopen your ' text += ('You can click "Restore tabs" to attempt to reopen your '
'open tabs.') 'open tabs.')
self.lbl.setText(text) self._lbl.setText(text)
self.lbl.setWordWrap(True) self._lbl.setWordWrap(True)
self.vbox.addWidget(self.lbl) self._vbox.addWidget(self._lbl)
self.txt = QTextEdit(self) self._txt = QTextEdit()
self.txt.setReadOnly(True) self._txt.setReadOnly(True)
self.txt.setText(self._crash_info(pages, cmdhist, exc)) self._txt.setText(self._crash_info(pages, cmdhist, exc))
self.vbox.addWidget(self.txt) self._vbox.addWidget(self._txt)
self.setLayout(self.vbox)
self.hbox = QHBoxLayout() self._hbox = QHBoxLayout()
self.btn_quit = QPushButton(self) self._hbox.addStretch()
self.btn_quit.setText('Quit') self._btn_quit = QPushButton()
self.btn_quit.clicked.connect(self.reject) self._btn_quit.setText('Quit')
self.hbox.addWidget(self.btn_quit) self._btn_quit.clicked.connect(self.reject)
self._hbox.addWidget(self._btn_quit)
if pages: if pages:
self.btn_restore = QPushButton(self) self._btn_restore = QPushButton()
self.btn_restore.setText('Restore tabs') self._btn_restore.setText('Restore tabs')
self.btn_restore.clicked.connect(self.accept) self._btn_restore.clicked.connect(self.accept)
self.btn_restore.setDefault(True) self._btn_restore.setDefault(True)
self.hbox.addWidget(self.btn_restore) self._hbox.addWidget(self._btn_restore)
self.vbox.addLayout(self.hbox) self._vbox.addLayout(self._hbox)
def _crash_info(self, pages, cmdhist, exc): def _crash_info(self, pages, cmdhist, exc):
"""Gather crash information to display.""" """Gather crash information to display."""

View File

@ -17,11 +17,16 @@
"""The main window of QuteBrowser.""" """The main window of QuteBrowser."""
import binascii
from base64 import b64decode
from PyQt5.QtCore import QRect
from PyQt5.QtWidgets import QWidget, QVBoxLayout from PyQt5.QtWidgets import QWidget, QVBoxLayout
from qutebrowser.widgets.statusbar import StatusBar from qutebrowser.widgets.statusbar import StatusBar
from qutebrowser.widgets.browser import TabbedBrowser from qutebrowser.widgets.browser import TabbedBrowser
from qutebrowser.widgets.completion import CompletionView from qutebrowser.widgets.completion import CompletionView
import qutebrowser.utils.config as config
class MainWindow(QWidget): class MainWindow(QWidget):
@ -31,30 +36,41 @@ class MainWindow(QWidget):
Adds all needed components to a vbox, initializes subwidgets and connects Adds all needed components to a vbox, initializes subwidgets and connects
signals. signals.
""" Attributes:
tabs: The TabbedBrowser widget.
status: The StatusBar widget.
_vbox: The main QVBoxLayout.
vbox = None """
tabs = None
status = None
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle('qutebrowser') self.setWindowTitle('qutebrowser')
# FIXME maybe store window position/size on exit try:
self.resize(800, 600) 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 = QVBoxLayout(self)
self.vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setContentsMargins(0, 0, 0, 0)
self.vbox.setSpacing(0) self._vbox.setSpacing(0)
self.tabs = TabbedBrowser() self.tabs = TabbedBrowser()
self.vbox.addWidget(self.tabs) self._vbox.addWidget(self.tabs)
self.completion = CompletionView(self) self.completion = CompletionView(self)
self.status = StatusBar() self.status = StatusBar()
self.vbox.addWidget(self.status) self._vbox.addWidget(self.status)
self.status.resized.connect(self.completion.resize_to_bar) self.status.resized.connect(self.completion.resize_to_bar)
self.status.moved.connect(self.completion.move_to_bar) self.status.moved.connect(self.completion.move_to_bar)
@ -80,3 +96,7 @@ class MainWindow(QWidget):
#self.retranslateUi(MainWindow) #self.retranslateUi(MainWindow)
#self.tabWidget.setCurrentIndex(0) #self.tabWidget.setCurrentIndex(0)
#QtCore.QMetaObject.connectSlotsByName(MainWindow) #QtCore.QMetaObject.connectSlotsByName(MainWindow)
def _set_default_geometry(self):
"""Set some sensible default geometry."""
self.setGeometry(QRect(50, 50, 800, 600))

View File

@ -21,8 +21,7 @@ import logging
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt
from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel, from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel,
QHBoxLayout, QSizePolicy, QShortcut, QStyle, QHBoxLayout, QSizePolicy, QShortcut)
QStyleOption)
from PyQt5.QtGui import QPainter, QKeySequence, QValidator from PyQt5.QtGui import QPainter, QKeySequence, QValidator
import qutebrowser.utils.config as config import qutebrowser.utils.config as config
@ -32,20 +31,33 @@ from qutebrowser.utils.url import urlstring
class StatusBar(QWidget): 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') resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint') moved = pyqtSignal('QPoint')
_error = False _STYLESHEET = """
_option = None
_stylesheet = """
QWidget#StatusBar[error="false"] {{ QWidget#StatusBar[error="false"] {{
{color[statusbar.bg]} {color[statusbar.bg]}
}} }}
@ -60,36 +72,39 @@ class StatusBar(QWidget):
}} }}
""" """
# TODO: the statusbar should be a bit smaller
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setObjectName(self.__class__.__name__) 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.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
self.hbox = QHBoxLayout(self) self._error = False
self.hbox.setContentsMargins(0, 0, 0, 0) self._option = None
self.hbox.setSpacing(5)
self.cmd = Command(self) self._hbox = QHBoxLayout(self)
self.hbox.addWidget(self.cmd) self._hbox.setContentsMargins(0, 0, 0, 0)
self._hbox.setSpacing(5)
self.txt = Text(self) self.cmd = _Command(self)
self.hbox.addWidget(self.txt) self._hbox.addWidget(self.cmd)
self.hbox.addStretch()
self.keystring = KeyString(self) self.txt = _Text(self)
self.hbox.addWidget(self.keystring) self._hbox.addWidget(self.txt)
self._hbox.addStretch()
self.url = Url(self) self.keystring = _KeyString(self)
self.hbox.addWidget(self.url) self._hbox.addWidget(self.keystring)
self.percentage = Percentage(self) self.url = _Url(self)
self.hbox.addWidget(self.percentage) self._hbox.addWidget(self.url)
self.prog = Progress(self) self.percentage = _Percentage(self)
self.hbox.addWidget(self.prog) self._hbox.addWidget(self.percentage)
self.prog = _Progress(self)
self._hbox.addWidget(self.prog)
@pyqtProperty(bool) @pyqtProperty(bool)
def error(self): def error(self):
@ -106,20 +121,11 @@ class StatusBar(QWidget):
""" """
self._error = val self._error = val
self.setStyleSheet(config.get_stylesheet(self._stylesheet)) 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)
@pyqtSlot(str) @pyqtSlot(str)
def disp_error(self, text): def disp_error(self, text):
"""Displaysan error in the statusbar.""" """Display an error in the statusbar."""
self.error = True self.error = True
self.txt.set_error(text) self.txt.set_error(text)
@ -147,24 +153,40 @@ class StatusBar(QWidget):
self.moved.emit(e.pos()) 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) got_cmd = pyqtSignal(str)
# Emitted for searches triggered by the user
got_search = pyqtSignal(str) got_search = pyqtSignal(str)
got_search_rev = pyqtSignal(str) got_search_rev = pyqtSignal(str)
statusbar = None # The status bar object esc_pressed = pyqtSignal()
esc_pressed = pyqtSignal() # Emitted when escape is pressed tab_pressed = pyqtSignal(bool)
tab_pressed = pyqtSignal(bool) # Emitted when tab is pressed (arg: shift) hide_completion = pyqtSignal()
hide_completion = pyqtSignal() # Hide completion window
history = [] # The command history, with newer commands at the bottom
_shortcuts = []
_tmphist = []
_histpos = None
_validator = None # CommandValidator
# FIXME won't the tab key switch to the next widget? # FIXME won't the tab key switch to the next widget?
# See [0] for a possible fix. # See [0] for a possible fix.
@ -173,7 +195,9 @@ class Command(QLineEdit):
def __init__(self, statusbar): def __init__(self, statusbar):
super().__init__(statusbar) super().__init__(statusbar)
# FIXME # FIXME
self.statusbar = statusbar self._statusbar = statusbar
self._histpos = None
self._tmphist = []
self.setStyleSheet(""" self.setStyleSheet("""
QLineEdit { QLineEdit {
border: 0px; border: 0px;
@ -181,16 +205,18 @@ class Command(QLineEdit):
background-color: transparent; background-color: transparent;
} }
""") """)
self._validator = CommandValidator(self) self._validator = _CommandValidator(self)
self.setValidator(self._validator) 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.textEdited.connect(self._histbrowse_stop)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
self.history = []
self._shortcuts = []
for (key, handler) in [ for (key, handler) in [
(Qt.Key_Escape, self.esc_pressed), (Qt.Key_Escape, self.esc_pressed),
(Qt.Key_Up, self.on_key_up_pressed), (Qt.Key_Up, self._on_key_up_pressed),
(Qt.Key_Down, self.on_key_down_pressed), (Qt.Key_Down, self._on_key_down_pressed),
(Qt.Key_Tab | Qt.SHIFT, lambda: self.tab_pressed.emit(True)), (Qt.Key_Tab | Qt.SHIFT, lambda: self.tab_pressed.emit(True)),
(Qt.Key_Tab, lambda: self.tab_pressed.emit(False)) (Qt.Key_Tab, lambda: self.tab_pressed.emit(False))
]: ]:
@ -201,7 +227,7 @@ class Command(QLineEdit):
self._shortcuts.append(sc) self._shortcuts.append(sc)
@pyqtSlot() @pyqtSlot()
def on_return_pressed(self): def _on_return_pressed(self):
"""Handle the command in the status bar.""" """Handle the command in the status bar."""
signals = { signals = {
':': self.got_cmd, ':': self.got_cmd,
@ -217,7 +243,7 @@ class Command(QLineEdit):
signals[text[0]].emit(text.lstrip(text[0])) signals[text[0]].emit(text.lstrip(text[0]))
@pyqtSlot(str) @pyqtSlot(str)
def on_set_cmd_text(self, text): def set_cmd_text(self, text):
"""Preset the statusbar to some text.""" """Preset the statusbar to some text."""
self.setText(text) self.setText(text)
self.setFocus() self.setFocus()
@ -240,7 +266,7 @@ class Command(QLineEdit):
def focusInEvent(self, e): def focusInEvent(self, e):
"""Clear error message when the statusbar is focused.""" """Clear error message when the statusbar is focused."""
self.statusbar.clear_error() self._statusbar.clear_error()
super().focusInEvent(e) super().focusInEvent(e)
def _histbrowse_start(self): def _histbrowse_start(self):
@ -264,7 +290,7 @@ class Command(QLineEdit):
self._histpos = None self._histpos = None
@pyqtSlot() @pyqtSlot()
def on_key_up_pressed(self): def _on_key_up_pressed(self):
"""Handle Up presses (go back in history).""" """Handle Up presses (go back in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos)) logging.debug("history up [pre]: pos {}".format(self._histpos))
if self._histpos is None: if self._histpos is None:
@ -277,10 +303,10 @@ class Command(QLineEdit):
return return
logging.debug("history up: {} / len {} / pos {}".format( logging.debug("history up: {} / len {} / pos {}".format(
self._tmphist, len(self._tmphist), self._histpos)) self._tmphist, len(self._tmphist), self._histpos))
self.set_cmd(self._tmphist[self._histpos]) self.set_cmd_text(self._tmphist[self._histpos])
@pyqtSlot() @pyqtSlot()
def on_key_down_pressed(self): def _on_key_down_pressed(self):
"""Handle Down presses (go forward in history).""" """Handle Down presses (go forward in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos, logging.debug("history up [pre]: pos {}".format(self._histpos,
self._tmphist, len(self._tmphist), self._histpos)) self._tmphist, len(self._tmphist), self._histpos))
@ -291,10 +317,10 @@ class Command(QLineEdit):
self._histpos += 1 self._histpos += 1
logging.debug("history up: {} / len {} / pos {}".format( logging.debug("history up: {} / len {} / pos {}".format(
self._tmphist, len(self._tmphist), self._histpos)) 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.""" """Validator to prevent the : from getting deleted."""
@ -313,13 +339,17 @@ class CommandValidator(QValidator):
return (QValidator.Invalid, string, pos) 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 # FIXME for some reason, margin-left is not shown
_stylesheet = """ _STYLESHEET = """
QProgressBar {{ QProgressBar {{
border-radius: 0px; border-radius: 0px;
border: 2px solid transparent; border: 2px solid transparent;
@ -332,10 +362,9 @@ class Progress(QProgressBar):
}} }}
""" """
def __init__(self, statusbar): def __init__(self, parent):
super().__init__(statusbar) super().__init__(parent)
self.statusbar = statusbar self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)
self.setTextVisible(False) self.setTextVisible(False)
self.hide() self.hide()
@ -356,15 +385,17 @@ class TextBase(QLabel):
Eliding is loosly based on Eliding is loosly based on
http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html 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): def __init__(self, bar, elidemode=Qt.ElideRight):
super().__init__(bar) super().__init__(bar)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
self.elidemode = elidemode self._elidemode = elidemode
self._elided_text = ''
def setText(self, txt): def setText(self, txt):
"""Extend QLabel::setText to update the elided text afterwards.""" """Extend QLabel::setText to update the elided text afterwards."""
@ -383,11 +414,11 @@ class TextBase(QLabel):
""" """
self._elided_text = self.fontMetrics().elidedText( self._elided_text = self.fontMetrics().elidedText(
self.text(), self.elidemode, width, Qt.TextShowMnemonic) self.text(), self._elidemode, width, Qt.TextShowMnemonic)
def paintEvent(self, e): def paintEvent(self, e):
"""Override QLabel::paintEvent to draw elided text.""" """Override QLabel::paintEvent to draw elided text."""
if self.elidemode == Qt.ElideNone: if self._elidemode == Qt.ElideNone:
super().paintEvent(e) super().paintEvent(e)
else: else:
painter = QPainter(self) painter = QPainter(self)
@ -396,30 +427,37 @@ class TextBase(QLabel):
self._elided_text) 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): def set_error(self, text):
"""Display an error message and save current text in old_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) self.setText(text)
def clear_error(self): def clear_error(self):
"""Clear a displayed error message.""" """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.""" """Keychain string displayed in the statusbar."""
pass pass
class Percentage(TextBase): class _Percentage(TextBase):
"""Reading percentage displayed in the statusbar.""" """Reading percentage displayed in the statusbar."""
@ -434,15 +472,20 @@ class Percentage(TextBase):
self.setText('[{:2}%]'.format(y)) self.setText('[{:2}%]'.format(y))
class Url(TextBase): class _Url(TextBase):
"""URL displayed in the statusbar.""" """URL displayed in the statusbar.
_old_url = None Attributes:
_old_urltype = None _old_url: The URL displayed before the hover URL.
_urltype = None # 'normal', 'ok', 'error', 'warn, 'hover' _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"] {{ QLabel#Url[urltype="normal"] {{
{color[statusbar.url.fg]} {color[statusbar.url.fg]}
}} }}
@ -468,7 +511,10 @@ class Url(TextBase):
"""Override TextBase::__init__ to elide in the middle by default.""" """Override TextBase::__init__ to elide in the middle by default."""
super().__init__(bar, elidemode) super().__init__(bar, elidemode)
self.setObjectName(self.__class__.__name__) 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) @pyqtProperty(str)
def urltype(self): def urltype(self):
@ -480,7 +526,7 @@ class Url(TextBase):
def urltype(self, val): def urltype(self, val):
"""Setter for self.urltype, so it can be used as Qt property.""" """Setter for self.urltype, so it can be used as Qt property."""
self._urltype = val self._urltype = val
self.setStyleSheet(config.get_stylesheet(self._stylesheet)) self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
@pyqtSlot(bool) @pyqtSlot(bool)
def on_loading_finished(self, ok): def on_loading_finished(self, ok):

View File

@ -26,12 +26,17 @@ from qutebrowser.utils.style import Style
class TabWidget(QTabWidget): 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 # FIXME there is still some ugly 1px white stripe from somewhere if we do
# background-color: grey for QTabBar... # background-color: grey for QTabBar...
_stylesheet = """ _STYLESHEET = """
QTabWidget::pane {{ QTabWidget::pane {{
position: absolute; position: absolute;
top: 0px; top: 0px;
@ -63,7 +68,7 @@ class TabWidget(QTabWidget):
super().__init__(parent) super().__init__(parent)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setStyle(Style(self.style())) self.setStyle(Style(self.style()))
self.setStyleSheet(config.get_stylesheet(self._stylesheet)) self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self.setDocumentMode(True) self.setDocumentMode(True)
self.setElideMode(Qt.ElideRight) self.setElideMode(Qt.ElideRight)
self._init_config() self._init_config()