Refactor ALL the things

This commit is contained in:
Florian Bruhin 2014-02-18 16:38:13 +01:00
parent 6cd02ca368
commit 911d5d3b4a
16 changed files with 602 additions and 435 deletions

View File

@ -64,39 +64,43 @@ class QuteBrowser(QApplication):
>>> app = QuteBrowser()
>>> sys.exit(app.exec_())
"""
Attributes:
mainwindow: The MainWindow QWidget.
commandparser: The main CommandParser instance.
keyparser: The main KeyParser instance.
searchparser: The main SearchParser instance.
_dirs: AppDirs instance for config/cache directories.
_args: ArgumentParser instance.
_timers: List of used QTimers so they don't get GCed.
_shutting_down: True if we're currently shutting down.
_quit_status: The current quitting status.
dirs = None # AppDirs - config/cache directories
config = None # Config(Parser) object
mainwindow = None
commandparser = None
keyparser = None
args = None # ArgumentParser
timers = None
shutting_down = False
_quit_status = None
"""
def __init__(self):
super().__init__(sys.argv)
self._quit_status = {}
self._timers = []
self._shutting_down = False
sys.excepthook = self._exception_hook
self._parseopts()
self._args = self._parseopts()
self._initlog()
self._initmisc()
self.dirs = AppDirs('qutebrowser')
if self.args.confdir is None:
confdir = self.dirs.user_config_dir
elif self.args.confdir == '':
self._dirs = AppDirs('qutebrowser')
if self._args.confdir is None:
confdir = self._dirs.user_config_dir
elif self._args.confdir == '':
confdir = None
else:
confdir = self.args.confdir
confdir = self._args.confdir
config.init(confdir)
self.commandparser = cmdutils.CommandParser()
self.searchparser = cmdutils.SearchParser()
self.keyparser = KeyParser(self.mainwindow)
self.keyparser = KeyParser(self)
self._init_cmds()
self.mainwindow = MainWindow()
@ -125,7 +129,7 @@ class QuteBrowser(QApplication):
self.mainwindow.show()
self._python_hacks()
timer = QTimer.singleShot(0, self._process_init_args)
self.timers.append(timer)
self._timers.append(timer)
def _process_init_args(self):
"""Process initial positional args.
@ -139,7 +143,7 @@ class QuteBrowser(QApplication):
QEventLoop.ExcludeSocketNotifiers)
opened_urls = False
for e in self.args.command:
for e in self._args.command:
if e.startswith(':'):
logging.debug('Startup cmd {}'.format(e))
self.commandparser.run(e.lstrip(':'))
@ -254,7 +258,7 @@ class QuteBrowser(QApplication):
timer = QTimer()
timer.start(500)
timer.timeout.connect(lambda: None)
self.timers.append(timer)
self._timers.append(timer)
def _parseopts(self):
"""Parse command line options."""
@ -269,11 +273,11 @@ class QuteBrowser(QApplication):
'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()
return parser.parse_args()
def _initlog(self):
"""Initialisation of the logging output."""
loglevel = 'debug' if self.args.debug else self.args.loglevel
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))
@ -285,11 +289,10 @@ class QuteBrowser(QApplication):
def _initmisc(self):
"""Initialize misc things."""
if self.args.debug:
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.
@ -315,9 +318,9 @@ class QuteBrowser(QApplication):
quit -- Whether to quit after shutting down.
"""
if self.shutting_down:
if self._shutting_down:
return
self.shutting_down = True
self._shutting_down = True
logging.debug("Shutting down... (do_quit={})".format(do_quit))
try:
config.config.save()
@ -331,7 +334,7 @@ class QuteBrowser(QApplication):
try:
if do_quit:
self.mainwindow.tabs.shutdown_complete.connect(
self._on_tab_shutdown_complete)
self.on_tab_shutdown_complete)
else:
self.mainwindow.tabs.shutdown_complete.connect(
functools.partial(self._maybe_quit, 'shutdown'))
@ -351,7 +354,7 @@ class QuteBrowser(QApplication):
config.state['geometry']['mainwindow'] = geom
@pyqtSlot()
def _on_tab_shutdown_complete(self):
def on_tab_shutdown_complete(self):
"""Quit application after a shutdown.
Gets called when all tabs finished shutting down after shutdown().
@ -375,32 +378,33 @@ class QuteBrowser(QApplication):
(count, argv) = tpl
cmd = argv[0]
args = argv[1:]
browser = self.mainwindow.tabs
handlers = {
'open': self.mainwindow.tabs.openurl,
'opencur': self.mainwindow.tabs.opencur,
'tabopen': self.mainwindow.tabs.tabopen,
'tabopencur': self.mainwindow.tabs.tabopencur,
'open': browser.openurl,
'opencur': browser.opencur,
'tabopen': browser.tabopen,
'tabopencur': browser.tabopencur,
'quit': self.shutdown,
'tabclose': self.mainwindow.tabs.cur_close,
'tabprev': self.mainwindow.tabs.switch_prev,
'tabnext': self.mainwindow.tabs.switch_next,
'reload': self.mainwindow.tabs.cur_reload,
'stop': self.mainwindow.tabs.cur_stop,
'back': self.mainwindow.tabs.cur_back,
'forward': self.mainwindow.tabs.cur_forward,
'print': self.mainwindow.tabs.cur_print,
'scroll': self.mainwindow.tabs.cur_scroll,
'scroll_page': self.mainwindow.tabs.cur_scroll_page,
'scroll_perc_x': self.mainwindow.tabs.cur_scroll_percent_x,
'scroll_perc_y': self.mainwindow.tabs.cur_scroll_percent_y,
'undo': self.mainwindow.tabs.undo_close,
'tabclose': browser.cur_close,
'tabprev': browser.switch_prev,
'tabnext': browser.switch_next,
'reload': browser.cur_reload,
'stop': browser.cur_stop,
'back': browser.cur_back,
'forward': browser.cur_forward,
'print': browser.cur_print,
'scroll': browser.cur_scroll,
'scroll_page': browser.cur_scroll_page,
'scroll_perc_x': browser.cur_scroll_percent_x,
'scroll_perc_y': browser.cur_scroll_percent_y,
'undo': browser.undo_close,
'pyeval': self.pyeval,
'nextsearch': self.searchparser.nextsearch,
'yank': self.mainwindow.tabs.cur_yank,
'yanktitle': self.mainwindow.tabs.cur_yank_title,
'paste': self.mainwindow.tabs.paste,
'tabpaste': self.mainwindow.tabs.tabpaste,
'yank': browser.cur_yank,
'yanktitle': browser.cur_yank_title,
'paste': browser.paste,
'tabpaste': browser.tabpaste,
'crash': self.crash,
}

View File

@ -32,17 +32,24 @@ startchars = ":/?"
class KeyParser(QObject):
"""Parser for vim-like key sequences."""
"""Parser for vim-like key sequences.
Attributes:
commandparser: Commandparser instance.
_keystring: The currently entered key sequence
_bindings: Bound keybindings
_modifier_bindings: Bound modifier bindings.
Signals:
set_cmd_text: Emitted when the statusbar should set a partial command.
arg: Text to set.
keystring_updated: Emitted when the keystring is updated.
arg: New keystring.
"""
keystring = '' # The currently entered key sequence
# Signal emitted when the statusbar should set a partial command
set_cmd_text = pyqtSignal(str)
# Signal emitted when the keystring is updated
keystring_updated = pyqtSignal(str)
# Keybindings
bindings = {}
modifier_bindings = {}
commandparser = None
MATCH_PARTIAL = 0
MATCH_DEFINITIVE = 1
@ -51,6 +58,9 @@ class KeyParser(QObject):
def __init__(self, mainwindow):
super().__init__(mainwindow)
self.commandparser = CommandParser()
self._keystring = ''
self._bindings = {}
self._modifier_bindings = {}
def from_config_sect(self, sect):
"""Load keybindings from a ConfigParser section.
@ -65,10 +75,10 @@ class KeyParser(QObject):
keystr = self._normalize_keystr(key.strip('@'))
logging.debug('registered mod key: {} -> {}'.format(keystr,
cmd))
self.modifier_bindings[keystr] = cmd
self._modifier_bindings[keystr] = cmd
else:
logging.debug('registered key: {} -> {}'.format(key, cmd))
self.bindings[key] = cmd
self._bindings[key] = cmd
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
@ -79,7 +89,7 @@ class KeyParser(QObject):
handled = self._handle_modifier_key(e)
if not handled:
self._handle_single_key(e)
self.keystring_updated.emit(self.keystring)
self.keystring_updated.emit(self._keystring)
def _handle_modifier_key(self, e):
"""Handle a new keypress with modifiers.
@ -108,7 +118,7 @@ class KeyParser(QObject):
modstr += s + '+'
keystr = QKeySequence(e.key()).toString()
try:
cmdstr = self.modifier_bindings[modstr + keystr]
cmdstr = self._modifier_bindings[modstr + keystr]
except KeyError:
logging.debug('No binding found for {}.'.format(modstr + keystr))
return True
@ -133,15 +143,15 @@ class KeyParser(QObject):
logging.debug('Ignoring, no text')
return
self.keystring += txt
self._keystring += txt
if any(self.keystring == c for c in startchars):
self.set_cmd_text.emit(self.keystring)
self.keystring = ''
if any(self._keystring == c for c in startchars):
self.set_cmd_text.emit(self._keystring)
self._keystring = ''
return
(countstr, cmdstr_needle) = re.match(r'^(\d*)(.*)',
self.keystring).groups()
self._keystring).groups()
if not cmdstr_needle:
return
@ -157,16 +167,16 @@ class KeyParser(QObject):
if match == self.MATCH_DEFINITIVE:
pass
elif match == self.MATCH_PARTIAL:
logging.debug('No match for "{}" (added {})'.format(self.keystring,
txt))
logging.debug('No match for "{}" (added {})'.format(
self._keystring, txt))
return
elif match == self.MATCH_NONE:
logging.debug('Giving up with "{}", no matches'.format(
self.keystring))
self.keystring = ''
self._keystring))
self._keystring = ''
return
self.keystring = ''
self._keystring = ''
count = int(countstr) if countstr else None
self._run_or_fill(cmdstr_hay, count=count, ignore_exc=False)
return
@ -178,11 +188,11 @@ class KeyParser(QObject):
"""
try:
cmdstr_hay = self.bindings[cmdstr_needle]
cmdstr_hay = self._bindings[cmdstr_needle]
return (self.MATCH_DEFINITIVE, cmdstr_hay)
except KeyError:
# No definitive match, check if there's a chance of a partial match
for hay in self.bindings:
for hay in self._bindings:
try:
if cmdstr_needle[-1] == hay[len(cmdstr_needle) - 1]:
return (self.MATCH_PARTIAL, None)

