Initial implementation of new messages

This commit is contained in:
Florian Bruhin 2016-09-14 20:52:32 +02:00
parent 5bef7dc74c
commit f16b96aa28
40 changed files with 389 additions and 584 deletions

View File

@ -180,8 +180,7 @@ def _process_args(args):
try: try:
config_obj.set('temp', sect, opt, val) config_obj.set('temp', sect, opt, val)
except (configexc.Error, configparser.Error) as e: except (configexc.Error, configparser.Error) as e:
message.error('current', "set: {} - {}".format( message.error("set: {} - {}".format(e.__class__.__name__, e))
e.__class__.__name__, e))
if not args.override_restore: if not args.override_restore:
_load_session(args.session) _load_session(args.session)
@ -216,10 +215,9 @@ def _load_session(name):
try: try:
session_manager.load(name) session_manager.load(name)
except sessions.SessionNotFoundError: except sessions.SessionNotFoundError:
message.error('current', "Session {} not found!".format(name)) message.error("Session {} not found!".format(name))
except sessions.SessionError as e: except sessions.SessionError as e:
message.error('current', "Failed to load session {}: {}".format( message.error("Failed to load session {}: {}".format(name, e))
name, e))
try: try:
del state_config['general']['session'] del state_config['general']['session']
except KeyError: except KeyError:
@ -271,8 +269,8 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
try: try:
url = urlutils.fuzzy_url(cmd, cwd, relative=True) url = urlutils.fuzzy_url(cmd, cwd, relative=True)
except urlutils.InvalidUrlError as e: except urlutils.InvalidUrlError as e:
message.error('current', "Error in startup argument '{}': " message.error("Error in startup argument '{}': {}".format(
"{}".format(cmd, e)) cmd, e))
else: else:
background = open_target in ['tab-bg', 'tab-bg-silent'] background = open_target in ['tab-bg', 'tab-bg-silent']
tabbed_browser.tabopen(url, background=background, tabbed_browser.tabopen(url, background=background,
@ -301,8 +299,7 @@ def _open_startpage(win_id=None):
try: try:
url = urlutils.fuzzy_url(urlstr, do_search=False) url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.InvalidUrlError as e: except urlutils.InvalidUrlError as e:
message.error('current', "Error when opening startpage: " message.error("Error when opening startpage: {}".format(e))
"{}".format(e))
tabbed_browser.tabopen(QUrl('about:blank')) tabbed_browser.tabopen(QUrl('about:blank'))
else: else:
tabbed_browser.tabopen(url) tabbed_browser.tabopen(url)
@ -370,6 +367,9 @@ def _init_modules(args, crash_handler):
crash_handler: The CrashHandler instance. crash_handler: The CrashHandler instance.
""" """
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
log.init.debug("Initializing messages...")
message.init()
log.init.debug("Initializing save manager...") log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp) save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager) objreg.register('save-manager', save_manager)

View File

@ -174,12 +174,10 @@ class HostBlocker:
args = objreg.get('args') args = objreg.get('args')
if (config.get('content', 'host-block-lists') is not None and if (config.get('content', 'host-block-lists') is not None and
args.basedir is None): args.basedir is None):
message.info('current', message.info("Run :adblock-update to get adblock lists.")
"Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker') @cmdutils.register(instance='host-blocker')
@cmdutils.argument('win_id', win_id=True) def adblock_update(self):
def adblock_update(self, win_id):
"""Update the adblock block lists. """Update the adblock block lists.
This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded
@ -201,12 +199,12 @@ class HostBlocker:
try: try:
fileobj = open(url.path(), 'rb') fileobj = open(url.path(), 'rb')
except OSError as e: except OSError as e:
message.error(win_id, "adblock: Error while reading {}: " message.error("adblock: Error while reading {}: {}".format(
"{}".format(url.path(), e.strerror)) url.path(), e.strerror))
continue continue
download = FakeDownload(fileobj) download = FakeDownload(fileobj)
self._in_progress.append(download) self._in_progress.append(download)
self.on_download_finished(download, win_id) self.on_download_finished(download)
else: else:
fobj = io.BytesIO() fobj = io.BytesIO()
fobj.name = 'adblock: ' + url.host() fobj.name = 'adblock: ' + url.host()
@ -215,8 +213,7 @@ class HostBlocker:
auto_remove=True) auto_remove=True)
self._in_progress.append(download) self._in_progress.append(download)
download.finished.connect( download.finished.connect(
functools.partial(self.on_download_finished, download, functools.partial(self.on_download_finished, download))
win_id))
def _merge_file(self, byte_io): def _merge_file(self, byte_io):
"""Read and merge host files. """Read and merge host files.
@ -233,8 +230,8 @@ class HostBlocker:
f = get_fileobj(byte_io) f = get_fileobj(byte_io)
except (OSError, UnicodeDecodeError, zipfile.BadZipFile, except (OSError, UnicodeDecodeError, zipfile.BadZipFile,
zipfile.LargeZipFile) as e: zipfile.LargeZipFile) as e:
message.error('current', "adblock: Error while reading {}: {} - " message.error("adblock: Error while reading {}: {} - {}".format(
"{}".format(byte_io.name, e.__class__.__name__, e)) byte_io.name, e.__class__.__name__, e))
return return
for line in f: for line in f:
line_count += 1 line_count += 1
@ -262,16 +259,16 @@ class HostBlocker:
self._blocked_hosts.add(host) self._blocked_hosts.add(host)
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count)) log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
if error_count > 0: if error_count > 0:
message.error('current', "adblock: {} read errors for {}".format( message.error("adblock: {} read errors for {}".format(error_count,
error_count, byte_io.name)) byte_io.name))
def on_lists_downloaded(self, win_id): def on_lists_downloaded(self):
"""Install block lists after files have been downloaded.""" """Install block lists after files have been downloaded."""
with open(self._local_hosts_file, 'w', encoding='utf-8') as f: with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
for host in sorted(self._blocked_hosts): for host in sorted(self._blocked_hosts):
f.write(host + '\n') f.write(host + '\n')
message.info(win_id, "adblock: Read {} hosts from {} sources." message.info("adblock: Read {} hosts from {} sources.".format(
.format(len(self._blocked_hosts), self._done_count)) len(self._blocked_hosts), self._done_count))
@config.change_filter('content', 'host-block-lists') @config.change_filter('content', 'host-block-lists')
def on_config_changed(self): def on_config_changed(self):
@ -285,12 +282,11 @@ class HostBlocker:
except OSError as e: except OSError as e:
log.misc.exception("Failed to delete hosts file: {}".format(e)) log.misc.exception("Failed to delete hosts file: {}".format(e))
def on_download_finished(self, download, win_id): def on_download_finished(self, download):
"""Check if all downloads are finished and if so, trigger reading. """Check if all downloads are finished and if so, trigger reading.
Arguments: Arguments:
download: The finished DownloadItem. download: The finished DownloadItem.
win_id: The window ID in which :adblock-update was called
""" """
self._in_progress.remove(download) self._in_progress.remove(download)
if download.successful: if download.successful:
@ -301,6 +297,6 @@ class HostBlocker:
download.fileobj.close() download.fileobj.close()
if not self._in_progress: if not self._in_progress:
try: try:
self.on_lists_downloaded(win_id) self.on_lists_downloaded()
except OSError: except OSError:
log.misc.exception("Failed to write host block list!") log.misc.exception("Failed to write host block list!")

View File

@ -620,7 +620,7 @@ class AbstractTab(QWidget):
if not url.isValid(): if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked") msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(self.win_id, msg) message.error(msg)
self.data.open_target = usertypes.ClickTarget.normal self.data.open_target = usertypes.ClickTarget.normal
return False return False

View File

