Skip to content

Commit 271b228

Browse files
committed
fix: fix context menu
1 parent 90c4dba commit 271b228

6 files changed

Lines changed: 222 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ All notable changes to DockCleat will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.2] - 2025-12-15
9+
10+
### Fixed
11+
- Fixed "Open Main Window" functionality when "Show in Dock" is disabled
12+
- Fixed symbol name error in menu bar language selection (empty string causing "No symbol named '' found" error)
13+
- Fixed duplicate restart dialogs appearing in both main window and menu bar
14+
- Fixed window creation logic to properly handle accessory mode (no Dock icon)
15+
- Fixed compilation error with `nonisolated deinit` for GitHub Actions compatibility
16+
- Improved restart dialog display logic to show in menu bar when main window is closed
17+
18+
### Changed
19+
- Improved window opening mechanism using WindowOpener singleton for better SwiftUI integration
20+
- Menu bar restart dialog now only appears when main window is not visible
21+
- Language selection menu items now use conditional rendering instead of empty string symbols
22+
23+
### Technical
24+
- Implemented WindowOpener helper class to store and use SwiftUI's openWindow action
25+
- Added temporary activation policy switching to ensure window creation in accessory mode
26+
- Removed experimental `nonisolated deinit` keyword for better compiler compatibility
27+
- Added `--test` flag to build-release.sh script to skip notarization for local testing
28+
829
## [1.0.1] - 2025-12-14
930

1031
### Fixed

DockCleat.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@
258258
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
259259
CODE_SIGN_STYLE = Automatic;
260260
COMBINE_HIDPI_IMAGES = YES;
261-
CURRENT_PROJECT_VERSION = 2;
261+
CURRENT_PROJECT_VERSION = 3;
262262
DEVELOPMENT_TEAM = JDMS6RY775;
263263
ENABLE_APP_SANDBOX = NO;
264264
ENABLE_HARDENED_RUNTIME = YES;
@@ -277,7 +277,7 @@
277277
"@executable_path/../Frameworks",
278278
);
279279
MACOSX_DEPLOYMENT_TARGET = 14.0;
280-
MARKETING_VERSION = 1.0.1;
280+
MARKETING_VERSION = 1.0.2;
281281
PRODUCT_BUNDLE_IDENTIFIER = com.aitiotekt.dockcleat;
282282
PRODUCT_NAME = "$(TARGET_NAME)";
283283
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -301,7 +301,7 @@
301301
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
302302
CODE_SIGN_STYLE = Automatic;
303303
COMBINE_HIDPI_IMAGES = YES;
304-
CURRENT_PROJECT_VERSION = 2;
304+
CURRENT_PROJECT_VERSION = 3;
305305
DEVELOPMENT_TEAM = JDMS6RY775;
306306
ENABLE_APP_SANDBOX = NO;
307307
ENABLE_HARDENED_RUNTIME = YES;
@@ -320,7 +320,7 @@
320320
"@executable_path/../Frameworks",
321321
);
322322
MACOSX_DEPLOYMENT_TARGET = 14.0;
323-
MARKETING_VERSION = 1.0.1;
323+
MARKETING_VERSION = 1.0.2;
324324
PRODUCT_BUNDLE_IDENTIFIER = com.aitiotekt.dockcleat;
325325
PRODUCT_NAME = "$(TARGET_NAME)";
326326
PROVISIONING_PROFILE_SPECIFIER = "";

DockCleat/DockCleatApp.swift

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,58 @@
88

99
import SwiftUI
1010

