/* * Copyright (c) 2021, Spencer Dixon * Copyright (c) 2022-2023, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include "Providers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Assistant { struct AppState { Optional selected_index; Vector> results; size_t visible_result_count { 0 }; Threading::Mutex lock; DeprecatedString last_query; }; class ResultRow final : public GUI::Button { C_OBJECT(ResultRow) ResultRow() { set_greedy_for_hits(true); set_fixed_height(36); set_text_alignment(Gfx::TextAlignment::CenterLeft); set_button_style(Gfx::ButtonStyle::Coolbar); set_focus_policy(GUI::FocusPolicy::NoFocus); on_context_menu_request = [this](auto& event) { if (!m_context_menu) { m_context_menu = GUI::Menu::construct(); if (LexicalPath path { text().to_deprecated_string() }; path.is_absolute()) { m_context_menu->add_action(GUI::Action::create("&Show in File Manager", MUST(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-file-manager.png"sv)), [=](auto&) { Desktop::Launcher::open(URL::create_with_file_scheme(path.dirname(), path.basename())); })); m_context_menu->add_separator(); } m_context_menu->add_action(GUI::Action::create("&Copy as Text", MUST(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-copy.png"sv)), [&](auto&) { GUI::Clipboard::the().set_plain_text(text()); })); } m_context_menu->popup(event.screen_position()); }; } RefPtr m_context_menu; }; template class Database { public: explicit Database(AppState& state, Array, ProviderCount>& providers) : m_state(state) , m_providers(providers) { } Function>)> on_new_results; void search(DeprecatedString const& query) { auto should_display_precached_results = false; for (size_t i = 0; i < ProviderCount; ++i) { auto& result_array = m_result_cache.ensure(query); if (result_array.at(i) == nullptr) { m_providers[i]->query(query, [this, query, i](auto results) { { Threading::MutexLocker db_locker(m_mutex); auto& result_array = m_result_cache.ensure(query); if (result_array.at(i) != nullptr) return; result_array[i] = make>>(results); } on_result_cache_updated(); }); } else { should_display_precached_results = true; } } if (should_display_precached_results) on_result_cache_updated(); } private: void on_result_cache_updated() { Threading::MutexLocker state_locker(m_state.lock); auto new_results = m_result_cache.find(m_state.last_query); if (new_results == m_result_cache.end()) return; // NonnullRefPtrVector will provide dual_pivot_quick_sort references rather than pointers, // and dual_pivot_quick_sort requires being able to construct the underlying type on the // stack. Assistant::Result is pure virtual, thus cannot be constructed on the stack. Vector> all_results; for (auto const& results_for_provider : new_results->value) { if (results_for_provider == nullptr) continue; for (auto const& result : *results_for_provider) { all_results.append(result); } } dual_pivot_quick_sort(all_results, 0, static_cast(all_results.size() - 1), [](auto& a, auto& b) { return a->score() > b->score(); }); on_new_results(all_results); } AppState& m_state; Array, ProviderCount> m_providers; Threading::Mutex m_mutex; HashMap>>, ProviderCount>> m_result_cache; }; } static constexpr size_t MAX_SEARCH_RESULTS = 6; ErrorOr serenity_main(Main::Arguments arguments) { TRY(Core::System::pledge("stdio recvfd sendfd rpath cpath unix proc exec thread")); Core::LockFile lockfile("/tmp/lock/assistant.lock"); if (!lockfile.is_held()) { if (lockfile.error_code()) { warnln("Core::LockFile: {}", strerror(lockfile.error_code())); return 1; } // Another assistant is open, so exit silently. return 0; } auto app = TRY(GUI::Application::try_create(arguments)); auto window = GUI::Window::construct(); window->set_minimizable(false); Assistant::AppState app_state; Array, 5> providers = { make_ref_counted(), make_ref_counted(), make_ref_counted(), make_ref_counted(), make_ref_counted() }; Assistant::Database db { app_state, providers }; auto container = TRY(window->set_main_widget()); container->set_fill_with_background_color(true); container->set_frame_shape(Gfx::FrameShape::Window); container->set_layout(8); auto& text_box = container->add(); auto& results_container = container->add(); results_container.set_layout(); auto mark_selected_item = [&]() { for (size_t i = 0; i < app_state.visible_result_count; ++i) { auto& row = static_cast(results_container.child_widgets()[i]); row.set_font_weight(i == app_state.selected_index ? 700 : 400); } }; text_box.on_change = Core::debounce([&]() { { Threading::MutexLocker locker(app_state.lock); if (app_state.last_query == text_box.text()) return; app_state.last_query = text_box.text(); } db.search(text_box.text()); }, 5); text_box.on_return_pressed = [&]() { if (!app_state.selected_index.has_value()) return; lockfile.release(); app_state.results[app_state.selected_index.value()]->activate(); GUI::Application::the()->quit(); }; text_box.on_up_pressed = [&]() { if (!app_state.visible_result_count) return; auto new_selected_index = app_state.selected_index.value_or(0); if (new_selected_index == 0) new_selected_index = app_state.visible_result_count - 1; else if (new_selected_index > 0) new_selected_index -= 1; app_state.selected_index = new_selected_index; mark_selected_item(); }; text_box.on_down_pressed = [&]() { if (!app_state.visible_result_count) return; auto new_selected_index = app_state.selected_index.value_or(0); if ((app_state.visible_result_count - 1) == new_selected_index) new_selected_index = 0; else if (app_state.visible_result_count > new_selected_index) new_selected_index += 1; app_state.selected_index = new_selected_index; mark_selected_item(); }; text_box.on_escape_pressed = []() { GUI::Application::the()->quit(); }; window->on_active_window_change = [](bool is_active_window) { if (!is_active_window) GUI::Application::the()->quit(); }; auto update_ui_timer = TRY(Core::Timer::create_single_shot(10, [&] { results_container.remove_all_children(); results_container.layout()->set_margins(app_state.visible_result_count ? GUI::Margins { 4, 0, 0, 0 } : GUI::Margins { 0 }); for (size_t i = 0; i < app_state.visible_result_count; ++i) { auto& result = app_state.results[i]; auto& match = results_container.add(); match.set_icon(result->bitmap()); match.set_text(String::from_deprecated_string(result->title()).release_value_but_fixme_should_propagate_errors()); match.set_tooltip(move(result->tooltip())); match.on_click = [&result](auto) { result->activate(); GUI::Application::the()->quit(); }; } mark_selected_item(); Core::deferred_invoke([&] { window->resize(GUI::Desktop::the().rect().width() / 3, {}); }); })); db.on_new_results = [&](auto results) { if (results.is_empty()) app_state.selected_index = {}; else app_state.selected_index = 0; app_state.results = results; app_state.visible_result_count = min(results.size(), MAX_SEARCH_RESULTS); update_ui_timer->restart(); }; window->set_window_type(GUI::WindowType::Popup); window->set_forced_shadow(true); window->resize(GUI::Desktop::the().rect().width() / 3, {}); window->center_on_screen(); window->move_to(window->x(), window->y() - (GUI::Desktop::the().rect().height() * 0.33)); window->show(); return app->exec(); }