diff --git a/Cargo.toml b/Cargo.toml index 3f6141a..163c662 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ serde = { version = "1.0.196", features = ["derive"] } once_cell = "1.13.0" async-channel = "2.1.1" -glib = { version = "^0.18" } +glib = { version = "^0.18", features = ["v2_74"]} gio = { version = "^0.18" } pango = { version = "^0.18" } gdk = { package = "gdk4", version = "^0.7", features = ["v4_12"] } diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 3bb59fe..06fc63b 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -18,7 +18,6 @@ ../../src/components/terminal_tab/terminal_tab.ui ../../src/components/search_toolbar/search_toolbar.ui ../../src/components/style_switcher/style_switcher.ui - ../../src/components/zoom_controls/zoom_controls.ui svg/theme-thumbnail.svg @@ -31,4 +30,7 @@ ../icons/io.github.vhdirk.Terms.Devel.svg ../icons/io.github.vhdirk.Terms-symbolic.svg + + ../../src/tile/zoom_controls/zoom_controls.ui + diff --git a/data/resources/style.css b/data/resources/style.css index 1a9f452..3f08211 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -17,6 +17,11 @@ window:not(.about) headerbar, background-color: @window_bg_color; } +#terms_tab_bar.integrated { + margin-top: -6px; + margin-bottom: -6px; +} + /* #terms_main_window.context-root .custom-headerbar { background-color: @root_context_color; } @@ -28,7 +33,8 @@ window:not(.about) headerbar, /* .custom-headerbar windowhandle>box { padding-top: 0; padding-bottom: 0; -} +} */ +/* .custom-headerbar windowcontrols:not(.empty).start { margin-right: 6px; diff --git a/src/application.rs b/src/application.rs index 28d3053..6901d5d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -171,7 +171,9 @@ mod imp { let app = self.obj(); info!("Window init args: {:?} {:?} {:?}", command, directory, env); - let window = Window::new(&*app, command, directory, env); + + let window = Window::new(&*app); + window.new_tab(command, directory, env); info!("Add window"); app.add_window(&window); diff --git a/src/components/header_bar/header_bar.rs b/src/components/header_bar/header_bar.rs index baa1399..b0d1412 100644 --- a/src/components/header_bar/header_bar.rs +++ b/src/components/header_bar/header_bar.rs @@ -7,8 +7,8 @@ use gtk::prelude::*; use tracing::info; use crate::components::StyleSwitcher; -use crate::components::ZoomControls; use crate::settings::Settings; +use crate::tile::ZoomControls; #[derive(Debug, Default, gtk::CompositeTemplate, Properties)] #[template(resource = "/io/github/vhdirk/Terms/gtk/header_bar.ui")] @@ -148,15 +148,16 @@ impl HeaderBar { fn set_integrated_tab_bar(&self) { let tab_bar = self.tab_bar.borrow(); - tab_bar.unparent(); - - if self.settings.headerbar_integrated_tabbar() && tab_bar.view().map_or(false, |view| view.n_pages() > 1) { - self.header_bar.set_title_widget(Some(&*tab_bar)); + if self.settings.headerbar_integrated_tabbar() { + if self.header_bar.title_widget() != Some(tab_bar.clone().into()) { + tab_bar.unparent(); + self.header_bar.set_title_widget(Some(&*tab_bar)); + } tab_bar.set_halign(gtk::Align::Fill); tab_bar.set_hexpand(true); tab_bar.set_autohide(false); tab_bar.set_can_focus(false); - tab_bar.set_css_classes(&["inline"]); + tab_bar.set_css_classes(&["inline", "integrated"]); } else { self.header_bar.set_title_widget(Some(&*self.title_widget)); self.header_box.append(&*tab_bar); diff --git a/src/components/header_bar/header_bar.ui b/src/components/header_bar/header_bar.ui index 4bc4d2c..0d6018f 100644 --- a/src/components/header_bar/header_bar.ui +++ b/src/components/header_bar/header_bar.ui @@ -4,7 +4,6 @@ -
@@ -72,15 +71,42 @@ true - + + + splitbutton + + + + tab-new-symbolic + New Tab + win.new-tab + false + + + + + vertical + + + + + + + pan-down-symbolic + + + + + + true Terms - - @@ -95,7 +121,8 @@ - + + @@ -103,45 +130,10 @@ - - - - - - - - - - diff --git a/src/components/mod.rs b/src/components/mod.rs index ad9edce..bae5208 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -12,7 +12,6 @@ mod terminal_preferences_page; mod terminal_tab; mod theme_thumbnail; mod window; -mod zoom_controls; pub use header_bar::*; pub use preferences_window::*; @@ -28,4 +27,3 @@ pub use terminal_preferences_page::*; pub use terminal_tab::*; pub use theme_thumbnail::*; pub use window::*; -pub use zoom_controls::*; diff --git a/src/components/style_switcher/style_switcher.rs b/src/components/style_switcher/style_switcher.rs index dd030bf..144f527 100644 --- a/src/components/style_switcher/style_switcher.rs +++ b/src/components/style_switcher/style_switcher.rs @@ -1,23 +1,22 @@ -/* StyleSwitcher.vala - * - * Copyright 2023 Paulo Queiroz - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - +/// This file is work derived from Black Box +/// +/// Copyright 2023 Paulo Queiroz +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU General Public License as published by +/// the Free Software Foundation, either version 3 of the License, or +/// (at your option) any later version. +/// +/// This program is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU General Public License for more details. +/// +/// You should have received a copy of the GNU General Public License +/// along with this program. If not, see . +/// +/// SPDX-License-Identifier: GPL-3.0-or-later +/// use glib::clone; use gtk::subclass::prelude::*; use gtk::{prelude::*, CompositeTemplate}; diff --git a/src/components/terminal/terminal.rs b/src/components/terminal/terminal.rs index a6f8906..decc89a 100644 --- a/src/components/terminal/terminal.rs +++ b/src/components/terminal/terminal.rs @@ -93,6 +93,9 @@ pub struct Terminal { #[property(get, set, construct, nullable)] title: RefCell>, + + #[property(get, set, construct, nullable)] + icon: RefCell>, } impl Default for Terminal { @@ -113,6 +116,8 @@ impl Default for Terminal { command: Default::default(), env: Default::default(), title: Default::default(), + icon: Default::default(), + update_source: Default::default(), } } @@ -297,7 +302,9 @@ impl Terminal { if let (Some(match_str), _tag) = this.term.check_match_at(x, y) { if event.modifier_state().contains(gdk::ModifierType::CONTROL_MASK) { + // TODO: get active window + // TODO: launch through xdg portal on flatpak glib::spawn_future_local(gtk::UriLauncher::new(&match_str).launch_future(None::<>k::Window>)); } } @@ -575,6 +582,11 @@ impl Terminal { *self.title.borrow_mut() = title; self.obj().notify_title(); + + // TODO: icon title. vte icon title is deprecated + // let icon = self.term.icon_title().map(|t| t.to_string()); + // *self.icon.borrow_mut() = icon; + // self.obj().notify_icon(); } fn enqueue_update(&self) { diff --git a/src/components/terminal_panel/terminal_panel.rs b/src/components/terminal_panel/terminal_panel.rs index 263a9db..73aa762 100644 --- a/src/components/terminal_panel/terminal_panel.rs +++ b/src/components/terminal_panel/terminal_panel.rs @@ -31,6 +31,9 @@ pub struct TerminalPanel { #[property(get, set, construct, nullable)] title: RefCell>, + + #[property(get, set, construct, nullable)] + icon: RefCell>, } #[glib::object_subclass] @@ -99,5 +102,3 @@ impl TerminalPanel { // // // // // // // // // - -// // }, diff --git a/src/components/terminal_tab/terminal_tab.rs b/src/components/terminal_tab/terminal_tab.rs index 2a1076d..9f3cb90 100644 --- a/src/components/terminal_tab/terminal_tab.rs +++ b/src/components/terminal_tab/terminal_tab.rs @@ -29,6 +29,9 @@ pub struct TerminalTab { #[property(get, set, construct, nullable)] title: RefCell>, + + #[property(get, set, construct, nullable)] + icon: RefCell>, } #[glib::object_subclass] diff --git a/src/components/window/mod.rs b/src/components/window/mod.rs index b3d0266..0696163 100644 --- a/src/components/window/mod.rs +++ b/src/components/window/mod.rs @@ -2,6 +2,7 @@ mod window; use std::path::PathBuf; use glib::subclass::prelude::*; +use tracing::*; use window as imp; use crate::util::EnvMap; @@ -15,12 +16,21 @@ glib::wrapper! { } impl Window { - pub fn new>(application: &P, command: Option, directory: Option, env: Option) -> Self { - glib::Object::builder() - .property("application", application) - .property("command", command) - .property("directory", directory) - .property("env", env) - .build() + pub fn new>(application: &P) -> Self { + glib::Object::builder().property("application", application).build() + } + + pub fn new_tab(&self, command: Option, directory: Option, env: Option) { + self.imp().new_tab(command, directory, env); + } + + pub fn transfer_tab(&self, view: &adw::TabView, page: &adw::TabPage, position: i32) { + let our_view = &*self.imp().tab_view; + debug!("Transferring tab page {:?} from {:?} to {:?}", page, view, our_view); + view.transfer_page(page, our_view, position); + } + + pub fn tab_view(&self) -> Option { + self.imp().tab_view.try_get() } } diff --git a/src/components/window/window.rs b/src/components/window/window.rs index b5d9b57..5d884bc 100644 --- a/src/components/window/window.rs +++ b/src/components/window/window.rs @@ -14,7 +14,7 @@ use crate::util::EnvMap; use super::*; -#[derive(Debug, Default, gtk::CompositeTemplate, Properties)] +#[derive(Debug, gtk::CompositeTemplate, Properties)] #[template(resource = "/io/github/vhdirk/Terms/gtk/window.ui")] #[properties(wrapper_type=super::Window)] pub struct Window { @@ -46,6 +46,29 @@ pub struct Window { #[template_child] pub tab_bar: TemplateChild, + + selected_page_signals: glib::SignalGroup, + active_tab_signals: glib::SignalGroup, + active_tab_bindings: glib::BindingGroup, +} + +impl Default for Window { + fn default() -> Self { + Self { + settings: Default::default(), + directory: Default::default(), + command: Default::default(), + env: Default::default(), + header_bar: Default::default(), + overlay: Default::default(), + container: Default::default(), + tab_view: Default::default(), + tab_bar: Default::default(), + selected_page_signals: glib::SignalGroup::new::(), + active_tab_signals: glib::SignalGroup::new::(), + active_tab_bindings: glib::BindingGroup::new(), + } + } } #[glib::object_subclass] @@ -78,6 +101,7 @@ impl ObjectImpl for Window { obj.add_css_class("devel"); } + self.setup_signals(); self.setup_widgets(); self.setup_gactions(); self.connect_signals(); @@ -92,6 +116,30 @@ impl AdwApplicationWindowImpl for Window {} #[gtk::template_callbacks] impl Window { + fn setup_signals(&self) { + // self.selected_page_signals + + self.selected_page_signals.connect_bind_local(move |sg, obj| { + info!("selected page: bind"); + }); + + self.selected_page_signals.connect_notify_local(Some("pinned"), move |obj, param| { + info!("selected page: pinned"); + }); + + self.active_tab_signals.connect_bind_local(move |sg, obj| { + info!("active tab: bind"); + }); + + // self.active_tab_signals.connect_local(Some("bell"), move |sg, obj| { + // info!("active tab: bind"); + // }); + + self.active_tab_signals.connect_notify_local(Some("zoom"), move |sg, obj| { + info!("active tab: zoom"); + }); + } + fn setup_widgets(&self) { self.header_bar.set_container(Some(&*self.container)); self.header_bar.set_overlay(Some(&*self.overlay)); @@ -99,8 +147,6 @@ impl Window { if self.settings.remember_window_size() { self.restore_window_size(); } - - self.new_tab(); } fn restore_window_size(&self) { @@ -154,7 +200,14 @@ impl Window { .activate(move |win: &super::Window, _, _| win.imp().open_preferences()) .build(), gio::ActionEntry::builder("new-tab") - .activate(move |win: &super::Window, _, _| win.imp().new_tab()) + .activate(move |win: &super::Window, _, _| { + let this = win.imp(); + let command = this.command.borrow().clone(); + let directory = this.directory.borrow().clone(); + let env = this.env.borrow().clone(); + + this.new_tab(command, directory, env); + }) .build(), gio::ActionEntry::builder("toggle-fullscreen") .activate(move |win: &super::Window, _, _| win.set_fullscreened(!win.is_fullscreened())) @@ -200,11 +253,7 @@ impl Window { prefs_window.set_visible(true); } - pub fn new_tab(&self) { - let command = self.command.borrow().clone(); - let directory = self.directory.borrow().clone(); - let env = self.env.borrow().clone(); - + pub fn new_tab(&self, command: Option, directory: Option, env: Option) { let tab = TerminalTab::new(directory, command, env); let page = self.tab_view.append(&tab); @@ -279,8 +328,16 @@ impl Window { } fn detach_tab(&self) { - // TODO - warn!("detach tab: not yet implemented"); + if let Some(page) = self.tab_view.selected_page() { + info!("Detaching tab page {:?}", page); + if let Some(app) = self.obj().application() { + let window = super::Window::new(&app); + if let Some(new_tab_view) = window.tab_view() { + self.tab_view.transfer_page(&page, &new_tab_view, 0); + } + window.present(); + } + } } fn pin_tab(&self, pinned: bool) { @@ -295,12 +352,48 @@ impl Window { } fn close_tab(&self) { - // TODO - warn!("Close tab: not yet implemented"); + if let Some(page) = self.tab_view.selected_page() { + self.tab_view.close_page(&page); + } } fn close_other_tabs(&self) { - // TODO - warn!("Close other tabs: not yet implemented"); + if let Some(page) = self.tab_view.selected_page() { + self.tab_view.close_other_pages(&page); + } + } + + #[template_callback] + fn on_selected_page_changed(&self) { + let page = self.tab_view.selected_page(); + debug!("on page changed: {:?}", page); + self.selected_page_signals.set_target(page.as_ref()); + if let Some(page) = page.as_ref() { + let tab = page.child().downcast::().ok(); + self.active_tab_signals.set_target(tab.as_ref()); + if let Some(tab) = tab.as_ref() { + tab.grab_focus(); + } + self.active_tab_bindings.set_source(tab.as_ref()); + } + } + + #[template_callback] + fn on_page_attached(&self) {} + #[template_callback] + fn on_page_detached(&self) {} + #[template_callback] + fn on_create_window(&self) -> Option { + self.obj().application().and_then(|app| { + let window = super::Window::new(&app); + window.present(); + window.tab_view() + }) + } + #[template_callback] + fn on_page_closed(&self) -> bool { + false } + #[template_callback] + fn on_setup_menu(&self) {} } diff --git a/src/components/window/window.ui b/src/components/window/window.ui index 22b7f05..c3a1af4 100644 --- a/src/components/window/window.ui +++ b/src/components/window/window.ui @@ -69,10 +69,10 @@ + terms_tab_bar true true tab_view - @@ -83,8 +83,13 @@ + + + + + + tab_menu - diff --git a/src/components/zoom_controls/zoom_controls.rs b/src/components/zoom_controls/zoom_controls.rs deleted file mode 100644 index a7f2793..0000000 --- a/src/components/zoom_controls/zoom_controls.rs +++ /dev/null @@ -1,54 +0,0 @@ -use adw::prelude::BinExt; -use adw::subclass::prelude::*; -use glib::{clone, subclass::Signal}; -use glib::{ObjectExt, Properties}; -use gtk::glib; -use gtk::CompositeTemplate; -use once_cell::sync::Lazy; -use std::cell::RefCell; -use std::path::PathBuf; -use tracing::info; - -use crate::components::terminal_panel::TerminalPanel; -use crate::util::EnvMap; - -#[derive(Debug, Default, CompositeTemplate)] -#[template(resource = "/io/github/vhdirk/Terms/gtk/zoom_controls.ui")] -pub struct ZoomControls {} - -#[glib::object_subclass] -impl ObjectSubclass for ZoomControls { - const NAME: &'static str = "TermsZoomControls"; - type Type = super::ZoomControls; - type ParentType = adw::Bin; - - fn class_init(klass: &mut Self::Class) { - klass.bind_template(); - klass.bind_template_callbacks(); - } - - fn instance_init(obj: &glib::subclass::InitializingObject) { - obj.init_template(); - } -} - -impl ObjectImpl for ZoomControls { - fn constructed(&self) { - self.parent_constructed(); - - self.setup_widgets(); - } - - fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = Lazy::new(|| vec![Signal::builder("zoom-in").build()]); - SIGNALS.as_ref() - } -} - -impl WidgetImpl for ZoomControls {} -impl BinImpl for ZoomControls {} - -#[gtk::template_callbacks] -impl ZoomControls { - fn setup_widgets(&self) {} -} diff --git a/src/main.rs b/src/main.rs index ed35c48..bf93ee7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ mod pcre2; mod services; mod settings; mod theme_provider; +mod tile; mod util; fn init_gettext() { diff --git a/src/tile/README.md b/src/tile/README.md new file mode 100644 index 0000000..c16a5df --- /dev/null +++ b/src/tile/README.md @@ -0,0 +1,6 @@ +# Tile + +A gtk widget library primarily focused on tiling in a gridlayout. +Inspired by libpanel + +When there is demand, can be extracted into a standalone library diff --git a/src/tile/mod.rs b/src/tile/mod.rs new file mode 100644 index 0000000..51dd042 --- /dev/null +++ b/src/tile/mod.rs @@ -0,0 +1,2 @@ +mod zoom_controls; +pub use zoom_controls::*; diff --git a/src/components/zoom_controls/mod.rs b/src/tile/zoom_controls/mod.rs similarity index 100% rename from src/components/zoom_controls/mod.rs rename to src/tile/zoom_controls/mod.rs diff --git a/src/tile/zoom_controls/zoom_controls.rs b/src/tile/zoom_controls/zoom_controls.rs new file mode 100644 index 0000000..c81530d --- /dev/null +++ b/src/tile/zoom_controls/zoom_controls.rs @@ -0,0 +1,83 @@ +use adw::prelude::BinExt; +use adw::subclass::prelude::*; +use glib::{prelude::*, subclass::Signal}; +use glib::{ObjectExt, Properties}; +use gtk::glib; +use gtk::prelude::*; +use gtk::CompositeTemplate; +use once_cell::sync::Lazy; +use std::cell::{Cell, RefCell}; +use std::path::PathBuf; +use tracing::info; + +#[derive(Debug, Default, CompositeTemplate, Properties)] +#[template(resource = "/io/github/vhdirk/Tile/gtk/zoom_controls.ui")] +#[properties(wrapper_type=super::ZoomControls)] +pub struct ZoomControls { + /// The current zoom value, in percent + #[property(get, set=Self::set_value, construct, default=100)] + value: Cell, + + #[template_child] + zoom_label: TemplateChild, +} + +#[glib::object_subclass] +impl ObjectSubclass for ZoomControls { + const NAME: &'static str = "TileZoomControls"; + type Type = super::ZoomControls; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } +} + +#[glib::derived_properties] +impl ObjectImpl for ZoomControls { + fn constructed(&self) { + self.parent_constructed(); + } + + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("zoom-in").build(), + Signal::builder("zoom-out").build(), + Signal::builder("zoom-reset").build(), + Signal::builder("zoom").param_types([u32::static_type()]).build(), + ] + }); + SIGNALS.as_ref() + } +} + +impl WidgetImpl for ZoomControls {} +impl BinImpl for ZoomControls {} + +#[gtk::template_callbacks] +impl ZoomControls { + fn set_value(&self, value: u32) { + self.zoom_label.set_label(&format!("{}%", value)); + } + + // #[template_callback] + // fn on_zoom_out_clicked(&self) { + // self.obj().emit_by_name::<()>("zoom-out", &[]); + // } + + // #[template_callback] + // fn on_zoom_label_clicked(&self) { + // self.obj().emit_by_name::<()>("zoom-reset", &[]); + // } + + // #[template_callback] + // fn on_zoom_in_clicked(&self) { + // self.obj().emit_by_name::<()>("zoom-in", &[]); + // } +} diff --git a/src/components/zoom_controls/zoom_controls.ui b/src/tile/zoom_controls/zoom_controls.ui similarity index 82% rename from src/components/zoom_controls/zoom_controls.ui rename to src/tile/zoom_controls/zoom_controls.ui index f4724ca..bcaf7dc 100644 --- a/src/components/zoom_controls/zoom_controls.ui +++ b/src/tile/zoom_controls/zoom_controls.ui @@ -3,8 +3,8 @@ -