mirror of
https://github.com/RGBCube/serenity
synced 2025-07-24 22:07:34 +00:00
LibGUI: Add an optional "automatic" autocomplete feature to TextEditor
This aims to be a "smart" autocomplete that tries to present the user with useful suggestions without being in the way (too much). Here is its current configuration: - Show suggestions 800ms after something is inserted in the editor - if something else is inserted in that period, reset it back to 800ms to allow the user to type uninterrupted - cancel any shown autocomplete (and the timer) on external changes (paste, cut, etc)
This commit is contained in:
parent
60f5f48dd1
commit
a4a238ddc8
3 changed files with 75 additions and 12 deletions
|
@ -92,6 +92,8 @@ public:
|
||||||
}
|
}
|
||||||
virtual void update() override {};
|
virtual void update() override {};
|
||||||
|
|
||||||
|
void set_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions) { m_suggestions = move(suggestions); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Vector<AutocompleteProvider::Entry> m_suggestions;
|
Vector<AutocompleteProvider::Entry> m_suggestions;
|
||||||
};
|
};
|
||||||
|
@ -111,16 +113,21 @@ AutocompleteBox::AutocompleteBox(TextEditor& editor)
|
||||||
|
|
||||||
void AutocompleteBox::update_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions)
|
void AutocompleteBox::update_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions)
|
||||||
{
|
{
|
||||||
if (suggestions.is_empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool has_suggestions = !suggestions.is_empty();
|
bool has_suggestions = !suggestions.is_empty();
|
||||||
m_suggestion_view->set_model(adopt(*new AutocompleteSuggestionModel(move(suggestions))));
|
if (m_suggestion_view->model()) {
|
||||||
|
auto& model = *static_cast<AutocompleteSuggestionModel*>(m_suggestion_view->model());
|
||||||
|
model.set_suggestions(move(suggestions));
|
||||||
|
} else {
|
||||||
|
m_suggestion_view->set_model(adopt(*new AutocompleteSuggestionModel(move(suggestions))));
|
||||||
|
m_suggestion_view->update();
|
||||||
|
if (has_suggestions)
|
||||||
|
m_suggestion_view->set_cursor(m_suggestion_view->model()->index(0), GUI::AbstractView::SelectionUpdate::Set);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_suggestion_view->model()->update();
|
||||||
|
m_suggestion_view->update();
|
||||||
if (!has_suggestions)
|
if (!has_suggestions)
|
||||||
m_suggestion_view->selection().clear();
|
close();
|
||||||
else
|
|
||||||
m_suggestion_view->selection().set(m_suggestion_view->model()->index(0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AutocompleteBox::is_visible() const
|
bool AutocompleteBox::is_visible() const
|
||||||
|
@ -130,7 +137,12 @@ bool AutocompleteBox::is_visible() const
|
||||||
|
|
||||||
void AutocompleteBox::show(Gfx::IntPoint suggstion_box_location)
|
void AutocompleteBox::show(Gfx::IntPoint suggstion_box_location)
|
||||||
{
|
{
|
||||||
|
if (!m_suggestion_view->model() || m_suggestion_view->model()->row_count() == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
m_popup_window->move_to(suggstion_box_location);
|
m_popup_window->move_to(suggstion_box_location);
|
||||||
|
if (!is_visible())
|
||||||
|
m_suggestion_view->move_cursor(GUI::AbstractView::CursorMovement::Home, GUI::AbstractTableView::SelectionUpdate::Set);
|
||||||
m_popup_window->show();
|
m_popup_window->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -713,6 +713,7 @@ void TextEditor::sort_selected_lines()
|
||||||
|
|
||||||
void TextEditor::keydown_event(KeyEvent& event)
|
void TextEditor::keydown_event(KeyEvent& event)
|
||||||
{
|
{
|
||||||
|
TemporaryChange change { m_should_keep_autocomplete_box, true };
|
||||||
if (m_autocomplete_box && m_autocomplete_box->is_visible() && (event.key() == KeyCode::Key_Return || event.key() == KeyCode::Key_Tab)) {
|
if (m_autocomplete_box && m_autocomplete_box->is_visible() && (event.key() == KeyCode::Key_Return || event.key() == KeyCode::Key_Tab)) {
|
||||||
m_autocomplete_box->apply_suggestion();
|
m_autocomplete_box->apply_suggestion();
|
||||||
m_autocomplete_box->close();
|
m_autocomplete_box->close();
|
||||||
|
@ -979,6 +980,8 @@ void TextEditor::keydown_event(KeyEvent& event)
|
||||||
if (event.key() == KeyCode::Key_Backspace) {
|
if (event.key() == KeyCode::Key_Backspace) {
|
||||||
if (!is_editable())
|
if (!is_editable())
|
||||||
return;
|
return;
|
||||||
|
if (m_autocomplete_box)
|
||||||
|
m_autocomplete_box->close();
|
||||||
if (has_selection()) {
|
if (has_selection()) {
|
||||||
delete_selection();
|
delete_selection();
|
||||||
did_update_selection();
|
did_update_selection();
|
||||||
|
@ -1017,6 +1020,8 @@ void TextEditor::keydown_event(KeyEvent& event)
|
||||||
if (event.modifiers() == Mod_Shift && event.key() == KeyCode::Key_Delete) {
|
if (event.modifiers() == Mod_Shift && event.key() == KeyCode::Key_Delete) {
|
||||||
if (!is_editable())
|
if (!is_editable())
|
||||||
return;
|
return;
|
||||||
|
if (m_autocomplete_box)
|
||||||
|
m_autocomplete_box->close();
|
||||||
delete_current_line();
|
delete_current_line();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1024,17 +1029,15 @@ void TextEditor::keydown_event(KeyEvent& event)
|
||||||
if (event.key() == KeyCode::Key_Delete) {
|
if (event.key() == KeyCode::Key_Delete) {
|
||||||
if (!is_editable())
|
if (!is_editable())
|
||||||
return;
|
return;
|
||||||
|
if (m_autocomplete_box)
|
||||||
|
m_autocomplete_box->close();
|
||||||
do_delete();
|
do_delete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!event.shift() && !event.alt() && event.ctrl() && event.key() == KeyCode::Key_Space) {
|
if (!event.shift() && !event.alt() && event.ctrl() && event.key() == KeyCode::Key_Space) {
|
||||||
if (m_autocomplete_provider) {
|
if (m_autocomplete_provider) {
|
||||||
m_autocomplete_provider->provide_completions([&](auto completions) {
|
try_show_autocomplete();
|
||||||
m_autocomplete_box->update_suggestions(move(completions));
|
|
||||||
auto position = content_rect_for_position(cursor()).bottom_right().translated(screen_relative_rect().top_left().translated(ruler_width(), 0).translated(10, 5));
|
|
||||||
m_autocomplete_box->show(position);
|
|
||||||
});
|
|
||||||
update_autocomplete.disarm();
|
update_autocomplete.disarm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1044,6 +1047,8 @@ void TextEditor::keydown_event(KeyEvent& event)
|
||||||
StringBuilder sb;
|
StringBuilder sb;
|
||||||
sb.append_code_point(event.code_point());
|
sb.append_code_point(event.code_point());
|
||||||
|
|
||||||
|
if (should_autocomplete_automatically())
|
||||||
|
m_autocomplete_timer->start();
|
||||||
insert_at_cursor_or_replace_selection(sb.to_string());
|
insert_at_cursor_or_replace_selection(sb.to_string());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1428,6 +1433,19 @@ void TextEditor::undefer_reflow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextEditor::try_show_autocomplete()
|
||||||
|
{
|
||||||
|
if (m_autocomplete_provider) {
|
||||||
|
m_autocomplete_provider->provide_completions([&](auto completions) {
|
||||||
|
auto has_completions = !completions.is_empty();
|
||||||
|
m_autocomplete_box->update_suggestions(move(completions));
|
||||||
|
auto position = content_rect_for_position(cursor()).bottom_right().translated(screen_relative_rect().top_left().translated(ruler_width(), 0).translated(10, 5));
|
||||||
|
if (has_completions)
|
||||||
|
m_autocomplete_box->show(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TextEditor::enter_event(Core::Event&)
|
void TextEditor::enter_event(Core::Event&)
|
||||||
{
|
{
|
||||||
m_automatic_selection_scroll_timer->stop();
|
m_automatic_selection_scroll_timer->stop();
|
||||||
|
@ -1437,6 +1455,10 @@ void TextEditor::leave_event(Core::Event&)
|
||||||
{
|
{
|
||||||
if (m_in_drag_select)
|
if (m_in_drag_select)
|
||||||
m_automatic_selection_scroll_timer->start();
|
m_automatic_selection_scroll_timer->start();
|
||||||
|
if (m_autocomplete_timer)
|
||||||
|
m_autocomplete_timer->stop();
|
||||||
|
if (m_autocomplete_box)
|
||||||
|
m_autocomplete_box->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextEditor::did_change()
|
void TextEditor::did_change()
|
||||||
|
@ -1445,6 +1467,10 @@ void TextEditor::did_change()
|
||||||
recompute_all_visual_lines();
|
recompute_all_visual_lines();
|
||||||
m_undo_action->set_enabled(can_undo());
|
m_undo_action->set_enabled(can_undo());
|
||||||
m_redo_action->set_enabled(can_redo());
|
m_redo_action->set_enabled(can_redo());
|
||||||
|
if (m_autocomplete_box && !m_should_keep_autocomplete_box) {
|
||||||
|
m_autocomplete_timer->stop();
|
||||||
|
m_autocomplete_box->close();
|
||||||
|
}
|
||||||
if (!m_has_pending_change_notification) {
|
if (!m_has_pending_change_notification) {
|
||||||
m_has_pending_change_notification = true;
|
m_has_pending_change_notification = true;
|
||||||
deferred_invoke([this](auto&) {
|
deferred_invoke([this](auto&) {
|
||||||
|
@ -1848,4 +1874,19 @@ void TextEditor::set_visualize_trailing_whitespace(bool enabled)
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextEditor::set_should_autocomplete_automatically(bool value)
|
||||||
|
{
|
||||||
|
if (value == should_autocomplete_automatically())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
ASSERT(m_autocomplete_provider);
|
||||||
|
m_autocomplete_timer = Core::Timer::create_single_shot(m_automatic_autocomplete_delay_ms, [this] { try_show_autocomplete(); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_child(*m_autocomplete_timer);
|
||||||
|
m_autocomplete_timer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
#include <AK/NonnullOwnPtrVector.h>
|
#include <AK/NonnullOwnPtrVector.h>
|
||||||
#include <AK/NonnullRefPtrVector.h>
|
#include <AK/NonnullRefPtrVector.h>
|
||||||
#include <LibCore/ElapsedTimer.h>
|
#include <LibCore/ElapsedTimer.h>
|
||||||
|
#include <LibCore/Timer.h>
|
||||||
#include <LibGUI/Forward.h>
|
#include <LibGUI/Forward.h>
|
||||||
#include <LibGUI/ScrollableWidget.h>
|
#include <LibGUI/ScrollableWidget.h>
|
||||||
#include <LibGUI/TextDocument.h>
|
#include <LibGUI/TextDocument.h>
|
||||||
|
@ -163,6 +164,9 @@ public:
|
||||||
const AutocompleteProvider* autocomplete_provider() const;
|
const AutocompleteProvider* autocomplete_provider() const;
|
||||||
void set_autocomplete_provider(OwnPtr<AutocompleteProvider>&&);
|
void set_autocomplete_provider(OwnPtr<AutocompleteProvider>&&);
|
||||||
|
|
||||||
|
bool should_autocomplete_automatically() const { return m_autocomplete_timer; }
|
||||||
|
void set_should_autocomplete_automatically(bool);
|
||||||
|
|
||||||
bool is_in_drag_select() const { return m_in_drag_select; }
|
bool is_in_drag_select() const { return m_in_drag_select; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -212,6 +216,8 @@ private:
|
||||||
void defer_reflow();
|
void defer_reflow();
|
||||||
void undefer_reflow();
|
void undefer_reflow();
|
||||||
|
|
||||||
|
void try_show_autocomplete();
|
||||||
|
|
||||||
int icon_size() const { return 16; }
|
int icon_size() const { return 16; }
|
||||||
int icon_padding() const { return 2; }
|
int icon_padding() const { return 2; }
|
||||||
|
|
||||||
|
@ -323,8 +329,12 @@ private:
|
||||||
OwnPtr<SyntaxHighlighter> m_highlighter;
|
OwnPtr<SyntaxHighlighter> m_highlighter;
|
||||||
OwnPtr<AutocompleteProvider> m_autocomplete_provider;
|
OwnPtr<AutocompleteProvider> m_autocomplete_provider;
|
||||||
OwnPtr<AutocompleteBox> m_autocomplete_box;
|
OwnPtr<AutocompleteBox> m_autocomplete_box;
|
||||||
|
bool m_should_keep_autocomplete_box { false };
|
||||||
|
size_t m_automatic_autocomplete_delay_ms { 800 };
|
||||||
|
|
||||||
RefPtr<Core::Timer> m_automatic_selection_scroll_timer;
|
RefPtr<Core::Timer> m_automatic_selection_scroll_timer;
|
||||||
|
RefPtr<Core::Timer> m_autocomplete_timer;
|
||||||
|
|
||||||
Gfx::IntPoint m_last_mousemove_position;
|
Gfx::IntPoint m_last_mousemove_position;
|
||||||
|
|
||||||
RefPtr<Gfx::Bitmap> m_icon;
|
RefPtr<Gfx::Bitmap> m_icon;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue