587 lines
23 KiB
Rust
587 lines
23 KiB
Rust
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
|
//
|
|
// 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.
|
|
|
|
use crate::{
|
|
command::{Command, CommandContext, CommandStack, CommandTrait},
|
|
fyrox::{
|
|
asset::manager::ResourceManager,
|
|
asset::Resource,
|
|
core::{
|
|
futures::executor::block_on, math::curve::Curve, pool::Handle, reflect::prelude::*,
|
|
some_or_return, visitor::prelude::*,
|
|
},
|
|
engine::Engine,
|
|
gui::{
|
|
border::BorderBuilder,
|
|
button::{ButtonBuilder, ButtonMessage},
|
|
curve::{CurveEditorBuilder, CurveEditorMessage},
|
|
file_browser::FileSelectorMessage,
|
|
grid::{Column, GridBuilder, Row},
|
|
menu::{MenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage},
|
|
message::UiMessage,
|
|
messagebox::{MessageBoxBuilder, MessageBoxResult},
|
|
stack_panel::StackPanelBuilder,
|
|
widget::{WidgetBuilder, WidgetMessage},
|
|
window::{WindowBuilder, WindowMessage, WindowTitle},
|
|
BuildContext, HorizontalAlignment, Orientation, Thickness, UserInterface,
|
|
},
|
|
gui::{
|
|
button::Button,
|
|
curve::CurveEditor,
|
|
file_browser::{FileSelector, FileSelectorMode, FileType},
|
|
menu::MenuItem,
|
|
messagebox::MessageBox,
|
|
style::resource::StyleResourceExt,
|
|
style::Style,
|
|
window::{Window, WindowAlignment},
|
|
},
|
|
resource::curve::{CurveResource, CurveResourceState},
|
|
},
|
|
menu::create_menu_item,
|
|
plugin::EditorPlugin,
|
|
utils::create_file_selector,
|
|
Editor, MessageBoxButtons, MessageBoxMessage,
|
|
};
|
|
use std::{fmt::Debug, path::PathBuf};
|
|
|
|
#[derive(PartialEq, Reflect, Clone, Debug)]
|
|
#[reflect(type_uuid = "f29aa9c7-e0c4-4602-8588-3e8a20ab7cdc")]
|
|
pub struct CurveEditorContext {}
|
|
|
|
impl CommandContext for CurveEditorContext {}
|
|
|
|
#[derive(Debug)]
|
|
struct ModifyCurveCommand {
|
|
curve_resource: CurveResource,
|
|
curve: Curve,
|
|
}
|
|
|
|
impl ModifyCurveCommand {
|
|
fn swap(&mut self) {
|
|
std::mem::swap(&mut self.curve_resource.data_ref().curve, &mut self.curve);
|
|
}
|
|
}
|
|
|
|
impl CommandTrait for ModifyCurveCommand {
|
|
fn name(&mut self, _: &dyn CommandContext) -> String {
|
|
"Modify Curve".to_owned()
|
|
}
|
|
|
|
fn execute(&mut self, _: &mut dyn CommandContext) {
|
|
self.swap();
|
|
}
|
|
|
|
fn revert(&mut self, _: &mut dyn CommandContext) {
|
|
self.swap();
|
|
}
|
|
}
|
|
|
|
struct FileMenu {
|
|
new: Handle<MenuItem>,
|
|
save: Handle<MenuItem>,
|
|
load: Handle<MenuItem>,
|
|
}
|
|
|
|
struct EditMenu {
|
|
undo: Handle<MenuItem>,
|
|
redo: Handle<MenuItem>,
|
|
}
|
|
|
|
struct Menu {
|
|
file: FileMenu,
|
|
edit: EditMenu,
|
|
}
|
|
|
|
pub struct CurveEditorWindow {
|
|
window: Handle<Window>,
|
|
curve_editor: Handle<CurveEditor>,
|
|
ok: Handle<Button>,
|
|
cancel: Handle<Button>,
|
|
curve_resource: Option<CurveResource>,
|
|
command_stack: CommandStack,
|
|
menu: Menu,
|
|
load_file_selector: Handle<FileSelector>,
|
|
save_file_selector: Handle<FileSelector>,
|
|
path: PathBuf,
|
|
save_changes_message_box: Handle<MessageBox>,
|
|
cancel_message_box: Handle<MessageBox>,
|
|
modified: bool,
|
|
backup: Curve,
|
|
}
|
|
|
|
impl CurveEditorWindow {
|
|
pub fn new(ctx: &mut BuildContext) -> Self {
|
|
let file_type = FileType::new()
|
|
.with_extension("crv")
|
|
.with_description("Curve");
|
|
let load_file_selector =
|
|
create_file_selector(ctx, file_type.clone(), FileSelectorMode::Open);
|
|
let save_file_selector = create_file_selector(
|
|
ctx,
|
|
file_type,
|
|
FileSelectorMode::Save {
|
|
default_file_name: PathBuf::from("unnamed.crv"),
|
|
},
|
|
);
|
|
|
|
let save_changes_message_box = MessageBoxBuilder::new(
|
|
WindowBuilder::new(WidgetBuilder::new())
|
|
.open(false)
|
|
.with_title(WindowTitle::text("Unsaved Changes")),
|
|
)
|
|
.with_text(
|
|
"You have unsaved changes, do you want to save it before closing the curve editor?",
|
|
)
|
|
.with_buttons(MessageBoxButtons::YesNoCancel)
|
|
.build(ctx);
|
|
|
|
let cancel_message_box = MessageBoxBuilder::new(
|
|
WindowBuilder::new(WidgetBuilder::new())
|
|
.open(false)
|
|
.with_title(WindowTitle::text("Unsaved Changes")),
|
|
)
|
|
.with_text("You have unsaved changes, do you want to quit the curve editor without saving?")
|
|
.with_buttons(MessageBoxButtons::YesNo)
|
|
.build(ctx);
|
|
|
|
let curve_editor;
|
|
let ok;
|
|
let cancel;
|
|
let new;
|
|
let save;
|
|
let load;
|
|
let undo;
|
|
let redo;
|
|
let window = WindowBuilder::new(WidgetBuilder::new().with_width(400.0).with_height(300.0))
|
|
.open(false)
|
|
.with_content(
|
|
GridBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_child(
|
|
MenuBuilder::new(WidgetBuilder::new())
|
|
.with_items(vec![
|
|
MenuItemBuilder::new(WidgetBuilder::new())
|
|
.with_content(MenuItemContent::text("File"))
|
|
.with_items(vec![
|
|
{
|
|
new = MenuItemBuilder::new(WidgetBuilder::new())
|
|
.with_content(
|
|
MenuItemContent::text_with_shortcut(
|
|
"New", "Ctrl+N",
|
|
),
|
|
)
|
|
.build(ctx);
|
|
new
|
|
},
|
|
{
|
|
load = MenuItemBuilder::new(WidgetBuilder::new())
|
|
.with_content(
|
|
MenuItemContent::text_with_shortcut(
|
|
"Load", "Ctrl+L",
|
|
),
|
|
)
|
|
.build(ctx);
|
|
load
|
|
},
|
|
{
|
|
save = MenuItemBuilder::new(WidgetBuilder::new())
|
|
.with_content(
|
|
MenuItemContent::text_with_shortcut(
|
|
"Save", "Ctrl+S",
|
|
),
|
|
)
|
|
.build(ctx);
|
|
save
|
|
},
|
|
])
|
|
.build(ctx),
|
|
MenuItemBuilder::new(WidgetBuilder::new())
|
|
.with_content(MenuItemContent::text("Edit"))
|
|
.with_items(vec![
|
|
{
|
|
undo = MenuItemBuilder::new(WidgetBuilder::new())
|
|
.with_content(
|
|
MenuItemContent::text_with_shortcut(
|
|
"Undo", "Ctrl+Z",
|
|
),
|
|
)
|
|
.build(ctx);
|
|
undo
|
|
},
|
|
{
|
|
redo = MenuItemBuilder::new(WidgetBuilder::new())
|
|
.with_content(
|
|
MenuItemContent::text_with_shortcut(
|
|
"Redo", "Ctrl+Y",
|
|
),
|
|
)
|
|
.build(ctx);
|
|
redo
|
|
},
|
|
])
|
|
.build(ctx),
|
|
])
|
|
.build(ctx),
|
|
)
|
|
.with_child(
|
|
BorderBuilder::new(
|
|
WidgetBuilder::new()
|
|
.on_row(1)
|
|
.on_column(0)
|
|
.with_background(ctx.style.property(Style::BRUSH_DARKEST))
|
|
.with_child({
|
|
curve_editor = CurveEditorBuilder::new(
|
|
WidgetBuilder::new().with_enabled(false),
|
|
)
|
|
.build(ctx);
|
|
curve_editor
|
|
}),
|
|
)
|
|
.build(ctx),
|
|
)
|
|
.with_child(
|
|
StackPanelBuilder::new(
|
|
WidgetBuilder::new()
|
|
.on_row(2)
|
|
.on_column(0)
|
|
.with_horizontal_alignment(HorizontalAlignment::Right)
|
|
.with_child({
|
|
ok = ButtonBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_margin(Thickness::uniform(1.0))
|
|
.with_width(100.0),
|
|
)
|
|
.with_text("OK")
|
|
.build(ctx);
|
|
ok
|
|
})
|
|
.with_child({
|
|
cancel = ButtonBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_margin(Thickness::uniform(1.0))
|
|
.with_width(100.0),
|
|
)
|
|
.with_text("Cancel")
|
|
.build(ctx);
|
|
cancel
|
|
}),
|
|
)
|
|
.with_orientation(Orientation::Horizontal)
|
|
.build(ctx),
|
|
),
|
|
)
|
|
.add_row(Row::strict(25.0))
|
|
.add_row(Row::stretch())
|
|
.add_row(Row::strict(25.0))
|
|
.add_column(Column::stretch())
|
|
.build(ctx),
|
|
)
|
|
.with_remove_on_close(true)
|
|
.with_title(WindowTitle::text("Curve Editor"))
|
|
.with_tab_label("Curve")
|
|
.build(ctx);
|
|
|
|
Self {
|
|
window,
|
|
curve_editor,
|
|
ok,
|
|
cancel,
|
|
curve_resource: None,
|
|
command_stack: CommandStack::new(false, 2048),
|
|
menu: Menu {
|
|
file: FileMenu { new, save, load },
|
|
edit: EditMenu { undo, redo },
|
|
},
|
|
load_file_selector,
|
|
save_file_selector,
|
|
path: Default::default(),
|
|
save_changes_message_box,
|
|
modified: false,
|
|
backup: Default::default(),
|
|
cancel_message_box,
|
|
}
|
|
}
|
|
|
|
fn destroy(self, ui: &UserInterface) {
|
|
ui.send(self.cancel_message_box, WidgetMessage::Remove);
|
|
ui.send(self.save_changes_message_box, WidgetMessage::Remove);
|
|
ui.send(self.load_file_selector, WidgetMessage::Remove);
|
|
ui.send(self.save_file_selector, WidgetMessage::Remove);
|
|
ui.send(self.window, WindowMessage::Close);
|
|
}
|
|
|
|
pub fn open(&self, ui: &UserInterface) {
|
|
ui.send(
|
|
self.window,
|
|
WindowMessage::Open {
|
|
alignment: WindowAlignment::Center,
|
|
modal: true,
|
|
focus_content: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
fn sync_to_model(&mut self, ui: &UserInterface) {
|
|
if let Some(curve_resource) = self.curve_resource.as_ref() {
|
|
ui.send_sync(
|
|
self.curve_editor,
|
|
CurveEditorMessage::Sync(vec![curve_resource.data_ref().curve.clone()]),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn save(&self) {
|
|
if let Some(curve_resource) = self.curve_resource.as_ref() {
|
|
if let Some(state) = curve_resource.state().data() {
|
|
let mut visitor = Visitor::new();
|
|
state.curve.visit("Curve", &mut visitor).unwrap();
|
|
visitor.save_binary_to_file(&self.path).unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_curve(
|
|
&mut self,
|
|
resource_manager: &ResourceManager,
|
|
curve: CurveResource,
|
|
ui: &UserInterface,
|
|
) {
|
|
self.backup = curve.data_ref().curve.clone();
|
|
self.curve_resource = Some(curve);
|
|
|
|
ui.send(self.curve_editor, WidgetMessage::Enabled(true));
|
|
|
|
self.sync_to_model(ui);
|
|
self.sync_title(resource_manager, ui);
|
|
|
|
self.modified = false;
|
|
|
|
self.command_stack.clear(&mut CurveEditorContext {});
|
|
}
|
|
|
|
fn sync_title(&self, resource_manager: &ResourceManager, ui: &UserInterface) {
|
|
let title = if let Some(curve_resource) = self.curve_resource.as_ref() {
|
|
match resource_manager.resource_path(curve_resource.as_ref()) {
|
|
Some(path) => {
|
|
format!("Curve Editor - {}", path.display())
|
|
}
|
|
None => "Curve Editor - Unnamed Curve".to_string(),
|
|
}
|
|
} else {
|
|
"Curve Editor".to_string()
|
|
};
|
|
|
|
ui.send(self.window, WindowMessage::Title(WindowTitle::text(title)));
|
|
}
|
|
|
|
fn revert(&self) {
|
|
if let Some(curve_resource) = self.curve_resource.as_ref() {
|
|
curve_resource.data_ref().curve = self.backup.clone();
|
|
}
|
|
}
|
|
|
|
fn open_save_file_dialog(&self, resource_manager: &ResourceManager, ui: &UserInterface) {
|
|
ui.send(
|
|
self.save_file_selector,
|
|
FileSelectorMessage::Root(Some(resource_manager.registry_folder())),
|
|
);
|
|
|
|
ui.send(
|
|
self.save_file_selector,
|
|
WindowMessage::Open {
|
|
alignment: WindowAlignment::Center,
|
|
modal: true,
|
|
focus_content: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
pub fn handle_ui_message(mut self, message: &UiMessage, engine: &mut Engine) -> Option<Self> {
|
|
let ui = &engine.user_interfaces.first_mut();
|
|
|
|
if let Some(ButtonMessage::Click) = message.data() {
|
|
if message.destination() == self.cancel {
|
|
if self.modified && self.curve_resource.is_some() {
|
|
ui.send(
|
|
self.cancel_message_box,
|
|
MessageBoxMessage::Open {
|
|
text: None,
|
|
title: None,
|
|
},
|
|
);
|
|
} else {
|
|
self.destroy(ui);
|
|
return None;
|
|
}
|
|
} else if message.destination() == self.ok {
|
|
if self.modified && self.curve_resource.is_some() {
|
|
if self.path == PathBuf::default() {
|
|
ui.send(
|
|
self.save_changes_message_box,
|
|
MessageBoxMessage::Open {
|
|
text: None,
|
|
title: None,
|
|
},
|
|
);
|
|
} else {
|
|
self.save();
|
|
self.destroy(ui);
|
|
return None;
|
|
}
|
|
} else {
|
|
self.destroy(ui);
|
|
return None;
|
|
}
|
|
}
|
|
} else if let Some(CurveEditorMessage::Sync(curve)) = message.data_from(self.curve_editor) {
|
|
if let Some(curve_resource) = self.curve_resource.as_ref() {
|
|
self.command_stack.do_command(
|
|
Command::new(ModifyCurveCommand {
|
|
curve_resource: curve_resource.clone(),
|
|
curve: curve.first().cloned().unwrap(),
|
|
}),
|
|
&mut CurveEditorContext {},
|
|
);
|
|
|
|
self.modified = true;
|
|
}
|
|
} else if let Some(MenuItemMessage::Click) = message.data() {
|
|
if message.destination() == self.menu.edit.undo {
|
|
self.command_stack.undo(&mut CurveEditorContext {});
|
|
|
|
self.sync_to_model(ui);
|
|
} else if message.destination() == self.menu.edit.redo {
|
|
self.command_stack.redo(&mut CurveEditorContext {});
|
|
|
|
self.sync_to_model(ui);
|
|
} else if message.destination() == self.menu.file.load {
|
|
ui.send(
|
|
self.load_file_selector,
|
|
FileSelectorMessage::Root(Some(engine.resource_manager.registry_folder())),
|
|
);
|
|
|
|
ui.send(
|
|
self.load_file_selector,
|
|
WindowMessage::Open {
|
|
alignment: WindowAlignment::Center,
|
|
modal: true,
|
|
focus_content: true,
|
|
},
|
|
);
|
|
} else if message.destination() == self.menu.file.new {
|
|
self.path = Default::default();
|
|
|
|
self.set_curve(
|
|
&engine.resource_manager,
|
|
Resource::new_embedded(CurveResourceState::default()),
|
|
ui,
|
|
);
|
|
} else if message.destination() == self.menu.file.save {
|
|
if self.path == PathBuf::default() {
|
|
self.open_save_file_dialog(&engine.resource_manager, ui);
|
|
} else {
|
|
self.save();
|
|
}
|
|
}
|
|
} else if let Some(FileSelectorMessage::Commit(path)) = message.data() {
|
|
if message.destination() == self.load_file_selector {
|
|
if let Ok(curve) =
|
|
block_on(engine.resource_manager.request::<CurveResourceState>(path))
|
|
{
|
|
self.path.clone_from(path);
|
|
self.set_curve(&engine.resource_manager, curve, ui);
|
|
}
|
|
} else if message.destination() == self.save_file_selector {
|
|
self.path.clone_from(path);
|
|
self.save();
|
|
}
|
|
} else if let Some(MessageBoxMessage::Close(result)) =
|
|
message.data_from(self.save_changes_message_box)
|
|
{
|
|
match result {
|
|
MessageBoxResult::No => {
|
|
self.revert();
|
|
self.destroy(ui);
|
|
return None;
|
|
}
|
|
MessageBoxResult::Yes => {
|
|
if self.path == PathBuf::default() {
|
|
self.open_save_file_dialog(&engine.resource_manager, ui);
|
|
} else {
|
|
self.save();
|
|
self.destroy(ui);
|
|
return None;
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
} else if let Some(MessageBoxMessage::Close(MessageBoxResult::Yes)) =
|
|
message.data_from(self.cancel_message_box)
|
|
{
|
|
self.revert();
|
|
self.destroy(ui);
|
|
return None;
|
|
}
|
|
|
|
Some(self)
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct CurveEditorPlugin {
|
|
curve_editor_window: Option<CurveEditorWindow>,
|
|
open_curve_editor: Handle<MenuItem>,
|
|
}
|
|
|
|
impl CurveEditorPlugin {
|
|
pub const CURVE_EDITOR: Uuid = uuid!("20705d17-741d-45bf-a9ba-ec3cee34ac2b");
|
|
|
|
fn on_open_curve_editor_clicked(&mut self, editor: &mut Editor) {
|
|
let ui = editor.engine.user_interfaces.first_mut();
|
|
let ctx = &mut ui.build_ctx();
|
|
let curve_editor = self
|
|
.curve_editor_window
|
|
.get_or_insert_with(|| CurveEditorWindow::new(ctx));
|
|
curve_editor.open(ui);
|
|
}
|
|
}
|
|
|
|
impl EditorPlugin for CurveEditorPlugin {
|
|
fn on_start(&mut self, editor: &mut Editor) {
|
|
let ui = editor.engine.user_interfaces.first_mut();
|
|
let ctx = &mut ui.build_ctx();
|
|
self.open_curve_editor = create_menu_item("Curve Editor", Self::CURVE_EDITOR, vec![], ctx);
|
|
ui.send(
|
|
editor.menu.utils_menu.menu,
|
|
MenuItemMessage::AddItem(self.open_curve_editor),
|
|
);
|
|
}
|
|
|
|
fn on_ui_message(&mut self, message: &mut UiMessage, editor: &mut Editor) {
|
|
if let Some(MenuItemMessage::Click) = message.data() {
|
|
if message.destination() == self.open_curve_editor {
|
|
self.on_open_curve_editor_clicked(editor)
|
|
}
|
|
}
|
|
let curve_editor = some_or_return!(self.curve_editor_window.take());
|
|
self.curve_editor_window = curve_editor.handle_ui_message(message, &mut editor.engine);
|
|
}
|
|
}
|