[object Object]

← back to Ticket System

Fix desktop ticket bar click and popup visibility

071635225db531dd09b6c139bbcf564e8d0def5f · 2026-09-02 16:05:24 -0700 · Steve Abrams

Files touched

Diff

commit 071635225db531dd09b6c139bbcf564e8d0def5f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 16:05:24 2026 -0700

    Fix desktop ticket bar click and popup visibility
---
 desktop-bar/TicketBar.swift | 79 +++++++++++++++++++++++++++++++++++----------
 1 file changed, 62 insertions(+), 17 deletions(-)

diff --git a/desktop-bar/TicketBar.swift b/desktop-bar/TicketBar.swift
index 2a637307..f64bac03 100644
--- a/desktop-bar/TicketBar.swift
+++ b/desktop-bar/TicketBar.swift
@@ -18,7 +18,6 @@ private final class BarTile: NSButton {
   var menuProvider: (() -> NSMenu)?
   var clickHandler: (() -> Void)?
   var dragHandler: ((String, CGFloat) -> Void)?
-  private var dragged = false
   var widthConstraint: NSLayoutConstraint!
 
   init(key: String, label: NSTextField, width: CGFloat) {
@@ -48,20 +47,8 @@ private final class BarTile: NSButton {
 
   required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
   override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
-  override func mouseDown(with event: NSEvent) {
-    dragged = false
-    let start = event.locationInWindow
-    while let next = window?.nextEvent(matching: [.leftMouseDragged, .leftMouseUp]) {
-      if next.type == .leftMouseUp { break }
-      if abs(next.locationInWindow.x - start.x) > 3 {
-        dragged = true
-        dragHandler?(key, next.locationInWindow.x)
-      }
-    }
-    if !dragged { showDropdown() }
-  }
   @objc private func showDropdown() {
-    guard !dragged else { return }
+    NSLog("TicketBar tile pressed: %@", key)
     if let clickHandler { clickHandler(); return }
     guard let menu = menuProvider?() else { return }
     NSApp.activate(ignoringOtherApps: true)
@@ -75,7 +62,7 @@ private final class BarTile: NSButton {
   }
 }
 
-private final class BarController: NSObject, NSApplicationDelegate, WKNavigationDelegate {
+private final class BarController: NSObject, NSApplicationDelegate, NSWindowDelegate, WKNavigationDelegate {
   private var panel: NSPanel!
   private let counts = NSTextField(labelWithString: "Tickets · loading…")
   private let working = NSTextField(labelWithString: "Working · —")
@@ -86,6 +73,9 @@ private final class BarController: NSObject, NSApplicationDelegate, WKNavigation
   private var latestDetail = "No activity yet"
   private var ticketPopup: NSWindow?
   private var timer: Timer?
+  private var dragMonitor: Any?
+  private var draggingKey: String?
+  private var dragStartX: CGFloat?
 
   func applicationDidFinishLaunching(_ notification: Notification) {
     NSApp.setActivationPolicy(.accessory)
@@ -138,10 +128,35 @@ private final class BarController: NSObject, NSApplicationDelegate, WKNavigation
     for tile in tiles { tile.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) }
     open.setContentHuggingPriority(.required, for: .horizontal)
     panel.contentView = stack
+    installDragMonitor()
     reposition()
     panel.orderFrontRegardless()
   }
 
+  private func installDragMonitor() {
+    guard dragMonitor == nil else { return }
+    dragMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp]) { [weak self] event in
+      guard let self, event.window === self.panel else { return event }
+      switch event.type {
+      case .leftMouseDown:
+        self.draggingKey = self.tiles.first(where: {
+          $0.bounds.contains($0.convert(event.locationInWindow, from: nil))
+        })?.key
+        self.dragStartX = event.locationInWindow.x
+      case .leftMouseDragged:
+        if let key = self.draggingKey, let start = self.dragStartX,
+           abs(event.locationInWindow.x - start) > 3 {
+          self.moveTile(key, toward: event.locationInWindow.x)
+        }
+      case .leftMouseUp:
+        self.draggingKey = nil
+        self.dragStartX = nil
+      default: break
+      }
+      return event
+    }
+  }
+
   private func moveTile(_ key: String, toward windowX: CGFloat) {
     guard let source = tiles.firstIndex(where: { $0.key == key }) else { return }
     var target = source
@@ -221,7 +236,14 @@ private final class BarController: NSObject, NSApplicationDelegate, WKNavigation
   @objc private func openViewer() { NSWorkspace.shared.open(viewerURL) }
 
   private func openTicketPopup() {
-    if let ticketPopup { ticketPopup.makeKeyAndOrderFront(nil); NSApp.activate(ignoringOtherApps: true); return }
+    NSLog("TicketBar opening control window")
+    if let ticketPopup {
+      centerOnPointerScreen(ticketPopup)
+      ticketPopup.makeKeyAndOrderFront(nil)
+      ticketPopup.orderFrontRegardless()
+      NSApp.activate(ignoringOtherApps: true)
+      return
+    }
     let size = NSSize(width: 1480, height: 860)
     let popup = NSWindow(contentRect: NSRect(origin: .zero, size: size), styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false)
     popup.title = "Fleet Ticket Control · All / Open / Blocked / Doing"
@@ -230,6 +252,7 @@ private final class BarController: NSObject, NSApplicationDelegate, WKNavigation
     popup.minSize = NSSize(width: 900, height: 560)
     popup.isReleasedWhenClosed = false
     popup.hidesOnDeactivate = false
+    popup.delegate = self
     let web = WKWebView(frame: popup.contentView?.bounds ?? .zero)
     web.navigationDelegate = self
     web.autoresizingMask = [.width, .height]
@@ -238,13 +261,34 @@ private final class BarController: NSObject, NSApplicationDelegate, WKNavigation
     // a one-off Authorization header would authenticate the HTML navigation but
     // leave the board's same-origin fetch() calls unauthenticated and blank.
     web.load(URLRequest(url: viewerURL))
-    popup.center()
+    centerOnPointerScreen(popup)
     ticketPopup = popup
     popup.makeKeyAndOrderFront(nil)
     popup.orderFrontRegardless()
     NSApp.activate(ignoringOtherApps: true)
   }
 
+  func windowWillClose(_ notification: Notification) {
+    guard let closing = notification.object as? NSWindow, closing === ticketPopup else { return }
+    ticketPopup = nil
+  }
+
+  private func centerOnPointerScreen(_ window: NSWindow) {
+    let pointer = NSEvent.mouseLocation
+    let screen = NSScreen.screens.first(where: { NSMouseInRect(pointer, $0.frame, false) })
+      ?? NSScreen.main
+      ?? NSScreen.screens.first
+    guard let visible = screen?.visibleFrame else { window.center(); return }
+    let width = min(window.frame.width, visible.width - 40)
+    let height = min(window.frame.height, visible.height - 40)
+    window.setFrame(NSRect(
+      x: visible.midX - width / 2,
+      y: visible.midY - height / 2,
+      width: width,
+      height: height
+    ), display: true)
+  }
+
   func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
     if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
       completionHandler(.useCredential, URLCredential(user: "admin", password: "DW2024!", persistence: .forSession))
@@ -286,6 +330,7 @@ private final class BarController: NSObject, NSApplicationDelegate, WKNavigation
           self.workingDetails = doing.map { "\(self.shortID($0.id)) · \($0.title)" }
           self.latestDetail = latestText
           self.panel.orderFrontRegardless()
+          if let popup = self.ticketPopup, !popup.isMiniaturized { popup.orderFrontRegardless() }
         }
       } catch {
         DispatchQueue.main.async { self.latest.stringValue = "Latest · ledger unavailable" }

← d9472b3e Keep ticket controls in a real window  ·  back to Ticket System  ·  Record desktop ticket bar interaction proof 35f0880e →