Compare commits

...

36 Commits

Author SHA1 Message Date
Nefo Fortressia 319bc3914b
Merge branch 'main' into feat/new-file 2021-12-28 08:55:01 +07:00
Nefo Fortressia e077e690f2
fix: panic! expecting &str in editor/mod.rs
In Rust 2021, panic! automatically formats the message.
Because of this, the panic! macro complained about the argument not being &str
as format! returns a String instead.
2021-12-28 08:37:09 +07:00
Nefo Fortressia c6da158bb6
feat: implement saving file normally
Merge branch 'feat/save' of https://gitea.treehouse.systems/fortressia/echidna

See #21
2021-12-27 21:03:08 +07:00
Nefo Fortressia f785463540
refactor: refactor things with Clippy's help
Merge branch 'refactor/clippy' of https://gitea.treehouse.systems/fortressia/echidna

See #23
2021-12-27 20:59:09 +07:00
Nefo Fortressia fa128ca284
feat: enable GtkSourceView line numbers and line marks
Merge branch 'feat/editor-gutter' of https://gitea.treehouse.systems/fortressia/echidna

See: #22
2021-12-27 20:50:25 +07:00
Nefo Fortressia e0d353a10b
refactor: remove double pointer in setting a file's tab title
This is because the file name is an &OsStr converted to &str and thus already a pointer.
2021-12-18 17:34:27 +07:00
Nefo Fortressia b53915afa0
refactor: set unused parameters in TabLabel as unused 2021-12-18 17:30:28 +07:00
Nefo Fortressia 9b99932fdd
refactor: use match when processing EchidnaCoreEditor's initial file
Just like in 8e31be7.
2021-12-18 17:29:39 +07:00
Nefo Fortressia 8e31be7e47
refactor: don't unwrap tab label' childs and use match when prepending it
If statements are for booleans, matches are intended for Options like these.

See: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_unwrap
2021-12-18 17:21:50 +07:00
Nefo Fortressia c8fd305d90
refactor: removed unused imports in closeable_tab.rs 2021-12-18 17:17:53 +07:00
Nefo Fortressia 10ae0cf783
feat: implement Default for EchidnaSidebar
EchidnaSidebar doesn't take any parameters, thus allowing Default to be easily implemented on it.
2021-12-18 17:17:15 +07:00
Nefo Fortressia 9e06e96fed
chore: remove clippy.toml
Clippy categories can only be configured trough parameters and clippy macros.

They can't be configured in config files.
2021-12-18 17:16:16 +07:00
Nefo Fortressia 5e574299f3 Merge branch 'main' into 'feat/save'
# Conflicts:
#   src/components/window/file.rs
2021-12-18 04:53:27 +00:00
Nefo Fortressia e3b330a57c fix: activity views not appearing
Merge branch 'feat/activitybar' into 'main'

See merge request EchidnaHQ/Echidna!11
2021-12-17 02:38:12 +00:00
Nefo Fortressia d98266255a refactor: use gtk::show_uri() instead of webbrowser crate
Merge branch 'refactor/gtk_show_uri_on_window' into 'main'

See merge request EchidnaHQ/Echidna!17
2021-12-17 02:32:51 +00:00
Nefo Fortressia c1193df8a2 refactor: make tab label their own widget
Merge branch 'refactor/closable-tabs' into 'main'

See merge request EchidnaHQ/Echidna!15
2021-12-17 02:29:51 +00:00
Nefo Fortressia 4583f993ab refactor: reexport functionalities to the crate for easier importing
Merge branch 'refactor/exports' into 'main'

See merge request EchidnaHQ/Echidna!13
2021-12-17 02:29:11 +00:00
Nefo Fortressia 3c9b665cbf fix: fix unexpected } in workspace.rs
Merge branch 'fix/unexpected-curly-bracket' into 'main'

See merge request EchidnaHQ/Echidna!18
2021-12-17 02:26:44 +00:00
Nefo Fortressia c3a0e5dcee
feat: enable GtkSourceView line numbers and line marks
See https://gitlab.com/EchidnaHQ/Echidna/-/issues/25
2021-12-17 09:22:57 +07:00
Nefo Fortressia 38f072617f refactor: use GtkFileChooserNative for file dialogs
Merge branch 'refactor/file-chooser-native' into 'main'

Closes #22

See merge request EchidnaHQ/Echidna!19
2021-12-16 10:17:44 +00:00
Nefo Fortressia d8c7f104ab
feat: implement "Save" menu 2021-12-16 14:06:33 +07:00
Nefo Fortressia 52d1bdf7db
refactor: move the code for saving files to EchidnaCoreEditor
Additionally add support for normal saving ("Save File").

This change gives support for saving files other than the current one.

EchidnaWindow.save_file_as() was only able to save the current tab opened.
2021-12-16 14:04:53 +07:00
Nefo Fortressia ce8fb5dd12
refactor: separate get_current_tab() from save_file_as()
EchidnaWindow.get_current_tab() can now be used independently.

This will help with reusing it in another places. I think it will be used a lot.

Additionally, it will return Result<A, &str> instead of panicking, as it's a utils function now and should never panic.
2021-12-16 11:08:42 +07:00
Nefo Fortressia f29b48b9b4 refactor: migrate to Rust 2021
Merge branch 'refactor/rust2021' into 'main'

See merge request EchidnaHQ/Echidna!16
2021-12-15 22:09:51 +00:00
Nefo Fortressia d480fdb02b
refactor: move saving file code to save_file_as()
save_file_as() can now be run independently.
2021-12-15 19:45:19 +07:00
Nefo Fortressia cfe8c4dbe4
refactor: use GtkFileChooserNative for file dialogs
GtkFileChooserNative uses the platform's APIs, and thus the Portal API.

This is important since we want to integrate this well with Flatpak.

See:
https://docs.flatpak.org/en/latest/portals-gtk.html
2021-12-14 08:45:21 +07:00
Nefo Fortressia 7478d894ea
fix: fix unexpected } in workspace.rs
I think this is due to git add -p mistakes when I did 1b9f20b.

Not affecting anything, as workspace.rs is still WIP.
2021-12-14 08:29:48 +07:00
Nefo Fortressia 92f6770876
refactor: use gtk::show_uri() instead of webbrowser crate 2021-12-13 12:30:43 +07:00
Nefo Fortressia d29223e17f
chore: bump Rust edition to Rust 2021
Follow up of 183d548, which migrate old syntaxes to the new ones.

See:
https://doc.rust-lang.org/nightly/edition-guide/rust-2021/index.html
2021-12-13 11:25:33 +07:00
Nefo Fortressia b9a0719750
refactor: migrate panic!() usage to Rust 2021
This commit refactors code to conform to some changes in Rust 2021:
- The panic!() macro now uses format_args!() like println!().
- The usage of panic!(x) is now deprecated if x is not a string literal.

See:
https://doc.rust-lang.org/nightly/edition-guide/rust-2021/panic-macro-consistency.html
2021-12-13 11:25:10 +07:00
Nefo Fortressia 0de1d86494 refactor: update all Git links to the GitLab repo
Merge branch 'refactor/gitlab' into 'main'

Closes #9

See merge request EchidnaHQ/Echidna!10
2021-11-16 07:56:42 +00:00
Nefo Fortressia 18ac470c47
refactor: make tab label their own widget
This helps with downcasting which greatly helps with type safety.
2021-11-15 07:14:59 +07:00
Nefo Fortressia 9a21bd54e5
Merge branch 'refactor/exports' into refactor/closable-tabs 2021-11-15 06:59:46 +07:00
Nefo Fortressia bc1aa68fbd
Merge branch 'main' into refactor/gitlab 2021-11-10 16:36:08 +07:00
Nefo Fortressia 89cc40cdcb
refactor: update all Git links to the GitLab repo 2021-11-10 16:32:40 +07:00
Nefo Fortressia 3f80738855
fix: activity views not appearing 2021-11-09 05:16:37 +07:00
16 changed files with 368 additions and 337 deletions

118
Cargo.lock generated
View File