@ -303,7 +303,7 @@ class CommandDispatcher:
except urlutils.InvalidUrlError as e: except urlutils.InvalidUrlError as e:
# We don't use cmdexc.CommandError here as this can be # We don't use cmdexc.CommandError here as this can be
# called async from edit_url # called async from edit_url
message.error(self._win_id, str(e)) message.error(str(e))
return None return None
def _parse_url_input(self, url): def _parse_url_input(self, url):
@ -719,7 +719,7 @@ class CommandDispatcher:
caret = self._current_widget().caret caret = self._current_widget().caret
s = caret.selection() s = caret.selection()
if not caret.has_selection() or not s: if not caret.has_selection() or not s:
message.info(self._win_id, "Nothing to yank") message.info("Nothing to yank")
return return
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Invalid value {!r} for `what'.".format(what)) raise ValueError("Invalid value {!r} for `what'.".format(what))
@ -732,10 +732,9 @@ class CommandDispatcher:
utils.set_clipboard(s, selection=sel) utils.set_clipboard(s, selection=sel)
if what != 'selection': if what != 'selection':
message.info(self._win_id, "Yanked {} to {}: {}".format( message.info("Yanked {} to {}: {}".format(what, target, s))
what, target, s))
else: else:
message.info(self._win_id, "{} {} yanked to {}".format( message.info("{} {} yanked to {}".format(
len(s), "char" if len(s) == 1 else "chars", target)) len(s), "char" if len(s) == 1 else "chars", target))
if not keep: if not keep:
modeman.maybe_leave(self._win_id, KeyMode.caret, modeman.maybe_leave(self._win_id, KeyMode.caret,
@ -754,7 +753,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(count) perc = tab.zoom.offset(count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
message.info(self._win_id, "Zoom level: {}%".format(perc)) message.info("Zoom level: {}%".format(perc))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -769,7 +768,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(-count) perc = tab.zoom.offset(-count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
message.info(self._win_id, "Zoom level: {}%".format(perc)) message.info("Zoom level: {}%".format(perc))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -793,7 +792,7 @@ class CommandDispatcher:
tab.zoom.set_factor(float(level) / 100) tab.zoom.set_factor(float(level) / 100)
except ValueError: except ValueError:
raise cmdexc.CommandError("Can't zoom {}%!".format(level)) raise cmdexc.CommandError("Can't zoom {}%!".format(level))
message.info(self._win_id, "Zoom level: {}%".format(level)) message.info("Zoom level: {}%".format(level))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, left=False, right=False): def tab_only(self, left=False, right=False):
@ -1073,8 +1072,7 @@ class CommandDispatcher:
self._run_userscript(cmd, *args, verbose=verbose) self._run_userscript(cmd, *args, verbose=verbose)
else: else:
cmd = os.path.expanduser(cmd) cmd = os.path.expanduser(cmd)
proc = guiprocess.GUIProcess(self._win_id, what='command', proc = guiprocess.GUIProcess(what='command', verbose=verbose,
verbose=verbose,
parent=self._tabbed_browser) parent=self._tabbed_browser)
if detach: if detach:
proc.start_detached(cmd, args) proc.start_detached(cmd, args)
@ -1211,8 +1209,7 @@ class CommandDispatcher:
raise cmdexc.CommandError(str(e)) raise cmdexc.CommandError(str(e))
else: else:
msg = "Bookmarked {}!" if was_added else "Removed bookmark {}!" msg = "Bookmarked {}!" if was_added else "Removed bookmark {}!"
message.info(self._win_id, message.info(msg.format(url.toDisplayString()))
msg.format(url.toDisplayString()))
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0) maxsplit=0)
@ -1304,9 +1301,8 @@ class CommandDispatcher:
mhtml_: Download the current page and all assets as mhtml file. mhtml_: Download the current page and all assets as mhtml file.
""" """
if dest_old is not None: if dest_old is not None:
message.warning(self._win_id, message.warning(":download [url] [dest] is deprecated - use "
":download [url] [dest] is deprecated - use" ":download --dest [dest] [url]")
" download --dest [dest] [url]")
if dest is not None: if dest is not None:
raise cmdexc.CommandError("Can't give two destinations for the" raise cmdexc.CommandError("Can't give two destinations for the"
" download.") " download.")
@ -1379,7 +1375,7 @@ class CommandDispatcher:
try: try:
current_url = self._current_url() current_url = self._current_url()
except cmdexc.CommandError as e: except cmdexc.CommandError as e:
message.error(self._win_id, str(e)) message.error(str(e))
return return
new_tab = self._tabbed_browser.tabopen(explicit=True) new_tab = self._tabbed_browser.tabopen(explicit=True)
new_tab.set_html(highlighted, current_url) new_tab.set_html(highlighted, current_url)
@ -1404,10 +1400,9 @@ class CommandDispatcher:
with open(dest, 'w', encoding='utf-8') as f: with open(dest, 'w', encoding='utf-8') as f:
f.write(data) f.write(data)
except OSError as e: except OSError as e:
message.error(self._win_id, message.error('Could not write page: {}'.format(e))
'Could not write page: {}'.format(e))
else: else:
message.info(self._win_id, "Dumped page to {}.".format(dest)) message.info("Dumped page to {}.".format(dest))
tab.dump_async(callback, plain=plain) tab.dump_async(callback, plain=plain)
@ -1477,14 +1472,14 @@ class CommandDispatcher:
def _open_editor_cb(self, elem): def _open_editor_cb(self, elem):
"""Open editor after the focus elem was found in open_editor.""" """Open editor after the focus elem was found in open_editor."""
if elem is None: if elem is None:
message.error(self._win_id, "No element focused!") message.error("No element focused!")
return return
if not elem.is_editable(strict=True): if not elem.is_editable(strict=True):
message.error(self._win_id, "Focused element is not editable!") message.error("Focused element is not editable!")
return return
text = elem.text(use_js=True) text = elem.text(use_js=True)
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser) ed = editor.ExternalEditor(self._tabbed_browser)
ed.editing_finished.connect(functools.partial( ed.editing_finished.connect(functools.partial(
self.on_editing_finished, elem)) self.on_editing_finished, elem))
ed.edit(text) ed.edit(text)
@ -1539,12 +1534,12 @@ class CommandDispatcher:
def _insert_text_cb(elem): def _insert_text_cb(elem):
if elem is None: if elem is None:
message.error(self._win_id, "No element focused!") message.error("No element focused!")
return return
try: try:
elem.insert_text(text) elem.insert_text(text)
except webelem.Error as e: except webelem.Error as e:
message.error(self._win_id, str(e)) message.error(str(e))
return return
tab.elements.find_focused(_insert_text_cb) tab.elements.find_focused(_insert_text_cb)
@ -1571,22 +1566,21 @@ class CommandDispatcher:
def single_cb(elem): def single_cb(elem):
"""Click a single element.""" """Click a single element."""
if elem is None: if elem is None:
message.error(self._win_id, "No element found!") message.error("No element found!")
return return
try: try:
elem.click(target) elem.click(target)
except webelem.Error as e: except webelem.Error as e:
message.error(self._win_id, str(e)) message.error(str(e))
return return
# def multiple_cb(elems): # def multiple_cb(elems):
# """Click multiple elements (with only one expected).""" # """Click multiple elements (with only one expected)."""
# if not elems: # if not elems:
# message.error(self._win_id, "No element found!") # message.error("No element found!")
# return # return
# elif len(elems) != 1: # elif len(elems) != 1:
# message.error(self._win_id, "{} elements found!".format( # message.error("{} elements found!".format(len(elems)))
# len(elems)))
# return # return
# elems[0].click(target) # elems[0].click(target)
@ -1616,14 +1610,11 @@ class CommandDispatcher:
if found: if found:
# Check if the scroll position got smaller and show info. # Check if the scroll position got smaller and show info.
if not going_up and tab.scroller.pos_px().y() < old_scroll_pos.y(): if not going_up and tab.scroller.pos_px().y() < old_scroll_pos.y():
message.info(self._win_id, "Search hit BOTTOM, continuing " message.info("Search hit BOTTOM, continuing at TOP")
"at TOP", immediately=True)
elif going_up and tab.scroller.pos_px().y() > old_scroll_pos.y(): elif going_up and tab.scroller.pos_px().y() > old_scroll_pos.y():
message.info(self._win_id, "Search hit TOP, continuing at " message.info("Search hit TOP, continuing at BOTTOM")
"BOTTOM", immediately=True)
else: else:
message.warning(self._win_id, "Text '{}' not found on " message.warning("Text '{}' not found on page!".format(text))
"page!".format(text), immediately=True)
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0) maxsplit=0)
@ -1941,7 +1932,7 @@ class CommandDispatcher:
# BrowserPage.javaScriptConsoleMessage(), but # BrowserPage.javaScriptConsoleMessage(), but
# distinguishing between :jseval errors and errors from the # distinguishing between :jseval errors and errors from the
# webpage is not trivial... # webpage is not trivial...
message.info(self._win_id, 'No output or error') message.info('No output or error')
else: else:
# The output can be a string, number, dict, array, etc. But # The output can be a string, number, dict, array, etc. But
# *don't* output too much data, as this will make # *don't* output too much data, as this will make
@ -1949,7 +1940,7 @@ class CommandDispatcher:
out = str(out) out = str(out)
if len(out) > 5000: if len(out) > 5000:
out = out[:5000] + ' [...trimmed...]' out = out[:5000] + ' [...trimmed...]'
message.info(self._win_id, out) message.info(out)
widget = self._current_widget() widget = self._current_widget()
widget.run_js_async(js_code, callback=jseval_cb, world=world) widget.run_js_async(js_code, callback=jseval_cb, world=world)
@ -2016,7 +2007,7 @@ class CommandDispatcher:
old_url = self._current_url().toString() old_url = self._current_url().toString()
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser) ed = editor.ExternalEditor(self._tabbed_browser)
# Passthrough for openurl args (e.g. -t, -b, -w) # Passthrough for openurl args (e.g. -t, -b, -w)
ed.editing_finished.connect(functools.partial( ed.editing_finished.connect(functools.partial(

View File

@ -238,7 +238,7 @@ class HintActions:
msg = "Yanked URL to {}: {}".format( msg = "Yanked URL to {}: {}".format(
"primary selection" if sel else "clipboard", "primary selection" if sel else "clipboard",
urlstr) urlstr)
message.info(self._win_id, msg) message.info(msg)
def run_cmd(self, url, context): def run_cmd(self, url, context):
"""Run the command based on a hint URL. """Run the command based on a hint URL.
@ -411,7 +411,7 @@ class HintManager(QObject):
try: try:
return self._word_hinter.hint(elems) return self._word_hinter.hint(elems)
except HintingError as e: except HintingError as e:
message.error(self._win_id, str(e), immediately=True) message.error(str(e))
# falls back on letter hints # falls back on letter hints
if hint_mode == 'number': if hint_mode == 'number':
chars = '0123456789' chars = '0123456789'
@ -570,7 +570,7 @@ class HintManager(QObject):
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True) filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
elems = [e for e in elems if filterfunc(e)] elems = [e for e in elems if filterfunc(e)]
if not elems: if not elems:
message.error(self._win_id, "No elements found.", immediately=True) message.error("No elements found.")
return return
strings = self._hint_strings(elems) strings = self._hint_strings(elems)
log.hints.debug("hints: {}".format(', '.join(strings))) log.hints.debug("hints: {}".format(', '.join(strings)))
@ -662,8 +662,8 @@ class HintManager(QObject):
raise cmdexc.CommandError("No WebView available yet!") raise cmdexc.CommandError("No WebView available yet!")
if (tab.backend == usertypes.Backend.QtWebEngine and if (tab.backend == usertypes.Backend.QtWebEngine and
target == Target.download): target == Target.download):
message.error(self._win_id, "The download target is not available " message.error("The download target is not available yet with "
"yet with QtWebEngine.", immediately=True) "QtWebEngine.")
return return
mode_manager = objreg.get('mode-manager', scope='window', mode_manager = objreg.get('mode-manager', scope='window',
@ -849,9 +849,7 @@ class HintManager(QObject):
elem = self._context.labels[keystr].elem elem = self._context.labels[keystr].elem
if not elem.has_frame(): if not elem.has_frame():
message.error(self._win_id, message.error("This element has no webframe.")
"This element has no webframe.",
immediately=True)
return return
if self._context.target in elem_handlers: if self._context.target in elem_handlers:
@ -860,9 +858,7 @@ class HintManager(QObject):
elif self._context.target in url_handlers: elif self._context.target in url_handlers:
url = elem.resolve_url(self._context.baseurl) url = elem.resolve_url(self._context.baseurl)
if url is None: if url is None:
message.error(self._win_id, message.error("No suitable link found for this element.")
"No suitable link found for this element.",
immediately=True)
return return
handler = functools.partial(url_handlers[self._context.target], handler = functools.partial(url_handlers[self._context.target],
url, self._context) url, self._context)
@ -882,7 +878,7 @@ class HintManager(QObject):
try: try:
handler() handler()
except HintingError as e: except HintingError as e:
message.error(self._win_id, str(e), immediately=True) message.error(str(e))
@cmdutils.register(instance='hintmanager', scope='tab', hide=True, @cmdutils.register(instance='hintmanager', scope='tab', hide=True,
modes=[usertypes.KeyMode.hint]) modes=[usertypes.KeyMode.hint])

View File

@ -125,7 +125,7 @@ class MouseEventFilter(QObject):
if factor < 0: if factor < 0:
return False return False
perc = int(100 * factor) perc = int(100 * factor)
message.info(self._tab.win_id, "Zoom level: {}%".format(perc)) message.info("Zoom level: {}%".format(perc))
self._tab.zoom.set_factor(factor) self._tab.zoom.set_factor(factor)
return False return False
@ -191,15 +191,13 @@ class MouseEventFilter(QObject):
if self._tab.history.can_go_back(): if self._tab.history.can_go_back():
self._tab.history.back() self._tab.history.back()
else: else:
message.error(self._tab.win_id, "At beginning of history.", message.error("At beginning of history.")
immediately=True)
elif e.button() in [Qt.XButton2, Qt.RightButton]: elif e.button() in [Qt.XButton2, Qt.RightButton]:
# Forward button on mice which have it, or rocker gesture # Forward button on mice which have it, or rocker gesture
if self._tab.history.can_go_forward(): if self._tab.history.can_go_forward():
self._tab.history.forward() self._tab.history.forward()
else: else:
message.error(self._tab.win_id, "At end of history.", message.error("At end of history.")
immediately=True)
def _mousepress_opentarget(self, e): def _mousepress_opentarget(self, e):
"""Set the open target when something was clicked. """Set the open target when something was clicked.

View File

@ -116,11 +116,11 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
word = 'prev' if prev else 'forward' word = 'prev' if prev else 'forward'
if elem is None: if elem is None:
message.error(win_id, "No {} links found!".format(word)) message.error("No {} links found!".format(word))
return return
url = elem.resolve_url(baseurl) url = elem.resolve_url(baseurl)
if url is None: if url is None:
message.error(win_id, "No {} links found!".format(word)) message.error("No {} links found!".format(word))
return return
qtutils.ensure_valid(url) qtutils.ensure_valid(url)

View File

@ -245,8 +245,8 @@ def qute_help(url):
else: else:
urlpath = urlpath.lstrip('/') urlpath = urlpath.lstrip('/')
if not docutils.docs_up_to_date(urlpath): if not docutils.docs_up_to_date(urlpath):
message.error('current', "Your documentation is outdated! Please " message.error("Your documentation is outdated! Please re-run "
"re-run scripts/asciidoc2html.py.") "scripts/asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath) path = 'html/doc/{}'.format(urlpath)
if urlpath.endswith('.png'): if urlpath.endswith('.png'):
return 'image/png', utils.read_file(path, binary=True) return 'image/png', utils.read_file(path, binary=True)

View File

@ -155,7 +155,7 @@ class QuickmarkManager(UrlMarkManager):
try: try:
key, url = line.rsplit(maxsplit=1) key, url = line.rsplit(maxsplit=1)
except ValueError: except ValueError:
message.error('current', "Invalid quickmark '{}'".format(line)) message.error("Invalid quickmark '{}'".format(line))
else: else:
self.marks[key] = url self.marks[key] = url
@ -167,7 +167,7 @@ class QuickmarkManager(UrlMarkManager):
url: The quickmark url as a QUrl. url: The quickmark url as a QUrl.
""" """
if not url.isValid(): if not url.isValid():
urlutils.invalid_url_error(win_id, url, "save quickmark") urlutils.invalid_url_error(url, "save quickmark")
return return
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
message.ask_async( message.ask_async(
@ -190,10 +190,10 @@ class QuickmarkManager(UrlMarkManager):
# We don't raise cmdexc.CommandError here as this can be called async # We don't raise cmdexc.CommandError here as this can be called async
# via prompt_save. # via prompt_save.
if not name: if not name:
message.error(win_id, "Can't set mark with empty name!") message.error("Can't set mark with empty name!")
return return
if not url: if not url:
message.error(win_id, "Can't set mark with empty URL!") message.error("Can't set mark with empty URL!")
return return
def set_mark(): def set_mark():

View File

@ -568,7 +568,7 @@ class DownloadItem(QObject):
args.append(filename) args.append(filename)
log.downloads.debug("Opening {} with {}" log.downloads.debug("Opening {} with {}"
.format(filename, [cmd] + args)) .format(filename, [cmd] + args))
proc = guiprocess.GUIProcess(self._win_id, what='download') proc = guiprocess.GUIProcess(what='download')
proc.start_detached(cmd, args) proc.start_detached(cmd, args)
def set_filename(self, filename): def set_filename(self, filename):
@ -599,7 +599,6 @@ class DownloadItem(QObject):
# may be set for XDG_DOWNLOAD_DIR # may be set for XDG_DOWNLOAD_DIR
if self._filename is None: if self._filename is None:
message.error( message.error(
self._win_id,
"XDG_DOWNLOAD_DIR points to a relative path - please check" "XDG_DOWNLOAD_DIR points to a relative path - please check"
" your ~/.config/user-dirs.dirs. The download is saved in" " your ~/.config/user-dirs.dirs. The download is saved in"
" your home directory.", " your home directory.",
@ -843,7 +842,7 @@ class DownloadManager(QObject):
The created DownloadItem. The created DownloadItem.
""" """
if not url.isValid(): if not url.isValid():
urlutils.invalid_url_error(self._win_id, url, "start download") urlutils.invalid_url_error(url, "start download")
return return
req = QNetworkRequest(url) req = QNetworkRequest(url)
return self.get_request(req, **kwargs) return self.get_request(req, **kwargs)
@ -1005,7 +1004,7 @@ class DownloadManager(QObject):
fobj = tmp_manager.get_tmpfile(suggested_filename) fobj = tmp_manager.get_tmpfile(suggested_filename)
except OSError as exc: except OSError as exc:
msg = "Download error: {}".format(exc) msg = "Download error: {}".format(exc)
message.error(self._win_id, msg) message.error(msg)
download.cancel() download.cancel()
return return
download.finished.connect( download.finished.connect(
@ -1056,7 +1055,7 @@ class DownloadManager(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def _on_error(self, msg): def _on_error(self, msg):
"""Display error message on download errors.""" """Display error message on download errors."""
message.error(self._win_id, "Download error: {}".format(msg)) message.error("Download error: {}".format(msg))
def has_downloads_with_nam(self, nam): def has_downloads_with_nam(self, nam):
"""Check if the DownloadManager has any downloads with the given QNAM. """Check if the DownloadManager has any downloads with the given QNAM.

View File

@ -447,11 +447,10 @@ class _Downloader:
with open(self.dest, 'wb') as file_output: with open(self.dest, 'wb') as file_output:
self.writer.write_to(file_output) self.writer.write_to(file_output)
except OSError as error: except OSError as error:
message.error(self._win_id, message.error("Could not save file: {}".format(error))
"Could not save file: {}".format(error))
return return
log.downloads.debug("File successfully written.") log.downloads.debug("File successfully written.")
message.info(self._win_id, "Page saved as {}".format(self.dest)) message.info("Page saved as {}".format(self.dest))
def _collect_zombies(self): def _collect_zombies(self):
"""Collect done downloads and add their data to the MHTML file. """Collect done downloads and add their data to the MHTML file.
@ -528,8 +527,7 @@ def start_download_checked(dest, tab):
# saving the file anyway. # saving the file anyway.
if not os.path.isdir(os.path.dirname(path)): if not os.path.isdir(os.path.dirname(path)):
folder = os.path.dirname(path) folder = os.path.dirname(path)
message.error(tab.win_id, message.error("Directory {} does not exist.".format(folder))
"Directory {} does not exist.".format(folder))
return return
if not os.path.isfile(path): if not os.path.isfile(path):

View File

@ -299,8 +299,7 @@ class NetworkManager(QNetworkAccessManager):
for err in errors: for err in errors:
# FIXME we might want to use warn here (non-fatal error) # FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114 # https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id, 'SSL error: {}'.format( message.error('SSL error: {}'.format(err.errorString()))
err.errorString()))
reply.ignoreSslErrors() reply.ignoreSslErrors()
self._accepted_ssl_errors[host_tpl] += errors self._accepted_ssl_errors[host_tpl] += errors

View File

@ -77,13 +77,13 @@ class JSBridge(QObject):
# https://github.com/The-Compiler/qutebrowser/issues/727 # https://github.com/The-Compiler/qutebrowser/issues/727
if ((sectname, optname) == ('content', 'allow-javascript') and if ((sectname, optname) == ('content', 'allow-javascript') and
value == 'false'): value == 'false'):
message.error('current', "Refusing to disable javascript via " message.error("Refusing to disable javascript via qute:settings "
"qute:settings as it needs javascript support.") "as it needs javascript support.")
return return
try: try:
objreg.get('config').set('conf', sectname, optname, value) objreg.get('config').set('conf', sectname, optname, value)
except (configexc.Error, configparser.Error) as e: except (configexc.Error, configparser.Error) as e:
message.error('current', e) message.error(e)
@qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit) @qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit)

View File

@ -249,8 +249,8 @@ class BrowserPage(QWebPage):
def on_print_requested(self, frame): def on_print_requested(self, frame):
"""Handle printing when requested via javascript.""" """Handle printing when requested via javascript."""
if not qtutils.check_print_compat(): if not qtutils.check_print_compat():
message.error(self._win_id, "Printing on Qt < 5.3.0 on Windows is " message.error("Printing on Qt < 5.3.0 on Windows is broken, "
"broken, please upgrade!", immediately=True) "please upgrade!")
return return
printdiag = QPrintDialog() printdiag = QPrintDialog()
printdiag.setAttribute(Qt.WA_DeleteOnClose) printdiag.setAttribute(Qt.WA_DeleteOnClose)

View File

@ -174,8 +174,8 @@ class Command:
"backend.".format(self.name, self.backend.name)) "backend.".format(self.name, self.backend.name))
if self.deprecated: if self.deprecated:
message.warning(win_id, '{} is deprecated - {}'.format( message.warning('{} is deprecated - {}'.format(self.name,
self.name, self.deprecated)) self.deprecated))
def _check_func(self): def _check_func(self):
"""Make sure the function parameters don't violate any rules.""" """Make sure the function parameters don't violate any rules."""
@ -507,7 +507,7 @@ class Command:
try: try:
self.namespace = self.parser.parse_args(args) self.namespace = self.parser.parse_args(args)
except argparser.ArgumentParserError as e: except argparser.ArgumentParserError as e:
message.error(win_id, '{}: {}'.format(self.name, e), message.error('{}: {}'.format(self.name, e),
stack=traceback.format_exc()) stack=traceback.format_exc())
return return
except argparser.ArgumentParserExit as e: except argparser.ArgumentParserExit as e:

View File

@ -320,8 +320,7 @@ class CommandRunner(QObject):
try: try:
self.run(text, count) self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(self._win_id, e, immediately=True, message.error(e, stack=traceback.format_exc())
stack=traceback.format_exc())
@pyqtSlot(str, int) @pyqtSlot(str, int)
def run_safely_init(self, text, count=None): def run_safely_init(self, text, count=None):
@ -333,4 +332,4 @@ class CommandRunner(QObject):
try: try:
self.run(text, count) self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(self._win_id, e, stack=traceback.format_exc()) message.error(e, stack=traceback.format_exc())

View File

@ -84,7 +84,6 @@ class _BaseUserscriptRunner(QObject):
Attributes: Attributes:
_filepath: The path of the file/FIFO which is being read. _filepath: The path of the file/FIFO which is being read.
_proc: The GUIProcess which is being executed. _proc: The GUIProcess which is being executed.
_win_id: The window ID this runner is associated with.
_cleaned_up: Whether temporary files were cleaned up. _cleaned_up: Whether temporary files were cleaned up.
_text_stored: Set when the page text was stored async. _text_stored: Set when the page text was stored async.
_html_stored: Set when the page html was stored async. _html_stored: Set when the page html was stored async.
@ -99,10 +98,9 @@ class _BaseUserscriptRunner(QObject):
got_cmd = pyqtSignal(str) got_cmd = pyqtSignal(str)
finished = pyqtSignal() finished = pyqtSignal()
def __init__(self, win_id, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._cleaned_up = False self._cleaned_up = False
self._win_id = win_id
self._filepath = None self._filepath = None
self._proc = None self._proc = None
self._env = {} self._env = {}
@ -151,7 +149,7 @@ class _BaseUserscriptRunner(QObject):
self._env['QUTE_FIFO'] = self._filepath self._env['QUTE_FIFO'] = self._filepath
if env is not None: if env is not None:
self._env.update(env) self._env.update(env)
self._proc = guiprocess.GUIProcess(self._win_id, 'userscript', self._proc = guiprocess.GUIProcess('userscript',
additional_env=self._env, additional_env=self._env,
verbose=verbose, parent=self) verbose=verbose, parent=self)
self._proc.finished.connect(self.on_proc_finished) self._proc.finished.connect(self.on_proc_finished)
@ -175,8 +173,7 @@ class _BaseUserscriptRunner(QObject):
except OSError as e: except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
message.error( message.error("Failed to delete tempfile {} ({})!".format(
self._win_id, "Failed to delete tempfile {} ({})!".format(
fn, e)) fn, e))
self._filepath = None self._filepath = None
self._proc = None self._proc = None
@ -223,8 +220,8 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
_reader: The _QtFIFOReader instance. _reader: The _QtFIFOReader instance.
""" """
def __init__(self, win_id, parent=None): def __init__(self, parent=None):
super().__init__(win_id, parent) super().__init__(parent)
self._reader = None self._reader = None
def prepare_run(self, *args, **kwargs): def prepare_run(self, *args, **kwargs):
@ -242,8 +239,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
# pylint: disable=no-member,useless-suppression # pylint: disable=no-member,useless-suppression
os.mkfifo(self._filepath) os.mkfifo(self._filepath)
except OSError as e: except OSError as e:
message.error(self._win_id, message.error("Error while creating FIFO: {}".format(e))
"Error while creating FIFO: {}".format(e))
return return
self._reader = _QtFIFOReader(self._filepath) self._reader = _QtFIFOReader(self._filepath)
@ -315,8 +311,7 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
handle.close() handle.close()
self._filepath = handle.name self._filepath = handle.name
except OSError as e: except OSError as e:
message.error(self._win_id, "Error while creating tempfile: " message.error("Error while creating tempfile: {}".format(e))
"{}".format(e))
return return
@ -348,9 +343,9 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
commandrunner = runners.CommandRunner(win_id, parent=tabbed_browser) commandrunner = runners.CommandRunner(win_id, parent=tabbed_browser)
if os.name == 'posix': if os.name == 'posix':
runner = _POSIXUserscriptRunner(win_id, tabbed_browser) runner = _POSIXUserscriptRunner(tabbed_browser)
elif os.name == 'nt': # pragma: no cover elif os.name == 'nt': # pragma: no cover
runner = _WindowsUserscriptRunner(win_id, tabbed_browser) runner = _WindowsUserscriptRunner(tabbed_browser)
else: # pragma: no cover else: # pragma: no cover
raise UnsupportedError raise UnsupportedError

View File

@ -816,8 +816,7 @@ class ConfigManager(QObject):
if print_: if print_:
with self._handle_config_error(): with self._handle_config_error():
val = self.get(section_, option, transformed=False) val = self.get(section_, option, transformed=False)
message.info(win_id, "{} {} = {}".format( message.info("{} {} = {}".format(section_, option, val))
section_, option, val), immediately=True)
def set(self, layer, sectname, optname, value, validate=True): def set(self, layer, sectname, optname, value, validate=True):
"""Set an option. """Set an option.

View File

@ -1069,22 +1069,6 @@ def data(readonly=False):
SettingValue(typ.QssColor(), 'black'), SettingValue(typ.QssColor(), 'black'),
"Background color of the statusbar."), "Background color of the statusbar."),
('statusbar.fg.error',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there was an error."),
('statusbar.bg.error',
SettingValue(typ.QssColor(), 'red'),
"Background color of the statusbar if there was an error."),
('statusbar.fg.warning',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there is a warning."),
('statusbar.bg.warning',
SettingValue(typ.QssColor(), 'darkorange'),
"Background color of the statusbar if there is a warning."),
('statusbar.fg.prompt', ('statusbar.fg.prompt',
SettingValue(typ.QssColor(), '${statusbar.fg}'), SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there is a prompt."), "Foreground color of the statusbar if there is a prompt."),
@ -1280,6 +1264,42 @@ def data(readonly=False):
SettingValue(typ.QssColor(), 'rgba(0, 0, 0, 80%)'), SettingValue(typ.QssColor(), 'rgba(0, 0, 0, 80%)'),
"Background color of the keyhint widget."), "Background color of the keyhint widget."),
('messages.fg.error',
SettingValue(typ.QssColor(), 'white'),
"Foreground color of an error message."),
('messages.bg.error',
SettingValue(typ.QssColor(), 'red'),
"Background color of an error message."),
('messages.border.error',
SettingValue(typ.QssColor(), '#bb0000'),
"Border color of an error message."),
('messages.fg.warning',
SettingValue(typ.QssColor(), 'white'),
"Foreground color a warning message."),
('messages.bg.warning',
SettingValue(typ.QssColor(), 'darkorange'),
"Background color of a warning message."),
('messages.border.warning',
SettingValue(typ.QssColor(), '#d47300'),
"Border color of an error message."),
('messages.fg.info',
SettingValue(typ.QssColor(), 'white'),
"Foreground color an info message."),
('messages.bg.info',
SettingValue(typ.QssColor(), 'black'),
"Background color of an info message."),
('messages.border.info',
SettingValue(typ.QssColor(), '#333333'),
"Border color of an info message."),
readonly=readonly readonly=readonly
)), )),
@ -1369,6 +1389,18 @@ def data(readonly=False):
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
"Font used in the keyhint widget."), "Font used in the keyhint widget."),
('messages.error',
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
"Font used for error messages."),
('messages.warning',
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
"Font used for warning messages."),
('messages.info',
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
"Font used for info messages."),
readonly=readonly readonly=readonly
)), )),
]) ])