View File

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

View File

@ -49,12 +49,26 @@ def register_all():
class SearchParser(QObject):
"""Parse qutebrowser searches."""
"""Parse qutebrowser searches.
Attributes:
_text: The text from the last search.
_flags: The flags from the last search.
Signals:
do_search: Emitted when a search should be started.
arg 1: Search string.
arg 2: Flags to use.
"""
text = None
flags = 0
do_search = pyqtSignal(str, 'QWebPage::FindFlags')
def __init__(self, parent=None):
self._text = None
self._flags = 0
super().__init__(parent)
@pyqtSlot(str)
def search(self, text):
"""Search for a text on a website.
@ -80,33 +94,45 @@ class SearchParser(QObject):
rev -- Search direction.
"""
if self.text != text:
if self._text is not None and self._text != text:
self.do_search.emit('', 0)
self.text = text
self.flags = 0
self._text = text
self._flags = 0
if config.config.getboolean('general', 'ignorecase', fallback=True):
self.flags |= QWebPage.FindCaseSensitively
self._flags |= QWebPage.FindCaseSensitively
if config.config.getboolean('general', 'wrapsearch', fallback=True):
self.flags |= QWebPage.FindWrapsAroundDocument
self._flags |= QWebPage.FindWrapsAroundDocument
if rev:
self.flags |= QWebPage.FindBackward
self.do_search.emit(self.text, self.flags)
self._flags |= QWebPage.FindBackward
self.do_search.emit(self._text, self._flags)
def nextsearch(self, count=1):
"""Continue the search to the ([count]th) next term."""
if self.text is not None:
if self._text is not None:
for i in range(count): # pylint: disable=unused-variable
self.do_search.emit(self.text, self.flags)
self.do_search.emit(self._text, self._flags)
class CommandParser(QObject):
"""Parse qutebrowser commandline commands."""
"""Parse qutebrowser commandline commands.
text = ''
cmd = ''
args = []
error = pyqtSignal(str) # Emitted if there's an error
Attributes:
_cmd: The command which was parsed.
_args: The arguments which were parsed.
Signals:
error: Emitted if there was an error.
arg: The error message.
"""
error = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._cmd = None
self._args = []
def _parse(self, text):
"""Split the commandline text into command and arguments.
@ -114,8 +140,7 @@ class CommandParser(QObject):
Raise NoSuchCommandError if a command wasn't found.
"""
self.text = text
parts = self.text.strip().split(maxsplit=1)
parts = text.strip().split(maxsplit=1)
if not parts:
raise NoSuchCommandError
cmdstr = parts[0]
@ -130,19 +155,19 @@ class CommandParser(QObject):
args = shlex.split(parts[1])
else:
args = [parts[1]]
self.cmd = cmd
self.args = args
self._cmd = cmd
self._args = args
def _check(self):
"""Check if the argument count for the command is correct."""
self.cmd.check(self.args)
self._cmd.check(self._args)
def _run(self, count=None):
"""Run a command with an optional count."""
if count is not None:
self.cmd.run(self.args, count=count)
self._cmd.run(self._args, count=count)
else:
self.cmd.run(self.args)
self._cmd.run(self._args)
@pyqtSlot(str, int, bool)
def run(self, text, count=None, ignore_exc=True):
@ -165,7 +190,7 @@ class CommandParser(QObject):
except ArgumentCountError:
if ignore_exc:
self.error.emit("{}: invalid argument count".format(
self.cmd.mainname))
self._cmd.mainname))
return False
else:
raise

