Skip to content

Commit 8fbfa4c

Browse files
authored
Merge pull request #355 from kdroidFilter/fix/external-screen-tray-position
Fix getTrayWindowPosition on external screens (macOS)
2 parents ee42b17 + 591c646 commit 8fbfa4c

2 files changed

Lines changed: 39 additions & 25 deletions

File tree

maclib/tray.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,10 @@ public func tray_get_status_item_position(
348348
x?.pointee = Int32(lround(rect.midX))
349349

350350
// -- Y ---------------------------------------------------------------
351-
// Inverted coordinate system to match Windows/Linux (origin at top)
352-
let flippedY = Int32(screen.frame.maxY - rect.maxY)
351+
// Convert macOS bottom-origin to AWT top-origin using primary screen height.
352+
// This produces correct global coordinates for all screens (including external).
353+
let primaryHeight = NSScreen.screens.first?.frame.height ?? screen.frame.height
354+
let flippedY = Int32(primaryHeight - rect.maxY)
353355
y?.pointee = flippedY
354356

355357
return 1 // precise coordinates
@@ -398,7 +400,9 @@ public func tray_get_status_item_position_for(
398400
rect = window.convertToScreen(rect)
399401

400402
x?.pointee = Int32(lround(rect.midX))
401-
let flippedY = Int32(screen.frame.maxY - rect.maxY)
403+
// Convert macOS bottom-origin to AWT top-origin using primary screen height.
404+
let primaryHeight = NSScreen.screens.first?.frame.height ?? screen.frame.height
405+
let flippedY = Int32(primaryHeight - rect.maxY)
402406
y?.pointee = flippedY
403407
return 1
404408
}

src/commonMain/kotlin/com/kdroid/composetray/utils/TrayPosition.kt

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import io.github.kdroidfilter.platformtools.LinuxDesktopEnvironment
1010
import io.github.kdroidfilter.platformtools.OperatingSystem
1111
import io.github.kdroidfilter.platformtools.detectLinuxDesktopEnvironment
1212
import io.github.kdroidfilter.platformtools.getOperatingSystem
13+
import java.awt.GraphicsEnvironment
14+
import java.awt.Rectangle
1315
import java.awt.Toolkit
1416
import java.io.File
1517
import java.util.*
@@ -30,16 +32,16 @@ internal object TrayClickTracker {
3032
Collections.synchronizedMap(mutableMapOf())
3133

3234
fun updateClickPosition(x: Int, y: Int) {
33-
val screenSize = getLogicalScreenSize()
34-
val position = convertPositionToCorner(x, y, screenSize.width, screenSize.height)
35+
val bounds = getScreenBoundsAt(x, y)
36+
val position = convertPositionToCorner(x - bounds.x, y - bounds.y, bounds.width, bounds.height)
3537
val pos = TrayClickPosition(x, y, position)
3638
lastClickPosition.set(pos)
3739
runCatching { saveTrayClickPosition(x, y, position) }
3840
}
3941

4042
fun updateClickPosition(instanceId: String, x: Int, y: Int) {
41-
val screenSize = getLogicalScreenSize()
42-
val position = convertPositionToCorner(x, y, screenSize.width, screenSize.height)
43+
val bounds = getScreenBoundsAt(x, y)
44+
val position = convertPositionToCorner(x - bounds.x, y - bounds.y, bounds.width, bounds.height)
4345
val pos = TrayClickPosition(x, y, position)
4446
perInstancePositions[instanceId] = pos
4547
lastClickPosition.set(pos)
@@ -71,6 +73,20 @@ private fun getLogicalScreenSize(): java.awt.Dimension {
7173
return Toolkit.getDefaultToolkit().screenSize
7274
}
7375

76+
/**
77+
* Returns the bounds of the screen that contains the given point.
78+
* Falls back to the primary screen if the point is not on any screen.
79+
*/
80+
private fun getScreenBoundsAt(x: Int, y: Int): Rectangle {
81+
val ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
82+
for (gd in ge.screenDevices) {
83+
val bounds = gd.defaultConfiguration.bounds
84+
if (bounds.contains(x, y)) return bounds
85+
}
86+
val primary = Toolkit.getDefaultToolkit().screenSize
87+
return Rectangle(0, 0, primary.width, primary.height)
88+
}
89+
7490
internal fun convertPositionToCorner(x: Int, y: Int, width: Int, height: Int): TrayPosition {
7591
// Use smarter margins based on typical taskbar/panel size
7692
// 100px from edge = probably within taskbar/panel area
@@ -247,15 +263,13 @@ fun getTrayWindowPosition(
247263
return calculateWindowPositionFromClick(
248264
sx, sy, corner,
249265
windowWidth, windowHeight,
250-
screenSize.width, screenSize.height,
251266
horizontalOffset, verticalOffset
252267
)
253268
}
254269

255270
return calculateWindowPositionFromClick(
256271
posToUse.x, posToUse.y, posToUse.position,
257272
windowWidth, windowHeight,
258-
screenSize.width, screenSize.height,
259273
horizontalOffset, verticalOffset
260274
)
261275
}
@@ -270,7 +284,6 @@ fun getTrayWindowPosition(
270284
return calculateWindowPositionFromClick(
271285
pos.x, pos.y, pos.position,
272286
windowWidth, windowHeight,
273-
screenSize.width, screenSize.height,
274287
horizontalOffset, verticalOffset
275288
)
276289
}
@@ -282,7 +295,6 @@ fun getTrayWindowPosition(
282295
return calculateWindowPositionFromClick(
283296
clickPos.x, clickPos.y, clickPos.position,
284297
windowWidth, windowHeight,
285-
screenSize.width, screenSize.height,
286298
horizontalOffset, verticalOffset
287299
)
288300
}
@@ -314,7 +326,6 @@ fun getTrayWindowPositionForInstance(
314326
verticalOffset: Int = 0
315327
): WindowPosition {
316328
val os = getOperatingSystem()
317-
val screenSize = Toolkit.getDefaultToolkit().screenSize
318329

319330
return when (os) {
320331
OperatingSystem.WINDOWS -> {
@@ -323,7 +334,6 @@ fun getTrayWindowPositionForInstance(
323334
calculateWindowPositionFromClick(
324335
pos.x, pos.y, pos.position,
325336
windowWidth, windowHeight,
326-
screenSize.width, screenSize.height,
327337
horizontalOffset, verticalOffset
328338
)
329339
}
@@ -341,12 +351,14 @@ fun getTrayWindowPositionForInstance(
341351
if (precise) {
342352
val regionStr = runCatching { lib.tray_get_status_item_region_for(trayStruct) }.getOrNull()
343353
val trayPos = if (regionStr != null) getMacTrayPosition(regionStr)
344-
else convertPositionToCorner(x, y, screenSize.width, screenSize.height)
354+
else {
355+
val bounds = getScreenBoundsAt(x, y)
356+
convertPositionToCorner(x - bounds.x, y - bounds.y, bounds.width, bounds.height)
357+
}
345358
TrayClickTracker.setClickPosition(instanceId, x, y, trayPos)
346359
return calculateWindowPositionFromClick(
347360
x, y, trayPos,
348361
windowWidth, windowHeight,
349-
screenSize.width, screenSize.height,
350362
horizontalOffset, verticalOffset
351363
)
352364
}
@@ -360,44 +372,42 @@ fun getTrayWindowPositionForInstance(
360372

361373
/**
362374
* Calcule la position (x,y) depuis un clic précis + applique les offsets et un clamp aux bords écran.
375+
* Uses the screen containing the click point for correct multi-monitor support.
363376
*/
364377
private fun calculateWindowPositionFromClick(
365378
clickX: Int, clickY: Int, trayPosition: TrayPosition,
366379
windowWidth: Int, windowHeight: Int,
367-
screenWidth: Int, screenHeight: Int,
368380
horizontalOffset: Int,
369381
verticalOffset: Int
370382
): WindowPosition {
371383
val os = getOperatingSystem()
372384
val isTop = trayPosition == TrayPosition.TOP_LEFT || trayPosition == TrayPosition.TOP_RIGHT
373385
val isRight = trayPosition == TrayPosition.TOP_RIGHT || trayPosition == TrayPosition.BOTTOM_RIGHT
374386

387+
val sb = getScreenBoundsAt(clickX, clickY)
388+
375389
return if (os == OperatingSystem.WINDOWS) {
376-
// ---- Legacy behavior for Windows (keep exact clickY & raw offsets) ----
377390
var x = clickX - (windowWidth / 2)
378391
var y = if (isTop) clickY else clickY - windowHeight
379392

380393
x += horizontalOffset
381394
y += verticalOffset
382395

383-
if (x < 0) x = 0 else if (x + windowWidth > screenWidth) x = screenWidth - windowWidth
384-
if (y < 0) y = 0 else if (y + windowHeight > screenHeight) y = screenHeight - windowHeight
396+
if (x < sb.x) x = sb.x else if (x + windowWidth > sb.x + sb.width) x = sb.x + sb.width - windowWidth
397+
if (y < sb.y) y = sb.y else if (y + windowHeight > sb.y + sb.height) y = sb.y + sb.height - windowHeight
385398
WindowPosition(x = x.dp, y = y.dp)
386399
} else {
387-
// ---- New behavior for macOS & Linux: snap to bar edge (ignore clickY height within icon) ----
388-
// Conservative guess of panel thickness; works well across GNOME/KDE and typical macOS menubar heights.
389400
val panelGuessPx = 28
390401

391402
var x = clickX - (windowWidth / 2)
392-
val anchorY = if (isTop) panelGuessPx else (screenHeight - panelGuessPx)
403+
val anchorY = if (isTop) sb.y + panelGuessPx else (sb.y + sb.height - panelGuessPx)
393404
var y = if (isTop) anchorY else anchorY - windowHeight
394405

395-
// Direction-aware offsets: always push AWAY from the bar/edge for consistency.
396406
x += if (isRight) -horizontalOffset else horizontalOffset
397407
y += if (isTop) verticalOffset else -verticalOffset
398408

399-
if (x < 0) x = 0 else if (x + windowWidth > screenWidth) x = screenWidth - windowWidth
400-
if (y < 0) y = 0 else if (y + windowHeight > screenHeight) y = screenHeight - windowHeight
409+
if (x < sb.x) x = sb.x else if (x + windowWidth > sb.x + sb.width) x = sb.x + sb.width - windowWidth
410+
if (y < sb.y) y = sb.y else if (y + windowHeight > sb.y + sb.height) y = sb.y + sb.height - windowHeight
401411
WindowPosition(x = x.dp, y = y.dp)
402412
}
403413
}

0 commit comments

Comments
 (0)