diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 3c7d66a67be3..2a438ee6b81e 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -798,6 +798,17 @@ Returns [code]true[/code] if the specified [param feature] is supported by the current [DisplayServer], [code]false[/code] otherwise. + + + + + + Sets native help system search callbacks. + [param search_callback] has the following arguments: [code]String search_string, int result_limit[/code] and return [Dictionary] with "key, display name" pairs for the search results. Called when user enters search term in the [code]Help[/code] menu. + [param action_callback] has the following arguments: [code]String key[/code]. Called when the user selects a search result in [code]Help[/code] menu. + [b]Note:[/b] This method is implemented only on macOS. + + @@ -1676,6 +1687,9 @@ Display server supports reading screen pixels. See [method screen_get_pixel]. + + Display server supports native help system search callbacks. See [method help_set_search_callbacks]. + Makes the mouse cursor visible if it is hidden. diff --git a/editor/editor_help_search.cpp b/editor/editor_help_search.cpp index 1029cfcf0ef4..c520b02b6d79 100644 --- a/editor/editor_help_search.cpp +++ b/editor/editor_help_search.cpp @@ -49,6 +49,101 @@ void EditorHelpSearch::_update_icons() { } } +bool EditorHelpSearch::_all_terms_in_name(const Vector &p_terms, const String &p_name) const { + for (int i = 0; i < p_terms.size(); i++) { + if (p_name.findn(p_terms[i]) < 0) { + return false; + } + } + return true; +} + +void EditorHelpSearch::_match_method_name_and_push_back(const String &p_term, const Vector &p_terms, Vector &p_methods, const String &p_type, const String &p_metatype, const String &p_class_name, Dictionary &r_result) const { + // Constructors, Methods, Operators... + for (int i = 0; i < p_methods.size(); i++) { + String method_name = p_methods[i].name.to_lower(); + if (_all_terms_in_name(p_terms, method_name) || + (p_term.begins_with(".") && method_name.begins_with(p_term.substr(1))) || + (p_term.ends_with("(") && method_name.ends_with(p_term.left(p_term.length() - 1).strip_edges())) || + (p_term.begins_with(".") && p_term.ends_with("(") && method_name == p_term.substr(1, p_term.length() - 2).strip_edges())) { + r_result[vformat("class_%s:%s:%s", p_metatype, p_class_name, p_methods[i].name)] = vformat("%s > %s: %s", p_class_name, p_type, p_methods[i].name); + } + } +} + +void EditorHelpSearch::_match_const_name_and_push_back(const String &p_term, const Vector &p_terms, Vector &p_constants, const String &p_type, const String &p_metatype, const String &p_class_name, Dictionary &r_result) const { + for (int i = 0; i < p_constants.size(); i++) { + String method_name = p_constants[i].name.to_lower(); + if (_all_terms_in_name(p_terms, method_name) || + (p_term.begins_with(".") && method_name.begins_with(p_term.substr(1))) || + (p_term.ends_with("(") && method_name.ends_with(p_term.left(p_term.length() - 1).strip_edges())) || + (p_term.begins_with(".") && p_term.ends_with("(") && method_name == p_term.substr(1, p_term.length() - 2).strip_edges())) { + r_result[vformat("class_%s:%s:%s", p_metatype, p_class_name, p_constants[i].name)] = vformat("%s > %s: %s", p_class_name, p_type, p_constants[i].name); + } + } +} + +void EditorHelpSearch::_match_property_name_and_push_back(const String &p_term, const Vector &p_terms, Vector &p_properties, const String &p_type, const String &p_metatype, const String &p_class_name, Dictionary &r_result) const { + for (int i = 0; i < p_properties.size(); i++) { + String method_name = p_properties[i].name.to_lower(); + if (_all_terms_in_name(p_terms, method_name) || + (p_term.begins_with(".") && method_name.begins_with(p_term.substr(1))) || + (p_term.ends_with("(") && method_name.ends_with(p_term.left(p_term.length() - 1).strip_edges())) || + (p_term.begins_with(".") && p_term.ends_with("(") && method_name == p_term.substr(1, p_term.length() - 2).strip_edges())) { + r_result[vformat("class_%s:%s:%s", p_metatype, p_class_name, p_properties[i].name)] = vformat("%s > %s: %s", p_class_name, p_type, p_properties[i].name); + } + } +} + +void EditorHelpSearch::_match_theme_property_name_and_push_back(const String &p_term, const Vector &p_terms, Vector &p_properties, const String &p_type, const String &p_metatype, const String &p_class_name, Dictionary &r_result) const { + for (int i = 0; i < p_properties.size(); i++) { + String method_name = p_properties[i].name.to_lower(); + if (_all_terms_in_name(p_terms, method_name) || + (p_term.begins_with(".") && method_name.begins_with(p_term.substr(1))) || + (p_term.ends_with("(") && method_name.ends_with(p_term.left(p_term.length() - 1).strip_edges())) || + (p_term.begins_with(".") && p_term.ends_with("(") && method_name == p_term.substr(1, p_term.length() - 2).strip_edges())) { + r_result[vformat("class_%s:%s:%s", p_metatype, p_class_name, p_properties[i].name)] = vformat("%s > %s: %s", p_class_name, p_type, p_properties[i].name); + } + } +} + +Dictionary EditorHelpSearch::_native_search_cb(const String &p_search_string, int p_result_limit) { + Dictionary ret; + const String &term = p_search_string.strip_edges().to_lower(); + Vector terms = term.split_spaces(); + if (terms.is_empty()) { + terms.append(term); + } + + for (HashMap::Iterator iterator_doc = EditorHelp::get_doc_data()->class_list.begin(); iterator_doc; ++iterator_doc) { + DocData::ClassDoc &class_doc = iterator_doc->value; + if (class_doc.name.is_empty()) { + continue; + } + if (class_doc.name.findn(term) > -1) { + ret[vformat("class_name:%s", class_doc.name)] = class_doc.name; + } + if (term.length() > 1 || term == "@") { + _match_method_name_and_push_back(term, terms, class_doc.constructors, TTRC("Constructor"), "method", class_doc.name, ret); + _match_method_name_and_push_back(term, terms, class_doc.methods, TTRC("Method"), "method", class_doc.name, ret); + _match_method_name_and_push_back(term, terms, class_doc.operators, TTRC("Operator"), "method", class_doc.name, ret); + _match_method_name_and_push_back(term, terms, class_doc.signals, TTRC("Signal"), "signal", class_doc.name, ret); + _match_const_name_and_push_back(term, terms, class_doc.constants, TTRC("Constant"), "constant", class_doc.name, ret); + _match_property_name_and_push_back(term, terms, class_doc.properties, TTRC("Property"), "property", class_doc.name, ret); + _match_theme_property_name_and_push_back(term, terms, class_doc.theme_properties, TTRC("Theme Property"), "theme_item", class_doc.name, ret); + _match_method_name_and_push_back(term, terms, class_doc.annotations, TTRC("Annotation"), "annotation", class_doc.name, ret); + } + if (ret.size() > p_result_limit) { + break; + } + } + return ret; +} + +void EditorHelpSearch::_native_action_cb(const String &p_item_string) { + emit_signal(SNAME("go_to_help"), p_item_string); +} + void EditorHelpSearch::_update_results() { String term = search_box->get_text(); @@ -106,6 +201,18 @@ void EditorHelpSearch::_confirmed() { void EditorHelpSearch::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_HELP)) { + DisplayServer::get_singleton()->help_set_search_callbacks(callable_mp(this, &EditorHelpSearch::_native_search_cb), callable_mp(this, &EditorHelpSearch::_native_action_cb)); + } + } break; + + case NOTIFICATION_EXIT_TREE: { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_HELP)) { + DisplayServer::get_singleton()->help_set_search_callbacks(); + } + } break; + case NOTIFICATION_VISIBILITY_CHANGED: { if (!is_visible()) { results_tree->call_deferred(SNAME("clear")); // Wait for the Tree's mouse event propagation. diff --git a/editor/editor_help_search.h b/editor/editor_help_search.h index e8056ce6acda..3db5674b3886 100644 --- a/editor/editor_help_search.h +++ b/editor/editor_help_search.h @@ -75,6 +75,15 @@ class EditorHelpSearch : public ConfirmationDialog { void _filter_combo_item_selected(int p_option); void _confirmed(); + bool _all_terms_in_name(const Vector &p_terms, const String &p_name) const; + void _match_method_name_and_push_back(const String &p_term, const Vector &p_terms, Vector &p_methods, const String &p_type, const String &p_metatype, const String &p_class_name, Dictionary &r_result) const; + void _match_const_name_and_push_back(const String &p_term, const Vector &p_terms, Vector &p_constants, const String &p_type, const String &p_metatype, const String &p_class_name, Dictionary &r_result) const; + void _match_property_name_and_push_back(const String &p_term, const Vector &p_terms, Vector &p_properties, const String &p_type, const String &p_metatype, const String &p_class_name, Dictionary &r_result) const; + void _match_theme_property_name_and_push_back(const String &p_term, const Vector &p_terms, Vector &p_properties, const String &p_type, const String &p_metatype, const String &p_class_name, Dictionary &r_result) const; + + Dictionary _native_search_cb(const String &p_search_string, int p_result_limit); + void _native_action_cb(const String &p_item_string); + protected: void _notification(int p_what); static void _bind_methods(); diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 66c89d6cc558..c8e21739dd11 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -197,6 +197,9 @@ class DisplayServerMacOS : public DisplayServer { IOPMAssertionID screen_keep_on_assertion = kIOPMNullAssertionID; + Callable help_search_callback; + Callable help_action_callback; + struct MenuCall { Variant tag; Callable callback; @@ -265,6 +268,10 @@ class DisplayServerMacOS : public DisplayServer { virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual void help_set_search_callbacks(const Callable &p_search_callback = Callable(), const Callable &p_action_callback = Callable()) override; + Callable _help_get_search_callback() const; + Callable _help_get_action_callback() const; + virtual void global_menu_set_popup_callbacks(const String &p_menu_root, const Callable &p_open_callback = Callable(), const Callable &p_close_callback = Callable()) override; virtual int global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index = -1) override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index 53db9a5abf01..2e6fcabb4fa5 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -815,6 +815,7 @@ case FEATURE_TEXT_TO_SPEECH: case FEATURE_EXTEND_TO_TITLE: case FEATURE_SCREEN_CAPTURE: + case FEATURE_NATIVE_HELP: return true; default: { } @@ -826,6 +827,19 @@ return "macOS"; } +void DisplayServerMacOS::help_set_search_callbacks(const Callable &p_search_callback, const Callable &p_action_callback) { + help_search_callback = p_search_callback; + help_action_callback = p_action_callback; +} + +Callable DisplayServerMacOS::_help_get_search_callback() const { + return help_search_callback; +} + +Callable DisplayServerMacOS::_help_get_action_callback() const { + return help_action_callback; +} + bool DisplayServerMacOS::_has_help_menu() const { if ([NSApp helpMenu]) { return true; diff --git a/platform/macos/godot_application_delegate.h b/platform/macos/godot_application_delegate.h index 2426fb0b1c1f..45bd85c45cf4 100644 --- a/platform/macos/godot_application_delegate.h +++ b/platform/macos/godot_application_delegate.h @@ -36,7 +36,7 @@ #import #import -@interface GodotApplicationDelegate : NSObject +@interface GodotApplicationDelegate : NSObject - (void)forceUnbundledWindowActivationHackStep1; - (void)forceUnbundledWindowActivationHackStep2; - (void)forceUnbundledWindowActivationHackStep3; diff --git a/platform/macos/godot_application_delegate.mm b/platform/macos/godot_application_delegate.mm index a1925195b8dd..4cbb05dfb169 100644 --- a/platform/macos/godot_application_delegate.mm +++ b/platform/macos/godot_application_delegate.mm @@ -35,6 +35,59 @@ @implementation GodotApplicationDelegate +- (NSArray *)localizedTitlesForItem:(id)item { + NSArray *item_name = @[ item[1] ]; + return item_name; +} + +- (void)searchForItemsWithSearchString:(NSString *)searchString resultLimit:(NSInteger)resultLimit matchedItemHandler:(void (^)(NSArray *items))handleMatchedItems { + NSMutableArray *found_items = [[NSMutableArray alloc] init]; + + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds && ds->_help_get_search_callback().is_valid()) { + Callable cb = ds->_help_get_search_callback(); + + Variant ret; + Variant search_string = String::utf8([searchString UTF8String]); + Variant result_limit = (uint64_t)resultLimit; + Callable::CallError ce; + const Variant *args[2] = { &search_string, &result_limit }; + + cb.callp(args, 2, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat(RTR("Failed to execute help search callback: %s."), Variant::get_callable_error_text(cb, args, 2, ce))); + } + Dictionary results = ret; + for (const Variant *E = results.next(); E; E = results.next(E)) { + const String &key = *E; + const String &value = results[*E]; + if (key.length() > 0 && value.length() > 0) { + NSArray *item = @[ [NSString stringWithUTF8String:key.utf8().get_data()], [NSString stringWithUTF8String:value.utf8().get_data()] ]; + [found_items addObject:item]; + } + } + } + + handleMatchedItems(found_items); +} + +- (void)performActionForItem:(id)item { + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds && ds->_help_get_action_callback().is_valid()) { + Callable cb = ds->_help_get_action_callback(); + + Variant ret; + Variant item_string = String::utf8([item[0] UTF8String]); + Callable::CallError ce; + const Variant *args[1] = { &item_string }; + + cb.callp(args, 1, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat(RTR("Failed to execute help action callback: %s."), Variant::get_callable_error_text(cb, args, 1, ce))); + } + } +} + - (void)forceUnbundledWindowActivationHackStep1 { // Step 1: Switch focus to macOS SystemUIServer process. // Required to perform step 2, TransformProcessType will fail if app is already the in focus. diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index 29dff683d530..dae0a004eeb7 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -821,6 +821,7 @@ id delegate = [[GodotApplicationDelegate alloc] init]; ERR_FAIL_NULL(delegate); [NSApp setDelegate:delegate]; + [NSApp registerUserInterfaceItemSearchHandler:delegate]; pre_wait_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, &pre_wait_observer_cb, nullptr); CFRunLoopAddObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 6459cc74622e..10486681cd23 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -47,6 +47,10 @@ DisplayServer::DisplayServerCreate DisplayServer::server_create_functions[Displa int DisplayServer::server_create_count = 1; +void DisplayServer::help_set_search_callbacks(const Callable &p_search_callback, const Callable &p_action_callback) { + WARN_PRINT("Native help is not supported by this display server."); +} + int DisplayServer::global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { WARN_PRINT("Global menus not supported by this display server."); return -1; @@ -598,6 +602,8 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("has_feature", "feature"), &DisplayServer::has_feature); ClassDB::bind_method(D_METHOD("get_name"), &DisplayServer::get_name); + ClassDB::bind_method(D_METHOD("help_set_search_callbacks", "search_callback", "action_callback"), &DisplayServer::help_set_search_callbacks); + ClassDB::bind_method(D_METHOD("global_menu_set_popup_callbacks", "menu_root", "open_callback", "close_callback"), &DisplayServer::global_menu_set_popup_callbacks); ClassDB::bind_method(D_METHOD("global_menu_add_submenu_item", "menu_root", "label", "submenu", "index"), &DisplayServer::global_menu_add_submenu_item, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("global_menu_add_item", "menu_root", "label", "callback", "key_callback", "tag", "accelerator", "index"), &DisplayServer::global_menu_add_item, DEFVAL(Callable()), DEFVAL(Callable()), DEFVAL(Variant()), DEFVAL(Key::NONE), DEFVAL(-1)); @@ -834,6 +840,7 @@ void DisplayServer::_bind_methods() { BIND_ENUM_CONSTANT(FEATURE_TEXT_TO_SPEECH); BIND_ENUM_CONSTANT(FEATURE_EXTEND_TO_TITLE); BIND_ENUM_CONSTANT(FEATURE_SCREEN_CAPTURE); + BIND_ENUM_CONSTANT(FEATURE_NATIVE_HELP); BIND_ENUM_CONSTANT(MOUSE_MODE_VISIBLE); BIND_ENUM_CONSTANT(MOUSE_MODE_HIDDEN); diff --git a/servers/display_server.h b/servers/display_server.h index d2e112d2241b..5e168d37d079 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -125,11 +125,14 @@ class DisplayServer : public Object { FEATURE_TEXT_TO_SPEECH, FEATURE_EXTEND_TO_TITLE, FEATURE_SCREEN_CAPTURE, + FEATURE_NATIVE_HELP, }; virtual bool has_feature(Feature p_feature) const = 0; virtual String get_name() const = 0; + virtual void help_set_search_callbacks(const Callable &p_search_callback = Callable(), const Callable &p_action_callback = Callable()); + virtual void global_menu_set_popup_callbacks(const String &p_menu_root, const Callable &p_open_callback = Callable(), const Callable &p_close_callback = Callable()); virtual int global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index = -1);