commit:a6e01ee9b13ea013237ddbe8f7a8aac464eb9020
author:Chip Black
committer:Chip Black
date:Sat Sep 27 03:26:19 2008 -0500
parents:
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).
diff --git a/FileInput.cs b/FileInput.cs
line changes: +87/-0
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 += "<span color=\"#7F7F7F\">" + common + "</span>" + 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
line changes: +5/-0
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
line changes: +12/-0
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
line changes: +102/-0
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
line changes: +190/-0
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
line changes: +134/-0
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
line changes: +87/-0
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();
+	}
+}