bmichotte bmichotte - 5 months ago 40
Swift Question

NSWindow positioning

I'm the author of a Hearthstone tracker, and I have to move several

NSWindow
over Hearthstone window.

I get the frame of Hearthstone using
CGWindowListCopyWindowInfo
.
Then, I have to move my windows at some positions relative to Hearthstone.

HSTracker overlays
The red arrows are over opponent cards, green arrow is over turn button and blue arrows are at the left and right of the window.

My actual screen setup is the following :

Screen setup

which gives me the following frames

// screen 1 : {x 0 y 0 w 1.440 h 900}
// screen 2 : {x 1.440 y -180 w 1.920 h 1.080}


To place the opponent tracker (the left frame) at the right position, which is the most simple case, I use
{x 0 y somepadding w 185 h hearthstoneHeight - somepadding}
and get the correct frame with this

func relativeFrame(frame: NSRect) -> NSRect {
var relative = frame
relative.origin.x = NSMinX(hearthstoneFrame) + NSMinX(frame)
relative.origin.y = NSMinY(hearthstoneFrame) + NSMinY(frame)

return relative
}


The right tracker is placed using
{x hearthstoneWidth - trackerWidth, ...}


For other overlays, I used my current (Hearthstone) resolution to place them and them calculate them using a simple math

x = x / 1404.0 * NSWidth(hearthstoneFrame)
y = y / 840.0 * NSHeight(hearthstoneFrame)


This works pretty well. Except if I use my second screen. In this case, the frames seems to be correct, but the position of the window is not good.

Here is a screenshot of a debug window with
{x 0 y 0 w hearthstoneWidth h hearthsoneHeight }
. If I compare the frames of Hearthstone and my overlay, they are identical.
Debug overlay

The complete function is the following (I'm in a "static class", I only show revelant code). I guess I'm missing something in the calculation but I can't find what.

class frameRelative {
static var hearthstoneFrame = NSZeroRect
static func findHearthstoneFrame() {
let options = CGWindowListOption(arrayLiteral: .ExcludeDesktopElements)
let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
if let info = (windowListInfo as NSArray? as? [[String: AnyObject]])?
.filter({
!$0.filter({ $0.0 == "kCGWindowName" && $0.1 as? String == "Hearthstone" }).isEmpty
})
.first {
var rect = NSRect()
let bounds = info["kCGWindowBounds"] as! CFDictionary
CGRectMakeWithDictionaryRepresentation(bounds, &rect)
rect.size.height -= titleBarHeight // remove the 22px from the title
hearthstoneFrame = rect
}
}

static func frameRelative(frame: NSRect, _ isRelative: Bool = false) -> NSRect {
var relative = frame
var pointX = NSMinX(relative)
var pointY = NSMinY(relative)

if isRelative {
pointX = pointX / 1404.0 * NSWidth(hearthstoneFrame)
pointY = pointY / 840.0 * NSHeight(hearthstoneFrame)
}

let x: CGFloat = NSMinX(hearthstoneFrame) + pointX
let y = NSMinY(hearthstoneFrame) + pointY

relative.origin = NSMakePoint(x, y)
return relative
}
}

// somewhere here
let frame = NSMakeRect(0, 0, hearthstoneWidth, hearthstoneHeight)
let relativeFrame = SizeHelper.frameRelative(frame)
myWindow.setFrame(relativeFrame, display: true)


Any help will be appreciate :)

Answer

I eventually solved this issue so I decided to post the answer to close this thread... and maybe if someone face the same issue one day.

The solution was to substract the max y from the first screen with the max y of the Hearthstone window.

The final code of findHearthstoneFrame is

static func findHearthstoneFrame() {
   let options = CGWindowListOption(arrayLiteral: .ExcludeDesktopElements)
        let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
        if let info = (windowListInfo as NSArray? as? [[String: AnyObject]])?.filter({
            !$0.filter({ $0.0 == "kCGWindowName"
                && $0.1 as? String == "Hearthstone" }).isEmpty
        }).first {
            if let id = info["kCGWindowNumber"] as? Int {
                self.windowId = CGWindowID(id)
            }
            var rect = NSRect()
            let bounds = info["kCGWindowBounds"] as! CFDictionary
            CGRectMakeWithDictionaryRepresentation(bounds, &rect)

            if let screen = NSScreen.screens()?.first {
                rect.origin.y = NSMaxY(screen.frame) - NSMaxY(rect)
            }

            self._frame = rect
        }
 }

And the frameRelative is

static let BaseWidth: CGFloat = 1440.0
static let BaseHeight: CGFloat = 922.0
var scaleX: CGFloat {
    return NSWidth(_frame) / SizeHelper.BaseWidth
}

var scaleY: CGFloat {
    // 22 is the height of the title bar
    return (NSHeight(_frame) - 22) / SizeHelper.BaseHeight
}
func frameRelative(frame: NSRect, relative: Bool = true) -> NSRect {
    var pointX = NSMinX(frame)
    var pointY = NSMinY(frame)
    let width = NSWidth(frame)
    let height = NSHeight(frame)

    if relative {
        pointX = pointX * scaleX
        pointY = pointY * scaleY
    }

    let x: CGFloat = NSMinX(self.frame) + pointX
    let y: CGFloat = NSMinY(self.frame) + pointY

    let relativeFrame = NSRect(x: x, y: y, width: width, height: height)
    return relativeFrame
}
Comments