11+
// MARK: - Window Opener Helper
12+
13+
/// A singleton to store the openWindow action for use outside SwiftUI context
14+
@MainActor
15+
class WindowOpener {
16+
static let shared = WindowOpener()
17+
18+
var openWindowAction: ((String) -> Void)?
19+
20+
private init() {}
21+
22+
func openMainWindow() {
23+
let shouldHideDock = !AppSettings.shared.showInDock
24+
25+
// Temporarily set to regular mode to allow window creation
26+
if shouldHideDock {
27+
NSApp.setActivationPolicy(.regular)
28+
}
29+
30+
// Activate app
31+
NSApp.activate(ignoringOtherApps: true)
32+
33+
// Call the stored openWindow action
34+
if let action = openWindowAction {
35+
print("[WindowOpener] Opening window with stored action")
36+
action("main")
37+
38+
// Restore accessory mode after window is shown
39+
if shouldHideDock {
40+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
41+
NSApp.setActivationPolicy(.accessory)
42+
}
43+
}
44+
} else {
45+
print("[WindowOpener] No openWindow action stored, trying fallback...")
46+
// Fallback: restore accessory mode
47+
if shouldHideDock {
48+
NSApp.setActivationPolicy(.accessory)
49+
}
50+
}
51+
}
52+
}
53+
1154
@main
1255
struct DockCleatApp: App {
1356
/// Use NSApplicationDelegateAdaptor to integrate AppDelegate with SwiftUI lifecycle
1457
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
1558

1659
@StateObject private var settings = AppSettings.shared
1760
@StateObject private var monitorService = DockMonitorService.shared
18-
@State private var showingSettings = false
61+
62+
@Environment(\.openWindow) private var openWindow
1963

2064
init() {
2165
// Request accessibility permission as early as possible
@@ -26,13 +70,22 @@ struct DockCleatApp: App {
2670
}
2771

2872
var body: some Scene {
29-
// Main Window
30-
WindowGroup {
73+
// Main Window with ID for programmatic opening
74+
WindowGroup(id: "main") {
3175
ContentView()
3276
.environmentObject(settings)
3377
.environmentObject(monitorService)
3478
.preferredColorScheme(settings.appTheme.colorScheme)
3579
.environment(\.locale, settings.currentLocale)
80+
.task {
81+
// Store the openWindow action when ContentView appears
82+
await MainActor.run {
83+
WindowOpener.shared.openWindowAction = { id in
84+
openWindow(id: id)
85+
}
86+
print("[DockCleatApp] Stored openWindow action")
87+
}
88+
}
3689
}
3790
.windowStyle(.hiddenTitleBar)
3891
.windowResizability(.contentSize)
@@ -69,6 +122,21 @@ class AppDelegate: NSObject, NSApplicationDelegate {
69122
// Start monitoring service after app is fully launched
70123
DockMonitorService.shared.startMonitoring()
71124
}
125+
126+
// Listen for window open requests
127+
NotificationCenter.default.addObserver(
128+
self,
129+
selector: #selector(handleOpenWindowRequest),
130+
name: NSNotification.Name("RequestMainWindow"),
131+
object: nil
132+
)
133+
}
134+
135+
@objc private func handleOpenWindowRequest() {
136+
// Use WindowOpener to open the main window
137+
Task { @MainActor in
138+
WindowOpener.shared.openMainWindow()
139+
}
72140
}
73141

74142
func applicationWillTerminate(_ notification: Notification) {

DockCleat/MenuBarManager.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ class MenuBarManager: NSObject, ObservableObject {
160160
if popover == nil {
161161
popover = NSPopover()
162162
popover?.behavior = .transient
163-
popover?.contentSize = NSSize(width: 260, height: 500)
164163
popover?.delegate = self
165164

166165
// Create hosting view with MenuBarView
@@ -170,21 +169,38 @@ class MenuBarManager: NSObject, ObservableObject {
170169
.preferredColorScheme(settings.appTheme.colorScheme)
171170
.environment(\.locale, settings.currentLocale)
172171

173-
// Create hosting view with proper configuration to avoid layout warnings
174-
// Use fixed size to prevent layout recursion
175-
hostingView = NSHostingView(
176-
rootView: AnyView(menuBarView.frame(width: 260, height: 500))
177-
)
172+
// Create hosting view - let SwiftUI determine the height
173+
hostingView = NSHostingView(rootView: AnyView(menuBarView))
174+
175+
// Create view controller
176+
let viewController = NSViewController()
178177

179-
// Set frame after creation to avoid layout during initialization
180178
if let hostingView = hostingView {
181-
hostingView.frame = NSRect(x: 0, y: 0, width: 260, height: 500)
182-
hostingView.autoresizingMask = [.width, .height]
179+
// Set initial frame - will be adjusted based on content
180+
hostingView.frame = NSRect(x: 0, y: 0, width: 260, height: 400)
183181
}
184182

185-
let viewController = NSViewController()
186183
viewController.view = hostingView ?? NSView()
187184
popover?.contentViewController = viewController
185+
186+
// Update popover size after view has laid out
187+
// Use async to avoid layout recursion warnings
188+
DispatchQueue.main.async { [weak self] in
189+
guard let self = self,
190+
let hostingView = self.hostingView,
191+
let popover = self.popover else { return }
192+
193+
// Get the preferred size from the hosting view
194+
// This calculates the size based on SwiftUI content
195+
let preferredSize = hostingView.fittingSize
196+
let contentHeight = max(300, preferredSize.height) // Minimum height of 300
197+
198+
// Update hosting view frame to match content
199+
hostingView.frame = NSRect(x: 0, y: 0, width: 260, height: contentHeight)
200+
201+
// Update popover size to match content
202+
popover.contentSize = NSSize(width: 260, height: contentHeight)
203+
}
188204
}
189205

190206
// Show popover
@@ -201,9 +217,10 @@ class MenuBarManager: NSObject, ObservableObject {
201217

202218
// MARK: - Cleanup
203219

204-
nonisolated deinit {
205-
// Clean up status item synchronously
206-
// Note: This is safe because NSStatusBar operations are thread-safe
220+
deinit {
221+
// Clean up status item
222+
// Note: Since MenuBarManager is @MainActor, deinit is called on main thread
223+
// NSStatusBar operations are thread-safe, so this is safe
207224
if let statusItem = statusItem {
208225
NSStatusBar.system.removeStatusItem(statusItem)
209226
}

DockCleat/MenuBarView.swift

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,21 @@ struct MenuBarView: View {
7272
hasAccessibilityPermission = AccessibilityService.shared.isAccessibilityEnabled
7373
}
7474
.onReceive(NotificationCenter.default.publisher(for: .showLanguageRestartDialog)) { _ in
75-
showRestartDialog = true
75+
// Check if main window is open - if so, let it handle the dialog
76+
// If main window is closed, show dialog in menu bar
77+
let hasMainWindow = NSApp.windows.contains { window in
78+
let className = window.className
79+
return !className.contains("NSPopover") &&
80+
!className.contains("NSStatusBarWindow") &&
81+
!className.contains("NSPanel") &&
82+
window.canBecomeKey &&
83+
window.isVisible
84+
}
85+
86+
// Only show dialog if no main window is visible
87+
if !hasMainWindow {
88+
showRestartDialog = true
89+
}
7690
}
7791
.alert(String(localized: "Restart Required"), isPresented: $showRestartDialog) {
7892
Button(String(localized: "Restart Now")) {
@@ -230,9 +244,14 @@ struct MenuBarView: View {
230244
settings.targetScreenID = screen.id
231245
} label: {
232246
HStack(spacing: 10) {
233-
Image(systemName: settings.targetScreenID == screen.id ? "checkmark" : "")
234-
.font(.system(size: 12))
235-
.frame(width: 16)
247+
if settings.targetScreenID == screen.id {
248+
Image(systemName: "checkmark")
249+
.font(.system(size: 12))
250+
.frame(width: 16)
251+
} else {
252+
Spacer()
253+
.frame(width: 16)
254+
}
236255

237256
Text(screen.name)
238257
.font(.system(size: 13))
@@ -258,9 +277,14 @@ struct MenuBarView: View {
258277
settings.dockPosition = position
259278
} label: {
260279
HStack(spacing: 10) {
261-
Image(systemName: settings.dockPosition == position ? "checkmark" : "")
262-
.font(.system(size: 12))
263-
.frame(width: 16)
280+
if settings.dockPosition == position {
281+
Image(systemName: "checkmark")
282+
.font(.system(size: 12))
283+
.frame(width: 16)
284+
} else {
285+
Spacer()
286+
.frame(width: 16)
287+
}
264288

265289
Text(position.localizedName)
266290
.font(.system(size: 13))
@@ -305,9 +329,14 @@ struct MenuBarView: View {
305329
settings.appLanguage = language
306330
} label: {
307331
HStack(spacing: 10) {
308-
Image(systemName: settings.appLanguage == language ? "checkmark" : "")
309-
.font(.system(size: 12))
310-
.frame(width: 16)
332+
if settings.appLanguage == language {
333+
Image(systemName: "checkmark")
334+
.font(.system(size: 12))
335+
.frame(width: 16)
336+
} else {
337+
Spacer()
338+
.frame(width: 16)
339+
}
311340

312341
Text(language.displayNameForPicker)
313342
.font(.system(size: 13))
@@ -371,23 +400,12 @@ struct MenuBarView: View {
371400
}
372401

373402
private func openMainWindow() {
374-
// Activate app and bring window to front
375-
NSApp.activate(ignoringOtherApps: true)
376-
377-
// Open or focus the main window
378-
if let window = NSApp.windows.first(where: {
379-
$0.title.contains("DockCleat") || $0.isVisible == false
380-
}) {
381-
window.makeKeyAndOrderFront(nil)
382-
} else {
383-
// If no window exists, create one
384-
for window in NSApp.windows {
385-
if window.contentView != nil {
386-
window.makeKeyAndOrderFront(nil)
387-
break
388-
}
389-
}
390-
}
403+
// Close popover if open
404+
dismiss()
405+
406+
// Send notification to AppDelegate to handle window creation
407+
// AppDelegate has the proper context to create SwiftUI windows
408+
NotificationCenter.default.post(name: NSNotification.Name("RequestMainWindow"), object: nil)
391409
}
392410
}
393411

0 commit comments

Comments
 (0)