Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy paste improvements #3849

Open
wants to merge 3 commits into
base: gui-open-shell-start-vm
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/client/gui/lib/platform/windows.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@ class WindowsPlatform extends MpPlatform {
Map<SingleActivator, Intent> get terminalShortcuts => const {
SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true):
CopySelectionTextIntent.copy,
SingleActivator(LogicalKeyboardKey.insert, control: true):
CopySelectionTextIntent.copy,
SingleActivator(LogicalKeyboardKey.keyV, control: true, shift: true):
PasteTextIntent(SelectionChangedCause.keyboard),
SingleActivator(LogicalKeyboardKey.insert, shift: true):
PasteTextIntent(SelectionChangedCause.keyboard),
SingleActivator(LogicalKeyboardKey.equal, control: true):
IncreaseTerminalFontIntent(),
SingleActivator(LogicalKeyboardKey.equal, control: true, shift: true):
Expand Down
220 changes: 150 additions & 70 deletions src/client/gui/lib/vm_details/terminal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:math';
import 'package:async/async.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:synchronized/synchronized.dart';
Expand Down Expand Up @@ -182,6 +183,8 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
static const fontSizeStep = 0.5;

final scrollController = ScrollController();
final contextMenuController = ContextMenuController();
final terminalController = TerminalController();
final focusNode = FocusNode();
var fontSize = defaultFontSize;
late final terminalIdentifier = (vmName: widget.name, shellId: widget.id);
Expand All @@ -195,6 +198,8 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
@override
void dispose() {
scrollController.dispose();
contextMenuController.remove();
terminalController.dispose();
focusNode.dispose();
super.dispose();
}
Expand Down Expand Up @@ -226,6 +231,87 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
ref.read(terminalProvider(terminalIdentifier).notifier).start();
}

final buttonStyle = ButtonStyle(
foregroundColor: WidgetStateColor.resolveWith((states) {
final disabled = states.contains(WidgetState.disabled);
return Colors.white.withOpacity(disabled ? 0.5 : 1.0);
}),
side: WidgetStateBorderSide.resolveWith((states) {
final disabled = states.contains(WidgetState.disabled);
var color = Colors.white.withOpacity(disabled ? 0.5 : 1.0);
return BorderSide(color: color);
}),
);

