Merge pull request #304 from CanonicalLtd/mwhudson/non-latin-keyboard-setting

handle non-latin keyboard layouts more intelligently
This commit is contained in:
Michael Hudson-Doyle 2018-03-28 14:44:13 +13:00 committed by GitHub
commit b98244b8c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 242 additions and 45 deletions

View File

@ -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')

View File

@ -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'])

View File

@ -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(),
}, },
}, },
} }

View File

@ -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

View File

@ -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)