From e05d718a7b46476d1fe4817c169008080e84f959 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 28 Mar 2022 07:17:28 -0700 Subject: [PATCH] feat(core): add hotkey to toggle devtools, closes #3776 (#3791) --- .changes/devtools-apis.md | 6 ++ .changes/devtools-hotkey.md | 5 ++ .changes/window-devtools-apis.md | 5 ++ core/tauri-runtime-wry/src/lib.rs | 33 +++++++ core/tauri-runtime/src/lib.rs | 130 ++++++++++++++-------------- core/tauri/scripts/hotkey.js | 2 + core/tauri/scripts/init.js | 4 + core/tauri/src/endpoints/window.rs | 11 +++ core/tauri/src/manager.rs | 31 +++++++ core/tauri/src/test/mock_runtime.rs | 8 ++ core/tauri/src/window.rs | 68 +++++++++++++++ 11 files changed, 239 insertions(+), 64 deletions(-) create mode 100644 .changes/devtools-apis.md create mode 100644 .changes/devtools-hotkey.md create mode 100644 .changes/window-devtools-apis.md create mode 100644 core/tauri/scripts/hotkey.js diff --git a/.changes/devtools-apis.md b/.changes/devtools-apis.md new file mode 100644 index 000000000..d61dc8a2b --- /dev/null +++ b/.changes/devtools-apis.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime": minor +"tauri-runtime-wry": minor +--- + +Added `close_devtools` and `is_devtools_open` APIs to the `Dispatch` trait. diff --git a/.changes/devtools-hotkey.md b/.changes/devtools-hotkey.md new file mode 100644 index 000000000..17a534378 --- /dev/null +++ b/.changes/devtools-hotkey.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Toggle devtools when `Ctrl + Shift + I` or `Command + Option + I` is pressed. diff --git a/.changes/window-devtools-apis.md b/.changes/window-devtools-apis.md new file mode 100644 index 000000000..ded3a8b29 --- /dev/null +++ b/.changes/window-devtools-apis.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Added `close_devtools` and `is_devtools_open` APIs to the `Window` struct. diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index 64bdc4287..9ed9b52c7 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -984,8 +984,13 @@ unsafe impl Send for GtkWindow {} #[derive(Debug, Clone)] pub enum WindowMessage { + // Devtools #[cfg(any(debug_assertions, feature = "devtools"))] OpenDevTools, + #[cfg(any(debug_assertions, feature = "devtools"))] + CloseDevTools, + #[cfg(any(debug_assertions, feature = "devtools"))] + IsDevToolsOpen(Sender), // Getters ScaleFactor(Sender), InnerPosition(Sender>>), @@ -1174,6 +1179,20 @@ impl Dispatch for WryDispatcher { ); } + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self) { + let _ = send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::CloseDevTools), + ); + } + + /// Gets the devtools window's current open state. + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result { + window_getter!(self, WindowMessage::IsDevToolsOpen) + } + // Getters fn scale_factor(&self) -> Result { @@ -2002,6 +2021,20 @@ fn handle_user_message( w.open_devtools(); } } + #[cfg(any(debug_assertions, feature = "devtools"))] + WindowMessage::CloseDevTools => { + if let WindowHandle::Webview(w) = &webview.inner { + w.close_devtools(); + } + } + #[cfg(any(debug_assertions, feature = "devtools"))] + WindowMessage::IsDevToolsOpen(tx) => { + if let WindowHandle::Webview(w) = &webview.inner { + tx.send(w.is_devtools_open()).unwrap(); + } else { + tx.send(false).unwrap(); + } + } // Getters WindowMessage::ScaleFactor(tx) => tx.send(window.scale_factor()).unwrap(), WindowMessage::InnerPosition(tx) => tx diff --git a/core/tauri-runtime/src/lib.rs b/core/tauri-runtime/src/lib.rs index b1e663c7c..82614dc80 100644 --- a/core/tauri-runtime/src/lib.rs +++ b/core/tauri-runtime/src/lib.rs @@ -284,33 +284,29 @@ pub trait RuntimeHandle: Debug + Clone + Send + Sync + Sized + 'st fn create_window( &self, pending: PendingWindow, - ) -> crate::Result>; + ) -> Result>; /// Run a task on the main thread. - fn run_on_main_thread(&self, f: F) -> crate::Result<()>; + fn run_on_main_thread(&self, f: F) -> Result<()>; #[cfg(all(windows, feature = "system-tray"))] #[cfg_attr(doc_cfg, doc(cfg(all(windows, feature = "system-tray"))))] - fn remove_system_tray(&self) -> crate::Result<()>; + fn remove_system_tray(&self) -> Result<()>; } /// A global shortcut manager. pub trait GlobalShortcutManager: Debug + Clone + Send + Sync { /// Whether the application has registered the given `accelerator`. - fn is_registered(&self, accelerator: &str) -> crate::Result; + fn is_registered(&self, accelerator: &str) -> Result; /// Register a global shortcut of `accelerator`. - fn register( - &mut self, - accelerator: &str, - handler: F, - ) -> crate::Result<()>; + fn register(&mut self, accelerator: &str, handler: F) -> Result<()>; /// Unregister all accelerators registered by the manager instance. - fn unregister_all(&mut self) -> crate::Result<()>; + fn unregister_all(&mut self) -> Result<()>; /// Unregister the provided `accelerator`. - fn unregister(&mut self, accelerator: &str) -> crate::Result<()>; + fn unregister(&mut self, accelerator: &str) -> Result<()>; } /// Clipboard manager. @@ -342,12 +338,12 @@ pub trait Runtime: Debug + Sized + 'static { type EventLoopProxy: EventLoopProxy; /// Creates a new webview runtime. Must be used on the main thread. - fn new() -> crate::Result; + fn new() -> Result; /// Creates a new webview runtime on any thread. #[cfg(any(windows, target_os = "linux"))] #[cfg_attr(doc_cfg, doc(cfg(any(windows, target_os = "linux"))))] - fn new_any_thread() -> crate::Result; + fn new_any_thread() -> Result; /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop. fn create_proxy(&self) -> Self::EventLoopProxy; @@ -362,15 +358,12 @@ pub trait Runtime: Debug + Sized + 'static { fn clipboard_manager(&self) -> Self::ClipboardManager; /// Create a new webview window. - fn create_window( - &self, - pending: PendingWindow, - ) -> crate::Result>; + fn create_window(&self, pending: PendingWindow) -> Result>; /// Adds the icon to the system tray with the specified menu items. #[cfg(feature = "system-tray")] #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - fn system_tray(&self, system_tray: SystemTray) -> crate::Result; + fn system_tray(&self, system_tray: SystemTray) -> Result; /// Registers a system tray event handler. #[cfg(feature = "system-tray")] @@ -398,7 +391,7 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static type WindowBuilder: WindowBuilder; /// Run a task on the main thread. - fn run_on_main_thread(&self, f: F) -> crate::Result<()>; + fn run_on_main_thread(&self, f: F) -> Result<()>; /// Registers a window event handler. fn on_window_event(&self, f: F) -> Uuid; @@ -406,68 +399,77 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static /// Registers a window event handler. fn on_menu_event(&self, f: F) -> Uuid; + /// Open the web inspector which is usually called devtools. #[cfg(any(debug_assertions, feature = "devtools"))] fn open_devtools(&self); + /// Close the web inspector which is usually called devtools. + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self); + + /// Gets the devtools window's current open state. + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result; + // GETTERS /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. - fn scale_factor(&self) -> crate::Result; + fn scale_factor(&self) -> Result; /// Returns the position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. - fn inner_position(&self) -> crate::Result>; + fn inner_position(&self) -> Result>; /// Returns the position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. - fn outer_position(&self) -> crate::Result>; + fn outer_position(&self) -> Result>; /// Returns the physical size of the window's client area. /// /// The client area is the content of the window, excluding the title bar and borders. - fn inner_size(&self) -> crate::Result>; + fn inner_size(&self) -> Result>; /// Returns the physical size of the entire window. /// /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. - fn outer_size(&self) -> crate::Result>; + fn outer_size(&self) -> Result>; /// Gets the window's current fullscreen state. - fn is_fullscreen(&self) -> crate::Result; + fn is_fullscreen(&self) -> Result; /// Gets the window's current maximized state. - fn is_maximized(&self) -> crate::Result; + fn is_maximized(&self) -> Result; /// Gets the window’s current decoration state. - fn is_decorated(&self) -> crate::Result; + fn is_decorated(&self) -> Result; /// Gets the window’s current resizable state. - fn is_resizable(&self) -> crate::Result; + fn is_resizable(&self) -> Result; /// Gets the window's current vibility state. - fn is_visible(&self) -> crate::Result; + fn is_visible(&self) -> Result; /// Gets the window menu current visibility state. - fn is_menu_visible(&self) -> crate::Result; + fn is_menu_visible(&self) -> Result; /// Returns the monitor on which the window currently resides. /// /// Returns None if current monitor can't be detected. - fn current_monitor(&self) -> crate::Result>; + fn current_monitor(&self) -> Result>; /// Returns the primary monitor of the system. /// /// Returns None if it can't identify any monitor as a primary one. - fn primary_monitor(&self) -> crate::Result>; + fn primary_monitor(&self) -> Result>; /// Returns the list of all the monitors available on the system. - fn available_monitors(&self) -> crate::Result>; + fn available_monitors(&self) -> Result>; /// Returns the native handle that is used by this window. #[cfg(windows)] - fn hwnd(&self) -> crate::Result; + fn hwnd(&self) -> Result; /// Returns the native handle that is used by this window. #[cfg(target_os = "macos")] - fn ns_window(&self) -> crate::Result<*mut std::ffi::c_void>; + fn ns_window(&self) -> Result<*mut std::ffi::c_void>; /// Returns the `ApplicatonWindow` from gtk crate that is used by this window. #[cfg(any( @@ -477,96 +479,96 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static target_os = "netbsd", target_os = "openbsd" ))] - fn gtk_window(&self) -> crate::Result; + fn gtk_window(&self) -> Result; // SETTERS /// Centers the window. - fn center(&self) -> crate::Result<()>; + fn center(&self) -> Result<()>; /// Opens the dialog to prints the contents of the webview. - fn print(&self) -> crate::Result<()>; + fn print(&self) -> Result<()>; /// Requests user attention to the window. /// /// Providing `None` will unset the request for user attention. - fn request_user_attention(&self, request_type: Option) -> crate::Result<()>; + fn request_user_attention(&self, request_type: Option) -> Result<()>; /// Create a new webview window. fn create_window( &mut self, pending: PendingWindow, - ) -> crate::Result>; + ) -> Result>; /// Updates the window resizable flag. - fn set_resizable(&self, resizable: bool) -> crate::Result<()>; + fn set_resizable(&self, resizable: bool) -> Result<()>; /// Updates the window title. - fn set_title>(&self, title: S) -> crate::Result<()>; + fn set_title>(&self, title: S) -> Result<()>; /// Maximizes the window. - fn maximize(&self) -> crate::Result<()>; + fn maximize(&self) -> Result<()>; /// Unmaximizes the window. - fn unmaximize(&self) -> crate::Result<()>; + fn unmaximize(&self) -> Result<()>; /// Minimizes the window. - fn minimize(&self) -> crate::Result<()>; + fn minimize(&self) -> Result<()>; /// Unminimizes the window. - fn unminimize(&self) -> crate::Result<()>; + fn unminimize(&self) -> Result<()>; /// Shows the window menu. - fn show_menu(&self) -> crate::Result<()>; + fn show_menu(&self) -> Result<()>; /// Hides the window menu. - fn hide_menu(&self) -> crate::Result<()>; + fn hide_menu(&self) -> Result<()>; /// Shows the window. - fn show(&self) -> crate::Result<()>; + fn show(&self) -> Result<()>; /// Hides the window. - fn hide(&self) -> crate::Result<()>; + fn hide(&self) -> Result<()>; /// Closes the window. - fn close(&self) -> crate::Result<()>; + fn close(&self) -> Result<()>; /// Updates the hasDecorations flag. - fn set_decorations(&self, decorations: bool) -> crate::Result<()>; + fn set_decorations(&self, decorations: bool) -> Result<()>; /// Updates the window alwaysOnTop flag. - fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()>; + fn set_always_on_top(&self, always_on_top: bool) -> Result<()>; /// Resizes the window. - fn set_size(&self, size: Size) -> crate::Result<()>; + fn set_size(&self, size: Size) -> Result<()>; /// Updates the window min size. - fn set_min_size(&self, size: Option) -> crate::Result<()>; + fn set_min_size(&self, size: Option) -> Result<()>; /// Updates the window max size. - fn set_max_size(&self, size: Option) -> crate::Result<()>; + fn set_max_size(&self, size: Option) -> Result<()>; /// Updates the window position. - fn set_position(&self, position: Position) -> crate::Result<()>; + fn set_position(&self, position: Position) -> Result<()>; /// Updates the window fullscreen state. - fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()>; + fn set_fullscreen(&self, fullscreen: bool) -> Result<()>; /// Bring the window to front and focus. - fn set_focus(&self) -> crate::Result<()>; + fn set_focus(&self) -> Result<()>; /// Updates the window icon. - fn set_icon(&self, icon: WindowIcon) -> crate::Result<()>; + fn set_icon(&self, icon: WindowIcon) -> Result<()>; /// Whether to show the window icon in the task bar or not. - fn set_skip_taskbar(&self, skip: bool) -> crate::Result<()>; + fn set_skip_taskbar(&self, skip: bool) -> Result<()>; /// Starts dragging the window. - fn start_dragging(&self) -> crate::Result<()>; + fn start_dragging(&self) -> Result<()>; /// Executes javascript on the window this [`Dispatch`] represents. - fn eval_script>(&self, script: S) -> crate::Result<()>; + fn eval_script>(&self, script: S) -> Result<()>; /// Applies the specified `update` to the menu item associated with the given `id`. - fn update_menu_item(&self, id: u16, update: menu::MenuUpdate) -> crate::Result<()>; + fn update_menu_item(&self, id: u16, update: menu::MenuUpdate) -> Result<()>; } diff --git a/core/tauri/scripts/hotkey.js b/core/tauri/scripts/hotkey.js new file mode 100644 index 000000000..4453fe2c0 --- /dev/null +++ b/core/tauri/scripts/hotkey.js @@ -0,0 +1,2 @@ +/*! hotkeys-js v3.8.7 | MIT (c) 2021 kenny wong | http://jaywcjlove.github.io/hotkeys */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).hotkeys=t()}(this,function(){"use strict";var e="undefined"!=typeof navigator&&0 { + if window.is_devtools_open() { + window.close_devtools(); + } else { + window.open_devtools(); + } + } #[allow(unreachable_patterns)] _ => return Err(cmd.into_allowlist_error()), } diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index ea3ade1cf..368461d69 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -880,6 +880,8 @@ impl WindowManager { plugin_initialization_script: &'a str, #[raw] freeze_prototype: &'a str, + #[raw] + hotkeys: &'a str, } let bundle_script = if with_global_tauri { @@ -894,6 +896,34 @@ impl WindowManager { "" }; + #[cfg(any(debug_assertions, feature = "devtools"))] + let hotkeys = &format!( + " + {}; + window.hotkeys('{}', () => {{ + window.__TAURI_INVOKE__('tauri', {{ + __tauriModule: 'Window', + message: {{ + cmd: 'manage', + data: {{ + cmd: {{ + type: '__toggleDevtools' + }} + }} + }} + }}); + }}); + ", + include_str!("../scripts/hotkey.js"), + if cfg!(target_os = "macos") { + "command+option+i" + } else { + "ctrl+shift+i" + } + ); + #[cfg(not(any(debug_assertions, feature = "devtools")))] + let hotkeys = ""; + InitJavascript { origin: self.get_browser_origin(), pattern_script, @@ -913,6 +943,7 @@ impl WindowManager { event_initialization_script: &self.event_initialization_script(), plugin_initialization_script, freeze_prototype, + hotkeys, } .render_default(&Default::default()) .map(|s| s.into_string()) diff --git a/core/tauri/src/test/mock_runtime.rs b/core/tauri/src/test/mock_runtime.rs index f44947b31..2ab8a2217 100644 --- a/core/tauri/src/test/mock_runtime.rs +++ b/core/tauri/src/test/mock_runtime.rs @@ -277,6 +277,14 @@ impl Dispatch for MockDispatcher { #[cfg(any(debug_assertions, feature = "devtools"))] fn open_devtools(&self) {} + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self) {} + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result { + Ok(false) + } + fn scale_factor(&self) -> Result { Ok(1.0) } diff --git a/core/tauri/src/window.rs b/core/tauri/src/window.rs index 85f29f837..14234118f 100644 --- a/core/tauri/src/window.rs +++ b/core/tauri/src/window.rs @@ -812,6 +812,74 @@ impl Window { self.window.dispatcher.open_devtools(); } + /// Closes the developer tools window (Web Inspector). + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** This is a private API on macOS, + /// so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(debug_assertions)] + /// { + /// let window = app.get_window("main").unwrap(); + /// window.open_devtools(); + /// std::thread::spawn(move || { + /// std::thread::sleep(std::time::Duration::from_secs(10)); + /// window.close_devtools(); + /// }); + /// } + /// Ok(()) + /// }); + /// ``` + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(doc_cfg, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn close_devtools(&self) { + self.window.dispatcher.close_devtools(); + } + + /// Checks if the developer tools window (Web Inspector) is opened. + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** This is a private API on macOS, + /// so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(debug_assertions)] + /// { + /// let window = app.get_window("main").unwrap(); + /// if !window.is_devtools_open() { + /// window.open_devtools(); + /// } + /// } + /// Ok(()) + /// }); + /// ``` + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(doc_cfg, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn is_devtools_open(&self) -> bool { + self + .window + .dispatcher + .is_devtools_open() + .unwrap_or_default() + } + // Getters /// Gets a handle to the window menu.