godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.17k stars 98 forks source link

Add interaction sounds to BaseButton (customizable via theme) #1472

Open SoloCarryGuy opened 4 years ago

SoloCarryGuy commented 4 years ago

Describe the project you are working on: I am working on a match 3 game

Describe the problem or limitation you are having in your project: For all the button presses I have to manually code a sound to be played by creating a global sound manager whenever a button is pressed

Describe the feature / enhancement and how it helps to overcome the problem or limitation: Add sound to be played on button pressed

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams: Sorry, I am a beginner in godot and game development, I don't know how to impplement it.

If this enhancement will not be used often, can it be worked around with a few lines of script?: I think this is used for every button in every game made.

Is there a reason why this should be core and not an add-on in the asset library?: Again, because there are always sounds on button presses

Jummit commented 4 years ago

Sorry, I am a beginner in godot and game development, I don't know how to impplement it.

https://gamedev.stackexchange.com/questions/184354/add-a-sound-to-all-the-buttons-in-a-project/184363#184363

Calinou commented 4 years ago

See also https://github.com/godotengine/godot/issues/3608.

JOELwindows7 commented 3 years ago

Sorry, I am a beginner in godot and game development, I don't know how to impplement it.

https://gamedev.stackexchange.com/questions/184354/add-a-sound-to-all-the-buttons-in-a-project/184363#184363

Thancc. cool and good. However though, there is a disadvantage you have to beware:

well, I guess, maybe an AudioStreamPlayer in each Button node might be sufficient way for now. anybody can solve the eventual lag everytime new button node appears for particularly using automatic signal connection method like @Jummit above?

Jummit commented 3 years ago

As long as you don't have thousands of buttons instantiated at the same time, there shouldn't be a problem, right?

JOELwindows7 commented 3 years ago

As long as you don't have thousands of buttons instantiated at the same time, there shouldn't be a problem, right?

at least it works really well. If the workflow is simply at only one way form of a UI in this node or anything simple and not complicated, this should not be a problem.

but yeah. my game workflow is like above, New nodes that will instantiated & freed. And I have alot.

perhaps by having _on_node_freed method which contains disconnect signal would help.

Calinou commented 3 years ago

I worked around this in godot-mdk by adding a custom class that extends Button and instantiating it in my scenes using the editor instead of using Button. Here's a more generic example:

# Save this script, then add MyButton nodes instead of Button using the Create New Node dialog.
extends Button
class_name MyButton

## Called when the button is pressed.
func _pressed():
    # This assumes you have a Sound singleton designed to play sounds with polyphony:
    # https://github.com/Calinou/godot-mdk/blob/master/autoload/sound.gd
    Sound.play(self, preload("res://button_click.wav"))

The _pressed() function is called automatically by Godot when you add it to a node that extends BaseButton. No need to connect it to a signal.

YuriSizov commented 3 years ago

As the theme guy on the team I fully support that controls should have an inherent ability to produce focus and interaction sounds, and that it should be customizable with a theme. It's not too hard to work around with nodes, but it's an unnecessary complication for getting your game that polished feel.

I will gladly work on this.

dalexeev commented 2 years ago

One more option (for 4.0-dev):

extends Button
class_name MyButton

var _allow_focus_sfx := true

func _init() -> void:
    focus_entered.connect(_on_focus_entered)

func _input(event: InputEvent) -> void:
    if !is_visible_in_tree() || !has_focus():
        return

    if event.is_action_pressed('ui_accept'):
        if disabled:
            SFX.play('gui_error.wav')
        else:
            SFX.play('gui_accept.wav')

func grab_focus_no_sfx() -> void:
    _allow_focus_sfx = false
    grab_focus()
    _allow_focus_sfx = true

func _on_focus_entered() -> void:
    if _allow_focus_sfx:
        SFX.play('gui_focus.wav')
Shadowblitz16 commented 1 year ago

@dalexeev that doesn't work for mouse right clicks infact that doesn't seem to work for alot of things

Calinou commented 1 year ago

Note that this will probably require something analogous to SceneTreeTimer/SceneTreeTween to be implemented. A SceneTreeAudioStreamPlayer should allow polyphonic audio playback without creating any nodes, so that using things like get_tree().change_scene_to_packed() does not stop sound playback.

