Require a filename for user-stylesheet; add hide-scrollbar setting
This commit is contained in:
parent
964ddb472b
commit
fcb955458c
@ -50,6 +50,8 @@ Added
|
|||||||
- New `cast` userscript to show a video on a Google Chromecast
|
- New `cast` userscript to show a video on a Google Chromecast
|
||||||
- New `:run-with-count` command which replaces the (undocumented) `:count:command` syntax.
|
- New `:run-with-count` command which replaces the (undocumented) `:count:command` syntax.
|
||||||
- New `:record-macro` (`q`) and `:run-macro` (`@`) commands for keyboard macros.
|
- New `:record-macro` (`q`) and `:run-macro` (`@`) commands for keyboard macros.
|
||||||
|
- New `ui -> hide-scrollbar` setting to hide the scrollbar independently of the
|
||||||
|
`user-stylesheet` setting.
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@ -143,6 +145,7 @@ Changed
|
|||||||
- Various functionality now works when javascript is disabled with QtWebKit
|
- Various functionality now works when javascript is disabled with QtWebKit
|
||||||
- Various commands/settings taking `left`/`right`/`previous` arguments now take
|
- Various commands/settings taking `left`/`right`/`previous` arguments now take
|
||||||
`prev`/`next`/`last-used` to remove ambiguity.
|
`prev`/`next`/`last-used` to remove ambiguity.
|
||||||
|
- The `ui -> user-stylesheet` setting now only takes filenames, not CSS snippets
|
||||||
|
|
||||||
Deprecated
|
Deprecated
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
@ -222,3 +222,19 @@ def get_tab(win_id, target):
|
|||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=win_id)
|
window=win_id)
|
||||||
return tabbed_browser.tabopen(url=None, background=bg_tab)
|
return tabbed_browser.tabopen(url=None, background=bg_tab)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_stylesheet():
|
||||||
|
"""Get the combined user-stylesheet."""
|
||||||
|
filename = config.get('ui', 'user-stylesheet')
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
css = ''
|
||||||
|
else:
|
||||||
|
with open(filename, 'r', encoding='utf-8') as f:
|
||||||
|
css = f.read()
|
||||||
|
|
||||||
|
if config.get('ui', 'hide-scrollbar'):
|
||||||
|
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
|
||||||
|
|
||||||
|
return css
|
||||||
|
@ -26,10 +26,12 @@ Module attributes:
|
|||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
from PyQt5.QtWebKit import QWebSettings
|
from PyQt5.QtWebKit import QWebSettings
|
||||||
|
|
||||||
from qutebrowser.config import config, websettings
|
from qutebrowser.config import config, websettings
|
||||||
from qutebrowser.utils import standarddir, objreg
|
from qutebrowser.utils import standarddir, objreg, urlutils
|
||||||
|
from qutebrowser.browser import shared
|
||||||
|
|
||||||
|
|
||||||
class Attribute(websettings.Attribute):
|
class Attribute(websettings.Attribute):
|
||||||
@ -80,14 +82,24 @@ class CookiePolicy(websettings.Base):
|
|||||||
self.MAPPING[value])
|
self.MAPPING[value])
|
||||||
|
|
||||||
|
|
||||||
|
def _set_user_stylesheet():
|
||||||
|
"""Set the generated user-stylesheet."""
|
||||||
|
stylesheet = shared.get_user_stylesheet().encode('utf-8')
|
||||||
|
url = urlutils.data_url('text/css;charset=utf-8', stylesheet)
|
||||||
|
QWebSettings.globalSettings().setUserStyleSheetUrl(url)
|
||||||
|
|
||||||
|
|
||||||
def update_settings(section, option):
|
def update_settings(section, option):
|
||||||
"""Update global settings when qwebsettings changed."""
|
"""Update global settings when qwebsettings changed."""
|
||||||
cache_path = standarddir.cache()
|
|
||||||
if (section, option) == ('general', 'private-browsing'):
|
if (section, option) == ('general', 'private-browsing'):
|
||||||
|
cache_path = standarddir.cache()
|
||||||
if config.get('general', 'private-browsing') or cache_path is None:
|
if config.get('general', 'private-browsing') or cache_path is None:
|
||||||
QWebSettings.setIconDatabasePath('')
|
QWebSettings.setIconDatabasePath('')
|
||||||
else:
|
else:
|
||||||
QWebSettings.setIconDatabasePath(cache_path)
|
QWebSettings.setIconDatabasePath(cache_path)
|
||||||
|
elif section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']:
|
||||||
|
_set_user_stylesheet()
|
||||||
|
|
||||||
websettings.update_mappings(MAPPINGS, section, option)
|
websettings.update_mappings(MAPPINGS, section, option)
|
||||||
|
|
||||||
|
|
||||||
@ -108,6 +120,7 @@ def init():
|
|||||||
os.path.join(data_path, 'offline-storage'))
|
os.path.join(data_path, 'offline-storage'))
|
||||||
|
|
||||||
websettings.init_mappings(MAPPINGS)
|
websettings.init_mappings(MAPPINGS)
|
||||||
|
_set_user_stylesheet()
|
||||||
objreg.get('config').changed.connect(update_settings)
|
objreg.get('config').changed.connect(update_settings)
|
||||||
|
|
||||||
|
|
||||||
@ -204,9 +217,7 @@ MAPPINGS = {
|
|||||||
Attribute(QWebSettings.ZoomTextOnly),
|
Attribute(QWebSettings.ZoomTextOnly),
|
||||||
'frame-flattening':
|
'frame-flattening':
|
||||||
Attribute(QWebSettings.FrameFlatteningEnabled),
|
Attribute(QWebSettings.FrameFlatteningEnabled),
|
||||||
'user-stylesheet':
|
# user-stylesheet is handled separately
|
||||||
Setter(getter=QWebSettings.userStyleSheetUrl,
|
|
||||||
setter=QWebSettings.setUserStyleSheetUrl),
|
|
||||||
'css-media-type':
|
'css-media-type':
|
||||||
NullStringSetter(getter=QWebSettings.cssMediaType,
|
NullStringSetter(getter=QWebSettings.cssMediaType,
|
||||||
setter=QWebSettings.setCSSMediaType),
|
setter=QWebSettings.setCSSMediaType),
|
||||||
|
@ -437,6 +437,11 @@ class ConfigManager(QObject):
|
|||||||
('fonts', 'hints'): _transform_hint_font,
|
('fonts', 'hints'): _transform_hint_font,
|
||||||
('completion', 'show'):
|
('completion', 'show'):
|
||||||
_get_value_transformer({'false': 'never', 'true': 'always'}),
|
_get_value_transformer({'false': 'never', 'true': 'always'}),
|
||||||
|
('ui', 'user-stylesheet'):
|
||||||
|
_get_value_transformer({
|
||||||
|
'html > ::-webkit-scrollbar { width: 0px; height: 0px; }': '',
|
||||||
|
'::-webkit-scrollbar { width: 0px; height: 0px; }': '',
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
changed = pyqtSignal(str, str)
|
changed = pyqtSignal(str, str)
|
||||||
|
@ -323,13 +323,15 @@ def data(readonly=False):
|
|||||||
"page."),
|
"page."),
|
||||||
|
|
||||||
('user-stylesheet',
|
('user-stylesheet',
|
||||||
SettingValue(typ.UserStyleSheet(none_ok=True),
|
SettingValue(typ.File(none_ok=True), '',
|
||||||
'html > ::-webkit-scrollbar { width: 0px; '
|
|
||||||
'height: 0px; }',
|
|
||||||
backends=[usertypes.Backend.QtWebKit]),
|
backends=[usertypes.Backend.QtWebKit]),
|
||||||
"User stylesheet to use (absolute filename, filename relative to "
|
"User stylesheet to use (absolute filename or filename relative to "
|
||||||
"the config directory or CSS string). Will expand environment "
|
"the config directory). Will expand environment variables."),
|
||||||
"variables."),
|
|
||||||
|
('hide-scrollbar',
|
||||||
|
SettingValue(typ.Bool(), 'true',
|
||||||
|
backends=[usertypes.Backend.QtWebKit]),
|
||||||
|
"Hide the main scrollbar."),
|
||||||
|
|
||||||
('css-media-type',
|
('css-media-type',
|
||||||
SettingValue(typ.String(none_ok=True), '',
|
SettingValue(typ.String(none_ok=True), '',
|
||||||
|
@ -1165,40 +1165,6 @@ class Encoding(BaseType):
|
|||||||
raise configexc.ValidationError(value, "is not a valid encoding!")
|
raise configexc.ValidationError(value, "is not a valid encoding!")
|
||||||
|
|
||||||
|
|
||||||
class UserStyleSheet(File):
|
|
||||||
|
|
||||||
"""QWebSettings UserStyleSheet."""
|
|
||||||
|
|
||||||
def transform(self, value):
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
|
|
||||||
path = super().transform(value)
|
|
||||||
if path is not None and os.path.exists(path):
|
|
||||||
return QUrl.fromLocalFile(path)
|
|
||||||
else:
|
|
||||||
return urlutils.data_url('text/css', value.encode('utf-8'))
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
self._basic_validation(value)
|
|
||||||
if not value:
|
|
||||||
return
|
|
||||||
value = os.path.expandvars(value)
|
|
||||||
value = os.path.expanduser(value)
|
|
||||||
try:
|
|
||||||
super().validate(value)
|
|
||||||
except configexc.ValidationError:
|
|
||||||
try:
|
|
||||||
if not os.path.isabs(value):
|
|
||||||
# probably a CSS, so we don't handle it as filename.
|
|
||||||
# FIXME We just try if it is encodable, maybe we should
|
|
||||||
# validate CSS?
|
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/115
|
|
||||||
value.encode('utf-8')
|
|
||||||
except UnicodeEncodeError as e:
|
|
||||||
raise configexc.ValidationError(value, str(e))
|
|
||||||
|
|
||||||
|
|
||||||
class AutoSearch(BaseType):
|
class AutoSearch(BaseType):
|
||||||
|
|
||||||
"""Whether to start a search when something else than a URL is entered."""
|
"""Whether to start a search when something else than a URL is entered."""
|
||||||
|
@ -1211,15 +1211,9 @@ def unrequired_class(**kwargs):
|
|||||||
|
|
||||||
@pytest.mark.usefixtures('qapp')
|
@pytest.mark.usefixtures('qapp')
|
||||||
@pytest.mark.usefixtures('config_tmpdir')
|
@pytest.mark.usefixtures('config_tmpdir')
|
||||||
class TestFileAndUserStyleSheet:
|
class TestFile:
|
||||||
|
|
||||||
"""Test File/UserStyleSheet."""
|
@pytest.fixture(params=[configtypes.File, unrequired_class])
|
||||||
|
|
||||||
@pytest.fixture(params=[
|
|
||||||
configtypes.File,
|
|
||||||
configtypes.UserStyleSheet,
|
|
||||||
unrequired_class,
|
|
||||||
])
|
|
||||||
def klass(self, request):
|
def klass(self, request):
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
@ -1227,18 +1221,12 @@ class TestFileAndUserStyleSheet:
|
|||||||
def file_class(self):
|
def file_class(self):
|
||||||
return configtypes.File
|
return configtypes.File
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def userstylesheet_class(self):
|
|
||||||
return configtypes.UserStyleSheet
|
|
||||||
|
|
||||||
def _expected(self, klass, arg):
|
def _expected(self, klass, arg):
|
||||||
"""Get the expected value."""
|
"""Get the expected value."""
|
||||||
if not arg:
|
if not arg:
|
||||||
return None
|
return None
|
||||||
elif klass is configtypes.File:
|
elif klass is configtypes.File:
|
||||||
return arg
|
return arg
|
||||||
elif klass is configtypes.UserStyleSheet:
|
|
||||||
return QUrl.fromLocalFile(arg)
|
|
||||||
elif klass is unrequired_class:
|
elif klass is unrequired_class:
|
||||||
return arg
|
return arg
|
||||||
else:
|
else:
|
||||||
@ -1262,11 +1250,6 @@ class TestFileAndUserStyleSheet:
|
|||||||
os_mock.path.isfile.return_value = False
|
os_mock.path.isfile.return_value = False
|
||||||
configtypes.File(required=False).validate('foobar')
|
configtypes.File(required=False).validate('foobar')
|
||||||
|
|
||||||
def test_validate_does_not_exist_userstylesheet(self, os_mock):
|
|
||||||
"""Test validate with a file which does not exist (UserStyleSheet)."""
|
|
||||||
os_mock.path.isfile.return_value = False
|
|
||||||
configtypes.UserStyleSheet().validate('foobar')
|
|
||||||
|
|
||||||
def test_validate_exists_abs(self, klass, os_mock):
|
def test_validate_exists_abs(self, klass, os_mock):
|
||||||
"""Test validate with a file which does exist."""
|
"""Test validate with a file which does exist."""
|
||||||
os_mock.path.isfile.return_value = True
|
os_mock.path.isfile.return_value = True
|
||||||
@ -1286,11 +1269,8 @@ class TestFileAndUserStyleSheet:
|
|||||||
|
|
||||||
@pytest.mark.parametrize('configtype, value, raises', [
|
@pytest.mark.parametrize('configtype, value, raises', [
|
||||||
(configtypes.File(), 'foobar', True),
|
(configtypes.File(), 'foobar', True),
|
||||||
(configtypes.UserStyleSheet(), 'foobar', False),
|
|
||||||
(configtypes.UserStyleSheet(), '\ud800', True),
|
|
||||||
(configtypes.File(required=False), 'foobar', False),
|
(configtypes.File(required=False), 'foobar', False),
|
||||||
], ids=['file-foobar', 'userstylesheet-foobar', 'userstylesheet-unicode',
|
], ids=['file-foobar', 'file-optional-foobar'])
|
||||||
'file-optional-foobar'])
|
|
||||||
def test_validate_rel_inexistent(self, os_mock, monkeypatch, configtype,
|
def test_validate_rel_inexistent(self, os_mock, monkeypatch, configtype,
|
||||||
value, raises):
|
value, raises):
|
||||||
"""Test with a relative path and standarddir.config returning None."""
|
"""Test with a relative path and standarddir.config returning None."""
|
||||||
@ -1339,7 +1319,6 @@ class TestFileAndUserStyleSheet:
|
|||||||
|
|
||||||
def test_transform_relative(self, klass, os_mock, monkeypatch):
|
def test_transform_relative(self, klass, os_mock, monkeypatch):
|
||||||
"""Test transform() with relative dir and an available configdir."""
|
"""Test transform() with relative dir and an available configdir."""
|
||||||
os_mock.path.exists.return_value = True # for TestUserStyleSheet
|
|
||||||
os_mock.path.isabs.return_value = False
|
os_mock.path.isabs.return_value = False
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
'qutebrowser.config.configtypes.standarddir.config',
|
'qutebrowser.config.configtypes.standarddir.config',
|
||||||
@ -1347,12 +1326,6 @@ class TestFileAndUserStyleSheet:
|
|||||||
expected = self._expected(klass, '/configdir/foo')
|
expected = self._expected(klass, '/configdir/foo')
|
||||||
assert klass().transform('foo') == expected
|
assert klass().transform('foo') == expected
|
||||||
|
|
||||||
def test_transform_userstylesheet_base64(self, monkeypatch):
|
|
||||||
"""Test transform with a data string."""
|
|
||||||
b64 = base64.b64encode(b"test").decode('ascii')
|
|
||||||
url = QUrl("data:text/css;base64,{}".format(b64))
|
|
||||||
assert configtypes.UserStyleSheet().transform("test") == url
|
|
||||||
|
|
||||||
|
|
||||||
class TestDirectory:
|
class TestDirectory:
|
||||||
|
|
||||||
|
@ -50,8 +50,7 @@ def gen_classes():
|
|||||||
@hypothesis.given(strategies.text())
|
@hypothesis.given(strategies.text())
|
||||||
@hypothesis.example('\x00')
|
@hypothesis.example('\x00')
|
||||||
def test_configtypes_hypothesis(klass, s):
|
def test_configtypes_hypothesis(klass, s):
|
||||||
if (klass in [configtypes.File, configtypes.UserStyleSheet] and
|
if (klass == configtypes.File and sys.platform == 'linux' and
|
||||||
sys.platform == 'linux' and
|
|
||||||
not os.environ.get('DISPLAY', '')):
|
not os.environ.get('DISPLAY', '')):
|
||||||
pytest.skip("No DISPLAY available")
|
pytest.skip("No DISPLAY available")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user