Skip to content

feat: Add French azerty, inputAction and used text - #3

Closed
EArminjon wants to merge 1 commit into
almasumdev:mainfrom
EArminjon:feat/french-azerty
Closed

feat: Add French azerty, inputAction and used text#3
EArminjon wants to merge 1 commit into
almasumdev:mainfrom
EArminjon:feat/french-azerty

Conversation

@EArminjon

@EArminjon EArminjon commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Context: I use your library for the keyboard only, not for the inputs.

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:virtual_keypad/virtual_keypad.dart';
import 'package:widget_book/utils/addons/virtual_keyboard_addon.dart';

class EditingValueTracker with TextInputControl {
  EditingValueTracker({this.onHide, this.onShow});

  final VoidCallback? onShow;
  final VoidCallback? onHide;

  TextEditingValue _currentValue = TextEditingValue.empty;
  TextInputConfiguration? _configuration;
  TextInputClient? _client;
  bool _attached = false;

  TextEditingValue get currentValue => _currentValue;

  TextInputConfiguration? get configuration => _configuration;

  TextInputClient? get client => _client;

  void performAction(TextInputAction action) => _client?.performAction(action);

  @override
  void attach(TextInputClient client, TextInputConfiguration configuration) {
    _client = client;
    _currentValue = client.currentTextEditingValue ?? TextEditingValue.empty;
    _configuration = configuration;
    _attached = true;
  }

  @override
  void detach(TextInputClient client) {
    _client = null;
    _configuration = null;
    _attached = false;
  }

  @override
  void show() {
    if (!_attached) return;
    onShow?.call();
  }

  @override
  void hide() {
    onHide?.call();
  }

  @override
  void setEditingState(TextEditingValue value) {
    _currentValue = value;
  }
}

class VirtualKeyboardLayout extends StatefulWidget {
  const VirtualKeyboardLayout({required this.child, super.key});

  final Widget child;

  @override
  State<VirtualKeyboardLayout> createState() => _VirtualKeyboardLayoutState();
}

class _VirtualKeyboardLayoutState extends State<VirtualKeyboardLayout> {
  late EditingValueTracker _tracker;
  bool _isKeyboardOpen = false;
  KeyboardType? _keyboardType;
  TextInputAction? _inputAction;

  @override
  void initState() {
    super.initState();
    _tracker = EditingValueTracker(onShow: _openKeyboard, onHide: _closeKeyboard);
    TextInput.setInputControl(_tracker);
    FocusManager.instance.addListener(listenFocus);
  }

  @override
  void dispose() {
    FocusManager.instance.removeListener(listenFocus);
    super.dispose();
  }

  void listenFocus() {
    if ((FocusManager.instance.primaryFocus == null || FocusManager.instance.primaryFocus?.context == null) &&
        _isKeyboardOpen) {
      _closeKeyboard();
    }
  }

  void _openKeyboard() {
    if (!mounted) return;

    final focusedContext = FocusManager.instance.primaryFocus?.context;
    final isInside =
        focusedContext != null && focusedContext.findAncestorStateOfType<_VirtualKeyboardLayoutState>() == this;

    if (!isInside) {
      _closeKeyboard();
    } else {
      setState(() {
        _isKeyboardOpen = true;
        _keyboardType = _toKeyboardType(_tracker.configuration?.inputType);
        _inputAction = _tracker.configuration?.inputAction;
      });
    }
  }

  void _closeKeyboard() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return;
      setState(() {
        _isKeyboardOpen = false;
      });
    });
  }

  void _onKeyPressed(VirtualKey key, String? text) {
    if (!mounted) return;

    final currentValue = _tracker.currentValue;
    final sel = currentValue.selection;

    String replaceText(int start, int end, String insert) {
      return currentValue.text.replaceRange(start, end, insert);
    }

    void updateValue(String newText, int offset) {
      final newValue = currentValue.copyWith(
        text: newText,
        selection: TextSelection.collapsed(offset: offset),
        composing: TextRange.collapsed(offset),
      );
      TextInput.updateEditingValue(newValue);
    }

    if (key.isCharacter) {
      final insert = text ?? '';
      final start = sel.start.clamp(0, currentValue.text.length);
      final end = sel.end.clamp(0, currentValue.text.length);

      final newText = replaceText(start, end, insert);
      updateValue(newText, start + insert.length);
    } else {
      switch (key.action) {
        case KeyAction.space:
          _onKeyPressed(VirtualKey.character(text: ' '), ' ');
        case KeyAction.enter:
          if (_keyboardType == KeyboardType.multiline) {
            _onKeyPressed(VirtualKey.character(text: '\n'), '\n');
          } else {
            _tracker.performAction(_inputAction ?? TextInputAction.done);
          }
        case KeyAction.backSpace:
          if (sel.isCollapsed && sel.start > 0) {
            final start = sel.start - 1;
            final newText = replaceText(start, sel.end, '');
            updateValue(newText, start);
          } else if (!sel.isCollapsed) {
            final newText = replaceText(sel.start, sel.end, '');
            updateValue(newText, sel.start);
          }
        case KeyAction.done:
        case KeyAction.go:
        case KeyAction.search:
        case KeyAction.send:
        case KeyAction.call:
          _tracker.performAction(_inputAction ?? TextInputAction.done);
        // ignore: no_default_cases ignored
        default:
          break;
      }
    }
  }

  KeyboardType _toKeyboardType(TextInputType? type) {
    if (type == TextInputType.text) return KeyboardType.text;
    if (type == TextInputType.multiline) return KeyboardType.multiline;
    if (type == TextInputType.number) return KeyboardType.number;
    if (type == const TextInputType.numberWithOptions(signed: true)) return KeyboardType.numberSigned;
    if (type == const TextInputType.numberWithOptions(decimal: true)) return KeyboardType.numberDecimal;
    if (type == TextInputType.phone) return KeyboardType.phone;
    if (type == TextInputType.datetime) return KeyboardType.datetime;
    if (type == TextInputType.emailAddress) return KeyboardType.emailAddress;
    if (type == TextInputType.url) return KeyboardType.url;
    if (type == TextInputType.visiblePassword) return KeyboardType.visiblePassword;
    if (type == TextInputType.name) return KeyboardType.name;
    if (type == TextInputType.streetAddress) return KeyboardType.streetAddress;
    if (type == TextInputType.none) return KeyboardType.none;
    return KeyboardType.text;
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(child: widget.child),
        if (_isKeyboardOpen && VirtualKeyboardEnabled.of(context).isEnabled)
          VirtualKeypad(onKeyPressed: _onKeyPressed, type: _keyboardType, inputAction: _inputAction),
      ],
    );
  }
}

Like that, I can use Flutter classic inputs.

@EArminjon
EArminjon force-pushed the feat/french-azerty branch 4 times, most recently from ef33c0e to 57ad361 Compare February 26, 2026 15:39
@EArminjon EArminjon changed the title feat: Add French azerty feat: Add French azerty, inputAction and used text Feb 26, 2026
@EArminjon

EArminjon commented Feb 26, 2026

Copy link
Copy Markdown
Contributor Author

Hot take: maybe we could refactor this package to better expose parameter for user (like me) which didn't use widgets like VirtualKeypadTextField 😄.

@almasumdev

Copy link
Copy Markdown
Owner

Thank you for the idea and the PR. I really appreciate the contribution. I’ll review it carefully and merge it once everything has been properly checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants