make keyboard view/controller interface more client/server friendly
This commit is contained in:
parent
8f8a87ad1c
commit
70fc2e64cd
|
@ -0,0 +1,83 @@
|
||||||
|
# Copyright 2020 Canonical, Ltd.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program 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 Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from subiquitycore.utils import arun_command
|
||||||
|
|
||||||
|
from subiquity.common.types import KeyboardSetting
|
||||||
|
|
||||||
|
|
||||||
|
etc_default_keyboard_template = """\
|
||||||
|
# KEYBOARD CONFIGURATION FILE
|
||||||
|
|
||||||
|
# Consult the keyboard(5) manual page.
|
||||||
|
|
||||||
|
XKBMODEL="pc105"
|
||||||
|
XKBLAYOUT="{layout}"
|
||||||
|
XKBVARIANT="{variant}"
|
||||||
|
XKBOPTIONS="{options}"
|
||||||
|
|
||||||
|
BACKSPACE="guess"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def from_config_file(config_file):
|
||||||
|
with open(config_file) as fp:
|
||||||
|
content = fp.read()
|
||||||
|
|
||||||
|
def optval(opt, default):
|
||||||
|
match = re.search(r'(?m)^\s*%s=(.*)$' % (opt,), content)
|
||||||
|
if match:
|
||||||
|
r = match.group(1).strip('"')
|
||||||
|
if r != '':
|
||||||
|
return r
|
||||||
|
return default
|
||||||
|
|
||||||
|
XKBLAYOUT = optval("XKBLAYOUT", "us")
|
||||||
|
XKBVARIANT = optval("XKBVARIANT", "")
|
||||||
|
XKBOPTIONS = optval("XKBOPTIONS", "")
|
||||||
|
toggle = None
|
||||||
|
for option in XKBOPTIONS.split(','):
|
||||||
|
if option.startswith('grp:'):
|
||||||
|
toggle = option[4:]
|
||||||
|
return KeyboardSetting(layout=XKBLAYOUT, variant=XKBVARIANT, toggle=toggle)
|
||||||
|
|
||||||
|
|
||||||
|
def render(setting):
|
||||||
|
options = ""
|
||||||
|
if setting.toggle:
|
||||||
|
options = "grp:" + setting.toggle
|
||||||
|
return etc_default_keyboard_template.format(
|
||||||
|
layout=setting.layout,
|
||||||
|
variant=setting.variant,
|
||||||
|
options=options)
|
||||||
|
|
||||||
|
|
||||||
|
async def set_keyboard(root, setting, dry_run):
|
||||||
|
path = os.path.join(root, 'etc', 'default', 'keyboard')
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
with open(path, 'w') as fp:
|
||||||
|
fp.write(render(setting))
|
||||||
|
cmds = [
|
||||||
|
['setupcon', '--save', '--force', '--keyboard-only'],
|
||||||
|
['/snap/bin/subiquity.subiquity-loadkeys'],
|
||||||
|
]
|
||||||
|
if dry_run:
|
||||||
|
scale = os.environ.get('SUBIQUITY_REPLAY_TIMESCALE', "1")
|
||||||
|
cmds = [['sleep', str(1/float(scale))]]
|
||||||
|
for cmd in cmds:
|
||||||
|
await arun_command(cmd)
|
|
@ -18,10 +18,11 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from subiquity.models.keyboard import (
|
from subiquity.common.keyboard import (
|
||||||
KeyboardModel,
|
from_config_file,
|
||||||
KeyboardSetting,
|
set_keyboard,
|
||||||
)
|
)
|
||||||
|
from subiquity.common.types import KeyboardSetting
|
||||||
|
|
||||||
|
|
||||||
class TestSubiquityModel(unittest.TestCase):
|
class TestSubiquityModel(unittest.TestCase):
|
||||||
|
@ -36,11 +37,10 @@ class TestSubiquityModel(unittest.TestCase):
|
||||||
async def t():
|
async def t():
|
||||||
os.environ['SUBIQUITY_REPLAY_TIMESCALE'] = '100'
|
os.environ['SUBIQUITY_REPLAY_TIMESCALE'] = '100'
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
model = KeyboardModel(tmpdir)
|
|
||||||
new_setting = KeyboardSetting('fr', 'azerty')
|
new_setting = KeyboardSetting('fr', 'azerty')
|
||||||
await model.set_keyboard(new_setting)
|
await set_keyboard(tmpdir, new_setting, True)
|
||||||
read_setting = KeyboardSetting.from_config_file(
|
read_setting = from_config_file(
|
||||||
model.config_path)
|
os.path.join(tmpdir, 'etc', 'default', 'keyboard'))
|
||||||
self.assertEqual(new_setting, read_setting)
|
self.assertEqual(new_setting, read_setting)
|
||||||
loop.run_until_complete(t())
|
loop.run_until_complete(t())
|
||||||
loop.close()
|
loop.close()
|
|
@ -19,11 +19,18 @@
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import enum
|
import enum
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class KeyboardSetting:
|
||||||
|
layout: str
|
||||||
|
variant: str = ''
|
||||||
|
toggle: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@attr.s(auto_attribs=True)
|
@attr.s(auto_attribs=True)
|
||||||
class IdentityData:
|
class IdentityData:
|
||||||
realname: str = ''
|
realname: str = ''
|
||||||
|
|
|
@ -17,10 +17,11 @@ import logging
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
from subiquitycore.async_helpers import schedule_task
|
|
||||||
from subiquitycore.context import with_context
|
from subiquitycore.context import with_context
|
||||||
|
|
||||||
|
from subiquity.common.keyboard import set_keyboard
|
||||||
from subiquity.controller import SubiquityTuiController
|
from subiquity.controller import SubiquityTuiController
|
||||||
|
from subiquity.keyboard import KeyboardList
|
||||||
from subiquity.models.keyboard import KeyboardSetting
|
from subiquity.models.keyboard import KeyboardSetting
|
||||||
from subiquity.ui.views import KeyboardView
|
from subiquity.ui.views import KeyboardView
|
||||||
|
|
||||||
|
@ -44,42 +45,57 @@ class KeyboardController(SubiquityTuiController):
|
||||||
('l10n:language-selected', 'language_selected'),
|
('l10n:language-selected', 'language_selected'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.needs_set_keyboard = False
|
||||||
|
super().__init__(app)
|
||||||
|
self.keyboard_list = KeyboardList()
|
||||||
|
|
||||||
def load_autoinstall_data(self, data):
|
def load_autoinstall_data(self, data):
|
||||||
if data is not None:
|
if data is None:
|
||||||
self.model.setting = KeyboardSetting(**data)
|
return
|
||||||
|
setting = KeyboardSetting(**data)
|
||||||
|
if self.model.setting != setting:
|
||||||
|
self.needs_set_keyboard = True
|
||||||
|
self.model.setting = setting
|
||||||
|
|
||||||
@with_context()
|
@with_context()
|
||||||
async def apply_autoinstall_config(self, context):
|
async def apply_autoinstall_config(self, context):
|
||||||
await self.model.set_keyboard(self.model.setting)
|
if self.needs_set_keyboard:
|
||||||
|
await set_keyboard(
|
||||||
|
self.app.root, self.model.setting, self.opts.dry_run)
|
||||||
|
|
||||||
def language_selected(self, code):
|
def language_selected(self, code):
|
||||||
log.debug("language_selected %s", code)
|
log.debug("language_selected %s", code)
|
||||||
if not self.model.has_language(code):
|
if not self.keyboard_list.has_language(code):
|
||||||
code = code.split('_')[0]
|
code = code.split('_')[0]
|
||||||
if not self.model.has_language(code):
|
if not self.keyboard_list.has_language(code):
|
||||||
code = 'C'
|
code = 'C'
|
||||||
log.debug("loading launguage %s", code)
|
log.debug("loading language %s", code)
|
||||||
self.model.load_language(code)
|
self.keyboard_list.load_language(code)
|
||||||
|
|
||||||
def make_ui(self):
|
def make_ui(self):
|
||||||
if self.model.current_lang is None:
|
if self.keyboard_list.current_lang is None:
|
||||||
self.model.load_language('C')
|
self.keyboard_list.load_language('C')
|
||||||
return KeyboardView(self.model, self, self.opts)
|
return KeyboardView(self, self.model.setting)
|
||||||
|
|
||||||
def run_answers(self):
|
def run_answers(self):
|
||||||
if 'layout' in self.answers:
|
if 'layout' in self.answers:
|
||||||
layout = self.answers['layout']
|
layout = self.answers['layout']
|
||||||
variant = self.answers.get('variant', '')
|
variant = self.answers.get('variant', '')
|
||||||
self.done(KeyboardSetting(layout=layout, variant=variant))
|
self.done(KeyboardSetting(layout=layout, variant=variant), True)
|
||||||
|
|
||||||
async def apply_settings(self, setting):
|
async def set_keyboard(self, setting):
|
||||||
await self.model.set_keyboard(setting)
|
await set_keyboard(self.app.root, setting, self.opts.dry_run)
|
||||||
log.debug("KeyboardController next_screen")
|
self.done(setting, False)
|
||||||
self.configured()
|
|
||||||
self.app.next_screen()
|
|
||||||
|
|
||||||
def done(self, setting):
|
def done(self, setting, apply):
|
||||||
schedule_task(self.apply_settings(setting))
|
log.debug("KeyboardController.done %s next_screen", setting)
|
||||||
|
if apply:
|
||||||
|
self.app.aio_loop.create_task(self.set_keyboard(setting))
|
||||||
|
else:
|
||||||
|
self.model.setting = setting
|
||||||
|
self.configured()
|
||||||
|
self.app.next_screen()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
self.app.prev_screen()
|
self.app.prev_screen()
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Copyright 2020 Canonical, Ltd.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program 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 Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
import os
|
||||||
|
|
||||||
|
from subiquity.common.types import KeyboardSetting
|
||||||
|
|
||||||
|
|
||||||
|
# Non-latin keyboard layouts that are handled in a uniform way
|
||||||
|
standard_non_latin_layouts = set(
|
||||||
|
('af', 'am', 'ara', 'ben', 'bd', 'bg', 'bt', 'by', 'et', 'ge',
|
||||||
|
'gh', 'gr', 'guj', 'guru', 'il', 'in', 'iq', 'ir', 'iku', 'kan',
|
||||||
|
'kh', 'kz', 'la', 'lao', 'lk', 'kg', 'ma', 'mk', 'mm', 'mn', 'mv',
|
||||||
|
'mal', 'np', 'ori', 'pk', 'ru', 'scc', 'sy', 'syr', 'tel', 'th',
|
||||||
|
'tj', 'tam', 'tib', 'ua', 'ug', 'uz')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def latinizable(setting):
|
||||||
|
"""
|
||||||
|
If this setting does not allow the typing of latin characters,
|
||||||
|
return a setting that can be switched to one that can.
|
||||||
|
"""
|
||||||
|
if setting.layout == 'rs':
|
||||||
|
if setting.variant.startswith('latin'):
|
||||||
|
return setting
|
||||||
|
else:
|
||||||
|
if setting.variant == 'yz':
|
||||||
|
new_variant = 'latinyz'
|
||||||
|
elif setting.variant == 'alternatequotes':
|
||||||
|
new_variant = 'latinalternatequotes'
|
||||||
|
else:
|
||||||
|
new_variant = 'latin'
|
||||||
|
return KeyboardSetting(layout='rs,rs',
|
||||||
|
variant=(new_variant +
|
||||||
|
',' + setting.variant))
|
||||||
|
elif setting.layout == 'jp':
|
||||||
|
if setting.variant in ('106', 'common', 'OADG109A',
|
||||||
|
'nicola_f_bs', ''):
|
||||||
|
return setting
|
||||||
|
else:
|
||||||
|
return KeyboardSetting(layout='jp,jp',
|
||||||
|
variant=',' + setting.variant)
|
||||||
|
elif setting.layout == 'lt':
|
||||||
|
if setting.variant == 'us':
|
||||||
|
return KeyboardSetting(layout='lt,lt', variant='us,')
|
||||||
|
else:
|
||||||
|
return KeyboardSetting(layout='lt,lt',
|
||||||
|
variant=setting.variant + ',us')
|
||||||
|
elif setting.layout == 'me':
|
||||||
|
if setting.variant == 'basic' or setting.variant.startswith('latin'):
|
||||||
|
return setting
|
||||||
|
else:
|
||||||
|
return KeyboardSetting(layout='me,me',
|
||||||
|
variant=setting.variant + ',us')
|
||||||
|
elif setting.layout in standard_non_latin_layouts:
|
||||||
|
return KeyboardSetting(layout='us,' + setting.layout,
|
||||||
|
variant=',' + setting.variant)
|
||||||
|
else:
|
||||||
|
return setting
|
||||||
|
|
||||||
|
|
||||||
|
def for_ui(setting):
|
||||||
|
"""
|
||||||
|
Attempt to guess a setting the user chose which resulted in the
|
||||||
|
current config. Basically the inverse of latinizable().
|
||||||
|
"""
|
||||||
|
if ',' in setting.layout:
|
||||||
|
layout1, layout2 = setting.layout.split(',', 1)
|
||||||
|
else:
|
||||||
|
layout1, layout2 = setting.layout, ''
|
||||||
|
if ',' in setting.variant:
|
||||||
|
variant1, variant2 = setting.variant.split(',', 1)
|
||||||
|
else:
|
||||||
|
variant1, variant2 = setting.variant, ''
|
||||||
|
if setting.layout == 'lt,lt':
|
||||||
|
layout = layout1
|
||||||
|
variant = variant1
|
||||||
|
elif setting.layout in ('rs,rs', 'us,rs', 'jp,jp', 'us,jp'):
|
||||||
|
layout = layout2
|
||||||
|
variant = variant2
|
||||||
|
elif layout1 == 'us' and layout2 in standard_non_latin_layouts:
|
||||||
|
layout = layout2
|
||||||
|
variant = variant2
|
||||||
|
elif ',' in setting.layout:
|
||||||
|
# Something unrecognized
|
||||||
|
layout = 'us'
|
||||||
|
variant = ''
|
||||||
|
else:
|
||||||
|
return setting
|
||||||
|
return KeyboardSetting(layout=layout, variant=variant)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyboardList:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._kbnames_file = os.path.join(
|
||||||
|
os.environ.get("SNAP", '.'),
|
||||||
|
'kbdnames.txt')
|
||||||
|
self._clear()
|
||||||
|
|
||||||
|
def has_language(self, code):
|
||||||
|
self.load_language(code)
|
||||||
|
return bool(self.layouts)
|
||||||
|
|
||||||
|
def load_language(self, code):
|
||||||
|
if code == self.current_lang:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._clear()
|
||||||
|
|
||||||
|
with open(self._kbnames_file, encoding='utf-8') as kbdnames:
|
||||||
|
self._load_file(code, kbdnames)
|
||||||
|
self.current_lang = code
|
||||||
|
|
||||||
|
def _clear(self):
|
||||||
|
self.current_lang = None
|
||||||
|
self.layouts = {}
|
||||||
|
self.variants = defaultdict(dict)
|
||||||
|
|
||||||
|
def _load_file(self, code, kbdnames):
|
||||||
|
for line in kbdnames:
|
||||||
|
line = line.rstrip('\n')
|
||||||
|
got_lang, element, name, value = line.split("*", 3)
|
||||||
|
if got_lang != code:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if element == "layout":
|
||||||
|
self.layouts[name] = value
|
||||||
|
elif element == "variant":
|
||||||
|
variantname, variantdesc = value.split("*", 1)
|
||||||
|
self.variants[name][variantname] = variantdesc
|
||||||
|
|
||||||
|
def lookup(self, code):
|
||||||
|
if ':' in code:
|
||||||
|
layout_code, variant_code = code.split(":", 1)
|
||||||
|
layout = self.layouts.get(layout_code, '?')
|
||||||
|
variant = self.variants.get(layout_code, {}).get(variant_code, '?')
|
||||||
|
return (layout, variant)
|
||||||
|
else:
|
||||||
|
return self.layouts.get(code, '?'), None
|
|
@ -1,224 +1,43 @@
|
||||||
|
# Copyright 2020 Canonical, Ltd.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program 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 Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
|
|
||||||
import attr
|
from subiquity.common.keyboard import from_config_file, render
|
||||||
|
from subiquity.common.types import KeyboardSetting
|
||||||
from subiquitycore.utils import arun_command
|
|
||||||
|
|
||||||
log = logging.getLogger("subiquity.models.keyboard")
|
log = logging.getLogger("subiquity.models.keyboard")
|
||||||
|
|
||||||
etc_default_keyboard_template = """\
|
|
||||||
# KEYBOARD CONFIGURATION FILE
|
|
||||||
|
|
||||||
# Consult the keyboard(5) manual page.
|
|
||||||
|
|
||||||
XKBMODEL="pc105"
|
|
||||||
XKBLAYOUT="{layout}"
|
|
||||||
XKBVARIANT="{variant}"
|
|
||||||
XKBOPTIONS="{options}"
|
|
||||||
|
|
||||||
BACKSPACE="guess"
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class KeyboardSetting:
|
|
||||||
layout = attr.ib()
|
|
||||||
variant = attr.ib(default='')
|
|
||||||
toggle = attr.ib(default=None)
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
options = ""
|
|
||||||
if self.toggle:
|
|
||||||
options = "grp:" + self.toggle
|
|
||||||
return etc_default_keyboard_template.format(
|
|
||||||
layout=self.layout, variant=self.variant, options=options)
|
|
||||||
|
|
||||||
def latinizable(self):
|
|
||||||
"""
|
|
||||||
If this setting does not allow the typing of latin characters,
|
|
||||||
return a setting that can be switched to one that can.
|
|
||||||
"""
|
|
||||||
if self.layout == 'rs':
|
|
||||||
if self.variant.startswith('latin'):
|
|
||||||
return self
|
|
||||||
else:
|
|
||||||
if self.variant == 'yz':
|
|
||||||
new_variant = 'latinyz'
|
|
||||||
elif self.variant == 'alternatequotes':
|
|
||||||
new_variant = 'latinalternatequotes'
|
|
||||||
else:
|
|
||||||
new_variant = 'latin'
|
|
||||||
return KeyboardSetting(layout='rs,rs',
|
|
||||||
variant=(new_variant +
|
|
||||||
',' + self.variant))
|
|
||||||
elif self.layout == 'jp':
|
|
||||||
if self.variant in ('106', 'common', 'OADG109A',
|
|
||||||
'nicola_f_bs', ''):
|
|
||||||
return self
|
|
||||||
else:
|
|
||||||
return KeyboardSetting(layout='jp,jp',
|
|
||||||
variant=',' + self.variant)
|
|
||||||
elif self.layout == 'lt':
|
|
||||||
if self.variant == 'us':
|
|
||||||
return KeyboardSetting(layout='lt,lt', variant='us,')
|
|
||||||
else:
|
|
||||||
return KeyboardSetting(layout='lt,lt',
|
|
||||||
variant=self.variant + ',us')
|
|
||||||
elif self.layout == 'me':
|
|
||||||
if self.variant == 'basic' or self.variant.startswith('latin'):
|
|
||||||
return self
|
|
||||||
else:
|
|
||||||
return KeyboardSetting(layout='me,me',
|
|
||||||
variant=self.variant + ',us')
|
|
||||||
elif self.layout in standard_non_latin_layouts:
|
|
||||||
return KeyboardSetting(layout='us,' + self.layout,
|
|
||||||
variant=',' + self.variant)
|
|
||||||
else:
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_config_file(cls, config_file):
|
|
||||||
with open(config_file) as fp:
|
|
||||||
content = fp.read()
|
|
||||||
|
|
||||||
def optval(opt, default):
|
|
||||||
match = re.search(r'(?m)^\s*%s=(.*)$' % (opt,), content)
|
|
||||||
if match:
|
|
||||||
r = match.group(1).strip('"')
|
|
||||||
if r != '':
|
|
||||||
return r
|
|
||||||
return default
|
|
||||||
XKBLAYOUT = optval("XKBLAYOUT", "us")
|
|
||||||
XKBVARIANT = optval("XKBVARIANT", "")
|
|
||||||
XKBOPTIONS = optval("XKBOPTIONS", "")
|
|
||||||
toggle = None
|
|
||||||
for option in XKBOPTIONS.split(','):
|
|
||||||
if option.startswith('grp:'):
|
|
||||||
toggle = option[4:]
|
|
||||||
return cls(layout=XKBLAYOUT, variant=XKBVARIANT, toggle=toggle)
|
|
||||||
|
|
||||||
def for_ui(self):
|
|
||||||
"""
|
|
||||||
Attempt to guess a setting the user chose which resulted in the
|
|
||||||
current config. Basically the inverse of latinizable().
|
|
||||||
"""
|
|
||||||
if ',' in self.layout:
|
|
||||||
layout1, layout2 = self.layout.split(',', 1)
|
|
||||||
else:
|
|
||||||
layout1, layout2 = self.layout, ''
|
|
||||||
if ',' in self.variant:
|
|
||||||
variant1, variant2 = self.variant.split(',', 1)
|
|
||||||
else:
|
|
||||||
variant1, variant2 = self.variant, ''
|
|
||||||
if self.layout == 'lt,lt':
|
|
||||||
layout = layout1
|
|
||||||
variant = variant1
|
|
||||||
elif self.layout in ('rs,rs', 'us,rs', 'jp,jp', 'us,jp'):
|
|
||||||
layout = layout2
|
|
||||||
variant = variant2
|
|
||||||
elif layout1 == 'us' and layout2 in standard_non_latin_layouts:
|
|
||||||
layout = layout2
|
|
||||||
variant = variant2
|
|
||||||
elif ',' in self.layout:
|
|
||||||
# Something unrecognized
|
|
||||||
layout = 'us'
|
|
||||||
variant = ''
|
|
||||||
else:
|
|
||||||
return self
|
|
||||||
return KeyboardSetting(layout=layout, variant=variant)
|
|
||||||
|
|
||||||
|
|
||||||
# Non-latin keyboard layouts that are handled in a uniform way
|
|
||||||
standard_non_latin_layouts = set(
|
|
||||||
('af', 'am', 'ara', 'ben', 'bd', 'bg', 'bt', 'by', 'et', 'ge',
|
|
||||||
'gh', 'gr', 'guj', 'guru', 'il', 'in', 'iq', 'ir', 'iku', 'kan',
|
|
||||||
'kh', 'kz', 'la', 'lao', 'lk', 'kg', 'ma', 'mk', 'mm', 'mn', 'mv',
|
|
||||||
'mal', 'np', 'ori', 'pk', 'ru', 'scc', 'sy', 'syr', 'tel', 'th',
|
|
||||||
'tj', 'tam', 'tib', 'ua', 'ug', 'uz')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class KeyboardModel:
|
class KeyboardModel:
|
||||||
|
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self._kbnames_file = os.path.join(os.environ.get("SNAP", '.'),
|
config_path = os.path.join(self.root, 'etc', 'default', 'keyboard')
|
||||||
'kbdnames.txt')
|
if os.path.exists(config_path):
|
||||||
self._clear()
|
self.setting = from_config_file(config_path)
|
||||||
if os.path.exists(self.config_path):
|
|
||||||
self.setting = KeyboardSetting.from_config_file(self.config_path)
|
|
||||||
else:
|
else:
|
||||||
self.setting = KeyboardSetting(layout='us')
|
self.setting = KeyboardSetting(layout='us')
|
||||||
|
|
||||||
@property
|
|
||||||
def config_path(self):
|
|
||||||
return os.path.join(self.root, 'etc', 'default', 'keyboard')
|
|
||||||
|
|
||||||
def has_language(self, code):
|
|
||||||
self.load_language(code)
|
|
||||||
return bool(self.layouts)
|
|
||||||
|
|
||||||
def load_language(self, code):
|
|
||||||
if code == self.current_lang:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._clear()
|
|
||||||
|
|
||||||
with open(self._kbnames_file, encoding='utf-8') as kbdnames:
|
|
||||||
self._load_file(code, kbdnames)
|
|
||||||
self.current_lang = code
|
|
||||||
|
|
||||||
def _clear(self):
|
|
||||||
self.current_lang = None
|
|
||||||
self.layouts = {}
|
|
||||||
self.variants = defaultdict(dict)
|
|
||||||
|
|
||||||
def _load_file(self, code, kbdnames):
|
|
||||||
for line in kbdnames:
|
|
||||||
line = line.rstrip('\n')
|
|
||||||
got_lang, element, name, value = line.split("*", 3)
|
|
||||||
if got_lang != code:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if element == "layout":
|
|
||||||
self.layouts[name] = value
|
|
||||||
elif element == "variant":
|
|
||||||
variantname, variantdesc = value.split("*", 1)
|
|
||||||
self.variants[name][variantname] = variantdesc
|
|
||||||
|
|
||||||
def lookup(self, code):
|
|
||||||
if ':' in code:
|
|
||||||
layout_code, variant_code = code.split(":", 1)
|
|
||||||
layout = self.layouts.get(layout_code, '?')
|
|
||||||
variant = self.variants.get(layout_code, {}).get(variant_code, '?')
|
|
||||||
return (layout, variant)
|
|
||||||
else:
|
|
||||||
return self.layouts.get(code, '?'), None
|
|
||||||
|
|
||||||
async def set_keyboard(self, setting):
|
|
||||||
path = self.config_path
|
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
||||||
with open(path, 'w') as fp:
|
|
||||||
fp.write(setting.render())
|
|
||||||
if setting != self.setting:
|
|
||||||
self.setting = setting
|
|
||||||
if self.root == '/':
|
|
||||||
await arun_command([
|
|
||||||
'setupcon', '--save', '--force', '--keyboard-only'])
|
|
||||||
await arun_command(['/snap/bin/subiquity.subiquity-loadkeys'])
|
|
||||||
else:
|
|
||||||
scale = os.environ.get('SUBIQUITY_REPLAY_TIMESCALE', "1")
|
|
||||||
await arun_command(['sleep', str(1/float(scale))])
|
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
return {
|
return {
|
||||||
'write_files': {
|
'write_files': {
|
||||||
'etc_default_keyboard': {
|
'etc_default_keyboard': {
|
||||||
'path': 'etc/default/keyboard',
|
'path': 'etc/default/keyboard',
|
||||||
'content': self.setting.render(),
|
'content': render(self.setting),
|
||||||
'permissions': 0o644,
|
'permissions': 0o644,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -44,7 +44,8 @@ from subiquitycore.ui.stretchy import (
|
||||||
from subiquitycore.ui.utils import button_pile, Color, Padding, screen
|
from subiquitycore.ui.utils import button_pile, Color, Padding, screen
|
||||||
from subiquitycore.view import BaseView
|
from subiquitycore.view import BaseView
|
||||||
|
|
||||||
from subiquity.models.keyboard import KeyboardSetting
|
from subiquity.common.types import KeyboardSetting
|
||||||
|
from subiquity.keyboard import latinizable, for_ui
|
||||||
from subiquity.ui.views import pc105
|
from subiquity.ui.views import pc105
|
||||||
|
|
||||||
log = logging.getLogger("subiquity.ui.views.keyboard")
|
log = logging.getLogger("subiquity.ui.views.keyboard")
|
||||||
|
@ -129,8 +130,8 @@ another layout or run the automated detection again.
|
||||||
self.keyboard_detector.keyboard_view.found_layout(self.step.result)
|
self.keyboard_detector.keyboard_view.found_layout(self.step.result)
|
||||||
|
|
||||||
def make_body(self):
|
def make_body(self):
|
||||||
model = self.keyboard_detector.keyboard_view.model
|
kl = self.keyboard_detector.keyboard_view.keyboard_list
|
||||||
layout, variant = model.lookup(self.step.result)
|
layout, variant = kl.lookup(self.step.result)
|
||||||
var_desc = []
|
var_desc = []
|
||||||
layout_text = _("Layout")
|
layout_text = _("Layout")
|
||||||
var_text = _("Variant")
|
var_text = _("Variant")
|
||||||
|
@ -340,9 +341,9 @@ class ToggleQuestion(Stretchy):
|
||||||
self.setting = setting
|
self.setting = setting
|
||||||
self.selector = Selector(toggle_options)
|
self.selector = Selector(toggle_options)
|
||||||
self.selector.value = 'alt_shift_toggle'
|
self.selector.value = 'alt_shift_toggle'
|
||||||
if self.parent.model.setting.toggle:
|
if self.parent.initial_setting.toggle:
|
||||||
try:
|
try:
|
||||||
self.selector.value = self.parent.model.setting.toggle
|
self.selector.value = self.parent.initial_setting.toggle
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -386,21 +387,21 @@ class KeyboardView(BaseView):
|
||||||
|
|
||||||
title = _("Keyboard configuration")
|
title = _("Keyboard configuration")
|
||||||
|
|
||||||
def __init__(self, model, controller, opts):
|
def __init__(self, controller, initial_setting):
|
||||||
self.model = model
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.opts = opts
|
self.keyboard_list = controller.keyboard_list
|
||||||
|
self.initial_setting = initial_setting
|
||||||
|
|
||||||
self.form = KeyboardForm()
|
self.form = KeyboardForm()
|
||||||
opts = []
|
opts = []
|
||||||
for layout, desc in model.layouts.items():
|
for layout, desc in self.keyboard_list.layouts.items():
|
||||||
opts.append(Option((desc, True, layout)))
|
opts.append(Option((desc, True, layout)))
|
||||||
opts.sort(key=lambda o: o.label.text)
|
opts.sort(key=lambda o: o.label.text)
|
||||||
connect_signal(self.form, 'submit', self.done)
|
connect_signal(self.form, 'submit', self.done)
|
||||||
connect_signal(self.form, 'cancel', self.cancel)
|
connect_signal(self.form, 'cancel', self.cancel)
|
||||||
connect_signal(self.form.layout.widget, "select", self.select_layout)
|
connect_signal(self.form.layout.widget, "select", self.select_layout)
|
||||||
self.form.layout.widget.options = opts
|
self.form.layout.widget.options = opts
|
||||||
setting = model.setting.for_ui()
|
setting = for_ui(initial_setting)
|
||||||
try:
|
try:
|
||||||
self.form.layout.widget.value = setting.layout
|
self.form.layout.widget.value = setting.layout
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -413,7 +414,7 @@ class KeyboardView(BaseView):
|
||||||
# Don't crash on pre-existing invalid config.
|
# Don't crash on pre-existing invalid config.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.opts.run_on_serial:
|
if self.controller.opts.run_on_serial:
|
||||||
excerpt = _('Please select the layout of the keyboard directly '
|
excerpt = _('Please select the layout of the keyboard directly '
|
||||||
'attached to the system, if any.')
|
'attached to the system, if any.')
|
||||||
else:
|
else:
|
||||||
|
@ -422,7 +423,7 @@ class KeyboardView(BaseView):
|
||||||
'automatically.')
|
'automatically.')
|
||||||
|
|
||||||
lb_contents = self.form.as_rows()
|
lb_contents = self.form.as_rows()
|
||||||
if not self.opts.run_on_serial:
|
if not self.controller.opts.run_on_serial:
|
||||||
lb_contents.extend([
|
lb_contents.extend([
|
||||||
Text(""),
|
Text(""),
|
||||||
button_pile([
|
button_pile([
|
||||||
|
@ -457,17 +458,19 @@ class KeyboardView(BaseView):
|
||||||
if self.form.variant.widget.value is not None:
|
if self.form.variant.widget.value is not None:
|
||||||
variant = self.form.variant.widget.value
|
variant = self.form.variant.widget.value
|
||||||
setting = KeyboardSetting(layout=layout, variant=variant)
|
setting = KeyboardSetting(layout=layout, variant=variant)
|
||||||
new_setting = setting.latinizable()
|
new_setting = latinizable(setting)
|
||||||
if new_setting != setting:
|
if new_setting != setting:
|
||||||
self.show_stretchy_overlay(ToggleQuestion(self, new_setting))
|
self.show_stretchy_overlay(ToggleQuestion(self, new_setting))
|
||||||
return
|
return
|
||||||
self.really_done(setting)
|
self.really_done(setting)
|
||||||
|
|
||||||
def really_done(self, setting):
|
def really_done(self, setting):
|
||||||
if setting != self.model.setting:
|
apply = False
|
||||||
|
if setting != self.initial_setting:
|
||||||
|
apply = True
|
||||||
ac = ApplyingConfig(self.controller.app.aio_loop)
|
ac = ApplyingConfig(self.controller.app.aio_loop)
|
||||||
self.show_overlay(ac, width=ac.width, min_width=None)
|
self.show_overlay(ac, width=ac.width, min_width=None)
|
||||||
self.controller.done(setting)
|
self.controller.done(setting, apply=apply)
|
||||||
|
|
||||||
def cancel(self, result=None):
|
def cancel(self, result=None):
|
||||||
self.controller.cancel()
|
self.controller.cancel()
|
||||||
|
@ -477,7 +480,7 @@ class KeyboardView(BaseView):
|
||||||
log.debug("select_layout %s", layout)
|
log.debug("select_layout %s", layout)
|
||||||
opts = []
|
opts = []
|
||||||
default_i = -1
|
default_i = -1
|
||||||
layout_items = enumerate(self.model.variants[layout].items())
|
layout_items = enumerate(self.keyboard_list.variants[layout].items())
|
||||||
for i, (variant, variant_desc) in layout_items:
|
for i, (variant, variant_desc) in layout_items:
|
||||||
if variant == "":
|
if variant == "":
|
||||||
default_i = i
|
default_i = i
|
||||||
|
|
Loading…
Reference in New Issue