Другое

Создание NSPanel с поведением NSOpenPanel

Узнайте, как создать пользовательский NSPanel с поведением, аналогичным NSOpenPanel, в macOS AppKit. Полное руководство с примерами кода и ключевыми свойствами.

macOS AppKit: Как создать пользовательский NSPanel с поведением стека, аналогичным NSOpenPanel

Задача

На macOS (AppKit) мне нужно создать пользовательский NSPanel, который демонстрирует такое же поведение стека, как системные панели, например NSOpenPanel или NSSavePanel, но при этом не блокирует другие окна.

Желаемое поведение

  • Окно всегда должно оставаться поверх всех других окон внутри моего приложения
  • Оно никогда не должно появляться поверх окон из других приложений
  • Окно должно оставаться видимым, когда мое приложение не активно (это основная проблема)
  • Оно не должно быть глобальным окном «всегда поверх», которое плавает над всем в системе

Технический контекст

Я экспериментировал с различными подходами, включая:

  • разные уровни окон (NSWindow.Level)
  • конфигурации NSPanel (isFloatingPanel, hidesOnDeactivate и т.д.)
  • различные комбинации флагов

Однако я не смог воспроизвести то же поведение, которое использует система для NSOpenPanel.

Вопрос

Есть ли поддерживаемый способ добиться поведения стека, аналогичного NSOpenPanel, для пользовательского, немодального окна в AppKit, или это поведение зарезервировано только для встроенных системных панелей? Любое разъяснение того, что возможно, а что нет в AppKit в этом контексте, было бы очень полезно.

Понимание поведения NSOpenPanel

NSOpenPanel и NSSavePanel демонстрируют уникальное поведение стека, которое отличается от стандартных окон. Согласно документации Apple, NSPanel – «специальный тип окна, которое обычно выполняет вспомогательную функцию по отношению к основному окну».

Ключевые характеристики поведения системных панелей:

  • Неактивируемые: они принимают пользовательский ввод, но не захватывают фокус от других окон
  • Относительные к приложению: остаются над окнами приложения, но под окнами других приложений
  • Постоянные: остаются видимыми даже когда приложение не активно
  • Учитывающие пространство: корректно работают в разных рабочих пространствах и полноэкранных приложениях

Как отмечено в обсуждениях Stack Overflow, такое поведение не наследуется автоматически для пользовательских экземпляров NSPanel и требует специфической настройки.


Ключевые свойства для пользовательского поведения панели

Стиль .nonactivatingPanel

Самое важное свойство – это styleMask с флагом .nonactivatingPanel, доступный только для NSPanel, а не для NSWindow. Как объясняется в ответе Stack Overflow, этот стиль:

«позволяет окну принимать клики, но не отнимать фокус от того, над чем пользователь работает»

Это критично для неблокирующего поведения, которое вы хотите.

Поведение коллекции

Свойство collectionBehavior определяет, как окно взаимодействует с разными пространствами и полноэкранными режимами. Для поведения, похожего на NSOpenPanel, нужно:

swift
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]

Согласно исследованиям, .canJoinAllSpaces позволяет панели появляться во всех рабочих пространствах с автоматическим порядком, а .fullScreenAuxiliary позволяет ей плавать над полноэкранными приложениями.

Уровень окна

Свойство level управляет иерархией стека. Для панелей уровня приложения используйте:

swift
panel.level = .mainMenu

Как отмечено в статье James Fisher о уровнях NSWindow, «если у одного окна выше уровень, чем у другого, оно всегда отображается поверх».

hidesOnDeactivate

В отличие от стандартного поведения NSPanel, которое «скрывается, когда приложение становится неактивным» (см. Cocoa in a Nutshell), для вашего случая вы захотите:

swift
panel.hidesOnDeactivate = false

Шаги реализации

Чтобы создать пользовательский NSPanel с поведением стека, похожим на NSOpenPanel, выполните следующие шаги:

  1. Создайте NSPanel с подходящим styleMask
  2. Установите collectionBehavior
  3. Настройте уровень окна
  4. Установите hidesOnDeactivate в false
  5. Разместите и покажите панель

Полный пример кода

Ниже приведена полная реализация, которая достигает поведения стека, похожего на NSOpenPanel:

swift
import Cocoa

class CustomPanel: NSPanel {
    override var canBecomeKey: Bool { return true }
    override var canBecomeMain: Bool { return false }
    
