feat: implement win.save-file-as action

merge-requests/8/head
Nefo Fortressia 2021-10-31 16:58:20 +07:00
parent b54780e45e
commit 390485b3b1
7 changed files with 134 additions and 16 deletions

1
Cargo.lock generated
View File

@ -82,6 +82,7 @@ dependencies = [
"gio", "gio",
"glib", "glib",
"gtk4", "gtk4",
"once_cell",
"relative-path", "relative-path",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -15,3 +15,4 @@ relative-path = "^1.5.0"
gdk = { version = "^0.3.0", package = "gdk4"} gdk = { version = "^0.3.0", package = "gdk4"}
sourceview = { package = "sourceview5", version = "^0.1.0", git = "https://github.com/EchidnaHQ/sourceview" } sourceview = { package = "sourceview5", version = "^0.1.0", git = "https://github.com/EchidnaHQ/sourceview" }
webbrowser = { version = "^0.5.5" } webbrowser = { version = "^0.5.5" }
once_cell = "1"

View File

@ -2,9 +2,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use glib::{ParamFlags, ParamSpec, Value};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::subclass::prelude::*; use gtk::subclass::prelude::*;
use gtk::CompositeTemplate; use gtk::CompositeTemplate;
use once_cell::sync::Lazy;
use std::cell::RefCell;
#[derive(Default, CompositeTemplate)] #[derive(Default, CompositeTemplate)]
#[template(file = "./editor.ui")] #[template(file = "./editor.ui")]
@ -13,6 +16,7 @@ pub struct EchidnaCoreEditor {
pub minimap: TemplateChild<sourceview::Map>, pub minimap: TemplateChild<sourceview::Map>,
#[template_child] #[template_child]
pub sourceview: TemplateChild<sourceview::View>, pub sourceview: TemplateChild<sourceview::View>,
pub file: RefCell<sourceview::File>,
} }
#[glib::object_subclass] #[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<Vec<ParamSpec>> = 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 WidgetImpl for EchidnaCoreEditor {}
impl BoxImpl for EchidnaCoreEditor {} impl BoxImpl for EchidnaCoreEditor {}

View File

@ -14,7 +14,7 @@ glib::wrapper! {
} }
impl EchidnaCoreEditor { impl EchidnaCoreEditor {
pub fn new<P: glib::IsA<sourceview::File>>(file: Option<&P>) -> Self { pub fn new(file: Option<sourceview::File>) -> Self {
let this: Self = let this: Self =
glib::Object::new(&[]).expect("Failed to create 'EchidnaCoreEditor' component."); glib::Object::new(&[]).expect("Failed to create 'EchidnaCoreEditor' component.");
let this_imp = this.to_imp(); let this_imp = this.to_imp();
@ -23,10 +23,12 @@ impl EchidnaCoreEditor {
if file.is_some() { if file.is_some() {
let file = file.unwrap(); let file = file.unwrap();
let file_location = file let file_location = file
.location() .location()
.expect("file is required to have a 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 cancellable = gio::Cancellable::new();
let filepath = file_location.path().expect("No filepath"); let filepath = file_location.path().expect("No filepath");
let file_info_result = let file_info_result =
@ -48,7 +50,7 @@ impl EchidnaCoreEditor {
None => {} None => {}
} }
let file_loader: FileLoader = FileLoader::new(&buffer, file); let file_loader: FileLoader = FileLoader::new(&buffer, &file);
file_loader.load_async( file_loader.load_async(
glib::Priority::default(), glib::Priority::default(),
@ -75,4 +77,8 @@ impl EchidnaCoreEditor {
pub fn to_imp(&self) -> &imp::EchidnaCoreEditor { pub fn to_imp(&self) -> &imp::EchidnaCoreEditor {
imp::EchidnaCoreEditor::from_instance(self) imp::EchidnaCoreEditor::from_instance(self)
} }
pub fn file(&self) -> sourceview::File {
self.property("file").expect("Could not get property 'file' of EchidnaCoreEditor").get::<sourceview::File>().expect("Could not get property 'file' of EchidnaCoreEditor because its type is not IsA<sourceview::File>")
}
} }

View File

@ -3,16 +3,17 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::components::editor::EchidnaCoreEditor; use crate::components::editor::EchidnaCoreEditor;
// use gio::Cancellable; use gio::Cancellable;
use glib::clone; use glib::{clone, Priority};
use gtk::{ use gtk::{
prelude::*, subclass::prelude::*, FileChooserAction, FileChooserDialog, Label, ResponseType, prelude::*, subclass::prelude::*, FileChooserAction, FileChooserDialog, Label, ResponseType,
}; };
use sourceview::File; use sourceview::{prelude::*, Buffer, File, FileSaver};
pub trait FileImplementedEditor { pub trait FileImplementedEditor {
fn action_open_file(&self); fn action_open_file(&self);
fn open_file(notebook: &gtk::Notebook, file: gio::File); fn open_file(notebook: &gtk::Notebook, file: gio::File);
fn action_save_file_as(&self);
} }
impl FileImplementedEditor for super::EchidnaWindow { impl FileImplementedEditor for super::EchidnaWindow {
@ -48,19 +49,19 @@ impl FileImplementedEditor for super::EchidnaWindow {
// TODO: Somehow inserts self to this function. // 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. // 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, => dialog.connect_response(clone!( @weak self as window, =>
move |dialog, response| { move |dialog, response| {
if response == ResponseType::Accept { if response == ResponseType::Accept {
let file = dialog.file().expect(""); let file = dialog.file().expect("");
Self::open_file(&super::imp::EchidnaWindow::from_instance(&window).notebook, file); Self::open_file(&super::imp::EchidnaWindow::from_instance(&window).notebook, file);
} else if response == ResponseType::Cancel { }
dialog.destroy(); dialog.destroy();
} })); }));
} }
fn open_file(notebook: &gtk::Notebook, file_location: gio::File) { fn open_file(notebook: &gtk::Notebook, file_location: gio::File) {
let file = File::builder().location(&file_location).build(); 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( notebook.prepend_page(
&editor_page, &editor_page,
Some(&Label::new(Some( 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::<EchidnaCoreEditor>() {
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();
}));
}
} }

View File

@ -51,6 +51,10 @@
<attribute name="label">Save</attribute> <attribute name="label">Save</attribute>
<attribute name="action">win.save</attribute> <attribute name="action">win.save</attribute>
</item> </item>
<item>
<attribute name="label">Save As...</attribute>
<attribute name="action">win.save-file-as</attribute>
</item>
<item> <item>
<attribute name="label">Save All</attribute> <attribute name="label">Save All</attribute>
<attribute name="action">win.save-all</attribute> <attribute name="action">win.save-all</attribute>

View File

@ -24,7 +24,6 @@ impl MenubarImplementedEditor for EchidnaWindow {
.expect("Could not get object 'menu' from builder."); .expect("Could not get object 'menu' from builder.");
app.set_menubar(Some(&menubar)); app.set_menubar(Some(&menubar));
self.set_show_menubar(true); self.set_show_menubar(true);
{ {
let act_exit: SimpleAction = SimpleAction::new("exit", None); let act_exit: SimpleAction = SimpleAction::new("exit", None);
app.add_action(&act_exit); app.add_action(&act_exit);
@ -111,5 +110,15 @@ impl MenubarImplementedEditor for EchidnaWindow {
window.action_open_file(); 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();
}));
}
} }
} }