mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
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
99 lines
4.3 KiB
Rust
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);
|
|
})
|
|
}
|