View File

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

View File

@ -15,15 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""The base completion model for completion in the command line.
Contains:
CompletionModel -- A simple tree model based on Python data.
CompletionItem -- One item in the CompletionModel.
"""
from collections import OrderedDict
"""The base completion model for completion in the command line."""
from PyQt5.QtCore import Qt, QVariant, QAbstractItemModel, QModelIndex
@ -34,15 +26,18 @@ class CompletionModel(QAbstractItemModel):
Used for showing completions later in the CompletionView.
Attributes:
_id_map: A mapping from Python object IDs (from id()) to objects, to be
used as internalIndex for the model.
_root: The root item.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._data = OrderedDict()
self.parents = []
self.id_map = {}
self.root = CompletionItem([""] * 2)
self.id_map[id(self.root)] = self.root
self._id_map = {}
self._root = CompletionItem([""] * 2)
self._id_map[id(self._root)] = self._root
def removeRows(self, position=0, count=1, parent=QModelIndex()):
"""Remove rows from the model.
@ -63,9 +58,9 @@ class CompletionModel(QAbstractItemModel):
"""
if index.isValid():
return self.id_map[index.internalId()]
return self._id_map[index.internalId()]
else:
return self.root
return self._root
def columnCount(self, parent=QModelIndex()):
"""Return the column count in the model.
@ -74,7 +69,7 @@ class CompletionModel(QAbstractItemModel):
"""
# pylint: disable=unused-argument
return self.root.column_count()
return self._root.column_count()
def data(self, index, role=Qt.DisplayRole):
"""Return the data for role/index as QVariant.
@ -86,7 +81,7 @@ class CompletionModel(QAbstractItemModel):
if not index.isValid():
return QVariant()
try:
item = self.id_map[index.internalId()]
item = self._id_map[index.internalId()]
except KeyError:
return QVariant()
try:
@ -106,7 +101,7 @@ class CompletionModel(QAbstractItemModel):
if not index.isValid():
return Qt.NoItemFlags
flags = Qt.ItemIsEnabled
if len(self.id_map[index.internalId()].children) > 0:
if len(self._id_map[index.internalId()].children) > 0:
return flags
else:
return flags | Qt.ItemIsSelectable
@ -119,7 +114,7 @@ class CompletionModel(QAbstractItemModel):
"""
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.root.data(section))
return QVariant(self._root.data(section))
return QVariant()
def setData(self, index, value, role=Qt.EditRole):
@ -131,7 +126,7 @@ class CompletionModel(QAbstractItemModel):
"""
if not index.isValid():
return False
item = self.id_map[index.internalId()]
item = self._id_map[index.internalId()]
try:
item.setdata(index.column(), value, role)
except (IndexError, ValueError):
@ -153,14 +148,14 @@ class CompletionModel(QAbstractItemModel):
return QModelIndex()
if not parent.isValid():
parent_item = self.root
parent_item = self._root
else:
parent_item = self.id_map[parent.internalId()]
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)
self._id_map.setdefault(index.internalId(), child_item)
return index
else:
return QModelIndex()
@ -174,8 +169,8 @@ class CompletionModel(QAbstractItemModel):
"""
if not index.isValid():
return QModelIndex()
item = self.id_map[index.internalId()].parent
if item == self.root or item is None:
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))
@ -190,9 +185,9 @@ class CompletionModel(QAbstractItemModel):
return 0
if not parent.isValid():
pitem = self.root
pitem = self._root
else:
pitem = self.id_map[parent.internalId()]
pitem = self._id_map[parent.internalId()]
return len(pitem.children)
@ -205,15 +200,19 @@ class CompletionModel(QAbstractItemModel):
"""
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)
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
self._id_map[id(newitem)] = newitem
newcat.children.append(newitem)
def mark_all_items(self, needle):
@ -247,12 +246,15 @@ class CompletionModel(QAbstractItemModel):
class CompletionItem():
"""An item (row) in a CompletionModel."""
"""An item (row) in a CompletionModel.
parent = None
children = None
_data = None
_marks = None
Attributes:
parent: The parent of this item.
children: The children of this item.
_data: The data of this item.
_marks: The marks of this item.
"""
def __init__(self, data, parent=None):
"""Constructor for CompletionItem.
@ -262,8 +264,8 @@ class CompletionItem():
"""
self.parent = parent
self._data = data
self.children = []
self._data = data
self._marks = []
def data(self, column, role=Qt.DisplayRole):

