use std::cell::{Ref, RefCell, RefMut}; use std::{env, thread}; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use gdk; use gtk; use gtk::prelude::*; use gtk::{AboutDialog, ApplicationWindow, Button, HeaderBar, Orientation, Paned, SettingsExt}; use gio::prelude::*; use gio::{Menu, MenuExt, MenuItem, SimpleAction}; use glib::variant::FromVariant; use toml; use neovim_lib::Value; use settings::{Settings, SettingsLoader}; use shell::{self, Shell, ShellOptions}; use shell_dlg; use project::Projects; use plug_manager; use file_browser::FileBrowserWidget; use subscriptions::SubscriptionHandle; macro_rules! clone { (@param _) => ( _ ); (@param $x:ident) => ( $x ); ($($n:ident),+ => move || $body:expr) => ( { $( let $n = $n.clone(); )+ move || $body } ); ($($n:ident),+ => move |$($p:tt),+| $body:expr) => ( { $( let $n = $n.clone(); )+ move |$(clone!(@param $p),)+| $body } ); } const DEFAULT_WIDTH: i32 = 800; const DEFAULT_HEIGHT: i32 = 600; const DEFAULT_SIDEBAR_WIDTH: i32 = 200; pub struct Ui { initialized: bool, comps: Arc>, settings: Rc>, shell: Rc>, projects: Rc>, plug_manager: Arc>, file_browser: Arc>, } pub struct Components { window: Option, window_state: WindowState, open_btn: Button, } impl Components { fn new() -> Components { let open_btn = Button::new(); let open_btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 3); open_btn_box.pack_start(>k::Label::new("Open"), false, false, 3); open_btn_box.pack_start( >k::Image::new_from_icon_name("pan-down-symbolic", gtk::IconSize::Menu.into()), false, false, 3, ); open_btn.add(&open_btn_box); open_btn.set_can_focus(false); Components { open_btn, window: None, window_state: WindowState::load(), } } pub fn close_window(&self) { self.window.as_ref().unwrap().destroy(); } pub fn window(&self) -> &ApplicationWindow { self.window.as_ref().unwrap() } } impl Ui { pub fn new(options: ShellOptions) -> Ui { let plug_manager = plug_manager::Manager::new(); let plug_manager = Arc::new(UiMutex::new(plug_manager)); let file_browser = Arc::new(UiMutex::new(FileBrowserWidget::new())); let comps = Arc::new(UiMutex::new(Components::new())); let settings = Rc::new(RefCell::new(Settings::new())); let shell = Rc::new(RefCell::new(Shell::new(settings.clone(), options))); settings.borrow_mut().set_shell(Rc::downgrade(&shell)); let projects = Projects::new(&comps.borrow().open_btn, shell.clone()); Ui { initialized: false, comps, shell, settings, projects, plug_manager, file_browser, } } pub fn init(&mut self, app: >k::Application, restore_win_state: bool) { if self.initialized { return; } self.initialized = true; let mut settings = self.settings.borrow_mut(); settings.init(); let window = ApplicationWindow::new(app); let main = Paned::new(Orientation::Horizontal); { // initialize window from comps // borrowing of comps must be leaved // for event processing let mut comps = self.comps.borrow_mut(); self.shell.borrow_mut().init(); comps.window = Some(window.clone()); let prefer_dark_theme = env::var("NVIM_GTK_PREFER_DARK_THEME") .map(|opt| opt.trim() == "1") .unwrap_or(false); if prefer_dark_theme { if let Some(settings) = window.get_settings() { settings.set_property_gtk_application_prefer_dark_theme(true); } } if restore_win_state { if comps.window_state.is_maximized { window.maximize(); } window.set_default_size( comps.window_state.current_width, comps.window_state.current_height, ); main.set_position(comps.window_state.sidebar_width); } else { window.set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT); main.set_position(DEFAULT_SIDEBAR_WIDTH); } } // Client side decorations including the toolbar are disabled via NVIM_GTK_NO_HEADERBAR=1 let use_header_bar = env::var("NVIM_GTK_NO_HEADERBAR") .map(|opt| opt.trim() != "1") .unwrap_or(true); if app.prefers_app_menu() || use_header_bar { self.create_main_menu(app, &window); } let update_subtitle = if use_header_bar { Some(self.create_header_bar()) } else { None }; let show_sidebar_action = SimpleAction::new_stateful("show-sidebar", None, &false.to_variant()); let file_browser_ref = self.file_browser.clone(); let comps_ref = self.comps.clone(); show_sidebar_action.connect_change_state(move |action, value| { if let Some(ref value) = *value { action.set_state(value); let is_active = value.get::().unwrap(); file_browser_ref.borrow().set_visible(is_active); comps_ref.borrow_mut().window_state.show_sidebar = is_active; } }); app.add_action(&show_sidebar_action); let comps_ref = self.comps.clone(); window.connect_size_allocate(clone!(main => move |window, _| { gtk_window_size_allocate( window, &mut *comps_ref.borrow_mut(), &main, ); })); let comps_ref = self.comps.clone(); window.connect_window_state_event(move |_, event| { gtk_window_state_event(event, &mut *comps_ref.borrow_mut()); Inhibit(false) }); let comps_ref = self.comps.clone(); window.connect_destroy(move |_| { comps_ref.borrow().window_state.save(); }); let shell = self.shell.borrow(); let file_browser = self.file_browser.borrow(); main.pack1(&**file_browser, false, false); main.pack2(&**shell, true, false); window.add(&main); window.show_all(); if restore_win_state { // Hide sidebar, if it wasn't shown last time. // Has to be done after show_all(), so it won't be shown again. let show_sidebar = self.comps.borrow().window_state.show_sidebar; show_sidebar_action.change_state(&show_sidebar.to_variant()); } let comps_ref = self.comps.clone(); let update_title = shell.state.borrow().subscribe( "BufEnter,DirChanged", &["expand('%:p')", "getcwd()"], move |args| { let comps = comps_ref.borrow(); let window = comps.window.as_ref().unwrap(); let file_path = &args[0]; let dir = Path::new(&args[1]); let filename = if file_path.is_empty() { "[No Name]" } else if let Some(rel_path) = Path::new(&file_path) .strip_prefix(&dir) .ok() .and_then(|p| p.to_str()) { rel_path } else { &file_path }; window.set_title(filename); }, ); let comps_ref = self.comps.clone(); let shell_ref = self.shell.clone(); window.connect_delete_event(move |_, _| gtk_delete(&*comps_ref, &*shell_ref)); shell.grab_focus(); let comps_ref = self.comps.clone(); shell.set_detach_cb(Some(move || { let comps_ref = comps_ref.clone(); gtk::idle_add(move || { comps_ref.borrow().close_window(); Continue(false) }); })); let state_ref = self.shell.borrow().state.clone(); let file_browser_ref = self.file_browser.clone(); let plug_manager_ref = self.plug_manager.clone(); shell.set_nvim_started_cb(Some(move || { let state = state_ref.borrow(); plug_manager_ref .borrow_mut() .init_nvim_client(state_ref.borrow().nvim_clone()); file_browser_ref.borrow_mut().init(&state); state.set_autocmds(); state.run_now(&update_title); if let Some(ref update_subtitle) = update_subtitle { state.run_now(&update_subtitle); } })); let sidebar_action = UiMutex::new(show_sidebar_action); shell.set_nvim_command_cb(Some(move |args: Vec| { if let Some(cmd) = args[0].as_str() { match cmd { "ToggleSidebar" => { let action = sidebar_action.borrow(); let state = !bool::from_variant(&action.get_state().unwrap()).unwrap(); action.change_state(&state.to_variant()); } _ => {} } } })); } fn create_header_bar(&self) -> SubscriptionHandle { let header_bar = HeaderBar::new(); let comps = self.comps.borrow(); let window = comps.window.as_ref().unwrap(); let projects = self.projects.clone(); header_bar.pack_start(&comps.open_btn); comps .open_btn .connect_clicked(move |_| projects.borrow_mut().show()); let new_tab_btn = Button::new_from_icon_name("tab-new-symbolic", gtk::IconSize::SmallToolbar.into()); let shell_ref = Rc::clone(&self.shell); new_tab_btn.connect_clicked(move |_| shell_ref.borrow_mut().new_tab()); new_tab_btn.set_can_focus(false); new_tab_btn.set_tooltip_text("Open a new tab"); header_bar.pack_start(&new_tab_btn); let paste_btn = Button::new_from_icon_name("edit-paste-symbolic", gtk::IconSize::SmallToolbar.into()); let shell = self.shell.clone(); paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste()); paste_btn.set_can_focus(false); paste_btn.set_tooltip_text("Paste from clipboard"); header_bar.pack_end(&paste_btn); let save_btn = Button::new_with_label("Save All"); let shell = self.shell.clone(); save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all()); save_btn.set_can_focus(false); header_bar.pack_end(&save_btn); header_bar.set_show_close_button(true); window.set_titlebar(Some(&header_bar)); let shell = self.shell.borrow(); let update_subtitle = shell.state.borrow().subscribe( "DirChanged", &["getcwd()"], move |args| { header_bar.set_subtitle(&*args[0]); }, ); update_subtitle } fn create_main_menu(&self, app: >k::Application, window: >k::ApplicationWindow) { let plug_manager = self.plug_manager.clone(); let menu = Menu::new(); let section = Menu::new(); section.append_item(&MenuItem::new("New Window", "app.new-window")); menu.append_section(None, §ion); let section = Menu::new(); section.append_item(&MenuItem::new("Sidebar", "app.show-sidebar")); menu.append_section(None, §ion); let section = Menu::new(); section.append_item(&MenuItem::new("Plugins", "app.Plugins")); section.append_item(&MenuItem::new("About", "app.HelpAbout")); menu.append_section(None, §ion); menu.freeze(); app.set_app_menu(Some(&menu)); let plugs_action = SimpleAction::new("Plugins", None); plugs_action.connect_activate( clone!(window => move |_, _| plug_manager::Ui::new(&plug_manager).show(&window)), ); let about_action = SimpleAction::new("HelpAbout", None); about_action.connect_activate(clone!(window => move |_, _| on_help_about(&window))); about_action.set_enabled(true); app.add_action(&about_action); app.add_action(&plugs_action); } } fn on_help_about(window: >k::ApplicationWindow) { let about = AboutDialog::new(); about.set_transient_for(window); about.set_program_name("NeovimGtk"); about.set_version(env!("CARGO_PKG_VERSION")); about.set_logo_icon_name("org.daa.NeovimGtk"); about.set_authors(&[env!("CARGO_PKG_AUTHORS")]); about.set_comments( format!( "Build on top of neovim\n\ Minimum supported neovim version: {}", shell::MINIMUM_SUPPORTED_NVIM_VERSION ).as_str(), ); about.connect_response(|about, _| about.destroy()); about.show(); } fn gtk_delete(comps: &UiMutex, shell: &RefCell) -> Inhibit { if !shell.borrow().is_nvim_initialized() { return Inhibit(false); } Inhibit(if shell_dlg::can_close_window(comps, shell) { let comps = comps.borrow(); comps.close_window(); shell.borrow_mut().detach_ui(); false } else { true }) } fn gtk_window_size_allocate( app_window: >k::ApplicationWindow, comps: &mut Components, main: &Paned, ) { if !app_window.is_maximized() { let (current_width, current_height) = app_window.get_size(); comps.window_state.current_width = current_width; comps.window_state.current_height = current_height; } if comps.window_state.show_sidebar { comps.window_state.sidebar_width = main.get_position(); } } fn gtk_window_state_event(event: &gdk::EventWindowState, comps: &mut Components) { comps.window_state.is_maximized = event .get_new_window_state() .contains(gdk::WindowState::MAXIMIZED); } #[derive(Serialize, Deserialize)] struct WindowState { current_width: i32, current_height: i32, is_maximized: bool, show_sidebar: bool, sidebar_width: i32, } impl WindowState { pub fn new() -> Self { WindowState { current_width: DEFAULT_WIDTH, current_height: DEFAULT_HEIGHT, is_maximized: false, show_sidebar: false, sidebar_width: DEFAULT_SIDEBAR_WIDTH, } } } impl SettingsLoader for WindowState { const SETTINGS_FILE: &'static str = "window.toml"; fn empty() -> WindowState { WindowState::new() } fn from_str(s: &str) -> Result { toml::from_str(&s).map_err(|e| format!("{}", e)) } } pub struct UiMutex { thread: thread::ThreadId, data: RefCell, } unsafe impl Send for UiMutex {} unsafe impl Sync for UiMutex {} impl UiMutex { pub fn new(t: T) -> UiMutex { UiMutex { thread: thread::current().id(), data: RefCell::new(t), } } } impl UiMutex { pub fn replace(&self, t: T) -> T { self.assert_ui_thread(); self.data.replace(t) } } impl UiMutex { pub fn borrow(&self) -> Ref { self.assert_ui_thread(); self.data.borrow() } pub fn borrow_mut(&self) -> RefMut { self.assert_ui_thread(); self.data.borrow_mut() } #[inline] fn assert_ui_thread(&self) { if thread::current().id() != self.thread { panic!("Can access to UI only from main thread"); } } }