use crate::fyrox::{
core::{
log::Log, parking_lot::Mutex, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
uuid_provider, visitor::prelude::*,
},
engine::SerializationContext,
graph::SceneGraph,
gui::{
button::{ButtonBuilder, ButtonMessage},
dropdown_list::{DropdownList, DropdownListMessage},
grid::{GridBuilder, GridDimension},
inspector::{
editors::{
PropertyEditorBuildContext, PropertyEditorDefinition,
PropertyEditorDefinitionContainer, PropertyEditorInstance,
PropertyEditorMessageContext, PropertyEditorTranslationContext,
},
make_expander_container, FieldKind, Inspector, InspectorBuilder, InspectorContext,
InspectorEnvironment, InspectorError, InspectorMessage, PropertyChanged,
PropertyFilter,
},
message::{MessageDirection, UiMessage},
text::TextBuilder,
utils::make_simple_tooltip,
widget::{Widget, WidgetBuilder},
BuildContext, Control, UiNode, UserInterface,
},
script::Script,
};
use crate::plugins::inspector::EditorEnvironment;
use crate::{
settings::{general::ScriptEditor, SettingsData},
DropdownListBuilder,
};
use fyrox::gui::button::Button;
use fyrox::gui::inspector::InspectorContextArgs;
use fyrox::gui::message::MessageData;
use fyrox::gui::utils::make_dropdown_list_option;
use std::{
any::TypeId,
cell::Cell,
ops::{Deref, DerefMut},
sync::Arc,
};
#[derive(Debug, PartialEq, Clone)]
pub enum ScriptPropertyEditorMessage {
Value(Option<Uuid>),
PropertyChanged(PropertyChanged),
}
impl MessageData for ScriptPropertyEditorMessage {}
#[derive(Clone, Debug, Visit, Reflect, ComponentProvider)]
#[reflect(derived_type = "UiNode")]
pub struct ScriptPropertyEditor {
widget: Widget,
inspector: Handle<Inspector>,
variant_selector: Handle<UiNode>,
open_in_ide_button: Handle<Button>,
selected_script_uuid: Option<Uuid>,
need_context_update: Cell<bool>,
}
impl Deref for ScriptPropertyEditor {
type Target = Widget;
fn deref(&self) -> &Self::Target {
&self.widget
}
}
impl DerefMut for ScriptPropertyEditor {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.widget
}
}
uuid_provider!(ScriptPropertyEditor = "f43c3bfb-8b39-4cc0-be77-04141a45822e");
impl Control for ScriptPropertyEditor {
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if let Some(ScriptPropertyEditorMessage::Value(id)) = message.data_for(self.handle()) {
if self.selected_script_uuid != *id {
self.selected_script_uuid = *id;
self.need_context_update.set(true);
ui.send_message(message.reverse());
}
} else if let Some(InspectorMessage::PropertyChanged(property_changed)) =
message.data_from(self.inspector)
{
ui.post(
self.handle(),
ScriptPropertyEditorMessage::PropertyChanged(property_changed.clone()),
)
}
}
fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
if let Some(ButtonMessage::Click) = message.data_from(self.open_in_ide_button) {
if let Some(uuid) = self.selected_script_uuid {
if let Some(selected_item) = ui
.node(self.variant_selector)
.cast::<DropdownList>()
.expect("Must be DropdownList")
.items
.iter()
.map(|el| {
ui.node(*el)
.user_data_cloned::<(Uuid, Option<String>)>()
.expect("Must be script (UUID, Option<String>)")
})
.find(|el| el.0 == uuid)
{
let (_, script_path) = selected_item;
let cd = std::env::current_dir().expect("Must be current directory");
if let (cd, Some(script_path)) = (cd, script_path) {
if let Some(cd) = cd.to_str() {
let script_full_path = format!("{cd}/{script_path}");
let settings = SettingsData::load();
let script_editor = settings
.expect("Must be editor settings data")
.general
.script_editor;
let script_editor = match &script_editor {
ScriptEditor::VSCode => {
#[cfg(target_os = "macos")]
let app_name = "Visual Studio Code";
#[cfg(not(target_os = "macos"))]
let app_name = "code";
Some(app_name)
}
ScriptEditor::XCode => Some("xcode"),
ScriptEditor::Emacs => Some("emacs"),
ScriptEditor::Zed => Some("zed"),
ScriptEditor::SystemDefault => None,
};
let open_result = if let Some(editor) = script_editor {
open::with(&script_full_path, editor)
} else {
open::that(&script_full_path)
};
if let Err(e) = open_result {
Log::err(format!(
"Error opening script {script_full_path} in external editor: {e}"
))
}
}
}
}
}
}
if let Some(DropdownListMessage::Selection(Some(i))) =
message.data_from(self.variant_selector)
{
let selected_item = ui
.node(self.variant_selector)
.cast::<DropdownList>()
.expect("Must be DropdownList")
.items[*i];
let new_selected_script_data = ui
.node(selected_item)
.user_data_cloned::<(Uuid, Option<String>)>()
.expect("Must be script (UUID, Option<String>)");
ui.send(
self.handle(),
ScriptPropertyEditorMessage::Value(Some(new_selected_script_data.0)),
);
}
}
}
pub struct ScriptPropertyEditorBuilder {
widget_builder: WidgetBuilder,
}
impl ScriptPropertyEditorBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self { widget_builder }
}
pub fn build(
self,
open_in_ide_button: Handle<Button>,
variant_selector: Handle<UiNode>,
script_uuid: Option<Uuid>,
environment: Option<Arc<dyn InspectorEnvironment>>,
layer_index: usize,
generate_property_string_values: bool,
filter: PropertyFilter,
script: &Option<Script>,
definition_container: Arc<PropertyEditorDefinitionContainer>,
name_column_width: f32,
has_parent_object: bool,
ctx: &mut BuildContext,
) -> Handle<ScriptPropertyEditor> {
let context = script.as_ref().map(|script| {
InspectorContext::from_object(InspectorContextArgs {
object: script,
ctx,
definition_container,
environment,
layer_index,
generate_property_string_values,
filter,
name_column_width,
base_path: Default::default(),
has_parent_object,
})
});
let inspector = InspectorBuilder::new(WidgetBuilder::new())
.with_opt_context(context)
.build(ctx);
ctx.add(ScriptPropertyEditor {
widget: self
.widget_builder
.with_preview_messages(true)
.with_child(inspector)
.build(ctx),
selected_script_uuid: script_uuid,
variant_selector,
open_in_ide_button,
inspector,
need_context_update: Cell::new(false),
})
}
}
fn create_items(
serialization_context: Arc<SerializationContext>,
ctx: &mut BuildContext,
) -> Vec<Handle<UiNode>> {
let mut items = vec![{
let empty = make_dropdown_list_option(ctx, "<No Script>");
ctx[empty].user_data = Some(Arc::new(Mutex::new((
Uuid::default(),
Option::<String>::None,
))));
empty
}];
items.extend(serialization_context.script_constructors.map().iter().map(
|(type_uuid, constructor)| {
let item = make_dropdown_list_option(ctx, &constructor.name);
ctx[item].user_data = Some(Arc::new(Mutex::new((
*type_uuid,
Some(constructor.source_path.to_string()),
))));
item
},
));
items
}
fn selected_script(
serialization_context: Arc<SerializationContext>,
value: &Option<Script>,
) -> Option<usize> {
value
.as_ref()
.and_then(|s| {
serialization_context
.script_constructors
.map()
.iter()
.position(|(type_uuid, _)| *type_uuid == s.id())
})
.map(|n| {
n + 1
})
}
#[derive(Debug)]
pub struct ScriptPropertyEditorDefinition {}
impl PropertyEditorDefinition for ScriptPropertyEditorDefinition {
fn value_type_id(&self) -> TypeId {
TypeId::of::<Option<Script>>()
}
fn create_instance(
&self,
ctx: PropertyEditorBuildContext,
) -> Result<PropertyEditorInstance, InspectorError> {
let value = ctx.property_info.cast_value::<Option<Script>>()?;
let environment = EditorEnvironment::try_get_from(&ctx.environment)?;
let items = create_items(environment.serialization_context.clone(), ctx.build_context);
let variant_selector = DropdownListBuilder::new(WidgetBuilder::new())
.with_selected(
selected_script(environment.serialization_context.clone(), value).unwrap_or(0),
)
.with_items(items)
.build(ctx.build_context)
.to_base();
let open_in_ide = ButtonBuilder::new(
WidgetBuilder::new()
.on_column(1)
.with_tooltip(make_simple_tooltip(ctx.build_context, "Open in IDE")),
)
.with_content(
TextBuilder::new(WidgetBuilder::new())
.with_text("Edit...")
.build(ctx.build_context),
)
.build(ctx.build_context);
let script_selector_panel = GridBuilder::new(
WidgetBuilder::new()
.with_child(variant_selector)
.with_child(open_in_ide),
)
.add_row(GridDimension::stretch())
.add_column(GridDimension::stretch())
.add_column(GridDimension::auto())
.build(ctx.build_context);
let editor;
let container = make_expander_container(
ctx.layer_index,
ctx.property_info.display_name,
ctx.property_info.doc,
script_selector_panel,
{
editor = ScriptPropertyEditorBuilder::new(WidgetBuilder::new()).build(
open_in_ide,
variant_selector,
value.as_ref().map(|s| s.id()),
ctx.environment.clone(),
ctx.layer_index,
ctx.generate_property_string_values,
ctx.filter,
value,
ctx.definition_container.clone(),
ctx.name_column_width,
ctx.has_parent_object,
ctx.build_context,
);
editor
},
ctx.name_column_width,
ctx.build_context,
);
Ok(PropertyEditorInstance::Custom {
container,
editor: editor.to_base(),
})
}
fn create_message(
&self,
ctx: PropertyEditorMessageContext,
) -> Result<Option<UiMessage>, InspectorError> {
let value = ctx.property_info.cast_value::<Option<Script>>()?;
let editor_environment = EditorEnvironment::try_get_from(&ctx.environment)?;
let new_script_definitions_items = create_items(
editor_environment.serialization_context.clone(),
&mut ctx.ui.build_ctx(),
);
let instance_ref = ctx
.ui
.node(ctx.instance)
.cast::<ScriptPropertyEditor>()
.ok_or(InspectorError::Custom("Must be EnumPropertyEditor!".into()))?;
let variant_selector_ref = ctx
.ui
.node(instance_ref.variant_selector)
.cast::<DropdownList>()
.ok_or(InspectorError::Custom("Must be a DropDownList".into()))?;
if variant_selector_ref.items.len()
!= editor_environment
.serialization_context
.script_constructors
.map()
.values()
.count()
{
ctx.ui.send_sync(
instance_ref.variant_selector,
DropdownListMessage::Items(new_script_definitions_items),
);
ctx.ui.send_sync(
ctx.instance,
ScriptPropertyEditorMessage::Value(value.as_ref().map(|s| s.id())),
);
}
if instance_ref.selected_script_uuid != value.as_ref().map(|s| s.id())
|| instance_ref.need_context_update.get()
{
instance_ref.need_context_update.set(false);
ctx.ui.send_sync(
ctx.instance,
ScriptPropertyEditorMessage::Value(value.as_ref().map(|s| s.id())),
);
let inspector = instance_ref.inspector;
let context = value
.as_ref()
.map(|script| {
InspectorContext::from_object(InspectorContextArgs {
object: script,
ctx: &mut ctx.ui.build_ctx(),
definition_container: ctx.definition_container.clone(),
environment: ctx.environment.clone(),
layer_index: ctx.layer_index + 1,
generate_property_string_values: ctx.generate_property_string_values,
filter: ctx.filter,
name_column_width: ctx.name_column_width,
base_path: Default::default(),
has_parent_object: ctx.has_parent_object,
})
})
.unwrap_or_default();
Ok(Some(UiMessage::for_widget(
inspector,
InspectorMessage::Context(context),
)))
} else {
let layer_index = ctx.layer_index;
let inspector_ctx = ctx.ui[instance_ref.inspector].context().clone();
if let Some(value) = value.as_ref() {
if let Err(e) = inspector_ctx.sync(
value,
ctx.ui,
layer_index + 1,
ctx.generate_property_string_values,
ctx.filter,
ctx.base_path.clone(),
) {
Err(InspectorError::Group(e))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
}
fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
if ctx.message.direction() == MessageDirection::FromWidget {
if let Some(message) = ctx.message.data::<ScriptPropertyEditorMessage>() {
match message {
ScriptPropertyEditorMessage::Value(value) => {
if let Ok(env) = EditorEnvironment::try_get_from(&ctx.environment) {
let script = value.and_then(|uuid| {
env.serialization_context
.script_constructors
.try_create(&uuid)
});
return Some(PropertyChanged {
name: ctx.name.to_string(),
value: FieldKind::object(script),
});
}
}
ScriptPropertyEditorMessage::PropertyChanged(property_changed) => {
return Some(PropertyChanged {
name: ctx.name.to_string() + ".Some@0",
value: FieldKind::Inspectable(Box::new(property_changed.clone())),
});
}
}
}
}
None
}
}