@@ -6,6 +6,7 @@ import 'dart:math';
66import  'package:async/async.dart' ;
77import  'package:dartssh2/dartssh2.dart' ;
88import  'package:flutter/material.dart' ;
9+ import  'package:flutter/services.dart' ;
910import  'package:flutter_riverpod/flutter_riverpod.dart' ;
1011import  'package:flutter_svg/flutter_svg.dart' ;
1112import  '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