View File

@ -152,9 +152,8 @@ class KeyConfigParser(QObject):
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True, @cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
no_replace_variables=True) no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('command', completion=usertypes.Completion.bind) @cmdutils.argument('command', completion=usertypes.Completion.bind)
def bind(self, key, win_id, command=None, *, mode='normal', force=False): def bind(self, key, command=None, *, mode='normal', force=False):
"""Bind a key to a command. """Bind a key to a command.
Args: Args:
@ -172,11 +171,10 @@ class KeyConfigParser(QObject):
if command is None: if command is None:
cmd = self.get_bindings_for(mode).get(key, None) cmd = self.get_bindings_for(mode).get(key, None)
if cmd is None: if cmd is None:
message.info(win_id, "{} is unbound in {} mode".format( message.info("{} is unbound in {} mode".format(key, mode))
key, mode))
else: else:
message.info(win_id, "{} is bound to '{}' in {} mode".format( message.info("{} is bound to '{}' in {} mode".format(key, cmd,
key, cmd, mode)) mode))
return return
mode = self._normalize_sectname(mode) mode = self._normalize_sectname(mode)

View File

@ -43,8 +43,7 @@ class CommandKeyParser(BaseKeyParser):
try: try:
self._commandrunner.run(cmdstr, count) self._commandrunner.run(cmdstr, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(self._win_id, e, immediately=True, message.error(str(e), stack=traceback.format_exc())
stack=traceback.format_exc())
class PassthroughKeyParser(CommandKeyParser): class PassthroughKeyParser(CommandKeyParser):

View File

@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
from qutebrowser.commands import runners, cmdutils from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils
from qutebrowser.mainwindow import tabbedbrowser from qutebrowser.mainwindow import tabbedbrowser, messageview
from qutebrowser.mainwindow.statusbar import bar from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget, completer from qutebrowser.completion import completionwidget, completer
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
@ -177,6 +177,7 @@ class MainWindow(QWidget):
partial_match=True) partial_match=True)
self._keyhint = keyhintwidget.KeyHintView(self.win_id, self) self._keyhint = keyhintwidget.KeyHintView(self.win_id, self)
self._messageview = messageview.MessageView(parent=self)
log.init.debug("Initializing modes...") log.init.debug("Initializing modes...")
modeman.init(self.win_id, self) modeman.init(self.win_id, self)
@ -195,8 +196,7 @@ class MainWindow(QWidget):
# When we're here the statusbar might not even really exist yet, so # When we're here the statusbar might not even really exist yet, so
# resizing will fail. Therefore, we use singleShot QTimers to make sure # resizing will fail. Therefore, we use singleShot QTimers to make sure
# we defer this until everything else is initialized. # we defer this until everything else is initialized.
QTimer.singleShot(0, self._connect_resize_completion) QTimer.singleShot(0, self._connect_resize_signals)
QTimer.singleShot(0, self._connect_resize_keyhint)
objreg.get('config').changed.connect(self.on_config_changed) objreg.get('config').changed.connect(self.on_config_changed)
objreg.get("app").new_window.emit(self) objreg.get("app").new_window.emit(self)
@ -303,15 +303,14 @@ class MainWindow(QWidget):
log.init.warning("Error while loading geometry.") log.init.warning("Error while loading geometry.")
self._set_default_geometry() self._set_default_geometry()
def _connect_resize_completion(self): def _connect_resize_signals(self):
"""Connect the resize_completion signal and resize it once.""" """Connect the resize signal and resize everything once."""
self._completion.resize_completion.connect(self.resize_completion) self._completion.resize_completion.connect(self.resize_completion)
self.resize_completion()
def _connect_resize_keyhint(self):
"""Connect the reposition_keyhint signal and resize it once."""
self._keyhint.reposition_keyhint.connect(self.reposition_keyhint) self._keyhint.reposition_keyhint.connect(self.reposition_keyhint)
self._messageview.reposition.connect(self._reposition_messageview)
self.resize_completion()
self.reposition_keyhint() self.reposition_keyhint()
self._reposition_messageview()
def _set_default_geometry(self): def _set_default_geometry(self):
"""Set some sensible default geometry.""" """Set some sensible default geometry."""
@ -360,9 +359,9 @@ class MainWindow(QWidget):
key_config.changed.connect(obj.on_keyconfig_changed) key_config.changed.connect(obj.on_keyconfig_changed)
# messages # messages
message_bridge.s_error.connect(status.disp_error) message.global_bridge.show_message.connect(
message_bridge.s_warning.connect(status.disp_warning) self._messageview.show_message)
message_bridge.s_info.connect(status.disp_temp_text)
message_bridge.s_set_text.connect(status.set_text) message_bridge.s_set_text.connect(status.set_text)
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text) message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
message_bridge.s_set_cmd_text.connect(cmd.set_cmd_text) message_bridge.s_set_cmd_text.connect(cmd.set_cmd_text)
@ -391,6 +390,23 @@ class MainWindow(QWidget):
completion_obj.on_clear_completion_selection) completion_obj.on_clear_completion_selection)
cmd.hide_completion.connect(completion_obj.hide) cmd.hide_completion.connect(completion_obj.hide)
def _get_overlay_position(self, height):
"""Get the position for a full-width overlay with the given height."""
status_position = config.get('ui', 'status-position')
if status_position == 'bottom':
top = self.height() - self.status.height() - height
top = qtutils.check_overflow(top, 'int', fatal=False)
topleft = QPoint(0, top)
bottomright = self.status.geometry().topRight()
elif status_position == 'top':
topleft = self.status.geometry().bottomLeft()
bottom = self.status.height() + height
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
bottomright = QPoint(self.width(), bottom)
else:
raise ValueError("Invalid position {}!".format(status_position))
return QRect(topleft, bottomright)
@pyqtSlot() @pyqtSlot()
def resize_completion(self): def resize_completion(self):
"""Adjust completion according to config.""" """Adjust completion according to config."""
@ -414,20 +430,7 @@ class MainWindow(QWidget):
height = contents_height height = contents_height
else: else:
contents_height = -1 contents_height = -1
status_position = config.get('ui', 'status-position') rect = self._get_overlay_position(height)
if status_position == 'bottom':
top = self.height() - self.status.height() - height
top = qtutils.check_overflow(top, 'int', fatal=False)
topleft = QPoint(0, top)
bottomright = self.status.geometry().topRight()
elif status_position == 'top':
topleft = self.status.geometry().bottomLeft()
bottom = self.status.height() + height
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
bottomright = QPoint(self.width(), bottom)
else:
raise ValueError("Invalid position {}!".format(status_position))
rect = QRect(topleft, bottomright)
log.misc.debug('completion rect: {}'.format(rect)) log.misc.debug('completion rect: {}'.format(rect))
if rect.isValid(): if rect.isValid():
self._completion.setGeometry(rect) self._completion.setGeometry(rect)
@ -450,6 +453,17 @@ class MainWindow(QWidget):
if rect.isValid(): if rect.isValid():
self._keyhint.setGeometry(rect) self._keyhint.setGeometry(rect)
@pyqtSlot()
def _reposition_messageview(self):
"""Position the message view correctly."""
if not self._messageview.isVisible():
return
height = self._messageview.message_height()
rect = self._get_overlay_position(height)
log.misc.debug('messageview rect: {}'.format(rect))
if rect.isValid():
self._messageview.setGeometry(rect)
@cmdutils.register(instance='main-window', scope='window') @cmdutils.register(instance='main-window', scope='window')
@pyqtSlot() @pyqtSlot()
def close(self): def close(self):