    static func createCustomPanel() -> CustomPanel {
        let panel = CustomPanel(
            contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
            styleMask: [.titled, .closable, .nonactivatingPanel],
            backing: .buffered,
            defer: false
        )
        
        // Настройка поведения панели
        panel.level = .mainMenu
        panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
        panel.hidesOnDeactivate = false
        panel.isFloatingPanel = true
        
        // Предотвращаем становление панелью ключевого окна
        panel.becomesKeyOnlyIfNeeded = true
        
        return panel
    }
    
    override func orderFront(_ sender: Any?) {
        super.orderFront(sender)
    }
}

// Использование
let customPanel = CustomPanel.createCustomPanel()
customPanel.title = "Custom Panel"
customPanel.center()
customPanel.makeKeyAndOrderFront(nil)

Ключевые детали реализации:

  • Флаг .nonactivatingPanel в styleMask критичен для неблокирующего поведения
  • becomesKeyOnlyIfNeeded = true предотвращает захват фокуса
  • isFloatingPanel = true способствует плавающему поведению
  • canBecomeMain = false гарантирует, что панель не станет главным окном

Как упомянуто в ответе Stack Overflow, такой подход помогает «устойчиво удерживать фокус» при сохранении интерактивности.


Альтернативные подходы

NSWindow с пользовательским уровнем

Если вам нужен более тонкий контроль, можно использовать NSWindow с конкретными уровнями:

swift
let window = NSWindow(
    contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
    styleMask: [.titled, .closable],
    backing: .buffered,
    defer: false
)

window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.mainMenuWindow)))
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]

Однако это не даст того же поведения, что и .nonactivatingPanel.

HUD‑стиль панели

Для HUD‑стиля панелей, которые вообще не захватывают фокус:

swift
panel.styleMask = [.hudWindow, .nonactivatingPanel]
panel.level = .floating

Ограничения и ограничения

Nonactivating доступен только для NSPanel

Как отмечено в GitHub issue, «NSWindow не поддерживает стиль .nonactivatingPanel». Поэтому для этого поведения необходимо использовать NSPanel.

Поведение в полноэкранном режиме

Получить корректную работу панелей в полноэкранных приложениях может быть сложно. Как упомянуто в исследовании, для оптимальных результатов нужны оба поведения: .canJoinAllSpaces и .fullScreenAuxiliary.

Безопасность

Согласно документации Apple, системные панели обладают специальными привилегиями и поведением, которые пользовательские панели не могут полностью воспроизвести. Некоторые поведения могут быть действительно зарезервированы только для встроенных панелей Apple.


Заключение

Создание пользовательского NSPanel с поведением стека, похожим на NSOpenPanel, возможно в AppKit, но требует тщательной настройки нескольких свойств окна. Ключевые выводы:

  1. Используйте NSPanel с .nonactivatingPanel – это самый критичный компонент для неблокирующего поведения
  2. Правильно настройте collectionBehavior – сочетание .canJoinAllSpaces и .fullScreenAuxiliary обеспечивает пространственную осведомленность
  3. Установите подходящий уровень окна – уровень .mainMenu хорошо подходит для панелей уровня приложения
  4. Контролируйте фокус – используйте becomesKeyOnlyIfNeeded = true и canBecomeMain = false
  5. Отключите авто‑скрытиеhidesOnDeactivate = false сохраняет панель видимой, когда приложение не активно

Хотя вы можете достичь большинства особенностей NSOpenPanel с пользовательскими панелями, некоторые системные поведения могут оставаться эксклюзивными для встроенных панелей Apple. Предложенный подход обеспечивает максимально близкую имитацию для пользовательских приложений.


Источники

  1. macOS – Allow an NSWindow (NSPanel) to float above full screen apps – Stack Overflow
  2. NSPanel | Apple Developer Documentation
  3. What is the order of NSWindow levels? – James Fisher
  4. NSPanel — Mac OS X 10.0 – Cocoa in a Nutshell
  5. macOS – Is there a way to get NSPanel “Non activating” style functionality on an NSWindow? – Stack Overflow
  6. macOS – NSPanel not hiding when focus is lost – Stack Overflow
  7. macOS – How to get NSPanel to resist key focus (with becomesKeyOnlyIfNeeded?) – Stack Overflow
  8. macOS – NSWindow at desktop level in Mission Control – Stack Overflow
Авторы
Проверено модерацией
Модерация