From eb6ac79eed490cce5004b3634f19d0d751708aa2 Mon Sep 17 00:00:00 2001 From: Nate Brown Date: Mon, 31 Aug 2020 18:32:40 -0500 Subject: [PATCH] Wrap SelectableText with keyboard shortcuts to help chromeos --- lib/components/SpecialSelectableText.dart | 683 ++++++++++++++++++ lib/components/SpecialTextField.dart | 3 + lib/screens/AboutScreen.dart | 3 +- lib/screens/HostInfoScreen.dart | 13 +- lib/screens/SiteDetailScreen.dart | 3 +- lib/screens/SiteLogsScreen.dart | 3 +- .../siteConfig/CertificateDetailsScreen.dart | 19 +- lib/screens/siteConfig/CertificateScreen.dart | 9 +- .../siteConfig/RenderedConfigScreen.dart | 3 +- lib/screens/siteConfig/SiteConfigScreen.dart | 3 +- 10 files changed, 721 insertions(+), 21 deletions(-) create mode 100644 lib/components/SpecialSelectableText.dart diff --git a/lib/components/SpecialSelectableText.dart b/lib/components/SpecialSelectableText.dart new file mode 100644 index 0000000..f63f9a0 --- /dev/null +++ b/lib/components/SpecialSelectableText.dart @@ -0,0 +1,683 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.8 + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; + + +/// An eyeballed value that moves the cursor slightly left of where it is +/// rendered for text on Android so its positioning more accurately matches the +/// native iOS text cursor positioning. +/// +/// This value is in device pixels, not logical pixels as is typically used +/// throughout the codebase. +const int iOSHorizontalOffset = -2; + +class _TextSpanEditingController extends TextEditingController { + _TextSpanEditingController({@required TextSpan textSpan}): + assert(textSpan != null), + _textSpan = textSpan, + super(text: textSpan.toPlainText()); + + final TextSpan _textSpan; + + @override + TextSpan buildTextSpan({TextStyle style ,bool withComposing}) { + // This does not care about composing. + return TextSpan( + style: style, + children: [_textSpan], + ); + } + + @override + set text(String newText) { + // This should never be reached. + throw UnimplementedError(); + } +} + +class _SpecialSelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { + _SpecialSelectableTextSelectionGestureDetectorBuilder({ + @required _SpecialSelectableTextState state, + }) : _state = state, + super(delegate: state); + + final _SpecialSelectableTextState _state; + + @override + void onForcePressStart(ForcePressDetails details) { + super.onForcePressStart(details); + if (delegate.selectionEnabled && shouldShowSelectionToolbar) { + editableText.showToolbar(); + } + } + + @override + void onForcePressEnd(ForcePressDetails details) { + // Not required. + } + + @override + void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { + if (delegate.selectionEnabled) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + renderEditable.selectWordsInRange( + from: details.globalPosition - details.offsetFromOrigin, + to: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + break; + } + } + } + + @override + void onSingleTapUp(TapUpDetails details) { + editableText.hideToolbar(); + if (delegate.selectionEnabled) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + renderEditable.selectPosition(cause: SelectionChangedCause.tap); + break; + } + } + if (_state.widget.onTap != null) + _state.widget.onTap(); + } + + @override + void onSingleLongTapStart(LongPressStartDetails details) { + if (delegate.selectionEnabled) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + renderEditable.selectWord(cause: SelectionChangedCause.longPress); + Feedback.forLongPress(_state.context); + break; + } + } + } +} + +/// A run of selectable text with a single style. +/// +/// The [SpecialSelectableText] widget displays a string of text with a single style. +/// The string might break across multiple lines or might all be displayed on +/// the same line depending on the layout constraints. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc} +/// +/// The [style] argument is optional. When omitted, the text will use the style +/// from the closest enclosing [DefaultTextStyle]. If the given style's +/// [TextStyle.inherit] property is true (the default), the given style will +/// be merged with the closest enclosing [DefaultTextStyle]. This merging +/// behavior is useful, for example, to make the text bold while using the +/// default font family and size. +/// +/// {@tool snippet} +/// +/// ```dart +/// SpecialSelectableText( +/// 'Hello! How are you?', +/// textAlign: TextAlign.center, +/// style: TextStyle(fontWeight: FontWeight.bold), +/// ) +/// ``` +/// {@end-tool} +/// +/// Using the [SpecialSelectableText.rich] constructor, the [SpecialSelectableText] widget can +/// display a paragraph with differently styled [TextSpan]s. The sample +/// that follows displays "Hello beautiful world" with different styles +/// for each word. +/// +/// {@tool snippet} +/// +/// ```dart +/// const SpecialSelectableText.rich( +/// TextSpan( +/// text: 'Hello', // default text style +/// children: [ +/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), +/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), +/// ], +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Interactivity +/// +/// To make [SpecialSelectableText] react to touch events, use callback [onTap] to achieve +/// the desired behavior. +/// +/// See also: +/// +/// * [Text], which is the non selectable version of this widget. +/// * [TextField], which is the editable version of this widget. +class SpecialSelectableText extends StatefulWidget { + /// Creates a selectable text widget. + /// + /// If the [style] argument is null, the text will use the style from the + /// closest enclosing [DefaultTextStyle]. + /// + + /// The [showCursor], [autofocus], [dragStartBehavior], and [data] parameters + /// must not be null. If specified, the [maxLines] argument must be greater + /// than zero. + const SpecialSelectableText( + this.data, { + Key key, + this.focusNode, + this.style, + this.strutStyle, + this.textAlign, + this.textDirection, + this.textScaleFactor, + this.showCursor = false, + this.autofocus = false, + ToolbarOptions toolbarOptions, + this.minLines, + this.maxLines, + this.cursorWidth = 2.0, + this.cursorRadius, + this.cursorColor, + this.dragStartBehavior = DragStartBehavior.start, + this.enableInteractiveSelection = true, + this.onTap, + this.scrollPhysics, + this.textHeightBehavior, + this.textWidthBasis, + }) : assert(showCursor != null), + assert(autofocus != null), + assert(dragStartBehavior != null), + assert(maxLines == null || maxLines > 0), + assert(minLines == null || minLines > 0), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + 'minLines can\'t be greater than maxLines', + ), + assert( + data != null, + 'A non-null String must be provided to a SpecialSelectableText widget.', + ), + textSpan = null, + toolbarOptions = toolbarOptions ?? + const ToolbarOptions( + selectAll: true, + copy: true, + ), + super(key: key); + + /// Creates a selectable text widget with a [TextSpan]. + /// + /// The [textSpan] parameter must not be null and only contain [TextSpan] in + /// [textSpan.children]. Other type of [InlineSpan] is not allowed. + /// + /// The [autofocus] and [dragStartBehavior] arguments must not be null. + const SpecialSelectableText.rich( + this.textSpan, { + Key key, + this.focusNode, + this.style, + this.strutStyle, + this.textAlign, + this.textDirection, + this.textScaleFactor, + this.showCursor = false, + this.autofocus = false, + ToolbarOptions toolbarOptions, + this.minLines, + this.maxLines, + this.cursorWidth = 2.0, + this.cursorRadius, + this.cursorColor, + this.dragStartBehavior = DragStartBehavior.start, + this.enableInteractiveSelection = true, + this.onTap, + this.scrollPhysics, + this.textHeightBehavior, + this.textWidthBasis, + }) : assert(showCursor != null), + assert(autofocus != null), + assert(dragStartBehavior != null), + assert(maxLines == null || maxLines > 0), + assert(minLines == null || minLines > 0), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + 'minLines can\'t be greater than maxLines', + ), + assert( + textSpan != null, + 'A non-null TextSpan must be provided to a SpecialSelectableText.rich widget.', + ), + data = null, + toolbarOptions = toolbarOptions ?? + const ToolbarOptions( + selectAll: true, + copy: true, + ), + super(key: key); + + /// The text to display. + /// + /// This will be null if a [textSpan] is provided instead. + final String data; + + /// The text to display as a [TextSpan]. + /// + /// This will be null if [data] is provided instead. + final TextSpan textSpan; + + /// Defines the focus for this widget. + /// + /// Text is only selectable when widget is focused. + /// + /// The [focusNode] is a long-lived object that's typically managed by a + /// [StatefulWidget] parent. See [FocusNode] for more information. + /// + /// To give the focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// + /// ```dart + /// FocusScope.of(context).requestFocus(myFocusNode); + /// ``` + /// + /// This happens automatically when the widget is tapped. + /// + /// To be notified when the widget gains or loses the focus, add a listener + /// to the [focusNode]: + /// + /// ```dart + /// focusNode.addListener(() { print(myFocusNode.hasFocus); }); + /// ``` + /// + /// If null, this widget will create its own [FocusNode]. + final FocusNode focusNode; + + /// The style to use for the text. + /// + /// If null, defaults [DefaultTextStyle] of context. + final TextStyle style; + + /// {@macro flutter.widgets.editableText.strutStyle} + final StrutStyle strutStyle; + + /// {@macro flutter.widgets.editableText.textAlign} + final TextAlign textAlign; + + /// {@macro flutter.widgets.editableText.textDirection} + final TextDirection textDirection; + + /// {@macro flutter.widgets.editableText.textScaleFactor} + final double textScaleFactor; + + /// {@macro flutter.widgets.editableText.autofocus} + final bool autofocus; + + /// {@macro flutter.widgets.editableText.minLines} + final int minLines; + + /// {@macro flutter.widgets.editableText.maxLines} + final int maxLines; + + /// {@macro flutter.widgets.editableText.showCursor} + final bool showCursor; + + /// {@macro flutter.widgets.editableText.cursorWidth} + final double cursorWidth; + + /// {@macro flutter.widgets.editableText.cursorRadius} + final Radius cursorRadius; + + /// The color to use when painting the cursor. + /// + /// Defaults to the theme's `cursorColor` when null. + final Color cursorColor; + + /// {@macro flutter.widgets.editableText.enableInteractiveSelection} + final bool enableInteractiveSelection; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// Configuration of toolbar options. + /// + /// Paste and cut will be disabled regardless. + /// + /// If not set, select all and copy will be enabled by default. + final ToolbarOptions toolbarOptions; + + /// {@macro flutter.rendering.editable.selectionEnabled} + bool get selectionEnabled { + return enableInteractiveSelection; + } + + /// Called when the user taps on this selectable text. + /// + /// The selectable text builds a [GestureDetector] to handle input events like tap, + /// to trigger focus requests, to move the caret, adjust the selection, etc. + /// Handling some of those events by wrapping the selectable text with a competing + /// GestureDetector is problematic. + /// + /// To unconditionally handle taps, without interfering with the selectable text's + /// internal gesture detector, provide this callback. + /// + /// To be notified when the text field gains or loses the focus, provide a + /// [focusNode] and add a listener to that. + /// + /// To listen to arbitrary pointer events without competing with the + /// selectable text's internal gesture detector, use a [Listener]. + final GestureTapCallback onTap; + + /// {@macro flutter.widgets.editableText.scrollPhysics} + final ScrollPhysics scrollPhysics; + + /// {@macro flutter.dart:ui.textHeightBehavior} + final TextHeightBehavior textHeightBehavior; + + /// {@macro flutter.painting.textPainter.textWidthBasis} + final TextWidthBasis textWidthBasis; + + @override + _SpecialSelectableTextState createState() => _SpecialSelectableTextState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('data', data, defaultValue: null)); + properties.add(DiagnosticsProperty('focusNode', focusNode, defaultValue: null)); + properties.add(DiagnosticsProperty('style', style, defaultValue: null)); + properties.add(DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty('showCursor', showCursor, defaultValue: false)); + properties.add(IntProperty('minLines', minLines, defaultValue: null)); + properties.add(IntProperty('maxLines', maxLines, defaultValue: null)); + properties.add(EnumProperty('textAlign', textAlign, defaultValue: null)); + properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); + properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); + properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); + properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, defaultValue: null)); + properties.add(DiagnosticsProperty('cursorColor', cursorColor, defaultValue: null)); + properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled')); + properties.add(DiagnosticsProperty('scrollPhysics', scrollPhysics, defaultValue: null)); + properties.add(DiagnosticsProperty('textHeightBehavior', textHeightBehavior, defaultValue: null)); + } +} + +class _SpecialSelectableTextState extends State with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { + EditableTextState get _editableText => editableTextKey.currentState; + + _TextSpanEditingController _controller; + + FocusNode _keyFocusNode = FocusNode(); + FocusNode _focusNode; + FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + + bool _showSelectionHandles = false; + + _SpecialSelectableTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; + + // API for TextSelectionGestureDetectorBuilderDelegate. + @override + bool forcePressEnabled; + + @override + final GlobalKey editableTextKey = GlobalKey(); + + @override + bool get selectionEnabled => widget.selectionEnabled; + // End of API for TextSelectionGestureDetectorBuilderDelegate. + + @override + void initState() { + super.initState(); + _selectionGestureDetectorBuilder = _SpecialSelectableTextSelectionGestureDetectorBuilder(state: this); + _controller = _TextSpanEditingController( + textSpan: widget.textSpan ?? TextSpan(text: widget.data) + ); + } + + @override + void didUpdateWidget(SpecialSelectableText oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) { + _controller = _TextSpanEditingController( + textSpan: widget.textSpan ?? TextSpan(text: widget.data) + ); + } + if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) { + _showSelectionHandles = false; + } + } + + @override + void dispose() { + _focusNode?.dispose(); + super.dispose(); + } + + void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { + final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); + if (willShowSelectionHandles != _showSelectionHandles) { + setState(() { + _showSelectionHandles = willShowSelectionHandles; + }); + } + + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + if (cause == SelectionChangedCause.longPress) { + _editableText?.bringIntoView(selection.base); + } + return; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + // Do nothing. + } + } + + /// Toggle the toolbar when a selection handle is tapped. + void _handleSelectionHandleTapped() { + if (_controller.selection.isCollapsed) { + _editableText.toggleToolbar(); + } + } + + bool _shouldShowSelectionHandles(SelectionChangedCause cause) { + // When the text field is activated by something that doesn't trigger the + // selection overlay, we shouldn't show the handles either. + if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) + return false; + + if (_controller.selection.isCollapsed) + return false; + + if (cause == SelectionChangedCause.keyboard) + return false; + + if (cause == SelectionChangedCause.longPress) + return true; + + if (_controller.text.isNotEmpty) + return true; + + return false; + } + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); // See AutomaticKeepAliveClientMixin. + assert(() { + return _controller._textSpan.visitChildren((InlineSpan span) => span.runtimeType == TextSpan); + }(), 'SpecialSelectableText only supports TextSpan; Other type of InlineSpan is not allowed'); + assert(debugCheckHasMediaQuery(context)); + assert(debugCheckHasDirectionality(context)); + assert( + !(widget.style != null && widget.style.inherit == false && + (widget.style.fontSize == null || widget.style.textBaseline == null)), + 'inherit false style must supply fontSize and textBaseline', + ); + + final ThemeData themeData = Theme.of(context); + final FocusNode focusNode = _effectiveFocusNode; + + TextSelectionControls textSelectionControls; + bool paintCursorAboveText; + bool cursorOpacityAnimates; + Offset cursorOffset; + Color cursorColor = widget.cursorColor; + Radius cursorRadius = widget.cursorRadius; + + switch (themeData.platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + forcePressEnabled = true; + textSelectionControls = cupertinoTextSelectionControls; + paintCursorAboveText = true; + cursorOpacityAnimates = true; + cursorColor ??= CupertinoTheme.of(context).primaryColor; + cursorRadius ??= const Radius.circular(2.0); + cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + forcePressEnabled = false; + textSelectionControls = materialTextSelectionControls; + paintCursorAboveText = false; + cursorOpacityAnimates = false; + cursorColor ??= themeData.cursorColor; + break; + } + + final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); + TextStyle effectiveTextStyle = widget.style; + if (widget.style == null || widget.style.inherit) + effectiveTextStyle = defaultTextStyle.style.merge(widget.style); + if (MediaQuery.boldTextOverride(context)) + effectiveTextStyle = effectiveTextStyle.merge(const TextStyle(fontWeight: FontWeight.bold)); + final Widget child = RepaintBoundary( + child: EditableText( + key: editableTextKey, + style: effectiveTextStyle, + readOnly: true, + textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis, + textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior, + showSelectionHandles: _showSelectionHandles, + showCursor: widget.showCursor, + controller: _controller, + focusNode: focusNode, + strutStyle: widget.strutStyle ?? const StrutStyle(), + textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, + textDirection: widget.textDirection, + textScaleFactor: widget.textScaleFactor, + autofocus: widget.autofocus, + forceLine: false, + toolbarOptions: widget.toolbarOptions, + minLines: widget.minLines, + maxLines: widget.maxLines ?? defaultTextStyle.maxLines, + selectionColor: themeData.textSelectionColor, + selectionControls: widget.selectionEnabled ? textSelectionControls : null, + onSelectionChanged: _handleSelectionChanged, + onSelectionHandleTapped: _handleSelectionHandleTapped, + rendererIgnoresPointer: true, + cursorWidth: widget.cursorWidth, + cursorRadius: cursorRadius, + cursorColor: cursorColor, + cursorOpacityAnimates: cursorOpacityAnimates, + cursorOffset: cursorOffset, + paintCursorAboveText: paintCursorAboveText, + backgroundCursorColor: CupertinoColors.inactiveGray, + enableInteractiveSelection: widget.enableInteractiveSelection, + dragStartBehavior: widget.dragStartBehavior, + scrollPhysics: widget.scrollPhysics, + ), + ); + + return Semantics( + onTap: () { + if (!_controller.selection.isValid) + _controller.selection = TextSelection.collapsed(offset: _controller.text.length); + _effectiveFocusNode.requestFocus(); + }, + onLongPress: () { + _effectiveFocusNode.requestFocus(); + }, + child: _selectionGestureDetectorBuilder.buildGestureDetector( + behavior: HitTestBehavior.translucent, + child: RawKeyboardListener( + focusNode: _keyFocusNode, + onKey: _onKey, + child: child, + ) + ), + ); + } + + _onKey(RawKeyEvent event) { + // We don't care about key up events + if (event is RawKeyUpEvent) { + return; + } + + //TODO: tab to next focus node + + // Handle special keyboard events with control key + if (event.data.isControlPressed) { + // Handle select all + if (event.logicalKey == LogicalKeyboardKey.keyA) { + _controller.selection = TextSelection(baseOffset: 0, extentOffset: _controller.text.length); + return; + } + + // Handle copy + if (event.logicalKey == LogicalKeyboardKey.keyC) { + Clipboard.setData(ClipboardData(text: _controller.selection.textInside(_controller.text))); + return; + } + } + } +} diff --git a/lib/components/SpecialTextField.dart b/lib/components/SpecialTextField.dart index ac5deca..0df620a 100644 --- a/lib/components/SpecialTextField.dart +++ b/lib/components/SpecialTextField.dart @@ -24,6 +24,7 @@ class SpecialTextField extends StatefulWidget { this.textAlign, this.autofocus, this.onChanged, + this.enabled, this.expands, this.keyboardAppearance, this.textAlignVertical, @@ -51,6 +52,7 @@ class SpecialTextField extends StatefulWidget { final bool autofocus; final ValueChanged onChanged; + final bool enabled; final List inputFormatters; final bool expands; @@ -98,6 +100,7 @@ class _SpecialTextFieldState extends State { autofocus: widget.autofocus, focusNode: widget.focusNode, onChanged: widget.onChanged, + enabled: widget.enabled, onSubmitted: (_) { if (widget.nextFocusNode != null) { FocusScope.of(context).requestFocus(widget.nextFocusNode); diff --git a/lib/screens/AboutScreen.dart b/lib/screens/AboutScreen.dart index 7741793..a489557 100644 --- a/lib/screens/AboutScreen.dart +++ b/lib/screens/AboutScreen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:mobile_nebula/components/SimplePage.dart'; +import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigPageItem.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart'; @@ -66,6 +67,6 @@ class _AboutScreenState extends State { } _buildText(String str) { - return Align(alignment: AlignmentDirectional.centerEnd, child: SelectableText(str)); + return Align(alignment: AlignmentDirectional.centerEnd, child: SpecialSelectableText(str)); } } \ No newline at end of file diff --git a/lib/screens/HostInfoScreen.dart b/lib/screens/HostInfoScreen.dart index 50cab7a..9151f7d 100644 --- a/lib/screens/HostInfoScreen.dart +++ b/lib/screens/HostInfoScreen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:mobile_nebula/components/SimplePage.dart'; +import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigPageItem.dart'; @@ -60,7 +61,7 @@ class _HostInfoScreenState extends State { Widget _buildMain() { return ConfigSection(children: [ - ConfigItem(label: Text('VPN IP'), labelWidth: 150, content: SelectableText(hostInfo.vpnIp)), + ConfigItem(label: Text('VPN IP'), labelWidth: 150, content: SpecialSelectableText(hostInfo.vpnIp)), hostInfo.cert != null ? ConfigPageItem( label: Text('Certificate'), @@ -75,12 +76,12 @@ class _HostInfoScreenState extends State { Widget _buildDetails() { return ConfigSection(children: [ ConfigItem( - label: Text('Lighthouse'), labelWidth: 150, content: SelectableText(widget.isLighthouse ? 'Yes' : 'No')), - ConfigItem(label: Text('Local Index'), labelWidth: 150, content: SelectableText('${hostInfo.localIndex}')), - ConfigItem(label: Text('Remote Index'), labelWidth: 150, content: SelectableText('${hostInfo.remoteIndex}')), + label: Text('Lighthouse'), labelWidth: 150, content: SpecialSelectableText(widget.isLighthouse ? 'Yes' : 'No')), + ConfigItem(label: Text('Local Index'), labelWidth: 150, content: SpecialSelectableText('${hostInfo.localIndex}')), + ConfigItem(label: Text('Remote Index'), labelWidth: 150, content: SpecialSelectableText('${hostInfo.remoteIndex}')), ConfigItem( - label: Text('Message Counter'), labelWidth: 150, content: SelectableText('${hostInfo.messageCounter}')), - ConfigItem(label: Text('Cached Packets'), labelWidth: 150, content: SelectableText('${hostInfo.cachedPackets}')), + label: Text('Message Counter'), labelWidth: 150, content: SpecialSelectableText('${hostInfo.messageCounter}')), + ConfigItem(label: Text('Cached Packets'), labelWidth: 150, content: SpecialSelectableText('${hostInfo.cachedPackets}')), ]); } diff --git a/lib/screens/SiteDetailScreen.dart b/lib/screens/SiteDetailScreen.dart index 58b2c38..7d8d84e 100644 --- a/lib/screens/SiteDetailScreen.dart +++ b/lib/screens/SiteDetailScreen.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:mobile_nebula/components/SimplePage.dart'; +import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/components/config/ConfigPageItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart'; @@ -109,7 +110,7 @@ class _SiteDetailScreenState extends State { List items = []; site.errors.forEach((error) { items.add(ConfigItem( - labelWidth: 0, content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error)))); + labelWidth: 0, content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SpecialSelectableText(error)))); }); return ConfigSection( diff --git a/lib/screens/SiteLogsScreen.dart b/lib/screens/SiteLogsScreen.dart index 19be308..b40774b 100644 --- a/lib/screens/SiteLogsScreen.dart +++ b/lib/screens/SiteLogsScreen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:mobile_nebula/components/SimplePage.dart'; +import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/models/Site.dart'; import 'package:mobile_nebula/services/share.dart'; import 'package:mobile_nebula/services/utils.dart'; @@ -54,7 +55,7 @@ class _SiteLogsScreenState extends State { child: Container( padding: EdgeInsets.all(5), constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width), - child: SelectableText(logs.trim(), style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))), + child: SpecialSelectableText(logs.trim(), style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))), bottomBar: _buildBottomBar(), ); } diff --git a/lib/screens/siteConfig/CertificateDetailsScreen.dart b/lib/screens/siteConfig/CertificateDetailsScreen.dart index 8a4e33f..313fc5f 100644 --- a/lib/screens/siteConfig/CertificateDetailsScreen.dart +++ b/lib/screens/siteConfig/CertificateDetailsScreen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:mobile_nebula/components/SimplePage.dart'; +import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/models/Certificate.dart'; @@ -36,7 +37,7 @@ class _CertificateDetailsScreenState extends State { Widget _buildID() { return ConfigSection(children: [ - ConfigItem(label: Text('Name'), content: SelectableText(widget.certificate.cert.details.name)), + ConfigItem(label: Text('Name'), content: SpecialSelectableText(widget.certificate.cert.details.name)), ConfigItem( label: Text('Type'), content: Text(widget.certificate.cert.details.isCa ? 'CA certificate' : 'Client certificate')), @@ -55,10 +56,10 @@ class _CertificateDetailsScreenState extends State { ConfigItem(label: Text('Valid?'), content: valid), ConfigItem( label: Text('Created'), - content: SelectableText(widget.certificate.cert.details.notBefore.toLocal().toString())), + content: SpecialSelectableText(widget.certificate.cert.details.notBefore.toLocal().toString())), ConfigItem( label: Text('Expires'), - content: SelectableText(widget.certificate.cert.details.notAfter.toLocal().toString())), + content: SpecialSelectableText(widget.certificate.cert.details.notAfter.toLocal().toString())), ], ); } @@ -67,17 +68,17 @@ class _CertificateDetailsScreenState extends State { List items = []; if (widget.certificate.cert.details.groups.length > 0) { items.add(ConfigItem( - label: Text('Groups'), content: SelectableText(widget.certificate.cert.details.groups.join(', ')))); + label: Text('Groups'), content: SpecialSelectableText(widget.certificate.cert.details.groups.join(', ')))); } if (widget.certificate.cert.details.ips.length > 0) { items - .add(ConfigItem(label: Text('IPs'), content: SelectableText(widget.certificate.cert.details.ips.join(', ')))); + .add(ConfigItem(label: Text('IPs'), content: SpecialSelectableText(widget.certificate.cert.details.ips.join(', ')))); } if (widget.certificate.cert.details.subnets.length > 0) { items.add(ConfigItem( - label: Text('Subnets'), content: SelectableText(widget.certificate.cert.details.subnets.join(', ')))); + label: Text('Subnets'), content: SpecialSelectableText(widget.certificate.cert.details.subnets.join(', ')))); } return items.length > 0 @@ -90,18 +91,18 @@ class _CertificateDetailsScreenState extends State { children: [ ConfigItem( label: Text('Fingerprint'), - content: SelectableText(widget.certificate.cert.fingerprint, + content: SpecialSelectableText(widget.certificate.cert.fingerprint, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), crossAxisAlignment: CrossAxisAlignment.start), ConfigItem( label: Text('Public Key'), - content: SelectableText(widget.certificate.cert.details.publicKey, + content: SpecialSelectableText(widget.certificate.cert.details.publicKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), crossAxisAlignment: CrossAxisAlignment.start), widget.certificate.rawCert != null ? ConfigItem( label: Text('PEM Format'), - content: SelectableText(widget.certificate.rawCert, + content: SpecialSelectableText(widget.certificate.rawCert, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), crossAxisAlignment: CrossAxisAlignment.start) : Container(), diff --git a/lib/screens/siteConfig/CertificateScreen.dart b/lib/screens/siteConfig/CertificateScreen.dart index f107182..035a48c 100644 --- a/lib/screens/siteConfig/CertificateScreen.dart +++ b/lib/screens/siteConfig/CertificateScreen.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:mobile_nebula/components/FormPage.dart'; +import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/components/config/ConfigButtonItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigPageItem.dart'; @@ -53,6 +54,12 @@ class _CertificateScreenState extends State { super.initState(); } + @override + void dispose() { + pasteController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { List items = []; @@ -135,7 +142,7 @@ class _CertificateScreenState extends State { children: [ ConfigItem( labelWidth: 0, - content: SelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), + content: SpecialSelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), ), ConfigButtonItem( content: Text('Share Public Key'), diff --git a/lib/screens/siteConfig/RenderedConfigScreen.dart b/lib/screens/siteConfig/RenderedConfigScreen.dart index 0a10a6d..b2fa2f3 100644 --- a/lib/screens/siteConfig/RenderedConfigScreen.dart +++ b/lib/screens/siteConfig/RenderedConfigScreen.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:mobile_nebula/components/SimplePage.dart'; +import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/services/share.dart'; class RenderedConfigScreen extends StatelessWidget { @@ -25,7 +26,7 @@ class RenderedConfigScreen extends StatelessWidget { child: Container( padding: EdgeInsets.all(5), constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width), - child: SelectableText(config, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))), + child: SpecialSelectableText(config, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))), ); } } diff --git a/lib/screens/siteConfig/SiteConfigScreen.dart b/lib/screens/siteConfig/SiteConfigScreen.dart index c88b20f..a819468 100644 --- a/lib/screens/siteConfig/SiteConfigScreen.dart +++ b/lib/screens/siteConfig/SiteConfigScreen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:mobile_nebula/components/FormPage.dart'; import 'package:mobile_nebula/components/PlatformTextFormField.dart'; +import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/components/config/ConfigPageItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart'; @@ -89,7 +90,7 @@ class _SiteConfigScreenState extends State { data = err.toString(); } - return ConfigSection(label: 'DEBUG', children: [ConfigItem(labelWidth: 0, content: SelectableText(data))]); + return ConfigSection(label: 'DEBUG', children: [ConfigItem(labelWidth: 0, content: SpecialSelectableText(data))]); } Widget _main() {