Shortcut.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. using System.ComponentModel;
  2. namespace Terminal.Gui;
  3. // TODO: I don't love the name Shortcut, but I can't think of a better one right now. Shortcut is a bit overloaded.
  4. // TODO: It can mean "Application-scoped key binding" or "A key binding that is displayed in a visual way".
  5. // TODO: I tried `BarItem` but that's not great either as it implies it can only be used in `Bar`s.
  6. /// <summary>
  7. /// Displays a command, help text, and a key binding. When the key is pressed, the command will be invoked. Useful for displaying a command in <see cref="Bar"/> such as a
  8. /// menu, toolbar, or status bar.
  9. /// </summary>
  10. /// <remarks>
  11. /// <para>
  12. /// When the user clicks on the <see cref="Shortcut"/> or presses the key
  13. /// specified by <see cref="Key"/> the <see cref="Command.Accept"/> command is invoked, causing the
  14. /// <see cref="Accept"/> event to be fired
  15. /// </para>
  16. /// <para>
  17. /// If <see cref="KeyBindingScope"/> is <see cref="KeyBindingScope.Application"/>, the <see cref="Command.Accept"/> command
  18. /// be invoked regardless of what View has focus, enabling an application-wide keyboard shortcut.
  19. /// </para>
  20. /// <para>
  21. /// A Shortcut displays the command text on the left side, the help text in the middle, and the key binding on the right side.
  22. /// </para>
  23. /// <para>
  24. /// The command text can be set by setting the <see cref="CommandView"/>'s Text property or by setting <see cref="View.Title"/>.
  25. /// </para>
  26. /// <para>
  27. /// The help text can be set by setting the <see cref="HelpText"/> property or by setting <see cref="View.Text"/>.
  28. /// </para>
  29. /// <para>
  30. /// The key text is set by setting the <see cref="Key"/> property.
  31. /// If the <see cref="Key"/> is <see cref="Key.Empty"/>, the <see cref="Key"/> text is not displayed.
  32. /// </para>
  33. /// </remarks>
  34. public class Shortcut : View
  35. {
  36. /// <summary>
  37. /// Creates a new instance of <see cref="Shortcut"/>.
  38. /// </summary>
  39. public Shortcut ()
  40. {
  41. Id = "_shortcut";
  42. HighlightStyle = HighlightStyle.Pressed;
  43. Highlight += Shortcut_Highlight;
  44. CanFocus = true;
  45. Width = GetWidthDimAuto ();
  46. Height = Dim.Auto (DimAutoStyle.Content, 1);
  47. AddCommand (Gui.Command.HotKey, OnAccept);
  48. AddCommand (Gui.Command.Accept, OnAccept);
  49. KeyBindings.Add (KeyCode.Space, Gui.Command.Accept);
  50. KeyBindings.Add (KeyCode.Enter, Gui.Command.Accept);
  51. TitleChanged += Shortcut_TitleChanged; // This needs to be set before CommandView is set
  52. CommandView = new View ();
  53. HelpView.Id = "_helpView";
  54. HelpView.CanFocus = false;
  55. SetHelpViewDefaultLayout ();
  56. Add (HelpView);
  57. // HelpView.TextAlignment = Alignment.End;
  58. HelpView.MouseClick += Shortcut_MouseClick;
  59. KeyView.Id = "_keyView";
  60. // Only the Shortcut should be able to have focus, not any subviews
  61. KeyView.CanFocus = false;
  62. // Right align the text in the keyview
  63. KeyView.TextAlignment = Alignment.End;
  64. SetKeyViewDefaultLayout ();
  65. Add (KeyView);
  66. KeyView.MouseClick += Shortcut_MouseClick;
  67. MouseClick += Shortcut_MouseClick;
  68. Initialized += OnInitialized;
  69. LayoutStarted += OnLayoutStarted;
  70. return;
  71. void OnInitialized (object sender, EventArgs e)
  72. {
  73. ShowHide (CommandView);
  74. ShowHide (HelpView);
  75. ShowHide (KeyView);
  76. // Force Width to DimAuto to calculate natural width and then set it back
  77. Dim savedDim = Width;
  78. Width = GetWidthDimAuto ();
  79. _naturalWidth = Frame.Width;
  80. Width = savedDim;
  81. if (ColorScheme != null)
  82. {
  83. var cs = new ColorScheme (ColorScheme)
  84. {
  85. Normal = ColorScheme.HotNormal,
  86. HotNormal = ColorScheme.Normal
  87. };
  88. KeyView.ColorScheme = cs;
  89. }
  90. }
  91. Dim GetWidthDimAuto ()
  92. {
  93. return Dim.Auto (DimAutoStyle.Content, maximumContentDim: Dim.Func (() => PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width)));
  94. }
  95. }
  96. // When one of the subviews is "empty" we don't want to show it. So we
  97. // Use Add/Remove. We need to be careful to add them in the right order
  98. // so Pos.Align works correctly.
  99. private void ShowHide (View subView)
  100. {
  101. RemoveAll ();
  102. if (!string.IsNullOrEmpty (CommandView.Text))
  103. {
  104. Add (CommandView);
  105. }
  106. if (!string.IsNullOrEmpty (HelpView.Text))
  107. {
  108. Add (HelpView);
  109. }
  110. if (Key != Key.Empty)
  111. {
  112. Add (KeyView);
  113. }
  114. }
  115. private int? _naturalWidth;
  116. private void OnLayoutStarted (object sender, LayoutEventArgs e)
  117. {
  118. if (Width is DimAuto widthAuto)
  119. {
  120. _naturalWidth = Frame.Width;
  121. }
  122. else
  123. {
  124. if (string.IsNullOrEmpty (HelpView.Text))
  125. {
  126. return;
  127. }
  128. int currentWidth = Frame.Width;
  129. // If our width is smaller than the natural then reduce width of HelpView.
  130. if (currentWidth < _naturalWidth)
  131. {
  132. int delta = _naturalWidth.Value - currentWidth;
  133. int maxHelpWidth = int.Max (0, HelpView.Text.GetColumns () + 2 - delta);
  134. switch (maxHelpWidth)
  135. {
  136. case 0:
  137. // Hide HelpView
  138. HelpView.Visible = false;
  139. HelpView.X = 0;
  140. break;
  141. case 1:
  142. // Scrunch it by removing margins
  143. HelpView.Margin.Thickness = new (0, 0, 0, 0);
  144. break;
  145. case 2:
  146. // Scrunch just the right margin
  147. HelpView.Margin.Thickness = new (1, 0, 0, 0);
  148. break;
  149. default:
  150. // Default margin
  151. HelpView.Margin.Thickness = new (1, 0, 1, 0);
  152. break;
  153. }
  154. if (maxHelpWidth > 0)
  155. {
  156. HelpView.X = Pos.Align (Alignment.End, AlignmentModes.IgnoreFirstOrLast);
  157. // Leverage Dim.Auto's max:
  158. HelpView.Width = Dim.Auto (DimAutoStyle.Text, maximumContentDim: maxHelpWidth);
  159. HelpView.Visible = true;
  160. }
  161. }
  162. else
  163. {
  164. // Reset to default
  165. SetHelpViewDefaultLayout ();
  166. }
  167. }
  168. }
  169. private Color? _savedForeColor;
  170. private void Shortcut_Highlight (object sender, HighlightEventArgs e)
  171. {
  172. if (e.HighlightStyle.HasFlag (HighlightStyle.Pressed))
  173. {
  174. if (!_savedForeColor.HasValue)
  175. {
  176. _savedForeColor = ColorScheme.Normal.Foreground;
  177. }
  178. var cs = new ColorScheme (ColorScheme)
  179. {
  180. Normal = new (ColorScheme.Normal.Foreground.GetHighlightColor (), ColorScheme.Normal.Background)
  181. };
  182. ColorScheme = cs;
  183. }
  184. if (e.HighlightStyle == HighlightStyle.None && _savedForeColor.HasValue)
  185. {
  186. var cs = new ColorScheme (ColorScheme)
  187. {
  188. Normal = new (_savedForeColor.Value, ColorScheme.Normal.Background)
  189. };
  190. ColorScheme = cs;
  191. }
  192. SuperView?.SetNeedsDisplay ();
  193. e.Cancel = true;
  194. }
  195. private void Shortcut_MouseClick (object sender, MouseEventEventArgs e)
  196. {
  197. // When the Shortcut is clicked, we want to invoke the Command and Set focus
  198. var view = sender as View;
  199. if (view != CommandView)
  200. {
  201. CommandView.InvokeCommand (Command.Accept);
  202. e.Handled = true;
  203. return;
  204. }
  205. if (!e.Handled)
  206. {
  207. // If the subview (likely CommandView) didn't handle the mouse click, invoke the command.
  208. bool? handled = false;
  209. handled = InvokeCommand (Command.Accept);
  210. if (handled.HasValue)
  211. {
  212. e.Handled = handled.Value;
  213. }
  214. }
  215. if (CanFocus)
  216. {
  217. SetFocus ();
  218. }
  219. e.Handled = true;
  220. }
  221. /// <inheritdoc/>
  222. public override ColorScheme ColorScheme
  223. {
  224. get
  225. {
  226. if (base.ColorScheme == null)
  227. {
  228. return SuperView?.ColorScheme ?? base.ColorScheme;
  229. }
  230. return base.ColorScheme;
  231. }
  232. set
  233. {
  234. base.ColorScheme = value;
  235. if (ColorScheme != null)
  236. {
  237. var cs = new ColorScheme (ColorScheme)
  238. {
  239. Normal = ColorScheme.HotNormal,
  240. HotNormal = ColorScheme.Normal
  241. };
  242. KeyView.ColorScheme = cs;
  243. }
  244. }
  245. }
  246. #region Command
  247. private View _commandView = new ();
  248. /// <summary>
  249. /// Gets or sets the View that displays the command text and hotkey.
  250. /// </summary>
  251. /// <remarks>
  252. /// <para>
  253. /// By default, the <see cref="View.Title"/> of the <see cref="CommandView"/> is displayed as the Shortcut's
  254. /// command text.
  255. /// </para>
  256. /// <para>
  257. /// By default, the CommandView is a <see cref="View"/> with <see cref="View.CanFocus"/> set to
  258. /// <see langword="false"/>.
  259. /// </para>
  260. /// <para>
  261. /// Setting the <see cref="CommandView"/> will add it to the <see cref="Shortcut"/> and remove any existing
  262. /// <see cref="CommandView"/>.
  263. /// </para>
  264. /// </remarks>
  265. /// <example>
  266. /// <para>
  267. /// This example illustrates how to add a <see cref="Shortcut"/> to a <see cref="StatusBar"/> that toggles the
  268. /// <see cref="Application.Force16Colors"/> property.
  269. /// </para>
  270. /// <code>
  271. /// var force16ColorsShortcut = new Shortcut
  272. /// {
  273. /// Key = Key.F6,
  274. /// KeyBindingScope = KeyBindingScope.HotKey,
  275. /// CommandView = new CheckBox { Text = "Force 16 Colors" }
  276. /// };
  277. /// var cb = force16ColorsShortcut.CommandView as CheckBox;
  278. /// cb.Checked = Application.Force16Colors;
  279. ///
  280. /// cb.Toggled += (s, e) =>
  281. /// {
  282. /// var cb = s as CheckBox;
  283. /// Application.Force16Colors = cb!.Checked == true;
  284. /// Application.Refresh();
  285. /// };
  286. /// StatusBar.Add(force16ColorsShortcut);
  287. /// </code>
  288. /// </example>
  289. public View CommandView
  290. {
  291. get => _commandView;
  292. set
  293. {
  294. if (value == null)
  295. {
  296. throw new ArgumentNullException ();
  297. }
  298. if (_commandView is { })
  299. {
  300. Remove (_commandView);
  301. _commandView?.Dispose ();
  302. }
  303. _commandView = value;
  304. _commandView.Id = "_commandView";
  305. // TODO: Determine if it makes sense to allow the CommandView to be focusable.
  306. // Right now, we don't set CanFocus to false here.
  307. _commandView.CanFocus = false;
  308. // Bar will set the width of all CommandViews to the width of the widest CommandViews.
  309. _commandView.Width = Dim.Auto (DimAutoStyle.Text);
  310. _commandView.Height = Dim.Auto (DimAutoStyle.Text);
  311. _commandView.X = Pos.Align (Alignment.End, AlignmentModes.IgnoreFirstOrLast);
  312. _commandView.Y = 0; //Pos.Center ();
  313. _commandView.MouseClick += Shortcut_MouseClick;
  314. _commandView.Accept += CommandViewAccept;
  315. _commandView.Margin.Thickness = new (1, 0, 1, 0);
  316. _commandView.HotKeyChanged += (s, e) =>
  317. {
  318. if (e.NewKey != Key.Empty)
  319. {
  320. // Add it
  321. AddKeyBindingsForHotKey (e.OldKey, e.NewKey);
  322. }
  323. };
  324. _commandView.HotKeySpecifier = new ('_');
  325. Title = _commandView.Text;
  326. _commandView.TextChanged += CommandViewTextChanged;
  327. Remove (HelpView);
  328. Remove (KeyView);
  329. Add (_commandView, HelpView, KeyView);
  330. ShowHide (_commandView);
  331. UpdateKeyBinding ();
  332. return;
  333. void CommandViewMouseEvent (object sender, MouseEventEventArgs e) { e.Handled = true; }
  334. void CommandViewTextChanged (object sender, StateEventArgs<string> e)
  335. {
  336. Title = _commandView.Text;
  337. ShowHide (_commandView);
  338. }
  339. void CommandViewAccept (object sender, CancelEventArgs e)
  340. {
  341. // When the CommandView fires its Accept event, we want to act as though the
  342. // Shortcut was clicked.
  343. var args = new HandledEventArgs ();
  344. Accept?.Invoke (this, args);
  345. if (args.Handled)
  346. {
  347. e.Cancel = args.Handled;
  348. }
  349. //e.Cancel = true;
  350. }
  351. }
  352. }
  353. private void Shortcut_TitleChanged (object sender, StateEventArgs<string> e)
  354. {
  355. // If the Title changes, update the CommandView text.
  356. // This is a helper to make it easier to set the CommandView text.
  357. // CommandView is public and replaceable, but this is a convenience.
  358. _commandView.Text = Title;
  359. }
  360. #endregion Command
  361. #region Help
  362. /// <summary>
  363. /// The subview that displays the help text for the command. Internal for unit testing.
  364. /// </summary>
  365. internal View HelpView { get; } = new ();
  366. private void SetHelpViewDefaultLayout ()
  367. {
  368. HelpView.Margin.Thickness = new (1, 0, 1, 0);
  369. HelpView.X = Pos.Align (Alignment.End, AlignmentModes.IgnoreFirstOrLast);
  370. HelpView.Y = 0; //Pos.Center (),
  371. HelpView.Width = Dim.Auto (DimAutoStyle.Text);
  372. HelpView.Height = Dim.Auto (DimAutoStyle.Text);
  373. HelpView.Visible = true;
  374. }
  375. /// <summary>
  376. /// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to <see cref="HelpText"/>
  377. /// .
  378. /// </summary>
  379. public override string Text
  380. {
  381. get => HelpView?.Text;
  382. set
  383. {
  384. if (HelpView != null)
  385. {
  386. HelpView.Text = value;
  387. ShowHide (HelpView);
  388. }
  389. }
  390. }
  391. /// <summary>
  392. /// Gets or sets the help text displayed in the middle of the Shortcut.
  393. /// </summary>
  394. public string HelpText
  395. {
  396. get => HelpView?.Text;
  397. set
  398. {
  399. if (HelpView != null)
  400. {
  401. HelpView.Text = value;
  402. ShowHide (HelpView);
  403. }
  404. }
  405. }
  406. #endregion Help
  407. #region Key
  408. private Key _key = Key.Empty;
  409. /// <summary>
  410. /// Gets or sets the <see cref="Key"/> that will be bound to the <see cref="Command.Accept"/> command.
  411. /// </summary>
  412. public Key Key
  413. {
  414. get => _key;
  415. set
  416. {
  417. if (value == null)
  418. {
  419. throw new ArgumentNullException ();
  420. }
  421. _key = value;
  422. UpdateKeyBinding ();
  423. KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}";
  424. ShowHide (KeyView);
  425. }
  426. }
  427. private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey;
  428. /// <summary>
  429. /// Gets or sets the scope for the key binding for how <see cref="Key"/> is bound to <see cref="Command"/>.
  430. /// </summary>
  431. public KeyBindingScope KeyBindingScope
  432. {
  433. get => _keyBindingScope;
  434. set
  435. {
  436. _keyBindingScope = value;
  437. UpdateKeyBinding ();
  438. }
  439. }
  440. // TODO: Make internal once Bar is done
  441. /// <summary>
  442. /// Gets the subview that displays the key. Internal for unit testing.
  443. /// </summary>
  444. public View KeyView { get; } = new ();
  445. private int _minimumKeyViewSize;
  446. /// <summary>
  447. ///
  448. /// </summary>
  449. public int MinimumKeyViewSize
  450. {
  451. get => _minimumKeyViewSize;
  452. set
  453. {
  454. if (value == _minimumKeyViewSize)
  455. {
  456. //return;
  457. }
  458. _minimumKeyViewSize = value;
  459. SetKeyViewDefaultLayout();
  460. CommandView.SetNeedsLayout();
  461. HelpView.SetNeedsLayout ();
  462. KeyView.SetNeedsLayout ();
  463. SetSubViewNeedsDisplay ();
  464. }
  465. }
  466. private int GetMinimumKeyViewSize () { return MinimumKeyViewSize; }
  467. private void SetKeyViewDefaultLayout ()
  468. {
  469. KeyView.Margin.Thickness = new (1, 0, 1, 0);
  470. KeyView.X = Pos.Align (Alignment.End, AlignmentModes.IgnoreFirstOrLast);
  471. KeyView.Y = 0; //Pos.Center (),
  472. KeyView.Width = Dim.Auto (DimAutoStyle.Text, minimumContentDim: Dim.Func(GetMinimumKeyViewSize));
  473. KeyView.Height = Dim.Auto (DimAutoStyle.Text);
  474. KeyView.Visible = true;
  475. }
  476. private void UpdateKeyBinding ()
  477. {
  478. if (KeyBindingScope == KeyBindingScope.Application)
  479. {
  480. // return;
  481. }
  482. if (Key != null)
  483. {
  484. // CommandView holds our command/keybinding
  485. // Add a key binding for this command to this Shortcut
  486. CommandView.KeyBindings.Remove (Key);
  487. CommandView.KeyBindings.Add (Key, KeyBindingScope, Command.Accept);
  488. }
  489. }
  490. #endregion Key
  491. /// <summary>
  492. /// The event fired when the <see cref="Command.Accept"/> command is received. This
  493. /// occurs if the user clicks on the Shortcut or presses <see cref="Key"/>.
  494. /// </summary>
  495. public new event EventHandler<HandledEventArgs> Accept;
  496. /// <summary>
  497. /// Called when the <see cref="Command.Accept"/> command is received. This
  498. /// occurs if the user clicks on the Bar with the mouse or presses the key bound to
  499. /// Command.Accept (Space by default).
  500. /// </summary>
  501. protected new bool? OnAccept ()
  502. {
  503. // TODO: This is not completely thought through.
  504. if (Key == null || Key == Key.Empty)
  505. {
  506. return false;
  507. }
  508. var handled = false;
  509. var keyCopy = new Key (Key);
  510. //switch (KeyBindingScope)
  511. //{
  512. // case KeyBindingScope.Application:
  513. // // Simulate a key down to invoke the Application scoped key binding
  514. // handled = Application.OnKeyDown (keyCopy);
  515. // break;
  516. // case KeyBindingScope.Focused:
  517. // handled = InvokeCommand (Command.Value) == true;
  518. // handled = false;
  519. // break;
  520. // case KeyBindingScope.HotKey:
  521. // if (Command.HasValue)
  522. // {
  523. // //handled = _commandView.InvokeCommand (Gui.Command.HotKey) == true;
  524. // //handled = false;
  525. // }
  526. // break;
  527. //}
  528. //if (handled == false)
  529. {
  530. var args = new HandledEventArgs ();
  531. Accept?.Invoke (this, args);
  532. handled = args.Handled;
  533. }
  534. return true;
  535. }
  536. /// <inheritdoc/>
  537. public override bool OnEnter (View view)
  538. {
  539. // TODO: This is a hack. Need to refine this.
  540. var cs = new ColorScheme (ColorScheme)
  541. {
  542. Normal = ColorScheme.Focus,
  543. HotNormal = ColorScheme.HotFocus
  544. };
  545. // _container.ColorScheme = cs;
  546. cs = new (ColorScheme)
  547. {
  548. Normal = ColorScheme.HotFocus,
  549. HotNormal = ColorScheme.Focus
  550. };
  551. //KeyView.ColorScheme = cs;
  552. return base.OnEnter (view);
  553. }
  554. /// <inheritdoc/>
  555. public override bool OnLeave (View view)
  556. {
  557. // TODO: This is a hack. Need to refine this.
  558. var cs = new ColorScheme (ColorScheme)
  559. {
  560. Normal = ColorScheme.Normal,
  561. HotNormal = ColorScheme.HotNormal
  562. };
  563. // _container.ColorScheme = cs;
  564. cs = new (ColorScheme)
  565. {
  566. Normal = ColorScheme.HotNormal,
  567. HotNormal = ColorScheme.Normal
  568. };
  569. //KeyView.ColorScheme = cs;
  570. return base.OnLeave (view);
  571. }
  572. }