@ -20,12 +20,6 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.14.7" version = "0.14.7"
@ -68,12 +62,6 @@ dependencies = [
"smallvec", "smallvec",
] ]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "echidna" name = "echidna"
version = "0.1.0" version = "0.1.0"
@ -87,7 +75,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sourceview5", "sourceview5",
"webbrowser",
] ]
[[package]] [[package]]
@ -438,36 +425,12 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "js-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.102" version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.4" version = "0.6.4"
@ -843,87 +806,6 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wasm-bindgen"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
[[package]]
name = "web-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webbrowser"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecad156490d6b620308ed411cfee90d280b3cbd13e189ea0d3fada8acc89158a"
dependencies = [
"web-sys",
"widestring",
"winapi",
]
[[package]]
name = "widestring"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "echidna" name = "echidna"
version = "0.1.0" version = "0.1.0"
edition = "2018" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -14,5 +14,4 @@ serde = { version = "^1.0.130", features = ["derive"] }
relative-path = "^1.5.0" 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" }
once_cell = "1" once_cell = "1"

View File

@ -1 +0,0 @@
"clippy::style" = "deny"

View File

@ -15,10 +15,11 @@
<object class="GtkSourceView" id="sourceview"> <object class="GtkSourceView" id="sourceview">
<property name="vexpand">1</property> <property name="vexpand">1</property>
<property name="hexpand">1</property> <property name="hexpand">1</property>
<property name="show-line-numbers">1</property>
<property name="show-line-marks">1</property>
<property name="buffer"> <property name="buffer">
<object class="GtkSourceBuffer"></object> <object class="GtkSourceBuffer"></object>
</property> </property>
</object> </object>
</child> </child>
</object> </object>

View File

@ -3,9 +3,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
pub mod imp; pub mod imp;
use gio::Cancellable;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::subclass::prelude::*; use gtk::subclass::prelude::*;
use sourceview::{prelude::*, Buffer, FileExt as SourceFileExt, FileLoader, LanguageManager}; use sourceview::{
prelude::*, Buffer, FileExt as SourceFileExt, FileLoader, FileSaver, LanguageManager,
};
glib::wrapper! { glib::wrapper! {
pub struct EchidnaCoreEditor(ObjectSubclass<imp::EchidnaCoreEditor>) pub struct EchidnaCoreEditor(ObjectSubclass<imp::EchidnaCoreEditor>)
@ -21,57 +24,59 @@ impl EchidnaCoreEditor {
// Without cloning it, for some reasons the Rust compiler complains about &this.to_imp().sourceview not being IsA<sourceview::View> // Without cloning it, for some reasons the Rust compiler complains about &this.to_imp().sourceview not being IsA<sourceview::View>
this_imp.minimap.set_view(&this_imp.sourceview.clone()); this_imp.minimap.set_view(&this_imp.sourceview.clone());
if file.is_some() { match file {
let file = file.unwrap(); Some(file) => {
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) this.set_property("file", &file)
.expect("Could not set the 'file' property of EchidnaCoreEditor"); .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 info = file_location let info = file_location
.query_info("*", gio::FileQueryInfoFlags::NONE, Some(&cancellable)) .query_info("*", gio::FileQueryInfoFlags::NONE, Some(&cancellable))
.expect("Could not query the info for file"); .expect("Could not query the info for file");
let content_type = info let content_type = info
.content_type() .content_type()
.expect(format!("It does not seem like {:?} has a type", filepath).as_str()); .expect(format!("It does not seem like {:?} has a type", filepath).as_str());
{ {
println!( println!(
"Opened {} and found its content type is {}.", "Opened {} and found its content type is {}.",
"file", "file",
content_type.to_string() content_type.to_string()
); );
let buffer = this_imp.sourceview.buffer().downcast::<Buffer>().expect("Cannot downcast the sourceview's buffer. Maybe the sourceview's buffer is not IsA<sourceview::Buffer>."); let buffer = this_imp.sourceview.buffer().downcast::<Buffer>().expect("Cannot downcast the sourceview's buffer. Maybe the sourceview's buffer is not IsA<sourceview::Buffer>.");
let language_manager = LanguageManager::new(); let language_manager = LanguageManager::new();
let language = language_manager.guess_language( let language = language_manager.guess_language(
Some(&info.name().to_str().expect( Some(&info.name().to_str().expect(
"Could not open the file because its name is not supported by Unicode.", "Could not open the file because its name is not supported by Unicode.",
)), )),
None, None,
); );
match language { match language {
Some(lang) => buffer.set_language(Some(&lang)), Some(lang) => buffer.set_language(Some(&lang)),
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(),
Some(&cancellable), Some(&cancellable),
|_, _| {}, |_, _| {},
|result| { |result| {
if result.is_err() { if result.is_err() {
panic!(result.err()); panic!("Found an error when loading the file into the text editor's buffer. {:#?}", result.err());
} }
}, },
); );
}
} }
None => {}
} }
this this
} }
@ -83,4 +88,50 @@ impl EchidnaCoreEditor {
pub fn file(&self) -> sourceview::File { 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>") 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>")
} }
pub fn save_file(&self, save_as: Option<&gio::File>) -> Result<(), &str> {
let window_imp = self.to_imp();
let buffer = self.to_imp().sourceview.buffer().downcast::<Buffer>();
match buffer {
Ok(buffer) => {
let cancellable = Cancellable::new();
let mut file_saver: Option<FileSaver> = None;
let result: Result<(), &str> = match save_as {
Some(file) => {
file_saver = Some(FileSaver::with_target(&buffer, &self.file(), file));
Ok(())
}
None => match self.file().location() {
Some(_) => {
file_saver = Some(FileSaver::new(&buffer, &self.file()));
Ok(())
}
None => Err("The file location must exist. Please do \"Save As\""),
},
};
match result {
Err(result) => Err(result),
Ok(_) => {
file_saver.unwrap().save_async(
glib::Priority::default(),
Some(&cancellable),
|_, _| {},
|result| {
if result.is_err() {
panic!(
"Found an error while saving the file:\n{}",
result.err().expect("No error")
)
}
},
);
Ok(())
}
}
}
Err(_) => Err("Can't downcast the buffer to GtkSourceBuffer."),
}
}
} }

