Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

WindowScreen

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:

Second 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)
}
like image 923
Lupurus Avatar asked May 06 '15 19:05

Lupurus


2 Answers

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

enter image description here

like image 102
Nikolai Nagornyi Avatar answered Sep 30 '22 06:09

Nikolai Nagornyi


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.

enter image description here

Full screen mode.

enter image description here

Real application

enter image description here


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)
like image 26
Vlad Avatar answered Sep 30 '22 05:09

Vlad