From 246c480ea3366be4adfd66d28644ceecd4fc26b0 Mon Sep 17 00:00:00 2001 From: Thomas Geymayer Date: Sun, 31 Aug 2014 18:08:00 +0200 Subject: [PATCH] canvas.gui: Add a basic LineEdit for text input. --- Nasal/canvas/gui.nas | 1 + Nasal/canvas/gui/Widget.nas | 4 + Nasal/canvas/gui/styles/DefaultStyle.nas | 92 ++++++++++- Nasal/canvas/gui/widgets/Button.nas | 4 +- Nasal/canvas/gui/widgets/LineEdit.nas | 144 ++++++++++++++++++ Nasal/canvas/gui/widgets/ScrollArea.nas | 4 +- .../widgets/backdrop-entry-disabled.png | Bin 0 -> 527 bytes .../widgets/backdrop-entry.png | Bin 0 -> 675 bytes .../widgets/entry-disabled.png | Bin 0 -> 579 bytes .../AmbianceClassic/widgets/entry-focused.png | Bin 0 -> 977 bytes gui/styles/AmbianceClassic/widgets/entry.png | Bin 0 -> 695 bytes 11 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 Nasal/canvas/gui/widgets/LineEdit.nas create mode 100644 gui/styles/AmbianceClassic/widgets/backdrop-entry-disabled.png create mode 100644 gui/styles/AmbianceClassic/widgets/backdrop-entry.png create mode 100644 gui/styles/AmbianceClassic/widgets/entry-disabled.png create mode 100644 gui/styles/AmbianceClassic/widgets/entry-focused.png create mode 100644 gui/styles/AmbianceClassic/widgets/entry.png 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 0000000000000000000000000000000000000000..35003d7547b7a71782528cbc4e8ce6371e5459cb GIT binary patch literal 527 zcmV+q0`UEbP)P000#T1^@s6vnxdy0005jNkl{;9EUwInkXLe2quaeiH)uL0D`H+2k-$beF1#}!J|=2+jN4Zy+u;wCYoSS5Lbvd z1o2?~ey|JcQRxIeW@q+4-!HqF$uvAYKK#J8?Z&_Io6jq5DwRrOxm<2Ho6R1BKG*OD z_BTG@1)kv6sk?+D=C43RK5vKuS}0hDa|#Y%nd|97p%BevGT~Gz734aQNF%Qt#uX&o!^%ZR5rJfKa7K_DE2A?S&(%^rQ2VWiUyk4O~S{Yd58*Qk-6!P&@d-5x! zjgXUcI=x^rL_Ynt@++jBPLbqUlOgiqP0Fv3fK~>BF~cD-yr=m63hAPA$NL*J86qFv z8~mLL>7nylE|=R(hRCPAguh!My>x8dFpa$ODOh6PSJw!F`sZo6d2Egz2egadF@|XQI Rrq%!e002ovPDHLkV1k~c^F9Co literal 0 HcmV?d00001 diff --git a/gui/styles/AmbianceClassic/widgets/backdrop-entry.png b/gui/styles/AmbianceClassic/widgets/backdrop-entry.png new file mode 100644 index 0000000000000000000000000000000000000000..cac6f3a14b79a5ee5b82347684d3041e36912c07 GIT binary patch literal 675 zcmV;U0$lxxP)P000#T1^@s6vnxdy0007NNkl94t_>nFag0?oH2x@3+T@(~_E82*ITik?fku1{xpg2Sd4ux((MG47}wluU! zle9^bL6=PNV=uNsiqO!eAh*~Mf6rjKW)eNalaq7L`#wMJ$GO*PAMF1@x7&3&aQ`K) zUYt&+*t5F2I=Z;Hm{?d?sEAKuSNs&+9y-DnZLues>f07Y@hqRu2jx43*BNqVX=&+Y zCX?AprBZJaiNuqcnVB2Y)6>_Y(dgB1I6M*xg)aL2exGGo1B}c$&N_9}(?`ETzbWiB zUi<`jN~O|-LfVl?lFQI{iWg zXA%4_c<{;l7C#~9vf1p%`T6;f7CnYSJow~&kDm~qCR=lJbGJEdLm_T_@_xoo$l%J# z%J+CYKFT#V6ym}s?^pbU1T^^(i^YaahTxO;0KaA1_GRsl&hYSXKLw`1C+{(StJP{v z=uxvxJy`(w@Zz_Qj*iB)LyrT2fY)RQKD_w3Cu3`CYx~-<7fgoW!;7Dg@j{`nolGXL zn7r}$@Zz`lDHe-g^xnE@G6Wyq-yvh`>+2u%xJ{Z2!G{+=A;Hbf&6HASO@`pZi=Pl* zqtSS}v9WQe8+D!n_*XgL!G{+=Aue&NR;#@$m&-fil?onf3~C(FI9RP#2eh&Jb)EZ= zF*4^k>(o(CAN_dn;l+RQ(Gqu>&1Se>uRm9FS*N;FsZ@5wp8mHr+M*-6jLbRCI(5|3 zM?W5Xc=7js--pC?aaT-=d*Z&B5)Z^f2h68fr;d91=*Q#q{sL*dK#4xkWPAVs002ov JPDHLkV1kh?P|*MY literal 0 HcmV?d00001 diff --git a/gui/styles/AmbianceClassic/widgets/entry-disabled.png b/gui/styles/AmbianceClassic/widgets/entry-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..6d51dbef7503eee3d375820d402c4a47bb2c26a4 GIT binary patch literal 579 zcmV-J0=)f+P)P000#T1^@s6vnxdy0006CNklFPK$9MR#J2xYVtS(w#p-{R1xCd08lKvzDyX>~ase5d;lg zBoTF?tp-A=%JF&SL}H-bIDva z`uW}!4u@Mj9#4bI<*HPFU3IUz+SLyn8fTBc;2}2UeF;%wDwXP^Ax}`ditj}pdjkAtZ*G&7{c?`S=@0 ziXkIxbSEUwn+%bUUENcBCxfwAY>(tklOgiO0LPn Rki-B0002ovPDHLkV1i%e4~GB% literal 0 HcmV?d00001 diff --git a/gui/styles/AmbianceClassic/widgets/entry-focused.png b/gui/styles/AmbianceClassic/widgets/entry-focused.png new file mode 100644 index 0000000000000000000000000000000000000000..8ae309984d170583a4e1853a58d07feb80f8d993 GIT binary patch literal 977 zcmeAS@N?(olHy`uVBq!ia0vp^l0YoV!3-o@zHZfGU|?Jo;1lBd>Enn0{~7)RK?B48 zh6WHLG!)Er_&?M5YJ>Lw1`zjluIKG-lm6#=LZ$xyf0pL^qQvdRqHdtb+g#5N72ZI` z$3pkdm7XuFoI!#`ZttsufQp_wR2{Yr}w2&uao+Pc3@cpZWU> z(B;2@B0%u6H3~#dE_g7Z=t*-lSPVpbzjy6kUD(UHwRaj~;DQjw&sR^NjOP#EpWOX? zPW}C+*mnolzFa%?PE-7=O>^$nN8W2seX+3p)w&taS5A1lZT`*Lh*zs8-)&EQxq9-m zg>Cn`GM+7HeLSb(#frY?OSXCh#pQVez5|i1}OeuLcyacWk5ENe6hS2h@{JA z&H+YVZ%L3}Faska6B83NGYcyl8yhC3JQseOUla2D<~)`tEj4}scUFx zYwPOi=^GlGm|Iy{S=&1}y12Nwy19D?1cyW=C8wljV<#?OzIyfE{RfX8J$~}!#mkqk-+lP_@#CjYpTB+o@#Dwu-+%u6{rflf zu)sE8a`N=CFPL~@F#Ssh{izT0b4>3xuDk0X z9e9grcWi)M1*?l-1^<)_&)1j)W@P*IFWl{D7I>q%+J=N(LOWu3Iji1Ye% zDV4A7-0z+@N?9|1&#TBgmQ^Ed{ry1kvnRr>?`=LEsI|E2G-cY#TJxOOoSK14I(4P1 zmdW?bRgycKc%5-Ya&gAI6_FVdQ&MBb@0KhRKegFUf literal 0 HcmV?d00001 diff --git a/gui/styles/AmbianceClassic/widgets/entry.png b/gui/styles/AmbianceClassic/widgets/entry.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a9f41e2d5f29867702b23cd642a55c495f1fc1 GIT binary patch literal 695 zcmV;o0!aOdP)P000#T1^@s6vnxdy0007hNkl67)%BNf%_8^6NBU9zH8}M;V!OoOSA`r;mOG zZbYNe1YZ0E94eJcR~6E5yWQs`aflw#CBQChK!XW$VBM^xkA6Jz?PRmrJNOCdi^t=y zLZQ$L6&ykEzu>_)J3E`fPsnjiK2AB1-P zC;Wu;ClZNoKA&%Z^J`Iv9iP1G_zAg^OeTMhjg6h)LRu7J!zb@HexuoJUQegfEvM7j zOMzA3lXoA#vAerFqFZgxytAwVA71>Nu93O9xqUrg4l6_O;l)qL==}Wrme=cbSs8*4 zFMdL9Ei5doPfbmoxAMZ{!;9bGC!f!Mo|%~$v@!%AUNdC0P$*RO=DcZT2tK^#>AJD9 zvXWm~T6$^a!-x+renQUFYPCSISln4$Ty*j;;9qwDc<|xHZ+;2yRVtM?dgXr+@p8F* zS6jWXyu5r$b5|~x>(llfdg*l|VPwv6)~TbOKKk+C!;Am-M@u}Y*Xz%!)oOTkb@ja( zH^ip+AsS*&v_zYcImcP2j(Ym&$Ab?q{)6B5i^44)3a@x19*Za9si65eBXf?kP9631 d(T}Io`wPjEDlzzR7ij