View File

@ -0,0 +1,118 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Showing messages above the statusbar."""
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
from qutebrowser.config import config, style
from qutebrowser.utils import usertypes, objreg
class Message(QLabel):
"""A single error/warning/info message."""
def __init__(self, level, text, parent=None):
super().__init__(text, parent)
self.setAttribute(Qt.WA_StyledBackground, True)
stylesheet = """
padding-top: 2px;
padding-bottom: 2px;
"""
if level == usertypes.MessageLevel.error:
stylesheet += """
background-color: {{ color['messages.bg.error'] }};
color: {{ color['messages.fg.error'] }};
font: {{ font['messages.error'] }};
border-bottom: 1px solid {{ color['messages.border.error'] }};
"""
elif level == usertypes.MessageLevel.warning:
stylesheet += """
background-color: {{ color['messages.bg.warning'] }};
color: {{ color['messages.fg.warning'] }};
font: {{ font['messages.warning'] }};
border-bottom:
1px solid {{ color['messages.border.warning'] }};
"""
elif level == usertypes.MessageLevel.info:
stylesheet += """
background-color: {{ color['messages.bg.info'] }};
color: {{ color['messages.fg.info'] }};
font: {{ font['messages.info'] }};
border-bottom: 1px solid {{ color['messages.border.info'] }}
"""
else: # pragma: no cover
raise ValueError("Invalid level {!r}".format(level))
# We don't bother with set_register_stylesheet here as it's short-lived
# anyways.
self.setStyleSheet(style.get_stylesheet(stylesheet))
class MessageView(QWidget):
reposition = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
self._clear_timer = QTimer()
self._clear_timer.timeout.connect(self._clear_messages)
self._set_clear_timer_interval()
objreg.get('config').changed.connect(self._set_clear_timer_interval)
self._last_text = None
self._messages = []
@config.change_filter('ui', 'message-timeout')
def _set_clear_timer_interval(self):
self._clear_timer.setInterval(config.get('ui', 'message-timeout'))
def message_height(self):
return sum(label.sizeHint().height() for label in self._messages)
@pyqtSlot()
def _clear_messages(self):
for widget in self._messages:
self._vbox.removeWidget(widget)
widget.hide()
widget.deleteLater()
self._messages = []
self._last_text = None
self.hide()
self._clear_timer.stop()
@pyqtSlot(usertypes.MessageLevel, str)
def show_message(self, level, text):
if text == self._last_text:
return
widget = Message(level, text, parent=self)
self._vbox.addWidget(widget)
widget.show()
self._clear_timer.start()
self._messages.append(widget)
self._last_text = text
self.show()
self.reposition.emit()

View File

@ -33,9 +33,6 @@ from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
from qutebrowser.mainwindow.statusbar import text as textwidget from qutebrowser.mainwindow.statusbar import text as textwidget
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
'command'])
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection']) CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
@ -52,22 +49,9 @@ class StatusBar(QWidget):
cmd: The Command widget in the statusbar. cmd: The Command widget in the statusbar.
_hbox: The main QHBoxLayout. _hbox: The main QHBoxLayout.
_stack: The QStackedLayout with cmd/txt widgets. _stack: The QStackedLayout with cmd/txt widgets.
_text_queue: A deque of (error, text) tuples to be displayed.
error: True if message is an error, False otherwise
_text_pop_timer: A Timer displaying the error messages.
_stopwatch: A QTime for the last displayed message.
_timer_was_active: Whether the _text_pop_timer was active before hiding
the command widget.
_previous_widget: A PreviousWidget member - the widget which was
displayed when an error interrupted it.
_win_id: The window ID the statusbar is associated with. _win_id: The window ID the statusbar is associated with.
Class attributes: Class attributes:
_severity: The severity of the current message, a Severity member.
For some reason we need to have this as class attribute so
pyqtProperty works correctly.
_prompt_active: If we're currently in prompt-mode. _prompt_active: If we're currently in prompt-mode.
For some reason we need to have this as class attribute For some reason we need to have this as class attribute
@ -129,20 +113,6 @@ class StatusBar(QWidget):
background-color: {{ color['statusbar.bg.caret-selection'] }}; background-color: {{ color['statusbar.bg.caret-selection'] }};
} }
QWidget#StatusBar[severity="error"],
QWidget#StatusBar[severity="error"] QLabel,
QWidget#StatusBar[severity="error"] QLineEdit {
color: {{ color['statusbar.fg.error'] }};
background-color: {{ color['statusbar.bg.error'] }};
}
QWidget#StatusBar[severity="warning"],
QWidget#StatusBar[severity="warning"] QLabel,
QWidget#StatusBar[severity="warning"] QLineEdit {
color: {{ color['statusbar.fg.warning'] }};
background-color: {{ color['statusbar.bg.warning'] }};
}
QWidget#StatusBar[prompt_active="true"], QWidget#StatusBar[prompt_active="true"],
QWidget#StatusBar[prompt_active="true"] QLabel, QWidget#StatusBar[prompt_active="true"] QLabel,
QWidget#StatusBar[prompt_active="true"] QLineEdit { QWidget#StatusBar[prompt_active="true"] QLineEdit {
@ -177,7 +147,6 @@ class StatusBar(QWidget):
self._win_id = win_id self._win_id = win_id
self._option = None self._option = None
self._stopwatch = QTime()
self._hbox = QHBoxLayout(self) self._hbox = QHBoxLayout(self)
self.set_hbox_padding() self.set_hbox_padding()
@ -195,16 +164,9 @@ class StatusBar(QWidget):
self.txt = textwidget.Text() self.txt = textwidget.Text()
self._stack.addWidget(self.txt) self._stack.addWidget(self.txt)
self._timer_was_active = False
self._text_queue = collections.deque()
self._text_pop_timer = usertypes.Timer(self, 'statusbar_text_pop')
self._text_pop_timer.timeout.connect(self._pop_text)
self.set_pop_timer_interval()
objreg.get('config').changed.connect(self.set_pop_timer_interval)
self.prompt = prompt.Prompt(win_id) self.prompt = prompt.Prompt(win_id)
self._stack.addWidget(self.prompt) self._stack.addWidget(self.prompt)
self._previous_widget = PreviousWidget.none
self.cmd.show_cmd.connect(self._show_cmd_widget) self.cmd.show_cmd.connect(self._show_cmd_widget)
self.cmd.hide_cmd.connect(self._hide_cmd_widget) self.cmd.hide_cmd.connect(self._hide_cmd_widget)
@ -252,40 +214,6 @@ class StatusBar(QWidget):
padding = config.get('ui', 'statusbar-padding') padding = config.get('ui', 'statusbar-padding')
self._hbox.setContentsMargins(padding.left, 0, padding.right, 0) self._hbox.setContentsMargins(padding.left, 0, padding.right, 0)
@pyqtProperty(str)
def severity(self):
"""Getter for self.severity, so it can be used as Qt property.
Return:
The severity as a string (!)
"""
if self._severity is None:
return ""
else:
return self._severity.name
def _set_severity(self, severity):
"""Set the severity for the current message.
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
Args:
severity: A Severity member.
"""
if self._severity == severity:
# This gets called a lot (e.g. if the completion selection was
# changed), and setStyleSheet is relatively expensive, so we ignore
# this if there's nothing to change.
return
log.statusbar.debug("Setting severity to {}".format(severity))
self._severity = severity
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
if severity != Severity.normal:
# If we got an error while command/prompt was shown, raise the text
# widget.
self._stack.setCurrentWidget(self.txt)
@pyqtProperty(bool) @pyqtProperty(bool)
def prompt_active(self): def prompt_active(self):
"""Getter for self.prompt_active, so it can be used as Qt property.""" """Getter for self.prompt_active, so it can be used as Qt property."""
@ -349,51 +277,14 @@ class StatusBar(QWidget):
text = "-- {} MODE --".format(mode.upper()) text = "-- {} MODE --".format(mode.upper())
self.txt.set_text(self.txt.Text.normal, text) self.txt.set_text(self.txt.Text.normal, text)
def _pop_text(self):
"""Display a text in the statusbar and pop it from _text_queue."""
try:
severity, text = self._text_queue.popleft()
except IndexError:
self._set_severity(Severity.normal)
self.txt.set_text(self.txt.Text.temp, '')
self._text_pop_timer.stop()
# If a previous widget was interrupted by an error, restore it.
if self._previous_widget == PreviousWidget.prompt:
self._stack.setCurrentWidget(self.prompt)
elif self._previous_widget == PreviousWidget.command:
self._stack.setCurrentWidget(self.cmd)
elif self._previous_widget == PreviousWidget.none:
self.maybe_hide()
else:
raise AssertionError("Unknown _previous_widget!")
return
self.show()
log.statusbar.debug("Displaying message: {} (severity {})".format(
text, severity))
log.statusbar.debug("Remaining: {}".format(self._text_queue))
self._set_severity(severity)
self.txt.set_text(self.txt.Text.temp, text)
def _show_cmd_widget(self): def _show_cmd_widget(self):
"""Show command widget instead of temporary text.""" """Show command widget instead of temporary text."""
self._set_severity(Severity.normal)
self._previous_widget = PreviousWidget.command
if self._text_pop_timer.isActive():
self._timer_was_active = True
self._text_pop_timer.stop()
self._stack.setCurrentWidget(self.cmd) self._stack.setCurrentWidget(self.cmd)
self.show() self.show()
def _hide_cmd_widget(self): def _hide_cmd_widget(self):
"""Show temporary text instead of command widget.""" """Show temporary text instead of command widget."""
log.statusbar.debug("Hiding cmd widget, queue: {}".format( log.statusbar.debug("Hiding cmd widget")
self._text_queue))
self._previous_widget = PreviousWidget.none
if self._timer_was_active:
# Restart the text pop timer if it was active before hiding.
self._pop_text()
self._text_pop_timer.start()
self._timer_was_active = False
self._stack.setCurrentWidget(self.txt) self._stack.setCurrentWidget(self.txt)
self.maybe_hide() self.maybe_hide()
@ -401,112 +292,17 @@ class StatusBar(QWidget):
"""Show prompt widget instead of temporary text.""" """Show prompt widget instead of temporary text."""
if self._stack.currentWidget() is self.prompt: if self._stack.currentWidget() is self.prompt:
return return
self._set_severity(Severity.normal)
self._set_prompt_active(True) self._set_prompt_active(True)
self._previous_widget = PreviousWidget.prompt
if self._text_pop_timer.isActive():
self._timer_was_active = True
self._text_pop_timer.stop()
self._stack.setCurrentWidget(self.prompt) self._stack.setCurrentWidget(self.prompt)
self.show() self.show()
def _hide_prompt_widget(self): def _hide_prompt_widget(self):
"""Show temporary text instead of prompt widget.""" """Show temporary text instead of prompt widget."""
self._set_prompt_active(False) self._set_prompt_active(False)
self._previous_widget = PreviousWidget.none log.statusbar.debug("Hiding prompt widget")
log.statusbar.debug("Hiding prompt widget, queue: {}".format(
self._text_queue))
if self._timer_was_active:
# Restart the text pop timer if it was active before hiding.
self._pop_text()
self._text_pop_timer.start()
self._timer_was_active = False
self._stack.setCurrentWidget(self.txt) self._stack.setCurrentWidget(self.txt)
self.maybe_hide() self.maybe_hide()
def _disp_text(self, text, severity, immediately=False):
"""Inner logic for disp_error and disp_temp_text.
Args:
text: The message to display.
severity: The severity of the messages.
immediately: If set, message gets displayed immediately instead of
queued.
"""
log.statusbar.debug("Displaying text: {} (severity={})".format(
text, severity))
mindelta = config.get('ui', 'message-timeout')
if self._stopwatch.isNull():
delta = None
self._stopwatch.start()
else:
delta = self._stopwatch.restart()
log.statusbar.debug("queue: {} / delta: {}".format(
self._text_queue, delta))
if not self._text_queue and (delta is None or delta > mindelta):
# If the queue is empty and we didn't print messages for long
# enough, we can take the short route and display the message
# immediately. We then start the pop_timer only to restore the
# normal state in 2 seconds.
log.statusbar.debug("Displaying immediately")
self._set_severity(severity)
self.show()
self.txt.set_text(self.txt.Text.temp, text)
self._text_pop_timer.start()
elif self._text_queue and self._text_queue[-1] == (severity, text):
# If we get the same message multiple times in a row and we're
# still displaying it *anyways* we ignore the new one
log.statusbar.debug("ignoring")
elif immediately:
# This message is a reaction to a keypress and should be displayed
# immediately, temporarily interrupting the message queue.
# We display this immediately and restart the timer.to clear it and
# display the rest of the queue later.
log.statusbar.debug("Moving to beginning of queue")
self._set_severity(severity)
self.show()
self.txt.set_text(self.txt.Text.temp, text)
self._text_pop_timer.start()
else:
# There are still some messages to be displayed, so we queue this
# up.
log.statusbar.debug("queueing")
self._text_queue.append((severity, text))
self._text_pop_timer.start()
@pyqtSlot(str, bool)
def disp_error(self, text, immediately=False):
"""Display an error in the statusbar.
Args:
text: The message to display.
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, Severity.error, immediately)
@pyqtSlot(str, bool)
def disp_warning(self, text, immediately=False):
"""Display a warning in the statusbar.
Args:
text: The message to display.
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, Severity.warning, immediately)
@pyqtSlot(str, bool)
def disp_temp_text(self, text, immediately):
"""Display a temporary text in the statusbar.
Args:
text: The message to display.
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, Severity.normal, immediately)
@pyqtSlot(str) @pyqtSlot(str)
def set_text(self, val): def set_text(self, val):
"""Set a normal (persistent) text in the status bar.""" """Set a normal (persistent) text in the status bar."""
@ -539,11 +335,6 @@ class StatusBar(QWidget):
usertypes.KeyMode.caret]: usertypes.KeyMode.caret]:
self.set_mode_active(old_mode, False) self.set_mode_active(old_mode, False)
@config.change_filter('ui', 'message-timeout')
def set_pop_timer_interval(self):
"""Update message timeout when config changed."""
self._text_pop_timer.setInterval(config.get('ui', 'message-timeout'))
def resizeEvent(self, e): def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards. """Extend resizeEvent of QWidget to emit a resized signal afterwards.

View File

@ -276,7 +276,7 @@ class TabbedBrowser(tabwidget.TabWidget):
# We display a warnings for URLs which are not empty but invalid - # We display a warnings for URLs which are not empty but invalid -
# but we don't return here because we want the tab to close either # but we don't return here because we want the tab to close either
# way. # way.
urlutils.invalid_url_error(self._win_id, tab.url(), "saving tab") urlutils.invalid_url_error(tab.url(), "saving tab")
tab.shutdown() tab.shutdown()
self.removeTab(idx) self.removeTab(idx)
tab.deleteLater() tab.deleteLater()
@ -650,7 +650,7 @@ class TabbedBrowser(tabwidget.TabWidget):
except qtutils.QtValueError: except qtutils.QtValueError:
# show an error only if the mark is not automatically set # show an error only if the mark is not automatically set
if key != "'": if key != "'":
message.error(self._win_id, "Failed to set mark: url invalid") message.error("Failed to set mark: url invalid")
return return
point = self.currentWidget().scroller.pos_px() point = self.currentWidget().scroller.pos_px()
@ -687,9 +687,9 @@ class TabbedBrowser(tabwidget.TabWidget):
self.openurl(url, newtab=False) self.openurl(url, newtab=False)
self.cur_load_finished.connect(callback) self.cur_load_finished.connect(callback)
else: else:
message.error(self._win_id, "Mark {} is not set".format(key)) message.error("Mark {} is not set".format(key))
elif urlkey is None: elif urlkey is None:
message.error(self._win_id, "Current URL is invalid!") message.error("Current URL is invalid!")
elif urlkey in self._local_marks and key in self._local_marks[urlkey]: elif urlkey in self._local_marks and key in self._local_marks[urlkey]:
point = self._local_marks[urlkey][key] point = self._local_marks[urlkey][key]
@ -700,4 +700,4 @@ class TabbedBrowser(tabwidget.TabWidget):
tab.scroller.to_point(point) tab.scroller.to_point(point)
else: else:
message.error(self._win_id, "Mark {} is not set".format(key)) message.error("Mark {} is not set".format(key))

View File

@ -38,17 +38,15 @@ class ExternalEditor(QObject):
_file: The file handle as tempfile.NamedTemporaryFile. Note that this _file: The file handle as tempfile.NamedTemporaryFile. Note that this
handle will be closed after the initial file has been created. handle will be closed after the initial file has been created.
_proc: The GUIProcess of the editor. _proc: The GUIProcess of the editor.
_win_id: The window ID the ExternalEditor is associated with.
""" """
editing_finished = pyqtSignal(str) editing_finished = pyqtSignal(str)
def __init__(self, win_id, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._text = None self._text = None
self._file = None self._file = None
self._proc = None self._proc = None
self._win_id = win_id
def _cleanup(self): def _cleanup(self):
"""Clean up temporary files after the editor closed.""" """Clean up temporary files after the editor closed."""
@ -61,8 +59,7 @@ class ExternalEditor(QObject):
except OSError as e: except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
message.error(self._win_id, message.error("Failed to delete tempfile... ({})".format(e))
"Failed to delete tempfile... ({})".format(e))
@pyqtSlot(int, QProcess.ExitStatus) @pyqtSlot(int, QProcess.ExitStatus)
def on_proc_closed(self, exitcode, exitstatus): def on_proc_closed(self, exitcode, exitstatus):
@ -85,8 +82,7 @@ class ExternalEditor(QObject):
except OSError as e: except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
message.error(self._win_id, "Failed to read back edited file: " message.error("Failed to read back edited file: {}".format(e))
"{}".format(e))
return return
log.procs.debug("Read back: {}".format(text)) log.procs.debug("Read back: {}".format(text))
self.editing_finished.emit(text) self.editing_finished.emit(text)
@ -119,11 +115,9 @@ class ExternalEditor(QObject):
fobj.write(text) fobj.write(text)
self._file = fobj self._file = fobj
except OSError as e: except OSError as e:
message.error(self._win_id, "Failed to create initial file: " message.error("Failed to create initial file: {}".format(e))
"{}".format(e))
return return
self._proc = guiprocess.GUIProcess(self._win_id, what='editor', self._proc = guiprocess.GUIProcess(what='editor', parent=self)
parent=self)
self._proc.finished.connect(self.on_proc_closed) self._proc.finished.connect(self.on_proc_closed)
self._proc.error.connect(self.on_proc_error) self._proc.error.connect(self.on_proc_error)
editor = config.get('general', 'editor') editor = config.get('general', 'editor')

View File

@ -50,7 +50,6 @@ class GUIProcess(QObject):
verbose: Whether to show more messages. verbose: Whether to show more messages.
_started: Whether the underlying process is started. _started: Whether the underlying process is started.
_proc: The underlying QProcess. _proc: The underlying QProcess.
_win_id: The window ID this process is used in.
_what: What kind of thing is spawned (process/editor/userscript/...). _what: What kind of thing is spawned (process/editor/userscript/...).
Used in messages. Used in messages.
@ -62,10 +61,9 @@ class GUIProcess(QObject):
finished = pyqtSignal(int, QProcess.ExitStatus) finished = pyqtSignal(int, QProcess.ExitStatus)
started = pyqtSignal() started = pyqtSignal()
def __init__(self, win_id, what, *, verbose=False, additional_env=None, def __init__(self, what, *, verbose=False, additional_env=None,
parent=None): parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._what = what self._what = what
self.verbose = verbose self.verbose = verbose
self._started = False self._started = False
@ -90,8 +88,7 @@ class GUIProcess(QObject):
def on_error(self, error): def on_error(self, error):
"""Show a message if there was an error while spawning.""" """Show a message if there was an error while spawning."""
msg = ERROR_STRINGS[error] msg = ERROR_STRINGS[error]
message.error(self._win_id, "Error while spawning {}: {}".format( message.error("Error while spawning {}: {}".format(self._what, msg))
self._what, msg), immediately=True)
@pyqtSlot(int, QProcess.ExitStatus) @pyqtSlot(int, QProcess.ExitStatus)
def on_finished(self, code, status): def on_finished(self, code, status):
@ -100,18 +97,16 @@ class GUIProcess(QObject):
log.procs.debug("Process finished with code {}, status {}.".format( log.procs.debug("Process finished with code {}, status {}.".format(
code, status)) code, status))
if status == QProcess.CrashExit: if status == QProcess.CrashExit:
message.error(self._win_id, message.error("{} crashed!".format(self._what.capitalize()))
"{} crashed!".format(self._what.capitalize()),
immediately=True)
elif status == QProcess.NormalExit and code == 0: elif status == QProcess.NormalExit and code == 0:
if self.verbose: if self.verbose:
message.info(self._win_id, "{} exited successfully.".format( message.info("{} exited successfully.".format(
self._what.capitalize())) self._what.capitalize()))
else: else:
assert status == QProcess.NormalExit assert status == QProcess.NormalExit
# We call this 'status' here as it makes more sense to the user - # We call this 'status' here as it makes more sense to the user -
# it's actually 'code'. # it's actually 'code'.
message.error(self._win_id, "{} exited with status {}.".format( message.error("{} exited with status {}.".format(
self._what.capitalize(), code)) self._what.capitalize(), code))
stderr = bytes(self._proc.readAllStandardError()).decode('utf-8') stderr = bytes(self._proc.readAllStandardError()).decode('utf-8')
@ -137,7 +132,7 @@ class GUIProcess(QObject):
fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args))
log.procs.debug("Executing: {}".format(fake_cmdline)) log.procs.debug("Executing: {}".format(fake_cmdline))
if self.verbose: if self.verbose:
message.info(self._win_id, 'Executing: ' + fake_cmdline) message.info('Executing: ' + fake_cmdline)
def start(self, cmd, args, mode=None): def start(self, cmd, args, mode=None):
"""Convenience wrapper around QProcess::start.""" """Convenience wrapper around QProcess::start."""
@ -158,8 +153,8 @@ class GUIProcess(QObject):
log.procs.debug("Process started.") log.procs.debug("Process started.")
self._started = True self._started = True
else: else:
message.error(self._win_id, "Error while spawning {}: {}.".format( message.error("Error while spawning {}: {}.".format(self._what,
self._what, self._proc.error()), immediately=True) self._proc.error()))
def exit_status(self): def exit_status(self):
return self._proc.exitStatus() return self._proc.exitStatus()

View File

@ -180,17 +180,14 @@ class SaveManager(QObject):
try: try:
saveable.save(silent=True) saveable.save(silent=True)
except OSError as e: except OSError as e:
message.error('current', "Failed to auto-save {}: " message.error("Failed to auto-save {}: {}".format(key, e))
"{}".format(key, e))
@cmdutils.register(instance='save-manager', name='save', @cmdutils.register(instance='save-manager', name='save',
star_args_optional=True) star_args_optional=True)
@cmdutils.argument('win_id', win_id=True) def save_command(self, *what):
def save_command(self, win_id, *what):
"""Save configs and state. """Save configs and state.
Args: Args:
win_id: The window this command is executed in.
*what: What to save (`config`/`key-config`/`cookies`/...). *what: What to save (`config`/`key-config`/`cookies`/...).
If not given, everything is saved. If not given, everything is saved.
""" """
@ -201,12 +198,10 @@ class SaveManager(QObject):
explicit = False explicit = False
for key in what: for key in what:
if key not in self.saveables: if key not in self.saveables:
message.error(win_id, "{} is nothing which can be " message.error("{} is nothing which can be saved".format(key))
"saved".format(key))
else: else:
try: try:
self.save(key, explicit=explicit, force=True) self.save(key, explicit=explicit, force=True)
except OSError as e: except OSError as e:
message.error(win_id, "Could not save {}: " message.error("Could not save {}: {}".format(key, e))
"{}".format(key, e))
log.save.debug(":save saved {}".format(', '.join(what))) log.save.debug(":save saved {}".format(', '.join(what)))

View File

@ -429,14 +429,12 @@ class SessionManager(QObject):
win.close() win.close()
@cmdutils.register(name=['session-save', 'w'], instance='session-manager') @cmdutils.register(name=['session-save', 'w'], instance='session-manager')
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('name', completion=usertypes.Completion.sessions) @cmdutils.argument('name', completion=usertypes.Completion.sessions)
def session_save(self, win_id, name: str=default, current=False, def session_save(self, name: str=default, current=False, quiet=False,
quiet=False, force=False): force=False):
"""Save a session. """Save a session.
Args: Args:
win_id: The current window ID.
name: The name of the session. If not given, the session configured name: The name of the session. If not given, the session configured
in general -> session-default-name is saved. in general -> session-default-name is saved.
current: Save the current session instead of the default. current: Save the current session instead of the default.
@ -460,8 +458,7 @@ class SessionManager(QObject):
.format(e)) .format(e))
else: else:
if not quiet: if not quiet:
message.info(win_id, "Saved session {}.".format(name), message.info("Saved session {}.".format(name))
immediately=True)
@cmdutils.register(instance='session-manager') @cmdutils.register(instance='session-manager')
@cmdutils.argument('name', completion=usertypes.Completion.sessions) @cmdutils.argument('name', completion=usertypes.Completion.sessions)

View File

@ -87,36 +87,33 @@ def repeat(times: int, command, win_id):
@cmdutils.register(hide=True) @cmdutils.register(hide=True)
@cmdutils.argument('win_id', win_id=True) def message_error(text):
def message_error(win_id, text):
"""Show an error message in the statusbar. """Show an error message in the statusbar.
Args: Args:
text: The text to show. text: The text to show.
""" """
message.error(win_id, text) message.error(text)
@cmdutils.register(hide=True) @cmdutils.register(hide=True)
@cmdutils.argument('win_id', win_id=True) def message_info(text):
def message_info(win_id, text):
"""Show an info message in the statusbar. """Show an info message in the statusbar.
Args: Args:
text: The text to show. text: The text to show.
""" """
message.info(win_id, text) message.info(text)
@cmdutils.register(hide=True) @cmdutils.register(hide=True)
@cmdutils.argument('win_id', win_id=True) def message_warning(text):
def message_warning(win_id, text):
"""Show a warning message in the statusbar. """Show a warning message in the statusbar.
Args: Args:
text: The text to show. text: The text to show.
""" """
message.warning(win_id, text) message.warning(text)
@cmdutils.register(debug=True) @cmdutils.register(debug=True)

View File

@ -37,6 +37,14 @@ QueuedMsg = collections.namedtuple(
'QueuedMsg', ['time', 'win_id', 'method_name', 'text', 'args', 'kwargs']) 'QueuedMsg', ['time', 'win_id', 'method_name', 'text', 'args', 'kwargs'])
global_bridge = None
def init():
global global_bridge
global_bridge = GlobalMessageBridge()
def _log_stack(typ, stack): def _log_stack(typ, stack):
"""Log the given message stacktrace. """Log the given message stacktrace.
@ -132,12 +140,11 @@ def on_focus_changed():
getattr(bridge, msg.method_name)(text, *msg.args, **msg.kwargs) getattr(bridge, msg.method_name)(text, *msg.args, **msg.kwargs)
def error(win_id, message, immediately=False, *, stack=None): def error(message, *, stack=None):
"""Convenience function to display an error message in the statusbar. """Convenience function to display an error message in the statusbar.
Args: Args:
win_id: The ID of the window which is calling this function. message: The message to show
others: See MessageBridge.error.
stack: The stack trace to show. stack: The stack trace to show.
""" """
if stack is None: if stack is None:
@ -146,28 +153,18 @@ def error(win_id, message, immediately=False, *, stack=None):
else: else:
typ = 'error (from exception)' typ = 'error (from exception)'
_log_stack(typ, stack) _log_stack(typ, stack)
_wrapper(win_id, 'error', message, immediately) global_bridge.show_message.emit(usertypes.MessageLevel.error, message)
def warning(win_id, message, immediately=False): def warning(message):
"""Convenience function to display a warning message in the statusbar. """Convenience function to display a warning message in the statusbar."""
Args:
win_id: The ID of the window which is calling this function.
others: See MessageBridge.warning.
"""
_log_stack('warning', traceback.format_stack()) _log_stack('warning', traceback.format_stack())
_wrapper(win_id, 'warning', message, immediately) global_bridge.show_message.emit(usertypes.MessageLevel.warning, message)
def info(win_id, message, immediately=True): def info(message):
"""Convenience function to display an info message in the statusbar. """Convenience function to display an info message in the statusbar."""
global_bridge.show_message.emit(usertypes.MessageLevel.info, message)
Args:
win_id: The ID of the window which is calling this function.
others: See MessageBridge.info.
"""
_wrapper(win_id, 'info', message, immediately)
def set_cmd_text(win_id, txt): def set_cmd_text(win_id, txt):
@ -252,19 +249,24 @@ def confirm_async(win_id, message, yes_action, no_action=None, default=None):
_wrapper(win_id, 'ask', q, blocking=False) _wrapper(win_id, 'ask', q, blocking=False)
class GlobalMessageBridge(QObject):
"""Global (not per-window) message bridge for errors/infos/warnings.
Signals:
show_message: Show a message
arg 0: A MessageLevel member
arg 1: The text to show
"""
show_message = pyqtSignal(usertypes.MessageLevel, str)
class MessageBridge(QObject): class MessageBridge(QObject):
"""Bridge for messages to be shown in the statusbar. """Bridge for messages to be shown in the statusbar.
Signals: Signals:
s_error: Display an error message.
arg 0: The error message to show.
arg 1: Whether to show it immediately (True) or queue it
(False).
s_info: Display an info message.
args: See s_error.
s_warning: Display a warning message.
args: See s_error.
s_set_text: Set a persistent text in the statusbar. s_set_text: Set a persistent text in the statusbar.
arg: The text to set. arg: The text to set.
s_maybe_reset_text: Reset the text if it hasn't been changed yet. s_maybe_reset_text: Reset the text if it hasn't been changed yet.
@ -280,9 +282,6 @@ class MessageBridge(QObject):
Qt.DirectConnection! Qt.DirectConnection!
""" """
s_error = pyqtSignal(str, bool)
s_warning = pyqtSignal(str, bool)
s_info = pyqtSignal(str, bool)
s_set_text = pyqtSignal(str) s_set_text = pyqtSignal(str)
s_maybe_reset_text = pyqtSignal(str) s_maybe_reset_text = pyqtSignal(str)
s_set_cmd_text = pyqtSignal(str) s_set_cmd_text = pyqtSignal(str)
@ -291,55 +290,6 @@ class MessageBridge(QObject):
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
def error(self, msg, immediately=False, *, log_stack=True):
"""Display an error in the statusbar.
Args:
msg: The message to show.
immediately: Whether to display the message immediately (True) or
queue it for displaying when all other messages are
displayed (False). Messages resulting from direct user
input should be displayed immediately, all other
messages should be queued.
log_stack: Log the stacktrace of the error.
"""
msg = str(msg)
log.message.error(msg)
if log_stack:
_log_stack('error (delayed)', traceback.format_stack())
self.s_error.emit(msg, immediately)
def warning(self, msg, immediately=False, *, log_stack=True):
"""Display a warning in the statusbar.
Args:
msg: The message to show.
immediately: Whether to display the message immediately (True) or
queue it for displaying when all other messages are
displayed (False). Messages resulting from direct user
input should be displayed immediately, all other
messages should be queued.
log_stack: Log the stacktrace of the warning.
"""
msg = str(msg)
log.message.warning(msg)
if log_stack:
_log_stack('warning (delayed)', traceback.format_stack())
self.s_warning.emit(msg, immediately)
def info(self, msg, immediately=True, *, log_stack=False):
"""Display an info text in the statusbar.
Args:
See error(). Note immediately is True by default, because messages
do rarely happen without user interaction.
log_stack is ignored.
"""
msg = str(msg)
log.message.info(msg)
self.s_info.emit(msg, immediately)
def set_cmd_text(self, text, *, log_stack=False): def set_cmd_text(self, text, *, log_stack=False):
"""Set the command text of the statusbar. """Set the command text of the statusbar.

View File

@ -321,11 +321,10 @@ def qurl_from_user_input(urlstr):
return QUrl('http://[{}]{}'.format(ipstr, rest)) return QUrl('http://[{}]{}'.format(ipstr, rest))
def invalid_url_error(win_id, url, action): def invalid_url_error(url, action):
"""Display an error message for a URL. """Display an error message for a URL.
Args: Args:
win_id: The window ID to show the error message in.
action: The action which was interrupted by the error. action: The action which was interrupted by the error.
""" """
if url.isValid(): if url.isValid():
@ -333,7 +332,7 @@ def invalid_url_error(win_id, url, action):
url.toDisplayString())) url.toDisplayString()))
errstring = get_errstring( errstring = get_errstring(
url, "Trying to {} with invalid URL".format(action)) url, "Trying to {} with invalid URL".format(action))
message.error(win_id, errstring) message.error(errstring)
def raise_cmdexc_if_invalid(url): def raise_cmdexc_if_invalid(url):

View File

@ -265,6 +265,9 @@ arg2backend = {
JsWorld = enum('JsWorld', ['main', 'application', 'user', 'jseval']) JsWorld = enum('JsWorld', ['main', 'application', 'user', 'jseval'])
MessageLevel = enum('MessageLevel', ['error', 'warning', 'info'])
# Where a download should be saved # Where a download should be saved
class DownloadTarget: class DownloadTarget:

View File

@ -274,21 +274,6 @@ def format_seconds(total_seconds):
return prefix + ':'.join(chunks) return prefix + ':'.join(chunks)
def format_timedelta(td):
"""Format a timedelta to get a "1h 5m 1s" string."""
prefix = '-' if td.total_seconds() < 0 else ''
hours, rem = divmod(abs(round(td.total_seconds())), 3600)
minutes, seconds = divmod(rem, 60)
chunks = []
if hours:
chunks.append('{}h'.format(hours))
if minutes:
chunks.append('{}m'.format(minutes))
if seconds or not chunks:
chunks.append('{}s'.format(seconds))
return prefix + ' '.join(chunks)
def format_size(size, base=1024, suffix=''): def format_size(size, base=1024, suffix=''):
"""Format a byte size so it's human readable. """Format a byte size so it's human readable.

