2018-07-19 17:34:22 +02:00
/**************************************************************************/
/* plugin_config_dialog.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
# include "plugin_config_dialog.h"
2021-07-15 00:46:35 +02:00
2018-07-19 17:34:22 +02:00
# include "core/io/config_file.h"
2021-06-11 14:51:48 +02:00
# include "core/io/dir_access.h"
2023-09-06 21:02:52 +02:00
# include "core/object/script_language.h"
2018-07-19 17:34:22 +02:00
# include "editor/editor_node.h"
# include "editor/editor_plugin.h"
2019-12-24 08:17:23 +01:00
# include "editor/editor_scale.h"
2023-05-06 10:54:46 +02:00
# include "editor/gui/editor_validation_panel.h"
2019-12-24 08:17:23 +01:00
# include "editor/project_settings_editor.h"
2022-11-11 20:12:48 +01:00
# include "scene/gui/grid_container.h"
2020-02-06 21:51:36 +01:00
2018-07-19 17:34:22 +02:00
void PluginConfigDialog : : _clear_fields ( ) {
name_edit - > set_text ( " " ) ;
subfolder_edit - > set_text ( " " ) ;
desc_edit - > set_text ( " " ) ;
author_edit - > set_text ( " " ) ;
version_edit - > set_text ( " " ) ;
script_edit - > set_text ( " " ) ;
}
void PluginConfigDialog : : _on_confirmed ( ) {
2022-05-07 23:19:55 +02:00
String path = " res://addons/ " + _get_subfolder ( ) ;
2018-07-19 17:34:22 +02:00
if ( ! _edit_mode ) {
2022-03-23 10:08:58 +01:00
Ref < DirAccess > d = DirAccess : : create ( DirAccess : : ACCESS_RESOURCES ) ;
if ( d . is_null ( ) | | d - > make_dir_recursive ( path ) ! = OK ) {
2018-07-19 17:34:22 +02:00
return ;
2020-05-14 16:41:43 +02:00
}
2018-07-19 17:34:22 +02:00
}
2022-05-07 23:19:55 +02:00
int lang_idx = script_option_edit - > get_selected ( ) ;
String ext = ScriptServer : : get_language ( lang_idx ) - > get_extension ( ) ;
String script_name = script_edit - > get_text ( ) . is_empty ( ) ? _get_subfolder ( ) : script_edit - > get_text ( ) ;
if ( script_name . get_extension ( ) . is_empty ( ) ) {
script_name + = " . " + ext ;
}
2022-08-30 02:34:01 +02:00
String script_path = path . path_join ( script_name ) ;
2022-05-07 23:19:55 +02:00
2018-07-19 17:34:22 +02:00
Ref < ConfigFile > cf = memnew ( ConfigFile ) ;
cf - > set_value ( " plugin " , " name " , name_edit - > get_text ( ) ) ;
cf - > set_value ( " plugin " , " description " , desc_edit - > get_text ( ) ) ;
cf - > set_value ( " plugin " , " author " , author_edit - > get_text ( ) ) ;
cf - > set_value ( " plugin " , " version " , version_edit - > get_text ( ) ) ;
2022-05-07 23:19:55 +02:00
cf - > set_value ( " plugin " , " script " , script_name ) ;
2018-07-19 17:34:22 +02:00
2022-08-30 02:34:01 +02:00
cf - > save ( path . path_join ( " plugin.cfg " ) ) ;
2018-07-19 17:34:22 +02:00
if ( ! _edit_mode ) {
2022-05-07 23:19:55 +02:00
String class_name = script_name . get_basename ( ) ;
2021-10-11 11:30:59 +02:00
String template_content = " " ;
Vector < ScriptLanguage : : ScriptTemplate > templates = ScriptServer : : get_language ( lang_idx ) - > get_built_in_templates ( " EditorPlugin " ) ;
2022-05-07 23:19:55 +02:00
if ( ! templates . is_empty ( ) ) {
template_content = templates [ 0 ] . content ;
2018-07-19 17:34:22 +02:00
}
2022-09-29 11:53:28 +02:00
Ref < Script > scr = ScriptServer : : get_language ( lang_idx ) - > make_template ( template_content , class_name , " EditorPlugin " ) ;
scr - > set_path ( script_path , true ) ;
ResourceSaver : : save ( scr ) ;
2018-07-19 17:34:22 +02:00
2022-09-29 11:53:28 +02:00
emit_signal ( SNAME ( " plugin_ready " ) , scr . ptr ( ) , active_edit - > is_pressed ( ) ? _to_absolute_plugin_path ( _get_subfolder ( ) ) : " " ) ;
2018-07-19 17:34:22 +02:00
} else {
EditorNode : : get_singleton ( ) - > get_project_settings ( ) - > update_plugins ( ) ;
}
_clear_fields ( ) ;
}
2023-01-21 12:25:29 +01:00
void PluginConfigDialog : : _on_canceled ( ) {
2018-07-19 17:34:22 +02:00
_clear_fields ( ) ;
}
2023-05-06 10:54:46 +02:00
void PluginConfigDialog : : _on_required_text_changed ( ) {
2019-01-23 14:54:33 +01:00
int lang_idx = script_option_edit - > get_selected ( ) ;
String ext = ScriptServer : : get_language ( lang_idx ) - > get_extension ( ) ;
2021-07-11 16:14:27 +02:00
if ( name_edit - > get_text ( ) . is_empty ( ) ) {
2023-05-06 10:54:46 +02:00
validation_panel - > set_message ( MSG_ID_PLUGIN , TTR ( " Plugin name cannot be blank. " ) , EditorValidationPanel : : MSG_ERROR ) ;
2021-07-11 16:14:27 +02:00
}
2022-05-07 23:19:55 +02:00
if ( ( ! script_edit - > get_text ( ) . get_extension ( ) . is_empty ( ) & & script_edit - > get_text ( ) . get_extension ( ) ! = ext ) | | script_edit - > get_text ( ) . ends_with ( " . " ) ) {
2023-05-06 10:54:46 +02:00
validation_panel - > set_message ( MSG_ID_SCRIPT , vformat ( TTR ( " Script extension must match chosen language extension (.%s). " ) , ext ) , EditorValidationPanel : : MSG_ERROR ) ;
2021-07-11 16:14:27 +02:00
}
2023-05-06 10:54:46 +02:00
if ( subfolder_edit - > is_visible ( ) ) {
if ( ! subfolder_edit - > get_text ( ) . is_empty ( ) & & ! subfolder_edit - > get_text ( ) . is_valid_filename ( ) ) {
validation_panel - > set_message ( MSG_ID_SUBFOLDER , TTR ( " Subfolder name is not a valid folder name. " ) , EditorValidationPanel : : MSG_ERROR ) ;
} else {
String path = " res://addons/ " + _get_subfolder ( ) ;
if ( ! _edit_mode & & DirAccess : : exists ( path ) ) { // Only show this error if in "create" mode.
validation_panel - > set_message ( MSG_ID_SUBFOLDER , TTR ( " Subfolder cannot be one which already exists. " ) , EditorValidationPanel : : MSG_ERROR ) ;
}
2021-07-11 16:14:27 +02:00
}
2023-05-06 10:54:46 +02:00
} else {
validation_panel - > set_message ( MSG_ID_SUBFOLDER , " " , EditorValidationPanel : : MSG_OK ) ;
2021-07-11 16:14:27 +02:00
}
2018-07-19 17:34:22 +02:00
}
2022-05-07 23:19:55 +02:00
String PluginConfigDialog : : _get_subfolder ( ) {
return subfolder_edit - > get_text ( ) . is_empty ( ) ? name_edit - > get_text ( ) . replace ( " " , " _ " ) . to_lower ( ) : subfolder_edit - > get_text ( ) ;
}
2021-03-04 19:22:47 +01:00
String PluginConfigDialog : : _to_absolute_plugin_path ( const String & p_plugin_name ) {
return " res://addons/ " + p_plugin_name + " /plugin.cfg " ;
}
2018-07-19 17:34:22 +02:00
void PluginConfigDialog : : _notification ( int p_what ) {
switch ( p_what ) {
2020-03-06 18:00:16 +01:00
case NOTIFICATION_VISIBILITY_CHANGED : {
if ( is_visible ( ) ) {
name_edit - > grab_focus ( ) ;
}
} break ;
2022-02-16 03:44:22 +01:00
2018-07-19 17:34:22 +02:00
case NOTIFICATION_READY : {
2020-02-21 18:28:45 +01:00
connect ( " confirmed " , callable_mp ( this , & PluginConfigDialog : : _on_confirmed ) ) ;
2023-01-21 12:25:29 +01:00
get_cancel_button ( ) - > connect ( " pressed " , callable_mp ( this , & PluginConfigDialog : : _on_canceled ) ) ;
2018-07-19 17:34:22 +02:00
} break ;
}
}
void PluginConfigDialog : : config ( const String & p_config_path ) {
2023-11-28 14:21:18 +01:00
if ( ! p_config_path . is_empty ( ) ) {
2018-07-19 17:34:22 +02:00
Ref < ConfigFile > cf = memnew ( ConfigFile ) ;
2019-08-07 12:54:30 +02:00
Error err = cf - > load ( p_config_path ) ;
2019-09-25 10:28:50 +02:00
ERR_FAIL_COND_MSG ( err ! = OK , " Cannot load config file from path ' " + p_config_path + " '. " ) ;
2018-07-19 17:34:22 +02:00
name_edit - > set_text ( cf - > get_value ( " plugin " , " name " , " " ) ) ;
subfolder_edit - > set_text ( p_config_path . get_base_dir ( ) . get_basename ( ) . get_file ( ) ) ;
desc_edit - > set_text ( cf - > get_value ( " plugin " , " description " , " " ) ) ;
author_edit - > set_text ( cf - > get_value ( " plugin " , " author " , " " ) ) ;
version_edit - > set_text ( cf - > get_value ( " plugin " , " version " , " " ) ) ;
script_edit - > set_text ( cf - > get_value ( " plugin " , " script " , " " ) ) ;
_edit_mode = true ;
set_title ( TTR ( " Edit a Plugin " ) ) ;
} else {
_clear_fields ( ) ;
_edit_mode = false ;
set_title ( TTR ( " Create a Plugin " ) ) ;
}
2023-11-28 14:21:18 +01:00
for ( Control * control : plugin_edit_hidden_controls ) {
control - > set_visible ( ! _edit_mode ) ;
}
2023-05-06 10:54:46 +02:00
validation_panel - > update ( ) ;
2021-07-11 16:14:27 +02:00
2020-12-14 19:37:30 +01:00
get_ok_button ( ) - > set_disabled ( ! _edit_mode ) ;
2022-07-08 02:31:19 +02:00
set_ok_button_text ( _edit_mode ? TTR ( " Update " ) : TTR ( " Create " ) ) ;
2018-07-19 17:34:22 +02:00
}
void PluginConfigDialog : : _bind_methods ( ) {
ADD_SIGNAL ( MethodInfo ( " plugin_ready " , PropertyInfo ( Variant : : STRING , " script_path " , PROPERTY_HINT_NONE , " " ) , PropertyInfo ( Variant : : STRING , " activate_name " ) ) ) ;
}
PluginConfigDialog : : PluginConfigDialog ( ) {
2020-12-14 19:37:30 +01:00
get_ok_button ( ) - > set_disabled ( true ) ;
2018-07-19 17:34:22 +02:00
set_hide_on_ok ( true ) ;
2021-07-11 16:14:27 +02:00
VBoxContainer * vbox = memnew ( VBoxContainer ) ;
vbox - > set_h_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
vbox - > set_v_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
add_child ( vbox ) ;
2018-07-19 17:34:22 +02:00
GridContainer * grid = memnew ( GridContainer ) ;
2023-05-06 10:54:46 +02:00
grid - > set_columns ( 2 ) ;
2023-01-01 16:59:15 +01:00
grid - > set_v_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
2021-07-11 16:14:27 +02:00
vbox - > add_child ( grid ) ;
2018-07-19 17:34:22 +02:00
2021-07-11 16:14:27 +02:00
// Plugin Name
2018-07-19 17:34:22 +02:00
Label * name_lb = memnew ( Label ) ;
name_lb - > set_text ( TTR ( " Plugin Name: " ) ) ;
2022-05-07 23:19:55 +02:00
name_lb - > set_horizontal_alignment ( HORIZONTAL_ALIGNMENT_RIGHT ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( name_lb ) ;
name_edit = memnew ( LineEdit ) ;
name_edit - > set_placeholder ( " MyPlugin " ) ;
2023-07-25 17:46:17 +02:00
name_edit - > set_tooltip_text ( TTR ( " Required. This name will be displayed in the list of plugins. " ) ) ;
2023-01-01 13:30:30 +01:00
name_edit - > set_h_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( name_edit ) ;
2021-07-11 16:14:27 +02:00
// Subfolder
2018-07-19 17:34:22 +02:00
Label * subfolder_lb = memnew ( Label ) ;
subfolder_lb - > set_text ( TTR ( " Subfolder: " ) ) ;
2022-05-07 23:19:55 +02:00
subfolder_lb - > set_horizontal_alignment ( HORIZONTAL_ALIGNMENT_RIGHT ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( subfolder_lb ) ;
2023-11-28 14:21:18 +01:00
plugin_edit_hidden_controls . push_back ( subfolder_lb ) ;
2018-07-19 17:34:22 +02:00
subfolder_edit = memnew ( LineEdit ) ;
subfolder_edit - > set_placeholder ( " \" my_plugin \" -> res://addons/my_plugin " ) ;
2023-07-25 17:46:17 +02:00
subfolder_edit - > set_tooltip_text ( TTR ( " Optional. The folder name should generally use `snake_case` naming (avoid spaces and special characters). \n If left empty, the folder will be named after the plugin name converted to `snake_case`. " ) ) ;
2023-01-01 13:30:30 +01:00
subfolder_edit - > set_h_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( subfolder_edit ) ;
2023-11-28 14:21:18 +01:00
plugin_edit_hidden_controls . push_back ( subfolder_edit ) ;
2018-07-19 17:34:22 +02:00
2021-07-11 16:14:27 +02:00
// Description
2018-07-19 17:34:22 +02:00
Label * desc_lb = memnew ( Label ) ;
desc_lb - > set_text ( TTR ( " Description: " ) ) ;
2022-05-07 23:19:55 +02:00
desc_lb - > set_horizontal_alignment ( HORIZONTAL_ALIGNMENT_RIGHT ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( desc_lb ) ;
desc_edit = memnew ( TextEdit ) ;
2023-07-25 17:46:17 +02:00
desc_edit - > set_tooltip_text ( TTR ( " Optional. This description should be kept relatively short (up to 5 lines). \n It will display when hovering the plugin in the list of plugins. " ) ) ;
2018-10-20 22:05:11 +02:00
desc_edit - > set_custom_minimum_size ( Size2 ( 400 , 80 ) * EDSCALE ) ;
2021-07-09 12:52:49 +02:00
desc_edit - > set_line_wrapping_mode ( TextEdit : : LineWrappingMode : : LINE_WRAPPING_BOUNDARY ) ;
2023-01-01 16:59:15 +01:00
desc_edit - > set_h_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
desc_edit - > set_v_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( desc_edit ) ;
2021-07-11 16:14:27 +02:00
// Author
2018-07-19 17:34:22 +02:00
Label * author_lb = memnew ( Label ) ;
author_lb - > set_text ( TTR ( " Author: " ) ) ;
2022-05-07 23:19:55 +02:00
author_lb - > set_horizontal_alignment ( HORIZONTAL_ALIGNMENT_RIGHT ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( author_lb ) ;
author_edit = memnew ( LineEdit ) ;
author_edit - > set_placeholder ( " Godette " ) ;
2023-07-25 17:46:17 +02:00
author_edit - > set_tooltip_text ( TTR ( " Optional. The author's username, full name, or organization name. " ) ) ;
2023-01-01 13:30:30 +01:00
author_edit - > set_h_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( author_edit ) ;
2021-07-11 16:14:27 +02:00
// Version
2018-07-19 17:34:22 +02:00
Label * version_lb = memnew ( Label ) ;
version_lb - > set_text ( TTR ( " Version: " ) ) ;
2022-05-07 23:19:55 +02:00
version_lb - > set_horizontal_alignment ( HORIZONTAL_ALIGNMENT_RIGHT ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( version_lb ) ;
version_edit = memnew ( LineEdit ) ;
2023-07-25 17:46:17 +02:00
version_edit - > set_tooltip_text ( TTR ( " Optional. A human-readable version identifier used for informational purposes only. " ) ) ;
2018-07-19 17:34:22 +02:00
version_edit - > set_placeholder ( " 1.0 " ) ;
2023-01-01 13:30:30 +01:00
version_edit - > set_h_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( version_edit ) ;
2021-07-11 16:14:27 +02:00
// Language dropdown
2018-07-19 17:34:22 +02:00
Label * script_option_lb = memnew ( Label ) ;
script_option_lb - > set_text ( TTR ( " Language: " ) ) ;
2022-05-07 23:19:55 +02:00
script_option_lb - > set_horizontal_alignment ( HORIZONTAL_ALIGNMENT_RIGHT ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( script_option_lb ) ;
script_option_edit = memnew ( OptionButton ) ;
2023-07-25 17:46:17 +02:00
script_option_edit - > set_tooltip_text ( TTR ( " Required. The scripting language to use for the script. \n Note that a plugin may use several languages at once by adding more scripts to the plugin. " ) ) ;
2019-02-12 15:41:00 +01:00
int default_lang = 0 ;
2019-01-23 14:54:33 +01:00
for ( int i = 0 ; i < ScriptServer : : get_language_count ( ) ; i + + ) {
ScriptLanguage * lang = ScriptServer : : get_language ( i ) ;
script_option_edit - > add_item ( lang - > get_name ( ) ) ;
2021-10-11 11:30:59 +02:00
if ( lang - > get_name ( ) = = " GDScript " ) {
2019-02-12 15:41:00 +01:00
default_lang = i ;
}
2019-01-23 14:54:33 +01:00
}
2019-02-12 15:41:00 +01:00
script_option_edit - > select ( default_lang ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( script_option_edit ) ;
2021-07-11 16:14:27 +02:00
// Plugin Script Name
2018-07-19 17:34:22 +02:00
Label * script_lb = memnew ( Label ) ;
script_lb - > set_text ( TTR ( " Script Name: " ) ) ;
2022-05-07 23:19:55 +02:00
script_lb - > set_horizontal_alignment ( HORIZONTAL_ALIGNMENT_RIGHT ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( script_lb ) ;
script_edit = memnew ( LineEdit ) ;
2023-07-25 17:46:17 +02:00
script_edit - > set_tooltip_text ( TTR ( " Optional. The path to the script (relative to the add-on folder). If left empty, will default to \" plugin.gd \" . " ) ) ;
2018-07-19 17:34:22 +02:00
script_edit - > set_placeholder ( " \" plugin.gd \" -> res://addons/my_plugin/plugin.gd " ) ;
2023-01-01 13:30:30 +01:00
script_edit - > set_h_size_flags ( Control : : SIZE_EXPAND_FILL ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( script_edit ) ;
2021-07-11 16:14:27 +02:00
// Activate now checkbox
2019-01-23 14:54:33 +01:00
// TODO Make this option work better with languages like C#. Right now, it does not work because the C# project must be compiled first.
2018-07-19 17:34:22 +02:00
Label * active_lb = memnew ( Label ) ;
active_lb - > set_text ( TTR ( " Activate now? " ) ) ;
2022-05-07 23:19:55 +02:00
active_lb - > set_horizontal_alignment ( HORIZONTAL_ALIGNMENT_RIGHT ) ;
2018-07-19 17:34:22 +02:00
grid - > add_child ( active_lb ) ;
2023-11-28 14:21:18 +01:00
plugin_edit_hidden_controls . push_back ( active_lb ) ;
2018-07-19 17:34:22 +02:00
active_edit = memnew ( CheckBox ) ;
active_edit - > set_pressed ( true ) ;
grid - > add_child ( active_edit ) ;
2023-11-28 14:21:18 +01:00
plugin_edit_hidden_controls . push_back ( active_edit ) ;
2023-05-06 10:54:46 +02:00
Control * spacing = memnew ( Control ) ;
vbox - > add_child ( spacing ) ;
spacing - > set_custom_minimum_size ( Size2 ( 0 , 10 * EDSCALE ) ) ;
validation_panel = memnew ( EditorValidationPanel ) ;
vbox - > add_child ( validation_panel ) ;
validation_panel - > add_line ( MSG_ID_PLUGIN , TTR ( " Plugin name is valid. " ) ) ;
validation_panel - > add_line ( MSG_ID_SCRIPT , TTR ( " Script extension is valid. " ) ) ;
validation_panel - > add_line ( MSG_ID_SUBFOLDER , TTR ( " Subfolder name is valid. " ) ) ;
validation_panel - > set_update_callback ( callable_mp ( this , & PluginConfigDialog : : _on_required_text_changed ) ) ;
validation_panel - > set_accept_button ( get_ok_button ( ) ) ;
script_option_edit - > connect ( " item_selected " , callable_mp ( validation_panel , & EditorValidationPanel : : update ) . unbind ( 1 ) ) ;
name_edit - > connect ( " text_changed " , callable_mp ( validation_panel , & EditorValidationPanel : : update ) . unbind ( 1 ) ) ;
subfolder_edit - > connect ( " text_changed " , callable_mp ( validation_panel , & EditorValidationPanel : : update ) . unbind ( 1 ) ) ;
script_edit - > connect ( " text_changed " , callable_mp ( validation_panel , & EditorValidationPanel : : update ) . unbind ( 1 ) ) ;
2018-07-19 17:34:22 +02:00
}
PluginConfigDialog : : ~ PluginConfigDialog ( ) {
}