Commit 81661997 authored by Gabriela Bittencourt's avatar Gabriela Bittencourt Committed by Albert Gräf
Browse files

Add the autocomplete dropdown feature

This adds a dropdown menu with completions from the completion index
when the user creates a new object, message or comment, and starts
typing. Note that this index is initially populated with objects from
the search index of the help browser. As the user types object names,
arguments, messages and comments, they will be added to the completion
index as well.

Entries from the menu can be chosen with the cursor up and down
keys. The enter key can then be used to select an entry and insert the
corresponding completion. Alternatively, clicking with the mouse also
selects an entry.

NOTES / TODO:

This is a very first implementation of autocompletion for purr-data, and
as such it still has a few minor quirks and shortcomings:

- Help index completions: Completions from the help browser's index will
  only be available once that index has been built. By default, this
  happens when the help browser is first launched. However, you can
  change this so that the help index is automatically created when
  purr-data launches, by ticking the corresponding checkbox in the GUI
  preferences. This will make sure that completions from the help index
  are always available, even before launching the help browser for the
  first time.

- Prefix matches: As shipped, completions will encompass all matches of
  the typed text, even within an index item. There is an option to
  select prefix matches only in the code of the search_obj() function in
  pdgui.js. Currently changing the code is the only way to get this
  behavior; there should probably be a checkbox in the GUI preferences
  to make this easier.

- Tab completion: There is no binding for the Tab key in order to select
  a default completion yet, so currently you have to use the cursor keys
  and enter or the mouse to select a completion. This will hopefully be
  added in the future.

- Stale completions: At present, there is no way to remove "stale"
  completion entries, such as obsolete entries from the help index, or
  manually typed completions which haven't been used for a long
  time. There should probably be a way to do this in a (semi-)automatic
  fashion, but at present the completion engine offers no support for
  this.
