Notepad.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. using System;
  2. using System.IO;
  3. using System.Linq;
  4. using Terminal.Gui;
  5. namespace UICatalog.Scenarios {
  6. [ScenarioMetadata (Name: "Notepad", Description: "Multi-tab text editor uising the TabView control.")]
  7. [ScenarioCategory ("Controls"), ScenarioCategory ("TabView")]
  8. public class Notepad : Scenario {
  9. TabView tabView;
  10. private int numbeOfNewTabs = 1;
  11. private TabView focusedTabView;
  12. private StatusItem lenStatusItem;
  13. // Don't create a Window, just return the top-level view
  14. public override void Init ()
  15. {
  16. Application.Init ();
  17. Application.Top.ColorScheme = Colors.Base;
  18. }
  19. public override void Setup ()
  20. {
  21. var menu = new MenuBar (new MenuBarItem [] {
  22. new MenuBarItem ("_File", new MenuItem [] {
  23. new MenuItem ("_New", "", () => New()),
  24. new MenuItem ("_Open", "", () => Open()),
  25. new MenuItem ("_Save", "", () => Save()),
  26. new MenuItem ("Save _As", "", () => SaveAs()),
  27. new MenuItem ("_Close", "", () => Close()),
  28. new MenuItem ("_Quit", "", () => Quit()),
  29. })
  30. });
  31. Application.Top.Add (menu);
  32. tabView = CreateNewTabView ();
  33. tabView.Style.ShowBorder = true;
  34. tabView.ApplyStyleChanges ();
  35. // Start with only a single view but support splitting to show side by side
  36. var split = new TileView(1) {
  37. X = 0,
  38. Y = 1,
  39. Width = Dim.Fill (),
  40. Height = Dim.Fill (1),
  41. };
  42. split.Tiles.ElementAt(0).ContentView.Add (tabView);
  43. split.BorderStyle = BorderStyle.None;
  44. Application.Top.Add (split);
  45. lenStatusItem = new StatusItem (Key.CharMask, "Len: ", null);
  46. var statusBar = new StatusBar (new StatusItem [] {
  47. new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()),
  48. // These shortcut keys don't seem to work correctly in linux
  49. //new StatusItem(Key.CtrlMask | Key.N, "~^O~ Open", () => Open()),
  50. //new StatusItem(Key.CtrlMask | Key.N, "~^N~ New", () => New()),
  51. new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()),
  52. new StatusItem(Key.CtrlMask | Key.W, "~^W~ Close", () => Close()),
  53. lenStatusItem,
  54. });
  55. focusedTabView = tabView;
  56. tabView.SelectedTabChanged += TabView_SelectedTabChanged;
  57. tabView.Enter += (s, e) => focusedTabView = tabView;
  58. Application.Top.Add (statusBar);
  59. New ();
  60. }
  61. private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e)
  62. {
  63. lenStatusItem.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}";
  64. }
  65. private void TabView_TabClicked (object sender, TabMouseEventArgs e)
  66. {
  67. // we are only interested in right clicks
  68. if(!e.MouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked)) {
  69. return;
  70. }
  71. MenuBarItem items;
  72. if (e.Tab == null) {
  73. items = new MenuBarItem (new MenuItem [] {
  74. new MenuItem ($"Open", "", () => Open()),
  75. });
  76. } else {
  77. var tv = (TabView)sender;
  78. var t = (OpenedFile)e.Tab;
  79. items = new MenuBarItem (new MenuItem [] {
  80. new MenuItem ($"Save", "", () => Save(focusedTabView, e.Tab)),
  81. new MenuItem ($"Close", "", () => Close(tv, e.Tab)),
  82. null,
  83. new MenuItem ($"Split Up", "", () => SplitUp(tv,t)),
  84. new MenuItem ($"Split Down", "", () => SplitDown(tv,t)),
  85. new MenuItem ($"Split Right", "", () => SplitRight(tv,t)),
  86. new MenuItem ($"Split Left", "", () => SplitLeft(tv,t)),
  87. });
  88. }
  89. ((View)sender).ViewToScreen (e.MouseEvent.X, e.MouseEvent.Y, out int screenX, out int screenY,true);
  90. var contextMenu = new ContextMenu (screenX,screenY, items);
  91. contextMenu.Show ();
  92. e.MouseEvent.Handled = true;
  93. }
  94. private void SplitUp (TabView sender, OpenedFile tab)
  95. {
  96. Split(0, Orientation.Horizontal,sender,tab);
  97. }
  98. private void SplitDown (TabView sender, OpenedFile tab)
  99. {
  100. Split(1, Orientation.Horizontal,sender,tab);
  101. }
  102. private void SplitLeft (TabView sender, OpenedFile tab)
  103. {
  104. Split(0, Orientation.Vertical,sender,tab);
  105. }
  106. private void SplitRight (TabView sender, OpenedFile tab)
  107. {
  108. Split(1, Orientation.Vertical,sender,tab);
  109. }
  110. private void Split (int offset, Orientation orientation,TabView sender, OpenedFile tab)
  111. {
  112. var split = (TileView)sender.SuperView.SuperView;
  113. var tileIndex = split.IndexOf(sender);
  114. if(tileIndex == -1)
  115. {
  116. return;
  117. }
  118. if(orientation != split.Orientation)
  119. {
  120. split.TrySplitTile(tileIndex,1,out split);
  121. split.Orientation = orientation;
  122. tileIndex = 0;
  123. }
  124. var newTile = split.InsertTile(tileIndex + offset);
  125. var newTabView = CreateNewTabView ();
  126. tab.CloneTo (newTabView);
  127. newTile.ContentView.Add(newTabView);
  128. newTabView.EnsureFocus();
  129. newTabView.FocusFirst();
  130. newTabView.FocusNext();
  131. }
  132. private TabView CreateNewTabView ()
  133. {
  134. var tv = new TabView () {
  135. X = 0,
  136. Y = 0,
  137. Width = Dim.Fill (),
  138. Height = Dim.Fill (),
  139. };
  140. tv.TabClicked += TabView_TabClicked;
  141. tv.SelectedTabChanged += TabView_SelectedTabChanged;
  142. tv.Enter += (s, e) => focusedTabView = tv;
  143. return tv;
  144. }
  145. private void New ()
  146. {
  147. Open (null, $"new {numbeOfNewTabs++}");
  148. }
  149. private void Close ()
  150. {
  151. Close (focusedTabView, focusedTabView.SelectedTab);
  152. }
  153. private void Close (TabView tv, TabView.Tab tabToClose)
  154. {
  155. var tab = tabToClose as OpenedFile;
  156. if (tab == null) {
  157. return;
  158. }
  159. focusedTabView = tv;
  160. if (tab.UnsavedChanges) {
  161. int result = MessageBox.Query ("Save Changes", $"Save changes to {tab.Text.ToString ().TrimEnd ('*')}", "Yes", "No", "Cancel");
  162. if (result == -1 || result == 2) {
  163. // user cancelled
  164. return;
  165. }
  166. if (result == 0) {
  167. if(tab.File == null) {
  168. SaveAs ();
  169. } else {
  170. tab.Save ();
  171. }
  172. }
  173. }
  174. // close and dispose the tab
  175. tv.RemoveTab (tab);
  176. tab.View.Dispose ();
  177. focusedTabView = tv;
  178. if(tv.Tabs.Count == 0) {
  179. var split = (TileView)tv.SuperView.SuperView;
  180. // if it is the last TabView on screen don't drop it or we will
  181. // be unable to open new docs!
  182. if(split.IsRootTileView() && split.Tiles.Count == 1) {
  183. return;
  184. }
  185. var tileIndex = split.IndexOf (tv);
  186. split.RemoveTile (tileIndex);
  187. if(split.Tiles.Count == 0) {
  188. var parent = split.GetParentTileView ();
  189. if (parent == null) {
  190. return;
  191. }
  192. var idx = parent.IndexOf (split);
  193. if (idx == -1) {
  194. return;
  195. }
  196. parent.RemoveTile (idx);
  197. }
  198. }
  199. }
  200. private void Open ()
  201. {
  202. var open = new OpenDialog ("Open") { AllowsMultipleSelection = true };
  203. Application.Run (open);
  204. if (!open.Canceled) {
  205. foreach (var path in open.FilePaths) {
  206. if (string.IsNullOrEmpty (path) || !File.Exists (path)) {
  207. return;
  208. }
  209. // TODO should open in focused TabView
  210. Open (new FileInfo (path), Path.GetFileName (path));
  211. }
  212. }
  213. }
  214. /// <summary>
  215. /// Creates a new tab with initial text
  216. /// </summary>
  217. /// <param name="fileInfo">File that was read or null if a new blank document</param>
  218. private void Open (FileInfo fileInfo, string tabName)
  219. {
  220. var tab = new OpenedFile (focusedTabView, tabName, fileInfo);
  221. focusedTabView.AddTab (tab, true);
  222. }
  223. public void Save ()
  224. {
  225. Save (focusedTabView, focusedTabView.SelectedTab);
  226. }
  227. public void Save (TabView tabViewToSave, TabView.Tab tabToSave)
  228. {
  229. var tab = tabToSave as OpenedFile;
  230. if (tab == null) {
  231. return;
  232. }
  233. if (tab.File == null) {
  234. SaveAs ();
  235. }
  236. tab.Save ();
  237. tabViewToSave.SetNeedsDisplay ();
  238. }
  239. public bool SaveAs ()
  240. {
  241. var tab = focusedTabView.SelectedTab as OpenedFile;
  242. if (tab == null) {
  243. return false;
  244. }
  245. var fd = new SaveDialog ();
  246. Application.Run (fd);
  247. if (string.IsNullOrWhiteSpace (fd.Path?.ToString ())) {
  248. return false;
  249. }
  250. if(fd.Canceled) {
  251. return false;
  252. }
  253. tab.File = new FileInfo (fd.Path.ToString ());
  254. tab.Text = fd.FileName.ToString ();
  255. tab.Save ();
  256. return true;
  257. }
  258. private class OpenedFile : TabView.Tab {
  259. public FileInfo File { get; set; }
  260. /// <summary>
  261. /// The text of the tab the last time it was saved
  262. /// </summary>
  263. /// <value></value>
  264. public string SavedText { get; set; }
  265. public bool UnsavedChanges => !string.Equals (SavedText, View.Text.ToString ());
  266. public OpenedFile (TabView parent, string name, FileInfo file)
  267. : base (name, CreateTextView(file))
  268. {
  269. File = file;
  270. SavedText = View.Text.ToString ();
  271. RegisterTextViewEvents (parent);
  272. }
  273. private void RegisterTextViewEvents (TabView parent)
  274. {
  275. var textView = (TextView)View;
  276. // when user makes changes rename tab to indicate unsaved
  277. textView.KeyUp += (s, k) => {
  278. // if current text doesn't match saved text
  279. var areDiff = this.UnsavedChanges;
  280. if (areDiff) {
  281. if (!this.Text.ToString ().EndsWith ('*')) {
  282. this.Text = this.Text.ToString () + '*';
  283. parent.SetNeedsDisplay ();
  284. }
  285. } else {
  286. if (Text.ToString ().EndsWith ('*')) {
  287. Text = Text.ToString ().TrimEnd ('*');
  288. parent.SetNeedsDisplay ();
  289. }
  290. }
  291. };
  292. }
  293. private static View CreateTextView (FileInfo file)
  294. {
  295. string initialText = string.Empty;
  296. if(file != null && file.Exists) {
  297. initialText = System.IO.File.ReadAllText (file.FullName);
  298. }
  299. return new TextView () {
  300. X = 0,
  301. Y = 0,
  302. Width = Dim.Fill (),
  303. Height = Dim.Fill (),
  304. Text = initialText,
  305. AllowsTab = false,
  306. };
  307. }
  308. public OpenedFile CloneTo(TabView other)
  309. {
  310. var newTab = new OpenedFile (other, base.Text.ToString(), File);
  311. other.AddTab (newTab, true);
  312. return newTab;
  313. }
  314. internal void Save ()
  315. {
  316. var newText = View.Text.ToString ();
  317. System.IO.File.WriteAllText (File.FullName, newText);
  318. SavedText = newText;
  319. Text = Text.ToString ().TrimEnd ('*');
  320. }
  321. }
  322. private void Quit ()
  323. {
  324. Application.RequestStop ();
  325. }
  326. }
  327. }