Merge pull request #304 from CanonicalLtd/mwhudson/non-latin-keyboard-setting
handle non-latin keyboard layouts more intelligently
This commit is contained in:
commit
b98244b8c8
|
@ -17,6 +17,7 @@ import logging
|
||||||
|
|
||||||
from subiquitycore.controller import BaseController
|
from subiquitycore.controller import BaseController
|
||||||
|
|
||||||
|
from subiquity.models.keyboard import KeyboardSetting
|
||||||
from subiquity.ui.views import KeyboardView
|
from subiquity.ui.views import KeyboardView
|
||||||
|
|
||||||
log = logging.getLogger('subiquity.controllers.keyboard')
|
log = logging.getLogger('subiquity.controllers.keyboard')
|
||||||
|
@ -57,15 +58,15 @@ class KeyboardController(BaseController):
|
||||||
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(layout, variant)
|
self.done(KeyboardSetting(layout=layout, variant=variant))
|
||||||
|
|
||||||
def done(self, layout, variant):
|
def done(self, setting):
|
||||||
self.run_in_bg(
|
self.run_in_bg(
|
||||||
lambda: self.model.set_keyboard(layout, variant),
|
lambda: self.model.set_keyboard(setting),
|
||||||
self._done)
|
self._done)
|
||||||
|
|
||||||
def _done(self, fut):
|
def _done(self, fut):
|
||||||
self.signal.emit_signal('next-screen')
|
self.loop.set_alarm_in(0.0, lambda loop, ud: self.signal.emit_signal('next-screen'))
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
self.signal.emit_signal('prev-screen')
|
self.signal.emit_signal('prev-screen')
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import gzip
|
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
from subiquitycore.utils import run_command
|
from subiquitycore.utils import run_command
|
||||||
|
|
||||||
log = logging.getLogger("subiquity.models.keyboard")
|
log = logging.getLogger("subiquity.models.keyboard")
|
||||||
|
@ -18,41 +18,135 @@ etc_default_keyboard_template = """\
|
||||||
XKBMODEL="pc105"
|
XKBMODEL="pc105"
|
||||||
XKBLAYOUT="{layout}"
|
XKBLAYOUT="{layout}"
|
||||||
XKBVARIANT="{variant}"
|
XKBVARIANT="{variant}"
|
||||||
XKBOPTIONS=""
|
XKBOPTIONS="{options}"
|
||||||
|
|
||||||
BACKSPACE="guess"
|
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
|
||||||
|
variant = self.variant
|
||||||
|
return etc_default_keyboard_template.format(
|
||||||
|
layout=self.layout, variant=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):
|
||||||
|
content = open(config_file).read()
|
||||||
|
def optval(opt, default):
|
||||||
|
match = re.search('(?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.layout = 'us'
|
|
||||||
self.variant = ''
|
|
||||||
self._kbnames_file = os.path.join(os.environ.get("SNAP", '.'), 'kbdnames.txt')
|
self._kbnames_file = os.path.join(os.environ.get("SNAP", '.'), 'kbdnames.txt')
|
||||||
self._clear()
|
self._clear()
|
||||||
if os.path.exists(self.config_path):
|
if os.path.exists(self.config_path):
|
||||||
content = open(self.config_path).read()
|
self.setting = KeyboardSetting.from_config_file(self.config_path)
|
||||||
pat_tmpl = '(?m)^\s*%s=(.*)$'
|
else:
|
||||||
log.debug("%r", content)
|
self.setting = KeyboardSetting(layout='us')
|
||||||
layout_match = re.search(pat_tmpl%("XKBLAYOUT",), content)
|
|
||||||
if layout_match:
|
|
||||||
log.debug("%s", layout_match)
|
|
||||||
self.layout = layout_match.group(1).strip('"')
|
|
||||||
variant_match = re.search(pat_tmpl%("XKBVARIANT",), content)
|
|
||||||
if variant_match:
|
|
||||||
log.debug("%s", variant_match)
|
|
||||||
self.variant = variant_match.group(1).strip('"')
|
|
||||||
if self.variant == '':
|
|
||||||
self.variant = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_path(self):
|
def config_path(self):
|
||||||
return os.path.join(self.root, 'etc', 'default', 'keyboard')
|
return os.path.join(self.root, 'etc', 'default', 'keyboard')
|
||||||
|
|
||||||
@property
|
|
||||||
def config_content(self):
|
|
||||||
return etc_default_keyboard_template.format(layout=self.layout, variant=self.variant)
|
|
||||||
|
|
||||||
def has_language(self, code):
|
def has_language(self, code):
|
||||||
self.load_language(code)
|
self.load_language(code)
|
||||||
return bool(self.layouts)
|
return bool(self.layouts)
|
||||||
|
@ -88,17 +182,16 @@ class KeyboardModel:
|
||||||
def lookup(self, code):
|
def lookup(self, code):
|
||||||
if ':' in code:
|
if ':' in code:
|
||||||
layout_code, variant_code = code.split(":", 1)
|
layout_code, variant_code = code.split(":", 1)
|
||||||
return self.layouts.get(layout_code, '?'), self.variants.get(layout_code).get(variant_code, '?')
|
return self.layouts.get(layout_code, '?'), self.variants.get(layout_code, {}).get(variant_code, '?')
|
||||||
else:
|
else:
|
||||||
return self.layouts.get(code, '?'), None
|
return self.layouts.get(code, '?'), None
|
||||||
|
|
||||||
def set_keyboard(self, layout, variant):
|
def set_keyboard(self, setting):
|
||||||
path = self.config_path
|
path = self.config_path
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
self.layout = layout
|
self.setting = setting
|
||||||
self.variant = variant
|
|
||||||
with open(path, 'w') as fp:
|
with open(path, 'w') as fp:
|
||||||
fp.write(self.config_content)
|
fp.write(self.setting.render())
|
||||||
if self.root == '/':
|
if self.root == '/':
|
||||||
run_command(['setupcon', '--save', '--force'])
|
run_command(['setupcon', '--save', '--force'])
|
||||||
run_command(['/snap/bin/subiquity.subiquity-loadkeys'])
|
run_command(['/snap/bin/subiquity.subiquity-loadkeys'])
|
||||||
|
|
|
@ -129,7 +129,7 @@ class SubiquityModel:
|
||||||
'write_files': {
|
'write_files': {
|
||||||
'etc_default_keyboard': {
|
'etc_default_keyboard': {
|
||||||
'path': 'etc/default/keyboard',
|
'path': 'etc/default/keyboard',
|
||||||
'content': self.keyboard.config_content,
|
'content': self.keyboard.setting.render(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,17 @@ import logging
|
||||||
from urwid import (
|
from urwid import (
|
||||||
connect_signal,
|
connect_signal,
|
||||||
LineBox,
|
LineBox,
|
||||||
|
Padding as UrwidPadding,
|
||||||
|
SolidFill,
|
||||||
Text,
|
Text,
|
||||||
WidgetWrap,
|
WidgetWrap,
|
||||||
)
|
)
|
||||||
|
|
||||||
from subiquitycore.ui.buttons import ok_btn, other_btn
|
from subiquitycore.ui.buttons import (
|
||||||
|
cancel_btn,
|
||||||
|
ok_btn,
|
||||||
|
other_btn,
|
||||||
|
)
|
||||||
from subiquitycore.ui.container import (
|
from subiquitycore.ui.container import (
|
||||||
Columns,
|
Columns,
|
||||||
ListBox,
|
ListBox,
|
||||||
|
@ -32,10 +38,11 @@ from subiquitycore.ui.form import (
|
||||||
ChoiceField,
|
ChoiceField,
|
||||||
Form,
|
Form,
|
||||||
)
|
)
|
||||||
from subiquitycore.ui.selector import Option
|
from subiquitycore.ui.selector import Selector, Option
|
||||||
from subiquitycore.ui.utils import button_pile, Color, Padding
|
from subiquitycore.ui.utils import button_pile, Color, Padding
|
||||||
from subiquitycore.view import BaseView
|
from subiquitycore.view import BaseView
|
||||||
|
|
||||||
|
from subiquity.models.keyboard import KeyboardSetting
|
||||||
from subiquity.ui.spinner import Spinner
|
from subiquity.ui.spinner import Spinner
|
||||||
from subiquity.ui.views import pc105
|
from subiquity.ui.views import pc105
|
||||||
|
|
||||||
|
@ -266,6 +273,80 @@ class ApplyingConfig(WidgetWrap):
|
||||||
])))
|
])))
|
||||||
|
|
||||||
|
|
||||||
|
toggle_text = _("""\
|
||||||
|
You will need a way to toggle the keyboard between the national layout and the standard Latin layout.
|
||||||
|
|
||||||
|
Right Alt or Caps Lock keys are often chosen for ergonomic reasons (in the latter case, use the combination Shift+Caps Lock for normal Caps toggle). Alt+Shift is also a popular combination; it will however lose its usual behavior in Emacs and other programs that use it for specific needs.
|
||||||
|
|
||||||
|
Not all listed keys are present on all keyboards. """)
|
||||||
|
|
||||||
|
|
||||||
|
toggle_options = [
|
||||||
|
(_('Caps Lock'), True, 'caps_toggle'),
|
||||||
|
(_('Right Alt (AltGr)'), True, 'toggle'),
|
||||||
|
(_('Right Control'), True, 'rctrl_toggle'),
|
||||||
|
(_('Right Shift'), True, 'rshift_toggle'),
|
||||||
|
(_('Right Logo key'), True, 'rwin_toggle'),
|
||||||
|
(_('Menu key'), True, 'menu_toggle'),
|
||||||
|
(_('Alt+Shift'), True, 'alt_shift_toggle'),
|
||||||
|
(_('Control+Shift'), True, 'ctrl_shift_toggle'),
|
||||||
|
(_('Control+Alt'), True, 'ctrl_alt_toggle'),
|
||||||
|
(_('Alt+Caps Lock'), True, 'alt_caps_toggle'),
|
||||||
|
(_('Left Control+Left Shift'), True, 'lctrl_lshift_toggle'),
|
||||||
|
(_('Left Alt'), True, 'lalt_toggle'),
|
||||||
|
(_('Left Control'), True, 'lctrl_toggle'),
|
||||||
|
(_('Left Shift'), True, 'lshift_toggle'),
|
||||||
|
(_('Left Logo key'), True, 'lwin_toggle'),
|
||||||
|
(_('Scroll Lock key'), True, 'sclk_toggle'),
|
||||||
|
(_('No toggling'), True, None),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleQuestion(WidgetWrap):
|
||||||
|
|
||||||
|
def __init__(self, parent, setting):
|
||||||
|
self.parent = parent
|
||||||
|
self.setting = setting
|
||||||
|
self.selector = Selector(toggle_options)
|
||||||
|
self.selector.value = 'alt_shift_toggle'
|
||||||
|
if self.parent.model.setting.toggle:
|
||||||
|
try:
|
||||||
|
self.selector.value = self.parent.model.setting.toggle
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
pile = Pile([
|
||||||
|
ListBox([
|
||||||
|
Text(_(toggle_text)),
|
||||||
|
]),
|
||||||
|
(1, SolidFill(" ")),
|
||||||
|
('pack', Padding.center_79(Columns([
|
||||||
|
('pack', Text(_("Shortcut: "))),
|
||||||
|
Color.string_input(self.selector),
|
||||||
|
]))),
|
||||||
|
(1, SolidFill(" ")),
|
||||||
|
('pack', button_pile([
|
||||||
|
ok_btn(label=_("OK"), on_press=self.ok),
|
||||||
|
cancel_btn(label=_("Cancel"), on_press=self.cancel),
|
||||||
|
])),
|
||||||
|
])
|
||||||
|
pile.focus_position = 4
|
||||||
|
super().__init__(
|
||||||
|
LineBox(
|
||||||
|
UrwidPadding(
|
||||||
|
pile,
|
||||||
|
left=1, right=1),
|
||||||
|
_("Select layout toggle")))
|
||||||
|
|
||||||
|
def ok(self, sender):
|
||||||
|
self.parent.remove_overlay()
|
||||||
|
self.setting.toggle = self.selector.value
|
||||||
|
self.parent.really_done(self.setting)
|
||||||
|
|
||||||
|
def cancel(self, sender):
|
||||||
|
self.parent.remove_overlay()
|
||||||
|
|
||||||
|
|
||||||
class KeyboardForm(Form):
|
class KeyboardForm(Form):
|
||||||
|
|
||||||
cancel_label = _("Back")
|
cancel_label = _("Back")
|
||||||
|
@ -290,9 +371,10 @@ class KeyboardView(BaseView):
|
||||||
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()
|
||||||
try:
|
try:
|
||||||
self.form.layout.widget.value = model.layout
|
self.form.layout.widget.value = setting.layout
|
||||||
self.form.variant.widget.value = model.variant
|
self.form.variant.widget.value = setting.variant
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Don't crash on pre-existing invalid config.
|
# Don't crash on pre-existing invalid config.
|
||||||
pass
|
pass
|
||||||
|
@ -337,9 +419,17 @@ class KeyboardView(BaseView):
|
||||||
variant = ''
|
variant = ''
|
||||||
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)
|
||||||
|
new_setting = setting.latinizable()
|
||||||
|
if new_setting != setting:
|
||||||
|
self.show_overlay(ToggleQuestion(self, new_setting), height=('relative', 100))
|
||||||
|
return
|
||||||
|
self.really_done(setting)
|
||||||
|
|
||||||
|
def really_done(self, setting):
|
||||||
ac = ApplyingConfig(self.controller.loop)
|
ac = ApplyingConfig(self.controller.loop)
|
||||||
self.show_overlay(ac, width=ac.width, min_width=None)
|
self.show_overlay(ac, width=ac.width, min_width=None)
|
||||||
self.controller.done(layout, variant)
|
self.controller.done(setting)
|
||||||
|
|
||||||
def cancel(self, result=None):
|
def cancel(self, result=None):
|
||||||
self.controller.cancel()
|
self.controller.cancel()
|
||||||
|
@ -347,10 +437,17 @@ class KeyboardView(BaseView):
|
||||||
def select_layout(self, sender, layout):
|
def select_layout(self, sender, layout):
|
||||||
log.debug("%s", layout)
|
log.debug("%s", layout)
|
||||||
opts = []
|
opts = []
|
||||||
for variant, variant_desc in self.model.variants[layout].items():
|
default_i = -1
|
||||||
|
for i, (variant, variant_desc) in enumerate(self.model.variants[layout].items()):
|
||||||
|
if variant == "":
|
||||||
|
default_i = i
|
||||||
opts.append(Option((variant_desc, True, variant)))
|
opts.append(Option((variant_desc, True, variant)))
|
||||||
opts.sort(key=lambda o:o.label)
|
opts.sort(key=lambda o:o.label)
|
||||||
opts.insert(0, Option(("default", True, None)))
|
if default_i < 0:
|
||||||
|
opts.insert(0, Option(("default", True, "")))
|
||||||
self.form.variant.widget._options = opts
|
self.form.variant.widget._options = opts
|
||||||
self.form.variant.widget.index = 0
|
if default_i < 0:
|
||||||
|
self.form.variant.widget.index = 0
|
||||||
|
else:
|
||||||
|
self.form.variant.widget.index = default_i
|
||||||
self.form.variant.enabled = len(opts) > 1
|
self.form.variant.enabled = len(opts) > 1
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
Contains some default key navigations
|
Contains some default key navigations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from urwid import Columns, Overlay, Pile, Text, WidgetWrap
|
from urwid import Columns, Overlay, Pile, SolidFill, Text, WidgetWrap
|
||||||
|
|
||||||
|
|
||||||
class BaseView(WidgetWrap):
|
class BaseView(WidgetWrap):
|
||||||
|
@ -42,14 +42,20 @@ class BaseView(WidgetWrap):
|
||||||
if isinstance(kw['width'], int):
|
if isinstance(kw['width'], int):
|
||||||
kw['width'] += 2*PADDING
|
kw['width'] += 2*PADDING
|
||||||
args.update(kw)
|
args.update(kw)
|
||||||
|
if 'height' in kw:
|
||||||
|
f = SolidFill(" ")
|
||||||
|
p = 1
|
||||||
|
else:
|
||||||
|
f = Text("")
|
||||||
|
p = 'pack'
|
||||||
top = Pile([
|
top = Pile([
|
||||||
('pack', Text("")),
|
(p, f),
|
||||||
Columns([
|
Columns([
|
||||||
(PADDING, Text("")),
|
(PADDING, f),
|
||||||
overlay_widget,
|
overlay_widget,
|
||||||
(PADDING, Text(""))
|
(PADDING, f)
|
||||||
]),
|
]),
|
||||||
('pack', Text("")),
|
(p, f),
|
||||||
])
|
])
|
||||||
self._w = Overlay(top_w=top, bottom_w=self._w, **args)
|
self._w = Overlay(top_w=top, bottom_w=self._w, **args)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue