Start event handling

This commit is contained in:
Miguel de Icaza
2017-12-18 23:53:23 -05:00
parent dd7e553c9c
commit afb6fbf300
6 changed files with 643 additions and 28 deletions

158
Core.cs
View File

@@ -3,6 +3,8 @@
// Pending:
// - Check for NeedDisplay on the hierarchy and repaint
// - Layout support
// - "Colors" type or "Attributes" type?
// - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors?
//
// Optimziations
// - Add rendering limitation to the exposed area
@@ -17,7 +19,79 @@ namespace Terminal {
public bool HasFocus { get; internal set; }
// Key handling
public virtual void KeyDown (Event.Key kb) { }
/// <summary>
/// This method can be overwritten by view that
/// want to provide accelerator functionality
/// (Alt-key for example).
/// </summary>
/// <remarks>
/// <para>
/// Before keys are sent to the subview on the
/// current view, all the views are
/// processed and the key is passed to the widgets
/// to allow some of them to process the keystroke
/// as a hot-key. </para>
/// <para>
/// For example, if you implement a button that
/// has a hotkey ok "o", you would catch the
/// combination Alt-o here. If the event is
/// caught, you must return true to stop the
/// keystroke from being dispatched to other
/// views.
/// </para>
/// </remarks>
public virtual bool ProcessHotKey (KeyEvent kb)
{
return false;
}
/// <summary>
/// If the view is focused, gives the view a
/// chance to process the keystroke.
/// </summary>
/// <remarks>
/// <para>
/// Views can override this method if they are
/// interested in processing the given keystroke.
/// If they consume the keystroke, they must
/// return true to stop the keystroke from being
/// processed by other widgets or consumed by the
/// widget engine. If they return false, the
/// keystroke will be passed using the ProcessColdKey
/// method to other views to process.
/// </para>
/// </remarks>
public virtual bool ProcessKey (KeyEvent kb)
{
return false;
}
/// <summary>
/// This method can be overwritten by views that
/// want to provide accelerator functionality
/// (Alt-key for example), but without
/// interefering with normal ProcessKey behavior.
/// </summary>
/// <remarks>
/// <para>
/// After keys are sent to the subviews on the
/// current view, all the view are
/// processed and the key is passed to the views
/// to allow some of them to process the keystroke
/// as a cold-key. </para>
/// <para>
/// This functionality is used, for example, by
/// default buttons to act on the enter key.
/// Processing this as a hot-key would prevent
/// non-default buttons from consuming the enter
/// keypress when they have the focus.
/// </para>
/// </remarks>
public virtual bool ProcessColdKey (KeyEvent kb)
{
return false;
}
// Mouse events
public virtual void MouseEvent (Event.Mouse me) { }
@@ -57,6 +131,8 @@ namespace Terminal {
}
}
public View SuperView => container;
public View (Rect frame)
{
this.Frame = frame;
@@ -223,6 +299,12 @@ namespace Terminal {
Move (frame.X, frame.Y);
}
/// <summary>
/// Returns the currently focused view inside this view, or null if nothing is focused.
/// </summary>
/// <value>The focused.</value>
public View Focused => focused;
/// <summary>
/// Displays the specified character in the specified column and row.
/// </summary>
@@ -305,6 +387,9 @@ namespace Terminal {
/// </summary>
public void FocusFirst ()
{
if (subviews == null)
return;
foreach (var view in subviews) {
if (view.CanFocus) {
SetFocus (view);
@@ -318,6 +403,9 @@ namespace Terminal {
/// </summary>
public void FocusLast ()
{
if (subviews == null)
return;
for (int i = subviews.Count; i > 0;) {
i--;
@@ -360,6 +448,7 @@ namespace Terminal {
return true;
}
}
if (focused != null) {
focused.HasFocus = false;
focused = null;
@@ -425,6 +514,48 @@ namespace Terminal {
return new Toplevel (new Rect (0, 0, Driver.Cols, Driver.Rows));
}
public override bool CanFocus {
get => true;
}
public override bool ProcessKey (KeyEvent kb)
{
if (ProcessHotKey (kb))
return true;
// Process the key normally
if (Focused?.ProcessKey (kb) == true)
return true;
if (ProcessColdKey (kb))
return true;
switch (kb.Key) {
case Key.ControlC:
// TODO: stop current execution of this container
break;
case Key.ControlZ:
// TODO: should suspend
// console_csharp_send_sigtstp ();
break;
case Key.Tab:
var old = Focused;
if (!FocusNext ())
FocusNext ();
old?.SetNeedsDisplay ();
Focused?.SetNeedsDisplay ();
break;
case Key.BackTab:
old = Focused;
if (!FocusPrev ())
FocusPrev ();
old?.SetNeedsDisplay ();
Focused?.SetNeedsDisplay ();
break;
}
return false;
}
#if false
public override void Redraw ()
{
@@ -460,7 +591,7 @@ namespace Terminal {
base.Add(contentView);
}
public IEnumerator GetEnumerator ()
public new IEnumerator GetEnumerator ()
{
return contentView.GetEnumerator ();
}
@@ -527,16 +658,10 @@ namespace Terminal {
if (Top != null)
return;
Driver.Init ();
Driver.Init (TerminalResized);
MainLoop = new Mono.Terminal.MainLoop ();
Top = Toplevel.Create ();
focus = Top;
MainLoop.AddWatch (0, Mono.Terminal.MainLoop.Condition.PollIn, x => {
//ProcessChar ();
return true;
});
}
public class RunState : IDisposable {
@@ -561,6 +686,10 @@ namespace Terminal {
}
}
static void KeyEvent (Key key)
{
}
static public RunState Begin (Toplevel toplevel)
{
if (toplevel == null)
@@ -568,10 +697,8 @@ namespace Terminal {
var rs = new RunState (toplevel);
Init ();
Driver.PrepareToRun ();
toplevels.Push (toplevel);
Driver.PrepareToRun (MainLoop, toplevel);
toplevel.LayoutSubviews ();
toplevel.FocusFirst ();
Redraw (toplevel);
@@ -674,5 +801,12 @@ namespace Terminal {
RunLoop (runToken);
End (runToken);
}
static void TerminalResized ()
{
foreach (var t in toplevels) {
t.Frame = new Rect (0, 0, Driver.Cols, Driver.Rows);
}
}
}
}

View File

@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using Mono.Terminal;
using Unix.Terminal;
namespace Terminal {
public enum Color
{
/// <summary>
/// Basic colors that can be used to set the foreground and background colors in console applications. These can only be
/// </summary>
public enum Color {
Black,
Blue,
Green,
@@ -49,13 +53,13 @@ namespace Terminal {
}
public abstract class ConsoleDriver {
public abstract int Cols {get;}
public abstract int Rows {get;}
public abstract void Init ();
public abstract int Cols { get; }
public abstract int Rows { get; }
public abstract void Init (Action terminalResized);
public abstract void Move (int col, int row);
public abstract void AddCh (int ch);
public abstract void AddStr (string str);
public abstract void PrepareToRun ();
public abstract void PrepareToRun (MainLoop mainLoop, Responder target);
public abstract void Refresh ();
public abstract void End ();
public abstract void RedrawTop ();
@@ -78,6 +82,8 @@ namespace Terminal {
}
public class CursesDriver : ConsoleDriver {
Action terminalResized;
public override int Cols => Curses.Cols;
public override int Rows => Curses.Lines;
@@ -115,12 +121,12 @@ namespace Terminal {
{
// TODO; optimize this to determine if the str fits in the clip region, and if so, use Curses.addstr directly
foreach (var c in str)
AddCh ((int) c);
AddCh ((int)c);
}
public override void Refresh() => Curses.refresh ();
public override void End() => Curses.endwin ();
public override void RedrawTop() => window.redrawwin ();
public override void Refresh () => Curses.refresh ();
public override void End () => Curses.endwin ();
public override void RedrawTop () => window.redrawwin ();
public override void SetAttribute (Attribute c) => Curses.attrset (c.value);
public Curses.Window window;
@@ -135,7 +141,7 @@ namespace Terminal {
public override void SetColors (ConsoleColor foreground, ConsoleColor background)
{
int f = (short) foreground;
int f = (short)foreground;
int b = (short)background;
var v = colorPairs [f, b];
if ((v & 0x10000) == 0) {
@@ -152,16 +158,80 @@ namespace Terminal {
Dictionary<int, int> rawPairs = new Dictionary<int, int> ();
public override void SetColors (short foreColorId, short backgroundColorId)
{
int key = (((ushort)foreColorId << 16)) | (ushort) backgroundColorId;
int key = (((ushort)foreColorId << 16)) | (ushort)backgroundColorId;
if (!rawPairs.TryGetValue (key, out var v)) {
v = MakeColor (foreColorId, backgroundColorId);
rawPairs [key] = v;
}
SetAttribute (v);
}
public override void PrepareToRun()
static Key MapCursesKey (int cursesKey)
{
switch (cursesKey) {
case Curses.KeyF1: return Key.F1;
case Curses.KeyF2: return Key.F2;
case Curses.KeyF3: return Key.F3;
case Curses.KeyF4: return Key.F4;
case Curses.KeyF5: return Key.F5;
case Curses.KeyF6: return Key.F6;
case Curses.KeyF7: return Key.F7;
case Curses.KeyF8: return Key.F8;
case Curses.KeyF9: return Key.F9;
case Curses.KeyF10: return Key.F10;
case Curses.KeyUp: return Key.CursorUp;
case Curses.KeyDown: return Key.CursorDown;
case Curses.KeyLeft: return Key.CursorLeft;
case Curses.KeyRight: return Key.CursorRight;
case Curses.KeyHome: return Key.Home;
case Curses.KeyEnd: return Key.End;
case Curses.KeyNPage: return Key.PageDown;
case Curses.KeyPPage: return Key.PageUp;
case Curses.KeyDeleteChar: return Key.DeleteChar;
case Curses.KeyInsertChar: return Key.InsertChar;
case Curses.KeyBackTab: return Key.BackTab;
default: return Key.Unknown;
}
}
void ProcessInput (Responder handler)
{
var code = Curses.getch ();
if ((code == -1) || (code == Curses.KeyResize)) {
if (Curses.CheckWinChange ()) {
terminalResized ();
}
}
if (code == Curses.KeyMouse) {
// TODO
// Curses.MouseEvent ev;
// Curses.getmouse (out ev);
// handler.HandleMouse ();
return;
}
// ESC+letter is Alt-Letter.
if (code == 27) {
Curses.timeout (100);
int k = Curses.getch ();
if (k != Curses.ERR && k != 27) {
var mapped = MapCursesKey (k) | Key.AltMask;
handler.ProcessKey (new KeyEvent (mapped));
}
} else {
handler.ProcessKey (new KeyEvent (MapCursesKey (code)));
}
}
public override void PrepareToRun (MainLoop mainLoop, Responder handler)
{
Curses.timeout (-1);
mainLoop.AddWatch (0, Mono.Terminal.MainLoop.Condition.PollIn, x => {
ProcessInput (handler);
return true;
});
}
public override void DrawFrame (Rect region, bool fill)
@@ -192,7 +262,7 @@ namespace Terminal {
AddCh (Curses.ACS_LRCORNER);
}
public override void Init()
public override void Init(Action terminalResized)
{
if (window != null)
return;
@@ -205,6 +275,7 @@ namespace Terminal {
Curses.raw ();
Curses.noecho ();
Curses.Window.Standard.keypad (true);
this.terminalResized = terminalResized;
Colors.Base = new ColorScheme ();
Colors.Dialog = new ColorScheme ();

View File

@@ -1,9 +1,95 @@
namespace Terminal {
/// <summary>
/// The Key enumeration contains special encoding for some keys, but can also
/// encode all the unicode values that can be passed.
/// </summary>
/// <remarks>
/// <para>
/// If the SpecialMask is set, then the value is that of the special mask,
/// otherwise, the value is the one of the lower bits (as extracted by CharMask)
/// </para>
/// <para>
/// Control keys are the values between 1 and 26 corresponding to Control-A to Control-Z
/// </para>
/// </remarks>
public enum Key : uint {
CharMask = 0xfffff,
SpecialMask = 0xfff00000,
ControlA = 1,
ControlB,
ControlC,
ControlD,
ControlE,
ControlF,
ControlG,
ControlH,
ControlI,
Tab = ControlI,
ControlJ,
ControlK,
ControlL,
ControlM,
ControlN,
ControlO,
ControlP,
ControlQ,
ControlR,
ControlS,
ControlT,
ControlU,
ControlV,
ControlW,
ControlX,
ControlY,
ControlZ,
Esc = 27,
Space = 32,
Delete = 127,
AltMask = 0x80000000,
Backspace = 0x100000,
CursorUp,
CursorDown,
CursorLeft,
CursorRight,
PageUp,
PageDown,
Home,
End,
DeleteChar,
InsertChar,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
BackTab,
Unknown
}
public struct KeyEvent {
public Key Key;
public int KeyValue => (int)KeyValue;
public bool IsAlt => (Key & Key.AltMask) != 0;
public bool IsCtrl => ((uint)Key >= 1) && ((uint)Key <= 26);
public KeyEvent (Key k)
{
Key = k;
}
}
public class Event {
public class Key : Event {
public int Code { get; private set; }
public bool Alt { get; private set; }
public Key (int code)
{
Code = code;

View File

@@ -40,6 +40,7 @@
<Compile Include="Types\Size.cs" />
<Compile Include="demo.cs" />
<Compile Include="Views\Label.cs" />
<Compile Include="Views\TextField.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="mono-curses.dll">

321
Views/TextField.cs Normal file
View File

@@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Terminal {
/// <summary>
/// Text data entry widget
/// </summary>
/// <remarks>
/// The Entry widget provides Emacs-like editing
/// functionality, and mouse support.
/// </remarks>
public class TextField : View {
string text, kill;
int first, point;
bool used;
/// <summary>
/// Changed event, raised when the text has clicked.
/// </summary>
/// <remarks>
/// Client code can hook up to this event, it is
/// raised when the text in the entry changes.
/// </remarks>
public event EventHandler Changed;
/// <summary>
/// Public constructor.
/// </summary>
/// <remarks>
/// </remarks>
public TextField (int x, int y, int w, string s) : base (new Rect (x, y, w, 1))
{
if (s == null)
s = "";
text = s;
point = s.Length;
first = point > w ? point - w : 0;
CanFocus = true;
Color = Colors.Dialog.Focus;
}
/// <summary>
/// Sets or gets the text in the entry.
/// </summary>
/// <remarks>
/// </remarks>
public string Text {
get {
return text;
}
set {
text = value;
if (point > text.Length)
point = text.Length;
first = point > Frame.Width ? point - Frame.Width : 0;
SetNeedsDisplay ();
}
}
/// <summary>
/// Sets the secret property.
/// </summary>
/// <remarks>
/// This makes the text entry suitable for entering passwords.
/// </remarks>
public bool Secret { get; set; }
Attribute color;
/// <summary>
/// Sets the color attribute to use (includes foreground and background).
/// </summary>
/// <value>The color.</value>
public Attribute Color {
get => color;
set {
color = value;
SetNeedsDisplay ();
}
}
/// <summary>
/// The current cursor position.
/// </summary>
public int CursorPosition { get { return point; } }
/// <summary>
/// Sets the cursor position.
/// </summary>
public override void PositionCursor ()
{
Move (point - first, 0);
}
public override void Redraw (Rect region)
{
Driver.SetAttribute (Color);
Move (0, 0);
for (int i = 0; i < Frame.Width; i++) {
int p = first + i;
if (p < text.Length) {
Driver.AddCh (Secret ? '*' : text [p]);
} else
Driver.AddCh (' ');
}
PositionCursor ();
}
void Adjust ()
{
if (point < first)
first = point;
else if (first + point >= Frame.Width)
first = point - (Frame.Width / 3);
Redraw (Bounds);
Driver.Refresh ();
}
void SetText (string new_text)
{
text = new_text;
if (Changed != null)
Changed (this, EventArgs.Empty);
}
public override bool CanFocus {
get => true;
set { base.CanFocus = value; }
}
public override bool ProcessKey (KeyEvent kb)
{
switch (kb.Key) {
case Key.Delete:
case Key.Backspace:
if (point == 0)
return true;
SetText (text.Substring (0, point - 1) + text.Substring (point));
point--;
Adjust ();
break;
// Home, C-A
case Key.Home:
case Key.ControlA:
point = 0;
Adjust ();
break;
case Key.CursorLeft:
case Key.ControlB:
if (point > 0) {
point--;
Adjust ();
}
break;
case Key.ControlD: // Delete
if (point == text.Length)
break;
SetText (text.Substring (0, point) + text.Substring (point + 1));
Adjust ();
break;
case Key.ControlE: // End
point = text.Length;
Adjust ();
break;
case Key.CursorRight:
case Key.ControlF:
if (point == text.Length)
break;
point++;
Adjust ();
break;
case Key.ControlK: // kill-to-end
kill = text.Substring (point);
SetText (text.Substring (0, point));
Adjust ();
break;
case Key.ControlY: // Control-y, yank
if (kill == null)
return true;
if (point == text.Length) {
SetText (text + kill);
point = text.Length;
} else {
SetText (text.Substring (0, point) + kill + text.Substring (point));
point += kill.Length;
}
Adjust ();
break;
case (Key)((int)'b' + Key.AltMask):
int bw = WordBackward (point);
if (bw != -1)
point = bw;
Adjust ();
break;
case (Key)((int)'f' + Key.AltMask):
int fw = WordForward (point);
if (fw != -1)
point = fw;
Adjust ();
break;
default:
// Ignore other control characters.
if (kb.Key < Key.Space || kb.Key > Key.CharMask)
return false;
if (used) {
if (point == text.Length) {
SetText (text + (char)kb.Key);
} else {
SetText (text.Substring (0, point) + (char)kb.Key + text.Substring (point));
}
point++;
} else {
SetText ("" + (char)kb.Key);
first = 0;
point = 1;
}
used = true;
Adjust ();
return true;
}
used = true;
return true;
}
int WordForward (int p)
{
if (p >= text.Length)
return -1;
int i = p;
if (Char.IsPunctuation (text [p]) || Char.IsWhiteSpace (text [p])) {
for (; i < text.Length; i++) {
if (Char.IsLetterOrDigit (text [i]))
break;
}
for (; i < text.Length; i++) {
if (!Char.IsLetterOrDigit (text [i]))
break;
}
} else {
for (; i < text.Length; i++) {
if (!Char.IsLetterOrDigit (text [i]))
break;
}
}
if (i != p)
return i;
return -1;
}
int WordBackward (int p)
{
if (p == 0)
return -1;
int i = p - 1;
if (i == 0)
return 0;
if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text [i])) {
for (; i >= 0; i--) {
if (Char.IsLetterOrDigit (text [i]))
break;
}
for (; i >= 0; i--) {
if (!Char.IsLetterOrDigit (text [i]))
break;
}
} else {
for (; i >= 0; i--) {
if (!Char.IsLetterOrDigit (text [i]))
break;
}
}
i++;
if (i != p)
return i;
return -1;
}
#if false
public override void ProcessMouse (Curses.MouseEvent ev)
{
if ((ev.ButtonState & Curses.Event.Button1Clicked) == 0)
return;
.SetFocus (this);
// We could also set the cursor position.
point = first + (ev.X - x);
if (point > text.Length)
point = text.Length;
if (point < first)
point = 0;
SetNeedsDisplay ();
}
#endif
}
}

View File

@@ -9,7 +9,9 @@ class Demo {
new Label (new Rect (0, 0, 40, 3), "1-Hello world, how are you doing today") { TextAlignment = TextAlignment.Left },
new Label (new Rect (0, 4, 40, 3), "2-Hello world, how are you doing today") { TextAlignment = TextAlignment.Right},
new Label (new Rect (0, 8, 40, 3), "3-Hello world, how are you doing today") { TextAlignment = TextAlignment.Centered },
new Label (new Rect (0, 12, 40, 3), "4-Hello world, how are you doing today") { TextAlignment = TextAlignment.Justified}
new Label (new Rect (0, 12, 40, 3), "4-Hello world, how are you doing today") { TextAlignment = TextAlignment.Justified},
new Label (3, 14, "Login: "),
new TextField (10, 14, 40, "")
};
top.Add (win);
Application.Run ();