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 +

+ 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

-[![pub package](https://img.shields.io/pub/v/virtual_keypad.svg)](https://pub.dev/packages/virtual_keypad) -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +

+ A fully customizable virtual on-screen keyboard for Flutter +

-## Features +

+ pub package + build status + License: MIT + Platform +

-- 🎹 **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', () {