//! Port of apprt/gtk/class/clipboard_confirmation_dialog.zig //! (`GhosttyClipboardConfirmationDialog`). //! //! Properties: `can-remember`, `request` (boxed [`ClipboardRequestBoxed`]), //! `clipboard-contents` (GtkTextBuffer), `blur`. Signals `confirm(bool)` and //! `deny(bool)` (bool = "remember choice"). use std::{ cell::{Cell, RefCell}, sync::OnceLock, }; use adw::{prelude::*, subclass::prelude::*}; use ghostty_apprt::structs::ClipboardRequest; use ghostty_os::i18n::tr; use gtk::{glib, glib::subclass::Signal}; use super::dialog::{Dialog, DialogImpl}; /// GBoxed wrapper for [`ClipboardRequest`] (Zig: boxed `apprt.ClipboardRequest`). #[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Boxed)] #[boxed_type(name = "GhosttyClipboardRequest", nullable)] pub struct ClipboardRequestBoxed(pub ClipboardRequest); mod imp { use adw::prelude::*; use gtk::{glib, subclass::prelude::*}; use super::{ AdwAlertDialogImpl, AdwDialogImpl, Cell, ClipboardRequest, ClipboardRequestBoxed, Dialog, DialogImpl, ObjectImpl, ObjectSubclass, OnceLock, RefCell, Signal, WidgetImpl, tr, }; #[derive(Default, glib::Properties, gtk::CompositeTemplate)] #[properties(wrapper_type = super::ClipboardConfirmationDialog)] #[template(resource = "/com/mitchellh/ghostty/ui/1.4/clipboard-confirmation-dialog.ui")] pub struct ClipboardConfirmationDialog { /// The request that this dialog is for. #[property(get, set, nullable)] pub request: RefCell>, /// The clipboard contents being read/written. #[property(get, set, nullable, name = "clipboard-contents")] pub clipboard_contents: RefCell>, /// Whether the contents should be blurred. #[property(get, set)] pub blur: Cell, /// Whether the user can remember the choice. #[property(get, set, name = "can-remember")] pub can_remember: Cell, #[template_child] pub text_view_scroll: TemplateChild, #[template_child] pub text_view: TemplateChild, #[template_child] pub reveal_button: TemplateChild, #[template_child] pub hide_button: TemplateChild, #[template_child] pub remember_choice: TemplateChild, } #[glib::object_subclass] impl ObjectSubclass for ClipboardConfirmationDialog { const NAME: &'static str = "GhosttyClipboardConfirmationDialog"; type Type = super::ClipboardConfirmationDialog; type ParentType = Dialog; fn class_init(klass: &mut Self::Class) { Dialog::ensure_type(); klass.bind_template(); klass.bind_template_callbacks(); } fn instance_init(obj: &glib::subclass::InitializingObject) { obj.init_template(); } } #[gtk::template_callbacks] impl ClipboardConfirmationDialog { fn set_blurred(&self, blurred: bool, reveal_visible: bool, hide_visible: bool) { self.text_view_scroll.set_sensitive(!blurred); if blurred { self.text_view.add_css_class("blurred"); } else { self.text_view.remove_css_class("blurred"); } self.reveal_button.set_visible(reveal_visible); self.hide_button.set_visible(hide_visible); } #[template_callback] fn notify_blur(&self, _pspec: &glib::ParamSpec) { if self.blur.get() { self.set_blurred(true, true, false); } else { self.set_blurred(false, false, false); } } #[template_callback] fn notify_request(&self, _pspec: &glib::ParamSpec) { self.update_request(); } pub fn update_request(&self) { let Some(ClipboardRequestBoxed(req)) = *self.request.borrow() else { return; }; let obj = self.obj(); match req { ClipboardRequest::Osc52Write(_) => { obj.set_heading(Some(&tr("Authorize Clipboard Access"))); obj.set_body(&tr("An application is attempting to write to the clipboard. The current clipboard contents are shown below.")); } ClipboardRequest::Osc52Read(_) => { obj.set_heading(Some(&tr("Authorize Clipboard Access"))); obj.set_body(&tr("An application is attempting to read from the clipboard. The current clipboard contents are shown below.")); } ClipboardRequest::Paste => { obj.set_heading(Some(&tr("Warning: Potentially Unsafe Paste"))); obj.set_body(&tr("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.")); } } } #[template_callback] fn reveal_clicked(_button: >k::Button, this: &super::ClipboardConfirmationDialog) { // The signal emitter is the button, so the dialog arrives as the // trailing user-data argument (a `&self` receiver would be // matched against the button and panic). this.imp().set_blurred(false, false, true); } #[template_callback] fn hide_clicked(_button: >k::Button, this: &super::ClipboardConfirmationDialog) { this.imp().set_blurred(true, true, false); } } #[glib::derived_properties] impl ObjectImpl for ClipboardConfirmationDialog { fn constructed(&self) { self.parent_constructed(); // Mirrors `self.propBlur(..); self.propRequest(..)` in init. self.notify_blur(&Self::type_pspec_dummy()); self.update_request(); } fn signals() -> &'static [Signal] { static SIGNALS: OnceLock> = OnceLock::new(); SIGNALS.get_or_init(|| { vec![ Signal::builder("deny") .param_types([bool::static_type()]) .build(), Signal::builder("confirm") .param_types([bool::static_type()]) .build(), ] }) } fn dispose(&self) { self.clipboard_contents.replace(None); self.dispose_template(); } } impl ClipboardConfirmationDialog { fn type_pspec_dummy() -> glib::ParamSpec { glib::ParamSpecBoolean::builder("blur").build() } } impl WidgetImpl for ClipboardConfirmationDialog {} impl AdwDialogImpl for ClipboardConfirmationDialog {} impl AdwAlertDialogImpl for ClipboardConfirmationDialog { fn response(&self, response: &str) { let remember = self.remember_choice.is_active(); let obj = self.obj(); if response == "cancel" { obj.emit_by_name::<()>("deny", &[&remember]); } else if response == "ok" { obj.emit_by_name::<()>("confirm", &[&remember]); } } } impl DialogImpl for ClipboardConfirmationDialog {} } glib::wrapper! { pub struct ClipboardConfirmationDialog(ObjectSubclass) @extends Dialog, adw::AlertDialog, adw::Dialog, gtk::Widget, @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager; } impl ClipboardConfirmationDialog { pub fn new() -> Self { crate::register_resources(); glib::Object::new() } pub fn present(&self, parent: Option<&impl IsA>) { self.upcast_ref::().present(parent); } /// Get the clipboard request (`getRequest`). pub fn get_request(&self) -> Option { self.imp().request.borrow().map(|b| b.0) } /// Get the clipboard contents (`getClipboardContents`). pub fn get_clipboard_contents(&self) -> Option { self.imp().clipboard_contents.borrow().clone() } pub fn set_request_value(&self, req: ClipboardRequest) { self.set_request(Some(ClipboardRequestBoxed(req))); } /// `confirm(remember)`. pub fn connect_confirm(&self, f: F) -> glib::SignalHandlerId { self.connect_closure( "confirm", false, glib::closure_local!(move |obj: &Self, remember: bool| f(obj, remember)), ) } /// `deny(remember)`. pub fn connect_deny(&self, f: F) -> glib::SignalHandlerId { self.connect_closure( "deny", false, glib::closure_local!(move |obj: &Self, remember: bool| f(obj, remember)), ) } } impl Default for ClipboardConfirmationDialog { fn default() -> Self { Self::new() } }