View File

@ -27,13 +27,17 @@ from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
class CompletionFilterModel(QSortFilterProxyModel):
"""Subclass of QSortFilterProxyModel with custom sorting/filtering."""
"""Subclass of QSortFilterProxyModel with custom sorting/filtering.
_pattern = None
srcmodel = None
Attributes:
_pattern: The pattern to filter with, used in pattern property.
srcmodel: The source model.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.srcmodel = None
self._pattern = ''
@property
@ -41,16 +45,6 @@ class CompletionFilterModel(QSortFilterProxyModel):
"""Getter for pattern."""
return self._pattern
def setsrc(self, model):
"""Set a new source model and clear the pattern.
model -- The new source model.
"""
self.setSourceModel(model)
self.srcmodel = model
self.pattern = ''
@pattern.setter
def pattern(self, val):
"""Setter for pattern.
@ -71,6 +65,16 @@ class CompletionFilterModel(QSortFilterProxyModel):
self.sort(sortcol)
self.invalidate()
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 = ''
def filterAcceptsRow(self, row, parent):
"""Custom filter implementation.

View File

@ -130,11 +130,14 @@ class FontDict(dict):
class Config(ConfigParser):
"""Our own ConfigParser subclass."""
"""Our own ConfigParser subclass.
configdir = None
default_cp = None
config_loaded = False
Attributes:
_configdir: The dictionary to save the config in.
_default_cp: The ConfigParser instance supplying the default values.
_config_loaded: Whether the config was loaded successfully.
"""
def __init__(self, configdir, fname, default_config=None,
always_save=False):
@ -148,22 +151,23 @@ class Config(ConfigParser):
"""
super().__init__(interpolation=ExtendedInterpolation())
self._config_loaded = False
self.always_save = always_save
self.configdir = configdir
self.default_cp = ConfigParser(interpolation=ExtendedInterpolation())
self.default_cp.optionxform = lambda opt: opt # be case-insensitive
self._configdir = configdir
self._default_cp = ConfigParser(interpolation=ExtendedInterpolation())
self._default_cp.optionxform = lambda opt: opt # be case-insensitive
if default_config is not None:
self.default_cp.read_string(default_config)
if not self.configdir:
self._default_cp.read_string(default_config)
if not self._configdir:
return
self.optionxform = lambda opt: opt # be case-insensitive
self.configdir = configdir
self.configfile = os.path.join(self.configdir, fname)
self._configdir = configdir
self.configfile = os.path.join(self._configdir, fname)
if not os.path.isfile(self.configfile):
return
logging.debug("Reading config from {}".format(self.configfile))
self.read(self.configfile)
self.config_loaded = True
self._config_loaded = True
def __getitem__(self, key):
"""Get an item from the configparser or default dict.
@ -174,7 +178,7 @@ class Config(ConfigParser):
try:
return super().__getitem__(key)
except KeyError:
return self.default_cp[key]
return self._default_cp[key]
def get(self, *args, raw=False, vars=None, fallback=_UNSET):
"""Get an item from the configparser or default dict.
@ -194,7 +198,7 @@ class Config(ConfigParser):
except (NoSectionError, NoOptionError):
pass
try:
return self.default_cp.get(*args, raw=raw, vars=vars)
return self._default_cp.get(*args, raw=raw, vars=vars)
except (NoSectionError, NoOptionError):
if fallback is _UNSET:
raise
@ -203,13 +207,13 @@ class Config(ConfigParser):
def save(self):
"""Save the config file."""
if self.configdir is None or (not self.config_loaded and
not self.always_save):
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))
self._configdir, self._config_loaded))
return
if not os.path.exists(self.configdir):
os.makedirs(self.configdir, 0o755)
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
logging.debug("Saving config to {}".format(self.configfile))
with open(self.configfile, 'w') as f:
self.write(f)

