Skip to content

Commit

Permalink
Add keyboard shortcuts for editor actions (singerdmx#986)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blquinn committed Oct 27, 2022
1 parent 47d80f1 commit 6456c5b
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 157 deletions.
19 changes: 1 addition & 18 deletions example/lib/pages/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,7 @@ class _HomePageState extends State<HomePage> {
color: Colors.grey.shade800,
child: _buildMenuBar(context),
),
body: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (event) {
if (event.data.isControlPressed && event.character == 'b') {
if (_controller!
.getSelectionStyle()
.attributes
.keys
.contains('bold')) {
_controller!
.formatSelection(Attribute.clone(Attribute.bold, null));
} else {
_controller!.formatSelection(Attribute.bold);
}
}
},
child: _buildWelcomeEditor(context),
),
body: _buildWelcomeEditor(context),
);
}

Expand Down
2 changes: 1 addition & 1 deletion example/linux/my_application.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ static void my_application_activate(GApplication* application) {
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
gboolean use_header_bar = FALSE;
#ifdef GDK_WINDOWING_X11
GdkScreen *screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
Expand Down
2 changes: 1 addition & 1 deletion example/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import FlutterMacOS
import Foundation

import device_info_plus_macos
import device_info_plus
import pasteboard
import path_provider_macos
import url_launcher_macos
Expand Down
4 changes: 2 additions & 2 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.4
path_provider: ^2.0.9
filesystem_picker: ^2.0.0
file_picker: ^4.6.1
filesystem_picker: ^3.1.0
file_picker: ^5.2.2
flutter_quill:
path: ../
flutter_quill_extensions:
Expand Down
234 changes: 232 additions & 2 deletions lib/src/widgets/raw_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'dart:math' as math;
// ignore: unnecessary_import
import 'dart:typed_data';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
Expand Down Expand Up @@ -38,6 +37,7 @@ import 'raw_editor/raw_editor_state_text_input_client_mixin.dart';
import 'text_block.dart';
import 'text_line.dart';
import 'text_selection.dart';
import 'toolbar/search_dialog.dart';

class RawEditor extends StatefulWidget {
const RawEditor(
Expand Down Expand Up @@ -370,13 +370,59 @@ class RawEditorState extends EditorState
data: _styles!,
child: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
// shortcuts added for Windows platform
// shortcuts added for Desktop platforms.
LogicalKeySet(LogicalKeyboardKey.escape):
const HideSelectionToolbarIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ):
const UndoTextIntent(SelectionChangedCause.keyboard),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyY):
const RedoTextIntent(SelectionChangedCause.keyboard),

// Selection formatting.
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyB):
const ToggleTextStyleIntent(Attribute.bold),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyU):
const ToggleTextStyleIntent(Attribute.underline),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyI):
const ToggleTextStyleIntent(Attribute.italic),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift,
LogicalKeyboardKey.keyS):
const ToggleTextStyleIntent(Attribute.strikeThrough),
LogicalKeySet(
LogicalKeyboardKey.control, LogicalKeyboardKey.backquote):
const ToggleTextStyleIntent(Attribute.inlineCode),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyL):
const ToggleTextStyleIntent(Attribute.ul),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyO):
const ToggleTextStyleIntent(Attribute.ol),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift,
LogicalKeyboardKey.keyB):
const ToggleTextStyleIntent(Attribute.blockQuote),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift,
LogicalKeyboardKey.tilde):
const ToggleTextStyleIntent(Attribute.codeBlock),
// Indent
LogicalKeySet(
LogicalKeyboardKey.control, LogicalKeyboardKey.bracketRight):
const IndentSelectionIntent(true),
LogicalKeySet(
LogicalKeyboardKey.control, LogicalKeyboardKey.bracketLeft):
const IndentSelectionIntent(false),

LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyF):
const OpenSearchIntent(),

LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit1):
const ApplyHeaderIntent(Attribute.h1),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit2):
const ApplyHeaderIntent(Attribute.h2),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit3):
const ApplyHeaderIntent(Attribute.h3),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit0):
const ApplyHeaderIntent(Attribute.header),

LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift,
LogicalKeyboardKey.keyL): const ApplyCheckListIntent(),
},
child: Actions(
actions: _actions,
Expand Down Expand Up @@ -1168,6 +1214,17 @@ class RawEditorState extends EditorState
_UpdateTextSelectionToAdjacentLineAction<
ExtendSelectionVerticallyToAdjacentLineIntent>(this);

late final _ToggleTextStyleAction _formatSelectionAction =
_ToggleTextStyleAction(this);

late final _IndentSelectionAction _indentSelectionAction =
_IndentSelectionAction(this);

late final _OpenSearchAction _openSearchAction = _OpenSearchAction(this);
late final _ApplyHeaderAction _applyHeaderAction = _ApplyHeaderAction(this);
late final _ApplyCheckListAction _applyCheckListAction =
_ApplyCheckListAction(this);

late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false),
ReplaceTextIntent: _replaceTextAction,
Expand Down Expand Up @@ -1214,6 +1271,13 @@ class RawEditorState extends EditorState
_makeOverridable(_HideSelectionToolbarAction(this)),
UndoTextIntent: _makeOverridable(_UndoKeyboardAction(this)),
RedoTextIntent: _makeOverridable(_RedoKeyboardAction(this)),

