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:
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.