final terminalTheme = TerminalTheme(
cursor: Color(0xFFE5E5E5),
selection: Color(0x80E5E5E5),
foreground: Color(0xffffffff),
background: Color(0xff380c2a),
black: Color(0xFF000000),
white: Color(0xFFE5E5E5),
red: Color(0xFFCD3131),
green: Color(0xFF0DBC79),
yellow: Color(0xFFE5E510),
blue: Color(0xFF2472C8),
magenta: Color(0xFFBC3FBC),
cyan: Color(0xFF11A8CD),
brightBlack: Color(0xFF666666),
brightRed: Color(0xFFF14C4C),
brightGreen: Color(0xFF23D18B),
brightYellow: Color(0xFFF5F543),
brightBlue: Color(0xFF3B8EEA),
brightMagenta: Color(0xFFD670D6),
brightCyan: Color(0xFF29B8DB),
brightWhite: Color(0xFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
);

void openContextMenu(Offset offset, BuildContext context) {
final buttonItems = [
ContextMenuButtonItem(
label: 'Copy',
onPressed: () {
ContextMenuController.removeAny();
Actions.maybeInvoke(
context,
CopySelectionTextIntent.copy,
);
},
),
ContextMenuButtonItem(
label: 'Paste',
onPressed: () {
ContextMenuController.removeAny();
Actions.maybeInvoke(
context,
PasteTextIntent(SelectionChangedCause.keyboard),
);
},
),
];

final style = Theme.of(context).textButtonTheme.style?.copyWith(
backgroundColor: WidgetStatePropertyAll(Colors.transparent),
);

contextMenuController.show(
context: context,
contextMenuBuilder: (_) => TapRegion(
onTapOutside: (_) => ContextMenuController.removeAny(),
child: TextButtonTheme(
data: TextButtonThemeData(style: style),
child: AdaptiveTextSelectionToolbar.buttonItems(
anchors: TextSelectionToolbarAnchors(primaryAnchor: offset),
buttonItems: buttonItems,
),
),
),
);
}

@override
Widget build(BuildContext context) {
final terminal = ref.watch(terminalProvider(terminalIdentifier));
Expand All @@ -235,19 +321,8 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
final vmRunning = vmStatus == Status.RUNNING;
final canStartVm = [Status.STOPPED, Status.SUSPENDED].contains(vmStatus);

final buttonStyle = ButtonStyle(
foregroundColor: WidgetStateColor.resolveWith((states) {
final disabled = states.contains(WidgetState.disabled);
return Colors.white.withOpacity(disabled ? 0.5 : 1.0);
}),
side: WidgetStateBorderSide.resolveWith((states) {
final disabled = states.contains(WidgetState.disabled);
var color = Colors.white.withOpacity(disabled ? 0.5 : 1.0);
return BorderSide(color: color);
}),
);

if (terminal == null) {
contextMenuController.remove();
return Container(
color: const Color(0xff380c2a),
alignment: Alignment.center,
Expand All @@ -270,66 +345,71 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
);
}

return Actions(
actions: {
IncreaseTerminalFontIntent: CallbackAction<IncreaseTerminalFontIntent>(
onInvoke: (_) => setState(() {
fontSize = min(fontSize + fontSizeStep, maxFontSize);
}),
),
DecreaseTerminalFontIntent: CallbackAction<DecreaseTerminalFontIntent>(
onInvoke: (_) => setState(() {
fontSize = max(fontSize - fontSizeStep, minFontSize);
}),
),
ResetTerminalFontIntent: CallbackAction<ResetTerminalFontIntent>(
onInvoke: (_) => setState(() => fontSize = defaultFontSize),
),
},
child: RawScrollbar(
controller: scrollController,
thickness: 9,
child: ClipRect(
child: TerminalView(
terminal,
scrollController: scrollController,
focusNode: focusNode,
shortcuts: mpPlatform.terminalShortcuts,
hardwareKeyboardOnly: true,
padding: const EdgeInsets.all(4),
textStyle: TerminalStyle(
fontFamily: 'UbuntuMono',
fontFamilyFallback: ['NotoColorEmoji', 'FreeSans'],
fontSize: fontSize,
),
theme: const TerminalTheme(
cursor: Color(0xFFE5E5E5),
selection: Color(0x80E5E5E5),
foreground: Color(0xffffffff),
background: Color(0xff380c2a),
black: Color(0xFF000000),
white: Color(0xFFE5E5E5),
red: Color(0xFFCD3131),
green: Color(0xFF0DBC79),
yellow: Color(0xFFE5E510),
blue: Color(0xFF2472C8),
magenta: Color(0xFFBC3FBC),
cyan: Color(0xFF11A8CD),
brightBlack: Color(0xFF666666),
brightRed: Color(0xFFF14C4C),
brightGreen: Color(0xFF23D18B),
brightYellow: Color(0xFFF5F543),
brightBlue: Color(0xFF3B8EEA),
brightMagenta: Color(0xFFD670D6),
brightCyan: Color(0xFF29B8DB),
brightWhite: Color(0xFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
),
),
// we need a builder so that we introduce a new BuildContext that will end up
// being below the BuildContext of the Actions widget so that the events can propagate
final terminalView = Builder(builder: (context) {
return TerminalView(
terminal,
controller: terminalController,
focusNode: focusNode,
hardwareKeyboardOnly: true,
onSecondaryTapUp: (d, _) => openContextMenu(d.globalPosition, context),
padding: const EdgeInsets.all(4),
scrollController: scrollController,
shortcuts: mpPlatform.terminalShortcuts,
theme: terminalTheme,
textStyle: TerminalStyle(
fontFamily: 'UbuntuMono',
fontFamilyFallback: ['NotoColorEmoji', 'FreeSans'],
fontSize: fontSize,
),
);
});

final scrollableTerminal = RawScrollbar(
controller: scrollController,
thickness: 9,
child: ClipRect(child: terminalView),
);

final terminalActions = {
IncreaseTerminalFontIntent: CallbackAction<IncreaseTerminalFontIntent>(
onInvoke: (_) => setState(() {
fontSize = min(fontSize + fontSizeStep, maxFontSize);
}),
),
DecreaseTerminalFontIntent: CallbackAction<DecreaseTerminalFontIntent>(
onInvoke: (_) => setState(() {
fontSize = max(fontSize - fontSizeStep, minFontSize);
}),
),
ResetTerminalFontIntent: CallbackAction<ResetTerminalFontIntent>(
onInvoke: (_) => setState(() => fontSize = defaultFontSize),
),
PasteTextIntent: CallbackAction<PasteTextIntent>(
onInvoke: (_) async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text;
if (text == null) return null;
terminal.paste(text);
terminalController.clearSelection();
return null;
},
),
CopySelectionTextIntent: CallbackAction<CopySelectionTextIntent>(
onInvoke: (_) async {
final selection = terminalController.selection;
if (selection == null) return null;
final text = terminal.buffer.getText(selection);
await Clipboard.setData(ClipboardData(text: text));
return null;
},
),
};

return Actions(
actions: terminalActions,
child: scrollableTerminal,
);
}
}
Expand Down
Loading