(This is also useful in game logic if you want to avoid the issue where freeing a node stops its audio playback. However, we'd need to add positional variants in this case as well, and you would most likely not be able to change their position after creating them.)

@dalexeev that doesn't work for mouse right clicks infact that doesn't seem to work for alot of things

It should work with right-clicks if the button has the appropriate click mask. Buttons should not play a sound if clicked with a button that doesn't actually press them, so it's expected that by default, right-clicking shouldn't play a sound.

Shadowblitz16 commented 1 year ago

@Calinou it doesn't. it seems like it has to do with the mouse clicks that don't have a mask that happen after it's already focused.

it only tends to play the focus sound with the mouse. I also tried changing action mode to press it doesn't change anything

try it

extends Button
class_name MyButton

var _allow_focus_sfx := true

@export var sound_error  : AudioStream
@export var sound_focus  : AudioStream
@export var sound_accept : AudioStream

func _init() -> void:
    focus_entered.connect(_on_focus_entered)

func _input(event: InputEvent) -> void:
    if !is_visible_in_tree() || !has_focus():
        return

    if event.is_action_pressed('ui_accept'):
        if    disabled && sound_error : Sound.play(self, sound_error)
        elif !disabled && sound_accept: Sound.play(self, sound_accept)

func grab_focus_no_sfx() -> void:
    _allow_focus_sfx = false
    grab_focus()
    _allow_focus_sfx = true

func _on_focus_entered() -> void:
    if _allow_focus_sfx && sound_focus:
        Sound.play(self, sound_focus)
dalexeev commented 1 year ago

@Shadowblitz16 Sorry, I forgot to mention that this class was only implemented for keyboard control. Mouse support requires other changes. For example like this:

Code ```gdscript extends Button class_name MyButton var _allow_focus_sfx := true func _init() -> void: focus_entered.connect(_on_focus_entered) button_down.connect(_on_button_down) pressed.connect(_on_pressed) func _gui_input(event: InputEvent) -> void: if disabled: if event.is_action_pressed("ui_accept") or ( event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed()): SFX.play("gui_error.wav") func grab_focus_no_sfx() -> void: _allow_focus_sfx = false grab_focus() _allow_focus_sfx = true func _on_focus_entered() -> void: if _allow_focus_sfx and not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): SFX.play("gui_focus.wav") func _on_button_down() -> void: SFX.play("gui_focus.wav") func _on_pressed() -> void: if toggle_mode: if button_pressed: SFX.play("gui_enabled.wav") else: SFX.play("gui_disabled.wav") else: SFX.play("gui_accept.wav") ```
Calinou commented 1 year ago

@YuriSizov How do you envision the audio theme items in terms of structure? I'm asking because I wonder how we could expose a way to set generic "hover", "focus", "pressed" sounds that work across the entire theme, without requiring users to override them on individual classes. However, if users wish to override the interaction sounds on a specific control, they should be able to do so.

Could this work in a manner similar to the Default Font top-level Theme property?

YuriSizov commented 1 year ago

@Calinou I'm not a fan of this idea. The default font works because we can decide to use it as a fallback based on the resource type. What you propose is that we add special resolution logic based on the resource type AND the name of a theme item. We don't really have a very consistent theme item naming convention and it would make the whole thing string-reliant.

So for now I'd focus on implementing the core system, having audio as a theme item type, making it work, figuring out how it must work. The hard part is figuring out how the audio stuff is going to be configured. Should we allow for positional audio or should it be position-less? How do we configure the bus? Would it be a part of some theme audio resource or a setting on the theme resource? Or a global setting per project?

Having some kind of fallback system can be added later, if we truly need it. Way after we ship the initial feature.

Calinou commented 1 year ago

7.5 years after originally proposing this feature, I have a working proof of concept: https://github.com/Calinou/godot/tree/add-theme-audio-items See commit message for technical details.

Testing project: control_gallery.zip You need to compile the above branch for that project to play audio. Audio might be quite loud by default depending on your setup, so consider turning down your volume before interacting with the UI.

I couldn't figure out how to play audio without creating nodes[^1], so I added a helper method to SceneTree that creates a temporary top-level internal node (similar to what https://github.com/godotengine/godot/pull/79599 does). The node is automatically freed once audio playback is finished. Multiple nodes may be created at once, which implicitly allows for polyphony (including with different, unrelated AudioStreams). [^1]: Some commented out code remains in the branch if you'd like to take a look.

Using an AudioStreamRandomizer resource should work too, if you want to add some variation to UI sounds. From a performance standpoint, creating nodes is probably not too bad since you're unlikely to have more than 5 active UI audio sources at a given time. (This is especially the case if you stick to WAV, as recommended for short audio files.)

YuriSizov commented 1 year ago

Amazing work!

I couldn't figure out how to play audio without creating nodes

Well, that's a bummer. I think this should be resolved first, we need to be able to do the playback directly with the server, at least for the non-positional audio (which is what the UI sfx would likely be). Maybe @ellenhp can help us figure out how feasible it would be to implement?

Calinou commented 1 year ago

I made further progress in my branch: https://github.com/Calinou/godot/tree/add-theme-audio-items

August 2023 patch ```patch From dd1e89856eb30261bd9552a5746f8b16dd505e56 Mon Sep 17 00:00:00 2001 From: Hugo Locurcio Date: Tue, 25 Jul 2023 03:13:07 +0200 Subject: [PATCH] Add theme audio items This allows themes to play sounds when hovering/pressing buttons. A new "audio" theme item type is added to allow custom controls to define their own audio streams, as well as benefit from the theme override system. Playback is handled by adding internal nodes at the top of the SceneTree, and is not affected by pause or scene changes. Nodes are automatically freed once playback is done. Polyphony is implicitly supported by creating a node for each audio playback event. `get_tree().create_audio_stream_player(stream: AudioStream, bus: StringName, volume_db: float)` can now be used to play a sound non-positionally, without having to worry about a parent node being freed and having the sound be cut off early. This is useful to play one-off sounds such as announcer audio. TODO: - Reimplement `focus` audio playback to work with all Control-derived nodes. Perhaps also use different theme items for mouse/touch and keyboard/gamepad-induced focus (to prevent playing audio twice when clicking a button with mouse/touch). - Fix BaseButton's `pressed_disabled` never being played as this isn't called on disabled buttons. SpinBox also has the same issue. - Return early in `SceneTree::create_audio_stream_player()` if using a bare AudioStream resource (useful to mute specific sounds with theme overrides without spamming errors). - Fix audio theme overrides not appearing in the inspector for Control-derived nodes. - Make `--doctool` able to see audio theme items. --- core/config/project_settings.cpp | 1 + doc/classes/AudioStreamPlayer.xml | 1 + doc/classes/Control.xml | 42 ++++ doc/classes/ProjectSettings.xml | 4 + doc/classes/SceneTree.xml | 18 ++ doc/classes/Theme.xml | 64 +++++- doc/classes/ThemeDB.xml | 3 + doc/classes/Window.xml | 52 ++++- editor/editor_help.cpp | 1 + editor/plugins/theme_editor_plugin.cpp | 203 +++++++++++++++- editor/plugins/theme_editor_plugin.h | 11 + scene/gui/base_button.cpp | 5 + scene/gui/control.cpp | 90 ++++++++ scene/gui/control.h | 7 + scene/gui/item_list.cpp | 3 + scene/gui/line_edit.cpp | 6 + scene/gui/popup_menu.cpp | 8 +- scene/gui/slider.cpp | 10 + scene/gui/spin_box.cpp | 5 + scene/gui/tab_bar.cpp | 21 ++ scene/gui/text_edit.cpp | 1 + scene/gui/tree.cpp | 5 + scene/main/scene_tree.cpp | 148 ++++++++++++ scene/main/scene_tree.h | 28 +++ scene/main/window.cpp | 88 +++++++ scene/main/window.h | 7 + .../resources/default_theme/default_theme.cpp | 37 +++ scene/resources/theme.cpp | 217 ++++++++++++++++++ scene/resources/theme.h | 17 ++ scene/theme/theme_db.cpp | 17 ++ scene/theme/theme_db.h | 5 + 31 files changed, 1117 insertions(+), 8 deletions(-) diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 973162a0664..a144d5b748b 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1302,6 +1302,7 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF("display/window/energy_saving/keep_screen_on.editor", false); GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "audio/buses/default_bus_layout", PROPERTY_HINT_FILE, "*.tres"), "res://default_bus_layout.tres"); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "audio/buses/gui_theme_bus"), "Master"); GLOBAL_DEF_RST("audio/general/text_to_speech", false); GLOBAL_DEF_RST(PropertyInfo(Variant::FLOAT, "audio/general/2d_panning_strength", PROPERTY_HINT_RANGE, "0,2,0.01"), 0.5f); GLOBAL_DEF_RST(PropertyInfo(Variant::FLOAT, "audio/general/3d_panning_strength", PROPERTY_HINT_RANGE, "0,2,0.01"), 0.5f); diff --git a/doc/classes/AudioStreamPlayer.xml b/doc/classes/AudioStreamPlayer.xml index c48c7f43008..ee1dcc0b957 100644 --- a/doc/classes/AudioStreamPlayer.xml +++ b/doc/classes/AudioStreamPlayer.xml @@ -6,6 +6,7 @@ Plays an audio stream non-positionally. To play audio positionally, use [AudioStreamPlayer2D] or [AudioStreamPlayer3D] instead of [AudioStreamPlayer]. + To keep playing audio after freeing the node using the [AudioStreamPlayer] or after changing scenes, use [method SceneTree.create_audio_stream_player] instead. $DOCS_URL/tutorials/audio/audio_streams.html diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml index ee790b69686..03a865750b2 100644 --- a/doc/classes/Control.xml +++ b/doc/classes/Control.xml @@ -220,6 +220,15 @@ [b]Note:[/b] This does not affect the methods in [Input], only the way events are propagated. + + + + + + Creates a local override for a theme audio with the specified [param name]. Local overrides always take precedence when fetching theme items for the control. An override can be removed with [method remove_theme_audio_override]. + See also [method get_theme_audio]. + + @@ -444,6 +453,15 @@ [/codeblock] + + + + + + Returns an audio from the first matching [Theme] in the tree if that [Theme] has an audio item with the specified [param name] and [param theme_type]. + See [method get_theme_color] for details. + + @@ -577,6 +595,23 @@ Returns [code]true[/code] if this is the current focused control. See [member focus_mode]. + + + + + + Returns [code]true[/code] if there is a matching [Theme] in the tree that has an audio item with the specified [param name] and [param theme_type]. + See [method get_theme_color] for details. + + + + + + + Returns [code]true[/code] if there is a local override for a theme audio with the specified [param name] in this [Control] node. + See [method add_theme_audio_override]. + + @@ -698,6 +733,13 @@ Give up the focus. No other control will be able to receive input. + + + + + Removes a local override for a theme audio with the specified [param name] previously added by [method add_theme_audio_override] or via the Inspector dock. + + diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 3c4b8837f81..f86f2dcebdb 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -359,6 +359,10 @@ Default [AudioBusLayout] resource file to use in the project, unless overridden by the scene. + + The name of the audio bus to play GUI theme audio in (case-sensitive). All sounds played using [method SceneTree.play_theme_audio] will go through the bus specified in [member audio/buses/gui_theme_bus]. This can be used to put UI sounds in a dedicated audio bus, which allows for adjusting UI volume independently from other sounds in the project. + If the specified audio bus doesn't exist, audio will play on the [code]Master[/code] bus instead. + Specifies the audio driver to use. This setting is platform-dependent as each platform supports different audio drivers. If left empty, the default audio driver will be used. The [code]Dummy[/code] audio driver disables all audio playback and recording, which is useful for non-game applications as it reduces CPU usage. It also prevents the engine from appearing as an application playing audio in the OS' audio mixer. diff --git a/doc/classes/SceneTree.xml b/doc/classes/SceneTree.xml index c0d98ef921d..80838ef0586 100644 --- a/doc/classes/SceneTree.xml +++ b/doc/classes/SceneTree.xml @@ -54,6 +54,16 @@ [b]Note:[/b] The new scene node is added to the tree at the end of the frame. You won't be able to access it immediately after the [method change_scene_to_packed] call. + + + + + + + Plays the specified [param stream] on [param bus] with volume offset by [param volume_db] and returns the associated [AudioStreamPlayer] instance, which is automatically freed once the audio is done playing. Audio is played in a non-positional manner. Unlike when using [AudioStreamPlayer] within another node, the audio will not stop if the node that called [method create_audio_stream_player] is freed or if the scene is changed using [method change_scene_to_file] or [method change_scene_to_packed]. + To play UI sounds in custom [Control]s, use [method play_theme_audio] instead. + + @@ -158,6 +168,14 @@ [b]Note:[/b] Group call flags are used to control the notification sending behavior. By default, notifications will be sent immediately in a way similar to [method notify_group]. However, if the [constant GROUP_CALL_DEFERRED] flag is present in the [param call_flags] argument, notifications will be sent at the end of the current frame in a way similar to using [code]Object.call_deferred("notification", ...)[/code]. + + + + + Plays the specified [param stream] and returns the [AudioStreamPlayer] instance, which is automatically freed once the audio is done playing. [method play_theme_audio] is intended to be used by custom [Control]s to play sounds on user interaction based on theme items. The bus used for theme audio can be configured using [member ProjectSettings.audio/buses/gui_theme_bus]. + To play generic audio in a non-positional manner without having to create an [AudioStreamPlayer] node, use [method create_audio_stream_player] instead. + + diff --git a/doc/classes/Theme.xml b/doc/classes/Theme.xml index eb3c1705835..a3ca4a3f145 100644 --- a/doc/classes/Theme.xml +++ b/doc/classes/Theme.xml @@ -27,6 +27,15 @@ Removes all the theme properties defined on the theme resource. + + + + + + Removes the audio property defined by [param name] and [param theme_type], if it exists. + Fails if it doesn't exist. Use [method has_audio] to check for existence. + + @@ -99,6 +108,28 @@ Unmarks [param theme_type] as being a variation of another theme type. See [method set_type_variation]. + + + + + + Returns the audio property defined by [param name] and [param theme_type], if it exists. + Returns the engine fallback audio value if the property doesn't exist (see [member ThemeDB.fallback_audio]). Use [method has_audio] to check for existence. + + + + + + + Returns a list of names for audio properties defined with [param theme_type]. Use [method get_audio_type_list] to get a list of possible theme type names. + + + + + + Returns a list of all unique theme type names for audio properties. Use [method get_type_list] to get a list of all unique theme types. + + @@ -281,6 +312,15 @@ Returns a list of all type variations for the given [param base_type]. + + + + + + Returns [code]true[/code] if the audio property defined by [param name] and [param theme_type] exists. + Returns [code]false[/code] if it doesn't exist. Use [method set_audio] to define it. + + @@ -390,6 +430,16 @@ Removes the theme type, gracefully discarding defined theme items. If the type is a variation, this information is also erased. If the type is a base for type variations, those variations lose their base. + + + + + + + Renames the audio property defined by [param old_name] and [param theme_type] to [param name], if it exists. + Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_audio] to check for existence, and [method clear_audio] to remove the existing property. + + @@ -462,6 +512,15 @@ [b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic. + + + + + + + Creates or changes the value of the audio property defined by [param name] and [param theme_type]. Use [method clear_audio] to remove the property. + + @@ -573,7 +632,10 @@ Theme's [StyleBox] item type. - + + Theme's [AudioStream] item type. + + Maximum value for the DataType enum. diff --git a/doc/classes/ThemeDB.xml b/doc/classes/ThemeDB.xml index 106d011c434..45c511eeaed 100644 --- a/doc/classes/ThemeDB.xml +++ b/doc/classes/ThemeDB.xml @@ -25,6 +25,9 @@ + + The fallback audio of every [Control] node and [Theme] resource. Used when no other value is available to the control. + The fallback base scale factor of every [Control] node and [Theme] resource. Used when no other value is available to the control. See also [member Theme.default_base_scale]. diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml index 82498b9ba47..7ce50ebb556 100644 --- a/doc/classes/Window.xml +++ b/doc/classes/Window.xml @@ -16,6 +16,15 @@ Virtual method to be implemented by the user. Overrides the value returned by [method get_contents_minimum_size]. + + + + + + Creates a local override for a theme audio with the specified [param name]. Local overrides always take precedence when fetching theme items for the control. An override can be removed with [method remove_theme_audio_override]. + See also [method get_theme_audio]. + + @@ -126,6 +135,15 @@ Returns the window's size including its border. + + + + + + Returns an audio from the first matching [Theme] in the tree if that [Theme] has an audio item with the specified [param name] and [param theme_type]. + See [method Control.get_theme_color] for details. + + @@ -219,6 +237,23 @@ Returns [code]true[/code] if the window is focused. + + + + + + Returns [code]true[/code] if there is a matching [Theme] in the tree that has an audio item with the specified [param name] and [param theme_type]. + See [method Control.get_theme_audio] for details. + + + + + + + Returns [code]true[/code] if there is a local override for a theme audio with the specified [param name] in this [Control] node. + See [method add_theme_audio_override]. + + @@ -242,7 +277,7 @@ Returns [code]true[/code] if there is a matching [Theme] in the tree that has a constant item with the specified [param name] and [param theme_type]. - See [method Control.get_theme_color] for details. + See [method Control.get_theme_constant] for details. @@ -259,7 +294,7 @@ Returns [code]true[/code] if there is a matching [Theme] in the tree that has a font item with the specified [param name] and [param theme_type]. - See [method Control.get_theme_color] for details. + See [method Control.get_theme_font] for details. @@ -276,7 +311,7 @@ Returns [code]true[/code] if there is a matching [Theme] in the tree that has a font size item with the specified [param name] and [param theme_type]. - See [method Control.get_theme_color] for details. + See [method Control.get_theme_font_size] for details. @@ -293,7 +328,7 @@ Returns [code]true[/code] if there is a matching [Theme] in the tree that has an icon item with the specified [param name] and [param theme_type]. - See [method Control.get_theme_color] for details. + See [method Control.get_theme_icon] for details. @@ -310,7 +345,7 @@ Returns [code]true[/code] if there is a matching [Theme] in the tree that has a stylebox item with the specified [param name] and [param theme_type]. - See [method Control.get_theme_color] for details. + See [method Control.get_theme_stylebox] for details. @@ -442,6 +477,13 @@ Popups the [Window] with a position shifted by parent [Window]'s position. If the [Window] is embedded, has the same effect as [method popup]. + + + + + Removes a local override for a theme audio with the specified [param name] previously added by [method add_theme_audio_override] or via the Inspector dock. + + diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 7573fcd21e9..63cffce8244 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -1186,6 +1186,7 @@ void EditorHelp::_update_doc() { data_type_names["font_size"] = TTR("Font Sizes"); data_type_names["icon"] = TTR("Icons"); data_type_names["style"] = TTR("Styles"); + data_type_names["audio"] = TTR("Audios"); for (int i = 0; i < cd.theme_properties.size(); i++) { theme_property_line[cd.theme_properties[i].name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description. diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp index a1ddfc4b850..a90097ac42b 100644 --- a/editor/plugins/theme_editor_plugin.cpp +++ b/editor/plugins/theme_editor_plugin.cpp @@ -71,6 +71,7 @@ void ThemeItemImportTree::_update_items_tree() { int font_size_amount = 0; int icon_amount = 0; int stylebox_amount = 0; + int audio_amount = 0; tree_color_items.clear(); tree_constant_items.clear(); @@ -78,6 +79,7 @@ void ThemeItemImportTree::_update_items_tree() { tree_font_size_items.clear(); tree_icon_items.clear(); tree_stylebox_items.clear(); + tree_audio_items.clear(); for (const StringName &E : types) { String type_name = (String)E; @@ -194,6 +196,14 @@ void ThemeItemImportTree::_update_items_tree() { stylebox_amount += filtered_names.size(); break; + case Theme::DATA_TYPE_AUDIOSTREAM: + data_type_node->set_icon(0, get_theme_icon(SNAME("AudioStream"), SNAME("EditorIcons"))); + data_type_node->set_text(0, TTR("Audios")); + + item_list = &tree_audio_items; + audio_amount += filtered_names.size(); + break; + case Theme::DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -316,6 +326,20 @@ void ThemeItemImportTree::_update_items_tree() { select_full_styleboxes_button->set_visible(false); deselect_all_styleboxes_button->set_visible(false); } + + if (audio_amount > 0) { + Array arr; + arr.push_back(audio_amount); + select_audios_label->set_text(TTRN("1 audio", "{num} audios", audio_amount).format(arr, "{num}")); + select_all_audios_button->set_visible(true); + select_full_audios_button->set_visible(true); + deselect_all_audios_button->set_visible(true); + } else { + select_audios_label->set_text(TTR("No audios found.")); + select_all_audios_button->set_visible(false); + select_full_audios_button->set_visible(false); + deselect_all_audios_button->set_visible(false); + } } void ThemeItemImportTree::_toggle_type_items(bool p_collapse) { @@ -433,6 +457,10 @@ void ThemeItemImportTree::_update_total_selected(Theme::DataType p_data_type) { total_selected_items_label = total_selected_styleboxes_label; break; + case Theme::DATA_TYPE_AUDIOSTREAM: + total_selected_items_label = total_selected_audios_label; + break; + case Theme::DATA_TYPE_MAX: return; // Can't happen, but silences warning. } @@ -598,6 +626,10 @@ void ThemeItemImportTree::_select_all_data_type_pressed(int p_data_type) { item_list = &tree_stylebox_items; break; + case Theme::DATA_TYPE_AUDIOSTREAM: + item_list = &tree_audio_items; + break; + case Theme::DATA_TYPE_MAX: return; // Can't happen, but silences warning. } @@ -653,6 +685,10 @@ void ThemeItemImportTree::_select_full_data_type_pressed(int p_data_type) { item_list = &tree_stylebox_items; break; + case Theme::DATA_TYPE_AUDIOSTREAM: + item_list = &tree_audio_items; + break; + case Theme::DATA_TYPE_MAX: return; // Can't happen, but silences warning. } @@ -710,6 +746,10 @@ void ThemeItemImportTree::_deselect_all_data_type_pressed(int p_data_type) { item_list = &tree_stylebox_items; break; + case Theme::DATA_TYPE_AUDIOSTREAM: + item_list = &tree_audio_items; + break; + case Theme::DATA_TYPE_MAX: return; // Can't happen, but silences warning. } @@ -788,6 +828,10 @@ void ThemeItemImportTree::_import_selected() { item_value = Ref(); break; + case Theme::DATA_TYPE_AUDIOSTREAM: + item_value = Ref(); + break; + case Theme::DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -839,6 +883,7 @@ void ThemeItemImportTree::reset_item_tree() { total_selected_font_sizes_label->hide(); total_selected_icons_label->hide(); total_selected_styleboxes_label->hide(); + total_selected_audios_label->hide(); _update_items_tree(); } @@ -894,6 +939,11 @@ void ThemeItemImportTree::_notification(int p_what) { deselect_all_styleboxes_button->set_icon(get_theme_icon(SNAME("ThemeDeselectAll"), SNAME("EditorIcons"))); select_all_styleboxes_button->set_icon(get_theme_icon(SNAME("ThemeSelectAll"), SNAME("EditorIcons"))); select_full_styleboxes_button->set_icon(get_theme_icon(SNAME("ThemeSelectFull"), SNAME("EditorIcons"))); + + select_audios_icon->set_texture(get_theme_icon(SNAME("AudioStream"), SNAME("EditorIcons"))); + deselect_all_audios_button->set_icon(get_theme_icon(SNAME("ThemeDeselectAll"), SNAME("EditorIcons"))); + select_all_audios_button->set_icon(get_theme_icon(SNAME("ThemeSelectAll"), SNAME("EditorIcons"))); + select_full_audios_button->set_icon(get_theme_icon(SNAME("ThemeSelectFull"), SNAME("EditorIcons"))); } break; } } @@ -988,6 +1038,13 @@ ThemeItemImportTree::ThemeItemImportTree() { select_full_styleboxes_button = memnew(Button); total_selected_styleboxes_label = memnew(Label); + select_audios_icon = memnew(TextureRect); + select_audios_label = memnew(Label); + deselect_all_audios_button = memnew(Button); + select_all_audios_button = memnew(Button); + select_full_audios_button = memnew(Button); + total_selected_audios_label = memnew(Label); + for (int i = 0; i < Theme::DATA_TYPE_MAX; i++) { Theme::DataType dt = (Theme::DataType)i; @@ -1088,6 +1145,20 @@ ThemeItemImportTree::ThemeItemImportTree() { deselect_all_items_tooltip = TTR("Deselect all visible stylebox items."); break; + case Theme::DATA_TYPE_AUDIOSTREAM: + select_items_icon = select_audios_icon; + select_items_label = select_audios_label; + deselect_all_items_button = deselect_all_audios_button; + select_all_items_button = select_all_audios_button; + select_full_items_button = select_full_audios_button; + total_selected_items_label = total_selected_audios_label; + + items_title = TTR("Audios"); + select_all_items_tooltip = TTR("Select all visible audio items."); + select_full_items_tooltip = TTR("Select all visible audio items and their data."); + deselect_all_items_tooltip = TTR("Deselect all visible audio items."); + break; + case Theme::DATA_TYPE_MAX: continue; // Can't happen, but silences warning. } @@ -1274,6 +1345,7 @@ void ThemeItemEditorDialog::_update_edit_types() { edit_items_add_font_size->set_disabled(false); edit_items_add_icon->set_disabled(false); edit_items_add_stylebox->set_disabled(false); + edit_items_add_audio->set_disabled(false); edit_items_remove_class->set_disabled(false); edit_items_remove_custom->set_disabled(false); @@ -1288,6 +1360,7 @@ void ThemeItemEditorDialog::_update_edit_types() { edit_items_add_font_size->set_disabled(true); edit_items_add_icon->set_disabled(true); edit_items_add_stylebox->set_disabled(true); + edit_items_add_audio->set_disabled(true); edit_items_remove_class->set_disabled(true); edit_items_remove_custom->set_disabled(true); @@ -1471,6 +1544,29 @@ void ThemeItemEditorDialog::_update_edit_item_tree(String p_item_type) { } } + { // Audios. + names.clear(); + edited_theme->get_audio_list(p_item_type, &names); + + if (names.size() > 0) { + TreeItem *audio_root = edit_items_tree->create_item(root); + audio_root->set_metadata(0, Theme::DATA_TYPE_AUDIOSTREAM); + audio_root->set_icon(0, get_theme_audio(SNAME("AudioStream"), SNAME("EditorIcons"))); + audio_root->set_text(0, TTR("Audios")); + audio_root->add_button(0, get_theme_audio(SNAME("Clear"), SNAME("EditorIcons")), ITEMS_TREE_REMOVE_DATA_TYPE, false, TTR("Remove All audio Items")); + + names.sort_custom(); + for (const StringName &E : names) { + TreeItem *item = edit_items_tree->create_item(audio_root); + item->set_text(0, E); + item->add_button(0, get_theme_audio(SNAME("Edit"), SNAME("EditorIcons")), ITEMS_TREE_RENAME_ITEM, false, TTR("Rename Item")); + item->add_button(0, get_theme_audio(SNAME("Remove"), SNAME("EditorIcons")), ITEMS_TREE_REMOVE_ITEM, false, TTR("Remove Item")); + } + + has_any_items = true; + } + } + // If some type is selected, but it doesn't seem to have any items, show a guiding message. TreeItem *selected_item = edit_type_list->get_selected(); if (selected_item) { @@ -1568,6 +1664,10 @@ void ThemeItemEditorDialog::_add_theme_item(Theme::DataType p_data_type, String ur->add_do_method(*edited_theme, "set_constant", p_item_name, p_item_type, 0); ur->add_undo_method(*edited_theme, "clear_constant", p_item_name, p_item_type); break; + case Theme::DATA_TYPE_AUDIOSTREAM: + ur->add_do_method(*edited_theme, "set_audio", p_item_name, p_item_type, 0); + ur->add_undo_method(*edited_theme, "clear_audio", p_item_name, p_item_type); + break; case Theme::DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -1758,6 +1858,9 @@ void ThemeItemEditorDialog::_open_add_theme_item_dialog(int p_data_type) { case Theme::DATA_TYPE_STYLEBOX: edit_theme_item_dialog->set_title(TTR("Add Stylebox Item")); break; + case Theme::DATA_TYPE_AUDIOSTREAM: + edit_theme_item_dialog->set_title(TTR("Add Audio Item")); + break; case Theme::DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -1794,6 +1897,9 @@ void ThemeItemEditorDialog::_open_rename_theme_item_dialog(Theme::DataType p_dat case Theme::DATA_TYPE_STYLEBOX: edit_theme_item_dialog->set_title(TTR("Rename Stylebox Item")); break; + case Theme::DATA_TYPE_AUDIOSTREAM: + edit_theme_item_dialog->set_title(TTR("Rename Audio Item")); + break; case Theme::DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -1878,6 +1984,7 @@ void ThemeItemEditorDialog::_notification(int p_what) { edit_items_add_font_size->set_icon(get_theme_icon(SNAME("FontSize"), SNAME("EditorIcons"))); edit_items_add_icon->set_icon(get_theme_icon(SNAME("ImageTexture"), SNAME("EditorIcons"))); edit_items_add_stylebox->set_icon(get_theme_icon(SNAME("StyleBoxFlat"), SNAME("EditorIcons"))); + edit_items_add_audio->set_icon(get_theme_icon(SNAME("AudioStream"), SNAME("EditorIcons"))); edit_items_remove_class->set_icon(get_theme_icon(SNAME("Control"), SNAME("EditorIcons"))); edit_items_remove_custom->set_icon(get_theme_icon(SNAME("ThemeRemoveCustomItems"), SNAME("EditorIcons"))); @@ -1999,6 +2106,13 @@ ThemeItemEditorDialog::ThemeItemEditorDialog(ThemeTypeEditor *p_theme_type_edito edit_items_toolbar->add_child(edit_items_add_stylebox); edit_items_add_stylebox->connect("pressed", callable_mp(this, &ThemeItemEditorDialog::_open_add_theme_item_dialog).bind(Theme::DATA_TYPE_STYLEBOX)); + edit_items_add_audio = memnew(Button); + edit_items_add_audio->set_tooltip_text(TTR("Add Audio Item")); + edit_items_add_audio->set_flat(true); + edit_items_add_audio->set_disabled(true); + edit_items_toolbar->add_child(edit_items_add_audio); + edit_items_add_audio->connect("pressed", callable_mp(this, &ThemeItemEditorDialog::_open_add_theme_item_dialog).bind(Theme::DATA_TYPE_AUDIOSTREAM)); + edit_items_toolbar->add_child(memnew(VSeparator)); Label *edit_items_toolbar_remove_label = memnew(Label); @@ -2732,6 +2846,44 @@ void ThemeTypeEditor::_update_type_items() { } } + // Audios. + { + for (int i = audio_items_list->get_child_count() - 1; i >= 0; i--) { + Node *node = audio_items_list->get_child(i); + node->queue_free(); + audio_items_list->remove_child(node); + } + + HashMap audio_items = _get_type_items(edited_type, &Theme::get_audio_list, show_default); + for (const KeyValue &E : audio_items) { + HBoxContainer *item_control = _create_property_control(Theme::DATA_TYPE_AUDIOSTREAM, E.key, E.value); + EditorResourcePicker *item_editor = memnew(EditorResourcePicker); + item_editor->set_h_size_flags(SIZE_EXPAND_FILL); + item_editor->set_base_type("AudioStream"); + item_control->add_child(item_editor); + + if (E.value) { + if (edited_theme->has_audio(E.key, edited_type)) { + item_editor->set_edited_resource(edited_theme->get_audio(E.key, edited_type)); + } else { + item_editor->set_edited_resource(Ref()); + } + item_editor->connect("resource_selected", callable_mp(this, &ThemeTypeEditor::_edit_resource_item)); + item_editor->connect("resource_changed", callable_mp(this, &ThemeTypeEditor::_audio_item_changed).bind(E.key)); + } else { + if (ThemeDB::get_singleton()->get_default_theme()->has_audio(E.key, edited_type)) { + item_editor->set_edited_resource(ThemeDB::get_singleton()->get_default_theme()->get_audio(E.key, edited_type)); + } else { + item_editor->set_edited_resource(Ref()); + } + item_editor->set_editable(false); + } + + _add_focusable(item_editor); + audio_items_list->add_child(item_control); + } + } + // Various type settings. if (edited_type.is_empty() || ClassDB::class_exists(edited_type)) { type_variation_edit->set_editable(false); @@ -2826,6 +2978,15 @@ void ThemeTypeEditor::_add_default_type_items() { } } } + { + names.clear(); + ThemeDB::get_singleton()->get_default_theme()->get_audio_list(default_type, &names); + for (const StringName &E : names) { + if (!new_snapshot->has_audio(E, edited_type)) { + new_snapshot->set_audio(E, edited_type, ThemeDB::get_singleton()->get_default_theme()->get_audio(E, edited_type)); + } + } + } updating = false; @@ -2882,6 +3043,10 @@ void ThemeTypeEditor::_item_add_cbk(int p_data_type, Control *p_control) { ur->add_undo_method(this, "_unpin_leading_stylebox"); } } break; + case Theme::DATA_TYPE_AUDIOSTREAM: { + ur->add_do_method(*edited_theme, "set_audio", item_name, edited_type, Ref()); + ur->add_undo_method(*edited_theme, "clear_audio", item_name, edited_type); + } break; } ur->commit_action(); @@ -2927,6 +3092,10 @@ void ThemeTypeEditor::_item_override_cbk(int p_data_type, String p_item_name) { ur->add_undo_method(this, "_unpin_leading_stylebox"); } } break; + case Theme::DATA_TYPE_AUDIOSTREAM: { + ur->add_do_method(*edited_theme, "set_audio", p_item_name, edited_type, Ref()); + ur->add_undo_method(*edited_theme, "clear_audio", p_item_name, edited_type); + } break; } ur->commit_action(); @@ -2979,6 +3148,14 @@ void ThemeTypeEditor::_item_remove_cbk(int p_data_type, String p_item_name) { ur->add_undo_method(this, "_pin_leading_stylebox", p_item_name, sb); } } break; + case Theme::DATA_TYPE_AUDIOSTREAM: { + ur->add_do_method(*edited_theme, "clear_audio", p_item_name, edited_type); + if (edited_theme->has_audio(p_item_name, edited_type)) { + ur->add_undo_method(*edited_theme, "set_audio", p_item_name, edited_type, edited_theme->get_audio(p_item_name, edited_type)); + } else { + ur->add_undo_method(*edited_theme, "set_audio", p_item_name, edited_type, Ref()); + } + } break; } ur->commit_action(); @@ -3043,6 +3220,10 @@ void ThemeTypeEditor::_item_rename_confirmed(int p_data_type, String p_item_name leading_stylebox.item_name = new_name; } } break; + case Theme::DATA_TYPE_AUDIOSTREAM: { + ur->add_do_method(*edited_theme, "rename_audio", p_item_name, new_name, edited_type); + ur->add_undo_method(*edited_theme, "rename_audio", new_name, p_item_name, edited_type); + } break; } ur->commit_action(); @@ -3148,6 +3329,23 @@ void ThemeTypeEditor::_stylebox_item_changed(Ref p_value, String p_ite ur->commit_action(); } +void ThemeTypeEditor::_audio_item_changed(Ref p_value, String p_item_name) { + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + ur->create_action(TTR("Set Audio Item in Theme")); + + ur->add_do_method(*edited_theme, "set_audio", p_item_name, edited_type, p_value.is_valid() ? p_value : Ref()); + if (edited_theme->has_audio(p_item_name, edited_type)) { + ur->add_undo_method(*edited_theme, "set_audio", p_item_name, edited_type, edited_theme->get_audio(p_item_name, edited_type)); + } else { + ur->add_undo_method(*edited_theme, "set_audio", p_item_name, edited_type, Ref()); + } + + ur->add_do_method(this, "call_deferred", "_update_type_items"); + ur->add_undo_method(this, "call_deferred", "_update_type_items"); + + ur->commit_action(); +} + void ThemeTypeEditor::_change_pinned_stylebox() { if (leading_stylebox.pinned) { if (leading_stylebox.stylebox.is_valid()) { @@ -3321,7 +3519,8 @@ void ThemeTypeEditor::_notification(int p_what) { data_type_tabs->set_tab_icon(3, get_theme_icon(SNAME("FontSize"), SNAME("EditorIcons"))); data_type_tabs->set_tab_icon(4, get_theme_icon(SNAME("ImageTexture"), SNAME("EditorIcons"))); data_type_tabs->set_tab_icon(5, get_theme_icon(SNAME("StyleBoxFlat"), SNAME("EditorIcons"))); - data_type_tabs->set_tab_icon(6, get_theme_icon(SNAME("Tools"), SNAME("EditorIcons"))); + data_type_tabs->set_tab_icon(6, get_theme_icon(SNAME("AudioStream"), SNAME("EditorIcons"))); + data_type_tabs->set_tab_icon(7, get_theme_icon(SNAME("Tools"), SNAME("EditorIcons"))); type_variation_button->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons"))); } break; @@ -3371,6 +3570,7 @@ void ThemeTypeEditor::select_type(String p_type_name) { edited_theme->add_font_size_type(edited_type); edited_theme->add_color_type(edited_type); edited_theme->add_constant_type(edited_type); + edited_theme->add_audio_type(edited_type); _update_type_list(); } @@ -3433,6 +3633,7 @@ ThemeTypeEditor::ThemeTypeEditor() { font_size_items_list = _create_item_list(Theme::DATA_TYPE_FONT_SIZE); icon_items_list = _create_item_list(Theme::DATA_TYPE_ICON); stylebox_items_list = _create_item_list(Theme::DATA_TYPE_STYLEBOX); + audio_items_list = _create_item_list(Theme::DATA_TYPE_AUDIOSTREAM); VBoxContainer *type_settings_tab = memnew(VBoxContainer); type_settings_tab->set_custom_minimum_size(Size2(0, 160) * EDSCALE); diff --git a/editor/plugins/theme_editor_plugin.h b/editor/plugins/theme_editor_plugin.h index 077ce8e8f75..dc04f483187 100644 --- a/editor/plugins/theme_editor_plugin.h +++ b/editor/plugins/theme_editor_plugin.h @@ -87,6 +87,7 @@ class ThemeItemImportTree : public VBoxContainer { List tree_font_size_items; List tree_icon_items; List tree_stylebox_items; + List tree_audio_items; bool updating_tree = false; @@ -137,6 +138,13 @@ class ThemeItemImportTree : public VBoxContainer { Button *deselect_all_styleboxes_button = nullptr; Label *total_selected_styleboxes_label = nullptr; + TextureRect *select_audios_icon = nullptr; + Label *select_audios_label = nullptr; + Button *select_all_audios_button = nullptr; + Button *select_full_audios_button = nullptr; + Button *deselect_all_audios_button = nullptr; + Label *total_selected_audios_label = nullptr; + HBoxContainer *select_icons_warning_hb = nullptr; TextureRect *select_icons_warning_icon = nullptr; Label *select_icons_warning = nullptr; @@ -210,6 +218,7 @@ class ThemeItemEditorDialog : public AcceptDialog { Button *edit_items_add_font_size = nullptr; Button *edit_items_add_icon = nullptr; Button *edit_items_add_stylebox = nullptr; + Button *edit_items_add_audio = nullptr; Button *edit_items_remove_class = nullptr; Button *edit_items_remove_custom = nullptr; Button *edit_items_remove_all = nullptr; @@ -348,6 +357,7 @@ class ThemeTypeEditor : public MarginContainer { VBoxContainer *font_size_items_list = nullptr; VBoxContainer *icon_items_list = nullptr; VBoxContainer *stylebox_items_list = nullptr; + VBoxContainer *audio_items_list = nullptr; LineEdit *type_variation_edit = nullptr; Button *type_variation_button = nullptr; @@ -392,6 +402,7 @@ class ThemeTypeEditor : public MarginContainer { void _font_item_changed(Ref p_value, String p_item_name); void _icon_item_changed(Ref p_value, String p_item_name); void _stylebox_item_changed(Ref p_value, String p_item_name); + void _audio_item_changed(Ref p_value, String p_item_name); void _change_pinned_stylebox(); void _on_pin_leader_button_pressed(Control *p_editor, String p_item_name); void _pin_leading_stylebox(String p_item_name, Ref p_stylebox); diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index f57afb66b30..6eb7ef7bb99 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -88,6 +88,9 @@ void BaseButton::_notification(int p_what) { switch (p_what) { case NOTIFICATION_MOUSE_ENTER: { status.hovering = true; + if (!status.disabled) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("hover"))); + } queue_redraw(); } break; @@ -134,6 +137,8 @@ void BaseButton::_notification(int p_what) { void BaseButton::_pressed() { GDVIRTUAL_CALL(_pressed); + // TODO: `pressed_disabled` never occurs as this isn't called on disabled buttons. + get_tree()->play_theme_audio(get_theme_audio(is_disabled() ? SNAME("pressed_disabled") : SNAME("pressed"))); pressed(); emit_signal(SNAME("pressed")); } diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 0ca04faf9b1..efaa92440a0 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -214,6 +214,8 @@ void Control::get_argument_options(const StringName &p_function, int p_idx, List ThemeDB::get_singleton()->get_default_theme()->get_font_size_list(get_class(), &sn); } else if (pf == "add_theme_constant_override" || pf == "has_theme_constant" || pf == "has_theme_constant_override" || pf == "get_theme_constant") { ThemeDB::get_singleton()->get_default_theme()->get_constant_list(get_class(), &sn); + } else if (pf == "add_theme_audio_override" || pf == "has_theme_audio" || pf == "has_theme_audio_override" || pf == "get_theme_audio") { + ThemeDB::get_singleton()->get_default_theme()->get_audio_list(get_class(), &sn); } sn.sort_custom(); @@ -301,6 +303,10 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) { String dname = name.get_slicec('/', 1); data.theme_constant_override.erase(dname); _notify_theme_override_changed(); + } else if (name.begins_with("theme_override_audios/")) { + String dname = name.get_slicec('/', 1); + data.theme_audio_override.erase(dname); + _notify_theme_override_changed(); } else { return false; } @@ -324,6 +330,9 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) { } else if (name.begins_with("theme_override_constants/")) { String dname = name.get_slicec('/', 1); add_theme_constant_override(dname, p_value); + } else if (name.begins_with("theme_override_audios/")) { + String dname = name.get_slicec('/', 1); + add_theme_audio_override(dname, p_value); } else { return false; } @@ -356,6 +365,9 @@ bool Control::_get(const StringName &p_name, Variant &r_ret) const { } else if (sname.begins_with("theme_override_constants/")) { String name = sname.get_slicec('/', 1); r_ret = data.theme_constant_override.has(name) ? Variant(data.theme_constant_override[name]) : Variant(); + } else if (sname.begins_with("theme_override_audios/")) { + String name = sname.get_slicec('/', 1); + r_ret = data.theme_audio_override.has(name) ? Variant(data.theme_audio_override[name]) : Variant(); } else { return false; } @@ -441,6 +453,18 @@ void Control::_get_property_list(List *p_list) const { p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("theme_override_styles") + String("/") + E, PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", usage)); } } + { + List names; + default_theme->get_audio_list(get_class_name(), &names); + for (const StringName &E : names) { + uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; + if (data.theme_audio_override.has(E)) { + usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; + } + + p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("theme_override_audios") + String("/") + E, PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", usage)); + } + } } void Control::_validate_property(PropertyInfo &p_property) const { @@ -2441,6 +2465,7 @@ void Control::_invalidate_theme_cache() { data.theme_font_size_cache.clear(); data.theme_color_cache.clear(); data.theme_constant_cache.clear(); + data.theme_audio_cache.clear(); } void Control::_update_theme_item_cache() { @@ -2660,6 +2685,30 @@ int Control::get_theme_constant(const StringName &p_name, const StringName &p_th return constant; } +Ref Control::get_theme_audio(const StringName &p_name, const StringName &p_theme_type) const { + ERR_READ_THREAD_GUARD_V(Ref()); + if (!data.initialized) { + WARN_PRINT_ONCE("Attempting to access theme items too early; prefer NOTIFICATION_POSTINITIALIZE and NOTIFICATION_THEME_CHANGED"); + } + + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { + const Ref *audio = data.theme_audio_override.getptr(p_name); + if (audio) { + return *audio; + } + } + + if (data.theme_audio_cache.has(p_theme_type) && data.theme_audio_cache[p_theme_type].has(p_name)) { + return data.theme_audio_cache[p_theme_type][p_name]; + } + + List theme_types; + data.theme_owner->get_theme_type_dependencies(this, p_theme_type, &theme_types); + Ref audio = data.theme_owner->get_theme_item_in_types(Theme::DATA_TYPE_AUDIOSTREAM, p_name, theme_types); + data.theme_audio_cache[p_theme_type][p_name] = audio; + return audio; +} + bool Control::has_theme_icon(const StringName &p_name, const StringName &p_theme_type) const { ERR_READ_THREAD_GUARD_V(false); if (!data.initialized) { @@ -2762,6 +2811,23 @@ bool Control::has_theme_constant(const StringName &p_name, const StringName &p_t return data.theme_owner->has_theme_item_in_types(Theme::DATA_TYPE_CONSTANT, p_name, theme_types); } +bool Control::has_theme_audio(const StringName &p_name, const StringName &p_theme_type) const { + ERR_READ_THREAD_GUARD_V(false); + if (!data.initialized) { + WARN_PRINT_ONCE("Attempting to access theme items too early; prefer NOTIFICATION_POSTINITIALIZE and NOTIFICATION_THEME_CHANGED"); + } + + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { + if (has_theme_audio_override(p_name)) { + return true; + } + } + + List theme_types; + data.theme_owner->get_theme_type_dependencies(this, p_theme_type, &theme_types); + return data.theme_owner->has_theme_item_in_types(Theme::DATA_TYPE_AUDIOSTREAM, p_name, theme_types); +} + /// Local property overrides. void Control::add_theme_icon_override(const StringName &p_name, const Ref &p_icon) { @@ -2821,6 +2887,12 @@ void Control::add_theme_constant_override(const StringName &p_name, int p_consta _notify_theme_override_changed(); } +void Control::add_theme_audio_override(const StringName &p_name, const Ref &p_audio) { + ERR_MAIN_THREAD_GUARD; + data.theme_audio_override[p_name] = p_audio; + _notify_theme_override_changed(); +} + void Control::remove_theme_icon_override(const StringName &p_name) { ERR_MAIN_THREAD_GUARD; if (data.theme_icon_override.has(p_name)) { @@ -2869,6 +2941,12 @@ void Control::remove_theme_constant_override(const StringName &p_name) { _notify_theme_override_changed(); } +void Control::remove_theme_audio_override(const StringName &p_name) { + ERR_MAIN_THREAD_GUARD; + data.theme_audio_override.erase(p_name); + _notify_theme_override_changed(); +} + bool Control::has_theme_icon_override(const StringName &p_name) const { ERR_READ_THREAD_GUARD_V(false); const Ref *tex = data.theme_icon_override.getptr(p_name); @@ -2905,6 +2983,12 @@ bool Control::has_theme_constant_override(const StringName &p_name) const { return constant != nullptr; } +bool Control::has_theme_audio_override(const StringName &p_name) const { + ERR_READ_THREAD_GUARD_V(false); + const Ref *audio = data.theme_audio_override.getptr(p_name); + return audio != nullptr; +} + /// Default theme properties. float Control::get_theme_default_base_scale() const { @@ -3351,6 +3435,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("add_theme_font_size_override", "name", "font_size"), &Control::add_theme_font_size_override); ClassDB::bind_method(D_METHOD("add_theme_color_override", "name", "color"), &Control::add_theme_color_override); ClassDB::bind_method(D_METHOD("add_theme_constant_override", "name", "constant"), &Control::add_theme_constant_override); + ClassDB::bind_method(D_METHOD("add_theme_audio_override", "name", "audio"), &Control::add_theme_audio_override); ClassDB::bind_method(D_METHOD("remove_theme_icon_override", "name"), &Control::remove_theme_icon_override); ClassDB::bind_method(D_METHOD("remove_theme_stylebox_override", "name"), &Control::remove_theme_style_override); @@ -3358,6 +3443,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_theme_font_size_override", "name"), &Control::remove_theme_font_size_override); ClassDB::bind_method(D_METHOD("remove_theme_color_override", "name"), &Control::remove_theme_color_override); ClassDB::bind_method(D_METHOD("remove_theme_constant_override", "name"), &Control::remove_theme_constant_override); + ClassDB::bind_method(D_METHOD("remove_theme_audio_override", "name"), &Control::remove_theme_audio_override); ClassDB::bind_method(D_METHOD("get_theme_icon", "name", "theme_type"), &Control::get_theme_icon, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_theme_stylebox", "name", "theme_type"), &Control::get_theme_stylebox, DEFVAL("")); @@ -3365,6 +3451,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_theme_font_size", "name", "theme_type"), &Control::get_theme_font_size, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_theme_color", "name", "theme_type"), &Control::get_theme_color, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_theme_constant", "name", "theme_type"), &Control::get_theme_constant, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_audio", "name", "theme_type"), &Control::get_theme_audio, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_icon_override", "name"), &Control::has_theme_icon_override); ClassDB::bind_method(D_METHOD("has_theme_stylebox_override", "name"), &Control::has_theme_stylebox_override); @@ -3372,6 +3459,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("has_theme_font_size_override", "name"), &Control::has_theme_font_size_override); ClassDB::bind_method(D_METHOD("has_theme_color_override", "name"), &Control::has_theme_color_override); ClassDB::bind_method(D_METHOD("has_theme_constant_override", "name"), &Control::has_theme_constant_override); + ClassDB::bind_method(D_METHOD("has_theme_audio_override", "name"), &Control::has_theme_audio_override); ClassDB::bind_method(D_METHOD("has_theme_icon", "name", "theme_type"), &Control::has_theme_icon, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_stylebox", "name", "theme_type"), &Control::has_theme_stylebox, DEFVAL("")); @@ -3379,6 +3467,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("has_theme_font_size", "name", "theme_type"), &Control::has_theme_font_size, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_color", "name", "theme_type"), &Control::has_theme_color, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_constant", "name", "theme_type"), &Control::has_theme_constant, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_audio", "name", "theme_type"), &Control::has_theme_audio, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_theme_default_base_scale"), &Control::get_theme_default_base_scale); ClassDB::bind_method(D_METHOD("get_theme_default_font"), &Control::get_theme_default_font); @@ -3646,4 +3735,5 @@ Control::~Control() { data.theme_font_size_override.clear(); data.theme_color_override.clear(); data.theme_constant_override.clear(); + data.theme_audio_override.clear(); } diff --git a/scene/gui/control.h b/scene/gui/control.h index 7cb8fc5bf6a..44a0b1934bd 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -238,6 +238,7 @@ class Control : public CanvasItem { Theme::ThemeFontSizeMap theme_font_size_override; Theme::ThemeColorMap theme_color_override; Theme::ThemeConstantMap theme_constant_override; + Theme::ThemeAudioMap theme_audio_override; mutable HashMap theme_icon_cache; mutable HashMap theme_style_cache; @@ -245,6 +246,7 @@ class Control : public CanvasItem { mutable HashMap theme_font_size_cache; mutable HashMap theme_color_cache; mutable HashMap theme_constant_cache; + mutable HashMap theme_audio_cache; // Internationalization. @@ -568,6 +570,7 @@ class Control : public CanvasItem { void add_theme_font_size_override(const StringName &p_name, int p_font_size); void add_theme_color_override(const StringName &p_name, const Color &p_color); void add_theme_constant_override(const StringName &p_name, int p_constant); + void add_theme_audio_override(const StringName &p_name, const Ref &p_audio); void remove_theme_icon_override(const StringName &p_name); void remove_theme_style_override(const StringName &p_name); @@ -575,6 +578,7 @@ class Control : public CanvasItem { void remove_theme_font_size_override(const StringName &p_name); void remove_theme_color_override(const StringName &p_name); void remove_theme_constant_override(const StringName &p_name); + void remove_theme_audio_override(const StringName &p_name); Ref get_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const; Ref get_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const; @@ -582,6 +586,7 @@ class Control : public CanvasItem { int get_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const; Color get_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const; int get_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + Ref get_theme_audio(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_icon_override(const StringName &p_name) const; bool has_theme_stylebox_override(const StringName &p_name) const; @@ -589,6 +594,7 @@ class Control : public CanvasItem { bool has_theme_font_size_override(const StringName &p_name) const; bool has_theme_color_override(const StringName &p_name) const; bool has_theme_constant_override(const StringName &p_name) const; + bool has_theme_audio_override(const StringName &p_name) const; bool has_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const; @@ -596,6 +602,7 @@ class Control : public CanvasItem { bool has_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + bool has_theme_audio(const StringName &p_name, const StringName &p_theme_type = StringName()) const; float get_theme_default_base_scale() const; Ref get_theme_default_font() const; diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index b273f709f2b..7958043212f 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -695,6 +695,7 @@ void ItemList::gui_input(const Ref &p_event) { emit_signal(SNAME("multi_selected"), j, true); } } + get_tree()->play_theme_audio(get_theme_audio(items[i].disabled ? SNAME("item_disabled_selected") : SNAME("item_selected"))); emit_signal(SNAME("item_clicked"), i, get_local_mouse_position(), mb->get_button_index()); } else { @@ -705,6 +706,7 @@ void ItemList::gui_input(const Ref &p_event) { if (!items[i].selected || allow_reselect) { select(i, select_mode == SELECT_SINGLE || !mb->is_command_or_control_pressed()); + get_tree()->play_theme_audio(get_theme_audio(items[i].disabled ? SNAME("item_disabled_selected") : SNAME("item_selected"))); if (select_mode == SELECT_SINGLE) { emit_signal(SNAME("item_selected"), i); @@ -722,6 +724,7 @@ void ItemList::gui_input(const Ref &p_event) { return; } else if (closest != -1) { + get_tree()->play_theme_audio(get_theme_audio(items[closest].disabled ? SNAME("item_disabled_selected") : SNAME("item_selected"))); emit_signal(SNAME("item_clicked"), closest, get_local_mouse_position(), mb->get_button_index()); } else { // Since closest is null, more likely we clicked on empty space, so send signal to interested controls. Allows, for example, implement items deselecting. diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 42ee6cb0bcd..4b12751df9f 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -474,6 +474,7 @@ void LineEdit::gui_input(const Ref &p_event) { // Default is ENTER and KP_ENTER. Cannot use ui_accept as default includes SPACE. if (k->is_action("ui_text_submit", false)) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("text_submitted"))); emit_signal(SNAME("text_submitted"), text); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); @@ -1752,6 +1753,7 @@ void LineEdit::insert_text_at_caret(String p_text) { // Truncate text to append to fit in max_length, if needed. int available_chars = max_length - text.length(); if (p_text.length() > available_chars) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("text_change_rejected"))); emit_signal(SNAME("text_change_rejected"), p_text.substr(available_chars)); p_text = p_text.substr(0, available_chars); } @@ -2277,6 +2279,10 @@ void LineEdit::_text_changed() { } void LineEdit::_emit_text_change() { + if (get_tree()) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("text_changed"))); + } + emit_signal(SNAME("text_changed"), text); text_changed_dirty = false; } diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 40db8deaace..20506f32241 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -459,7 +459,13 @@ void PopupMenu::gui_input(const Ref &p_event) { return; } - if (items[over].separator || items[over].disabled) { + if (items[over].separator) { + return; + } + + get_tree()->play_theme_audio(get_theme_audio(items[over].disabled ? SNAME("item_disabled_activated") : SNAME("item_activated"))); + + if (items[over].disabled) { return; } diff --git a/scene/gui/slider.cpp b/scene/gui/slider.cpp index 398f637e856..0083f8c1e9f 100644 --- a/scene/gui/slider.cpp +++ b/scene/gui/slider.cpp @@ -75,19 +75,23 @@ void Slider::gui_input(const Ref &p_event) { grab.active = true; grab.uvalue = get_as_ratio(); + get_tree()->play_theme_audio(get_theme_audio(SNAME("drag_started"))); emit_signal(SNAME("drag_started")); } else { grab.active = false; const bool value_changed = !Math::is_equal_approx((double)grab.uvalue, get_as_ratio()); + get_tree()->play_theme_audio(get_theme_audio(SNAME("drag_ended"))); emit_signal(SNAME("drag_ended"), value_changed); } } else if (scrollable) { if (mb->is_pressed() && mb->get_button_index() == MouseButton::WHEEL_UP) { grab_focus(); + get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed"))); set_value(get_value() + get_step()); } else if (mb->is_pressed() && mb->get_button_index() == MouseButton::WHEEL_DOWN) { grab_focus(); + get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed"))); set_value(get_value() - get_step()); } } @@ -128,6 +132,7 @@ void Slider::gui_input(const Ref &p_event) { } set_process_internal(true); } + get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed"))); set_value(get_value() - (custom_step >= 0 ? custom_step : get_step())); accept_event(); } else if (p_event->is_action_pressed("ui_right", true)) { @@ -140,6 +145,7 @@ void Slider::gui_input(const Ref &p_event) { } set_process_internal(true); } + get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed"))); set_value(get_value() + (custom_step >= 0 ? custom_step : get_step())); accept_event(); } else if (p_event->is_action_pressed("ui_up", true)) { @@ -152,6 +158,7 @@ void Slider::gui_input(const Ref &p_event) { } set_process_internal(true); } + get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed"))); set_value(get_value() + (custom_step >= 0 ? custom_step : get_step())); accept_event(); } else if (p_event->is_action_pressed("ui_down", true)) { @@ -164,12 +171,15 @@ void Slider::gui_input(const Ref &p_event) { } set_process_internal(true); } + get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed"))); set_value(get_value() - (custom_step >= 0 ? custom_step : get_step())); accept_event(); } else if (p_event->is_action("ui_home", true) && p_event->is_pressed()) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed"))); set_value(get_min()); accept_event(); } else if (p_event->is_action("ui_end", true) && p_event->is_pressed()) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed"))); set_value(get_max()); accept_event(); } diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp index 7cb54f24ea9..9957e374cd7 100644 --- a/scene/gui/spin_box.cpp +++ b/scene/gui/spin_box.cpp @@ -96,6 +96,7 @@ void SpinBox::_range_click_timeout() { bool up = get_local_mouse_position().y < (get_size().height / 2); double step = get_custom_arrow_step() != 0.0 ? get_custom_arrow_step() : get_step(); set_value(get_value() + (up ? step : -step)); + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); if (range_click_timer->is_one_shot()) { range_click_timer->set_wait_time(0.075); @@ -136,6 +137,7 @@ void SpinBox::gui_input(const Ref &p_event) { line_edit->grab_focus(); set_value(get_value() + (up ? step : -step)); + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); range_click_timer->set_wait_time(0.6); range_click_timer->set_one_shot(true); @@ -147,16 +149,19 @@ void SpinBox::gui_input(const Ref &p_event) { case MouseButton::RIGHT: { line_edit->grab_focus(); set_value((up ? get_max() : get_min())); + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); } break; case MouseButton::WHEEL_UP: { if (line_edit->has_focus()) { set_value(get_value() + step * mb->get_factor()); + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); accept_event(); } } break; case MouseButton::WHEEL_DOWN: { if (line_edit->has_focus()) { set_value(get_value() - step * mb->get_factor()); + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); accept_event(); } } break; diff --git a/scene/gui/tab_bar.cpp b/scene/gui/tab_bar.cpp index 959a51eff91..bae8dc88a41 100644 --- a/scene/gui/tab_bar.cpp +++ b/scene/gui/tab_bar.cpp @@ -188,6 +188,7 @@ void TabBar::gui_input(const Ref &p_event) { if (rb_pressing && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { if (rb_hover != -1) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); emit_signal(SNAME("tab_button_pressed"), rb_hover); } @@ -197,6 +198,7 @@ void TabBar::gui_input(const Ref &p_event) { if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { if (cb_hover != -1) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); emit_signal(SNAME("tab_close_pressed"), cb_hover); } @@ -212,15 +214,21 @@ void TabBar::gui_input(const Ref &p_event) { if (pos.x < theme_cache.decrement_icon->get_width()) { if (missing_right) { offset++; + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); _update_cache(); queue_redraw(); + } else { + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed_disabled"))); } return; } else if (pos.x < theme_cache.increment_icon->get_width() + theme_cache.decrement_icon->get_width()) { if (offset > 0) { offset--; + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); _update_cache(); queue_redraw(); + } else { + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed_disabled"))); } return; } @@ -229,15 +237,21 @@ void TabBar::gui_input(const Ref &p_event) { if (pos.x > limit + theme_cache.decrement_icon->get_width()) { if (missing_right) { offset++; + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); _update_cache(); queue_redraw(); + } else { + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed_disabled"))); } return; } else if (pos.x > limit) { if (offset > 0) { offset--; + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); _update_cache(); queue_redraw(); + } else { + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed_disabled"))); } return; } @@ -278,6 +292,10 @@ void TabBar::gui_input(const Ref &p_event) { } if (found != -1) { + if (current != found) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed"))); + } + set_current_tab(found); if (mb->get_button_index() == MouseButton::RIGHT) { @@ -879,6 +897,9 @@ void TabBar::_update_hover() { hover = hover_now; if (hover != -1) { + if (!is_tab_disabled(hover)) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("hover"))); + } emit_signal(SNAME("tab_hovered"), hover); } diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 3b2013f7ecf..946b7502caf 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -7611,6 +7611,7 @@ Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) { /*** Super internal Core API. Everything builds on it. ***/ void TextEdit::_text_changed_emit() { + get_tree()->play_theme_audio(get_theme_audio(SNAME("text_changed"))); emit_signal(SNAME("text_changed")); text_changed_dirty = false; } diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index 433ae656ba3..c1d009ccc7a 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -2626,6 +2626,7 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c selected_item = p_selected; selected_col = 0; if (!emitted_row) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("item_selected"))); emit_signal(SNAME("item_selected")); emitted_row = true; } @@ -2643,6 +2644,10 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c selected_item = p_selected; selected_col = i; + if (get_tree()) { + get_tree()->play_theme_audio(get_theme_audio(SNAME("item_selected"))); + } + emit_signal(SNAME("cell_selected")); if (select_mode == SELECT_MULTI) { emit_signal(SNAME("multi_selected"), p_current, i, true); diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index db9c1efa68e..cb2f2291d6c 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -44,6 +44,7 @@ #include "core/string/print_string.h" #include "node.h" #include "scene/animation/tween.h" +#include "scene/audio/audio_stream_player.h" #include "scene/debugger/scene_debugger.h" #include "scene/gui/control.h" #include "scene/main/multiplayer_api.h" @@ -117,6 +118,79 @@ void SceneTreeTimer::release_connections() { SceneTreeTimer::SceneTreeTimer() {} +// void SceneTreeAudioStreamPlayer::_bind_methods() { +// ClassDB::bind_method(D_METHOD("set_stream", "stream"), &SceneTreeAudioStreamPlayer::set_stream); +// ClassDB::bind_method(D_METHOD("get_stream"), &SceneTreeAudioStreamPlayer::get_stream); + +// ClassDB::bind_method(D_METHOD("get_stream_playback"), &SceneTreeAudioStreamPlayer::get_stream_playback); + +// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); + +// ADD_SIGNAL(MethodInfo("finished")); +// } + +// void SceneTreeAudioStreamPlayer::set_stream(Ref p_stream) { +// stream = p_stream; + +// if (stream.is_valid()) { +// stream_playback = stream->instantiate_playback(); +// } + +// if (stream_playback.is_null()) { +// return; +// } + +// // TODO: Add a parameter to control output bus. + +// Vector volume_vector; +// // We need at most four stereo pairs (for 7.1 systems). +// volume_vector.resize(4); + +// // Initialize the volume vector to zero. +// for (AudioFrame &channel_volume_db : volume_vector) { +// channel_volume_db = AudioFrame(0, 0); +// } + +// // TODO: Add option to change volume dB. +// float volume_linear = Math::db_to_linear(0.0); + +// // Set the volume vector up according to the speaker mode and mix target. +// // TODO do we need to scale the volume down when we output to more channels? +// if (AudioServer::get_singleton()->get_speaker_mode() == AudioServer::SPEAKER_MODE_STEREO) { +// volume_vector.write[0] = AudioFrame(volume_linear, volume_linear); +// } else { +// switch (1) { +// case 1: { +// volume_vector.write[0] = AudioFrame(volume_linear, volume_linear); +// } break; +// case 2: { +// // TODO Make sure this is right. +// volume_vector.write[0] = AudioFrame(volume_linear, volume_linear); +// volume_vector.write[1] = AudioFrame(volume_linear, /* LFE= */ 1.0f); +// volume_vector.write[2] = AudioFrame(volume_linear, volume_linear); +// volume_vector.write[3] = AudioFrame(volume_linear, volume_linear); +// } break; +// case 3: { +// // TODO Make sure this is right. +// volume_vector.write[1] = AudioFrame(volume_linear, /* LFE= */ 1.0f); +// } break; +// } +// } + +// AudioServer::get_singleton()->start_playback_stream(stream_playback, SNAME("Master"), volume_vector); +// //active.set(); +// } + +// Ref SceneTreeAudioStreamPlayer::get_stream() const { +// return stream; +// } + +// Ref SceneTreeAudioStreamPlayer::get_stream_playback() const { +// return stream_playback; +// } + +// SceneTreeAudioStreamPlayer::SceneTreeAudioStreamPlayer() {} + void SceneTree::tree_changed() { tree_version++; emit_signal(tree_changed_name); @@ -519,6 +593,8 @@ bool SceneTree::process(double p_time) { process_tweens(p_time, false); + // process_audio_stream_players(p_time); + flush_transform_notifications(); //additional transforms after timers update _call_idle_callbacks(); @@ -617,6 +693,40 @@ void SceneTree::process_tweens(double p_delta, bool p_physics) { } } +// void SceneTree::process_audio_stream_players(double p_delta) { +// _THREAD_SAFE_METHOD_ +// // This method works similarly to how SceneTreeTimers are handled. +// List>::Element *L = audio_stream_players.back(); + +// for (List>::Element *E = audio_stream_players.front(); E;) { +// List>::Element *N = E->next(); +// Vector> playbacks_to_remove; +// // for (Ref &playback : stream_playbacks) { +// // if (playback.is_valid() && !AudioServer::get_singleton()->is_playback_active(playback) && !AudioServer::get_singleton()->is_playback_paused(playback)) { +// // playbacks_to_remove.push_back(playback); +// // } +// // } +// // // Now go through and remove playbacks that have finished. Removing elements from a Vector in a range based for is asking for trouble. +// // for (Ref &playback : playbacks_to_remove) { +// // stream_playbacks.erase(playback); +// // } +// if (E->get()->get_stream_playback().is_valid() && AudioServer::get_singleton()->is_playback_active(E->get()->get_stream_playback())) { +// print_line("Freeing audio"); +// // This node is no longer actively playing audio. +// //active.clear(); +// //set_process_internal(false); +// E->get()->emit_signal(SNAME("finished")); +// AudioServer::get_singleton()->stop_playback_stream(E->get()->get_stream_playback()); +// audio_stream_players.erase(E); +// } +// if (E == L) { +// // Break on last, so if new timers were added during list traversal, ignore them. +// break; +// } +// E = N; +// } +// } + void SceneTree::finalize() { _flush_delete_queue(); @@ -1467,6 +1577,41 @@ Ref SceneTree::create_tween() { return tween; } +// Ref SceneTree::create_audio_stream_player(Ref p_stream) { +// _THREAD_SAFE_METHOD_ +// Ref stasp; +// stasp.instantiate(); +// stasp->set_stream(p_stream); +// audio_stream_players.push_back(stasp); +// return stasp; +// } + +// Helper method that automatically sets the bus based on the project setting. +AudioStreamPlayer *SceneTree::play_theme_audio(const Ref &p_stream) { + return create_audio_stream_player(p_stream, gui_theme_bus); +} + +AudioStreamPlayer *SceneTree::create_audio_stream_player(const Ref &p_stream, const StringName &p_bus, float p_volume_db) { + _THREAD_SAFE_METHOD_ + // TODO: Early return if using a bare AudioStream resource + // (useful to mute specific sounds with theme overrides without spamming errors). + if (p_stream.is_null()) { + return nullptr; + } + + AudioStreamPlayer *asp = memnew(AudioStreamPlayer); + asp->connect(SceneStringNames::get_singleton()->finished, callable_mp(this, &SceneTree::_on_audio_finished).bind(asp)); + asp->set_bus(p_bus); + asp->set_stream(p_stream); + asp->set_autoplay(true); + root->add_child(asp, false, Node::INTERNAL_MODE_BACK); + return asp; +} + +void SceneTree::_on_audio_finished(AudioStreamPlayer *p_player) { + p_player->queue_free(); +} + TypedArray SceneTree::get_processed_tweens() { _THREAD_SAFE_METHOD_ TypedArray ret; @@ -1561,6 +1706,8 @@ void SceneTree::_bind_methods() { ClassDB::bind_method(D_METHOD("create_timer", "time_sec", "process_always", "process_in_physics", "ignore_time_scale"), &SceneTree::create_timer, DEFVAL(true), DEFVAL(false), DEFVAL(false)); ClassDB::bind_method(D_METHOD("create_tween"), &SceneTree::create_tween); ClassDB::bind_method(D_METHOD("get_processed_tweens"), &SceneTree::get_processed_tweens); + ClassDB::bind_method(D_METHOD("create_audio_stream_player", "stream", "bus", "volume_db"), &SceneTree::create_audio_stream_player, DEFVAL(SNAME("Master")), DEFVAL(0.0f)); + ClassDB::bind_method(D_METHOD("play_theme_audio", "stream"), &SceneTree::play_theme_audio); ClassDB::bind_method(D_METHOD("get_node_count"), &SceneTree::get_node_count); ClassDB::bind_method(D_METHOD("get_frame"), &SceneTree::get_frame); @@ -1693,6 +1840,7 @@ SceneTree::SceneTree() { debug_paths_color = GLOBAL_DEF("debug/shapes/paths/geometry_color", Color(0.1, 1.0, 0.7, 0.4)); debug_paths_width = GLOBAL_DEF("debug/shapes/paths/geometry_width", 2.0); collision_debug_contacts = GLOBAL_DEF(PropertyInfo(Variant::INT, "debug/shapes/collision/max_contacts_displayed", PROPERTY_HINT_RANGE, "0,20000,1"), 10000); + gui_theme_bus = GLOBAL_GET("audio/buses/gui_theme_bus"); GLOBAL_DEF("debug/shapes/collision/draw_2d_outlines", true); diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h index e1597d3890a..520b251d2c9 100644 --- a/scene/main/scene_tree.h +++ b/scene/main/scene_tree.h @@ -41,6 +41,9 @@ class PackedScene; class Node; +class AudioStream; +class AudioStreamPlayback; +class AudioStreamPlayer; class Window; class Material; class Mesh; @@ -78,6 +81,24 @@ class SceneTreeTimer : public RefCounted { SceneTreeTimer(); }; +// class SceneTreeAudioStreamPlayer : public RefCounted { +// GDCLASS(SceneTreeAudioStreamPlayer, RefCounted); + +// Ref stream; +// Ref stream_playback; + +// protected: +// static void _bind_methods(); + +// public: +// void set_stream(Ref p_stream); +// Ref get_stream() const; + +// Ref get_stream_playback() const; + +// SceneTreeAudioStreamPlayer(); +// }; + class SceneTree : public MainLoop { _THREAD_SAFE_CLASS_ @@ -189,11 +210,13 @@ class SceneTree : public MainLoop { Ref debug_paths_material; Ref collision_material; int collision_debug_contacts; + StringName gui_theme_bus; void _flush_scene_change(); List> timers; List> tweens; + // List> audio_stream_players; ///network/// @@ -210,6 +233,7 @@ class SceneTree : public MainLoop { void node_renamed(Node *p_node); void process_timers(double p_delta, bool p_physics_frame); void process_tweens(double p_delta, bool p_physics_frame); + // void process_audio_stream_players(double p_delta); Group *add_to_group(const StringName &p_group, Node *p_node); void remove_from_group(const StringName &p_group, Node *p_node); @@ -401,6 +425,10 @@ class SceneTree : public MainLoop { Ref create_timer(double p_delay_sec, bool p_process_always = true, bool p_process_in_physics = false, bool p_ignore_time_scale = false); Ref create_tween(); + // Ref create_audio_stream_player(Ref p_stream); + AudioStreamPlayer *play_theme_audio(const Ref &p_stream); + AudioStreamPlayer *create_audio_stream_player(const Ref &p_stream, const StringName &p_bus = SNAME("Master"), float p_volume_db = 0.0f); + void _on_audio_finished(AudioStreamPlayer *p_player); TypedArray get_processed_tweens(); //used by Main::start, don't use otherwise diff --git a/scene/main/window.cpp b/scene/main/window.cpp index d0658c489d9..1ac74b40d09 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -84,6 +84,10 @@ bool Window::_set(const StringName &p_name, const Variant &p_value) { String dname = name.get_slicec('/', 1); theme_constant_override.erase(dname); _notify_theme_override_changed(); + } else if (name.begins_with("theme_override_audios/")) { + String dname = name.get_slicec('/', 1); + theme_audio_override.erase(dname); + _notify_theme_override_changed(); } else { return false; } @@ -107,6 +111,9 @@ bool Window::_set(const StringName &p_name, const Variant &p_value) { } else if (name.begins_with("theme_override_constants/")) { String dname = name.get_slicec('/', 1); add_theme_constant_override(dname, p_value); + } else if (name.begins_with("theme_override_audios/")) { + String dname = name.get_slicec('/', 1); + add_theme_audio_override(dname, p_value); } else { return false; } @@ -140,6 +147,9 @@ bool Window::_get(const StringName &p_name, Variant &r_ret) const { } else if (sname.begins_with("theme_override_constants/")) { String name = sname.get_slicec('/', 1); r_ret = theme_constant_override.has(name) ? Variant(theme_constant_override[name]) : Variant(); + } else if (sname.begins_with("theme_override_audios/")) { + String name = sname.get_slicec('/', 1); + r_ret = theme_audio_override.has(name) ? Variant(theme_audio_override[name]) : Variant(); } else { return false; } @@ -226,6 +236,18 @@ void Window::_get_property_list(List *p_list) const { p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("theme_override_styles") + String("/") + E, PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", usage)); } } + { + List names; + default_theme->get_stylebox_list(get_class_name(), &names); + for (const StringName &E : names) { + uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; + if (theme_style_override.has(E)) { + usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; + } + + p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("theme_override_audios") + String("/") + E, PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", usage)); + } + } } void Window::_validate_property(PropertyInfo &p_property) const { @@ -1882,6 +1904,7 @@ void Window::_invalidate_theme_cache() { theme_font_size_cache.clear(); theme_color_cache.clear(); theme_constant_cache.clear(); + theme_audio_cache.clear(); } void Window::_update_theme_item_cache() { @@ -2066,6 +2089,30 @@ int Window::get_theme_constant(const StringName &p_name, const StringName &p_the return constant; } +Ref Window::get_theme_audio(const StringName &p_name, const StringName &p_theme_type) const { + ERR_READ_THREAD_GUARD_V(Ref()); + if (!initialized) { + WARN_PRINT_ONCE("Attempting to access theme items too early; prefer NOTIFICATION_POSTINITIALIZE and NOTIFICATION_THEME_CHANGED"); + } + + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == theme_type_variation) { + const Ref *audio = theme_audio_override.getptr(p_name); + if (audio) { + return *audio; + } + } + + if (theme_audio_cache.has(p_theme_type) && theme_audio_cache[p_theme_type].has(p_name)) { + return theme_audio_cache[p_theme_type][p_name]; + } + + List theme_types; + theme_owner->get_theme_type_dependencies(this, p_theme_type, &theme_types); + Ref audio = theme_owner->get_theme_item_in_types(Theme::DATA_TYPE_AUDIOSTREAM, p_name, theme_types); + theme_audio_cache[p_theme_type][p_name] = audio; + return audio; +} + bool Window::has_theme_icon(const StringName &p_name, const StringName &p_theme_type) const { ERR_READ_THREAD_GUARD_V(false); if (!initialized) { @@ -2168,6 +2215,23 @@ bool Window::has_theme_constant(const StringName &p_name, const StringName &p_th return theme_owner->has_theme_item_in_types(Theme::DATA_TYPE_CONSTANT, p_name, theme_types); } +bool Window::has_theme_audio(const StringName &p_name, const StringName &p_theme_type) const { + ERR_READ_THREAD_GUARD_V(false); + if (!initialized) { + WARN_PRINT_ONCE("Attempting to access theme items too early; prefer NOTIFICATION_POSTINITIALIZE and NOTIFICATION_THEME_CHANGED"); + } + + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == theme_type_variation) { + if (has_theme_audio_override(p_name)) { + return true; + } + } + + List theme_types; + theme_owner->get_theme_type_dependencies(this, p_theme_type, &theme_types); + return theme_owner->has_theme_item_in_types(Theme::DATA_TYPE_AUDIOSTREAM, p_name, theme_types); +} + /// Local property overrides. void Window::add_theme_icon_override(const StringName &p_name, const Ref &p_icon) { @@ -2227,6 +2291,12 @@ void Window::add_theme_constant_override(const StringName &p_name, int p_constan _notify_theme_override_changed(); } +void Window::add_theme_audio_override(const StringName &p_name, const Ref &p_audio) { + ERR_MAIN_THREAD_GUARD; + theme_audio_override[p_name] = p_audio; + _notify_theme_override_changed(); +} + void Window::remove_theme_icon_override(const StringName &p_name) { ERR_MAIN_THREAD_GUARD; if (theme_icon_override.has(p_name)) { @@ -2275,6 +2345,12 @@ void Window::remove_theme_constant_override(const StringName &p_name) { _notify_theme_override_changed(); } +void Window::remove_theme_audio_override(const StringName &p_name) { + ERR_MAIN_THREAD_GUARD; + theme_audio_override.erase(p_name); + _notify_theme_override_changed(); +} + bool Window::has_theme_icon_override(const StringName &p_name) const { ERR_READ_THREAD_GUARD_V(false); const Ref *tex = theme_icon_override.getptr(p_name); @@ -2311,6 +2387,12 @@ bool Window::has_theme_constant_override(const StringName &p_name) const { return constant != nullptr; } +bool Window::has_theme_audio_override(const StringName &p_name) const { + ERR_READ_THREAD_GUARD_V(false); + const Ref *audio = theme_audio_override.getptr(p_name); + return audio != nullptr; +} + /// Default theme properties. float Window::get_theme_default_base_scale() const { @@ -2614,6 +2696,7 @@ void Window::_bind_methods() { ClassDB::bind_method(D_METHOD("add_theme_font_size_override", "name", "font_size"), &Window::add_theme_font_size_override); ClassDB::bind_method(D_METHOD("add_theme_color_override", "name", "color"), &Window::add_theme_color_override); ClassDB::bind_method(D_METHOD("add_theme_constant_override", "name", "constant"), &Window::add_theme_constant_override); + ClassDB::bind_method(D_METHOD("add_theme_audio_override", "name", "audio"), &Window::add_theme_audio_override); ClassDB::bind_method(D_METHOD("remove_theme_icon_override", "name"), &Window::remove_theme_icon_override); ClassDB::bind_method(D_METHOD("remove_theme_stylebox_override", "name"), &Window::remove_theme_style_override); @@ -2621,6 +2704,7 @@ void Window::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_theme_font_size_override", "name"), &Window::remove_theme_font_size_override); ClassDB::bind_method(D_METHOD("remove_theme_color_override", "name"), &Window::remove_theme_color_override); ClassDB::bind_method(D_METHOD("remove_theme_constant_override", "name"), &Window::remove_theme_constant_override); + ClassDB::bind_method(D_METHOD("remove_theme_audio_override", "name"), &Window::remove_theme_audio_override); ClassDB::bind_method(D_METHOD("get_theme_icon", "name", "theme_type"), &Window::get_theme_icon, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_theme_stylebox", "name", "theme_type"), &Window::get_theme_stylebox, DEFVAL("")); @@ -2628,6 +2712,7 @@ void Window::_bind_methods() { ClassDB::bind_method(D_METHOD("get_theme_font_size", "name", "theme_type"), &Window::get_theme_font_size, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_theme_color", "name", "theme_type"), &Window::get_theme_color, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_theme_constant", "name", "theme_type"), &Window::get_theme_constant, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_audio", "name", "theme_type"), &Window::get_theme_audio, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_icon_override", "name"), &Window::has_theme_icon_override); ClassDB::bind_method(D_METHOD("has_theme_stylebox_override", "name"), &Window::has_theme_stylebox_override); @@ -2635,6 +2720,7 @@ void Window::_bind_methods() { ClassDB::bind_method(D_METHOD("has_theme_font_size_override", "name"), &Window::has_theme_font_size_override); ClassDB::bind_method(D_METHOD("has_theme_color_override", "name"), &Window::has_theme_color_override); ClassDB::bind_method(D_METHOD("has_theme_constant_override", "name"), &Window::has_theme_constant_override); + ClassDB::bind_method(D_METHOD("has_theme_audio_override", "name"), &Window::has_theme_audio_override); ClassDB::bind_method(D_METHOD("has_theme_icon", "name", "theme_type"), &Window::has_theme_icon, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_stylebox", "name", "theme_type"), &Window::has_theme_stylebox, DEFVAL("")); @@ -2642,6 +2728,7 @@ void Window::_bind_methods() { ClassDB::bind_method(D_METHOD("has_theme_font_size", "name", "theme_type"), &Window::has_theme_font_size, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_color", "name", "theme_type"), &Window::has_theme_color, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_constant", "name", "theme_type"), &Window::has_theme_constant, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_audio", "name", "theme_type"), &Window::has_theme_audio, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_theme_default_base_scale"), &Window::get_theme_default_base_scale); ClassDB::bind_method(D_METHOD("get_theme_default_font"), &Window::get_theme_default_font); @@ -2800,4 +2887,5 @@ Window::~Window() { theme_font_size_override.clear(); theme_color_override.clear(); theme_constant_override.clear(); + theme_audio_override.clear(); } diff --git a/scene/main/window.h b/scene/main/window.h index 18ddd896625..594d7b40fcd 100644 --- a/scene/main/window.h +++ b/scene/main/window.h @@ -172,6 +172,7 @@ class Window : public Viewport { Theme::ThemeFontSizeMap theme_font_size_override; Theme::ThemeColorMap theme_color_override; Theme::ThemeConstantMap theme_constant_override; + Theme::ThemeAudioMap theme_audio_override; mutable HashMap theme_icon_cache; mutable HashMap theme_style_cache; @@ -179,6 +180,7 @@ class Window : public Viewport { mutable HashMap theme_font_size_cache; mutable HashMap theme_color_cache; mutable HashMap theme_constant_cache; + mutable HashMap theme_audio_cache; void _theme_changed(); void _notify_theme_override_changed(); @@ -366,6 +368,7 @@ class Window : public Viewport { void add_theme_font_size_override(const StringName &p_name, int p_font_size); void add_theme_color_override(const StringName &p_name, const Color &p_color); void add_theme_constant_override(const StringName &p_name, int p_constant); + void add_theme_audio_override(const StringName &p_name, const Ref &p_audio); void remove_theme_icon_override(const StringName &p_name); void remove_theme_style_override(const StringName &p_name); @@ -373,6 +376,7 @@ class Window : public Viewport { void remove_theme_font_size_override(const StringName &p_name); void remove_theme_color_override(const StringName &p_name); void remove_theme_constant_override(const StringName &p_name); + void remove_theme_audio_override(const StringName &p_name); Ref get_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const; Ref get_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const; @@ -380,6 +384,7 @@ class Window : public Viewport { int get_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const; Color get_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const; int get_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + Ref get_theme_audio(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_icon_override(const StringName &p_name) const; bool has_theme_stylebox_override(const StringName &p_name) const; @@ -387,6 +392,7 @@ class Window : public Viewport { bool has_theme_font_size_override(const StringName &p_name) const; bool has_theme_color_override(const StringName &p_name) const; bool has_theme_constant_override(const StringName &p_name) const; + bool has_theme_audio_override(const StringName &p_name) const; bool has_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const; @@ -394,6 +400,7 @@ class Window : public Viewport { bool has_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + bool has_theme_audio(const StringName &p_name, const StringName &p_theme_type = StringName()) const; float get_theme_default_base_scale() const; Ref get_theme_default_font() const; diff --git a/scene/resources/default_theme/default_theme.cpp b/scene/resources/default_theme/default_theme.cpp index b6a1737acb4..a474a985a27 100644 --- a/scene/resources/default_theme/default_theme.cpp +++ b/scene/resources/default_theme/default_theme.cpp @@ -145,9 +145,20 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const icons[default_theme_icons_names[i]] = generate_icon(i); } + // Control + + theme->set_audio("focus", "Control", Ref()); + // Panel + theme->set_stylebox("panel", "Panel", make_flat_stylebox(style_normal_color, 0, 0, 0, 0)); + // BaseButton + + theme->set_audio("hover", "BaseButton", Ref()); + theme->set_audio("pressed", "BaseButton", Ref()); + theme->set_audio("pressed_disabled", "BaseButton", Ref()); + // Button const Ref button_normal = make_flat_stylebox(style_normal_color); @@ -420,6 +431,10 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_icon("clear", "LineEdit", icons["line_edit_clear"]); + theme->set_audio("text_changed", "LineEdit", Ref()); + theme->set_audio("text_submitted", "LineEdit", Ref()); + theme->set_audio("text_change_rejected", "LineEdit", Ref()); + // ProgressBar theme->set_stylebox("background", "ProgressBar", make_flat_stylebox(style_disabled_color, 2, 2, 2, 2, 6)); @@ -464,6 +479,8 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_constant("outline_size", "TextEdit", 0); theme->set_constant("caret_width", "TextEdit", 1); + theme->set_audio("text_changed", "TextEdit", Ref()); + // CodeEdit theme->set_stylebox("normal", "CodeEdit", style_line_edit); @@ -558,6 +575,12 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const const Ref style_slider_grabber = make_flat_stylebox(style_progress_color, 4, 4, 4, 4, 4); const Ref style_slider_grabber_highlight = make_flat_stylebox(style_focus_color, 4, 4, 4, 4, 4); + // Slider + + theme->set_audio("drag_started", "Slider", Ref()); + theme->set_audio("drag_ended", "Slider", Ref()); + theme->set_audio("value_changed", "Slider", Ref()); + // HSlider theme->set_stylebox("slider", "HSlider", style_slider); @@ -590,6 +613,8 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_icon("updown", "SpinBox", icons["updown"]); + theme->set_audio("pressed", "SpinBox", Ref()); + // ScrollContainer Ref empty; @@ -698,6 +723,9 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_constant("item_end_padding", "PopupMenu", Math::round(2 * scale)); theme->set_constant("icon_max_width", "PopupMenu", 0); + theme->set_audio("item_activated", "PopupMenu", Ref()); + theme->set_audio("item_disabled_activated", "PopupMenu", Ref()); + // GraphNode Ref graphnode_normal = make_flat_stylebox(style_normal_color, 18, 42, 18, 12); graphnode_normal->set_border_width(SIDE_TOP, 30); @@ -799,6 +827,8 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_constant("scrollbar_h_separation", "Tree", Math::round(4 * scale)); theme->set_constant("scrollbar_v_separation", "Tree", Math::round(4 * scale)); + theme->set_audio("item_selected", "Tree", Ref()); + // ItemList theme->set_stylebox("panel", "ItemList", make_flat_stylebox(style_normal_color)); @@ -824,6 +854,9 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_constant("outline_size", "ItemList", 0); + theme->set_audio("item_selected", "ItemList", Ref()); + theme->set_audio("item_disabled_selected", "ItemList", Ref()); + // TabContainer Ref style_tab_selected = make_flat_stylebox(style_normal_color, 10, 4, 10, 4, 0); @@ -899,6 +932,10 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_constant("icon_max_width", "TabBar", 0); theme->set_constant("outline_size", "TabBar", 0); + theme->set_audio("hover", "TabBar", Ref()); + theme->set_audio("pressed", "TabBar", Ref()); + theme->set_audio("pressed_disabled", "TabBar", Ref()); + // Separators theme->set_stylebox("separator", "HSeparator", separator_horizontal); diff --git a/scene/resources/theme.cpp b/scene/resources/theme.cpp index 799a8471b90..fc6e448b187 100644 --- a/scene/resources/theme.cpp +++ b/scene/resources/theme.cpp @@ -54,6 +54,8 @@ bool Theme::_set(const StringName &p_name, const Variant &p_value) { set_color(prop_name, theme_type, p_value); } else if (type == "constants") { set_constant(prop_name, theme_type, p_value); + } else if (type == "audios") { + set_audio(prop_name, theme_type, p_value); } else if (type == "base_type") { set_type_variation(theme_type, p_value); } else { @@ -98,6 +100,12 @@ bool Theme::_get(const StringName &p_name, Variant &r_ret) const { r_ret = get_color(prop_name, theme_type); } else if (type == "constants") { r_ret = get_constant(prop_name, theme_type); + } else if (type == "audios") { + if (!has_audio(prop_name, theme_type)) { + r_ret = Ref(); + } else { + r_ret = get_audio(prop_name, theme_type); + } } else if (type == "base_type") { r_ret = get_type_variation_base(theme_type); } else { @@ -160,6 +168,13 @@ void Theme::_get_property_list(List *p_list) const { } } + // Audios. + for (const KeyValue &E : audio_map) { + for (const KeyValue> &F : E.value) { + list.push_back(PropertyInfo(Variant::OBJECT, String() + E.key + "/audios/" + F.key, PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL)); + } + } + // Sort and store properties. list.sort(); String prev_type; @@ -852,6 +867,115 @@ void Theme::get_constant_type_list(List *p_list) const { } } +void Theme::set_audio(const StringName &p_name, const StringName &p_theme_type, const Ref &p_audio) { + ERR_FAIL_COND_MSG(!is_valid_item_name(p_name), vformat("Invalid item name: '%s'", p_name)); + ERR_FAIL_COND_MSG(!is_valid_type_name(p_theme_type), vformat("Invalid type name: '%s'", p_theme_type)); + + bool existing = false; + if (audio_map[p_theme_type][p_name].is_valid()) { + existing = true; + audio_map[p_theme_type][p_name]->disconnect_changed(callable_mp(this, &Theme::_emit_theme_changed)); + } + + audio_map[p_theme_type][p_name] = p_audio; + + if (p_audio.is_valid()) { + audio_map[p_theme_type][p_name]->connect_changed(callable_mp(this, &Theme::_emit_theme_changed).bind(false), CONNECT_REFERENCE_COUNTED); + } + + _emit_theme_changed(!existing); +} + +Ref Theme::get_audio(const StringName &p_name, const StringName &p_theme_type) const { + if (audio_map.has(p_theme_type) && audio_map[p_theme_type].has(p_name) && audio_map[p_theme_type][p_name].is_valid()) { + return audio_map[p_theme_type][p_name]; + } else { + return ThemeDB::get_singleton()->get_fallback_audio(); + } +} + +bool Theme::has_audio(const StringName &p_name, const StringName &p_theme_type) const { + return (audio_map.has(p_theme_type) && audio_map[p_theme_type].has(p_name) && audio_map[p_theme_type][p_name].is_valid()); +} + +bool Theme::has_audio_nocheck(const StringName &p_name, const StringName &p_theme_type) const { + return (audio_map.has(p_theme_type) && audio_map[p_theme_type].has(p_name)); +} + +void Theme::rename_audio(const StringName &p_old_name, const StringName &p_name, const StringName &p_theme_type) { + ERR_FAIL_COND_MSG(!is_valid_item_name(p_name), vformat("Invalid item name: '%s'", p_name)); + ERR_FAIL_COND_MSG(!is_valid_type_name(p_theme_type), vformat("Invalid type name: '%s'", p_theme_type)); + ERR_FAIL_COND_MSG(!audio_map.has(p_theme_type), "Cannot rename the audio '" + String(p_old_name) + "' because the node type '" + String(p_theme_type) + "' does not exist."); + ERR_FAIL_COND_MSG(audio_map[p_theme_type].has(p_name), "Cannot rename the audio '" + String(p_old_name) + "' because the new name '" + String(p_name) + "' already exists."); + ERR_FAIL_COND_MSG(!audio_map[p_theme_type].has(p_old_name), "Cannot rename the audio '" + String(p_old_name) + "' because it does not exist."); + + audio_map[p_theme_type][p_name] = audio_map[p_theme_type][p_old_name]; + audio_map[p_theme_type].erase(p_old_name); + + _emit_theme_changed(true); +} + +void Theme::clear_audio(const StringName &p_name, const StringName &p_theme_type) { + ERR_FAIL_COND_MSG(!audio_map.has(p_theme_type), "Cannot clear the audio '" + String(p_name) + "' because the node type '" + String(p_theme_type) + "' does not exist."); + ERR_FAIL_COND_MSG(!audio_map[p_theme_type].has(p_name), "Cannot clear the audio '" + String(p_name) + "' because it does not exist."); + + if (audio_map[p_theme_type][p_name].is_valid()) { + audio_map[p_theme_type][p_name]->disconnect_changed(callable_mp(this, &Theme::_emit_theme_changed)); + } + + audio_map[p_theme_type].erase(p_name); + + _emit_theme_changed(true); +} + +void Theme::get_audio_list(StringName p_theme_type, List *p_list) const { + ERR_FAIL_NULL(p_list); + + if (!audio_map.has(p_theme_type)) { + return; + } + + for (const KeyValue> &E : audio_map[p_theme_type]) { + p_list->push_back(E.key); + } +} + +void Theme::add_audio_type(const StringName &p_theme_type) { + ERR_FAIL_COND_MSG(!is_valid_type_name(p_theme_type), vformat("Invalid type name: '%s'", p_theme_type)); + + if (audio_map.has(p_theme_type)) { + return; + } + audio_map[p_theme_type] = ThemeAudioMap(); +} + +void Theme::remove_audio_type(const StringName &p_theme_type) { + if (!audio_map.has(p_theme_type)) { + return; + } + + _freeze_change_propagation(); + + for (const KeyValue> &E : audio_map[p_theme_type]) { + Ref audio = E.value; + if (audio.is_valid()) { + audio->disconnect_changed(callable_mp(this, &Theme::_emit_theme_changed)); + } + } + + audio_map.erase(p_theme_type); + + _unfreeze_and_propagate_changes(); +} + +void Theme::get_audio_type_list(List *p_list) const { + ERR_FAIL_NULL(p_list); + + for (const KeyValue &E : audio_map) { + p_list->push_back(E.key); + } +} + // Generic methods for managing theme items. void Theme::set_theme_item(DataType p_data_type, const StringName &p_name, const StringName &p_theme_type, const Variant &p_value) { switch (p_data_type) { @@ -891,6 +1015,12 @@ void Theme::set_theme_item(DataType p_data_type, const StringName &p_name, const Ref stylebox_value = Object::cast_to(p_value.get_validated_object()); set_stylebox(p_name, p_theme_type, stylebox_value); } break; + case DATA_TYPE_AUDIOSTREAM: { + ERR_FAIL_COND_MSG(p_value.get_type() != Variant::OBJECT, "Theme item's data type (Object) does not match Variant's type (" + Variant::get_type_name(p_value.get_type()) + ")."); + + Ref audio_value = Object::cast_to(p_value.get_validated_object()); + set_audio(p_name, p_theme_type, audio_value); + } break; case DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -910,6 +1040,8 @@ Variant Theme::get_theme_item(DataType p_data_type, const StringName &p_name, co return get_icon(p_name, p_theme_type); case DATA_TYPE_STYLEBOX: return get_stylebox(p_name, p_theme_type); + case DATA_TYPE_AUDIOSTREAM: + return get_audio(p_name, p_theme_type); case DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -931,6 +1063,8 @@ bool Theme::has_theme_item(DataType p_data_type, const StringName &p_name, const return has_icon(p_name, p_theme_type); case DATA_TYPE_STYLEBOX: return has_stylebox(p_name, p_theme_type); + case DATA_TYPE_AUDIOSTREAM: + return has_audio(p_name, p_theme_type); case DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -952,6 +1086,8 @@ bool Theme::has_theme_item_nocheck(DataType p_data_type, const StringName &p_nam return has_icon_nocheck(p_name, p_theme_type); case DATA_TYPE_STYLEBOX: return has_stylebox_nocheck(p_name, p_theme_type); + case DATA_TYPE_AUDIOSTREAM: + return has_audio_nocheck(p_name, p_theme_type); case DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -978,6 +1114,8 @@ void Theme::rename_theme_item(DataType p_data_type, const StringName &p_old_name break; case DATA_TYPE_STYLEBOX: rename_stylebox(p_old_name, p_name, p_theme_type); + case DATA_TYPE_AUDIOSTREAM: + rename_audio(p_old_name, p_name, p_theme_type); break; case DATA_TYPE_MAX: break; // Can't happen, but silences warning. @@ -1003,6 +1141,8 @@ void Theme::clear_theme_item(DataType p_data_type, const StringName &p_name, con break; case DATA_TYPE_STYLEBOX: clear_stylebox(p_name, p_theme_type); + case DATA_TYPE_AUDIOSTREAM: + clear_audio(p_name, p_theme_type); break; case DATA_TYPE_MAX: break; // Can't happen, but silences warning. @@ -1028,6 +1168,8 @@ void Theme::get_theme_item_list(DataType p_data_type, StringName p_theme_type, L break; case DATA_TYPE_STYLEBOX: get_stylebox_list(p_theme_type, p_list); + case DATA_TYPE_AUDIOSTREAM: + get_audio_list(p_theme_type, p_list); break; case DATA_TYPE_MAX: break; // Can't happen, but silences warning. @@ -1053,6 +1195,8 @@ void Theme::add_theme_item_type(DataType p_data_type, const StringName &p_theme_ break; case DATA_TYPE_STYLEBOX: add_stylebox_type(p_theme_type); + case DATA_TYPE_AUDIOSTREAM: + add_audio_type(p_theme_type); break; case DATA_TYPE_MAX: break; // Can't happen, but silences warning. @@ -1078,6 +1222,8 @@ void Theme::remove_theme_item_type(DataType p_data_type, const StringName &p_the break; case DATA_TYPE_STYLEBOX: remove_stylebox_type(p_theme_type); + case DATA_TYPE_AUDIOSTREAM: + remove_audio_type(p_theme_type); break; case DATA_TYPE_MAX: break; // Can't happen, but silences warning. @@ -1103,6 +1249,8 @@ void Theme::get_theme_item_type_list(DataType p_data_type, List *p_l break; case DATA_TYPE_STYLEBOX: get_stylebox_type_list(p_list); + case DATA_TYPE_AUDIOSTREAM: + get_audio_type_list(p_list); break; case DATA_TYPE_MAX: break; // Can't happen, but silences warning. @@ -1240,6 +1388,11 @@ void Theme::get_type_list(List *p_list) const { types.insert(E.key); } + // Audios. + for (const KeyValue &E : audio_map) { + types.insert(E.key); + } + for (const StringName &E : types) { p_list->push_back(E); } @@ -1451,6 +1604,36 @@ Vector Theme::_get_constant_type_list() const { return ilret; } +Vector Theme::_get_audio_list(const String &p_theme_type) const { + Vector ilret; + List il; + + get_audio_list(p_theme_type, &il); + ilret.resize(il.size()); + + int i = 0; + String *w = ilret.ptrw(); + for (List::Element *E = il.front(); E; E = E->next(), i++) { + w[i] = E->get(); + } + return ilret; +} + +Vector Theme::_get_audio_type_list() const { + Vector ilret; + List il; + + get_audio_type_list(&il); + ilret.resize(il.size()); + + int i = 0; + String *w = ilret.ptrw(); + for (List::Element *E = il.front(); E; E = E->next(), i++) { + w[i] = E->get(); + } + return ilret; +} + Vector Theme::_get_theme_item_list(DataType p_data_type, const String &p_theme_type) const { switch (p_data_type) { case DATA_TYPE_COLOR: @@ -1465,6 +1648,8 @@ Vector Theme::_get_theme_item_list(DataType p_data_type, const String &p return _get_icon_list(p_theme_type); case DATA_TYPE_STYLEBOX: return _get_stylebox_list(p_theme_type); + case DATA_TYPE_AUDIOSTREAM: + return _get_audio_list(p_theme_type); case DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -1486,6 +1671,8 @@ Vector Theme::_get_theme_item_type_list(DataType p_data_type) const { return _get_icon_type_list(); case DATA_TYPE_STYLEBOX: return _get_stylebox_type_list(); + case DATA_TYPE_AUDIOSTREAM: + return _get_audio_type_list(); case DATA_TYPE_MAX: break; // Can't happen, but silences warning. } @@ -1605,6 +1792,15 @@ void Theme::merge_with(const Ref &p_other) { } } + // Audios. + { + for (const KeyValue &E : p_other->audio_map) { + for (const KeyValue> &F : E.value) { + set_audio(F.key, E.key, F.value); + } + } + } + // Type variations. { for (const KeyValue &E : p_other->variation_map) { @@ -1650,12 +1846,24 @@ void Theme::clear() { } } + { + for (const KeyValue &E : audio_map) { + for (const KeyValue> &F : E.value) { + if (F.value.is_valid()) { + Ref audio = F.value; + audio->disconnect_changed(callable_mp(this, &Theme::_emit_theme_changed)); + } + } + } + } + icon_map.clear(); style_map.clear(); font_map.clear(); font_size_map.clear(); color_map.clear(); constant_map.clear(); + audio_map.clear(); variation_map.clear(); variation_base_map.clear(); @@ -1716,6 +1924,14 @@ void Theme::_bind_methods() { ClassDB::bind_method(D_METHOD("get_constant_list", "theme_type"), &Theme::_get_constant_list); ClassDB::bind_method(D_METHOD("get_constant_type_list"), &Theme::_get_constant_type_list); + ClassDB::bind_method(D_METHOD("set_audio", "name", "theme_type", "audio"), &Theme::set_audio); + ClassDB::bind_method(D_METHOD("get_audio", "name", "theme_type"), &Theme::get_audio); + ClassDB::bind_method(D_METHOD("has_audio", "name", "theme_type"), &Theme::has_audio); + ClassDB::bind_method(D_METHOD("rename_audio", "old_name", "name", "theme_type"), &Theme::rename_audio); + ClassDB::bind_method(D_METHOD("clear_audio", "name", "theme_type"), &Theme::clear_audio); + ClassDB::bind_method(D_METHOD("get_audio_list", "theme_type"), &Theme::_get_audio_list); + ClassDB::bind_method(D_METHOD("get_audio_type_list"), &Theme::_get_font_type_list); + ClassDB::bind_method(D_METHOD("set_default_base_scale", "base_scale"), &Theme::set_default_base_scale); ClassDB::bind_method(D_METHOD("get_default_base_scale"), &Theme::get_default_base_scale); ClassDB::bind_method(D_METHOD("has_default_base_scale"), &Theme::has_default_base_scale); @@ -1759,6 +1975,7 @@ void Theme::_bind_methods() { BIND_ENUM_CONSTANT(DATA_TYPE_FONT_SIZE); BIND_ENUM_CONSTANT(DATA_TYPE_ICON); BIND_ENUM_CONSTANT(DATA_TYPE_STYLEBOX); + BIND_ENUM_CONSTANT(DATA_TYPE_AUDIOSTREAM); BIND_ENUM_CONSTANT(DATA_TYPE_MAX); } diff --git a/scene/resources/theme.h b/scene/resources/theme.h index c7a76f4c5ec..53d14f5982b 100644 --- a/scene/resources/theme.h +++ b/scene/resources/theme.h @@ -35,6 +35,7 @@ #include "scene/resources/font.h" #include "scene/resources/style_box.h" #include "scene/resources/texture.h" +#include "servers/audio/audio_stream.h" class Theme : public Resource { GDCLASS(Theme, Resource); @@ -53,6 +54,7 @@ class Theme : public Resource { using ThemeFontSizeMap = HashMap; using ThemeColorMap = HashMap; using ThemeConstantMap = HashMap; + using ThemeAudioMap = HashMap>; enum DataType { DATA_TYPE_COLOR, @@ -61,6 +63,7 @@ class Theme : public Resource { DATA_TYPE_FONT_SIZE, DATA_TYPE_ICON, DATA_TYPE_STYLEBOX, + DATA_TYPE_AUDIOSTREAM, DATA_TYPE_MAX }; @@ -75,6 +78,7 @@ class Theme : public Resource { HashMap font_size_map; HashMap color_map; HashMap constant_map; + HashMap audio_map; HashMap variation_map; HashMap> variation_base_map; @@ -90,6 +94,8 @@ class Theme : public Resource { Vector _get_color_type_list() const; Vector _get_constant_list(const String &p_theme_type) const; Vector _get_constant_type_list() const; + Vector _get_audio_list(const String &p_theme_type) const; + Vector _get_audio_type_list() const; Vector _get_theme_item_list(DataType p_data_type, const String &p_theme_type) const; Vector _get_theme_item_type_list(DataType p_data_type) const; @@ -196,6 +202,17 @@ class Theme : public Resource { void remove_constant_type(const StringName &p_theme_type); void get_constant_type_list(List *p_list) const; + void set_audio(const StringName &p_name, const StringName &p_theme_type, const Ref &p_audio); + Ref get_audio(const StringName &p_name, const StringName &p_theme_type) const; + bool has_audio(const StringName &p_name, const StringName &p_theme_type) const; + bool has_audio_nocheck(const StringName &p_name, const StringName &p_theme_type) const; + void rename_audio(const StringName &p_old_name, const StringName &p_name, const StringName &p_theme_type); + void clear_audio(const StringName &p_name, const StringName &p_theme_type); + void get_audio_list(StringName p_theme_type, List *p_list) const; + void add_audio_type(const StringName &p_theme_type); + void remove_audio_type(const StringName &p_theme_type); + void get_audio_type_list(List *p_list) const; + void set_theme_item(DataType p_data_type, const StringName &p_name, const StringName &p_theme_type, const Variant &p_value); Variant get_theme_item(DataType p_data_type, const StringName &p_name, const StringName &p_theme_type) const; bool has_theme_item(DataType p_data_type, const StringName &p_name, const StringName &p_theme_type) const; diff --git a/scene/theme/theme_db.cpp b/scene/theme/theme_db.cpp index 9b85a62c6ec..87f6148fc15 100644 --- a/scene/theme/theme_db.cpp +++ b/scene/theme/theme_db.cpp @@ -176,6 +176,19 @@ Ref ThemeDB::get_fallback_stylebox() { return fallback_stylebox; } +void ThemeDB::set_fallback_audio(const Ref &p_audio) { + if (fallback_audio == p_audio) { + return; + } + + fallback_audio = p_audio; + emit_signal(SNAME("fallback_changed")); +} + +Ref ThemeDB::get_fallback_audio() { + return fallback_audio; +} + // Object methods. void ThemeDB::_bind_methods() { ClassDB::bind_method(D_METHOD("get_default_theme"), &ThemeDB::get_default_theme); @@ -191,6 +204,8 @@ void ThemeDB::_bind_methods() { ClassDB::bind_method(D_METHOD("get_fallback_icon"), &ThemeDB::get_fallback_icon); ClassDB::bind_method(D_METHOD("set_fallback_stylebox", "stylebox"), &ThemeDB::set_fallback_stylebox); ClassDB::bind_method(D_METHOD("get_fallback_stylebox"), &ThemeDB::get_fallback_stylebox); + ClassDB::bind_method(D_METHOD("set_fallback_audio", "audio"), &ThemeDB::set_fallback_audio); + ClassDB::bind_method(D_METHOD("get_fallback_audio"), &ThemeDB::get_fallback_audio); ADD_GROUP("Fallback values", "fallback_"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fallback_base_scale", PROPERTY_HINT_RANGE, "0.0,2.0,0.01,or_greater"), "set_fallback_base_scale", "get_fallback_base_scale"); @@ -198,6 +213,7 @@ void ThemeDB::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "fallback_font_size", PROPERTY_HINT_RANGE, "0,256,1,or_greater,suffix:px"), "set_fallback_font_size", "get_fallback_font_size"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "fallback_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", PROPERTY_USAGE_NONE), "set_fallback_icon", "get_fallback_icon"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "fallback_stylebox", PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", PROPERTY_USAGE_NONE), "set_fallback_stylebox", "get_fallback_stylebox"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "fallback_audio", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", PROPERTY_USAGE_NONE), "set_fallback_audio", "get_fallback_audio"); ADD_SIGNAL(MethodInfo("fallback_changed")); } @@ -224,6 +240,7 @@ ThemeDB::~ThemeDB() { fallback_font.unref(); fallback_icon.unref(); fallback_stylebox.unref(); + fallback_audio.unref(); singleton = nullptr; } diff --git a/scene/theme/theme_db.h b/scene/theme/theme_db.h index f65899f5ea7..916d8fbfc63 100644 --- a/scene/theme/theme_db.h +++ b/scene/theme/theme_db.h @@ -35,6 +35,7 @@ #include "core/object/ref_counted.h" class Font; +class AudioStream; class StyleBox; class Texture2D; class Theme; @@ -54,6 +55,7 @@ class ThemeDB : public Object { int fallback_font_size; Ref fallback_icon; Ref fallback_stylebox; + Ref fallback_audio; protected: static void _bind_methods(); @@ -87,6 +89,9 @@ class ThemeDB : public Object { void set_fallback_stylebox(const Ref &p_stylebox); Ref get_fallback_stylebox(); + void set_fallback_audio(const Ref &p_audio); + Ref get_fallback_audio(); + static ThemeDB *get_singleton(); ThemeDB(); ~ThemeDB(); ```

https://github.com/godotengine/godot-proposals/assets/180032/8d15a153-62a7-4a56-9703-5050cdffc8c6

Sounds from https://github.com/redeclipse/sounds.

There are still some things to iron out (specifically regarding the inspector and --doctool not "seeing" audio theme items for some reason), but it's getting there.

conde2 commented 9 months ago

Is there a pull request for this in godot master? This is a must feature. @Calinou amazing work!

Calinou commented 9 months ago

Is there a pull request for this in godot master? This is a must feature.

Not yet, as my branch still has some work left to do (check the TODO part of the commit message). This is too late for 4.3 either way, which is entering feature freeze soon.

conde2 commented 9 months ago

That's sad to hear. Maybe this comes to the 4.4 then? For the video it was looking very promising, keep the good work.

I will leave it out here as I don't have much to contribute to the proposal, but imo audio theme UI is brilliant suggestion that I strongly support.

TruFelix commented 3 months ago

I am too waiting for this feature! But I also need to add sounds to disabled buttons as well.

Calinou commented 3 months ago

But I also need to add sounds to disabled buttons as well.

The branch I linked above already handles this for some controls, although Button doesn't play its disabled sounds when pressed yet. This will require some refactoring of the GUI input code in BaseButton so that some of its code is run even when the button is disabled.

ch0m5 commented 4 weeks ago

Is there an ETA for this feature or someone actively working on it? I've been following this thread for a while but I don't know how far has the feature progressed.

I'm working the pre-production of a UI-heavy project of considerable size and I'm unsure if I should account for this being in the engine sometime in the near future regarding the game's UI, and currently working around it requires to define a specific behavior for most if not all Buttons in a game.

Progress so far looks great!

Calinou commented 4 weeks ago

Is there an ETA for this feature or someone actively working on it?

I'm still working on the feature on-and-off, but I can't give an ETA. I rebased it on top of master a few weeks ago, but several items remain to be implemented before I can open a PR.

I'm working the pre-production of a UI-heavy project of considerable size and I'm unsure if I should account for this being in the engine sometime in the near future regarding the game's UI, and currently working around it requires to define a specific behavior for most if not all Buttons in a game.

I wouldn't expect this to be finished in time for 4.4, especially since the PR would need to be reviewed and the diff is quite large.

If you need the feature right now, you can always take the branch, rebase it on top of master and compile custom editor and export template binaries. There are some missing features and quirks noted in the commit message though.