OpenSearchIntent: _openSearchAction,
// Selection Formatting
ToggleTextStyleIntent: _formatSelectionAction,
IndentSelectionIntent: _indentSelectionAction,
ApplyHeaderIntent: _applyHeaderAction,
ApplyCheckListIntent: _applyCheckListAction,
};

@override
Expand Down Expand Up @@ -1962,3 +2026,169 @@ class _RedoKeyboardAction extends ContextAction<RedoTextIntent> {
@override
bool get isActionEnabled => true;
}

class ToggleTextStyleIntent extends Intent {
const ToggleTextStyleIntent(this.attribute);

final Attribute attribute;
}

// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class _ToggleTextStyleAction extends Action<ToggleTextStyleIntent> {
_ToggleTextStyleAction(this.state);

final RawEditorState state;

bool _isStyleActive(Attribute styleAttr, Map<String, Attribute> attrs) {
if (styleAttr.key == Attribute.list.key) {
final attribute = attrs[styleAttr.key];
if (attribute == null) {
return false;
}
return attribute.value == styleAttr.value;
}
return attrs.containsKey(styleAttr.key);
}

@override
void invoke(ToggleTextStyleIntent intent, [BuildContext? context]) {
final isActive = _isStyleActive(
intent.attribute, state.controller.getSelectionStyle().attributes);
state.controller.formatSelection(
isActive ? Attribute.clone(intent.attribute, null) : intent.attribute);
}

@override
bool get isActionEnabled => true;
}

class IndentSelectionIntent extends Intent {
const IndentSelectionIntent(this.isIncrease);

final bool isIncrease;
}

// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class _IndentSelectionAction extends Action<IndentSelectionIntent> {
_IndentSelectionAction(this.state);

final RawEditorState state;

@override
void invoke(IndentSelectionIntent intent, [BuildContext? context]) {
final indent =
state.controller.getSelectionStyle().attributes[Attribute.indent.key];
if (indent == null) {
if (intent.isIncrease) {
state.controller.formatSelection(Attribute.indentL1);
}
return;
}
if (indent.value == 1 && !intent.isIncrease) {
state.controller
.formatSelection(Attribute.clone(Attribute.indentL1, null));
return;
}
if (intent.isIncrease) {
state.controller
.formatSelection(Attribute.getIndentLevel(indent.value + 1));
return;
}
state.controller
.formatSelection(Attribute.getIndentLevel(indent.value - 1));
}

@override
bool get isActionEnabled => true;
}

class OpenSearchIntent extends Intent {
const OpenSearchIntent();
}

// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class _OpenSearchAction extends ContextAction<OpenSearchIntent> {
_OpenSearchAction(this.state);

final RawEditorState state;

@override
Future invoke(OpenSearchIntent intent, [BuildContext? context]) async {
await showDialog<String>(
context: context!,
builder: (_) => SearchDialog(controller: state.controller, text: ''),
);
}

@override
bool get isActionEnabled => true;
}

class ApplyHeaderIntent extends Intent {
const ApplyHeaderIntent(this.header);

final Attribute header;
}

// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class _ApplyHeaderAction extends Action<ApplyHeaderIntent> {
_ApplyHeaderAction(this.state);

final RawEditorState state;

Attribute<dynamic> _getHeaderValue() {
return state.controller
.getSelectionStyle()
.attributes[Attribute.header.key] ??
Attribute.header;
}

@override
void invoke(ApplyHeaderIntent intent, [BuildContext? context]) {
final _attribute =
_getHeaderValue() == intent.header ? Attribute.header : intent.header;
state.controller.formatSelection(_attribute);
}

@override
bool get isActionEnabled => true;
}

class ApplyCheckListIntent extends Intent {
const ApplyCheckListIntent();
}

// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class _ApplyCheckListAction extends Action<ApplyCheckListIntent> {
_ApplyCheckListAction(this.state);

final RawEditorState state;

bool _getIsToggled() {
final attrs = state.controller.getSelectionStyle().attributes;
var attribute = state.controller.toolbarButtonToggler[Attribute.list.key];

if (attribute == null) {
attribute = attrs[Attribute.list.key];
} else {
// checkbox tapping causes controller.selection to go to offset 0
state.controller.toolbarButtonToggler.remove(Attribute.list.key);
}

if (attribute == null) {
return false;
}
return attribute.value == Attribute.unchecked.value ||
attribute.value == Attribute.checked.value;
}

@override
void invoke(ApplyCheckListIntent intent, [BuildContext? context]) {
state.controller.formatSelection(_getIsToggled()
? Attribute.clone(Attribute.unchecked, null)
: Attribute.unchecked);
}

@override
bool get isActionEnabled => true;
}
Loading

0 comments on commit 6456c5b

Please sign in to comment.