View File

@ -5,11 +5,13 @@
pub mod app; pub mod app;
pub mod editor; pub mod editor;
pub mod sidebar; pub mod sidebar;
pub mod tab_label;
pub mod window; pub mod window;
pub use app::EchidnaEditor; pub use app::EchidnaEditor;
pub use editor::EchidnaCoreEditor; pub use editor::EchidnaCoreEditor;
pub use sidebar::EchidnaSidebar; pub use sidebar::EchidnaSidebar;
pub use tab_label::TabLabel;
pub use window::EchidnaWindow; pub use window::EchidnaWindow;
pub mod prelude { pub mod prelude {

View File

@ -15,3 +15,9 @@ impl EchidnaSidebar {
glib::Object::new(&[]).expect("Failed to create 'EchidnaSidebar' component.") glib::Object::new(&[]).expect("Failed to create 'EchidnaSidebar' component.")
} }
} }
impl Default for EchidnaSidebar {
fn default() -> Self {
Self::new()
}
}

View File

@ -6,67 +6,71 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/. - file, You can obtain one at https://mozilla.org/MPL/2.0/.
--> -->
<template class="EchidnaSidebar"> <template class="EchidnaSidebar">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<child> <child>
<object class="GtkStackSidebar"> <object class="GtkStackSidebar">
<property name="stack"> <property name="stack">sidebar_stack</property>
<object class="GtkStack">
<child>
<object class="GtkStackPage">
<!-- Explorer Tab -->
<property name="name">explorer</property>
<property name="title">Explorer</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkTreeView" id="treeview">
<property name="model">liststore1</property>
<child></child>
<child internal-child="selection">
<object class="GtkTreeSelection" id="selection">
<signal name="changed" handler="on_treeview_selection_changed" />
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<!-- Search Tab -->
<property name="name">search</property>
<property name="title">Search</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkLabel">
<property name="label">Search</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<!-- Extensions Tab -->
<property name="name">extensions</property>
<property name="title">Extensions</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
</object>
</property>
</object>
</child>
</object>
</property>
</object> </object>
</child> </child>
<child>
<object class="GtkStack" id="sidebar_stack">
<property name="hhomogeneous">1</property>
<property name="width-request">170</property>
<child>
<object class="GtkStackPage">
<!-- Explorer Tab -->
<property name="name">explorer</property>
<property name="title">Explorer</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkLabel">
<property name="label">Explorer</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<!-- Search Tab -->
<property name="name">search</property>
<property name="title">Search</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkLabel">
<property name="label">Search</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<!-- Extensions Tab -->
<property name="name">extensions</property>
<property name="title">Extensions</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkLabel">
<property name="label">Extensions</property>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</child>
</template> </template>
</interface> </interface>

View File

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
#[derive(Default, CompositeTemplate)]
#[template(file = "./tab-label.ui")]
pub struct TabLabel {
#[template_child]
pub button: TemplateChild<gtk::Button>,
}
#[glib::object_subclass]
impl ObjectSubclass for TabLabel {
const NAME: &'static str = "TabLabel";
type Type = super::TabLabel;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for TabLabel {}
impl WidgetImpl for TabLabel {}
impl BoxImpl for TabLabel {}
impl BuildableImpl for TabLabel {
fn add_child(
&self,
buildable: &Self::Type,
_builder: &gtk::Builder,
child: &glib::Object,
_type_: Option<&str>,
) {
buildable.prepend(child.downcast_ref::<gtk::Widget>().unwrap());
}
}

View File

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
pub mod imp;
use glib::IsA;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
glib::wrapper! {
pub struct TabLabel(ObjectSubclass<imp::TabLabel>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl TabLabel {
pub fn new<U: IsA<gtk::Widget>>(tab_label: Option<&U>) -> Self {
let this: Self = glib::Object::new(&[]).expect("Failed to create 'TabLabel' component.");
match tab_label {
Some(tab_label) => this.prepend(tab_label),
None => {}
}
this
}
pub fn to_imp(&self) -> &imp::TabLabel {
imp::TabLabel::from_instance(self)
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="4.5.0" />
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- 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/.
-->
<template class="TabLabel">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<child>
<object class="GtkButton" id="button">
<property name="has-frame">0</property>
<property name="icon-name">window-close-symbolic</property>
</object>
</child>
</template>
</interface>

View File

@ -4,18 +4,18 @@
use crate::components::editor::EchidnaCoreEditor; use crate::components::editor::EchidnaCoreEditor;
use crate::lib::prelude::*; use crate::lib::prelude::*;
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, FileChooserNative, Label, ResponseType,
}; };
use sourceview::{prelude::*, Buffer, File, FileSaver}; use sourceview::{File, FileExt as SourceFileExt};
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); fn action_save_file_as(&self);
fn action_new_file(&self); fn action_new_file(&self);
fn action_save_file(&self);
} }
impl FileImplementedEditor for super::EchidnaWindow { impl FileImplementedEditor for super::EchidnaWindow {
@ -36,14 +36,12 @@ impl FileImplementedEditor for super::EchidnaWindow {
Perhaps some of the last points should not be implemented in this function but rather in another function that keeps track of every files. Perhaps some of the last points should not be implemented in this function but rather in another function that keeps track of every files.
*/ */
fn action_open_file(&self) { fn action_open_file(&self) {
let dialog: FileChooserDialog = FileChooserDialog::new( let dialog = FileChooserNative::new(
Some("Open a file"), Some("Open a file"),
Some(self), Some(self),
FileChooserAction::Open, FileChooserAction::Open,
&[ Some("Open"),
("Cancel", ResponseType::Cancel), Some("Cancel"),
("Open", ResponseType::Accept),
],
); );
dialog.set_visible(true); dialog.set_visible(true);
@ -69,7 +67,7 @@ impl FileImplementedEditor for super::EchidnaWindow {
notebook.prepend_closable_page( notebook.prepend_closable_page(
&editor_page, &editor_page,
Some(&Label::new(Some( Some(&Label::new(Some(
&file_location file_location
.path() .path()
.expect("The file's path is missing") .expect("The file's path is missing")
.file_name() .file_name()
@ -79,16 +77,13 @@ impl FileImplementedEditor for super::EchidnaWindow {
))), ))),
); );
} }
fn action_save_file_as(&self) { fn action_save_file_as(&self) {
let dialog = FileChooserDialog::new( let dialog = FileChooserNative::new(
Some("Save File As"), Some("Save File As"),
Some(self), Some(self),
FileChooserAction::Save, FileChooserAction::Save,
&[ Some("Open"),
("Cancel", ResponseType::Cancel), Some("Cancel"),
("Save", ResponseType::Accept),
],
); );
dialog.set_current_name("untitled"); dialog.set_current_name("untitled");
@ -99,41 +94,9 @@ impl FileImplementedEditor for super::EchidnaWindow {
move |dialog, response| { move |dialog, response| {
if response == ResponseType::Accept { if response == ResponseType::Accept {
let file = dialog.file().expect(""); let file = dialog.file().expect("");
let window_imp = window.to_imp(); let tab: EchidnaCoreEditor = window.get_current_tab().expect("error");
let page: EchidnaCoreEditor; tab.save_file(Some(&file));
}
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(); dialog.destroy();
@ -147,4 +110,16 @@ impl FileImplementedEditor for super::EchidnaWindow {
.notebook .notebook
.prepend_closable_page(&editor_page, Some(&gtk::Label::new(Some(&"Untitled")))); .prepend_closable_page(&editor_page, Some(&gtk::Label::new(Some(&"Untitled"))));
} }
fn action_save_file(&self) {
let page: EchidnaCoreEditor = self
.get_current_tab()
.expect("Can't find the current tab because there are no tabs.");
match page.file().location() {
Some(_) => {
page.save_file(None);
}
None => self.action_save_file_as(),
}
}
} }

View File

@ -67,7 +67,7 @@ impl MenubarImplementedEditor for EchidnaWindow {
about_dialog.set_license_type(gtk::License::Mpl20); about_dialog.set_license_type(gtk::License::Mpl20);
about_dialog.set_program_name(Some("Echidna Code Editor")); about_dialog.set_program_name(Some("Echidna Code Editor"));
about_dialog.set_website(Some("https://github.com/EchidnaHQ/Echidna")); about_dialog.set_website(Some("https://gitlab.com/EchidnaHQ/Echidna"));
about_dialog.set_authors(&["FortressValkriye"]); about_dialog.set_authors(&["FortressValkriye"]);
about_dialog.set_copyright(Some("Made with by ❤️ Echidna contributors")); about_dialog.set_copyright(Some("Made with by ❤️ Echidna contributors"));
about_dialog.set_visible(true); about_dialog.set_visible(true);
@ -78,18 +78,20 @@ impl MenubarImplementedEditor for EchidnaWindow {
app.add_action(&act_report_issue); app.add_action(&act_report_issue);
act_report_issue.connect_activate(|_action, _variant| { act_report_issue.connect_activate(clone!(@weak self as win =>
webbrowser::open("https://github.com/EchidnaHQ/Echidna/issues/new"); move |_action, _variant| {
}); gtk::show_uri(Some(&win), "https://github.com/EchidnaHQ/Echidna/issues/new", gdk::CURRENT_TIME);
}));
} }
{ {
let act_search_feature_requests = SimpleAction::new("search-feature-requests", None); let act_search_feature_requests = SimpleAction::new("search-feature-requests", None);
app.add_action(&act_search_feature_requests); app.add_action(&act_search_feature_requests);
act_search_feature_requests.connect_activate(|_action, _variant| { act_search_feature_requests.connect_activate(clone!(@weak self as win =>
webbrowser::open("https://github.com/EchidnaHQ/Echidna/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement"); move |_action, _variant| {
}); gtk::show_uri(Some(&win), "https://github.com/EchidnaHQ/Echidna/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement", gdk::CURRENT_TIME);
}));
} }
{ {
let act_window_close = SimpleAction::new("close", None); let act_window_close = SimpleAction::new("close", None);
@ -130,5 +132,16 @@ impl MenubarImplementedEditor for EchidnaWindow {
window.action_new_file(); window.action_new_file();
})); }));
} }
{
let action_save = SimpleAction::new("save", None);
self.add_action(&action_save);
action_save.connect_activate(clone!(@weak self as window =>
move |_, _| {
window.action_save_file();
}
));
}
} }
} }

View File

@ -6,7 +6,7 @@ pub mod file;
pub mod imp; pub mod imp;
pub mod menubar; pub mod menubar;
use glib::object::IsA; use glib::object::{Cast, IsA};
use gtk::subclass::prelude::*; use gtk::subclass::prelude::*;
glib::wrapper! { glib::wrapper! {
@ -28,4 +28,25 @@ impl EchidnaWindow {
pub fn to_imp(&self) -> &imp::EchidnaWindow { pub fn to_imp(&self) -> &imp::EchidnaWindow {
imp::EchidnaWindow::from_instance(self) imp::EchidnaWindow::from_instance(self)
} }
pub fn get_current_tab<A: IsA<gtk::Widget>>(&self) -> Result<A, &str> {
let window_imp = self.to_imp();
let nth = window_imp.notebook.current_page();
match nth {
None => Err("No tabs are currently opened, maybe there are no tabs."),
Some(nth) => {
let page = window_imp
.notebook
.nth_page(Some(nth))
.expect("Couldn't get the page of the current index.");
match page.downcast::<A>()
{
Ok(page) => Ok(page),
Err(e) => Err("Cannot downcast to type parameter A. Maybe it's not in the type you are looking for."),
}
}
}
}
} }

View File

@ -9,7 +9,7 @@ use glib::clone;
use glib::subclass::types::ObjectSubclassExt; use glib::subclass::types::ObjectSubclassExt;
use glib::types::Type; use glib::types::Type;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{ApplicationWindow, FileChooserAction, FileChooserDialog, ResponseType, TreeStore}; use gtk::{ApplicationWindow, FileChooserAction, FileChooserNative, ResponseType, TreeStore};
use relative_path::RelativePath; use relative_path::RelativePath;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
@ -46,14 +46,12 @@ impl WorkspaceImplementedEditor for EchidnaEditor {
_action: &SimpleAction, _action: &SimpleAction,
_variant: Option<&glib::Variant>, _variant: Option<&glib::Variant>,
) { ) {
let dialog: FileChooserDialog = FileChooserDialog::new( let dialog = FileChooserNative::new(
Some("Open a file"), Some("Open a file"),
Some(&window), Some(&window),
FileChooserAction::Open, FileChooserAction::Open,
&[ Some("Open"),
("Cancel", ResponseType::Cancel), Some("Cancel"),
("Open", ResponseType::Accept),
],
); );
dialog.set_visible(true); dialog.set_visible(true);
dialog.connect_response(clone!(@weak window, @weak app => dialog.connect_response(clone!(@weak window, @weak app =>
@ -78,7 +76,7 @@ impl WorkspaceImplementedEditor for EchidnaEditor {
* *
* Basically, this is just the same as Open Folder, but it's many folders. * Basically, this is just the same as Open Folder, but it's many folders.
* *
* - Open a FileChooserDialog, set to only view .code-workspace files. * - Open a FileChooserNative, set to only view .code-workspace files.
* - If the user pressed cancel, destroy the dialog. If the user opened a .code-workspace file: * - If the user pressed cancel, destroy the dialog. If the user opened a .code-workspace file:
* - Get the workspace file, load and parse its content, .code-workspace files are in JSON with comments. But JSON only should be fine, for now. * - Get the workspace file, load and parse its content, .code-workspace files are in JSON with comments. But JSON only should be fine, for now.
* - Iterate over folders listed in the workspace file. * - Iterate over folders listed in the workspace file.
@ -125,31 +123,30 @@ impl WorkspaceImplementedEditor for EchidnaEditor {
self.open_folder(folder); self.open_folder(folder);
} }
} }
}
/** /**
* *
* *
*/ */
fn recursive_add_files_into_tree_store(&self, parent_file: File, tree: &TreeStore) { fn recursive_add_files_into_tree_store(&self, parent_file: File, tree: &TreeStore) {
let child_enumerate_cancellable = Cancellable::new(); let child_enumerate_cancellable = Cancellable::new();
let child_files = parent_file let child_files = parent_file
.enumerate_children( .enumerate_children(
"*", "*",
FileQueryInfoFlags::NONE, FileQueryInfoFlags::NONE,
Some(&child_enumerate_cancellable), Some(&child_enumerate_cancellable),
)
.expect(
format!(
"Could not look up the children files of {:?} because:\n{:#?}",
filepath
) )
.as_str(), .expect(
); format!(
let filepath = &parent_file "Could not look up the children files of {:?} because:\n{:#?}",
.path() filepath
.expect("Could not get the file path of the file."); )
.as_str(),
);
let filepath = &parent_file
.path()
.expect("Could not get the file path of the file.");
for file_iter in files { for file_iter in files {
let file_info = file_iter.expect(); let file_info = file_iter.expect();
let file = parent_file.child(file_info.name()); let file = parent_file.child(file_info.name());

View File

@ -2,8 +2,9 @@
* 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 crate::components::tab_label::TabLabel;
use glib::IsA; use glib::IsA;
use gtk::{prelude::*, Box, Button, Widget}; use gtk::{prelude::*, Widget};
pub trait ClosableTabImplementedNotebook { pub trait ClosableTabImplementedNotebook {
fn prepend_closable_page<T: IsA<Widget>, U: IsA<Widget>>( fn prepend_closable_page<T: IsA<Widget>, U: IsA<Widget>>(
@ -16,39 +17,24 @@ pub trait ClosableTabImplementedNotebook {
child: &T, child: &T,
tab_label: Option<&U>, tab_label: Option<&U>,
) -> u32; ) -> u32;
fn create_closable_tab<U: IsA<Widget>>(tab_label: Option<&U>) -> (Box, Button);
} }
impl ClosableTabImplementedNotebook for gtk::Notebook { impl ClosableTabImplementedNotebook for gtk::Notebook {
fn create_closable_tab<U: IsA<Widget>>(tab_label: Option<&U>) -> (Box, Button) {
let tab = Box::new(gtk::Orientation::Horizontal, 5);
if tab_label.is_some() {
tab.append(tab_label.unwrap());
}
let button = gtk::Button::new();
button.set_icon_name("window-close-symbolic");
button.set_has_frame(false);
tab.append(&button);
(tab, button)
}
fn prepend_closable_page<T: IsA<Widget>, U: IsA<Widget>>( fn prepend_closable_page<T: IsA<Widget>, U: IsA<Widget>>(
&self, &self,
child: &T, child: &T,
tab_label: Option<&U>, tab_label: Option<&U>,
) -> u32 { ) -> u32 {
let (tab, button) = &Self::create_closable_tab(tab_label); let tab_label_widget = TabLabel::new(tab_label);
let page = self.prepend_page(child, Some(tab)); let page = self.prepend_page(child, Some(&tab_label_widget));
button.connect_clicked(glib::clone!(@weak self as notebook => tab_label_widget
move |_| { .to_imp()
notebook.remove_page(Some(page)); .button
})); .connect_clicked(glib::clone!(@weak self as notebook =>
move |_| {
notebook.remove_page(Some(page));
}));
page page
} }
@ -58,13 +44,16 @@ impl ClosableTabImplementedNotebook for gtk::Notebook {
child: &T, child: &T,
tab_label: Option<&U>, tab_label: Option<&U>,
) -> u32 { ) -> u32 {
let (tab, button) = &Self::create_closable_tab(tab_label); let tab_label_widget = TabLabel::new(tab_label);
let page = self.append_page(child, Some(tab)); let page = self.append_page(child, Some(&tab_label_widget));
button.connect_clicked(glib::clone!(@weak self as notebook => tab_label_widget
move |_| { .to_imp()
notebook.remove_page(Some(page)); .button
})); .connect_clicked(glib::clone!(@weak self as notebook =>
move |_| {
notebook.remove_page(Some(page));
}));
page page
} }