diff --git a/.gitignore b/.gitignore index ea8c4bf..81d1e2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +*.glade~ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0df02c9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'my-studio'", + "cargo": { + "args": [ + "build", + "--bin=my-studio", + "--package=my-studio" + ], + "filter": { + "name": "my-studio", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'my-studio'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=my-studio", + "--package=my-studio" + ], + "filter": { + "name": "my-studio", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/res/ui/main_window.glade b/res/ui/main_window.glade index 35b0da7..5323174 100644 --- a/res/ui/main_window.glade +++ b/res/ui/main_window.glade @@ -160,12 +160,27 @@ Author: Surya Teja K True in - + True - True - True - 2 - 2 + False + + + True + True + + + True + True + True + 2 + 2 + + + + + + + diff --git a/src/comms.rs b/src/comms.rs index b4ceb46..93d5142 100644 --- a/src/comms.rs +++ b/src/comms.rs @@ -1,15 +1,14 @@ use std::path::Path; use gtk::glib::{self, Receiver, Sender}; -use gtk::prelude::{ObjectExt, StatusbarExt, TextBufferExt, TextViewExt, WidgetExt}; -use sourceview4::LanguageManager; +use gtk::prelude::{ObjectExt, StatusbarExt}; +use gtk::traits::TextViewExt; use crate::{ action_handler, ui::{self, tree_model::RootTreeModel}, workspace::Workspace, }; -use sourceview4::prelude::*; use crate::{G_STATUS_BAR, G_TEXT_VIEW, G_TREE}; @@ -17,7 +16,8 @@ use crate::{G_STATUS_BAR, G_TEXT_VIEW, G_TREE}; pub enum CommEvents { // Triggers TreeView#set_model UpdateRootTree(), - + // Spawn/Focus Notebook Tab, + SpawnOrFocusTab(Option, Option), // used to read text files RootTreeItemClicked(Option), // Sets text to RootTextView @@ -34,9 +34,14 @@ pub fn handle_comm_event(tx: Sender, rx: Receiver) { ui::tree_view::update_tree_model(&tree.borrow().clone().unwrap()); // Reset UI tx.send(CommEvents::RootTreeItemClicked(None)).ok(); - tx.send(CommEvents::UpdateRootTextViewContent(None, None)).ok(); + tx.send(CommEvents::SpawnOrFocusTab(None, None)).ok(); + tx.send(CommEvents::UpdateRootTextViewContent(None, None)) + .ok(); }); } + CommEvents::SpawnOrFocusTab(file_path, content) => { + ui::notebook::handle_notebook_event(content, file_path); + } CommEvents::RootTreeItemClicked(tree_model) => { match tree_model { Some(tree_model) => { @@ -48,7 +53,8 @@ pub fn handle_comm_event(tx: Sender, rx: Receiver) { if file_path.is_file() { match std::fs::read(file_path) { Ok(data) => { - content = String::from_utf8(data).unwrap_or_else(|_| "File not supported".to_string()); + content = String::from_utf8(data) + .unwrap_or_else(|_| "File not supported".to_string()); // Update workspace's 'current open file' tracker let open_file_path = file_path.as_os_str().to_str().unwrap(); Workspace::set_open_file_path(Some(String::from( @@ -61,9 +67,13 @@ pub fn handle_comm_event(tx: Sender, rx: Receiver) { } } - let file_path_string: String = String::from(file_path.to_str().unwrap()); - tx.send(CommEvents::UpdateRootTextViewContent(Some(file_path_string), Some(content))) - .ok(); + let file_path_string = String::from(file_path.to_str().unwrap()); + + tx.send(CommEvents::SpawnOrFocusTab( + Some(file_path_string), + Some(content), + )) + .ok(); } None => { // Reset workspace's 'current open file' tracker @@ -74,59 +84,31 @@ pub fn handle_comm_event(tx: Sender, rx: Receiver) { CommEvents::UpdateRootTextViewContent(path, content) => { G_TEXT_VIEW.with(|editor| { let text_editor = &editor.borrow().clone().unwrap(); - - match content { - Some(content) => { - let source_buffer = sourceview4::Buffer::builder() - .text(content.as_str()) - .build(); - - // Detect language for syntax highlight - let lang_manager = LanguageManager::new(); - match lang_manager.guess_language(Some(path.unwrap()), None) { - Some(lang) => { - source_buffer.set_language(Some(&lang)); - }, - None => { - source_buffer.set_language(sourceview4::Language::NONE); - } - } - // update buffer in View - text_editor.set_buffer(Some(&source_buffer)); - // Show cursor on text_view so user can start modifying file - text_editor.grab_focus(); - } - None => { - // Reset text content - text_editor.buffer().unwrap().set_text(""); - } - } + ui::utils::set_text_on_editor(text_editor, path, content); }); } CommEvents::SaveEditorChanges() => { - G_TEXT_VIEW.with(|editor| { - let text_editor = &editor.borrow().clone().unwrap(); - - let text_buffer = text_editor.buffer().unwrap(); + let file_absolute_path = Workspace::get_open_file_path(); + match file_absolute_path { + Some(file_abs_path) => { + // Get View widget of open file + let text_editor = + ui::notebook::get_current_page_editor(file_abs_path.clone()); + let text_buffer = text_editor.expect("Unable to find editor for open file").buffer().unwrap(); - let file_absolute_path = Workspace::get_open_file_path(); - match file_absolute_path { - Some(file_abs_path) => { - action_handler::save_file_changes(text_buffer, file_abs_path.clone()); - G_STATUS_BAR.with(|status_bar| { - let status_bar_ref = status_bar.borrow(); - let status_bar = - status_bar_ref.as_ref().expect("Unable to use status_bar"); + action_handler::save_file_changes(text_buffer, file_abs_path.clone()); + G_STATUS_BAR.with(|status_bar| { + let status_bar_ref = status_bar.borrow(); + let status_bar = + status_bar_ref.as_ref().expect("Unable to use status_bar"); - status_bar - .push(0, &format!("Saved changes to '{}'", &file_abs_path)); - }); - } - None => { - println!("Unable to write Workspace#open_file_path"); - } + status_bar.push(0, &format!("Saved changes to '{}'", &file_abs_path)); + }); } - }); + None => { + println!("Unable to write Workspace#open_file_path"); + } + } } } // Don't forget to include this! diff --git a/src/main.rs b/src/main.rs index 0eb1d3b..333c343 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,9 @@ use gtk::{ glib, prelude::{ ApplicationCommandLineExt, ApplicationExt, ApplicationExtManual, BuilderExtManual, - GtkWindowExt, WidgetExt, + GtkWindowExt, WidgetExt, NotebookExtManual, }, - Application, ApplicationWindow, Builder, Statusbar, TreeView, + Application, ApplicationWindow, Builder, Statusbar, TreeView, Notebook }; mod action_handler; @@ -23,6 +23,7 @@ thread_local! { pub static G_WINDOW: RefCell> = RefCel thread_local! { pub static G_TREE: RefCell> = RefCell::new(None) } thread_local! { pub static G_TEXT_VIEW: RefCell> = RefCell::new(None) } thread_local! { pub static G_STATUS_BAR: RefCell> = RefCell::new(None) } +thread_local! { pub static G_NOTEBOOK: RefCell> = RefCell::new(None) } fn build_ui(app: &Application) { G_WINDOW.with(|window| { @@ -49,6 +50,16 @@ fn build_ui(app: &Application) { ui::tree_view::setup_tree(&builder, tx.clone()); }); + G_NOTEBOOK.with(|notebook| { + *notebook.borrow_mut() = builder.object("editor_notebook"); + let notebook = notebook.borrow().clone(); + assert!(notebook.is_some()); + + let notebook = notebook.unwrap(); + // Remove placeholder + notebook.remove_page(Some(0)); + }); + // Text Editor G_TEXT_VIEW.with(|editor| { *editor.borrow_mut() = builder.object("main_text_editor"); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e20ce1a..a867b33 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,5 @@ pub mod btn_action_row; pub mod tree_model; pub mod tree_view; +pub mod notebook; +pub mod utils; diff --git a/src/ui/notebook.rs b/src/ui/notebook.rs new file mode 100644 index 0000000..29e2dc2 --- /dev/null +++ b/src/ui/notebook.rs @@ -0,0 +1,130 @@ +use std::{cell::RefCell, path::Path}; +use gtk::{ + glib, + prelude::{Cast, NotebookExtManual}, + traits::{BoxExt, ButtonExt, ContainerExt, WidgetExt}, + IconSize, Notebook, Orientation, ReliefStyle, Widget, +}; +use crate::{ui, G_NOTEBOOK}; + +#[derive(Debug)] +pub struct NotebookTabItem { + file_path: String, + position: u32, +} + +// Holds reference to NotebookTabCache +thread_local! {pub static NOTEBOOK_TABS_CACHE: RefCell>> = RefCell::new(Some(Vec::new()))} + +pub fn handle_notebook_event(content: Option, file_path: Option) { + G_NOTEBOOK.with(move |notebook| { + let notebook: Notebook = notebook.borrow().clone().unwrap(); + + // Reset UI & return + if file_path.is_none() || content.is_none() { + for _ in 0..notebook.n_pages() { + notebook.remove_page(Some(0)); + } + return; + } + + let file_path_str = file_path.unwrap(); + + // Check if tab is already created for the file and focus it instead + + let mut has_focussed_page = false; + NOTEBOOK_TABS_CACHE.with(|cache| { + let cache = cache.borrow(); + let entries = cache.as_ref().unwrap(); + for iter in entries { + if iter.file_path.trim().eq(file_path_str.trim()) { + notebook.set_current_page(Some(iter.position)); + has_focussed_page = true; + break; + } + } + }); + if has_focussed_page { + return; + }; + + // Create New Tab + let file_name = String::from( + Path::new(&file_path_str) + .file_name() + .unwrap() + .to_str() + .unwrap(), + ); + + // Add content to child of tab + let editor = sourceview4::View::new(); + ui::utils::set_text_on_editor(&editor, Some(file_path_str.clone()), content); + + // create new tab + let tab_position = create_tab(notebook, file_name.as_str(), editor.upcast()); + NOTEBOOK_TABS_CACHE.with(|cache| { + let mut cache = cache.to_owned().borrow_mut(); + cache.as_mut().unwrap().push(NotebookTabItem { file_path: file_path_str.clone(), position: tab_position }); + }); + }); +} + +// Borrowed from https://github.com/gtk-rs/gtk3-rs/blob/9046f47158093d6fa40aa32ffbb0abaa75d57fd0/examples/notebook/notebook.rs#L18 +pub fn create_tab(notebook: Notebook, title: &str, widget: Widget) -> u32 { + let close_image = gtk::Image::from_icon_name(Some("window-close"), IconSize::Button); + let button = gtk::Button::new(); + let label = gtk::Label::new(Some(title)); + let tab = gtk::Box::new(Orientation::Horizontal, 0); + + button.set_relief(ReliefStyle::None); + button.add(&close_image); + + tab.pack_start(&label, false, false, 0); + tab.pack_start(&button, false, false, 0); + tab.show_all(); + + let index = notebook.append_page(&widget, Some(&tab)); + + button.connect_clicked(glib::clone!(@weak notebook => move |_| { + let index = notebook + .page_num(&widget) + .expect("Couldn't get page_num from notebook"); + notebook.remove_page(Some(index)); + })); + + // Show Notebook widget (GTK+ widgets hide themselves by default) + notebook.show_all(); + + // open the newly created page + notebook.set_current_page(Some(index)); + + index +} + +pub fn get_current_page_editor(file_path: String) -> Option { + + + let position = NOTEBOOK_TABS_CACHE.with(|cache| { + let cache = cache.borrow(); + let cache = cache.as_ref().unwrap(); + + let mut result = -1; + for iter in cache { + if iter.file_path == file_path { + result = iter.position as i32; + break; + } + } + + result as u32 + }); + + G_NOTEBOOK.with(|notebook| { + let notebook = notebook.borrow(); + let notebook = notebook.as_ref().unwrap(); + + let page = notebook.nth_page(Some(position)); + page.map(|page| page.downcast::().unwrap()) + }) +} diff --git a/src/ui/utils.rs b/src/ui/utils.rs new file mode 100644 index 0000000..5978e9c --- /dev/null +++ b/src/ui/utils.rs @@ -0,0 +1,39 @@ +use gtk::traits::{TextViewExt, TextBufferExt}; +use sourceview4::{ + traits::{BufferExt, LanguageManagerExt}, + LanguageManager, +}; + +pub fn set_text_on_editor( + text_editor: &sourceview4::View, + file_path: Option, + content: Option, +) { + match content { + Some(content) => { + let source_buffer = sourceview4::Buffer::builder() + .text(content.as_str()) + .build(); + + // Detect language for syntax highlight + let lang_manager = LanguageManager::new(); + match lang_manager.guess_language(Some(file_path.unwrap()), None) { + Some(lang) => { + source_buffer.set_language(Some(&lang)); + } + None => { + source_buffer.set_language(sourceview4::Language::NONE); + } + } + // update buffer in View + text_editor.set_buffer(Some(&source_buffer)); + // Show cursor on text_view so user can start modifying file + // FIXME: this is broken because of Notebook UI impl. + // text_editor.grab_focus(); + } + None => { + // Reset text content + text_editor.buffer().unwrap().set_text(""); + } + } +}