diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..aa77bbb
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,57 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - dev
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ analyze:
+ name: Analyze & Test
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+
+ - name: Install dependencies
+ run: flutter pub get
+
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+
+ - name: Analyze code
+ run: flutter analyze
+
+ - name: Run tests
+ run: flutter test
+
+ build-example:
+ name: Build Example
+ runs-on: ubuntu-latest
+ needs: analyze
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+
+ - name: Get dependencies
+ working-directory: example
+ run: flutter pub get
+
+ - name: Build web
+ working-directory: example
+ run: flutter build web
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..715e3b1
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,56 @@
+name: Release & Publish
+
+on:
+ pull_request:
+ types: [ closed ]
+ branches: [ main ]
+
+jobs:
+ release:
+ if: github.event.pull_request.merged == true
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write
+ id-token: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+
+ - name: Read version
+ id: version
+ run: |
+ VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}')
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Ensure tag does not already exist
+ run: |
+ if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
+ echo "β Version already published"
+ exit 1
+ fi
+
+ - name: Install dependencies
+ run: flutter pub get
+
+ - name: Analyze
+ run: flutter analyze
+
+ - name: Dry run publish
+ run: flutter pub publish --dry-run
+
+ - name: Create tag
+ run: |
+ git tag v${{ steps.version.outputs.version }}
+ git push origin v${{ steps.version.outputs.version }}
+
+ - name: Publish to pub.dev
+ run: flutter pub publish --force
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6c6f0e..d9c531e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,29 @@
+## 0.1.1
+
+### Fixed
+* **Physical keyboard handling** - Virtual keyboard now correctly hides when `allowPhysicalKeyboard: true`
+* **Layout persistence on close** - Keyboard maintains its layout during close animation instead of resetting to default
+* **Selection handling** - `insertText()` and `deleteBackward()` now properly handle text selections
+
+### Added
+* `selectAll()` method to `VirtualKeypadController` for selecting all text
+* Stricter lint rules for better code quality
+* Comprehensive documentation in `doc/` folder
+ - API Reference
+ - Custom Layouts Guide
+ - Adding Languages Guide
+ - Theming Guide
+
+### Improved
+* Enhanced `VirtualKeypadController` selection awareness
+* Better scope state management for physical keyboard mode
+* Professional README with cleaner structure
+
+---
+
## 0.1.0
-* Initial release
+### Initial Release
* **VirtualKeypadScope** - Manages keyboard-to-textfield connections
* **VirtualKeypadTextField** - Text field with virtual keyboard integration
* **VirtualKeypad** - Customizable on-screen keyboard widget
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..05815f8
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,98 @@
+# Contributing to Virtual Keypad
+
+Thank you for your interest in contributing! This guide will help you get started.
+
+## Getting Started
+
+1. **Fork the repository** on GitHub
+2. **Clone your fork** locally:
+ ```bash
+ git clone https://github.com/YOUR_USERNAME/virtual_keypad.git
+ cd virtual_keypad
+ ```
+3. **Install dependencies**:
+ ```bash
+ flutter pub get
+ cd example && flutter pub get && cd ..
+ ```
+
+## Development Workflow
+
+### Running Tests
+
+```bash
+flutter test
+```
+
+### Running the Example App
+
+```bash
+cd example
+flutter run
+```
+
+### Code Style
+
+- Follow [Dart style guide](https://dart.dev/guides/language/effective-dart/style)
+- Run `dart format .` before committing
+- Ensure `flutter analyze` passes with no errors
+
+## Making Changes
+
+### Branch Naming
+
+- `feature/description` - New features
+- `fix/description` - Bug fixes
+- `docs/description` - Documentation updates
+
+### Commit Messages
+
+Use clear, descriptive commit messages:
+
+```
+feat: add haptic feedback support
+fix: keyboard layout reset on close
+docs: update README installation section
+```
+
+### Pull Request Process
+
+1. Create a feature branch from `main`
+2. Make your changes with tests
+3. Ensure all tests pass
+4. Update documentation if needed
+5. Submit a PR with a clear description
+
+## Adding Features
+
+### New Keyboard Layout
+
+See [Adding Languages Guide](doc/adding-languages.md)
+
+### New Theme
+
+See [Theming Guide](doc/theming.md)
+
+### New Actions
+
+1. Add to `KeyAction` enum in `lib/src/enums.dart`
+2. Handle in `_handleAction()` in `lib/src/widgets/keyboard.dart`
+3. Add icon/label in `_buildKeyContent()`
+4. Update documentation
+
+## Reporting Issues
+
+- Use the [issue tracker](https://github.com/Masum-MSNR/virtual_keypad/issues)
+- Include Flutter version (`flutter --version`)
+- Provide minimal reproduction code
+- Include screenshots/videos if UI-related
+
+## Code of Conduct
+
+- Be respectful and inclusive
+- Focus on constructive feedback
+- Help others learn and grow
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the MIT License.
diff --git a/README.md b/README.md
index eeb6036..2749f53 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,74 @@
-# Virtual Keypad
+
+
+
-A fully customizable virtual on-screen keyboard for Flutter with multi-language support. Perfect for kiosk applications, password entry UIs, custom input interfaces, and any application that needs to disable the system keyboard.
+Virtual Keypad
-[](https://pub.dev/packages/virtual_keypad)
-[](LICENSE)
+
+ A fully customizable virtual on-screen keyboard for Flutter
+
-## Features
+
+
+
+
+
+
-- πΉ **Multiple keyboard layouts** - Text, numeric, phone, email, URL, or custom layouts
-- π **Multi-language support** - Built-in English and Bengali, easily extensible
-- π **Smart TextField** - Auto-adapts keyboard based on input type
-- β¨οΈ **Physical keyboard support** - Optional dual input (virtual + physical)
-- π¨ **Fully themeable** - Light, dark, or custom themes
-- π± **Cross-platform** - Works on mobile, web, and desktop
-- β‘ **Full text editing** - Selection, copy/paste, cursor positioning
-- π **Password mode** - Secure text obscuring
-- π **Auto-hide** - Animated show/hide when focus changes
-- β¨ **Input-aware layouts** - Email shows @, URL shows /, etc.
+
+ Perfect for kiosk applications, password entry UIs, custom input interfaces, and any application requiring system keyboard suppression.
+
-## Installation
+---
+
+## β¨ Features
+
+| Feature | Description |
+|---------|-------------|
+| πΉ **Multiple Layouts** | Text, numeric, phone, email, URL, or fully custom layouts |
+| π **Multi-Language** | Built-in English & Bengali, easily extensible |
+| π **Smart TextField** | Auto-adapts keyboard based on input type |
+| β¨οΈ **Physical Keyboard** | Optional dual input mode (virtual + physical) |
+| π¨ **Themeable** | Light, dark, or custom themes |
+| π± **Cross-Platform** | Mobile, web, and desktop support |
+| β‘ **Full Editing** | Selection, copy/paste, cursor control |
+| π **Password Mode** | Secure text obscuring |
+| π **Auto-Hide** | Animated show/hide on focus change |
+
+---
+
+## π¦ Installation
Add to your `pubspec.yaml`:
```yaml
dependencies:
- virtual_keypad: ^0.1.0
+ virtual_keypad: ^0.1.1
```
-Then run:
-
```bash
flutter pub get
```
-## Quick Start
+---
+
+## π Quick Start
```dart
import 'package:virtual_keypad/virtual_keypad.dart';
-class MyWidget extends StatefulWidget {
+class MyApp extends StatefulWidget {
@override
- State createState() => _MyWidgetState();
+ State createState() => _MyAppState();
}
-class _MyWidgetState extends State {
+class _MyAppState extends State {
final controller = VirtualKeypadController();
@override
void initState() {
super.initState();
- initializeKeyboardLayouts(); // Initialize language layouts
+ initializeKeyboardLayouts();
}
@override
@@ -69,11 +88,15 @@ class _MyWidgetState extends State {
}
```
-## Components
+> π‘ **Three components work together:** `VirtualKeypadScope` β `VirtualKeypadTextField` β `VirtualKeypad`
+
+---
+
+## π Core Components
### VirtualKeypadScope
-**Required wrapper** that connects text fields to the keyboard. Place it above all `VirtualKeypadTextField` and `VirtualKeypad` widgets.
+**Required wrapper** connecting text fields to the keyboard.
```dart
VirtualKeypadScope(
@@ -81,7 +104,7 @@ VirtualKeypadScope(
children: [
VirtualKeypadTextField(controller: controller1),
VirtualKeypadTextField(controller: controller2),
- VirtualKeypad(), // Automatically connects to focused field
+ VirtualKeypad(), // Auto-connects to focused field
],
),
)
@@ -89,33 +112,26 @@ VirtualKeypadScope(
### VirtualKeypadTextField
-A TextField replacement that integrates with the virtual keyboard.
+Drop-in `TextField` replacement with virtual keyboard integration.
```dart
VirtualKeypadTextField(
controller: controller,
- decoration: InputDecoration(labelText: 'Email'),
- keyboardType: KeyboardType.emailAddress, // Shows @ on keyboard
- textInputAction: TextInputAction.next,
+ keyboardType: KeyboardType.emailAddress,
obscureText: false,
- maxLength: 50,
- maxLines: 1,
- allowPhysicalKeyboard: false, // Block system keyboard
- onSubmitted: (value) => print('Submitted: $value'),
+ allowPhysicalKeyboard: false, // Block system keyboard
+ onSubmitted: (value) => print(value),
)
```
-**Key Properties:**
-
-| Property | Type | Description |
-|----------|------|-------------|
-| `controller` | `VirtualKeypadController` | Required text controller |
-| `keyboardType` | `KeyboardType` | Determines keyboard layout |
-| `textInputAction` | `TextInputAction` | Action button behavior |
-| `obscureText` | `bool` | Password mode |
-| `allowPhysicalKeyboard` | `bool` | Allow system keyboard |
-| `maxLength` | `int?` | Character limit |
-| `maxLines` | `int` | Single or multi-line |
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `controller` | `VirtualKeypadController` | *required* | Text controller |
+| `keyboardType` | `KeyboardType` | `text` | Keyboard layout type |
+| `obscureText` | `bool` | `false` | Password mode |
+| `allowPhysicalKeyboard` | `bool` | `false` | Enable system keyboard |
+| `maxLength` | `int?` | `null` | Character limit |
+| `maxLines` | `int` | `1` | Line count |
### VirtualKeypad
@@ -123,68 +139,58 @@ The on-screen keyboard widget.
```dart
VirtualKeypad(
- type: KeyboardType.text, // Override keyboard type
- height: 280, // Keyboard height
- theme: VirtualKeypadTheme.dark, // Visual theme
- hideWhenUnfocused: true, // Auto-hide animation
- animationDuration: Duration(milliseconds: 200),
- onKeyPressed: (key) => print('Key: $key'),
+ height: 280,
+ theme: VirtualKeypadTheme.dark,
+ hideWhenUnfocused: true,
)
```
-**Key Properties:**
-
-| Property | Type | Description |
-|----------|------|-------------|
-| `type` | `KeyboardType?` | Override layout (null = auto from text field) |
-| `height` | `double` | Keyboard height (default: 280) |
-| `theme` | `VirtualKeypadTheme` | Visual styling |
-| `hideWhenUnfocused` | `bool` | Animate hide when no focus |
-| `customLayout` | `KeyboardLayout?` | Custom key arrangement |
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `type` | `KeyboardType?` | `null` | Override layout (auto if null) |
+| `height` | `double` | `280` | Keyboard height |
+| `theme` | `VirtualKeypadTheme` | `light` | Visual theme |
+| `hideWhenUnfocused` | `bool` | `false` | Auto-hide animation |
+| `customLayout` | `KeyboardLayout?` | `null` | Custom key arrangement |
### VirtualKeypadController
-Extended `TextEditingController` with additional methods:
+Extended `TextEditingController` with additional methods.
```dart
final controller = VirtualKeypadController();
-// Text manipulation
controller.insertText('Hello');
controller.deleteBackward();
+controller.selectAll();
controller.clear();
-
-// Cursor control
controller.cursorPosition = 5;
-int pos = controller.cursorPosition;
-
-// Selection
-controller.selectAll();
-TextSelection sel = controller.selection;
```
-## Keyboard Types
+---
+
+## β¨οΈ Keyboard Types
-The keyboard automatically adapts based on `keyboardType`:
+| Type | Layout | Use Case |
+|------|--------|----------|
+| `text` | Full QWERTY | General text input |
+| `emailAddress` | QWERTY + `@` `.` | Email fields |
+| `url` | QWERTY + `/` `:` `.` | URL fields |
+| `number` | 0-9, decimal | Numeric input |
+| `numberSigned` | 0-9, `-`, decimal | Signed numbers |
+| `phone` | Phone dialer | Phone numbers |
+| `multiline` | QWERTY + newline | Text areas |
+| `custom` | User-defined | Custom layouts |
-| KeyboardType | Layout | Special Keys |
-|--------------|--------|--------------|
-| `text` | Full QWERTY | Letters, numbers, symbols |
-| `emailAddress` | QWERTY + email | @ . _ - on primary row |
-| `url` | QWERTY + URL | / : . on primary row |
-| `number` | Number pad | 0-9, decimal |
-| `numberSigned` | Signed number pad | 0-9, decimal, minus |
-| `phone` | Phone dialer | 0-9, *, #, + |
-| `multiline` | Full QWERTY | Enter inserts newline |
-| `custom` | User-defined | Via `customLayout` |
+---
-## Theming
+## π¨ Theming
### Built-in Themes
```dart
-VirtualKeypad(theme: VirtualKeypadTheme.light) // Light theme
-VirtualKeypad(theme: VirtualKeypadTheme.dark) // Dark theme
+VirtualKeypad(theme: VirtualKeypadTheme.light)
+VirtualKeypad(theme: VirtualKeypadTheme.dark)
```
### Custom Theme
@@ -198,167 +204,66 @@ VirtualKeypad(
keyTextColor: Colors.white,
keyTextSize: 20,
keyBorderRadius: 8,
- horizontalGap: 6,
- verticalGap: 8,
- keyDecoration: BoxDecoration(
- color: Colors.grey[800],
- borderRadius: BorderRadius.circular(8),
- boxShadow: [BoxShadow(blurRadius: 2, offset: Offset(0, 1))],
- ),
),
)
```
-## Multi-Language Support
+---
+
+## π Multi-Language Support
### Built-in Languages
-- **English** (`en`) - QWERTY layout
-- **Bengali** (`bn`) - বাΰ¦ΰ¦²ΰ¦Ύ layout with Bengali numerals
+| Code | Language | Layout |
+|------|----------|--------|
+| `en` | English | QWERTY |
+| `bn` | Bengali | বাΰ¦ΰ¦²ΰ¦Ύ |
-### Switching Languages
+### Switch Language
```dart
-// Initialize at app startup
initializeKeyboardLayouts();
-// Switch to Bengali
-KeyboardLayoutProvider.instance.setLanguage('bn');
-
-// Switch to English
-KeyboardLayoutProvider.instance.setLanguage('en');
-
-// Get current language
-String code = KeyboardLayoutProvider.instance.currentLanguageCode;
-KeyboardLanguage lang = KeyboardLayoutProvider.instance.currentLanguage;
+KeyboardLayoutProvider.instance.setLanguage('bn'); // Bengali
+KeyboardLayoutProvider.instance.setLanguage('en'); // English
```
-### Adding a New Language
-
-1. Create a language file in `lib/src/layouts/languages/`:
-
-```dart
-// my_language.dart
-import '../../enums.dart';
-import '../../models.dart';
-import '../keyboard_language.dart';
-
-final KeyboardLayout _textLayoutPrimary = [
- [
- VirtualKey.character(text: 'a'),
- VirtualKey.character(text: 'b'),
- // ... more keys
- ],
- // ... more rows
-];
-
-final KeyboardLanguage myLanguage = KeyboardLanguage(
- code: 'xx',
- name: 'My Language',
- nativeName: 'Native Name',
- textLayouts: KeyboardLayoutSet(
- primary: _textLayoutPrimary,
- secondary: _textLayoutSecondary,
- tertiary: _textLayoutTertiary,
- ),
- emailLayouts: KeyboardLayoutSet(...),
- numberLayouts: KeyboardLayoutSet.single(_numberLayout),
- phoneLayouts: KeyboardLayoutSet.single(_phoneLayout),
-);
-```
+### Add Custom Language
-2. Register the language:
+See [Adding Languages Guide](doc/adding-languages.md) for detailed instructions.
-```dart
-KeyboardLayoutProvider.instance.registerLanguage(myLanguage);
-KeyboardLayoutProvider.instance.setLanguage('xx');
-```
+---
-## Custom Layouts
+## π§ Common Use Cases
-Create completely custom keyboard layouts:
+
+Password Entry
```dart
-final myLayout = [
- [
- VirtualKey.character(text: '1'),
- VirtualKey.character(text: '2'),
- VirtualKey.character(text: '3'),
- ],
- [
- VirtualKey.character(text: '4'),
- VirtualKey.character(text: '5'),
- VirtualKey.character(text: '6'),
- ],
- [
- VirtualKey.action(action: KeyAction.backSpace, flex: 2),
- VirtualKey.action(action: KeyAction.done, label: 'OK'),
- ],
-];
-
-VirtualKeypad(
- type: KeyboardType.custom,
- customLayout: myLayout,
-)
-```
-
-### VirtualKey Options
-
-```dart
-// Character key
-VirtualKey.character(
- text: 'a', // Lowercase character
- capsText: 'A', // Uppercase (optional, auto-generated)
- flex: 1, // Relative width
-)
-
-// Action key
-VirtualKey.action(
- action: KeyAction.shift,
- label: 'β§', // Primary state label
- altLabel: 'βͺ', // Secondary state label
- flex: 2, // Relative width
+VirtualKeypadTextField(
+ controller: passwordController,
+ obscureText: true,
+ decoration: InputDecoration(
+ labelText: 'Password',
+ prefixIcon: Icon(Icons.lock),
+ ),
)
```
+
-### Available Actions
-
-| KeyAction | Description |
-|-----------|-------------|
-| `backSpace` | Delete character before cursor |
-| `enter` | Newline or submit |
-| `shift` | Toggle uppercase |
-| `space` | Insert space |
-| `symbols` | Switch to symbols layout |
-| `symbolsAlt` | Switch to alternate symbols |
-| `done` | Submit and close |
-| `go` | Navigate (for URLs) |
-| `search` | Search action |
-| `send` | Send action |
-
-## Examples
-
-### Password Entry
+
+Kiosk Mode (No System Keyboard)
```dart
-VirtualKeypadScope(
- child: Column(
- children: [
- VirtualKeypadTextField(
- controller: passwordController,
- obscureText: true,
- decoration: InputDecoration(
- labelText: 'Password',
- prefixIcon: Icon(Icons.lock),
- ),
- ),
- VirtualKeypad(hideWhenUnfocused: true),
- ],
- ),
+VirtualKeypadTextField(
+ controller: controller,
+ allowPhysicalKeyboard: false,
)
```
+
-### Multi-Field Form
+
+Multi-Field Form
```dart
VirtualKeypadScope(
@@ -367,79 +272,78 @@ VirtualKeypadScope(
VirtualKeypadTextField(
controller: emailController,
keyboardType: KeyboardType.emailAddress,
- decoration: InputDecoration(labelText: 'Email'),
),
VirtualKeypadTextField(
controller: phoneController,
keyboardType: KeyboardType.phone,
- decoration: InputDecoration(labelText: 'Phone'),
),
- VirtualKeypad(), // Auto-switches layout based on focus
+ VirtualKeypad(), // Auto-switches layout
],
),
)
```
+
-### Kiosk Mode (No System Keyboard)
+
+Custom Layout
```dart
-VirtualKeypadTextField(
- controller: controller,
- allowPhysicalKeyboard: false, // Blocks system keyboard
-)
-```
-
-### Dual Input (Virtual + Physical)
+final pinLayout = [
+ [
+ VirtualKey.character(text: '1'),
+ VirtualKey.character(text: '2'),
+ VirtualKey.character(text: '3'),
+ ],
+ [
+ VirtualKey.character(text: '4'),
+ VirtualKey.character(text: '5'),
+ VirtualKey.character(text: '6'),
+ ],
+ [
+ VirtualKey.character(text: '7'),
+ VirtualKey.character(text: '8'),
+ VirtualKey.character(text: '9'),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.backSpace),
+ VirtualKey.character(text: '0'),
+ VirtualKey.action(action: KeyAction.done, label: 'β'),
+ ],
+];
-```dart
-VirtualKeypadTextField(
- controller: controller,
- allowPhysicalKeyboard: true, // Both keyboards work
+VirtualKeypad(
+ type: KeyboardType.custom,
+ customLayout: pinLayout,
)
```
+
-## API Reference
+---
-### VirtualKeypadScope
+## π Documentation
-| Method | Description |
-|--------|-------------|
-| `VirtualKeypadScope.of(context)` | Get scope state from context |
+| Document | Description |
+|----------|-------------|
+| [API Reference](doc/api-reference.md) | Complete API documentation |
+| [Custom Layouts](doc/custom-layouts.md) | Creating custom keyboard layouts |
+| [Adding Languages](doc/adding-languages.md) | Multi-language implementation guide |
+| [Theming Guide](doc/theming.md) | Customizing keyboard appearance |
+| [Examples](example/) | Full example applications |
-### VirtualKeypadController
+---
-| Property/Method | Description |
-|----------------|-------------|
-| `text` | Current text value |
-| `selection` | Current selection |
-| `cursorPosition` | Cursor position (get/set) |
-| `insertText(String)` | Insert at cursor |
-| `deleteBackward()` | Delete before cursor |
-| `selectAll()` | Select all text |
-| `clear()` | Clear all text |
-
-### KeyboardLayoutProvider
-
-| Property/Method | Description |
-|----------------|-------------|
-| `instance` | Singleton instance |
-| `currentLanguage` | Current KeyboardLanguage |
-| `currentLanguageCode` | Current language code |
-| `languages` | All registered languages |
-| `registerLanguage(lang)` | Add a language |
-| `setLanguage(code)` | Switch language |
-| `getLayouts(inputType)` | Get layouts for input type |
-
-## License
+## π License
MIT License - see [LICENSE](LICENSE) for details.
-## Contributing
+---
+
+## π€ Contributing
-Contributions are welcome! Please submit pull requests to the [repository](https://github.com/Masum-MSNR/virtual_keypad).
+Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md) and submit PRs to the [repository](https://github.com/Masum-MSNR/virtual_keypad).
-## Support
+---
-- π [Documentation](https://pub.dev/packages/virtual_keypad)
-- π [Issue Tracker](https://github.com/Masum-MSNR/virtual_keypad/issues)
-- π¬ [Discussions](https://github.com/Masum-MSNR/virtual_keypad/discussions)
+
+ Made with β€οΈ by Masum
+
diff --git a/analysis_options.yaml b/analysis_options.yaml
index a5744c1..6006d3a 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -2,3 +2,22 @@ include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+
+linter:
+ rules:
+ - prefer_const_constructors
+ - prefer_const_declarations
+ - prefer_final_locals
+ - prefer_final_in_for_each
+ - avoid_print
+ - unawaited_futures
+ - always_declare_return_types
+ - avoid_unused_constructor_parameters
+ - use_super_parameters
+
+analyzer:
+ errors:
+ # Treat these as info/ignore for existing codebase compatibility
+ prefer_const_constructors: info
+ prefer_single_quotes: ignore
+ require_trailing_commas: ignore
diff --git a/doc/adding-languages.md b/doc/adding-languages.md
new file mode 100644
index 0000000..558ee15
--- /dev/null
+++ b/doc/adding-languages.md
@@ -0,0 +1,358 @@
+# Adding Languages Guide
+
+Learn how to add new languages to the Virtual Keypad package.
+
+## Table of Contents
+
+- [Overview](#overview)
+- [File Structure](#file-structure)
+- [Creating a Language](#creating-a-language)
+- [Layout Sets](#layout-sets)
+- [Registration](#registration)
+- [Complete Example](#complete-example)
+
+---
+
+## Overview
+
+Each language in Virtual Keypad provides:
+
+- **Text layouts** - Primary letters, numbers, and symbols
+- **Email layouts** (optional) - With `@` easily accessible
+- **URL layouts** (optional) - With `/` `:` `.` accessible
+- **Number layouts** (optional) - Numeric keypad
+- **Phone layouts** (optional) - Phone dialer
+
+---
+
+## File Structure
+
+Create a new file in `lib/src/layouts/languages/`:
+
+```
+lib/src/layouts/languages/
+βββ english.dart
+βββ bengali.dart
+βββ your_language.dart β New file
+βββ languages.dart β Export file
+```
+
+---
+
+## Creating a Language
+
+### Step 1: Define Primary Layout
+
+```dart
+// lib/src/layouts/languages/spanish.dart
+
+import '../../enums.dart';
+import '../../models.dart';
+import '../keyboard_language.dart';
+
+final KeyboardLayout _textLayoutPrimary = [
+ // Row 1
+ [
+ VirtualKey.character(text: 'q'),
+ VirtualKey.character(text: 'w'),
+ VirtualKey.character(text: 'e'),
+ VirtualKey.character(text: 'r'),
+ VirtualKey.character(text: 't'),
+ VirtualKey.character(text: 'y'),
+ VirtualKey.character(text: 'u'),
+ VirtualKey.character(text: 'i'),
+ VirtualKey.character(text: 'o'),
+ VirtualKey.character(text: 'p'),
+ ],
+ // Row 2
+ [
+ VirtualKey.character(text: 'a'),
+ VirtualKey.character(text: 's'),
+ VirtualKey.character(text: 'd'),
+ VirtualKey.character(text: 'f'),
+ VirtualKey.character(text: 'g'),
+ VirtualKey.character(text: 'h'),
+ VirtualKey.character(text: 'j'),
+ VirtualKey.character(text: 'k'),
+ VirtualKey.character(text: 'l'),
+ VirtualKey.character(text: 'Γ±'), // Spanish-specific
+ ],
+ // Row 3
+ [
+ VirtualKey.action(action: KeyAction.shift),
+ VirtualKey.character(text: 'z'),
+ VirtualKey.character(text: 'x'),
+ VirtualKey.character(text: 'c'),
+ VirtualKey.character(text: 'v'),
+ VirtualKey.character(text: 'b'),
+ VirtualKey.character(text: 'n'),
+ VirtualKey.character(text: 'm'),
+ VirtualKey.action(action: KeyAction.backSpace),
+ ],
+ // Row 4
+ [
+ VirtualKey.action(action: KeyAction.symbols, label: '123', altLabel: 'ABC', flex: 2),
+ VirtualKey.character(text: ','),
+ VirtualKey.action(action: KeyAction.space, flex: 4),
+ VirtualKey.character(text: '.'),
+ VirtualKey.action(action: KeyAction.enter, flex: 2),
+ ],
+];
+```
+
+### Step 2: Define Secondary Layout (Numbers/Symbols)
+
+```dart
+final KeyboardLayout _textLayoutSecondary = [
+ [
+ VirtualKey.character(text: '1'),
+ VirtualKey.character(text: '2'),
+ VirtualKey.character(text: '3'),
+ VirtualKey.character(text: '4'),
+ VirtualKey.character(text: '5'),
+ VirtualKey.character(text: '6'),
+ VirtualKey.character(text: '7'),
+ VirtualKey.character(text: '8'),
+ VirtualKey.character(text: '9'),
+ VirtualKey.character(text: '0'),
+ ],
+ [
+ VirtualKey.character(text: '-'),
+ VirtualKey.character(text: '/'),
+ VirtualKey.character(text: ':'),
+ VirtualKey.character(text: ';'),
+ VirtualKey.character(text: '('),
+ VirtualKey.character(text: ')'),
+ VirtualKey.character(text: 'β¬'), // Euro symbol
+ VirtualKey.character(text: '&'),
+ VirtualKey.character(text: '@'),
+ VirtualKey.character(text: '"'),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.symbolsAlt, label: '#+=', altLabel: '123'),
+ VirtualKey.character(text: '.'),
+ VirtualKey.character(text: ','),
+ VirtualKey.character(text: '?'),
+ VirtualKey.character(text: '!'),
+ VirtualKey.character(text: 'ΒΏ'), // Spanish-specific
+ VirtualKey.character(text: 'Β‘'), // Spanish-specific
+ VirtualKey.character(text: '%'),
+ VirtualKey.action(action: KeyAction.backSpace),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.symbols, label: '123', altLabel: 'ABC', flex: 2),
+ VirtualKey.character(text: ','),
+ VirtualKey.action(action: KeyAction.space, flex: 4),
+ VirtualKey.character(text: '.'),
+ VirtualKey.action(action: KeyAction.enter, flex: 2),
+ ],
+];
+```
+
+### Step 3: Define Tertiary Layout (More Symbols)
+
+```dart
+final KeyboardLayout _textLayoutTertiary = [
+ [
+ VirtualKey.character(text: '['),
+ VirtualKey.character(text: ']'),
+ VirtualKey.character(text: '{'),
+ VirtualKey.character(text: '}'),
+ VirtualKey.character(text: '#'),
+ VirtualKey.character(text: '%'),
+ VirtualKey.character(text: '^'),
+ VirtualKey.character(text: '*'),
+ VirtualKey.character(text: '+'),
+ VirtualKey.character(text: '='),
+ ],
+ [
+ VirtualKey.character(text: '_'),
+ VirtualKey.character(text: '\\'),
+ VirtualKey.character(text: '|'),
+ VirtualKey.character(text: '~'),
+ VirtualKey.character(text: '<'),
+ VirtualKey.character(text: '>'),
+ VirtualKey.character(text: 'β¬'),
+ VirtualKey.character(text: 'Β£'),
+ VirtualKey.character(text: 'Β₯'),
+ VirtualKey.character(text: 'β’'),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.symbolsAlt, label: '#+=', altLabel: '123'),
+ VirtualKey.character(text: '.'),
+ VirtualKey.character(text: ','),
+ VirtualKey.character(text: '?'),
+ VirtualKey.character(text: '!'),
+ VirtualKey.character(text: "'"),
+ VirtualKey.character(text: '`'),
+ VirtualKey.character(text: 'Β°'),
+ VirtualKey.action(action: KeyAction.backSpace),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.symbols, label: '123', altLabel: 'ABC', flex: 2),
+ VirtualKey.character(text: ','),
+ VirtualKey.action(action: KeyAction.space, flex: 4),
+ VirtualKey.character(text: '.'),
+ VirtualKey.action(action: KeyAction.enter, flex: 2),
+ ],
+];
+```
+
+---
+
+## Layout Sets
+
+Group layouts into `KeyboardLayoutSet`:
+
+```dart
+final _textLayoutSet = KeyboardLayoutSet(
+ primary: _textLayoutPrimary,
+ secondary: _textLayoutSecondary,
+ tertiary: _textLayoutTertiary,
+);
+```
+
+For simple layouts (like number pads):
+
+```dart
+final _numberLayoutSet = KeyboardLayoutSet.single(_numberLayout);
+```
+
+---
+
+## Registration
+
+### Step 4: Create Language Definition
+
+```dart
+final KeyboardLanguage spanishLanguage = KeyboardLanguage(
+ code: 'es', // ISO 639-1 code
+ name: 'Spanish', // English name
+ nativeName: 'EspaΓ±ol', // Native name
+ isRTL: false, // Right-to-left?
+ textLayouts: _textLayoutSet,
+ emailLayouts: _emailLayoutSet, // Optional
+ urlLayouts: _urlLayoutSet, // Optional
+ numberLayouts: _numberLayoutSet,
+ phoneLayouts: _phoneLayoutSet,
+);
+```
+
+### Step 5: Export from languages.dart
+
+```dart
+// lib/src/layouts/languages/languages.dart
+
+export 'english.dart';
+export 'bengali.dart';
+export 'spanish.dart'; // Add export
+```
+
+### Step 6: Register at Runtime
+
+```dart
+// In your app
+import 'package:virtual_keypad/virtual_keypad.dart';
+import 'path/to/spanish.dart';
+
+void main() {
+ initializeKeyboardLayouts();
+ KeyboardLayoutProvider.instance.registerLanguage(spanishLanguage);
+ runApp(MyApp());
+}
+
+// Switch to Spanish
+KeyboardLayoutProvider.instance.setLanguage('es');
+```
+
+---
+
+## Complete Example
+
+Here's a complete Spanish language file:
+
+```dart
+// lib/src/layouts/languages/spanish.dart
+
+import '../../enums.dart';
+import '../../models.dart';
+import '../keyboard_language.dart';
+
+// Primary text layout (letters)
+final KeyboardLayout _textLayoutPrimary = [
+ [
+ VirtualKey.character(text: 'q'),
+ VirtualKey.character(text: 'w'),
+ VirtualKey.character(text: 'e'),
+ VirtualKey.character(text: 'r'),
+ VirtualKey.character(text: 't'),
+ VirtualKey.character(text: 'y'),
+ VirtualKey.character(text: 'u'),
+ VirtualKey.character(text: 'i'),
+ VirtualKey.character(text: 'o'),
+ VirtualKey.character(text: 'p'),
+ ],
+ [
+ VirtualKey.character(text: 'a'),
+ VirtualKey.character(text: 's'),
+ VirtualKey.character(text: 'd'),
+ VirtualKey.character(text: 'f'),
+ VirtualKey.character(text: 'g'),
+ VirtualKey.character(text: 'h'),
+ VirtualKey.character(text: 'j'),
+ VirtualKey.character(text: 'k'),
+ VirtualKey.character(text: 'l'),
+ VirtualKey.character(text: 'Γ±'),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.shift),
+ VirtualKey.character(text: 'z'),
+ VirtualKey.character(text: 'x'),
+ VirtualKey.character(text: 'c'),
+ VirtualKey.character(text: 'v'),
+ VirtualKey.character(text: 'b'),
+ VirtualKey.character(text: 'n'),
+ VirtualKey.character(text: 'm'),
+ VirtualKey.action(action: KeyAction.backSpace),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.symbols, label: '123', altLabel: 'ABC', flex: 2),
+ VirtualKey.character(text: ','),
+ VirtualKey.action(action: KeyAction.space, flex: 4),
+ VirtualKey.character(text: '.'),
+ VirtualKey.action(action: KeyAction.enter, flex: 2),
+ ],
+];
+
+// Secondary layout (numbers & symbols)
+final KeyboardLayout _textLayoutSecondary = [
+ // ... (as shown above)
+];
+
+// Tertiary layout (more symbols)
+final KeyboardLayout _textLayoutTertiary = [
+ // ... (as shown above)
+];
+
+// Language definition
+final KeyboardLanguage spanishLanguage = KeyboardLanguage(
+ code: 'es',
+ name: 'Spanish',
+ nativeName: 'EspaΓ±ol',
+ textLayouts: KeyboardLayoutSet(
+ primary: _textLayoutPrimary,
+ secondary: _textLayoutSecondary,
+ tertiary: _textLayoutTertiary,
+ ),
+);
+```
+
+---
+
+## Tips
+
+1. **Use ISO 639-1 codes** - Standard 2-letter language codes
+2. **Reuse common layouts** - Numbers and symbols can be shared
+3. **Test thoroughly** - Verify all keys work correctly
+4. **Consider RTL** - Set `isRTL: true` for Arabic, Hebrew, etc.
+5. **Email/URL layouts** - Add `@` and `/` to primary row for convenience
diff --git a/doc/api-reference.md b/doc/api-reference.md
new file mode 100644
index 0000000..ab95793
--- /dev/null
+++ b/doc/api-reference.md
@@ -0,0 +1,392 @@
+# API Reference
+
+Complete API documentation for Virtual Keypad package.
+
+## Table of Contents
+
+- [VirtualKeypadScope](#virtualkeypadscope)
+- [VirtualKeypadTextField](#virtualkeypadtextfield)
+- [VirtualKeypad](#virtualkeypad)
+- [VirtualKeypadController](#virtualkeypadcontroller)
+- [VirtualKeypadTheme](#virtualkeypadtheme)
+- [KeyboardLayoutProvider](#keyboardlayoutprovider)
+- [VirtualKey](#virtualkey)
+- [Enums](#enums)
+
+---
+
+## VirtualKeypadScope
+
+Required wrapper widget that manages the connection between text fields and the keyboard.
+
+### Usage
+
+```dart
+VirtualKeypadScope(
+ child: Column(
+ children: [
+ VirtualKeypadTextField(controller: controller),
+ VirtualKeypad(),
+ ],
+ ),
+)
+```
+
+### Static Methods
+
+| Method | Return Type | Description |
+|--------|-------------|-------------|
+| `of(BuildContext context)` | `VirtualKeypadScopeState?` | Gets the scope state from context |
+
+### State Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `activeController` | `VirtualKeypadController?` | Currently focused text field's controller |
+| `activeKeyboardType` | `KeyboardType` | Keyboard type of active field |
+| `activeInputAction` | `TextInputAction` | Input action of active field |
+| `hasActiveController` | `bool` | Whether any field is focused |
+| `allowPhysicalKeyboard` | `bool` | Whether physical keyboard is allowed |
+
+---
+
+## VirtualKeypadTextField
+
+A TextField replacement that integrates with the virtual keyboard.
+
+### Constructor
+
+```dart
+VirtualKeypadTextField({
+ required VirtualKeypadController controller,
+ InputDecoration? decoration,
+ TextStyle? style,
+ int? maxLength,
+ bool obscureText = false,
+ String obscuringCharacter = 'β’',
+ bool enabled = true,
+ bool readOnly = false,
+ bool autofocus = false,
+ TextAlign textAlign = TextAlign.start,
+ TextAlignVertical? textAlignVertical,
+ int? maxLines = 1,
+ int? minLines,
+ ValueChanged? onChanged,
+ VoidCallback? onTap,
+ ValueChanged? onSubmitted,
+ bool allowPhysicalKeyboard = false,
+ KeyboardType keyboardType = KeyboardType.text,
+ TextInputAction? textInputAction,
+})
+```
+
+### Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `controller` | `VirtualKeypadController` | *required* | Text editing controller |
+| `decoration` | `InputDecoration?` | `null` | Input decoration |
+| `style` | `TextStyle?` | `null` | Text style |
+| `maxLength` | `int?` | `null` | Maximum character count |
+| `obscureText` | `bool` | `false` | Hide text (password mode) |
+| `obscuringCharacter` | `String` | `'β’'` | Character for obscured text |
+| `enabled` | `bool` | `true` | Whether field accepts input |
+| `readOnly` | `bool` | `false` | Display only, no input |
+| `autofocus` | `bool` | `false` | Auto-focus on build |
+| `textAlign` | `TextAlign` | `start` | Text alignment |
+| `maxLines` | `int?` | `1` | Maximum lines (null = unlimited) |
+| `minLines` | `int?` | `null` | Minimum lines |
+| `allowPhysicalKeyboard` | `bool` | `false` | Allow system keyboard |
+| `keyboardType` | `KeyboardType` | `text` | Keyboard layout type |
+| `textInputAction` | `TextInputAction?` | `null` | Action button type |
+
+### Callbacks
+
+| Callback | Type | Description |
+|----------|------|-------------|
+| `onChanged` | `ValueChanged?` | Called when text changes |
+| `onTap` | `VoidCallback?` | Called when field is tapped |
+| `onSubmitted` | `ValueChanged?` | Called on submit action |
+
+---
+
+## VirtualKeypad
+
+The on-screen keyboard widget.
+
+### Constructor
+
+```dart
+VirtualKeypad({
+ KeyboardType? type,
+ double height = 280,
+ double? width,
+ VirtualKeypadTheme theme = VirtualKeypadTheme.light,
+ void Function(VirtualKey key)? onKeyPressed,
+ KeyboardLayout? customLayout,
+ bool hideWhenUnfocused = false,
+ Duration animationDuration = const Duration(milliseconds: 200),
+ Curve animationCurve = Curves.easeInOut,
+})
+```
+
+### Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `type` | `KeyboardType?` | `null` | Override keyboard type (auto if null) |
+| `height` | `double` | `280` | Keyboard height in pixels |
+| `width` | `double?` | `null` | Width (screen width if null) |
+| `theme` | `VirtualKeypadTheme` | `light` | Visual theme |
+| `customLayout` | `KeyboardLayout?` | `null` | Custom key layout |
+| `hideWhenUnfocused` | `bool` | `false` | Hide when no field focused |
+| `animationDuration` | `Duration` | `200ms` | Show/hide animation duration |
+| `animationCurve` | `Curve` | `easeInOut` | Animation curve |
+
+### Callbacks
+
+| Callback | Type | Description |
+|----------|------|-------------|
+| `onKeyPressed` | `void Function(VirtualKey)?` | Called when any key is pressed |
+
+---
+
+## VirtualKeypadController
+
+Extended `TextEditingController` with additional text manipulation methods.
+
+### Constructor
+
+```dart
+VirtualKeypadController({String? text})
+```
+
+### Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `text` | `String` | Current text content |
+| `selection` | `TextSelection` | Current selection |
+| `cursorPosition` | `int` | Cursor offset (get/set) |
+
+### Methods
+
+| Method | Return Type | Description |
+|--------|-------------|-------------|
+| `insertText(String text)` | `void` | Insert text at cursor or replace selection |
+| `deleteBackward()` | `void` | Delete character before cursor or selection |
+| `deleteForward()` | `void` | Delete character after cursor |
+| `clear()` | `void` | Clear all text |
+| `selectAll()` | `void` | Select all text |
+| `moveCursorLeft()` | `void` | Move cursor left one position |
+| `moveCursorRight()` | `void` | Move cursor right one position |
+| `moveCursorToStart()` | `void` | Move cursor to beginning |
+| `moveCursorToEnd()` | `void` | Move cursor to end |
+| `deleteRange(int start, int end)` | `void` | Delete text in range |
+| `replaceRange(int start, int end, String text)` | `void` | Replace text in range |
+
+---
+
+## VirtualKeypadTheme
+
+Theme configuration for keyboard appearance.
+
+### Constructor
+
+```dart
+VirtualKeypadTheme({
+ Color backgroundColor = VkpColors.backgroundColor,
+ Color keyColor = VkpColors.keyColor,
+ Color actionKeyColor = VkpColors.actionKeyColor,
+ Color keyTextColor = VkpColors.keyTextColor,
+ double keyTextSize = 22.0,
+ double keyBorderRadius = 6.0,
+ bool keyShadow = true,
+ Color? splashColor,
+ double horizontalGap = 6.0,
+ double verticalGap = 8.0,
+})
+```
+
+### Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `backgroundColor` | `Color` | `#D1D3D9` | Keyboard background |
+| `keyColor` | `Color` | `#FFFFFF` | Character key color |
+| `actionKeyColor` | `Color` | `#ADB3BC` | Action key color |
+| `keyTextColor` | `Color` | `#1C1C1E` | Text/icon color |
+| `keyTextSize` | `double` | `22.0` | Font size |
+| `keyBorderRadius` | `double` | `6.0` | Key corner radius |
+| `keyShadow` | `bool` | `true` | Show key shadows |
+| `splashColor` | `Color?` | `null` | Tap ripple color |
+| `horizontalGap` | `double` | `6.0` | Gap between keys |
+| `verticalGap` | `double` | `8.0` | Gap between rows |
+
+### Static Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `light` | `VirtualKeypadTheme` | Light theme preset |
+| `dark` | `VirtualKeypadTheme` | Dark theme preset |
+
+### Methods
+
+| Method | Return Type | Description |
+|--------|-------------|-------------|
+| `copyWith(...)` | `VirtualKeypadTheme` | Create copy with overrides |
+
+---
+
+## KeyboardLayoutProvider
+
+Singleton managing keyboard languages and layouts.
+
+### Access
+
+```dart
+KeyboardLayoutProvider.instance
+```
+
+### Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `currentLanguage` | `KeyboardLanguage` | Current language |
+| `currentLanguageCode` | `String` | Current language code |
+| `languages` | `Iterable` | All registered languages |
+| `languageCodes` | `Iterable` | All language codes |
+
+### Methods
+
+| Method | Return Type | Description |
+|--------|-------------|-------------|
+| `registerLanguage(KeyboardLanguage)` | `void` | Register a language |
+| `unregisterLanguage(String code)` | `bool` | Remove a language |
+| `setLanguage(String code)` | `bool` | Switch language |
+| `getLanguage(String code)` | `KeyboardLanguage?` | Get language by code |
+| `hasLanguage(String code)` | `bool` | Check if registered |
+| `getLayouts(KeyboardInputType)` | `KeyboardLayoutSet` | Get layouts for input type |
+| `reset()` | `void` | Reset to defaults |
+
+### Initialization
+
+```dart
+initializeKeyboardLayouts(); // Call at app startup
+```
+
+---
+
+## VirtualKey
+
+Represents a single key on the keyboard.
+
+### Constructors
+
+```dart
+// Character key
+VirtualKey.character({
+ required String text,
+ String? capsText,
+ int flex = 1,
+})
+
+// Action key
+VirtualKey.action({
+ required KeyAction action,
+ String? text,
+ String? label,
+ String? altLabel,
+ int flex = 1,
+})
+```
+
+### Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `text` | `String?` | Character to insert |
+| `capsText` | `String?` | Uppercase character |
+| `keyType` | `KeyType` | `character` or `action` |
+| `action` | `KeyAction?` | Action type (for action keys) |
+| `label` | `String?` | Primary label |
+| `altLabel` | `String?` | Alternate label |
+| `flex` | `int` | Relative width |
+| `isCharacter` | `bool` | Is character key |
+| `isAction` | `bool` | Is action key |
+
+### Methods
+
+| Method | Return Type | Description |
+|--------|-------------|-------------|
+| `getDisplayText({shift, capsLock})` | `String` | Get display text |
+| `getInsertText({shift, capsLock})` | `String` | Get text to insert |
+
+---
+
+## Enums
+
+### KeyboardType
+
+```dart
+enum KeyboardType {
+ text, // Standard QWERTY
+ multiline, // QWERTY with newline
+ number, // Number pad
+ numberSigned, // Numbers with minus
+ numberDecimal, // Numbers with decimal
+ phone, // Phone dialer
+ datetime, // Date/time input
+ emailAddress, // Email layout
+ url, // URL layout
+ visiblePassword,// Same as text
+ name, // Name input
+ streetAddress, // Address input
+ none, // Hidden
+ custom, // Custom layout
+}
+```
+
+### KeyAction
+
+```dart
+enum KeyAction {
+ backSpace, // Delete backward
+ enter, // Newline/submit
+ shift, // Toggle shift
+ space, // Insert space
+ symbols, // Switch to symbols
+ symbolsAlt, // Switch to alt symbols
+ switchLanguage, // Change language
+ done, // Submit
+ go, // Navigate
+ search, // Search
+ send, // Send
+ next, // Next field
+ previous, // Previous field
+ call, // Call action
+}
+```
+
+### KeyType
+
+```dart
+enum KeyType {
+ action, // Action key
+ character, // Character key
+}
+```
+
+### KeyboardInputType
+
+```dart
+enum KeyboardInputType {
+ text, // General text
+ email, // Email input
+ url, // URL input
+ number, // Number input
+ numberSigned, // Signed number
+ numberDecimal, // Decimal number
+ phone, // Phone input
+}
+```
diff --git a/doc/assets/logo.png b/doc/assets/logo.png
new file mode 100644
index 0000000..fa3e298
Binary files /dev/null and b/doc/assets/logo.png differ
diff --git a/doc/custom-layouts.md b/doc/custom-layouts.md
new file mode 100644
index 0000000..91a65f5
--- /dev/null
+++ b/doc/custom-layouts.md
@@ -0,0 +1,248 @@
+# Custom Layouts Guide
+
+Learn how to create custom keyboard layouts for the Virtual Keypad package.
+
+## Table of Contents
+
+- [Basic Structure](#basic-structure)
+- [Character Keys](#character-keys)
+- [Action Keys](#action-keys)
+- [Key Sizing with Flex](#key-sizing-with-flex)
+- [Complete Examples](#complete-examples)
+
+---
+
+## Basic Structure
+
+A keyboard layout is a 2D array of `VirtualKey` objects:
+
+```dart
+KeyboardLayout myLayout = [
+ [ /* Row 1 keys */ ],
+ [ /* Row 2 keys */ ],
+ [ /* Row 3 keys */ ],
+];
+```
+
+---
+
+## Character Keys
+
+Character keys insert text when pressed.
+
+```dart
+VirtualKey.character(
+ text: 'a', // Lowercase (required)
+ capsText: 'A', // Uppercase (optional, auto-generated)
+ flex: 1, // Relative width (default: 1)
+)
+```
+
+### Examples
+
+```dart
+// Simple character
+VirtualKey.character(text: 'a')
+
+// With custom caps
+VirtualKey.character(text: '1', capsText: '!')
+
+// Special character
+VirtualKey.character(text: '@')
+```
+
+---
+
+## Action Keys
+
+Action keys perform keyboard functions.
+
+```dart
+VirtualKey.action(
+ action: KeyAction.backSpace, // Required action type
+ label: 'β«', // Primary label (optional)
+ altLabel: 'DEL', // Alt state label (optional)
+ flex: 2, // Relative width
+)
+```
+
+### Available Actions
+
+| Action | Description | Default Icon/Label |
+|--------|-------------|-------------------|
+| `backSpace` | Delete before cursor | β« icon |
+| `enter` | Submit or newline | β΅ icon |
+| `shift` | Toggle uppercase | β§ icon |
+| `space` | Insert space | "space" |
+| `symbols` | Switch to symbols | "123" |
+| `symbolsAlt` | Switch alt symbols | "#+=" |
+| `done` | Submit action | "Done" |
+| `go` | Navigate action | "Go" |
+| `search` | Search action | π icon |
+| `send` | Send action | β€ icon |
+
+### Examples
+
+```dart
+// Backspace key
+VirtualKey.action(action: KeyAction.backSpace)
+
+// Enter with custom label
+VirtualKey.action(action: KeyAction.enter, label: 'Submit')
+
+// Done button
+VirtualKey.action(action: KeyAction.done, label: 'OK', flex: 2)
+```
+
+---
+
+## Key Sizing with Flex
+
+The `flex` property controls relative key width. Default is 1.
+
+```dart
+// Row with flex values
+[
+ VirtualKey.character(text: 'A', flex: 1), // 1 unit wide
+ VirtualKey.action(action: KeyAction.space, flex: 4), // 4 units wide
+ VirtualKey.character(text: 'B', flex: 1), // 1 unit wide
+]
+// Total: 6 units, space takes 4/6 = 66% of row width
+```
+
+---
+
+## Complete Examples
+
+### PIN Pad
+
+```dart
+final pinLayout = [
+ [
+ VirtualKey.character(text: '1'),
+ VirtualKey.character(text: '2'),
+ VirtualKey.character(text: '3'),
+ ],
+ [
+ VirtualKey.character(text: '4'),
+ VirtualKey.character(text: '5'),
+ VirtualKey.character(text: '6'),
+ ],
+ [
+ VirtualKey.character(text: '7'),
+ VirtualKey.character(text: '8'),
+ VirtualKey.character(text: '9'),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.backSpace),
+ VirtualKey.character(text: '0'),
+ VirtualKey.action(action: KeyAction.done, label: 'β'),
+ ],
+];
+
+// Usage
+VirtualKeypad(
+ type: KeyboardType.custom,
+ customLayout: pinLayout,
+)
+```
+
+### Calculator Pad
+
+```dart
+final calculatorLayout = [
+ [
+ VirtualKey.character(text: '7'),
+ VirtualKey.character(text: '8'),
+ VirtualKey.character(text: '9'),
+ VirtualKey.character(text: 'Γ·'),
+ ],
+ [
+ VirtualKey.character(text: '4'),
+ VirtualKey.character(text: '5'),
+ VirtualKey.character(text: '6'),
+ VirtualKey.character(text: 'Γ'),
+ ],
+ [
+ VirtualKey.character(text: '1'),
+ VirtualKey.character(text: '2'),
+ VirtualKey.character(text: '3'),
+ VirtualKey.character(text: '-'),
+ ],
+ [
+ VirtualKey.character(text: '0'),
+ VirtualKey.character(text: '.'),
+ VirtualKey.action(action: KeyAction.backSpace),
+ VirtualKey.character(text: '+'),
+ ],
+];
+```
+
+### Emoji Picker
+
+```dart
+final emojiLayout = [
+ [
+ VirtualKey.character(text: 'π'),
+ VirtualKey.character(text: 'π'),
+ VirtualKey.character(text: 'π'),
+ VirtualKey.character(text: 'π€'),
+ VirtualKey.character(text: 'π'),
+ ],
+ [
+ VirtualKey.character(text: 'π'),
+ VirtualKey.character(text: 'π'),
+ VirtualKey.character(text: 'β€οΈ'),
+ VirtualKey.character(text: 'π₯'),
+ VirtualKey.character(text: 'β¨'),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.backSpace, flex: 2),
+ VirtualKey.action(action: KeyAction.done, label: 'Done', flex: 3),
+ ],
+];
+```
+
+### Hex Input
+
+```dart
+final hexLayout = [
+ [
+ VirtualKey.character(text: '0'),
+ VirtualKey.character(text: '1'),
+ VirtualKey.character(text: '2'),
+ VirtualKey.character(text: '3'),
+ ],
+ [
+ VirtualKey.character(text: '4'),
+ VirtualKey.character(text: '5'),
+ VirtualKey.character(text: '6'),
+ VirtualKey.character(text: '7'),
+ ],
+ [
+ VirtualKey.character(text: '8'),
+ VirtualKey.character(text: '9'),
+ VirtualKey.character(text: 'A'),
+ VirtualKey.character(text: 'B'),
+ ],
+ [
+ VirtualKey.character(text: 'C'),
+ VirtualKey.character(text: 'D'),
+ VirtualKey.character(text: 'E'),
+ VirtualKey.character(text: 'F'),
+ ],
+ [
+ VirtualKey.action(action: KeyAction.backSpace, flex: 2),
+ VirtualKey.action(action: KeyAction.done, label: 'OK', flex: 2),
+ ],
+];
+```
+
+---
+
+## Tips
+
+1. **Keep rows balanced** - Use flex values to ensure consistent layout
+2. **Provide visual feedback** - Use action keys with clear labels
+3. **Consider touch targets** - Don't make keys too small
+4. **Test on target devices** - Verify layout works on all screen sizes
diff --git a/doc/theming.md b/doc/theming.md
new file mode 100644
index 0000000..bfe6188
--- /dev/null
+++ b/doc/theming.md
@@ -0,0 +1,227 @@
+# Theming Guide
+
+Customize the appearance of Virtual Keypad to match your app's design.
+
+## Table of Contents
+
+- [Built-in Themes](#built-in-themes)
+- [Custom Themes](#custom-themes)
+- [Theme Properties](#theme-properties)
+- [Dynamic Theming](#dynamic-theming)
+- [Theme Examples](#theme-examples)
+
+---
+
+## Built-in Themes
+
+Virtual Keypad includes two pre-built themes:
+
+### Light Theme (Default)
+
+```dart
+VirtualKeypad(theme: VirtualKeypadTheme.light)
+```
+
+
+| Background | #D1D3D9 |
+| Key Color | #FFFFFF |
+| Action Key | #ADB3BC |
+| Text Color | #1C1C1E |
+
+
+### Dark Theme
+
+```dart
+VirtualKeypad(theme: VirtualKeypadTheme.dark)
+```
+
+
+| Background | #2C2C2E |
+| Key Color | #636366 |
+| Action Key | #48484A |
+| Text Color | #FFFFFF |
+
+
+---
+
+## Custom Themes
+
+Create a custom theme by instantiating `VirtualKeypadTheme`:
+
+```dart
+VirtualKeypad(
+ theme: VirtualKeypadTheme(
+ backgroundColor: Colors.grey[900]!,
+ keyColor: Colors.grey[800]!,
+ actionKeyColor: Colors.grey[700]!,
+ keyTextColor: Colors.white,
+ keyTextSize: 20,
+ keyBorderRadius: 8,
+ horizontalGap: 6,
+ verticalGap: 8,
+ ),
+)
+```
+
+---
+
+## Theme Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `backgroundColor` | `Color` | `#D1D3D9` | Keyboard background |
+| `keyColor` | `Color` | `#FFFFFF` | Character key background |
+| `actionKeyColor` | `Color` | `#ADB3BC` | Action key background |
+| `keyTextColor` | `Color` | `#1C1C1E` | Text and icon color |
+| `keyTextSize` | `double` | `22.0` | Font size in pixels |
+| `keyBorderRadius` | `double` | `6.0` | Corner radius |
+| `keyShadow` | `bool` | `true` | Show drop shadows |
+| `splashColor` | `Color?` | `null` | Tap ripple color |
+| `horizontalGap` | `double` | `6.0` | Space between keys |
+| `verticalGap` | `double` | `8.0` | Space between rows |
+
+---
+
+## Dynamic Theming
+
+### Match System Theme
+
+```dart
+VirtualKeypad(
+ theme: Theme.of(context).brightness == Brightness.dark
+ ? VirtualKeypadTheme.dark
+ : VirtualKeypadTheme.light,
+)
+```
+
+### Match App ColorScheme
+
+```dart
+VirtualKeypad(
+ theme: VirtualKeypadTheme(
+ backgroundColor: Theme.of(context).colorScheme.surface,
+ keyColor: Theme.of(context).colorScheme.primaryContainer,
+ actionKeyColor: Theme.of(context).colorScheme.secondaryContainer,
+ keyTextColor: Theme.of(context).colorScheme.onSurface,
+ splashColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
+ ),
+)
+```
+
+### Using copyWith
+
+Modify existing themes:
+
+```dart
+VirtualKeypad(
+ theme: VirtualKeypadTheme.dark.copyWith(
+ keyBorderRadius: 12,
+ keyTextSize: 24,
+ ),
+)
+```
+
+---
+
+## Theme Examples
+
+### iOS Style
+
+```dart
+final iosTheme = VirtualKeypadTheme(
+ backgroundColor: Color(0xFFD1D3D9),
+ keyColor: Colors.white,
+ actionKeyColor: Color(0xFFADB3BC),
+ keyTextColor: Colors.black,
+ keyTextSize: 22,
+ keyBorderRadius: 5,
+ keyShadow: true,
+ horizontalGap: 6,
+ verticalGap: 12,
+);
+```
+
+### Material You
+
+```dart
+VirtualKeypadTheme materialYouTheme(BuildContext context) {
+ final scheme = Theme.of(context).colorScheme;
+ return VirtualKeypadTheme(
+ backgroundColor: scheme.surfaceVariant,
+ keyColor: scheme.surface,
+ actionKeyColor: scheme.secondaryContainer,
+ keyTextColor: scheme.onSurface,
+ keyBorderRadius: 12,
+ keyShadow: false,
+ splashColor: scheme.primary.withOpacity(0.12),
+ );
+}
+```
+
+### Flat/Minimal
+
+```dart
+final flatTheme = VirtualKeypadTheme(
+ backgroundColor: Colors.grey[100]!,
+ keyColor: Colors.grey[100]!,
+ actionKeyColor: Colors.grey[300]!,
+ keyTextColor: Colors.grey[800]!,
+ keyBorderRadius: 0,
+ keyShadow: false,
+ horizontalGap: 2,
+ verticalGap: 2,
+);
+```
+
+### Neon/Gaming
+
+```dart
+final neonTheme = VirtualKeypadTheme(
+ backgroundColor: Colors.black,
+ keyColor: Colors.grey[900]!,
+ actionKeyColor: Colors.purple[900]!,
+ keyTextColor: Colors.cyanAccent,
+ keyBorderRadius: 4,
+ keyShadow: true,
+ splashColor: Colors.cyanAccent.withOpacity(0.3),
+);
+```
+
+### Rounded
+
+```dart
+final roundedTheme = VirtualKeypadTheme(
+ backgroundColor: Colors.grey[200]!,
+ keyColor: Colors.white,
+ actionKeyColor: Colors.grey[400]!,
+ keyTextColor: Colors.black87,
+ keyBorderRadius: 20,
+ keyShadow: true,
+ horizontalGap: 8,
+ verticalGap: 10,
+);
+```
+
+### High Contrast (Accessibility)
+
+```dart
+final highContrastTheme = VirtualKeypadTheme(
+ backgroundColor: Colors.black,
+ keyColor: Colors.white,
+ actionKeyColor: Colors.yellow,
+ keyTextColor: Colors.black,
+ keyTextSize: 26,
+ keyBorderRadius: 4,
+ keyShadow: false,
+);
+```
+
+---
+
+## Tips
+
+1. **Test contrast** - Ensure text is readable on all key types
+2. **Consider accessibility** - Large text and high contrast help users
+3. **Match your brand** - Use your app's color palette
+4. **Test on devices** - Colors may appear differently on various screens
+5. **Shadows on dark themes** - May not be visible, consider disabling
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 6915f9c..4f9edfe 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
-import 'package:virtual_keypad/virtual_keypad.dart';
import 'screens/password_entry_example.dart';
import 'screens/numeric_input_example.dart';
import 'screens/multi_field_example.dart';
diff --git a/example/lib/screens/custom_theme_example.dart b/example/lib/screens/custom_theme_example.dart
index 399a923..2dfe54d 100644
--- a/example/lib/screens/custom_theme_example.dart
+++ b/example/lib/screens/custom_theme_example.dart
@@ -65,11 +65,16 @@ class _CustomThemeExampleState extends State {
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
- borderSide: const BorderSide(color: Color(0xFF0F3460)),
+ borderSide: const BorderSide(
+ color: Color(0xFF0F3460),
+ ),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
- borderSide: const BorderSide(color: Colors.blue, width: 2),
+ borderSide: const BorderSide(
+ color: Colors.blue,
+ width: 2,
+ ),
),
filled: true,
fillColor: const Color(0xFF16213E),
diff --git a/example/lib/screens/multi_field_example.dart b/example/lib/screens/multi_field_example.dart
index 4ffbed0..90e853e 100644
--- a/example/lib/screens/multi_field_example.dart
+++ b/example/lib/screens/multi_field_example.dart
@@ -54,6 +54,7 @@ class _MultiFieldExampleState extends State {
const SizedBox(height: 16),
VirtualKeypadTextField(
controller: _emailController,
+ allowPhysicalKeyboard: true,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
diff --git a/example/lib/screens/multiline_text_example.dart b/example/lib/screens/multiline_text_example.dart
index c634823..d7d29c7 100644
--- a/example/lib/screens/multiline_text_example.dart
+++ b/example/lib/screens/multiline_text_example.dart
@@ -61,7 +61,10 @@ class _MultilineTextExampleState extends State {
builder: (context, _) {
return Text(
'${_controller.text.length} characters',
- style: const TextStyle(color: Colors.grey, fontSize: 12),
+ style: const TextStyle(
+ color: Colors.grey,
+ fontSize: 12,
+ ),
textAlign: TextAlign.end,
);
},
diff --git a/example/lib/screens/password_entry_example.dart b/example/lib/screens/password_entry_example.dart
index 80f3a87..63d4dc4 100644
--- a/example/lib/screens/password_entry_example.dart
+++ b/example/lib/screens/password_entry_example.dart
@@ -64,7 +64,9 @@ class _PasswordEntryExampleState extends State {
child: FilledButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Password: ${_controller.text}')),
+ SnackBar(
+ content: Text('Password: ${_controller.text}'),
+ ),
);
},
child: const Text('Submit'),
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 7d46fd8..2a463b1 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -206,7 +206,7 @@ packages:
path: ".."
relative: true
source: path
- version: "0.1.0"
+ version: "0.1.1"
vm_service:
dependency: transitive
description:
diff --git a/lib/src/controller.dart b/lib/src/controller.dart
index 6d29ac0..f3cd337 100644
--- a/lib/src/controller.dart
+++ b/lib/src/controller.dart
@@ -37,8 +37,17 @@ class VirtualKeypadController extends TextEditingController {
/// Inserts text at the current cursor position.
///
+ /// If text is selected, replaces the selection with the new text.
/// The cursor moves to the end of the inserted text.
void insertText(String newText) {
+ final sel = selection;
+
+ // If there's a valid selection, replace it
+ if (sel.isValid && !sel.isCollapsed) {
+ replaceRange(sel.start, sel.end, newText);
+ return;
+ }
+
final pos = cursorPosition;
final before = text.substring(0, pos);
final after = text.substring(pos);
@@ -52,8 +61,18 @@ class VirtualKeypadController extends TextEditingController {
/// Deletes the character before the cursor (backspace).
///
- /// Does nothing if the cursor is at position 0.
+ /// If text is selected, deletes the entire selection.
+ /// Otherwise, deletes the character before the cursor.
+ /// Does nothing if the cursor is at position 0 and no selection.
void deleteBackward() {
+ final sel = selection;
+
+ // If there's a valid selection, delete it
+ if (sel.isValid && !sel.isCollapsed) {
+ deleteRange(sel.start, sel.end);
+ return;
+ }
+
final pos = cursorPosition;
if (pos > 0 && text.isNotEmpty) {
final before = text.substring(0, pos - 1);
@@ -115,7 +134,16 @@ class VirtualKeypadController extends TextEditingController {
/// Moves the cursor to the beginning of the text.
void moveCursorToStart() {
- selection = TextSelection.collapsed(offset: 0);
+ selection = const TextSelection.collapsed(offset: 0);
+ }
+
+ /// Selects all text in the field.
+ ///
+ /// Does nothing if the text is empty.
+ void selectAll() {
+ if (text.isNotEmpty) {
+ selection = TextSelection(baseOffset: 0, extentOffset: text.length);
+ }
}
/// Deletes text in the specified range [start, end).
diff --git a/lib/src/scope.dart b/lib/src/scope.dart
index b75ea88..b7a67a3 100644
--- a/lib/src/scope.dart
+++ b/lib/src/scope.dart
@@ -49,6 +49,7 @@ class VirtualKeypadScopeState extends State {
int? _activeMaxLength;
KeyboardType _activeKeyboardType = KeyboardType.text;
TextInputAction _activeInputAction = TextInputAction.done;
+ bool _allowPhysicalKeyboard = false;
final List _listeners = [];
bool Function()? _deleteSelectionCallback;
@@ -68,6 +69,10 @@ class VirtualKeypadScopeState extends State {
/// The input action for the active text field (done, next, search, etc.).
TextInputAction get activeInputAction => _activeInputAction;
+ /// Whether the active text field allows physical keyboard input.
+ /// When true, the virtual keyboard should be hidden.
+ bool get allowPhysicalKeyboard => _allowPhysicalKeyboard;
+
/// Whether a text field is currently focused.
bool get hasActiveController => _activeController != null;
@@ -113,10 +118,12 @@ class VirtualKeypadScopeState extends State {
int? maxLength,
KeyboardType keyboardType = KeyboardType.text,
TextInputAction inputAction = TextInputAction.done,
+ bool allowPhysicalKeyboard = false,
}) {
final changed = _activeController != controller ||
_activeKeyboardType != keyboardType ||
- _activeInputAction != inputAction;
+ _activeInputAction != inputAction ||
+ _allowPhysicalKeyboard != allowPhysicalKeyboard;
if (changed) {
setState(() {
@@ -124,6 +131,7 @@ class VirtualKeypadScopeState extends State {
_activeMaxLength = maxLength;
_activeKeyboardType = keyboardType;
_activeInputAction = inputAction;
+ _allowPhysicalKeyboard = allowPhysicalKeyboard;
});
_notifyListeners();
}
@@ -137,6 +145,7 @@ class VirtualKeypadScopeState extends State {
_activeMaxLength = null;
_activeKeyboardType = KeyboardType.text;
_activeInputAction = TextInputAction.done;
+ _allowPhysicalKeyboard = false;
});
_notifyListeners();
}
@@ -203,6 +212,7 @@ class _VirtualKeypadScopeInherited extends InheritedWidget {
bool updateShouldNotify(_VirtualKeypadScopeInherited oldWidget) {
return state.activeController != oldWidget.state.activeController ||
state.activeKeyboardType != oldWidget.state.activeKeyboardType ||
- state.activeInputAction != oldWidget.state.activeInputAction;
+ state.activeInputAction != oldWidget.state.activeInputAction ||
+ state.allowPhysicalKeyboard != oldWidget.state.allowPhysicalKeyboard;
}
}
diff --git a/lib/src/widgets/keyboard.dart b/lib/src/widgets/keyboard.dart
index 1e24416..8e7a79c 100644
--- a/lib/src/widgets/keyboard.dart
+++ b/lib/src/widgets/keyboard.dart
@@ -80,6 +80,10 @@ class _VirtualKeypadState extends State {
VirtualKeypadScopeState? _scope;
KeyboardType? _lastKeyboardType;
+ // Cache the layout when keyboard is visible for smooth close animation
+ KeyboardLayout? _cachedLayout;
+ bool _wasVisible = false;
+
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -99,13 +103,24 @@ class _VirtualKeypadState extends State {
void _onActiveControllerChanged() {
if (mounted) {
- final newType = _effectiveKeyboardType;
- if (_lastKeyboardType != newType) {
- _layoutStage = LayoutStage.primary;
- _shift = false;
- _capsLock = false;
- _lastKeyboardType = newType;
+ final hasController = _scope?.hasActiveController ?? false;
+ final allowPhysical = _scope?.allowPhysicalKeyboard ?? false;
+ final isVisible = hasController && !allowPhysical;
+
+ // Only reset layout when a new field gains focus (not when losing focus)
+ if (isVisible) {
+ final newType = _effectiveKeyboardType;
+ if (_lastKeyboardType != newType) {
+ _layoutStage = LayoutStage.primary;
+ _shift = false;
+ _capsLock = false;
+ _lastKeyboardType = newType;
+ }
+ // Cache the current layout while visible
+ _cachedLayout = _currentLayout;
+ _wasVisible = true;
}
+
setState(() {});
}
}
@@ -261,15 +276,37 @@ class _VirtualKeypadState extends State {
@override
Widget build(BuildContext context) {
- final isVisible =
- !widget.hideWhenUnfocused || (_scope?.hasActiveController ?? false);
+ final hasController = _scope?.hasActiveController ?? false;
+ final allowPhysical = _scope?.allowPhysicalKeyboard ?? false;
+
+ // Hide virtual keyboard when physical keyboard is allowed for the active field
+ final shouldShowKeyboard = hasController && !allowPhysical;
+ final isVisible = !widget.hideWhenUnfocused || shouldShowKeyboard;
if (_effectiveKeyboardType == KeyboardType.none) {
return const SizedBox.shrink();
}
+ // Update cache when visible
+ if (shouldShowKeyboard) {
+ _cachedLayout = _currentLayout;
+ _wasVisible = true;
+ }
+
+ // Use cached layout during close animation, or current layout when visible
+ final layout = shouldShowKeyboard
+ ? _currentLayout
+ : (_wasVisible && _cachedLayout != null
+ ? _cachedLayout!
+ : _currentLayout);
+
+ // Reset cache after animation would complete
+ if (!shouldShowKeyboard && _wasVisible && !widget.hideWhenUnfocused) {
+ _wasVisible = false;
+ _cachedLayout = null;
+ }
+
final width = widget.width ?? MediaQuery.of(context).size.width;
- final layout = _currentLayout;
final rows = layout.length;
final maxColumns = layout.map((row) => row.length).reduce(max);
diff --git a/lib/src/widgets/text_field.dart b/lib/src/widgets/text_field.dart
index 258172f..2ca3c98 100644
--- a/lib/src/widgets/text_field.dart
+++ b/lib/src/widgets/text_field.dart
@@ -280,6 +280,7 @@ class _VirtualKeypadTextFieldState extends State {
maxLength: widget.maxLength,
keyboardType: widget.keyboardType,
inputAction: inputAction,
+ allowPhysicalKeyboard: widget.allowPhysicalKeyboard,
);
_scope!.setDeleteSelectionCallback(_deleteSelection);
_scope!.setGetSelectionCallback(_getSelection);
diff --git a/pubspec.yaml b/pubspec.yaml
index 56dc04f..3774d7d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: virtual_keypad
description: "A customizable virtual on-screen keyboard for Flutter with TextField integration. Supports multiple layouts, themes, and works on all platforms."
-version: 0.1.0
+version: 0.1.1
homepage: https://github.com/Masum-MSNR/virtual_keypad
repository: https://github.com/Masum-MSNR/virtual_keypad
issue_tracker: https://github.com/Masum-MSNR/virtual_keypad/issues
diff --git a/test/virtual_keypad_test.dart b/test/virtual_keypad_test.dart
index 4477af6..312ef24 100644
--- a/test/virtual_keypad_test.dart
+++ b/test/virtual_keypad_test.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:virtual_keypad/virtual_keypad.dart';
@@ -32,6 +33,34 @@ void main() {
expect(controller.text, 'Hello');
expect(controller.cursorPosition, 4);
});
+
+ test('selectAll selects entire text', () {
+ final controller = VirtualKeypadController(text: 'Hello');
+ controller.selectAll();
+ expect(controller.selection.start, 0);
+ expect(controller.selection.end, 5);
+ expect(controller.selection.isCollapsed, false);
+ });
+
+ test('insertText replaces selection', () {
+ final controller = VirtualKeypadController(text: 'Hello World');
+ // Select "World"
+ controller.selection =
+ const TextSelection(baseOffset: 6, extentOffset: 11);
+ controller.insertText('Flutter');
+ expect(controller.text, 'Hello Flutter');
+ expect(controller.cursorPosition, 13);
+ });
+
+ test('deleteBackward removes selection', () {
+ final controller = VirtualKeypadController(text: 'Hello World');
+ // Select "World"
+ controller.selection =
+ const TextSelection(baseOffset: 6, extentOffset: 11);
+ controller.deleteBackward();
+ expect(controller.text, 'Hello ');
+ expect(controller.cursorPosition, 6);
+ });
});
group('VirtualKey', () {