parent e9f794ff
Pipeline #3732 canceled with stage
......@@ -779,3 +779,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -947,3 +947,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -761,3 +761,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -903,3 +903,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -813,3 +813,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -783,3 +783,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -784,3 +784,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -775,3 +775,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -768,3 +768,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -761,3 +761,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -773,3 +773,19 @@ input[type="color"] {
position: relative;
bottom: 2px;
}
/* GB: Autocomplete Dropdown style */
#autocomplete_dropdown {
background-color: #ebebec;
border: 1px solid;
padding: 1px;
}
#autocomplete_dropdown p {
padding: 2px;
margin: 1px;
}
#autocomplete_dropdown p.selected {
background-color: #cfcfd0;
}
\ No newline at end of file
......@@ -484,11 +484,19 @@ var canvas_events = (function() {
evt.preventDefault();
},
text_mousemove: function(evt) {
if (evt.target.parentNode === document.getElementById("autocomplete_dropdown")) {
let sel = document.getElementById("autocomplete_dropdown").getAttribute("selected_item");
let new_sel = evt.target.getAttribute("idx");
pdgui.update_autocomplete_selected(document.getElementById("autocomplete_dropdown"), sel, new_sel)
}
evt.stopPropagation();
//evt.preventDefault();
return false;
},
text_mousedown: function(evt) {
if (evt.target.parentNode === document.getElementById("autocomplete_dropdown")) {
pdgui.select_result_autocomplete_dd(textbox(), document.getElementById("autocomplete_dropdown"));
}
if (textbox() !== evt.target && !target_is_scrollbar(evt)) {
utils.create_obj();
// send a mousedown and mouseup event to Pd to instantiate
......@@ -518,10 +526,49 @@ var canvas_events = (function() {
},
text_keyup: function(evt) {
evt.stopPropagation();
if (evt.keyCode === 13) {
grow_svg_for_element(textbox());
// GB: Autocomplete feature
let ac_dropdown = function() {
return document.getElementById("autocomplete_dropdown")
}
pdgui.autocomplete(document.getElementById("new_object_textentry").className, textbox().innerText);
switch (evt.keyCode) {
case 40: // arrowdown
pdgui.update_autocomplete_dd_arrowdown(ac_dropdown())
break;
case 38: // arrowup
pdgui.update_autocomplete_dd_arrowup(ac_dropdown())
break;
case 13: // enter
// if there is no item selected on autocomplete dropdown, enter make the obj box bigger
if(ac_dropdown() === null || ac_dropdown().getAttribute("selected_item") === "-1") {
grow_svg_for_element(textbox());
} else { // else, if there is a selected item on autocompletion tool, the selected item is written on the box
pdgui.select_result_autocomplete_dd(textbox(), ac_dropdown());
// TODO: Substitute the editing box by the object itself
// utils.create_obj(); // not working, it's not that simple.
// canvas_events.normal();
}
break;
case 9: // tab
// TODO: Substitute this function by one that autocomplete with the prefix in common in all results
pdgui.select_result_autocomplete_dd(textbox(), ac_dropdown());
break;
default:
if (textbox().innerText === "") {
pdgui.delete_autocomplete_dd(ac_dropdown());
} else {
pdgui.create_autocomplete_dd(document, ac_dropdown(), textbox());
if (ac_dropdown().getAttribute("searched_text") !== textbox().innerText) {
// finding the class from obj: find obj throwout tag of textbox, get obj class and remove from the class the word "selected".
// this has to be done because in textbox obj and comment have class: 'obj'
// and it's import here to differentiate them
let obj_class = document.getElementById(textbox().getAttribute("tag")+"gobj")
.getAttribute("class").toString().split(" ").slice(0,1).toString();
pdgui.repopulate_autocomplete_dd(document, ac_dropdown,obj_class,textbox().innerText);
}
}
}
//evt.preventDefault();
return false;
},
......
......@@ -575,53 +575,158 @@ function index_obj_completion(obj_or_msg, obj_or_msg_text) {
}
function write_completion_index() {
try {
try { // be sure the dir exists
fs.mkdirSync(expand_tilde(path.dirname(compl_name)));
} catch (err) {
// post("err: " + err);
}
try {
try { // create the file
fs.writeFileSync(expand_tilde(compl_name), JSON.stringify(completion_index._docs), {mode: 0o644});
} catch (err) {
post("err: " + err);
}
}
function autocomplete(obj_class, text) {
if (text.length !== 0) {
let title, arg;
if (obj_class === "obj") {
let text_array = text.split(" ");
title = text_array[0].toString();
arg = text_array.slice(1, text_array.length);
arg = (arg.length !== 0) ? arg.toString().replace(/\,/g, " ") : "";
} else {
title = "msg";
arg = text;
}
let n = 10;
post(" -------- First " + n + " results -------- ");
let results = (arg.length > 0) ? (search_arg(title, arg).slice(1,)) : (search_obj(title));
if (results.length > n) results = results.slice(0,n);
if (results.length > 0) {
results.forEach(function (f,i,a) {
let suggestion;
if (arg.length < 1) { // autocomplete title
suggestion = f.item.title;
} else { // autocomplete argument
suggestion = ((obj_class==="obj")?(title+" "):"") + f.value;
}
post("- " + suggestion);
})
} else {
post("No suggestions found!");
// GB: manage the selection of the autocomplete dropdown options
function update_autocomplete_selected(ac_dropdown, sel, new_sel) {
if (sel > -1) ac_dropdown.children.item(sel).classList.remove("selected");
if (new_sel > -1 && new_sel < ac_dropdown.children.length) {
ac_dropdown.children.item(new_sel).classList.add("selected");
} else {
new_sel = -1;
}
ac_dropdown.setAttribute("selected_item", new_sel);
}
function update_autocomplete_dd_arrowdown(ac_dropdown) {
if (ac_dropdown !== null) {
let sel = ac_dropdown.getAttribute("selected_item");
update_autocomplete_selected(ac_dropdown, sel, parseInt(sel) + 1);
}
}
function update_autocomplete_dd_arrowup(ac_dropdown) {
if (ac_dropdown !== null) {
let sel = ac_dropdown.getAttribute("selected_item");
update_autocomplete_selected(ac_dropdown, sel, parseInt(sel) - 1);
}
}
// GB TODO: In messages, when the chosen autocomplete is bigger than the message box, it doesn't resize it
// (so the text is written partially outside the message box in gui, what looks strange to the user)
function select_result_autocomplete_dd(textbox, ac_dropdown) {
if (ac_dropdown !== null) {
let sel = ac_dropdown.getAttribute("selected_item");
if (sel > -1) {
textbox.innerText = ac_dropdown.children.item(sel).innerText;
delete_autocomplete_dd(ac_dropdown);
} else { // it only passes here when the user presses 'tab' and there is no option selected
textbox.innerText = ac_dropdown.children.item(0).innerText;
}
}
}
// GB: update autocomplete dropdown with new results
function repopulate_autocomplete_dd(doc, ac_dropdown, obj_class, text) {
ac_dropdown().setAttribute("searched_text", text);
let title, arg;
if (obj_class === "obj") {
let text_array = text.split(" ");
title = text_array[0].toString();
arg = text_array.slice(1, text_array.length);
arg = (arg.length !== 0) ? arg.toString().replace(/\,/g, " ") : "";
} else if (obj_class === "msg"){
title = "message";
arg = text;
} else if (obj_class === "comment") {
title = "text";
arg = text;
} else { // The code should never enter this 'else', but it's covered just in case there is a situation not covered above
return;
}
// If there are arg, we are autocompleting the arg field, and if there isn't we are autocompleting the obj_title field
let results = (arg.length > 0) ? (search_arg(title, arg).slice(1,)) : (search_obj(title));
// GB TODO: ideally we should be able to show all the results in a limited window with a scroll bar
let n = 8; // Maximum number of suggestions
if (results.length > n) results = results.slice(0,n);
ac_dropdown().innerHTML = ""; // clear all old results
if (results.length > 0) {
// for each result, make a paragraph child of autocomplete_dropdown
results.forEach(function (f,i,a) {
let h = ac_dropdown().getAttribute("font_height");
let y = h*(i+1);
let r = doc.createElement("p");
r.setAttribute("width", "150");
r.setAttribute("height", h);
r.setAttribute("y", y);
r.setAttribute("class", "border");
r.setAttribute("idx", i);
if (arg.length < 1) { // autocomplete object
r.textContent = f.item.title;
} else { // autocomplete argument, message or comment
r.textContent = ((obj_class==="obj")?(title+" "):"") + f.value;
}
ac_dropdown().appendChild(r);
})
ac_dropdown().setAttribute("selected_item", "-1");
} else { // if there is no suggestion candidate, the autocompletion dropdown should disappear
delete_autocomplete_dd (ac_dropdown());
}
}
// GB: create autocomplete dropdown based on the properties of the textbox for new_obj_element
function create_autocomplete_dd (doc, ac_dropdown, new_obj_element) {
if(ac_dropdown === null) {
let font_width = new_obj_element.getAttribute("font_width");
let font_height = new_obj_element.getAttribute("font_height");
let style = new_obj_element.style;
let font_size = style.getPropertyValue("font-size").toString();
font_size = parseFloat(font_size.slice(0,font_size.length-2));
let line_height = style.getPropertyValue("line-height").toString();
let zoom = parseFloat(line_height.slice(0,line_height.length-1))/100;
let offset_y = zoom*font_size + 4;
let top = style.getPropertyValue("top").toString();
top = parseFloat(top.slice(0, top.length-2)) + offset_y;
var dd = doc.createElement("div");
configure_item(dd, {
id: "autocomplete_dropdown",
font_width: font_width,
font_height: font_height
});
dd.style.setProperty("position", "fixed");
dd.style.setProperty("left", style.getPropertyValue("left"));
dd.style.setProperty("top", top.toString() + "px");
dd.style.setProperty("font-size", style.getPropertyValue("font-size"));
dd.style.setProperty("line-height", line_height);
dd.style.setProperty("transform", style.getPropertyValue("transform"));
dd.style.setProperty("max-width", style.getPropertyValue("max-width"));
// dd.style.setProperty("-webkit-padding-after", style.getPropertyValue("-webkit-padding-after"));
dd.style.setProperty("min-width", style.getPropertyValue("min-width"));
dd.setAttribute("selected_item", "-1");
dd.setAttribute("searched_text", "");
doc.body.appendChild(dd);
}
}
function delete_autocomplete_dd (ac_dropdown) {
if (ac_dropdown !== null) {
ac_dropdown.parentNode.removeChild(ac_dropdown);
}
}
exports.index_obj_completion = index_obj_completion;
exports.write_completion_index = write_completion_index;
exports.autocomplete = autocomplete;
exports.update_autocomplete_selected = update_autocomplete_selected;
exports.update_autocomplete_dd_arrowdown = update_autocomplete_dd_arrowdown;
exports.update_autocomplete_dd_arrowup = update_autocomplete_dd_arrowup;
exports.select_result_autocomplete_dd = select_result_autocomplete_dd;
exports.repopulate_autocomplete_dd = repopulate_autocomplete_dd;
exports.create_autocomplete_dd = create_autocomplete_dd;
exports.delete_autocomplete_dd = delete_autocomplete_dd;
// Modules
......@@ -6907,6 +7012,13 @@ function gui_textarea(cid, tag, type, x, y, width_spec, height_spec, text,
} else {
patchwin[cid].window.canvas_events.normal();
}
// GB: Autocomplete dropdown -- if the paragraph element of "new_object_textentry" is deleted,
// the dropdown of autocompletion shall be deleted also
let autocomplete_dropdown = patchwin[cid].window.document.getElementById("autocomplete_dropdown");
if (autocomplete_dropdown !== null) {
autocomplete_dropdown.parentNode.removeChild(autocomplete_dropdown);
}
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment