From ed43ee83789cb99b1cb95db340fee939dd0dedec Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 05 Jun 2026 02:07:17 +0800
Subject: [PATCH] Add final 7.8.14 App Store submission package
---
Apptag/TagNavigationView.swift | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 267 insertions(+), 15 deletions(-)
diff --git a/Apptag/TagNavigationView.swift b/Apptag/TagNavigationView.swift
index 2736f53..449895d 100644
--- a/Apptag/TagNavigationView.swift
+++ b/Apptag/TagNavigationView.swift
@@ -16,8 +16,19 @@
let items: [TagNavigationItem]
let orientation: Orientation
let contentInsets: NSEdgeInsets
+ let dragModeActive: Bool
+ let draggingItemID: String?
+ let appDragModeActive: Bool
+ let appDropTargetID: String?
let onActivate: (String) -> Void
let onHoverChange: (String, Bool) -> Void
+ let canReorder: (String) -> Bool
+ let canAcceptAppDrop: (String) -> Bool
+ let onReorderBegan: (String) -> Void
+ let onReorderMoved: (String, String) -> Void
+ let onReorderEnded: () -> Void
+ let onAppDropHoverChange: (String, Bool) -> Void
+ let onAppDrop: (String, String, String, Bool) -> Void
func makeNSView(context: Context) -> TagNavigationHostView {
let view = TagNavigationHostView()
@@ -25,8 +36,19 @@
items: items,
orientation: orientation,
contentInsets: contentInsets,
+ dragModeActive: dragModeActive,
+ draggingItemID: draggingItemID,
+ appDragModeActive: appDragModeActive,
+ appDropTargetID: appDropTargetID,
onActivate: onActivate,
- onHoverChange: onHoverChange
+ onHoverChange: onHoverChange,
+ canReorder: canReorder,
+ canAcceptAppDrop: canAcceptAppDrop,
+ onReorderBegan: onReorderBegan,
+ onReorderMoved: onReorderMoved,
+ onReorderEnded: onReorderEnded,
+ onAppDropHoverChange: onAppDropHoverChange,
+ onAppDrop: onAppDrop
)
return view
}
@@ -36,8 +58,19 @@
items: items,
orientation: orientation,
contentInsets: contentInsets,
+ dragModeActive: dragModeActive,
+ draggingItemID: draggingItemID,
+ appDragModeActive: appDragModeActive,
+ appDropTargetID: appDropTargetID,
onActivate: onActivate,
- onHoverChange: onHoverChange
+ onHoverChange: onHoverChange,
+ canReorder: canReorder,
+ canAcceptAppDrop: canAcceptAppDrop,
+ onReorderBegan: onReorderBegan,
+ onReorderMoved: onReorderMoved,
+ onReorderEnded: onReorderEnded,
+ onAppDropHoverChange: onAppDropHoverChange,
+ onAppDrop: onAppDrop
)
}
}
@@ -62,18 +95,40 @@
items: [TagNavigationItem],
orientation: TagNavigationView.Orientation,
contentInsets: NSEdgeInsets,
+ dragModeActive: Bool,
+ draggingItemID: String?,
+ appDragModeActive: Bool,
+ appDropTargetID: String?,
onActivate: @escaping (String) -> Void,
- onHoverChange: @escaping (String, Bool) -> Void
+ onHoverChange: @escaping (String, Bool) -> Void,
+ canReorder: @escaping (String) -> Bool,
+ canAcceptAppDrop: @escaping (String) -> Bool,
+ onReorderBegan: @escaping (String) -> Void,
+ onReorderMoved: @escaping (String, String) -> Void,
+ onReorderEnded: @escaping () -> Void,
+ onAppDropHoverChange: @escaping (String, Bool) -> Void,
+ onAppDrop: @escaping (String, String, String, Bool) -> Void
) {
documentView.update(
items: items,
orientation: orientation,
contentInsets: contentInsets,
+ dragModeActive: dragModeActive,
+ draggingItemID: draggingItemID,
+ appDragModeActive: appDragModeActive,
+ appDropTargetID: appDropTargetID,
onActivate: onActivate,
- onHoverChange: onHoverChange
+ onHoverChange: onHoverChange,
+ canReorder: canReorder,
+ canAcceptAppDrop: canAcceptAppDrop,
+ onReorderBegan: onReorderBegan,
+ onReorderMoved: onReorderMoved,
+ onReorderEnded: onReorderEnded,
+ onAppDropHoverChange: onAppDropHoverChange,
+ onAppDrop: onAppDrop
)
- scrollView.hasHorizontalScroller = orientation == .horizontal
- scrollView.hasVerticalScroller = orientation == .vertical
+ scrollView.hasHorizontalScroller = false
+ scrollView.hasVerticalScroller = false
needsLayout = true
}
@@ -96,6 +151,8 @@
scrollView.borderType = .noBorder
scrollView.autohidesScrollers = true
scrollView.scrollerStyle = .overlay
+ scrollView.horizontalScrollElasticity = .allowed
+ scrollView.verticalScrollElasticity = .allowed
scrollView.hasHorizontalScroller = false
scrollView.hasVerticalScroller = false
scrollView.documentView = documentView
@@ -108,8 +165,19 @@
private var orientation: TagNavigationView.Orientation = .horizontal
private var contentInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
private var buttons: [TagNavigationButton] = []
+ private var dragModeActive = false
+ private var draggingItemID: String?
+ private var appDragModeActive = false
+ private var appDropTargetID: String?
private var onActivate: (String) -> Void = { _ in }
private var onHoverChange: (String, Bool) -> Void = { _, _ in }
+ private var canReorder: (String) -> Bool = { _ in false }
+ private var canAcceptAppDrop: (String) -> Bool = { _ in false }
+ private var onReorderBegan: (String) -> Void = { _ in }
+ private var onReorderMoved: (String, String) -> Void = { _, _ in }
+ private var onReorderEnded: () -> Void = {}
+ private var onAppDropHoverChange: (String, Bool) -> Void = { _, _ in }
+ private var onAppDrop: (String, String, String, Bool) -> Void = { _, _, _, _ in }
override var isFlipped: Bool { true }
@@ -117,23 +185,46 @@
items: [TagNavigationItem],
orientation: TagNavigationView.Orientation,
contentInsets: NSEdgeInsets,
+ dragModeActive: Bool,
+ draggingItemID: String?,
+ appDragModeActive: Bool,
+ appDropTargetID: String?,
onActivate: @escaping (String) -> Void,
- onHoverChange: @escaping (String, Bool) -> Void
+ onHoverChange: @escaping (String, Bool) -> Void,
+ canReorder: @escaping (String) -> Bool,
+ canAcceptAppDrop: @escaping (String) -> Bool,
+ onReorderBegan: @escaping (String) -> Void,
+ onReorderMoved: @escaping (String, String) -> Void,
+ onReorderEnded: @escaping () -> Void,
+ onAppDropHoverChange: @escaping (String, Bool) -> Void,
+ onAppDrop: @escaping (String, String, String, Bool) -> Void
) {
- let needsRebuild = self.items != items || self.orientation != orientation
+ let oldNames = self.items.map(\.name)
+ let newNames = items.map(\.name)
+ let needsRebuild = Set(oldNames) != Set(newNames) || self.orientation != orientation
self.items = items
self.orientation = orientation
self.contentInsets = contentInsets
+ self.dragModeActive = dragModeActive
+ self.draggingItemID = draggingItemID
+ self.appDragModeActive = appDragModeActive
+ self.appDropTargetID = appDropTargetID
self.onActivate = onActivate
self.onHoverChange = onHoverChange
+ self.canReorder = canReorder
+ self.canAcceptAppDrop = canAcceptAppDrop
+ self.onReorderBegan = onReorderBegan
+ self.onReorderMoved = onReorderMoved
+ self.onReorderEnded = onReorderEnded
+ self.onAppDropHoverChange = onAppDropHoverChange
+ self.onAppDrop = onAppDrop
if needsRebuild {
rebuildButtons()
} else {
- for (button, item) in zip(buttons, items) {
- button.configure(item: item, orientation: orientation)
- }
+ reuseButtonsInCurrentOrder()
}
+ updateButtonRuntimeState()
needsLayout = true
}
@@ -171,9 +262,66 @@
let button = TagNavigationButton(item: item, orientation: orientation)
button.onActivate = { [weak self] tagID in self?.onActivate(tagID) }
button.onHoverChange = { [weak self] tagID, active in self?.onHoverChange(tagID, active) }
+ button.canReorder = { [weak self] tagID in self?.canReorder(tagID) ?? false }
+ button.onReorderBegan = { [weak self] tagID in self?.onReorderBegan(tagID) }
+ button.onReorderMoved = { [weak self] tagID, screenPoint in
+ self?.handleReorderMove(tagID: tagID, screenPoint: screenPoint)
+ }
+ button.onReorderEnded = { [weak self] in self?.onReorderEnded() }
+ button.canAcceptAppDrop = { [weak self] tagID in self?.canAcceptAppDrop(tagID) ?? false }
+ button.onAppDropHoverChange = { [weak self] tagID, active in
+ self?.setLocalAppDropHover(tagID, active: active)
+ self?.onAppDropHoverChange(tagID, active)
+ }
+ button.onAppDrop = { [weak self] path, source, targetTag, copy in
+ self?.onAppDrop(path, source, targetTag, copy)
+ }
addSubview(button)
return button
}
+ }
+
+ private func reuseButtonsInCurrentOrder() {
+ let existing = Dictionary(uniqueKeysWithValues: buttons.map { ($0.itemName, $0) })
+ buttons = items.compactMap { item in
+ guard let button = existing[item.name] else { return nil }
+ button.configure(item: item, orientation: orientation)
+ button.refreshAppDropRegistration()
+ return button
+ }
+ }
+
+ private func updateButtonRuntimeState() {
+ for button in buttons {
+ let isAppDropTarget = appDropTargetID == button.itemName
+ let appDropVisualActive = appDropTargetID != nil
+ button.configureRuntime(
+ dragModeActive: (dragModeActive && canReorder(button.itemName)) || appDropVisualActive,
+ isDragging: draggingItemID == button.itemName || isAppDropTarget
+ )
+ button.refreshAppDropRegistration()
+ }
+ }
+
+ private func setLocalAppDropHover(_ tagID: String, active: Bool) {
+ if active {
+ appDropTargetID = tagID
+ } else if appDropTargetID == tagID {
+ appDropTargetID = nil
+ }
+ updateButtonRuntimeState()
+ }
+
+ private func handleReorderMove(tagID: String, screenPoint: NSPoint) {
+ guard let window else { return }
+ let windowPoint = window.convertPoint(fromScreen: screenPoint)
+ let point = convert(windowPoint, from: nil)
+ guard let target = buttons.first(where: { button in
+ button.itemName != tagID
+ && canReorder(button.itemName)
+ && button.frame.insetBy(dx: -4, dy: -4).contains(point)
+ }) else { return }
+ onReorderMoved(tagID, target.itemName)
}
private func layoutHorizontal() {
@@ -198,14 +346,27 @@
}
}
-final class TagNavigationButton: NSButton {
+final class TagNavigationButton: NSButton, AppDropTargetReceivingView {
var onActivate: (String) -> Void = { _ in }
var onHoverChange: (String, Bool) -> Void = { _, _ in }
+ var canReorder: (String) -> Bool = { _ in false }
+ var onReorderBegan: (String) -> Void = { _ in }
+ var onReorderMoved: (String, NSPoint) -> Void = { _, _ in }
+ var onReorderEnded: () -> Void = {}
+ var canAcceptAppDrop: (String) -> Bool = { _ in false }
+ var onAppDropHoverChange: (String, Bool) -> Void = { _, _ in }
+ var onAppDrop: (String, String, String, Bool) -> Void = { _, _, _, _ in }
private var item: TagNavigationItem
private var orientation: TagNavigationView.Orientation
private var trackingAreaRef: NSTrackingArea?
private var isMouseInside = false
+ private var dragModeActive = false
+ private var isDragging = false
+ private let appDropTargetID = UUID()
+ private var appDropRegistered = false
+
+ var itemName: String { item.name }
var preferredSize: NSSize {
let font = NSFont.systemFont(ofSize: 13, weight: .medium)
@@ -237,13 +398,51 @@
}
func configure(item: TagNavigationItem, orientation: TagNavigationView.Orientation) {
+ if self.item.name != item.name {
+ unregisterAppDropTarget()
+ }
self.item = item
self.orientation = orientation
title = item.name
alignment = orientation == .horizontal ? .center : .left
setAccessibilityLabel(item.name)
updateAppearance()
+ refreshAppDropRegistration()
needsLayout = true
+ }
+
+ func configureRuntime(dragModeActive: Bool, isDragging: Bool) {
+ self.dragModeActive = dragModeActive
+ self.isDragging = isDragging
+ updateAppearance()
+ }
+
+ func refreshAppDropRegistration() {
+ guard window != nil, canAcceptAppDrop(item.id) else {
+ unregisterAppDropTarget()
+ return
+ }
+ AppDragCoordinator.shared.register(id: appDropTargetID, view: self, tag: item.name)
+ appDropRegistered = true
+ }
+
+ override func viewDidMoveToWindow() {
+ super.viewDidMoveToWindow()
+ refreshAppDropRegistration()
+ }
+
+ func appDragHoverChanged(active: Bool) {
+ guard canAcceptAppDrop(item.id) || !active else { return }
+ onAppDropHoverChange(item.name, active)
+ }
+
+ func performDrop(path: String, source: String, copy: Bool) {
+ guard canAcceptAppDrop(item.id) else { return }
+ onAppDrop(path, source, item.name, copy)
+ }
+
+ deinit {
+ unregisterAppDropTarget()
}
override func updateTrackingAreas() {
@@ -275,18 +474,71 @@
onHoverChange(item.id, false)
}
+ override func mouseDown(with event: NSEvent) {
+ guard canReorder(item.id) else {
+ onActivate(item.id)
+ return
+ }
+
+ let start = Date()
+ let longPressDuration: TimeInterval = 0.35
+ var reorderActive = false
+ var finished = false
+
+ while !finished {
+ if !reorderActive && Date().timeIntervalSince(start) >= longPressDuration {
+ reorderActive = true
+ onReorderBegan(item.id)
+ }
+
+ let nextEvent = window?.nextEvent(
+ matching: [.leftMouseDragged, .leftMouseUp],
+ until: Date().addingTimeInterval(0.04),
+ inMode: .eventTracking,
+ dequeue: true
+ )
+
+ guard let nextEvent else { continue }
+ switch nextEvent.type {
+ case .leftMouseDragged:
+ if reorderActive {
+ onReorderMoved(item.id, NSEvent.mouseLocation)
+ }
+ case .leftMouseUp:
+ if reorderActive {
+ onReorderEnded()
+ } else {
+ onActivate(item.id)
+ }
+ finished = true
+ default:
+ break
+ }
+ }
+ }
+
@objc private func performActivation() {
onActivate(item.id)
}
+ private func unregisterAppDropTarget() {
+ guard appDropRegistered else { return }
+ AppDragCoordinator.shared.unregister(id: appDropTargetID)
+ appDropRegistered = false
+ }
+
private func updateAppearance() {
let background = TagColor.nsColor(for: item.colorIndex)
+ let dragScale: CGFloat = isDragging ? 1.04 : 1.0
layer?.backgroundColor = background.cgColor
layer?.cornerRadius = orientation == .horizontal ? 7 : 6
- layer?.shadowColor = NSColor.black.withAlphaComponent(0.20).cgColor
+ layer?.shadowColor = NSColor.black.withAlphaComponent(isDragging ? 0.50 : 0.20).cgColor
layer?.shadowOpacity = 1
- layer?.shadowRadius = 3
- layer?.shadowOffset = NSSize(width: 0, height: -1)
+ layer?.shadowRadius = isDragging ? 18 : 3
+ layer?.shadowOffset = NSSize(width: 0, height: isDragging ? -9 : -1)
+ layer?.zPosition = isDragging ? 30 : 0
+ layer?.setAffineTransform(CGAffineTransform(scaleX: dragScale, y: dragScale))
+ alphaValue = dragModeActive ? (isDragging ? 1.0 : 0.62) : 1.0
let textColor: NSColor = (item.colorIndex == 0 || item.colorIndex == 5) ? .labelColor : .white
let paragraph = NSMutableParagraphStyle()
--
Gitblit v1.9.3