View File

@ -42,10 +42,13 @@ def dbg_signal(sig, args):
class SignalCache(QObject):
"""Cache signals emitted by an object, and re-emit them later."""
"""Cache signals emitted by an object, and re-emit them later.
uncached = None
signal_dict = None
Attributes:
_uncached: A list of signals which should not be cached.
_signal_dict: The internal mapping of signals we got.
"""
def __init__(self, uncached=None):
"""Create a new SignalCache.
@ -56,10 +59,10 @@ class SignalCache(QObject):
"""
super().__init__()
if uncached is None:
self.uncached = []
self._uncached = []
else:
self.uncached = uncached
self.signal_dict = OrderedDict()
self._uncached = uncached
self._signal_dict = OrderedDict()
def add(self, sig, args):
"""Add a new signal to the signal cache.
@ -71,21 +74,21 @@ class SignalCache(QObject):
"""
if not self._signal_needs_caching(sig):
return
had_signal = sig.signal in self.signal_dict
self.signal_dict[sig.signal] = (sig, args)
had_signal = sig.signal in self._signal_dict
self._signal_dict[sig.signal] = (sig, args)
if had_signal:
self.signal_dict.move_to_end(sig.signal)
self._signal_dict.move_to_end(sig.signal)
def clear(self):
"""Clear/purge the signal cache."""
self.signal_dict.clear()
self._signal_dict.clear()
def replay(self):
"""Replay all cached signals."""
for (signal, args) in self.signal_dict.values():
for (signal, args) in self._signal_dict.values():
logging.debug('emitting {}'.format(dbg_signal(signal, args)))
signal.emit(*args)
def _signal_needs_caching(self, signal):
"""Return True if a signal should be cached, false otherwise."""
return not signal_name(signal) in self.uncached
return not signal_name(signal) in self._uncached

View File

