From a6e01ee9b13ea013237ddbe8f7a8aac464eb9020 Mon Sep 17 00:00:00 2001 From: Chip Black Date: Sat, 27 Sep 2008 03:26:19 -0500 Subject: [PATCH] Initial commit - Working file open dialog w/sexy tab completion. - Can open files via open dialog and command-line. - Text is rendered using pango, but cursor position is still basic (it's especially wrong for CJK text). --- FileInput.cs | 87 ++++++++++++++++++++++ Interfaces.cs | 5 ++ Makefile | 12 +++ TextDisplay.cs | 102 ++++++++++++++++++++++++++ TextDocument.cs | 190 ++++++++++++++++++++++++++++++++++++++++++++++++ TextInput.cs | 134 ++++++++++++++++++++++++++++++++++ main.cs | 87 ++++++++++++++++++++++ 7 files changed, 617 insertions(+) create mode 100644 FileInput.cs create mode 100644 Interfaces.cs create mode 100644 Makefile create mode 100644 TextDisplay.cs create mode 100644 TextDocument.cs create mode 100644 TextInput.cs create mode 100644 main.cs diff --git a/FileInput.cs b/FileInput.cs new file mode 100644 index 0000000..f801109 --- /dev/null +++ b/FileInput.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Collections; +using Cairo; +using Gtk; +using Gdk; + +class FileSelector : TextInput { + private string filestr; + + public FileSelector() : base(Directory.GetCurrentDirectory() + System.IO.Path.DirectorySeparatorChar) { + Complete(); + } + + protected override void draw(Cairo.Context gr, int width, int height) { + base.draw(gr, width, height); + + gr.LineWidth = 1.0; + gr.NewPath(); + gr.MoveTo(0, 13.5); + gr.LineTo(width, 13.5); + gr.Stroke(); + + if (filestr != null) { + Pango.Layout layout = new Pango.Layout(this.PangoContext); + layout.Width = Pango.Units.FromPixels(width); + layout.FontDescription = Pango.FontDescription.FromString("Courier 12"); + layout.Wrap = Pango.WrapMode.WordChar; + layout.SetMarkup(filestr); + gr.MoveTo(0, 15); + Pango.CairoHelper.ShowLayout(gr, layout); + } + } + + [GLib.ConnectBefore()] + public override void KeyPress(object o, KeyPressEventArgs args) { + base.KeyPress(o, args); + switch(args.Event.Key) { + case Gdk.Key.Tab: + Complete(); + QueueDraw(); + break; + } + } + + private void Complete() { + string dir = System.IO.Path.GetDirectoryName(Value); + string search = System.IO.Path.GetFileName(Value); + string[] files = Directory.GetFileSystemEntries(dir, search + "*"); + filestr = null; + if (files.Length == 1 && Directory.Exists(files[0])) { + Value = files[0] + System.IO.Path.DirectorySeparatorChar; + Complete(); + return; + } else if (files.Length > 0) { + int i; + filestr = ""; + + string common = System.IO.Path.GetFileName(files[0]); + + for (i = 0; i < files.Length; i++) { + files[i] = System.IO.Path.GetFileName(files[i]); + common = CommonPart(common, files[i]); + } + + Value = dir + System.IO.Path.DirectorySeparatorChar + common; + cursor = Value.Length; + + for (i = 0; i < files.Length; i++) { + string f = files[i]; + filestr += "" + common + "" + f.Substring(common.Length) + " "; + } + } + } + + private string CommonPart(string s1, string s2) { + CharEnumerator i1 = s1.GetEnumerator(); + CharEnumerator i2 = s2.GetEnumerator(); + int c = 0; + + while (i1.MoveNext() && i2.MoveNext()) { + if (i1.Current != i2.Current) break; + c++; + } + return s1.Substring(0, c); + } +} diff --git a/Interfaces.cs b/Interfaces.cs new file mode 100644 index 0000000..9eeaf09 --- /dev/null +++ b/Interfaces.cs @@ -0,0 +1,5 @@ +using Gtk; + +interface IKeyPress { + void KeyPress(object o, KeyPressEventArgs args); +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9e3e810 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +CSFLAGS = -debug -pkg:gtk-sharp-2.0 -r:Mono.Cairo +SOURCES = main.cs Interfaces.cs TextDisplay.cs TextDocument.cs TextInput.cs \ + FileInput.cs +EXE = Nebula.exe + +all: $(EXE) + +clean: + rm -f $(EXE) + +$(EXE): $(SOURCES) + mcs $(CSFLAGS) -out:$@ $^ diff --git a/TextDisplay.cs b/TextDisplay.cs new file mode 100644 index 0000000..0abb9e9 --- /dev/null +++ b/TextDisplay.cs @@ -0,0 +1,102 @@ +using System; +using Cairo; +using Pango; +using Gtk; + +public class TextDisplay : DrawingArea, IKeyPress { + private TextDocument doc = null; + private FontDescription font; + + public TextDisplay() { + font = Pango.FontDescription.FromString("Courier 12"); + ModifyBg(StateType.Normal, new Gdk.Color(0xFF, 0xFF, 0xFF)); + } + + public void SetDocument(TextDocument doc) { + this.doc = doc; + } + + void draw(Cairo.Context gr, int width, int height) { + if (doc == null) return; + + Pango.Layout layout = new Pango.Layout(this.PangoContext); + layout.Width = Pango.Units.FromPixels(width); + layout.FontDescription = font; + + //gr.Operator = Operator.Atop; + gr.SetSourceRGB(0,0,0); + int h = Pango.Units.ToPixels(layout.FontDescription.Size) + 1; + int y = 0; + int line = 0; + while (line < doc.Lines.Count && y < height) { + layout.SetText((String)doc.Lines[line]); + gr.MoveTo(0, y); + Pango.CairoHelper.ShowLayout(gr, layout); + y += h; + line++; + } + + //gr.Operator = Operator.Xor; + gr.NewPath(); + gr.Rectangle(new Cairo.Rectangle(doc.cursor.x*8, doc.cursor.y*h, 8, h)); + gr.Fill(); + } + + protected override bool OnExposeEvent(Gdk.EventExpose args) + { + Gdk.Window win = args.Window; + + #if OLD_SYSTEMS + // + // For old versions of Gtk# (before 2.8), you need the helper class + // available in gtk-sharp/sample/GtkCairo.cs + // + Cairo.Context g = Gdk.Graphics.CreateDrawable (win); + #else + // + // Starting with Gtk 2.8 Gtk has direct support for + // Cairo, as its built on top of it, on older + // versions, a helper routine is used + // + Cairo.Context g = Gdk.CairoHelper.Create (args.Window); + #endif + + int x, y, w, h, d; + win.GetGeometry(out x, out y, out w, out h, out d); + + draw (g, w, h); + ((IDisposable) g.Target).Dispose(); + ((IDisposable) g).Dispose(); + return true; + } + + public void Update() { + QueueDraw(); + } + + + [GLib.ConnectBefore ()] + public void KeyPress(object o, KeyPressEventArgs args) { + if (doc == null) return; + + if ((args.Event.State & Gdk.ModifierType.ControlMask) != 0) { + } else if ((args.Event.State & Gdk.ModifierType.Mod1Mask) != 0) { + } else { + switch(args.Event.Key) { + case Gdk.Key.Up: + case Gdk.Key.Down: + case Gdk.Key.Left: + case Gdk.Key.Right: + case Gdk.Key.Page_Up: + case Gdk.Key.Page_Down: + case Gdk.Key.Home: + case Gdk.Key.End: + doc.MoveCursor(args.Event.Key); + break; + default: + doc.AddChar((int)args.Event.KeyValue); + break; + } + } + } +} diff --git a/TextDocument.cs b/TextDocument.cs new file mode 100644 index 0000000..e05e992 --- /dev/null +++ b/TextDocument.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.IO; + +public struct Cursor { + private int _x; + private int _y; + + public Cursor(int x, int y) { + _x = x; + _y = y; + } + + public int x { + get { + return _x; + } + set { + if (value < 0) _x = 0; + else _x = value; + } + } + + public int y { + get { + return _y; + } + set { + if (value < 0) _y = 0; + else _y = value; + } + } +} + +public class TextArray { + private ArrayList lines; + + public TextArray() { + lines = new ArrayList(); + lines.Add(""); + } + + public TextArray(string filename) { + lines = new ArrayList(); + StreamReader file; + string line; + + file = new StreamReader(filename); + while ((line = file.ReadLine()) != null) { + lines.Add(line); + } + } + + public ArrayList Lines { + get { + return lines; + } + } + + public void Insert(int x, int y, char c) { + while (lines.Count < y+1) + lines.Add(""); + string s = (string)lines[y]; + + if (x >= s.Length && (c == ' ' || c == '\t')) return; + if (x > s.Length) { + lines[y] = s.PadRight(x) + c; + } else if (x <= s.Length) { + lines[y] = s.Insert(x, new string(c, 1)); + } + } + + public void Delete(int x, int y) { + if (y >= lines.Count) return; + + string s = (string)lines[y]; + if (x >= s.Length) return; + + if (s.Length == 0 && x == 0) { + lines.RemoveAt(y); + } else { + s.Remove(x, 1); + } + } + + public void Backspace(int x, int y) { + if (x == 0 && y == 0) return; + if (y >= lines.Count) return; + + string s = (string)lines[y]; + if (x > s.Length) return; + + if (s.Length == 0 && x == 0) { + lines.RemoveAt(y); + } else if (x == 0 && y > 0) { + lines.RemoveAt(y); + lines[y-1] += s; + } else { + lines[y] = s.Remove(x-1, 1); + } + } + + public void Newline(int x, int y) { + if (y >= lines.Count) return; + string s = (string) lines[y]; + if (x < s.Length) { + lines[y] = s.Substring(0, x); + lines.Insert(y + 1, s.Substring(x)); + } else { + if (y == lines.Count - 1) return; + lines.Insert(y + 1, ""); + } + } + + public int LineLength(int y) { + if (y >= lines.Count) return 0; + return ((string)lines[y]).Length; + } +} + +public class TextDocument { + private TextArray text; + public Cursor cursor; + + public ArrayList Lines { + get { + return text.Lines; + } + } + + public TextDocument() { + text = new TextArray(); + } + + public TextDocument(string filename) { + text = new TextArray(filename); + } + + public void MoveCursor(Gdk.Key k) { + switch(k) { + case Gdk.Key.Up: + cursor.y--; + break; + case Gdk.Key.Down: + cursor.y++; + break; + case Gdk.Key.Left: + cursor.x--; + break; + case Gdk.Key.Right: + cursor.x++; + break; + case Gdk.Key.End: + cursor.x = text.LineLength(cursor.y); + break; + case Gdk.Key.Home: + cursor.x = 0; + break; + } + } + + public void AddChar(int c) { + c = c & 0xFF; + if (c > 128) return; + + switch(c) { + case 8: + int l = 0; + if (cursor.y > 0) l = text.LineLength(cursor.y-1); + text.Backspace(cursor.x, cursor.y); + if (cursor.x == 0) { + cursor.y--; + if (cursor.y >= text.Lines.Count) return; + cursor.x = l; + } else { + cursor.x--; + } + break; + case 13: + text.Newline(cursor.x, cursor.y); + cursor.x = 0; + cursor.y++; + break; + default: + text.Insert(cursor.x, cursor.y, (char)c); + cursor.x++; + break; + } + } +} diff --git a/TextInput.cs b/TextInput.cs new file mode 100644 index 0000000..4934d20 --- /dev/null +++ b/TextInput.cs @@ -0,0 +1,134 @@ +using System; +using Cairo; +using Pango; +using Gtk; +using Gdk; + +public class TextInputEventArgs : EventArgs { + public string Value; + + public TextInputEventArgs(string s) { + Value = s; + } +} + + +public class TextInput : DrawingArea, IKeyPress { + public string Value; + protected int cursor; + private string killring = null; + Pango.FontDescription font; + + public delegate void SelectHandler(object o, TextInputEventArgs s); + public event SelectHandler Selected; + + public TextInput() : this("") { } + + public TextInput(string Value) { + this.Value = Value; + cursor = Value.Length; + font = Pango.FontDescription.FromString("Courier 12"); + SetSizeRequest(-1, 12); + ModifyBg(StateType.Normal, new Gdk.Color(0xFF, 0xFF, 0xFF)); + } + + protected virtual void draw (Cairo.Context gr, int width, int height) { + gr.SetSourceRGB(0,0,0); + gr.NewPath(); + gr.Rectangle(new Cairo.Rectangle(0, 0, width, height)); + gr.Stroke(); + + Pango.Layout layout = new Pango.Layout(this.PangoContext); + layout.Width = Pango.Units.FromPixels(width); + layout.Alignment = Pango.Alignment.Left; + layout.FontDescription = font; + layout.SetText(Value); + Pango.CairoHelper.ShowLayout(gr, layout); + + gr.NewPath(); + int h = Pango.Units.ToPixels(layout.FontDescription.Size) + 1; + gr.Rectangle(new Cairo.Rectangle(cursor*8, 0, 7, h)); + gr.Fill(); + } + + protected override bool OnExposeEvent (Gdk.EventExpose args) { + Gdk.Window win = args.Window; + Cairo.Context g = Gdk.CairoHelper.Create (args.Window); + + int x, y, w, h, d; + win.GetGeometry(out x, out y, out w, out h, out d); + + draw(g, w, h); + + ((IDisposable) g.Target).Dispose(); + ((IDisposable) g).Dispose(); + return true; + } + + [GLib.ConnectBefore ()] + public virtual void KeyPress(object o, KeyPressEventArgs args) { + if ((args.Event.State & ModifierType.ControlMask) != 0) { + switch(args.Event.Key) { + case Gdk.Key.u: + killring = Value.Substring(0, cursor); + Value = Value.Substring(cursor); + cursor = 0; + break; + case Gdk.Key.k: + killring = Value.Substring(cursor); + Value = Value.Substring(0, cursor); + break; + case Gdk.Key.w: + int ws = Value.LastIndexOfAny(" ".ToCharArray()); + if (ws == -1) { + killring = Value; + Value = ""; + } else { + killring = Value.Substring(ws + 1); + Value = Value.Substring(0, ws); + } + cursor = Value.Length; + break; + case Gdk.Key.y: + if (killring == null) return; + Value = Value.Insert(cursor, killring); + cursor += killring.Length; + break; + } + } else { + switch(args.Event.Key) { + case Gdk.Key.BackSpace: + if (cursor == 0) return; + cursor--; + Value = Value.Remove(cursor, 1); + break; + case Gdk.Key.Return: + if (Selected != null) { + TextInputEventArgs t = new TextInputEventArgs(Value); + Selected(this, t); + Selected = null; + } + break; + case Gdk.Key.Left: + if (cursor > 0) cursor--; + break; + case Gdk.Key.Right: + if (cursor < Value.Length) cursor++; + break; + case Gdk.Key.Escape: + if (Selected != null) { + TextInputEventArgs t = new TextInputEventArgs(null); + Selected(this, t); + Selected = null; + } + break; + default: + if (args.Event.KeyValue > 128) return; + Value = Value.Insert(cursor, new String((char)args.Event.KeyValue, 1)); + cursor++; + break; + } + } + QueueDraw(); + } +} diff --git a/main.cs b/main.cs new file mode 100644 index 0000000..bdfdd49 --- /dev/null +++ b/main.cs @@ -0,0 +1,87 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using Cairo; +using Gtk; + +public class Nebula { + static Gtk.Window window; + static VBox stack; + static TextDisplay display; + static TextDocument doc; + static bool fullscreened = false; + + static void Main (string[] args) { + Application.Init (); + window = new Gtk.Window("Nebula"); + stack = new VBox(false, 0); + + if (args.Length > 0) { + doc = new TextDocument(args[0]); + } else { + doc = new TextDocument(); + } + display = new TextDisplay(); + display.SetDocument(doc); + + stack.Add(display); + + window.Add(stack); + window.Resize(640,480); + window.ShowAll(); + + window.KeyPressEvent += new KeyPressEventHandler(KeyPress); + SelectInput(display); + window.DeleteEvent += delegate(object o, DeleteEventArgs e) { + Application.Quit(); + }; + + Application.Run(); + } + + static void SelectInput(IKeyPress w) { + window.KeyPressEvent -= display.KeyPress; + window.KeyPressEvent += w.KeyPress; + } + + [GLib.ConnectBefore ()] + static void KeyPress(object o, KeyPressEventArgs args) { + if ((args.Event.State & Gdk.ModifierType.ControlMask) != 0) { + switch(args.Event.Key) { + case Gdk.Key.n: + doc = new TextDocument(); + display.SetDocument(doc); + break; + case Gdk.Key.o: + TextInput filename = new FileSelector(); + stack.PackEnd(filename); + SelectInput(filename); + filename.Show(); + filename.Selected += delegate(object o2, TextInputEventArgs t) { + if (t.Value != null) { + doc = new TextDocument(t.Value); + display.SetDocument(doc); + } + SelectInput(display); + stack.Remove(filename); + filename.Destroy(); + }; + break; + case Gdk.Key.q: + Application.Quit(); + break; + } + } else if ((args.Event.State & Gdk.ModifierType.Mod1Mask) != 0) { + switch(args.Event.Key) { + case Gdk.Key.Return: + if (fullscreened) + ((Window)o).Unfullscreen(); + else + ((Window)o).Fullscreen(); + fullscreened = ! fullscreened; + break; + } + } + display.Update(); + } +} -- 2.25.1