View File

@ -83,12 +83,7 @@ class LogLine(testprocess.Line):
if self.function is None and self.line == 0: if self.function is None and self.line == 0:
self.line = None self.line = None
self.traceback = line.get('traceback') self.traceback = line.get('traceback')
self.message = line['message']
self.full_message = line['message']
msg_match = re.match(r'^(\[(?P<prefix>\d+s ago)\] )?(?P<message>.*)',
self.full_message, re.DOTALL)
self.prefix = msg_match.group('prefix')
self.message = msg_match.group('message')
self.expected = is_ignored_qt_message(self.message) self.expected = is_ignored_qt_message(self.message)
self.use_color = False self.use_color = False

View File

@ -211,13 +211,6 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
'line': 233, 'line': 233,
} }
), ),
(
# With [2s ago] marker
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "foo", '
'"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 10, '
'"message": "[2s ago] test"}',
{'prefix': '2s ago', 'message': 'test'}
),
( (
# ResourceWarning # ResourceWarning
'{"created": 0, "msecs": 0, "levelname": "WARNING", ' '{"created": 0, "msecs": 0, "levelname": "WARNING", '
@ -228,8 +221,7 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
{'category': 'py.warnings'} {'category': 'py.warnings'}
), ),
], ids=['normal', 'vdebug', 'unknown module', 'expected message', ], ids=['normal', 'vdebug', 'unknown module', 'expected message',
'weird Qt location', 'QXcbXSettings', '2s ago marker', 'weird Qt location', 'QXcbXSettings', 'resourcewarning'])
'resourcewarning'])
def test_log_line_parse(data, attrs): def test_log_line_parse(data, attrs):
line = quteprocess.LogLine(data) line = quteprocess.LogLine(data)
for name, expected in attrs.items(): for name, expected in attrs.items():

