diff --git a/Cargo.lock b/Cargo.lock index 4fca738..9326399 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,7 @@ dependencies = [ "gio", "glib", "gtk4", + "once_cell", "relative-path", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 86f72b7..65a0203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ relative-path = "^1.5.0" gdk = { version = "^0.3.0", package = "gdk4"} sourceview = { package = "sourceview5", version = "^0.1.0", git = "https://github.com/EchidnaHQ/sourceview" } webbrowser = { version = "^0.5.5" } +once_cell = "1" \ No newline at end of file diff --git a/src/components/editor/imp.rs b/src/components/editor/imp.rs index 5c95c86..5a963bc 100644 --- a/src/components/editor/imp.rs +++ b/src/components/editor/imp.rs @@ -2,9 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use glib::{ParamFlags, ParamSpec, Value}; use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::CompositeTemplate; +use once_cell::sync::Lazy; +use std::cell::RefCell; #[derive(Default, CompositeTemplate)] #[template(file = "./editor.ui")] @@ -13,6 +16,7 @@ pub struct EchidnaCoreEditor { pub minimap: TemplateChild, #[template_child] pub sourceview: TemplateChild, + pub file: RefCell, } #[glib::object_subclass] @@ -30,6 +34,39 @@ impl ObjectSubclass for EchidnaCoreEditor { } } -impl ObjectImpl for EchidnaCoreEditor {} +impl ObjectImpl for EchidnaCoreEditor { + fn properties() -> &'static [ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ParamSpec::new_object( + "file", + "file", + "the file of the editor", + sourceview::File::static_type(), + ParamFlags::READWRITE, + )] + }); + + PROPERTIES.as_ref() + } + + fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, spec: &ParamSpec) { + match spec.name() { + "file" => { + let file: sourceview::File = value + .get() + .expect("The file needs to be a sourceview::File"); + self.file.replace(file); + } + _ => unimplemented!(), + } + } + + fn property(&self, _obj: &Self::Type, _id: usize, spec: &ParamSpec) -> Value { + match spec.name() { + "file" => self.file.borrow().to_value(), + _ => unimplemented!(), + } + } +} impl WidgetImpl for EchidnaCoreEditor {} impl BoxImpl for EchidnaCoreEditor {} diff --git a/src/components/editor/mod.rs b/src/components/editor/mod.rs index e743455..28cadf6 100644 --- a/src/components/editor/mod.rs +++ b/src/components/editor/mod.rs @@ -14,7 +14,7 @@ glib::wrapper! { } impl EchidnaCoreEditor { - pub fn new>(file: Option<&P>) -> Self { + pub fn new(file: Option) -> Self { let this: Self = glib::Object::new(&[]).expect("Failed to create 'EchidnaCoreEditor' component."); let this_imp = this.to_imp(); @@ -23,10 +23,12 @@ impl EchidnaCoreEditor { if file.is_some() { let file = file.unwrap(); - let file_location = file .location() .expect("file is required to have a location"); + + this.set_property("file", &file).expect("Could not set the 'file' property of EchidnaCoreEditor"); + let cancellable = gio::Cancellable::new(); let filepath = file_location.path().expect("No filepath"); let file_info_result = @@ -48,7 +50,7 @@ impl EchidnaCoreEditor { None => {} } - let file_loader: FileLoader = FileLoader::new(&buffer, file); + let file_loader: FileLoader = FileLoader::new(&buffer, &file); file_loader.load_async( glib::Priority::default(), @@ -75,4 +77,8 @@ impl EchidnaCoreEditor { pub fn to_imp(&self) -> &imp::EchidnaCoreEditor { imp::EchidnaCoreEditor::from_instance(self) } + + pub fn file(&self) -> sourceview::File { + self.property("file").expect("Could not get property 'file' of EchidnaCoreEditor").get::().expect("Could not get property 'file' of EchidnaCoreEditor because its type is not IsA") + } } diff --git a/src/components/window/file.rs b/src/components/window/file.rs index 84f8568..3025484 100644 --- a/src/components/window/file.rs +++ b/src/components/window/file.rs @@ -3,16 +3,17 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::components::editor::EchidnaCoreEditor; -// use gio::Cancellable; -use glib::clone; +use gio::Cancellable; +use glib::{clone, Priority}; use gtk::{ prelude::*, subclass::prelude::*, FileChooserAction, FileChooserDialog, Label, ResponseType, }; -use sourceview::File; +use sourceview::{prelude::*, Buffer, File, FileSaver}; pub trait FileImplementedEditor { fn action_open_file(&self); fn open_file(notebook: >k::Notebook, file: gio::File); + fn action_save_file_as(&self); } impl FileImplementedEditor for super::EchidnaWindow { @@ -48,19 +49,19 @@ impl FileImplementedEditor for super::EchidnaWindow { // TODO: Somehow inserts self to this function. // This function sets the callback function as 'static, which for some reasons ban cloning self into it. Idk why. dialog.connect_response(clone!( @weak self as window, => - move |dialog, response| { - if response == ResponseType::Accept { - let file = dialog.file().expect(""); - Self::open_file(&super::imp::EchidnaWindow::from_instance(&window).notebook, file); + move |dialog, response| { + if response == ResponseType::Accept { + let file = dialog.file().expect(""); + Self::open_file(&super::imp::EchidnaWindow::from_instance(&window).notebook, file); - } else if response == ResponseType::Cancel { - dialog.destroy(); - } })); + } + dialog.destroy(); + })); } fn open_file(notebook: >k::Notebook, file_location: gio::File) { let file = File::builder().location(&file_location).build(); - let editor_page = EchidnaCoreEditor::new(Some(&file)); + let editor_page = EchidnaCoreEditor::new(Some(file)); notebook.prepend_page( &editor_page, Some(&Label::new(Some( @@ -74,4 +75,63 @@ impl FileImplementedEditor for super::EchidnaWindow { ))), ); } + + fn action_save_file_as(&self) { + let dialog = FileChooserDialog::new( + Some("Save File As"), + Some(self), + FileChooserAction::Save, + &[ + ("Cancel", ResponseType::Cancel), + ("Save", ResponseType::Accept), + ], + ); + + dialog.set_current_name("untitled"); + + dialog.show(); + + dialog.connect_response(clone!( @weak self as window, => + move |dialog, response| { + if response == ResponseType::Accept { + let file = dialog.file().expect(""); + let window_imp = window.to_imp(); + let page: EchidnaCoreEditor; + + match window_imp.notebook + .nth_page( + Some(window_imp.notebook + .current_page() + .expect( + "No tabs is the current tab, probably all tabs closed. No files to save" + ) + ) + ).expect( + "Couldn't get the page of the current index. Try again." + ).downcast::() { + Ok(res) => page = res, + Err(e) => panic!(format!("We got an error when trying to downcast the current tab page into EchidnaCoreEditor:\n{}", e)) + } + + let buffer: Buffer = page.to_imp().sourceview.buffer().downcast().expect("Could not downcast the editor's buffer to GtkSourceBuffer."); + let cancellable = Cancellable::new(); + + let file_saver = FileSaver::with_target( + &buffer, + &page.file(), &file); + file_saver.save_async( + Priority::default(), + Some(&cancellable), + |_, _| {}, + |result| { + if result.is_err() { + panic!(format!("Found an error while saving the file:\n{}", result.err().expect("No error"))) + } + }); + + } + dialog.destroy(); + + })); + } } diff --git a/ui/menu.ui b/src/components/window/menu.ui similarity index 97% rename from ui/menu.ui rename to src/components/window/menu.ui index 5380d0c..1d08627 100644 --- a/ui/menu.ui +++ b/src/components/window/menu.ui @@ -51,6 +51,10 @@ Save win.save + + Save As... + win.save-file-as + Save All win.save-all diff --git a/src/components/window/menubar.rs b/src/components/window/menubar.rs index bc78fca..dec0196 100644 --- a/src/components/window/menubar.rs +++ b/src/components/window/menubar.rs @@ -24,7 +24,6 @@ impl MenubarImplementedEditor for EchidnaWindow { .expect("Could not get object 'menu' from builder."); app.set_menubar(Some(&menubar)); self.set_show_menubar(true); - { let act_exit: SimpleAction = SimpleAction::new("exit", None); app.add_action(&act_exit); @@ -111,5 +110,15 @@ impl MenubarImplementedEditor for EchidnaWindow { window.action_open_file(); })); } + { + let action_save_file_as = SimpleAction::new("save-file-as", None); + + self.add_action(&action_save_file_as); + + action_save_file_as.connect_activate(clone!(@weak self as window => + move |_action, _variant| { + window.action_save_file_as(); + })); + } } }