Skip to content

Commit

Permalink
feat: 支持在 MacOS 上打开剪贴板窗口时不抢占其他窗口的焦点 (#727)
Browse files Browse the repository at this point in the history
  • Loading branch information
ayangweb authored Oct 23, 2024
1 parent 877e787 commit a3f1aa8
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 154 deletions.
242 changes: 141 additions & 101 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ log = "0.4.22"
cocoa = "0.25.0"
tauri-plugin-shell = "2.0.1"
tauri-plugin = { version = "2.0.1", features = ["build"] }
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
tauri-plugin-eco-window = { path = "./src-tauri/src/plugins/window" }
tauri-plugin-eco-macos-permissions = { path = "./src-tauri/src/plugins/macos-permissions" }
tauri-plugin-eco-mouse = { path = "./src-tauri/src/plugins/mouse" }
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ tauri-plugin-eco-window-state.workspace = true
tauri-plugin-eco-autostart.workspace = true

[target."cfg(target_os = \"macos\")".dependencies]
cocoa.workspace = true
tauri-nspanel.workspace = true
51 changes: 43 additions & 8 deletions src-tauri/src/core/setup/mac.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
use cocoa::appkit::{NSMainMenuWindowLevel, NSWindow};
use cocoa::base::id;
use tauri::{ActivationPolicy, App, WebviewWindow};
use tauri::{ActivationPolicy, App, Emitter, Manager, WebviewWindow};
use tauri_nspanel::{
cocoa::appkit::{NSMainMenuWindowLevel, NSWindowCollectionBehavior},
panel_delegate, WebviewWindowExt,
};

#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
#[allow(non_upper_case_globals)]
const NSResizableWindowMask: i32 = 1 << 3;
const MACOS_PANEL_FOCUS: &str = "macos-panel-focus";

pub fn platform(app: &mut App, main_window: WebviewWindow, _preference_window: WebviewWindow) {
let app_handle = app.app_handle().clone();

// 隐藏 mac 的程序坞图标:https://github.com/tauri-apps/tauri/issues/4852#issuecomment-1312716378
app.set_activation_policy(ActivationPolicy::Accessory);

unsafe {
let ns_window = main_window.ns_window().unwrap() as id;
// 把 ns_window 转换为 ns_panel
let panel = main_window.to_panel().unwrap();

// 让窗口在程序坞之上
panel.set_level(NSMainMenuWindowLevel + 1);

// 不抢占其它窗口的焦点和支持缩放
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel | NSResizableWindowMask);

// 定义面板的委托 (delegate),用于监听面板窗口的事件
let delegate = panel_delegate!(EcoPanelDelegate {
window_did_become_key,
window_did_resign_key
});

delegate.set_listener(Box::new(move |delegate_name: String| {
match delegate_name.as_str() {
// 当窗口获得键盘焦点时调用
"window_did_become_key" => {
app_handle.emit(MACOS_PANEL_FOCUS, true).unwrap();
}
// 当窗口失去键盘焦点时调用
"window_did_resign_key" => {
app_handle.emit(MACOS_PANEL_FOCUS, false).unwrap();
}
_ => (),
}
}));

// 让窗口在程序坞和菜单栏之上
ns_window.setLevel_(NSMainMenuWindowLevel as i64 + 1);
}
// 设置窗口的委托对象,用于处理窗口的事件。
panel.set_delegate(delegate);
}
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ pub fn run() {
.plugin(tauri_plugin_updater::Builder::new().build())
// 进程相关插件:https://github.com/tauri-apps/tauri-plugin-process
.plugin(tauri_plugin_process::init())
// macos window 转 ns_panel
.plugin(tauri_nspanel::init())
// 自定义的窗口管理插件
.plugin(tauri_plugin_eco_window::init())
// 自定义的 fs_extra 插件
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/plugins/paste/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ tauri-plugin.workspace = true
[target."cfg(target_os = \"macos\")".dependencies]
cocoa.workspace = true
objc = "0.2.7"
tauri-nspanel.workspace = true

[target."cfg(target_os = \"windows\")".dependencies]
log.workspace = true
Expand Down
41 changes: 15 additions & 26 deletions src-tauri/src/plugins/paste/src/commands/mac.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use cocoa::appkit::{NSApplicationActivationOptions, NSRunningApplication};
use cocoa::base::{id, nil};
use cocoa::foundation::{NSAutoreleasePool, NSString};
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{msg_send, sel, sel_impl};
use std::ffi::CStr;
use std::process::Command;
use std::sync::Mutex;
use std::thread;
use tauri::command;
use tauri_plugin_eco_window::MAIN_WINDOW_TITLE;
use tauri::{command, AppHandle, Runtime};
use tauri_nspanel::ManagerExt;
use tauri_plugin_eco_window::{MAIN_WINDOW_LABEL, MAIN_WINDOW_TITLE};

static PREVIOUS_WINDOW: Mutex<Option<i32>> = Mutex::new(None);

Expand Down Expand Up @@ -78,33 +79,21 @@ pub fn get_previous_window() -> Option<i32> {
return PREVIOUS_WINDOW.lock().unwrap().clone();
}

// 聚焦前一个窗口
fn focus_previous_window() {
let process_id = match get_previous_window() {
Some(process_id) => process_id,
None => return,
};

unsafe {
let app = NSRunningApplication::runningApplicationWithProcessIdentifier(nil, process_id);

app.activateWithOptions_(
NSApplicationActivationOptions::NSApplicationActivateIgnoringOtherApps,
);
}
}

// 粘贴
#[command]
pub async fn paste() {
focus_previous_window();
pub async fn paste<R: Runtime>(app_handle: AppHandle<R>) {
let handle = app_handle.clone();

let _ = app_handle.run_on_main_thread(move || {
let panel = handle.get_webview_panel(MAIN_WINDOW_LABEL).unwrap();

panel.resign_key_window();
});

let script =
r#"osascript -e 'tell application "System Events" to keystroke "v" using command down'"#;
let script = r#"tell application "System Events" to keystroke "v" using command down"#;

std::process::Command::new("sh")
.arg("-c")
.arg(script)
Command::new("osascript")
.args(["-e", script])
.output()
.expect("failed to execute process");
}
3 changes: 3 additions & 0 deletions src-tauri/src/plugins/window/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ serde.workspace = true

[build-dependencies]
tauri-plugin.workspace = true

[target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel.workspace = true
41 changes: 33 additions & 8 deletions src-tauri/src/plugins/window/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use tauri::{async_runtime::spawn, command, AppHandle, Manager, Runtime, WebviewWindow};
use tauri_nspanel::ManagerExt;

// 主窗口的label
pub static MAIN_WINDOW_LABEL: &str = "main";
Expand All @@ -9,16 +10,36 @@ pub static MAIN_WINDOW_TITLE: &str = "EcoPaste";

// 显示窗口
#[command]
pub async fn show_window<R: Runtime>(window: WebviewWindow<R>) {
window.show().unwrap();
window.unminimize().unwrap();
window.set_focus().unwrap();
pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
if cfg!(target_os = "macos") && window.label() == MAIN_WINDOW_LABEL {
let handle = app_handle.clone();

let _ = app_handle.run_on_main_thread(move || {
let panel = handle.get_webview_panel(MAIN_WINDOW_LABEL).unwrap();

panel.show();
});
} else {
window.show().unwrap();
window.unminimize().unwrap();
window.set_focus().unwrap();
}
}

// 隐藏窗口
#[command]
pub async fn hide_window<R: Runtime>(window: WebviewWindow<R>) {
window.hide().unwrap();
pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
if cfg!(target_os = "macos") && window.label() == MAIN_WINDOW_LABEL {
let handle = app_handle.clone();

let _ = app_handle.run_on_main_thread(move || {
let panel = handle.get_webview_panel(MAIN_WINDOW_LABEL).unwrap();

panel.order_out(None);
});
} else {
window.hide().unwrap();
}
}

// 显示任务栏图标
Expand All @@ -44,20 +65,24 @@ pub fn show_taskbar_icon<R: Runtime>(

// 显示主窗口
pub fn show_main_window(app_handle: &AppHandle) {
let app_handle_clone = app_handle.clone();

let window = app_handle.get_webview_window(MAIN_WINDOW_LABEL).unwrap();

spawn(async move {
show_window(window).await;
show_window(app_handle_clone, window).await;
});
}

// 显示偏好设置窗口
pub fn show_preference_window(app_handle: &AppHandle) {
let app_handle_clone = app_handle.clone();

let window = app_handle
.get_webview_window(PREFERENCE_WINDOW_LABEL)
.unwrap();

spawn(async move {
show_window(window).await;
show_window(app_handle_clone, window).await;
});
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const LISTEN_KEY = {
CLIPBOARD_ITEM_SELECT_NEXT: "clipboard-item-select-next",
CLOSE_DATABASE: "close-database",
TOGGLE_LISTEN_CLIPBOARD: "toggle-listen-clipboard",
MACOS_PANEL_FOCUS: "macos-panel-focus",
};

export const WINDOW_PLUGIN = {
Expand Down
13 changes: 3 additions & 10 deletions src/hooks/useFocus.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";

interface Props {
onFocus?: () => void;
onBlur?: () => void;
}

interface State {
unlisten?: () => void;
}

export const useFocus = (props: Props) => {
const { onFocus, onBlur } = props;

const state = useReactive<State>({});

const { run } = useDebounceFn(
({ payload }) => {
if (payload) {
Expand All @@ -28,10 +23,8 @@ export const useFocus = (props: Props) => {
useMount(async () => {
const appWindow = getCurrentWebviewWindow();

state.unlisten = await appWindow.onFocusChanged(run);
});
appWindow.onFocusChanged(run);

useUnmount(() => {
state.unlisten?.();
listen(LISTEN_KEY.MACOS_PANEL_FOCUS, run);
});
};

0 comments on commit a3f1aa8

Please sign in to comment.