From 8277a6d60891fc4ef97d1f3766e716bdb84aa86e Mon Sep 17 00:00:00 2001 From: TheFGFSEagle Date: Fri, 27 Jan 2023 22:59:58 +0100 Subject: [PATCH] LineEdit: selection work Implemented line edit selection highlighting and Shift+arrow keys, mouse dragging, and mouse double-clicking text selection --- .../gui/dialogs/WidgetsFactoryDialog.nas | 22 +-- Nasal/canvas/gui/styles/DefaultStyle.nas | 40 +++- Nasal/canvas/gui/widgets/LineEdit.nas | 181 ++++++++++++++---- Nasal/keyboard.nas | 3 + gui/styles/AmbianceClassic/style.xml | 18 ++ 5 files changed, 213 insertions(+), 51 deletions(-) diff --git a/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas b/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas index 63f09bbeb..a0ef88dc5 100644 --- a/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas +++ b/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas @@ -92,7 +92,7 @@ var WidgetsFactoryDialog = { m.button = gui.widgets.Button.new(m.tabsContent, style, {}) .setText("A button") - .setFixedSize(60, 30) + .setFixedSize(128, 30) .listen("clicked", func { InputDialog.getText("You clicked the button …", "Enter some text:", func (button, text) { MessageBox.information("You clicked the button …", "… and entered '" ~ (text != nil ? text : "nothing") ~ "' !"); @@ -124,7 +124,7 @@ var WidgetsFactoryDialog = { .setCheckable(1) .setChecked(0) .setText("Checkable button") - .setFixedSize(120, 30) + .setFixedSize(128, 30) .listen("toggled", func (e) { m.image.setVisible(int(e.detail.checked)); }); @@ -132,7 +132,7 @@ var WidgetsFactoryDialog = { m.upsize_button = gui.widgets.Button.new(m.tabsContent, style, {}) .setText("Upsize window") - .setFixedSize(130, 30) + .setFixedSize(128, 30) .listen("clicked", func { var s = m.window.getSize(); m.window.setSize(s[0] + 100, s[1] + 100); @@ -141,12 +141,19 @@ var WidgetsFactoryDialog = { m.downsize_button = gui.widgets.Button.new(m.tabsContent, style, {}) .setText("Downsize window") - .setFixedSize(130, 30) + .setFixedSize(128, 30) .listen("clicked", func { var s = m.window.getSize(); m.window.setSize(s[0] - 100, s[1] - 100); }); m.button_box.addItem(m.downsize_button); + m.combo1 = gui.widgets.ComboBox.new(m.tabsContent, style, {}); + m.combo1.addMenuItem("Apples", 0); + m.combo1.addMenuItem("Pears", 1); + m.combo1.addMenuItem("Lemons", 2); + m.combo1.addMenuItem("Oranges", 3); + m.combo1.setFixedSize(128, 30); + m.button_box.addItem(m.combo1); m.switch_box = HBoxLayout.new(); m.button_box.addItem(m.switch_box); @@ -201,13 +208,6 @@ var WidgetsFactoryDialog = { m.numericControlsTab.addItem(m.slider); - m.combo1 = gui.widgets.ComboBox.new(m.tabsContent, style, {}); - m.combo1.addMenuItem("Apples", 0); - m.combo1.addMenuItem("Pears", 1); - m.combo1.addMenuItem("Lemons", 2); - m.combo1.addMenuItem("Oranges", 3); - - m.numericControlsTab.addItem(m.combo1); return m; }, diff --git a/Nasal/canvas/gui/styles/DefaultStyle.nas b/Nasal/canvas/gui/styles/DefaultStyle.nas index 1943c3b35..6f067e601 100644 --- a/Nasal/canvas/gui/styles/DefaultStyle.nas +++ b/Nasal/canvas/gui/styles/DefaultStyle.nas @@ -415,6 +415,15 @@ DefaultStyle.widgets["line-edit"] = { .set("character-size", 14) .set("alignment", "left-baseline") .set("clip-frame", Element.PARENT); + me._selection = me._root.createChild("path", "selection") + .set("clip-frame", Element.PARENT) + .set("fill", "#3333ff"); + me._selected_text = me._root.createChild("text", "selected-text") + .set("font", "LiberationFonts/LiberationSans-Regular.ttf") + .set("character-size", 14) + .set("alignment", "left-baseline") + .set("clip-frame", Element.PARENT) + .set("fill", "#ffffff"); me._cursor = me._root.createChild("path", "cursor") .set("stroke", "#333") @@ -422,6 +431,13 @@ DefaultStyle.widgets["line-edit"] = { .moveTo(me._hpadding, 5) .vert(10); me._hscroll = 0; + me._cursor_blink = 1; + me._cursor_blink_timer = maketimer(0.5, func { + me._cursor_blink = !me._cursor_blink; + me._cursor.setVisible(me._cursor_visible and me._cursor_blink); + }); + me._cursor_blink_timer.simulatedTime = 0; + me._cursor_blink_timer.start(); }, setSize: func(model, w, h) { @@ -430,6 +446,14 @@ DefaultStyle.widgets["line-edit"] = { "clip", "rect(0, " ~ (w - me._hpadding) ~ ", " ~ h ~ ", " ~ me._hpadding ~ ")" ); + me._selected_text.set( + "clip", + "rect(0, " ~ (w - me._hpadding) ~ ", " ~ h ~ ", " ~ me._hpadding ~ ")" + ); + me._selection.set( + "clip", + "rect(0, " ~ (w - me._hpadding) ~ ", " ~ h ~ ", " ~ me._hpadding ~ ")" + ); me._cursor.setDouble("coord[2]", h - 10); return me.update(model); @@ -458,8 +482,17 @@ DefaultStyle.widgets["line-edit"] = { var color_name = backdrop ? "backdrop_fg_color" : "fg_color"; me._text.set("fill", me._style.getColor(color_name)); + me._selected_text.set("fill", me._style.getColor("text_color_selected")); + me._selection.set("fill", me._style.getColor((backdrop ? "backdrop_" : "") ~ "text_color_bg_selected")); - me._cursor.setVisible(model._enabled and model._focused and !backdrop); + me._cursor_visible = model._enabled and model._focused and !backdrop and model._selection_start == model._selection_end; + me._cursor.setVisible(me._cursor_visible and me._cursor_blink); + me._selection.reset() + .moveTo(me._text.getCursorPos(0, model._selection_start)[0], 0) + .vert(16) + .horizTo(me._text.getCursorPos(0, model._selection_end)[0]) + .vert(-16); + me._selected_text.setText(model.selectedText()); var width = model._size[0] - 2 * me._hpadding; var cursor_pos = me._text.getCursorPos(0, model._cursor)[0]; @@ -486,6 +519,10 @@ DefaultStyle.widgets["line-edit"] = { me._cursor .setDouble("coord[0]", text_pos + cursor_pos) .update(); + me._selection.setTranslation(text_pos, model._size[1] / 2 - 8) + .update(); + me._selected_text.setTranslation(text_pos + me._text.getCursorPos(0, model._selection_start)[0], model._size[1] / 2 + 5) + .update(); } }; @@ -1198,6 +1235,7 @@ DefaultStyle.widgets["list-item"] = { var min_width = m + me._label.maxWidth() + m; model.setLayoutMinimumSize([min_width, me._itemHeight]); model.setLayoutSizeHint([min_width, me._itemHeight]); + model.setLayoutMaximumSize([model._MAX_SIZE, me._itemHeight]); return me; }, diff --git a/Nasal/canvas/gui/widgets/LineEdit.nas b/Nasal/canvas/gui/widgets/LineEdit.nas index 3aeb3c401..de286f327 100644 --- a/Nasal/canvas/gui/widgets/LineEdit.nas +++ b/Nasal/canvas/gui/widgets/LineEdit.nas @@ -37,11 +37,11 @@ gui.widgets.LineEdit = { me._text = utf8.substr(text, 0, me._max_length); me._cursor = utf8.size(me._text); - me._selection_start = me._cursor; - me._selection_end = me._cursor; + me.clearSelection(); if( me._view != nil ) me._view.setText(me, me._text); + me._trigger("text-changed"); return me; }, @@ -49,11 +49,12 @@ gui.widgets.LineEdit = { { me._text = ""; me._cursor = 0; - me._selection_start = 0; - me._selection_end = 0; + me.clearSelection(); if( me._view != nil ) me._view.setText(me, ""); + me._trigger("text-changed"); + me._onStateChange(); }, text: func() { @@ -66,10 +67,14 @@ gui.widgets.LineEdit = { { me._max_length = len; - if( utf8.size(me._text) <= len ) + if (utf8.size(me._text) <= len) { return me; + } me._text = utf8.substr(me._text, 0, me._max_length); + if( me._view != nil ) + me._view.setText(me, ""); + me._trigger("text-changed"); me.moveCursor(me._cursor); return me; }, @@ -78,41 +83,71 @@ gui.widgets.LineEdit = { 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(); + return me; + }, + clearSelection: func { + me._selection_start = me._selection_end = 0; + me._onStateChange(); + }, + setSelection: func(start, end) { + me._selection_start = start; + me._selection_end = end; + me._onStateChange(); + }, + getSelection: func { + if (me._selection_start != me._selection_end) { + return [me._selection_start, me._selection_end]; + } else { + return nil; + } + }, + _getNearestCursorPos: func(x) { + var crs = me._getNearestCursor(x); + return me._view._text.getCursorPos(crs[0], crs[1]); + }, + _getNearestCursor: func(x) { + return me._view._text.getNearestCursor([x, 5]); + }, + moveCursorX: func(x) { + me._cursor = me._getNearestCursor(x)[1]; me._onStateChange(); return me; }, home: func() { me.moveCursor(0); + me.clearSelection(); }, end: func() { me.moveCursor(utf8.size(me._text)); + me.clearSelection(); }, # 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); + var after = utf8.substr(me._text, me._cursor); + me._text = utf8.substr(me._text, 0, me._cursor); # 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 ) + var remaining = me._max_length - me._cursor - utf8.size(after); + if (remaining > 0) { me._text ~= utf8.substr(text, 0, remaining); + } + #me.clearSelection(); me._cursor = utf8.size(me._text); - me._selection_start = me._cursor; - me._selection_end = me._cursor; me._text ~= after; - if( me._view != nil ) + if (me._view != nil) { me._view.setText(me, me._text); + } + me._trigger("text-changed"); + me._onStateChange(); return me; }, copy: func() { @@ -127,36 +162,40 @@ gui.widgets.LineEdit = { me.insert(clipboard.getText(mode != nil ? mode : clipboard.CLIPBOARD)); }, selectAll: func() { - me._selection_start = 0; - me._selection_end = utf8.size(me._text) - 1; + me.setSelection(0, utf8.size(me._text)); }, # Remove selected text removeSelection: func() { - if( me._selection_start == me._selection_end ) + if (me._selection_start == me._selection_end) { + me._selection_start = me._selection_end = 0; return me; + } 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; + me.clearSelection(); - if( me._view != nil ) + if (me._view != nil) { me._view.setText(me, me._text); + } + me._trigger("text-changed"); - return me; + me._onStateChange(); + return me }, # 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 ) + if (me._selection_start == me._selection_end) { + if (me._cursor == 0) { # Before first character... return me; - - me._selection_start -= 1; + } + me._selection_start = me._cursor - 1; + me._selection_end = me._cursor; } me.removeSelection(); @@ -165,13 +204,14 @@ gui.widgets.LineEdit = { # Remove selection or if nothing is selected the character after the cursor delete: func() { - if( me._selection_start == me._selection_end ) - { - if( me._selection_end == utf8.size(me._text) ) + if (me._selection_start == me._selection_end) { + if (me._cursor == utf8.size(me._text)) { # After last character... return me; + } - me._selection_end += 1; + me._selection_start = me._cursor; + me._selection_end = me._cursor + 1; } me.removeSelection(); @@ -185,6 +225,7 @@ gui.widgets.LineEdit = { var el = view._root; el.addEventListener("keypress", func (e) { if (!e.ctrlKey and !e.altKey and !e.metaKey) { + me.removeSelection(); me.insert(e.key); } }); @@ -193,28 +234,90 @@ gui.widgets.LineEdit = { if( me._view == nil ) return; - if( e.key == "Enter" ) + if (e.key == "Enter") { me._trigger("editingFinished", {text: me.text()}); # TODO validator/etc. - else if( e.key == "Backspace" ) + } elsif (e.key == "Backspace") { me.backspace(); - else if( e.key == "Delete" ) + } elsif (e.key == "Delete") { me.delete(); - 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" ) + } elsif (e.key == "Left") { + if (e.shiftKey) { + if (me._selection_start == 0 and me._selection_end == 0) { + var start = me._cursor; + var end = me._cursor; + } else { + var start = me._selection_start; + var end = me._selection_end; + } + if (start > 0) { + me.setSelection(start - 1, end); + } + } else { + if (me._selection_start != 0 or me._selection_end != 0) { + me.moveCursor(me._selection_start); + me.clearSelection(); + } else { + me.moveCursor(me._cursor - 1); + } + } + } elsif (e.key == "Right") { + if (e.shiftKey) { + if (me._selection_start == 0 and me._selection_end == 0) { + var start = me._cursor; + var end = me._cursor; + } else { + var start = me._selection_start; + var end = me._selection_end; + } + if (end + 1< utf8.size(me._text)) { + me.setSelection(start, end + 1); + } + } else { + if (me._selection_end != 0 or me._selection_start != 0) { + me.moveCursor(me._selection_end); + me.clearSelection(); + } else { + me.moveCursor(me._cursor + 1); + } + } + } elsif (e.key == "Home") { me.home(); - else if( e.key == "End" ) + } elsif (e.key == "End") { me.end(); + } }); el.addEventListener("click", func(e) { if (e.button == 2) { me.showContextMenu(e); + } elsif (e.button == 0) { + me.clearSelection(); + me.moveCursorX(e.localX - view._text.getTranslation()[0]); + } + }); + el.addEventListener("dblclick", func(e) { + me.selectAll(); + }); + el.addEventListener("drag", func(e) { + var pos = me._getNearestCursor(e.localX - view._text.getTranslation()[0])[1]; + if (me._selection_start < pos and me._selection_end > pos) { # dragging within existing selection + # TODO: implement full drag / drop support + } elsif (me._selection_start != me._selection_end) { # existing selection, but dragging outside + if (e.deltaX < 0) { + me.setSelection(pos, me._selection_end); + } elsif (e.deltaX > 0) { + me.setSelection(me._selection_start, pos); + } + } else { # no existing selection, create one from drag position + if (math.abs(e.deltaX) > 1) { + var start = pos; + var end = pos + math.sgn(e.deltaX); + me.setSelection(math.min(start, end), math.max(start, end)); + } } }); }, del: func() { me.context_menu.del(); + me._view._cursor_blink_timer.stop(); } }; diff --git a/Nasal/keyboard.nas b/Nasal/keyboard.nas index ce06812d4..3c606cdf2 100644 --- a/Nasal/keyboard.nas +++ b/Nasal/keyboard.nas @@ -353,6 +353,9 @@ var Shortcut = { }, match: func(keys, shift=0, ctrl=0, alt=0, meta=0) { + if (typeof(keys) != "vector") { + keys = [keys]; + } if (!me.modifiers and !me.keys) { return 0; } diff --git a/gui/styles/AmbianceClassic/style.xml b/gui/styles/AmbianceClassic/style.xml index 887ab9dca..571a840d3 100644 --- a/gui/styles/AmbianceClassic/style.xml +++ b/gui/styles/AmbianceClassic/style.xml @@ -66,6 +66,24 @@ 0.235 + + 1 + 1 + 1 + + + + 0.3 + 0.3 + 1 + + + + 0.5 + 0.5 + 0.5 + + 0.428 0.427