@ -38,6 +38,9 @@ class Style(QCommonStyle):
http://stackoverflow.com/a/17294081
https://code.google.com/p/makehuman/source/browse/trunk/makehuman/lib/qtgui.py # noqa # pylint: disable=line-too-long
Attributes:
_style: The base/"parent" style.
"""
def __init__(self, style):

View File

@ -56,23 +56,40 @@ class TabbedBrowser(TabWidget):
- the signal gets filtered with _filter_signals and self.cur_* gets
emitted if the signal occured in the current tab.
Attributes:
_url_stack: Stack of URLs of closed tabs.
_space: Space QShortcut to avoid garbage collection
_tabs: A list of open tabs.
Signals:
cur_progress: Progress of the current tab changed (loadProgress).
cur_load_started: Current tab started loading (loadStarted)
cur_load_finished: Current tab finished loading (loadFinished)
cur_statusbar_message: Current tab got a statusbar message
(statusBarMessage)
cur_url_changed: Current URL changed (urlChanged)
cur_link_hovered: Link hovered in current tab (linkHovered)
cur_scroll_perc_changed: Scroll percentage of current tab changed.
arg 1: x-position in %.
arg 2: y-position in %.
keypress: A key was pressed.
arg: The QKeyEvent leading to the keypress.
shutdown_complete: The shuttdown is completed.
quit: The last tab was closed, quit application.
"""
cur_progress = pyqtSignal(int) # Progress of the current tab changed
cur_load_started = pyqtSignal() # Current tab started loading
cur_load_finished = pyqtSignal(bool) # Current tab finished loading
cur_statusbar_message = pyqtSignal(str) # Status bar message
cur_url_changed = pyqtSignal('QUrl') # Current URL changed
cur_link_hovered = pyqtSignal(str, str, str) # Link hovered in cur tab
# Current tab changed scroll position
cur_progress = pyqtSignal(int)
cur_load_started = pyqtSignal()
cur_load_finished = pyqtSignal(bool)
cur_statusbar_message = pyqtSignal(str)
cur_url_changed = pyqtSignal('QUrl')
cur_link_hovered = pyqtSignal(str, str, str)
cur_scroll_perc_changed = pyqtSignal(int, int)
set_cmd_text = pyqtSignal(str) # Set commandline to a given text
set_cmd_text = pyqtSignal(str)
keypress = pyqtSignal('QKeyEvent')
shutdown_complete = pyqtSignal() # All tabs have been shut down.
quit = pyqtSignal() # Last tab closed, quit application.
_url_stack = [] # Stack of URLs of closed tabs
_space = None # Space QShortcut
_tabs = None
shutdown_complete = pyqtSignal()
quit = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
@ -469,27 +486,37 @@ class BrowserTab(QWebView):
Our own subclass of a QWebView with some added bells and whistles.
Attributes:
page_: The QWebPage behind the view
signal_cache: The signal cache associated with the view.
_scroll_pos: The old scroll position.
_shutdown_callback: Callback to be called after shutdown.
_open_new_tab: Whether to open a new tab for the next action.
_shutdown_callback: The callback to call after shutting down.
_destroyed: Dict of all items to be destroyed on shtudown.
Signals:
scroll_pos_changed: Scroll percentage of current tab changed.
arg 1: x-position in %.
arg 2: y-position in %.
open_tab: A new tab should be opened.
arg: The address to open
linkHovered: QWebPages linkHovered signal exposed.
"""
progress = 0
scroll_pos_changed = pyqtSignal(int, int)
open_tab = pyqtSignal('QUrl')
linkHovered = pyqtSignal(str, str, str)
_scroll_pos = (-1, -1)
_shutdown_callback = None # callback to be called after shutdown
_open_new_tab = False # open new tab for the next action
_destroyed = None # Dict of all items to be destroyed.
page_ = None # QWebPage
# dict of tab specific signals, and the values we last got from them.
signal_cache = None
def __init__(self, parent=None):
super().__init__(parent)
self._scroll_pos = (-1, -1)
self._shutdown_callback = None
self._open_new_tab = False
self._destroyed = {}
self.page_ = BrowserPage(self)
self.setPage(self.page_)
self.signal_cache = SignalCache(uncached=['linkHovered'])
self.loadProgress.connect(self.on_load_progress)
self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page_.linkHovered.connect(self.linkHovered)
self.installEventFilter(self)
@ -532,17 +559,6 @@ class BrowserTab(QWebView):
else:
self.openurl(url)
@pyqtSlot(int)
def on_load_progress(self, prog):
"""Update the progress property if the loading progress changed.
Slot for the loadProgress signal.
prog -- New progress.
"""
self.progress = prog
def shutdown(self, callback=None):
"""Shut down the tab cleanly and remove it.
@ -562,22 +578,22 @@ class BrowserTab(QWebView):
self.settings().setAttribute(QWebSettings.JavascriptEnabled, False)
self._destroyed[self.page_] = False
self.page_.destroyed.connect(functools.partial(self.on_destroyed,
self.page_.destroyed.connect(functools.partial(self._on_destroyed,
self.page_))
self.page_.deleteLater()
self._destroyed[self] = False
self.destroyed.connect(functools.partial(self.on_destroyed, self))
self.destroyed.connect(functools.partial(self._on_destroyed, self))
self.deleteLater()
netman = self.page_.network_access_manager
self._destroyed[netman] = False
netman.abort_requests()
netman.destroyed.connect(functools.partial(self.on_destroyed, netman))
netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
netman.deleteLater()
logging.debug("Tab shutdown scheduled")
def on_destroyed(self, sender):
def _on_destroyed(self, sender):
"""Called when a subsystem has been destroyed during shutdown."""
self._destroyed[sender] = True
dbgout = '\n'.join(['{}: {}'.format(k.__class__.__name__, v)
@ -636,10 +652,13 @@ class BrowserTab(QWebView):
class BrowserPage(QWebPage):
"""Our own QWebPage with advanced features."""
"""Our own QWebPage with advanced features.
_extension_handlers = None
network_access_manager = None
Attributes:
_extension_handlers: Mapping of QWebPage extensions to their handlers.
network_access_manager: The QNetworkAccessManager used.
"""
def __init__(self, parent=None):
super().__init__(parent)
@ -683,9 +702,12 @@ class BrowserPage(QWebPage):
class NetworkManager(QNetworkAccessManager):
"""Our own QNetworkAccessManager."""
"""Our own QNetworkAccessManager.
_requests = None
Attributes:
_requests: Pending requests.
"""
def __init__(self, parent=None):
self._requests = {}

View File

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

View File

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

View File

@ -36,11 +36,12 @@ class MainWindow(QWidget):
Adds all needed components to a vbox, initializes subwidgets and connects
signals.
"""
Attributes:
tabs: The TabbedBrowser widget.
status: The StatusBar widget.
_vbox: The main QVBoxLayout.
vbox = None
tabs = None
status = None
"""
def __init__(self):
super().__init__()
@ -59,17 +60,17 @@ class MainWindow(QWidget):
if not ok:
self._set_default_geometry()
self.vbox = QVBoxLayout(self)
self.vbox.setContentsMargins(0, 0, 0, 0)
self.vbox.setSpacing(0)
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
self.tabs = TabbedBrowser()
self.vbox.addWidget(self.tabs)
self._vbox.addWidget(self.tabs)
self.completion = CompletionView(self)
self.status = StatusBar()
self.vbox.addWidget(self.status)
self._vbox.addWidget(self.status)
self.status.resized.connect(self.completion.resize_to_bar)
self.status.moved.connect(self.completion.move_to_bar)

View File

@ -32,20 +32,33 @@ from qutebrowser.utils.url import urlstring
class StatusBar(QWidget):
"""The statusbar at the bottom of the mainwindow."""
"""The statusbar at the bottom of the mainwindow.
Attributes:
cmd: The Command widget in the statusbar.
txt: The Text widget in the statusbar.
keystring: The KeyString widget in the statusbar.
percentage: The Percentage widget in the statusbar.
url: The Url widget in the statusbar.
prog: The Progress widget in the statusbar.
_hbox: The main QHBoxLayout.
_error: If there currently is an error, accessed through the error
property.
_STYLESHEET: The stylesheet template.
Signals:
resized: Emitted when the statusbar has resized, so the completion
widget can adjust its size to it.
arg: The new size.
moved: Emitted when the statusbar has moved, so the completion widget
can move the the right position.
arg: The new position.
"""
hbox = None
cmd = None
txt = None
keystring = None
percentage = None
url = None
prog = None
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
_error = False
_option = None
_stylesheet = """
_STYLESHEET = """
QWidget#StatusBar[error="false"] {{
{color[statusbar.bg]}
}}
@ -60,36 +73,38 @@ class StatusBar(QWidget):
}}
"""
# TODO: the statusbar should be a bit smaller
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName(self.__class__.__name__)
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
self.hbox = QHBoxLayout(self)
self.hbox.setContentsMargins(0, 0, 0, 0)
self.hbox.setSpacing(5)
self._error = False
self._option = None
self.cmd = Command(self)
self.hbox.addWidget(self.cmd)
self._hbox = QHBoxLayout(self)
self._hbox.setContentsMargins(0, 0, 0, 0)
self._hbox.setSpacing(5)
self.txt = Text(self)
self.hbox.addWidget(self.txt)
self.hbox.addStretch()
self.cmd = _Command(self)
self._hbox.addWidget(self.cmd)
self.keystring = KeyString(self)
self.hbox.addWidget(self.keystring)
self.txt = _Text(self)
self._hbox.addWidget(self.txt)
self._hbox.addStretch()
self.url = Url(self)
self.hbox.addWidget(self.url)
self.keystring = _KeyString(self)
self._hbox.addWidget(self.keystring)
self.percentage = Percentage(self)
self.hbox.addWidget(self.percentage)
self.url = _Url(self)
self._hbox.addWidget(self.url)
self.prog = Progress(self)
self.hbox.addWidget(self.prog)
self.percentage = _Percentage(self)
self._hbox.addWidget(self.percentage)
self.prog = _Progress(self)
self._hbox.addWidget(self.prog)
@pyqtProperty(bool)
def error(self):
@ -106,20 +121,19 @@ class StatusBar(QWidget):
"""
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)
option = QStyleOption()
option.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(QStyle.PE_Widget, self._option,
painter, self)
self.style().drawPrimitive(QStyle.PE_Widget, option, painter, self)
@pyqtSlot(str)
def disp_error(self, text):
"""Displaysan error in the statusbar."""
"""Display an error in the statusbar."""
self.error = True
self.txt.set_error(text)
@ -147,24 +161,40 @@ class StatusBar(QWidget):
self.moved.emit(e.pos())
class Command(QLineEdit):
class _Command(QLineEdit):
"""The commandline part of the statusbar."""
"""The commandline part of the statusbar.
Attributes:
history: The command history, with newer commands at the bottom.
_statusbar: The statusbar (parent) QWidget.
_shortcuts: Defined QShortcuts to prevent GCing.
_tmphist: The temporary history for history browsing
_histpos: The current position inside _tmphist
_validator: The current command validator.
Signals:
got_cmd: Emitted when a command is triggered by the user.
arg: The command string.
got_search: Emitted when the user started a new search.
arg: The search term.
got_rev_search: Emitted when the user started a new reverse search.
arg: The search term.
esc_pressed: Emitted when the escape key was pressed.
tab_pressed: Emitted when the tab key was pressed.
arg: Whether shift has been pressed.
hide_completion: Emitted when the completion widget should be hidden.
"""
# FIXME we should probably use a proper model for the command history.
# Emitted when a command is triggered by the user
got_cmd = pyqtSignal(str)
# Emitted for searches triggered by the user
got_search = pyqtSignal(str)
got_search_rev = pyqtSignal(str)
statusbar = None # The status bar object
esc_pressed = pyqtSignal() # Emitted when escape is pressed
tab_pressed = pyqtSignal(bool) # Emitted when tab is pressed (arg: shift)
hide_completion = pyqtSignal() # Hide completion window
history = [] # The command history, with newer commands at the bottom
_shortcuts = []
_tmphist = []
_histpos = None
_validator = None # CommandValidator
esc_pressed = pyqtSignal()
tab_pressed = pyqtSignal(bool)
hide_completion = pyqtSignal()
# FIXME won't the tab key switch to the next widget?
# See [0] for a possible fix.
@ -173,7 +203,9 @@ class Command(QLineEdit):
def __init__(self, statusbar):
super().__init__(statusbar)
# FIXME
self.statusbar = statusbar
self._statusbar = statusbar
self._histpos = None
self._tmphist = []
self.setStyleSheet("""
QLineEdit {
border: 0px;
@ -181,16 +213,18 @@ class Command(QLineEdit):
background-color: transparent;
}
""")
self._validator = CommandValidator(self)
self._validator = _CommandValidator(self)
self.setValidator(self._validator)
self.returnPressed.connect(self.on_return_pressed)
self.returnPressed.connect(self._on_return_pressed)
self.textEdited.connect(self._histbrowse_stop)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
self.history = []
self._shortcuts = []
for (key, handler) in [
(Qt.Key_Escape, self.esc_pressed),
(Qt.Key_Up, self.on_key_up_pressed),
(Qt.Key_Down, self.on_key_down_pressed),
(Qt.Key_Up, self._on_key_up_pressed),
(Qt.Key_Down, self._on_key_down_pressed),
(Qt.Key_Tab | Qt.SHIFT, lambda: self.tab_pressed.emit(True)),
(Qt.Key_Tab, lambda: self.tab_pressed.emit(False))
]:
@ -201,7 +235,7 @@ class Command(QLineEdit):
self._shortcuts.append(sc)
@pyqtSlot()
def on_return_pressed(self):
def _on_return_pressed(self):
"""Handle the command in the status bar."""
signals = {
':': self.got_cmd,
@ -240,7 +274,7 @@ class Command(QLineEdit):
def focusInEvent(self, e):
"""Clear error message when the statusbar is focused."""
self.statusbar.clear_error()
self._statusbar.clear_error()
super().focusInEvent(e)
def _histbrowse_start(self):
@ -264,7 +298,7 @@ class Command(QLineEdit):
self._histpos = None
@pyqtSlot()
def on_key_up_pressed(self):
def _on_key_up_pressed(self):
"""Handle Up presses (go back in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos))
if self._histpos is None:
@ -280,7 +314,7 @@ class Command(QLineEdit):
self.set_cmd_text(self._tmphist[self._histpos])
@pyqtSlot()
def on_key_down_pressed(self):
def _on_key_down_pressed(self):
"""Handle Down presses (go forward in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos,
self._tmphist, len(self._tmphist), self._histpos))
@ -294,7 +328,7 @@ class Command(QLineEdit):
self.set_cmd_text(self._tmphist[self._histpos])
class CommandValidator(QValidator):
class _CommandValidator(QValidator):
"""Validator to prevent the : from getting deleted."""
@ -313,13 +347,17 @@ class CommandValidator(QValidator):
return (QValidator.Invalid, string, pos)
class Progress(QProgressBar):
class _Progress(QProgressBar):
"""The progress bar part of the status bar."""
"""The progress bar part of the status bar.
Attributes:
_STYLESHEET: The stylesheet template.
"""
statusbar = None
# FIXME for some reason, margin-left is not shown
_stylesheet = """
_STYLESHEET = """
QProgressBar {{
border-radius: 0px;
border: 2px solid transparent;
@ -332,10 +370,9 @@ class Progress(QProgressBar):
}}
"""
def __init__(self, statusbar):
super().__init__(statusbar)
self.statusbar = statusbar
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
def __init__(self, parent):
super().__init__(parent)
self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)
self.setTextVisible(False)
self.hide()
@ -356,15 +393,17 @@ class TextBase(QLabel):
Eliding is loosly based on
http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html
"""
Attributes:
_elidemode: Where to elide the text.
_elided_text: The current elided text.
elidemode = None
_elided_text = None
"""
def __init__(self, bar, elidemode=Qt.ElideRight):
super().__init__(bar)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
self.elidemode = elidemode
self._elidemode = elidemode
self._elided_text = ''
def setText(self, txt):
"""Extend QLabel::setText to update the elided text afterwards."""
@ -383,11 +422,11 @@ class TextBase(QLabel):
"""
self._elided_text = self.fontMetrics().elidedText(
self.text(), self.elidemode, width, Qt.TextShowMnemonic)
self.text(), self._elidemode, width, Qt.TextShowMnemonic)
def paintEvent(self, e):
"""Override QLabel::paintEvent to draw elided text."""
if self.elidemode == Qt.ElideNone:
if self._elidemode == Qt.ElideNone:
super().paintEvent(e)
else:
painter = QPainter(self)
@ -396,30 +435,37 @@ class TextBase(QLabel):
self._elided_text)
class Text(TextBase):
class _Text(TextBase):
"""Text displayed in the statusbar."""
"""Text displayed in the statusbar.
old_text = ''
Attributes:
_old_text: The text displayed before the temporary error message.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._old_text = ''
def set_error(self, text):
"""Display an error message and save current text in old_text."""
self.old_text = self.text()
self._old_text = self.text()
self.setText(text)
def clear_error(self):
"""Clear a displayed error message."""
self.setText(self.old_text)
self.setText(self._old_text)
class KeyString(TextBase):
class _KeyString(TextBase):
"""Keychain string displayed in the statusbar."""
pass
class Percentage(TextBase):
class _Percentage(TextBase):
"""Reading percentage displayed in the statusbar."""
@ -434,15 +480,20 @@ class Percentage(TextBase):
self.setText('[{:2}%]'.format(y))
class Url(TextBase):
class _Url(TextBase):
"""URL displayed in the statusbar."""
"""URL displayed in the statusbar.
_old_url = None
_old_urltype = None
_urltype = None # 'normal', 'ok', 'error', 'warn, 'hover'
Attributes:
_old_url: The URL displayed before the hover URL.
_old_urltype: The type of the URL displayed before the hover URL.
_urltype: The current URL type. One of normal/ok/error/warn/hover.
Accessed via the urltype property.
_STYLESHEET: The stylesheet template.
_stylesheet = """
"""
_STYLESHEET = """
QLabel#Url[urltype="normal"] {{
{color[statusbar.url.fg]}
}}
@ -468,7 +519,10 @@ class Url(TextBase):
"""Override TextBase::__init__ to elide in the middle by default."""
super().__init__(bar, elidemode)
self.setObjectName(self.__class__.__name__)
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
self._urltype = None
self._old_urltype = None
self._old_url = None
@pyqtProperty(str)
def urltype(self):
@ -480,7 +534,7 @@ class Url(TextBase):
def urltype(self, val):
"""Setter for self.urltype, so it can be used as Qt property."""
self._urltype = val
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
self.setStyleSheet(config.get_stylesheet(self._STYLESHEET))
@pyqtSlot(bool)
def on_loading_finished(self, ok):

View File

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