1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-16 19:45:07 +00:00
serenity/Userland/DevTools/HackStudio/Locator.cpp
Sam Atkins 9322f0d110 HackStudio: Use Autocomplete window type for Locator pop-up
This being a Popup window meant it behaved in a couple of janky ways:
- It would steal the focus each time it was shown, so after every key
  press in the TextBox.
- It would disappear when you focused that TextBox again.

Using the Autocomplete window type fixes both of these. While the
Locator is not technically an autocomplete, it shares the general "type
and get suggestions based on the input, which you can select" behavior,
so this is close enough.
2024-01-24 11:05:54 +00:00

227 lines
7.9 KiB
C++

/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Locator.h"
#include "HackStudio.h"
#include "Project.h"
#include "ProjectDeclarations.h"
#include <LibGUI/AutocompleteProvider.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Window.h>
namespace HackStudio {
class LocatorSuggestionModel final : public GUI::Model {
public:
struct Suggestion {
static Suggestion create_filename(ByteString const& filename);
static Suggestion create_symbol_declaration(CodeComprehension::Declaration const&);
bool is_filename() const { return as_filename.has_value(); }
bool is_symbol_declaration() const { return as_symbol_declaration.has_value(); }
Optional<ByteString> as_filename;
Optional<CodeComprehension::Declaration> as_symbol_declaration;
};
explicit LocatorSuggestionModel(Vector<Suggestion>&& suggestions)
: m_suggestions(move(suggestions))
{
}
enum Column {
Icon,
Name,
Filename,
__Column_Count,
};
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_suggestions.size(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Column_Count; }
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
{
auto& suggestion = m_suggestions.at(index.row());
if (role != GUI::ModelRole::Display)
return {};
if (suggestion.is_filename()) {
if (index.column() == Column::Name)
return suggestion.as_filename.value();
if (index.column() == Column::Filename)
return "";
if (index.column() == Column::Icon)
return GUI::FileIconProvider::icon_for_path(suggestion.as_filename.value());
}
if (suggestion.is_symbol_declaration()) {
if (index.column() == Column::Name) {
if (!suggestion.as_symbol_declaration.value().scope.is_empty())
return suggestion.as_symbol_declaration.value().name;
return ByteString::formatted("{}::{}", suggestion.as_symbol_declaration.value().scope, suggestion.as_symbol_declaration.value().name);
}
if (index.column() == Column::Filename)
return suggestion.as_symbol_declaration.value().position.file;
if (index.column() == Column::Icon) {
auto icon = ProjectDeclarations::get_icon_for(suggestion.as_symbol_declaration.value().type);
if (icon.has_value())
return icon.value();
return {};
}
}
return {};
}
Vector<Suggestion> const& suggestions() const { return m_suggestions; }
private:
Vector<Suggestion> m_suggestions;
};
LocatorSuggestionModel::Suggestion LocatorSuggestionModel::Suggestion::create_filename(ByteString const& filename)
{
LocatorSuggestionModel::Suggestion s;
s.as_filename = filename;
return s;
}
LocatorSuggestionModel::Suggestion LocatorSuggestionModel::Suggestion::create_symbol_declaration(CodeComprehension::Declaration const& decl)
{
LocatorSuggestionModel::Suggestion s;
s.as_symbol_declaration = decl;
return s;
}
Locator::Locator(Core::EventReceiver* parent)
{
set_layout<GUI::VerticalBoxLayout>();
set_fixed_height(22);
m_textbox = add<GUI::TextBox>();
m_textbox->on_change = [this] {
update_suggestions();
};
m_textbox->on_escape_pressed = [this] {
m_popup_window->hide();
m_textbox->set_focus(false);
};
m_textbox->on_up_pressed = [this] {
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
if (new_index.is_valid())
new_index = m_suggestion_view->model()->index(new_index.row() - 1);
else
new_index = m_suggestion_view->model()->index(0);
if (m_suggestion_view->model()->is_within_range(new_index)) {
m_suggestion_view->selection().set(new_index);
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
}
};
m_textbox->on_down_pressed = [this] {
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
if (new_index.is_valid())
new_index = m_suggestion_view->model()->index(new_index.row() + 1);
else
new_index = m_suggestion_view->model()->index(0);
if (m_suggestion_view->model()->is_within_range(new_index)) {
m_suggestion_view->selection().set(new_index);
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
}
};
m_textbox->on_return_pressed = [this] {
auto selected_index = m_suggestion_view->selection().first();
if (!selected_index.is_valid())
return;
open_suggestion(selected_index);
};
m_textbox->on_focusout = [&]() {
close();
};
m_popup_window = GUI::Window::construct(parent);
m_popup_window->set_window_type(GUI::WindowType::Autocomplete);
m_popup_window->set_rect(0, 0, 500, 200);
m_suggestion_view = m_popup_window->set_main_widget<GUI::TableView>();
m_suggestion_view->set_column_headers_visible(false);
m_suggestion_view->on_activation = [this](auto& index) {
open_suggestion(index);
};
}
void Locator::open_suggestion(const GUI::ModelIndex& index)
{
auto& model = reinterpret_cast<LocatorSuggestionModel&>(*m_suggestion_view->model());
auto suggestion = model.suggestions()[index.row()];
if (suggestion.is_filename()) {
auto filename = suggestion.as_filename.value();
open_file(filename);
}
if (suggestion.is_symbol_declaration()) {
auto position = suggestion.as_symbol_declaration.value().position;
open_file(position.file, position.line, position.column);
}
close();
}
void Locator::open()
{
m_textbox->set_focus(true);
if (!m_textbox->text().is_empty()) {
m_textbox->select_all();
m_popup_window->show();
}
}
void Locator::close()
{
m_popup_window->hide();
}
void Locator::update_suggestions()
{
auto typed_text = m_textbox->text();
Vector<LocatorSuggestionModel::Suggestion> suggestions;
project().for_each_text_file([&](auto& file) {
if (file.name().contains(typed_text, CaseSensitivity::CaseInsensitive))
suggestions.append(LocatorSuggestionModel::Suggestion::create_filename(file.name()));
});
ProjectDeclarations::the().for_each_declared_symbol([&suggestions, &typed_text](auto& decl) {
if (decl.name.contains(typed_text, CaseSensitivity::CaseInsensitive) || decl.scope.contains(typed_text, CaseSensitivity::CaseInsensitive))
suggestions.append((LocatorSuggestionModel::Suggestion::create_symbol_declaration(decl)));
});
dbgln("I have {} suggestion(s):", suggestions.size());
// Limit the debug logging otherwise this can be very slow for large projects
if (suggestions.size() < 100) {
for (auto& s : suggestions) {
if (s.is_filename())
dbgln(" {}", s.as_filename.value());
if (s.is_symbol_declaration())
dbgln(" {} ({})", s.as_symbol_declaration.value().name, s.as_symbol_declaration.value().position.file);
}
}
bool has_suggestions = !suggestions.is_empty();
m_suggestion_view->set_model(adopt_ref(*new LocatorSuggestionModel(move(suggestions))));
if (!has_suggestions)
m_suggestion_view->selection().clear();
else
m_suggestion_view->selection().set(m_suggestion_view->model()->index(0));
m_popup_window->move_to(screen_relative_rect().top_left().translated(0, -m_popup_window->height()));
dbgln("Popup rect: {}", m_popup_window->rect());
m_popup_window->show();
}
}