Files
openidea/src-tauri/src/macos.rs
T
l0ng-ai 8d7ce81249 fix(macos): open project windows as tabs per the system tab preference (#693)
Project windows only joined a tab group when merged explicitly or restored at startup; the macOS "Prefer tabs when opening documents" setting was never read, so opening another project or a workspace always created a separate window.

New project windows now follow that preference: with Always they join the active project window's tab group (forming one if needed) and are selected; with In Full Screen they do so only while the active window is full screen; with Manually they stay separate. This covers every path that creates a project window — Open, recent projects, workspaces, drag and drop, and opening from the command line or Finder — while LightEdit windows and restored groups keep their behaviour. New Workspace now opens through the same Open project in policy as Open Workspace, and a race when toggling full screen during the join is fixed.

Fixes #678
2026-11-01 20:14:08 +08:00

99 lines
4.3 KiB
Rust

//! macOS window tab preferences, applied to application-drawn project tabs.
use crate::windows::{Launch, WindowsState};
use ide_core::{IdeError, IdeResult};
use ide_workspace::window_groups::{TabbingPreference, should_group_new_project};
use objc2::{MainThreadMarker, rc::Retained, runtime::ProtocolObject};
use objc2_app_kit::{NSWindow, NSWindowUserTabbingPreference};
use objc2_foundation::{NSNotificationCenter, NSObjectProtocol};
use tauri::{AppHandle, Manager, WebviewWindow};
fn tabbing_preference(app: &AppHandle) -> IdeResult<TabbingPreference> {
fn read(mtm: MainThreadMarker) -> TabbingPreference {
match NSWindow::userTabbingPreference(mtm) {
NSWindowUserTabbingPreference::Always => TabbingPreference::Always,
NSWindowUserTabbingPreference::InFullScreen => TabbingPreference::InFullScreen,
_ => TabbingPreference::Manually,
}
}
if let Some(mtm) = MainThreadMarker::new() {
return Ok(read(mtm));
}
let (tx, rx) = std::sync::mpsc::sync_channel(1);
app.run_on_main_thread(move || {
let _ = tx.send(read(MainThreadMarker::new().expect("AppKit main thread")));
})
.map_err(|e| IdeError::internal(e.to_string()))?;
rx.recv().map_err(|e| IdeError::internal(e.to_string()))
}
/// Capture before building the new window, which takes focus itself.
pub fn new_project_tab_target(app: &AppHandle, launch: &Launch) -> IdeResult<Option<WebviewWindow>> {
let windows = app.webview_windows();
let active = windows
.values()
.find(|w| w.is_focused().unwrap_or(false))
.cloned()
.or_else(|| app.state::<WindowsState>().last_focused().and_then(|label| windows.get(&label).cloned()));
let active = active.and_then(|w| {
let label = crate::windows::settings_parent(w.label()).unwrap_or(w.label());
windows.get(label).cloned()
});
let active = active.filter(|w| {
let host = app.state::<crate::state::AppState>();
host.project(w.label()).is_ok_and(|p| !p.light) || host.remote_dev.get(w.label()).is_some()
});
let fullscreen = active.as_ref().is_some_and(|w| w.is_fullscreen().unwrap_or(false));
let light_edit = launch.project.is_none() && launch.remote.is_none();
Ok(should_group_new_project(tabbing_preference(app)?, fullscreen, active.is_some(), light_edit)
.then_some(active)
.flatten())
}
struct FullscreenObserver(Retained<ProtocolObject<dyn NSObjectProtocol>>);
impl Drop for FullscreenObserver {
fn drop(&mut self) {
let observer: &ProtocolObject<dyn NSObjectProtocol> = &self.0;
unsafe { NSNotificationCenter::defaultCenter().removeObserver(observer.as_ref()) };
}
}
thread_local! {
static FULLSCREEN_TRANSFERS: std::cell::RefCell<std::collections::HashMap<String, FullscreenObserver>> = std::cell::RefCell::default();
}
pub fn forget_fullscreen(label: &str) {
FULLSCREEN_TRANSFERS.with_borrow_mut(|observers| observers.remove(label));
}
/// AppKit cannot enter another window's fullscreen Space until the old one has exited.
pub fn transfer_fullscreen(from: &WebviewWindow, to: &WebviewWindow) -> tauri::Result<()> {
let source = from.label().to_owned();
let to = to.clone();
from.with_webview(move |webview| unsafe {
let native = &*webview.ns_window().cast::<NSWindow>();
forget_fullscreen(&source);
let label = source.clone();
let block = block2::RcBlock::new(move |_| {
forget_fullscreen(&label);
let app = to.app_handle();
let selected = app.state::<WindowsState>().groups.lock().group(to.label()).map(|g| g.selected.clone());
if let Some(selected) = selected {
// Exiting fullscreen can show the old window again; reapply group visibility.
let _ = crate::window_groups::select(app, &selected, &selected);
if selected == to.label() {
let _ = to.set_fullscreen(true);
}
}
});
let observer = NSNotificationCenter::defaultCenter().addObserverForName_object_queue_usingBlock(
Some(objc2_app_kit::NSWindowDidExitFullScreenNotification),
Some(native),
None,
&block,
);
FULLSCREEN_TRANSFERS.with_borrow_mut(|observers| observers.insert(source, FullscreenObserver(observer)));
native.toggleFullScreen(None);
})
}