View File

@ -43,7 +43,7 @@ def patch_things(config_stub, message_mock, monkeypatch, stubs):
@pytest.fixture @pytest.fixture
def editor(): def editor():
ed = editormod.ExternalEditor(0) ed = editormod.ExternalEditor()
ed.editing_finished = mock.Mock() ed.editing_finished = mock.Mock()
yield ed yield ed
ed._cleanup() ed._cleanup()

View File

@ -37,7 +37,7 @@ def guiprocess_message_mock(message_mock):
@pytest.fixture() @pytest.fixture()
def proc(qtbot): def proc(qtbot):
"""A fixture providing a GUIProcess and cleaning it up after the test.""" """A fixture providing a GUIProcess and cleaning it up after the test."""
p = guiprocess.GUIProcess(0, 'testprocess') p = guiprocess.GUIProcess('testprocess')
yield p yield p
if p._proc.state() == QProcess.Running: if p._proc.state() == QProcess.Running:
with qtbot.waitSignal(p.finished, timeout=10000, with qtbot.waitSignal(p.finished, timeout=10000,
@ -50,7 +50,7 @@ def proc(qtbot):
@pytest.fixture() @pytest.fixture()
def fake_proc(monkeypatch, stubs): def fake_proc(monkeypatch, stubs):
"""A fixture providing a GUIProcess with a mocked QProcess.""" """A fixture providing a GUIProcess with a mocked QProcess."""
p = guiprocess.GUIProcess(0, 'testprocess') p = guiprocess.GUIProcess('testprocess')
monkeypatch.setattr(p, '_proc', stubs.fake_qprocess()) monkeypatch.setattr(p, '_proc', stubs.fake_qprocess())
return p return p
@ -86,7 +86,7 @@ def test_start_verbose(proc, qtbot, guiprocess_message_mock, py_proc):
def test_start_env(monkeypatch, qtbot, py_proc): def test_start_env(monkeypatch, qtbot, py_proc):
monkeypatch.setenv('QUTEBROWSER_TEST_1', '1') monkeypatch.setenv('QUTEBROWSER_TEST_1', '1')
env = {'QUTEBROWSER_TEST_2': '2'} env = {'QUTEBROWSER_TEST_2': '2'}
proc = guiprocess.GUIProcess(0, 'testprocess', additional_env=env) proc = guiprocess.GUIProcess('testprocess', additional_env=env)
argv = py_proc(""" argv = py_proc("""
import os import os

View File

@ -431,11 +431,11 @@ def test_invalid_url_error(urlutils_message_mock, url, valid, has_err_string):
assert qurl.isValid() == valid assert qurl.isValid() == valid
if valid: if valid:
with pytest.raises(ValueError): with pytest.raises(ValueError):
urlutils.invalid_url_error(0, qurl, '') urlutils.invalid_url_error(qurl, '')
assert not urlutils_message_mock.messages assert not urlutils_message_mock.messages
else: else:
assert bool(qurl.errorString()) == has_err_string assert bool(qurl.errorString()) == has_err_string
urlutils.invalid_url_error(0, qurl, 'frozzle') urlutils.invalid_url_error(qurl, 'frozzle')
msg = urlutils_message_mock.getmsg(urlutils_message_mock.Level.error) msg = urlutils_message_mock.getmsg(urlutils_message_mock.Level.error)
if has_err_string: if has_err_string:

View File

@ -377,25 +377,6 @@ def test_format_seconds(seconds, out):
assert utils.format_seconds(seconds) == out assert utils.format_seconds(seconds) == out
@pytest.mark.parametrize('td, out', [
(datetime.timedelta(seconds=-1), '-1s'),
(datetime.timedelta(seconds=0), '0s'),
(datetime.timedelta(seconds=59), '59s'),
(datetime.timedelta(seconds=120), '2m'),
(datetime.timedelta(seconds=60.4), '1m'),
(datetime.timedelta(seconds=63), '1m 3s'),
(datetime.timedelta(seconds=-64), '-1m 4s'),
(datetime.timedelta(seconds=3599), '59m 59s'),
(datetime.timedelta(seconds=3600), '1h'),
(datetime.timedelta(seconds=3605), '1h 5s'),
(datetime.timedelta(seconds=3723), '1h 2m 3s'),
(datetime.timedelta(seconds=3780), '1h 3m'),
(datetime.timedelta(seconds=36000), '10h'),
])
def test_format_timedelta(td, out):
assert utils.format_timedelta(td) == out
class TestFormatSize: class TestFormatSize:
"""Tests for format_size. """Tests for format_size.