#include "platform/window_drag.h" #import @interface CrossDeskStreamFullscreenTarget : NSObject - (void)toggleStreamFullscreen:(id)sender; @end @implementation CrossDeskStreamFullscreenTarget - (void)toggleStreamFullscreen:(id)sender { (void)sender; crossdesk::SetStreamWindowFullscreen( !crossdesk::IsStreamWindowFullscreen()); } @end namespace crossdesk { namespace { NSWindow *stream_window = nil; CrossDeskStreamFullscreenTarget *stream_fullscreen_target = nil; bool stream_fullscreen = false; NSRect stream_restore_frame = NSZeroRect; NSWindowStyleMask stream_restore_style_mask = NSWindowStyleMaskTitled; NSWindowTitleVisibility stream_restore_title_visibility = NSWindowTitleVisible; BOOL stream_restore_titlebar_transparent = NO; BOOL stream_restore_movable = YES; BOOL stream_restore_close_hidden = NO; BOOL stream_restore_miniaturize_hidden = NO; BOOL stream_restore_zoom_hidden = NO; NSApplicationPresentationOptions stream_restore_presentation_options = NSApplicationPresentationDefault; void InstallStreamFullscreenButton(NSWindow *window) { if (window == nil) { return; } if (stream_fullscreen_target == nil) { stream_fullscreen_target = [[CrossDeskStreamFullscreenTarget alloc] init]; } NSButton *zoom_button = [window standardWindowButton:NSWindowZoomButton]; zoom_button.target = stream_fullscreen_target; zoom_button.action = @selector(toggleStreamFullscreen:); } void SetTitlebarButtonsHidden(NSWindow *window, BOOL hidden) { [window standardWindowButton:NSWindowCloseButton].hidden = hidden; [window standardWindowButton:NSWindowMiniaturizeButton].hidden = hidden; [window standardWindowButton:NSWindowZoomButton].hidden = hidden; } void CenterWindowOnScreen(NSWindow *window, NSScreen *screen) { if (window == nil || screen == nil) { return; } const NSRect available_frame = screen.visibleFrame; const NSRect window_frame = window.frame; const NSPoint centered_origin = NSMakePoint( NSMidX(available_frame) - NSWidth(window_frame) * 0.5, NSMidY(available_frame) - NSHeight(window_frame) * 0.5); [window setFrameOrigin:centered_origin]; } } // namespace bool StartNativeWindowDrag() { @autoreleasepool { NSEvent *event = [[NSApplication sharedApplication] currentEvent]; if (event == nil || event.type != NSEventTypeLeftMouseDown || event.window == nil) { return false; } [event.window performWindowDragWithEvent:event]; return true; } } bool HideDisabledMainWindowZoomButton() { @autoreleasepool { for (NSWindow *window in [NSApp windows]) { if (![window.title isEqualToString:@"CrossDesk"]) { continue; } NSButton *zoom_button = [window standardWindowButton:NSWindowZoomButton]; // The main window has a fixed size, so AppKit disables its zoom button. // Stream windows remain resizable and must keep their native button. if (zoom_button == nil || zoom_button.enabled) { continue; } zoom_button.hidden = YES; return true; } return false; } } bool ConfigureStreamWindowLiveResize() { @autoreleasepool { for (NSWindow *window in [NSApp windows]) { if (![window.title isEqualToString:@"CrossDesk"]) { continue; } NSButton *zoom_button = [window standardWindowButton:NSWindowZoomButton]; // The stream window is resizable and therefore keeps an enabled native // zoom button. The fixed-size main window's zoom button is disabled. if (zoom_button == nil || !zoom_button.enabled) { continue; } NSView *content_view = window.contentView; if (content_view == nil) { return false; } // During an ordinary edge resize, scale one cached frame and redraw once // the new window size has settled. window.preservesContentDuringLiveResize = YES; content_view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawBeforeViewResize; content_view.layerContentsPlacement = NSViewLayerContentsPlacementScaleProportionallyToFit; stream_window = window; // Native Space fullscreen cross-fades an AppKit source snapshot over a // separately rendered destination window. Disable that path for the // stream window and route the green button to the immediate transition. window.collectionBehavior = (window.collectionBehavior & ~(NSWindowCollectionBehaviorFullScreenPrimary | NSWindowCollectionBehaviorFullScreenAuxiliary | NSWindowCollectionBehaviorFullScreenAllowsTiling)) | NSWindowCollectionBehaviorFullScreenNone; InstallStreamFullscreenButton(window); content_view.needsDisplay = YES; return true; } return false; } } bool SetStreamWindowFullscreen(bool fullscreen) { @autoreleasepool { if (stream_window == nil || stream_fullscreen == fullscreen) { return stream_window != nil; } if (fullscreen) { NSScreen *screen = stream_window.screen ?: NSScreen.mainScreen; if (screen == nil) { return false; } stream_restore_frame = stream_window.frame; stream_restore_style_mask = stream_window.styleMask; stream_restore_title_visibility = stream_window.titleVisibility; stream_restore_titlebar_transparent = stream_window.titlebarAppearsTransparent; stream_restore_movable = stream_window.movable; stream_restore_close_hidden = [stream_window standardWindowButton:NSWindowCloseButton].hidden; stream_restore_miniaturize_hidden = [stream_window standardWindowButton:NSWindowMiniaturizeButton].hidden; stream_restore_zoom_hidden = [stream_window standardWindowButton:NSWindowZoomButton].hidden; stream_restore_presentation_options = NSApp.presentationOptions; stream_fullscreen = true; stream_window.styleMask = stream_restore_style_mask | NSWindowStyleMaskFullSizeContentView; stream_window.titleVisibility = NSWindowTitleHidden; stream_window.titlebarAppearsTransparent = YES; stream_window.movable = NO; SetTitlebarButtonsHidden(stream_window, YES); NSApplicationPresentationOptions presentation_options = stream_restore_presentation_options; if ((presentation_options & (NSApplicationPresentationHideDock | NSApplicationPresentationAutoHideDock)) == 0) { presentation_options |= NSApplicationPresentationAutoHideDock; } if ((presentation_options & (NSApplicationPresentationHideMenuBar | NSApplicationPresentationAutoHideMenuBar)) == 0) { presentation_options |= NSApplicationPresentationAutoHideMenuBar; } NSApp.presentationOptions = presentation_options; [stream_window setFrame:screen.frame display:YES animate:NO]; [stream_window makeKeyAndOrderFront:nil]; } else { stream_fullscreen = false; NSApp.presentationOptions = stream_restore_presentation_options; stream_window.styleMask = stream_restore_style_mask; stream_window.titleVisibility = stream_restore_title_visibility; stream_window.titlebarAppearsTransparent = stream_restore_titlebar_transparent; stream_window.movable = stream_restore_movable; [stream_window setFrame:stream_restore_frame display:YES animate:NO]; [stream_window standardWindowButton:NSWindowCloseButton].hidden = stream_restore_close_hidden; [stream_window standardWindowButton:NSWindowMiniaturizeButton].hidden = stream_restore_miniaturize_hidden; [stream_window standardWindowButton:NSWindowZoomButton].hidden = stream_restore_zoom_hidden; InstallStreamFullscreenButton(stream_window); [stream_window makeKeyAndOrderFront:nil]; } stream_window.contentView.alphaValue = 1.0; stream_window.contentView.needsDisplay = YES; [stream_window displayIfNeeded]; return true; } } bool IsStreamWindowFullscreen() { return stream_fullscreen; } std::string OpenNativeFileDialog(const std::string &title) { @autoreleasepool { NSOpenPanel *panel = [NSOpenPanel openPanel]; panel.canChooseFiles = YES; panel.canChooseDirectories = NO; panel.allowsMultipleSelection = NO; panel.resolvesAliases = YES; NSString *prompt = [[NSString alloc] initWithBytes:title.data() length:title.size() encoding:NSUTF8StringEncoding]; if (prompt != nil && prompt.length > 0) { panel.title = prompt; panel.message = prompt; } // tinyfiledialogs uses an external osascript process on macOS, so its // chooser can appear without becoming the active window. Keep the picker // inside CrossDesk and explicitly activate both the app and panel. [NSApp activateIgnoringOtherApps:YES]; NSWindow *owner = stream_window ?: NSApp.keyWindow ?: NSApp.mainWindow; if (owner != nil) { [owner makeKeyAndOrderFront:nil]; } NSScreen *target_screen = owner.screen ?: NSScreen.mainScreen; CenterWindowOnScreen(panel, target_screen); [panel makeKeyAndOrderFront:nil]; if ([panel runModal] != NSModalResponseOK || panel.URL == nil) { return {}; } const char *path = panel.URL.fileSystemRepresentation; return path != nullptr ? std::string(path) : std::string{}; } } } // namespace crossdesk