| | |
| | | static let tagLauncherAppNoteEditingChanged = Notification.Name("TagLauncherAppNoteEditingChanged") |
| | | static let tagLauncherDataDidChange = Notification.Name("TagLauncherDataDidChange") |
| | | static let tagLauncherOpenPreferencesRequested = Notification.Name("TagLauncherOpenPreferencesRequested") |
| | | static let tagLauncherOverlayDidShow = Notification.Name("TagLauncherOverlayDidShow") |
| | | } |
| | | |
| | | // MARK: - Edit Phase |
| | |
| | | private let editSidebarHorizontalInset: CGFloat = 12 |
| | | private let floatingControlsTrailingInset: CGFloat = 20 |
| | | private let floatingControlsReservedWidth: CGFloat = 120 |
| | | private var appBubbleDisabled: Bool { |
| | | appDragModeActive || pendingUncategorizedDrop != nil |
| | | } |
| | | private let rightSidebarFloatingClearance: CGFloat = 44 |
| | | |
| | | private var isSideLayout: Bool { |
| | |
| | | } |
| | | } |
| | | .onAppear { |
| | | let mousePoint = NSEvent.mouseLocation |
| | | let activeScreen = NSScreen.screens.first(where: { |
| | | NSMouseInRect(mousePoint, $0.frame, false) |
| | | }) ?? NSScreen.main |
| | | notchHeight = activeScreen?.safeAreaInsets.top ?? 0 |
| | | refreshNotchHeight() |
| | | refreshApps() |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in |
| | | refreshNotchHeight() |
| | | refreshApps() |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in |
| | | guard allApps.isEmpty, !refreshInProgress else { return } |
| | | refreshApps() |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherDataDidChange)) { _ in |
| | |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | hoveredAppItemID: $hoveredAppItemID, |
| | | onDropApp: { path, source, copy in |
| | | dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy) |
| | |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | itemID: "\(group.name)|\(app.path.path)", |
| | | hoveredAppItemID: $hoveredAppItemID, |
| | | onSelect: { openApp(app) } |
| | |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | itemID: "\(group.name)|\(app.path.path)", |
| | | hoveredAppItemID: $hoveredAppItemID, |
| | | onSelect: { openApp(app) } |
| | |
| | | |
| | | // MARK: - Actions |
| | | |
| | | private func refreshNotchHeight() { |
| | | let mousePoint = NSEvent.mouseLocation |
| | | let activeScreen = NSScreen.screens.first(where: { |
| | | NSMouseInRect(mousePoint, $0.frame, false) |
| | | }) ?? NSScreen.main |
| | | notchHeight = activeScreen?.safeAreaInsets.top ?? 0 |
| | | } |
| | | |
| | | private func handleBubbleHover(app: AppInfo, frame: CGRect, hovering: Bool) { |
| | | guard !appBubbleDisabled else { |
| | | clearAppBubbleState() |
| | | return |
| | | } |
| | | guard editingBubble == nil else { return } |
| | | if hovering { |
| | | hoveredBubble = AppBubbleContext(app: app, frame: frame) |
| | |
| | | } |
| | | |
| | | private func beginEditingBubbleNote(app: AppInfo, frame: CGRect) { |
| | | guard !appBubbleDisabled else { |
| | | clearAppBubbleState() |
| | | return |
| | | } |
| | | bubbleDraftNote = currentNote(for: app) |
| | | hoveredBubble = nil |
| | | editingBubble = AppBubbleContext(app: app, frame: frame) |
| | |
| | | } |
| | | |
| | | private func dismissAppBubble() { |
| | | hoveredBubble = nil |
| | | if editingBubble != nil { |
| | | notifyAppNoteEditing(active: false) |
| | | } |
| | | editingBubble = nil |
| | | bubbleNoteFocused = false |
| | | } |
| | | |
| | | private func clearAppBubbleState() { |
| | | hoveredAppItemID = nil |
| | | hoveredBubble = nil |
| | | if editingBubble != nil { |
| | | notifyAppNoteEditing(active: false) |
| | |
| | | let assignedTags = assignedRegularDisplayTags(for: app) |
| | | guard !assignedTags.isEmpty else { return } |
| | | |
| | | clearAppBubbleState() |
| | | withAnimation(.spring(response: 0.24, dampingFraction: 0.84)) { |
| | | pendingUncategorizedDrop = PendingUncategorizedDrop(app: app, assignedTags: assignedTags) |
| | | } |
| | |
| | | private func setAppDragMode(_ active: Bool) { |
| | | if active { |
| | | endTagNavReorder() |
| | | clearAppBubbleState() |
| | | } |
| | | appDragModeActive = active |
| | | if active { |
| | |
| | | |
| | | func refreshApps(forceLayoutRefresh: Bool = false) { |
| | | guard !refreshInProgress else { |
| | | refreshAgainAfterCurrent = true |
| | | refreshAgainForceLayout = refreshAgainForceLayout || forceLayoutRefresh |
| | | if forceLayoutRefresh { |
| | | refreshAgainAfterCurrent = true |
| | | refreshAgainForceLayout = true |
| | | } |
| | | return |
| | | } |
| | | |