Skip to content

Commit 341beb9

Browse files
Merge pull request #3849 from canonical/copy-paste-improvements
Copy paste improvements
2 parents 78bdb6d + b940cde commit 341beb9

File tree

3 files changed

+158
-70
lines changed

3 files changed

+158
-70
lines changed

src/client/gui/lib/platform/linux.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ class LinuxPlatform extends MpPlatform {
3131
Map<SingleActivator, Intent> get terminalShortcuts => const {
3232
SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true):
3333
CopySelectionTextIntent.copy,
34+
SingleActivator(LogicalKeyboardKey.insert, control: true):
35+
CopySelectionTextIntent.copy,
3436
SingleActivator(LogicalKeyboardKey.keyV, control: true, shift: true):
3537
PasteTextIntent(SelectionChangedCause.keyboard),
38+
SingleActivator(LogicalKeyboardKey.insert, shift: true):
39+
PasteTextIntent(SelectionChangedCause.keyboard),
3640
SingleActivator(LogicalKeyboardKey.equal, control: true):
3741
IncreaseTerminalFontIntent(),
3842
SingleActivator(LogicalKeyboardKey.equal, control: true, shift: true):

src/client/gui/lib/platform/windows.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ class WindowsPlatform extends MpPlatform {
3333
Map<SingleActivator, Intent> get terminalShortcuts => const {
3434
SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true):
3535
CopySelectionTextIntent.copy,
36+
SingleActivator(LogicalKeyboardKey.insert, control: true):
37+
CopySelectionTextIntent.copy,
3638
SingleActivator(LogicalKeyboardKey.keyV, control: true, shift: true):
3739
PasteTextIntent(SelectionChangedCause.keyboard),
40+
SingleActivator(LogicalKeyboardKey.insert, shift: true):
41+
PasteTextIntent(SelectionChangedCause.keyboard),
3842
SingleActivator(LogicalKeyboardKey.equal, control: true):
3943
IncreaseTerminalFontIntent(),
4044
SingleActivator(LogicalKeyboardKey.equal, control: true, shift: true):

src/client/gui/lib/vm_details/terminal.dart

Lines changed: 150 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:math';
66
import 'package:async/async.dart';
77
import 'package:dartssh2/dartssh2.dart';
88
import 'package:flutter/material.dart';
9+
import 'package:flutter/services.dart';
910
import 'package:flutter_riverpod/flutter_riverpod.dart';
1011
import 'package:flutter_svg/flutter_svg.dart';
1112
import 'package:synchronized/synchronized.dart';
@@ -182,6 +183,8 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
182183
static const fontSizeStep = 0.5;
183184

184185
final scrollController = ScrollController();
186+
final contextMenuController = ContextMenuController();
187+
final terminalController = TerminalController();
185188
final focusNode = FocusNode();
186189
var fontSize = defaultFontSize;
187190
late final terminalIdentifier = (vmName: widget.name, shellId: widget.id);
@@ -195,6 +198,8 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
195198
@override
196199
void dispose() {
197200
scrollController.dispose();
201+
contextMenuController.remove();
202+
terminalController.dispose();
198203
focusNode.dispose();
199204
super.dispose();
200205
}
@@ -226,6 +231,87 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
226231
ref.read(terminalProvider(terminalIdentifier).notifier).start();
227232
}
228233

234+
final buttonStyle = ButtonStyle(
235+
foregroundColor: WidgetStateColor.resolveWith((states) {
236+
final disabled = states.contains(WidgetState.disabled);
237+
return Colors.white.withOpacity(disabled ? 0.5 : 1.0);
238+
}),
239+
side: WidgetStateBorderSide.resolveWith((states) {
240+
final disabled = states.contains(WidgetState.disabled);
241+
var color = Colors.white.withOpacity(disabled ? 0.5 : 1.0);
242+
return BorderSide(color: color);
243+
}),
244+
);
245+
246+
final terminalTheme = TerminalTheme(
247+
cursor: Color(0xFFE5E5E5),
248+
selection: Color(0x80E5E5E5),
249+
foreground: Color(0xffffffff),
250+
background: Color(0xff380c2a),
251+
black: Color(0xFF000000),
252+
white: Color(0xFFE5E5E5),
253+
red: Color(0xFFCD3131),
254+
green: Color(0xFF0DBC79),
255+
yellow: Color(0xFFE5E510),
256+
blue: Color(0xFF2472C8),
257+
magenta: Color(0xFFBC3FBC),
258+
cyan: Color(0xFF11A8CD),
259+
brightBlack: Color(0xFF666666),
260+
brightRed: Color(0xFFF14C4C),
261+
brightGreen: Color(0xFF23D18B),
262+
brightYellow: Color(0xFFF5F543),
263+
brightBlue: Color(0xFF3B8EEA),
264+
brightMagenta: Color(0xFFD670D6),
265+
brightCyan: Color(0xFF29B8DB),
266+
brightWhite: Color(0xFFFFFFFF),
267+
searchHitBackground: Color(0XFFFFFF2B),
268+
searchHitBackgroundCurrent: Color(0XFF31FF26),
269+
searchHitForeground: Color(0XFF000000),
270+
);
271+
272+
void openContextMenu(Offset offset, BuildContext context) {
273+
final buttonItems = [
274+
ContextMenuButtonItem(
275+
label: 'Copy',
276+
onPressed: () {
277+
ContextMenuController.removeAny();
278+
Actions.maybeInvoke(
279+
context,
280+
CopySelectionTextIntent.copy,
281+
);
282+
},
283+
),
284+
ContextMenuButtonItem(
285+
label: 'Paste',
286+
onPressed: () {
287+
ContextMenuController.removeAny();
288+
Actions.maybeInvoke(
289+
context,
290+
PasteTextIntent(SelectionChangedCause.keyboard),
291+
);
292+
},
293+
),
294+
];
295+
296+
final style = Theme.of(context).textButtonTheme.style?.copyWith(
297+
backgroundColor: WidgetStatePropertyAll(Colors.transparent),
298+
);
299+
300+
contextMenuController.show(
301+
context: context,
302+
contextMenuBuilder: (_) => TapRegion(
303+
onTapOutside: (_) => ContextMenuController.removeAny(),
304+
child: TextButtonTheme(
305+
data: TextButtonThemeData(style: style),
306+
child: AdaptiveTextSelectionToolbar.buttonItems(
307+
anchors: TextSelectionToolbarAnchors(primaryAnchor: offset),
308+
buttonItems: buttonItems,
309+
),
310+
),
311+
),
312+
);
313+
}
314+
229315
@override
230316
Widget build(BuildContext context) {
231317
final terminal = ref.watch(terminalProvider(terminalIdentifier));
@@ -235,19 +321,8 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
235321
final vmRunning = vmStatus == Status.RUNNING;
236322
final canStartVm = [Status.STOPPED, Status.SUSPENDED].contains(vmStatus);
237323

238-
final buttonStyle = ButtonStyle(
239-
foregroundColor: WidgetStateColor.resolveWith((states) {
240-
final disabled = states.contains(WidgetState.disabled);
241-
return Colors.white.withOpacity(disabled ? 0.5 : 1.0);
242-
}),
243-
side: WidgetStateBorderSide.resolveWith((states) {
244-
final disabled = states.contains(WidgetState.disabled);
245-
var color = Colors.white.withOpacity(disabled ? 0.5 : 1.0);
246-
return BorderSide(color: color);
247-
}),
248-
);
249-
250324
if (terminal == null) {
325+
contextMenuController.remove();
251326
return Container(
252327
color: const Color(0xff380c2a),
253328
alignment: Alignment.center,
@@ -270,66 +345,71 @@ class _VmTerminalState extends ConsumerState<VmTerminal> {
270345
);
271346
}
272347

273-
return Actions(
274-
actions: {
275-
IncreaseTerminalFontIntent: CallbackAction<IncreaseTerminalFontIntent>(
276-
onInvoke: (_) => setState(() {
277-
fontSize = min(fontSize + fontSizeStep, maxFontSize);
278-
}),
279-
),
280-
DecreaseTerminalFontIntent: CallbackAction<DecreaseTerminalFontIntent>(
281-
onInvoke: (_) => setState(() {
282-
fontSize = max(fontSize - fontSizeStep, minFontSize);
283-
}),
284-
),
285-
ResetTerminalFontIntent: CallbackAction<ResetTerminalFontIntent>(
286-
onInvoke: (_) => setState(() => fontSize = defaultFontSize),
287-
),
288-
},
289-
child: RawScrollbar(
290-
controller: scrollController,
291-
thickness: 9,
292-
child: ClipRect(
293-
child: TerminalView(
294-
terminal,
295-
scrollController: scrollController,
296-
focusNode: focusNode,
297-
shortcuts: mpPlatform.terminalShortcuts,
298-
hardwareKeyboardOnly: true,
299-
padding: const EdgeInsets.all(4),
300-
textStyle: TerminalStyle(
301-
fontFamily: 'UbuntuMono',
302-
fontFamilyFallback: ['NotoColorEmoji', 'FreeSans'],
303-
fontSize: fontSize,
304-
),
305-
theme: const TerminalTheme(
306-
cursor: Color(0xFFE5E5E5),
307-
selection: Color(0x80E5E5E5),
308-
foreground: Color(0xffffffff),
309-
background: Color(0xff380c2a),
310-
black: Color(0xFF000000),
311-
white: Color(0xFFE5E5E5),
312-
red: Color(0xFFCD3131),
313-
green: Color(0xFF0DBC79),
314-
yellow: Color(0xFFE5E510),
315-
blue: Color(0xFF2472C8),
316-
magenta: Color(0xFFBC3FBC),
317-
cyan: Color(0xFF11A8CD),
318-
brightBlack: Color(0xFF666666),
319-
brightRed: Color(0xFFF14C4C),
320-
brightGreen: Color(0xFF23D18B),
321-
brightYellow: Color(0xFFF5F543),
322-
brightBlue: Color(0xFF3B8EEA),
323-
brightMagenta: Color(0xFFD670D6),
324-
brightCyan: Color(0xFF29B8DB),
325-
brightWhite: Color(0xFFFFFFFF),
326-
searchHitBackground: Color(0XFFFFFF2B),
327-
searchHitBackgroundCurrent: Color(0XFF31FF26),
328-
searchHitForeground: Color(0XFF000000),
329-
),
330-
),
348+
// we need a builder so that we introduce a new BuildContext that will end up
349+
// being below the BuildContext of the Actions widget so that the events can propagate
350+
final terminalView = Builder(builder: (context) {
351+
return TerminalView(
352+
terminal,
353+
controller: terminalController,
354+
focusNode: focusNode,
355+
hardwareKeyboardOnly: true,
356+
onSecondaryTapUp: (d, _) => openContextMenu(d.globalPosition, context),
357+
padding: const EdgeInsets.all(4),
358+
scrollController: scrollController,
359+
shortcuts: mpPlatform.terminalShortcuts,
360+
theme: terminalTheme,
361+
textStyle: TerminalStyle(
362+
fontFamily: 'UbuntuMono',
363+
fontFamilyFallback: ['NotoColorEmoji', 'FreeSans'],
364+
fontSize: fontSize,
331365
),
366+
);
367+
});
368+
369+
final scrollableTerminal = RawScrollbar(
370+
controller: scrollController,
371+
thickness: 9,
372+
child: ClipRect(child: terminalView),
373+
);
374+
375+
final terminalActions = {
376+
IncreaseTerminalFontIntent: CallbackAction<IncreaseTerminalFontIntent>(
377+
onInvoke: (_) => setState(() {
378+
fontSize = min(fontSize + fontSizeStep, maxFontSize);
379+
}),
380+
),
381+
DecreaseTerminalFontIntent: CallbackAction<DecreaseTerminalFontIntent>(
382+
onInvoke: (_) => setState(() {
383+
fontSize = max(fontSize - fontSizeStep, minFontSize);
384+
}),
385+
),
386+
ResetTerminalFontIntent: CallbackAction<ResetTerminalFontIntent>(
387+
onInvoke: (_) => setState(() => fontSize = defaultFontSize),
332388
),
389+
PasteTextIntent: CallbackAction<PasteTextIntent>(
390+
onInvoke: (_) async {
391+
final data = await Clipboard.getData(Clipboard.kTextPlain);
392+
final text = data?.text;
393+
if (text == null) return null;
394+
terminal.paste(text);
395+
terminalController.clearSelection();
396+
return null;
397+
},
398+
),
399+
CopySelectionTextIntent: CallbackAction<CopySelectionTextIntent>(
400+
onInvoke: (_) async {
401+
final selection = terminalController.selection;
402+
if (selection == null) return null;
403+
final text = terminal.buffer.getText(selection);
404+
await Clipboard.setData(ClipboardData(text: text));
405+
return null;
406+
},
407+
),
408+
};
409+
410+
return Actions(
411+
actions: terminalActions,
412+
child: scrollableTerminal,
333413
);
334414
}
335415
}

0 commit comments

Comments
 (0)