diff --git a/Nasal/canvas/gui.nas b/Nasal/canvas/gui.nas index a01af5e76..98869c87d 100644 --- a/Nasal/canvas/gui.nas +++ b/Nasal/canvas/gui.nas @@ -23,6 +23,7 @@ loadGUIFile("styles/DefaultStyle.nas"); loadWidget("Button"); loadWidget("CheckBox"); loadWidget("Label"); +loadWidget("LineEdit"); loadWidget("ScrollArea"); loadDialog("MessageBox"); diff --git a/Nasal/canvas/gui/Widget.nas b/Nasal/canvas/gui/Widget.nas index 95955bcd2..bcbd1c492 100644 --- a/Nasal/canvas/gui/Widget.nas +++ b/Nasal/canvas/gui/Widget.nas @@ -85,6 +85,9 @@ gui.Widget = { me._focused = 1; canvas._focused_widget = me; + if( me._view != nil ) + me._view._root.setFocus(); + me._trigger("focus-in"); me._onStateChange(); @@ -98,6 +101,7 @@ gui.Widget = { me._focused = 0; me.getCanvas()._focused_widget = nil; + me.getCanvas().clearFocusElement(); me._trigger("focus-out"); me._onStateChange(); diff --git a/Nasal/canvas/gui/styles/DefaultStyle.nas b/Nasal/canvas/gui/styles/DefaultStyle.nas index cc8008e4a..4d3de27fa 100644 --- a/Nasal/canvas/gui/styles/DefaultStyle.nas +++ b/Nasal/canvas/gui/styles/DefaultStyle.nas @@ -107,7 +107,7 @@ DefaultStyle.widgets.button = { } }; -# A checbox +# A checkbox DefaultStyle.widgets.checkbox = { new: func(parent, cfg) { @@ -293,6 +293,96 @@ DefaultStyle.widgets.label = { } }; +# A one line text input field +DefaultStyle.widgets["line-edit"] = { + new: func(parent, cfg) + { + me._hpadding = cfg.get("hpadding", 8); + + me._root = parent.createChild("group", "line-edit"); + me._border = + me._root.createChild("image", "border") + .set("slice", "10 12"); #"7") + me._text = + me._root.createChild("text", "input") + .set("font", "LiberationFonts/LiberationSans-Regular.ttf") + .set("character-size", 14) + .set("alignment", "left-baseline") + .set("clip-frame", Element.PARENT); + me._cursor = + me._root.createChild("path", "cursor") + .set("stroke", "#333") + .set("stroke-width", 1) + .moveTo(me._hpadding, 5) + .vert(10); + me._hscroll = 0; + }, + setSize: func(model, w, h) + { + me._border.setSize(w, h); + me._text.set( + "clip", + "rect(0, " ~ (w - me._hpadding) ~ ", " ~ h ~ ", " ~ me._hpadding ~ ")" + ); + me._cursor.setDouble("coord[2]", h - 10); + + return me.update(model); + }, + setText: func(model, text) + { + me._text.set("text", text); + model._onStateChange(); + }, + update: func(model) + { + var backdrop = !model._windowFocus(); + var file = me._style._dir_widgets ~ "/"; + + if( backdrop ) + file ~= "backdrop-"; + + file ~= "entry"; + + if( !model._enabled ) + file ~= "-disabled"; + else if( model._focused and !backdrop ) + file ~= "-focused"; + + me._border.set("src", file ~ ".png"); + + var color_name = backdrop ? "backdrop_fg_color" : "fg_color"; + me._text.set("fill", me._style.getColor(color_name)); + + me._cursor.setVisible(model._enabled and model._focused and !backdrop); + + var width = model._size[0] - 2 * me._hpadding; + var cursor_pos = me._text.getCursorPos(0, model._cursor)[0]; + var text_width = me._text.getCursorPos(0, me._text.lineLength(0))[0]; + + if( text_width <= width ) + # fit -> align left (TODO handle different alignment) + me._hscroll = 0; + else if( me._hscroll + cursor_pos > width ) + # does not fit, cursor to the right + me._hscroll = width - cursor_pos; + else if( me._hscroll + cursor_pos < 0 ) + # does not fit, cursor to the left + me._hscroll = -cursor_pos; + else if( me._hscroll + text_width < width ) + # does not fit, limit scroll to align with right side + me._hscroll = width - text_width; + + var text_pos = me._hscroll + me._hpadding; + + me._text + .setTranslation(text_pos, model._size[1] / 2 + 5) + .update(); + me._cursor + .setDouble("coord[0]", text_pos + cursor_pos) + .update(); + } +}; + # ScrollArea DefaultStyle.widgets["scroll-area"] = { new: func(parent, cfg) diff --git a/Nasal/canvas/gui/widgets/Button.nas b/Nasal/canvas/gui/widgets/Button.nas index 03ba3a5e2..e18532309 100644 --- a/Nasal/canvas/gui/widgets/Button.nas +++ b/Nasal/canvas/gui/widgets/Button.nas @@ -57,6 +57,8 @@ gui.widgets.Button = { # protected: _setView: func(view) { + call(gui.Widget._setView, [view], me); + var el = view._root; el.addEventListener("mousedown", func if( me._enabled ) me.setDown(1)); el.addEventListener("mouseup", func if( me._enabled ) me.setDown(0)); @@ -64,7 +66,5 @@ gui.widgets.Button = { el.addEventListener("mouseleave",func me.setDown(0)); el.addEventListener("drag", func(e) e.stopPropagation()); - - call(gui.Widget._setView, [view], me); } }; diff --git a/Nasal/canvas/gui/widgets/LineEdit.nas b/Nasal/canvas/gui/widgets/LineEdit.nas new file mode 100644 index 000000000..812966661 --- /dev/null +++ b/Nasal/canvas/gui/widgets/LineEdit.nas @@ -0,0 +1,144 @@ +gui.widgets.LineEdit = { + new: func(parent, style, cfg) + { + var m = gui.Widget.new(gui.widgets.LineEdit); + m._cfg = Config.new(cfg); + m._focus_policy = m.StrongFocus; + m._setView( style.createWidget(parent, "line-edit", m._cfg) ); + + m.setLayoutMinimumSize([28, 16]); + m.setLayoutSizeHint([150, 28]); + + m._text = ""; + m._max_length = 32767; + m._cursor = 0; + m._selection_start = 0; + m._selection_end = 0; + + return m; + }, + setMaxLength: func(len) + { + me._max_length = len; + + if( utf8.size(me._text) <= len ) + return; + + me._text = utf8.substr(me._text, 0, me._max_length); + me.moveCursor(me._cursor); + }, + moveCursor: func(pos, mark = 0) + { + var len = utf8.size(me._text); + me._cursor = math.max(0, math.min(pos, len)); + + me._selection_start = me._cursor; + me._selection_end = me._cursor; + + me._onStateChange(); + }, + home: func() + { + me.moveCursor(0); + }, + end: func() + { + me.moveCursor(utf8.size(me._text)); + }, + # Insert given text after cursor (and first remove selection if set) + insert: func(text) + { + var after = utf8.substr(me._text, me._selection_end); + me._text = utf8.substr(me._text, 0, me._selection_start); + + # Replace selected text, insert new text and place cursor after inserted + # text + var remaining = me._max_length - me._selection_start - utf8.size(after); + if( remaining != 0 ) + me._text ~= utf8.substr(text, 0, remaining); + + me._cursor = utf8.size(me._text); + me._selection_start = me._cursor; + me._selection_end = me._cursor; + + me._text ~= after; + + if( me._view != nil ) + me._view.setText(me, me._text); + }, + paste: func(mode = nil) + { + me.insert(clipboard.getText(mode != nil ? mode : clipboard.CLIPBOARD)); + }, + # Remove selected text + removeSelection: func() + { + if( me._selection_start == me._selection_end ) + return; + + me._text = utf8.substr(me._text, 0, me._selection_start) + ~ utf8.substr(me._text, me._selection_end); + + me._cursor = me._selection_start; + me._selection_end = me._selection_start; + + if( me._view != nil ) + me._view.setText(me, me._text); + }, + # Remove selection or if nothing is selected the character before the cursor + backspace: func() + { + if( me._selection_start == me._selection_end ) + { + if( me._selection_start == 0 ) + # Before first character... + return; + + me._selection_start -= 1; + } + + me.removeSelection(); + }, + # Remove selection or if nothing is selected the character after the cursor + del: func() + { + if( me._selection_start == me._selection_end ) + { + if( me._selection_end == utf8.size(me._text) ) + # After last character... + return; + + me._selection_end += 1; + } + + me.removeSelection(); + }, +# protected: + _setView: func(view) + { + call(gui.Widget._setView, [view], me); + + var el = view._root; + el.addEventListener("keypress", func (e) me.insert(e.key)); + el.addEventListener("keydown", func (e) + { + if( me._view == nil ) + return; + + if( e.key == "Backspace" ) + me.backspace(); + else if( e.key == "Delete" ) + me.del(); + else if( e.key == "Left" ) + me.moveCursor(me._cursor - 1); + else if( e.key == "Right") + me.moveCursor(me._cursor + 1); + else if( e.key == "Home" ) + me.home(); + else if( e.key == "End" ) + me.end(); + else if( e.keyCode == `v` and e.ctrlKey ) + me.paste(); + }); + } +}; diff --git a/Nasal/canvas/gui/widgets/ScrollArea.nas b/Nasal/canvas/gui/widgets/ScrollArea.nas index e1ba7ebe1..810e84953 100644 --- a/Nasal/canvas/gui/widgets/ScrollArea.nas +++ b/Nasal/canvas/gui/widgets/ScrollArea.nas @@ -119,6 +119,8 @@ gui.widgets.ScrollArea = { # protected: _setView: func(view) { + call(gui.Widget._setView, [view], me); + view.vert.addEventListener("mousedown", func(e) me._dragStart(e)); view.horiz.addEventListener("mousedown", func(e) me._dragStart(e)); view._root.addEventListener("mousedown", func(e) @@ -177,8 +179,6 @@ gui.widgets.ScrollArea = { e.stopPropagation(); } ); - - call(gui.Widget._setView, [view], me); }, _dragStart: func(e) { diff --git a/gui/styles/AmbianceClassic/widgets/backdrop-entry-disabled.png b/gui/styles/AmbianceClassic/widgets/backdrop-entry-disabled.png new file mode 100644 index 000000000..35003d754 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/backdrop-entry-disabled.png differ diff --git a/gui/styles/AmbianceClassic/widgets/backdrop-entry.png b/gui/styles/AmbianceClassic/widgets/backdrop-entry.png new file mode 100644 index 000000000..cac6f3a14 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/backdrop-entry.png differ diff --git a/gui/styles/AmbianceClassic/widgets/entry-disabled.png b/gui/styles/AmbianceClassic/widgets/entry-disabled.png new file mode 100644 index 000000000..6d51dbef7 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/entry-disabled.png differ diff --git a/gui/styles/AmbianceClassic/widgets/entry-focused.png b/gui/styles/AmbianceClassic/widgets/entry-focused.png new file mode 100644 index 000000000..8ae309984 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/entry-focused.png differ diff --git a/gui/styles/AmbianceClassic/widgets/entry.png b/gui/styles/AmbianceClassic/widgets/entry.png new file mode 100644 index 000000000..c8a9f41e2 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/entry.png differ