简体   繁体   中英

NSWindow: change positions of window buttons

I want to fake a titlebar (bigger and with a different color), so my way until now is the following:

I added a NSView directly below the titlebar and then I set the titlebar to transparent with this code:

self.window.titlebarAppearsTransparent = true
self.window.styleMask |= NSFullSizeContentViewWindowMask    

The next step is, that I subclassed the NSView to add some drawing methods (background etc.) and especially the code, so that I can use the complete NSView for moving the window (therefore I use this code: https://stackoverflow.com/a/4564630/2062613 )

This is the result:

窗纱

Now the next thing I want to do is to vertically center the traffic light buttons in this new titlebar. I know, that I can access the buttons with self.window.standardWindowButton(NSWindowButton.CloseButton) (for example). But changing the frame.origin of one of the button doesn't have any effect.

How can I change the origin.y value of the buttons?

UPDATE

I discovered, that the window resizing re-arranges the buttons. Now I decided to add the buttons as subviews to my fake titlebar, because moving the origin in the titlebar cuts off the buttons (it's obviously limited to the titlebar rect).

This works, but strangely the mouseover effect of the buttons still remains in the titlebar. Look at this screen:

第二屏

This is actually my code:

func moveButtons() {
    self.moveButtonDownFirst(self.window.standardWindowButton(NSWindowButton.CloseButton)!)
    self.moveButtonDownFirst(self.window.standardWindowButton(NSWindowButton.MiniaturizeButton)!)
    self.moveButtonDownFirst(self.window.standardWindowButton(NSWindowButton.ZoomButton)!)
}

func moveButtonDownFirst(button: NSView) {
    button.setFrameOrigin(NSMakePoint(button.frame.origin.x, button.frame.origin.y+10.0))
    self.fakeTitleBar.addSubview(button)
}

You need to add toolbar and change window property titleVisibility . Here more details NSWindow Style Showcase .

let customToolbar = NSToolbar()
window?.titleVisibility = .hidden
window?.toolbar = customToolbar

在此处输入图片说明

Swift 4.2 version (without Toolbar).

Idea behind:

  • We adjusting frames of standard window buttons without changing superview.
  • To prevent clipping we need increase height of title bar. This can be achieved by adding transparent title bar accessory .
  • When window goes to full screen we hiding title bar accessory.
  • When window goes out of full screen we showing title bar accessory.
  • Additionally we need to adjust layout of UI elements shown alongside standard buttons in full screen mode.

Normal screen.

在此处输入图片说明

Full screen mode.

在此处输入图片说明

Real application

在此处输入图片说明


File FullContentWindow.swift

public class FullContentWindow: Window {

   private var buttons: [NSButton] = []

   public let titleBarAccessoryViewController = TitlebarAccessoryViewController()
   private lazy var titleBarHeight = calculatedTitleBarHeight
   private let titleBarLeadingOffset: CGFloat?
   private var originalLeadingOffsets: [CGFloat] = []

   public init(contentRect: NSRect, titleBarHeight: CGFloat, titleBarLeadingOffset: CGFloat? = nil) {
      self.titleBarLeadingOffset = titleBarLeadingOffset
      let styleMask: NSWindow.StyleMask = [.closable, .titled, .miniaturizable, .resizable, .fullSizeContentView]
      super.init(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: true)
      titleVisibility = .hidden
      titlebarAppearsTransparent = true
      buttons = [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton].compactMap {
         standardWindowButton($0)
      }
      var accessoryViewHeight = titleBarHeight - calculatedTitleBarHeight
      accessoryViewHeight = max(0, accessoryViewHeight)
      titleBarAccessoryViewController.view.frame = CGRect(dimension: accessoryViewHeight) // Width not used.
      if accessoryViewHeight > 0 {
         addTitlebarAccessoryViewController(titleBarAccessoryViewController)
      }
      self.titleBarHeight = max(titleBarHeight, calculatedTitleBarHeight)
   }

   public override func layoutIfNeeded() {
      super.layoutIfNeeded()
      if originalLeadingOffsets.isEmpty {
         let firstButtonOffset = buttons.first?.frame.origin.x ?? 0
         originalLeadingOffsets = buttons.map { $0.frame.origin.x - firstButtonOffset }
      }
      if titleBarAccessoryViewController.view.frame.height > 0, !titleBarAccessoryViewController.isHidden {
         setupButtons()
      }
   }

}

extension FullContentWindow {

   public var standardWindowButtonsRect: CGRect {
      var result = CGRect()
      if let firstButton = buttons.first, let lastButton = buttons.last {
         let leadingOffset = firstButton.frame.origin.x
         let maxX = lastButton.frame.maxX
         result = CGRect(x: leadingOffset, y: 0, width: maxX - leadingOffset, height: titleBarHeight)
         if let titleBarLeadingOffset = titleBarLeadingOffset {
            result = result.offsetBy(dx: titleBarLeadingOffset - leadingOffset, dy: 0)
         }
      }
      return result
   }

}

extension FullContentWindow {

   private func setupButtons() {
      let barHeight = calculatedTitleBarHeight
      for (idx, button) in buttons.enumerated() {
         let coordY = (barHeight - button.frame.size.height) * 0.5
         var coordX = button.frame.origin.x
         if let titleBarLeadingOffset = titleBarLeadingOffset {
            coordX = titleBarLeadingOffset + originalLeadingOffsets[idx]
         }
         button.setFrameOrigin(CGPoint(x: coordX, y: coordY))
      }
   }

   private var calculatedTitleBarHeight: CGFloat {
      let result = contentRect(forFrameRect: frame).height - contentLayoutRect.height
      return result
   }
}

File FullContentWindowController.swift

open class FullContentWindowController: WindowController {

   private let fullContentWindow: FullContentWindow
   private let fullContentViewController = ViewController()

   public private (set) lazy var titleBarContentContainer = View().autolayoutView()
   public private (set) lazy var contentContainer = View().autolayoutView()

   private lazy var titleOffsetConstraint =
      titleBarContentContainer.leadingAnchor.constraint(equalTo: fullContentViewController.contentView.leadingAnchor)

   public init(contentRect: CGRect, titleBarHeight: CGFloat, titleBarLeadingOffset: CGFloat? = nil) {
      fullContentWindow = FullContentWindow(contentRect: contentRect, titleBarHeight: titleBarHeight,
                                            titleBarLeadingOffset: titleBarLeadingOffset)
      super.init(window: fullContentWindow, viewController: fullContentViewController)
      contentWindow.delegate = self
      fullContentViewController.contentView.addSubviews(titleBarContentContainer, contentContainer)

      let standardWindowButtonsRect = fullContentWindow.standardWindowButtonsRect

      LayoutConstraint.withFormat("V:|[*][*]|", titleBarContentContainer, contentContainer).activate()
      LayoutConstraint.pin(to: .horizontally, contentContainer).activate()
      LayoutConstraint.constrainHeight(constant: standardWindowButtonsRect.height, titleBarContentContainer).activate()
      LayoutConstraint.withFormat("[*]|", titleBarContentContainer).activate()
      titleOffsetConstraint.activate()

      titleOffsetConstraint.constant = standardWindowButtonsRect.maxX
   }

   open override func prepareForInterfaceBuilder() {
      titleBarContentContainer.backgroundColor = .green
      contentContainer.backgroundColor = .yellow
      fullContentViewController.contentView.backgroundColor = .blue
      fullContentWindow.titleBarAccessoryViewController.contentView.backgroundColor = Color.red.withAlphaComponent(0.4)
   }

   public required init?(coder: NSCoder) {
      fatalError()
   }
}

extension FullContentWindowController {

   public func embedTitleBarContent(_ viewController: NSViewController) {
      fullContentViewController.embedChildViewController(viewController, container: titleBarContentContainer)
   }

   public func embedContent(_ viewController: NSViewController) {
      fullContentViewController.embedChildViewController(viewController, container: contentContainer)
   }
}

extension FullContentWindowController: NSWindowDelegate {

   public func windowWillEnterFullScreen(_ notification: Notification) {
      fullContentWindow.titleBarAccessoryViewController.isHidden = true
      titleOffsetConstraint.constant = 0
   }

   public  func windowWillExitFullScreen(_ notification: Notification) {
      fullContentWindow.titleBarAccessoryViewController.isHidden = false
      titleOffsetConstraint.constant = fullContentWindow.standardWindowButtonsRect.maxX
   }
}

Usage

let windowController = FullContentWindowController(contentRect: CGRect(...),
                                                   titleBarHeight: 30,
                                                   titleBarLeadingOffset: 7)
windowController.embedContent(viewController) // Content "Yellow area"
windowController.embedTitleBarContent(titleBarController) // Titlebar "Green area"
windowController.showWindow(nil)

My answer involves a bit of the answers from @Vlad and @Lupurus. To change the buttons position a simple call to a function func moveButton(ofType type: NSWindow.ButtonType) in the NSWindow subclass handles the moving.

Note : in my case I just need the buttons to be lower a bit by 2px.

To handle the normal case (not fullscreen) I have just overridden the function func standardWindowButton(_ b: NSWindow.ButtonType) -> NSButton? of NSWindow to move the buttons as needed before they are returned.

Note : better code would have a separate method to compute the new frame and storing the new value would be stored somewhere else

To handle the animation properly when coming back from fullscreen we need to override the func layoutIfNeeded() method of NSWindow, this method will be called when needed by the animation returning from fullscreen.

We need to keep the updated frames in NSWindow. A nil value will trigger frames recomputations.

You need to keep the updated frame in the NSWindow window:

    var closeButtonUpdatedFrame: NSRect?
    var miniaturizeButtonUpdatedFrame: NSRect?
    var zoomButtonUpdatedFrame: NSRect?

    public override func layoutIfNeeded() {
        super.layoutIfNeeded()

        if closeButtonUpdatedFrame == nil {
            moveButton(ofType: .closeButton)
        }

        if miniaturizeButtonUpdatedFrame == nil {
            moveButton(ofType: .miniaturizeButton)
        }

        if zoomButtonUpdatedFrame == nil {
            moveButton(ofType: .zoomButton)
        }
    }

    override public func standardWindowButton(_ b: NSWindow.ButtonType) -> NSButton? {

        switch b {
        case .closeButton:
            if closeButtonUpdatedFrame == nil {
                moveButton(ofType: b)
            }
        case .miniaturizeButton:
            if miniaturizeButtonUpdatedFrame == nil {
                moveButton(ofType: b)
            }
        case .zoomButton:
            if zoomButtonUpdatedFrame == nil {
                moveButton(ofType: b)
            }
        default:
            break
        }
        return super.standardWindowButton(b)
    }

    func moveButton(ofType type: NSWindow.ButtonType) {

        guard let button = super.standardWindowButton(type) else {
            return
        }

        switch type {
        case .closeButton:
            self.moveButtonDown(button: button)
            closeButtonUpdatedFrame = button.frame
        case .miniaturizeButton:
            self.moveButtonDown(button: button)
            miniaturizeButtonUpdatedFrame = button.frame
        case .zoomButton:
            self.moveButtonDown(button: button)
            zoomButtonUpdatedFrame = button.frame
        default:
            break
        }
    }

    func moveButtonDown(button: NSView) {

        button.setFrameOrigin(NSMakePoint(button.frame.origin.x, button.frame.origin.y-2.0))
    }

To handle the full screen case, we need to put some code in the NSWindowDelegate, in my case this delegate is the NSWindowController instance. This code will force the func layoutIfNeeded() method to recompute the buttons frames when coming from fullscreen:


    public func windowWillExitFullScreen(_ notification: Notification) {

        self.window.closeButtonUpdatedFrame = nil
        self.window.miniaturizeButtonUpdatedFrame = nil
        self.window.zoomButtonUpdatedFrame = nil
    }

Et voilà!

In my testing, this code handles all cases.

To make the toolbar bigger and the window controls go a bit down like in Apple Mail or Notes app, just set the window title to visible and use an empty window title string:

self.window.titleVisibility = .visible
self.window.title = ""

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM