688 lines
25 KiB
Swift
688 lines
25 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import MacToolsPluginKit
|
|
|
|
struct FeatureManagementTableView: NSViewRepresentable {
|
|
static let rowHeight: CGFloat = 62
|
|
static let rowSpacing: CGFloat = 6
|
|
static let verticalContentInset: CGFloat = 6
|
|
private static let dragType = NSPasteboard.PasteboardType("com.ggbond.mactools.feature-management-item")
|
|
|
|
let items: [PluginFeatureManagementItem]
|
|
var isReorderEnabled: Bool = true
|
|
let onVisibilityChange: (String, Bool) -> Void
|
|
let onMove: (String, Int) -> Void
|
|
|
|
static func preferredHeight(for itemCount: Int) -> CGFloat {
|
|
let visibleItemCount = max(itemCount, 1)
|
|
let spacing = CGFloat(max(itemCount - 1, 0)) * rowSpacing
|
|
return CGFloat(visibleItemCount) * rowHeight + spacing + verticalContentInset * 2
|
|
}
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
Coordinator(parent: self)
|
|
}
|
|
|
|
func makeNSView(context: Context) -> NSScrollView {
|
|
let scrollView = NonScrollingTableScrollView()
|
|
scrollView.contentView = LockedClipView()
|
|
scrollView.drawsBackground = false
|
|
scrollView.borderType = .noBorder
|
|
scrollView.hasVerticalScroller = false
|
|
scrollView.hasHorizontalScroller = false
|
|
scrollView.autohidesScrollers = true
|
|
scrollView.verticalScrollElasticity = .none
|
|
scrollView.horizontalScrollElasticity = .none
|
|
scrollView.contentInsets = NSEdgeInsets(
|
|
top: Self.verticalContentInset,
|
|
left: 0,
|
|
bottom: Self.verticalContentInset,
|
|
right: 0
|
|
)
|
|
|
|
let tableView = NSTableView()
|
|
tableView.headerView = nil
|
|
tableView.rowHeight = Self.rowHeight
|
|
tableView.intercellSpacing = NSSize(width: 0, height: Self.rowSpacing)
|
|
tableView.backgroundColor = .clear
|
|
tableView.selectionHighlightStyle = .none
|
|
tableView.focusRingType = .none
|
|
tableView.usesAlternatingRowBackgroundColors = false
|
|
tableView.allowsColumnReordering = false
|
|
tableView.allowsColumnResizing = false
|
|
tableView.allowsEmptySelection = true
|
|
tableView.allowsTypeSelect = false
|
|
tableView.columnAutoresizingStyle = .uniformColumnAutoresizingStyle
|
|
tableView.draggingDestinationFeedbackStyle = .gap
|
|
tableView.verticalMotionCanBeginDrag = true
|
|
tableView.setDraggingSourceOperationMask(.move, forLocal: true)
|
|
tableView.registerForDraggedTypes([Self.dragType])
|
|
|
|
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("feature"))
|
|
column.isEditable = false
|
|
column.resizingMask = .autoresizingMask
|
|
tableView.addTableColumn(column)
|
|
|
|
tableView.delegate = context.coordinator
|
|
tableView.dataSource = context.coordinator
|
|
|
|
scrollView.documentView = tableView
|
|
context.coordinator.tableView = tableView
|
|
syncLayout(in: scrollView, coordinator: context.coordinator)
|
|
return scrollView
|
|
}
|
|
|
|
func updateNSView(_ scrollView: NSScrollView, context: Context) {
|
|
context.coordinator.parent = self
|
|
syncLayout(in: scrollView, coordinator: context.coordinator)
|
|
}
|
|
|
|
private func syncLayout(in scrollView: NSScrollView, coordinator: Coordinator) {
|
|
guard let tableView = coordinator.tableView else {
|
|
return
|
|
}
|
|
|
|
guard !coordinator.isDragging else {
|
|
return
|
|
}
|
|
|
|
let contentHeight = Self.preferredHeight(for: items.count)
|
|
let contentWidth = max(scrollView.contentSize.width, 1)
|
|
let signature = FeatureManagementTableSignature(
|
|
items: items,
|
|
isReorderEnabled: isReorderEnabled,
|
|
contentWidth: contentWidth
|
|
)
|
|
|
|
guard coordinator.lastSignature != signature else {
|
|
return
|
|
}
|
|
|
|
coordinator.lastSignature = signature
|
|
|
|
tableView.reloadData()
|
|
tableView.noteNumberOfRowsChanged()
|
|
|
|
tableView.frame = NSRect(x: 0, y: 0, width: contentWidth, height: contentHeight)
|
|
scrollView.contentView.scroll(to: .zero)
|
|
scrollView.reflectScrolledClipView(scrollView.contentView)
|
|
}
|
|
|
|
@MainActor
|
|
final class Coordinator: NSObject, NSTableViewDataSource, NSTableViewDelegate {
|
|
var parent: FeatureManagementTableView
|
|
weak var tableView: NSTableView?
|
|
fileprivate var lastSignature: FeatureManagementTableSignature?
|
|
|
|
init(parent: FeatureManagementTableView) {
|
|
self.parent = parent
|
|
}
|
|
|
|
func numberOfRows(in tableView: NSTableView) -> Int {
|
|
parent.items.count
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
|
|
FeatureManagementTableView.rowHeight
|
|
}
|
|
|
|
func tableView(
|
|
_ tableView: NSTableView,
|
|
viewFor tableColumn: NSTableColumn?,
|
|
row: Int
|
|
) -> NSView? {
|
|
let identifier = NSUserInterfaceItemIdentifier("FeatureManagementCell")
|
|
let view = (tableView.makeView(withIdentifier: identifier, owner: nil) as? FeatureManagementTableCellView)
|
|
?? FeatureManagementTableCellView(frame: .zero)
|
|
view.identifier = identifier
|
|
|
|
let item = parent.items[row]
|
|
view.configure(
|
|
item: item,
|
|
showsHandle: parent.isReorderEnabled,
|
|
onVisibilityChange: { [weak self] isVisible in
|
|
self?.parent.onVisibilityChange(item.id, isVisible)
|
|
}
|
|
)
|
|
return view
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
|
|
guard parent.isReorderEnabled else {
|
|
return nil
|
|
}
|
|
|
|
let pasteboardItem = NSPasteboardItem()
|
|
pasteboardItem.setString(parent.items[row].id, forType: FeatureManagementTableView.dragType)
|
|
return pasteboardItem
|
|
}
|
|
|
|
private(set) var isDragging = false
|
|
|
|
func tableView(
|
|
_ tableView: NSTableView,
|
|
draggingSession session: NSDraggingSession,
|
|
willBeginAt screenPoint: NSPoint,
|
|
forRowIndexes rowIndexes: IndexSet
|
|
) {
|
|
isDragging = true
|
|
session.animatesToStartingPositionsOnCancelOrFail = true
|
|
session.draggingFormation = .none
|
|
}
|
|
|
|
func tableView(
|
|
_ tableView: NSTableView,
|
|
draggingSession session: NSDraggingSession,
|
|
endedAt screenPoint: NSPoint,
|
|
operation: NSDragOperation
|
|
) {
|
|
isDragging = false
|
|
lastSignature = nil
|
|
tableView.reloadData()
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView, updateDraggingItemsForDrag draggingInfo: NSDraggingInfo) {
|
|
draggingInfo.enumerateDraggingItems(
|
|
options: [],
|
|
for: tableView,
|
|
classes: [NSPasteboardItem.self],
|
|
searchOptions: [:]
|
|
) { [weak self] draggingItem, _, _ in
|
|
guard
|
|
let self,
|
|
let pasteboardItem = draggingItem.item as? NSPasteboardItem,
|
|
let pluginID = pasteboardItem.string(forType: FeatureManagementTableView.dragType),
|
|
let item = parent.items.first(where: { $0.id == pluginID })
|
|
else {
|
|
return
|
|
}
|
|
|
|
let image = FeatureManagementDragPreview.image(
|
|
for: item,
|
|
width: tableView.bounds.width
|
|
)
|
|
let frame = NSRect(
|
|
origin: draggingItem.draggingFrame.origin,
|
|
size: image.size
|
|
)
|
|
draggingItem.setDraggingFrame(frame, contents: image)
|
|
}
|
|
}
|
|
|
|
func tableView(
|
|
_ tableView: NSTableView,
|
|
validateDrop info: NSDraggingInfo,
|
|
proposedRow row: Int,
|
|
proposedDropOperation dropOperation: NSTableView.DropOperation
|
|
) -> NSDragOperation {
|
|
guard parent.isReorderEnabled else {
|
|
return []
|
|
}
|
|
|
|
guard info.draggingPasteboard.availableType(from: [FeatureManagementTableView.dragType]) != nil else {
|
|
return []
|
|
}
|
|
|
|
let targetRow = min(max(row, 0), parent.items.count)
|
|
tableView.setDropRow(targetRow, dropOperation: .above)
|
|
return .move
|
|
}
|
|
|
|
func tableView(
|
|
_ tableView: NSTableView,
|
|
acceptDrop info: NSDraggingInfo,
|
|
row: Int,
|
|
dropOperation: NSTableView.DropOperation
|
|
) -> Bool {
|
|
guard parent.isReorderEnabled else {
|
|
return false
|
|
}
|
|
|
|
guard
|
|
let draggedID = info.draggingPasteboard.string(forType: FeatureManagementTableView.dragType)
|
|
else {
|
|
return false
|
|
}
|
|
|
|
let targetRow = min(max(row, 0), parent.items.count)
|
|
parent.onMove(draggedID, targetRow)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate struct FeatureManagementTableSignature: Equatable {
|
|
struct Row: Equatable {
|
|
let id: String
|
|
let title: String
|
|
let description: String
|
|
let iconName: String
|
|
let isVisible: Bool
|
|
let isActive: Bool
|
|
let presentation: PluginFeaturePresentation
|
|
let category: String?
|
|
let releaseChannel: String?
|
|
}
|
|
|
|
let rows: [Row]
|
|
let isReorderEnabled: Bool
|
|
let contentWidth: CGFloat
|
|
|
|
init(
|
|
items: [PluginFeatureManagementItem],
|
|
isReorderEnabled: Bool,
|
|
contentWidth: CGFloat
|
|
) {
|
|
self.rows = items.map {
|
|
Row(
|
|
id: $0.id,
|
|
title: $0.title,
|
|
description: $0.description,
|
|
iconName: $0.iconName,
|
|
isVisible: $0.isVisible,
|
|
isActive: $0.isActive,
|
|
presentation: $0.presentation,
|
|
category: $0.category,
|
|
releaseChannel: $0.releaseChannel
|
|
)
|
|
}
|
|
self.isReorderEnabled = isReorderEnabled
|
|
self.contentWidth = contentWidth.rounded(.toNearestOrAwayFromZero)
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
enum FeatureManagementTableUpdatePolicy {
|
|
static func needsUpdate(
|
|
previousItems: [PluginFeatureManagementItem],
|
|
currentItems: [PluginFeatureManagementItem],
|
|
previousIsReorderEnabled: Bool,
|
|
currentIsReorderEnabled: Bool,
|
|
previousContentWidth: CGFloat,
|
|
currentContentWidth: CGFloat
|
|
) -> Bool {
|
|
FeatureManagementTableSignature(
|
|
items: previousItems,
|
|
isReorderEnabled: previousIsReorderEnabled,
|
|
contentWidth: previousContentWidth
|
|
) != FeatureManagementTableSignature(
|
|
items: currentItems,
|
|
isReorderEnabled: currentIsReorderEnabled,
|
|
contentWidth: currentContentWidth
|
|
)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private final class NonScrollingTableScrollView: NSScrollView {
|
|
override func scrollWheel(with event: NSEvent) {
|
|
nextResponder?.scrollWheel(with: event)
|
|
}
|
|
}
|
|
|
|
private final class LockedClipView: NSClipView {
|
|
override func constrainBoundsRect(_ proposedBounds: NSRect) -> NSRect {
|
|
var bounds = super.constrainBoundsRect(proposedBounds)
|
|
bounds.origin = .zero
|
|
return bounds
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private enum FeatureManagementDragPreview {
|
|
static func image(for item: PluginFeatureManagementItem, width: CGFloat) -> NSImage {
|
|
let imageSize = NSSize(
|
|
width: min(max(width, 320), 620),
|
|
height: FeatureManagementTableView.rowHeight
|
|
)
|
|
let image = NSImage(size: imageSize)
|
|
|
|
image.lockFocus()
|
|
defer { image.unlockFocus() }
|
|
|
|
NSGraphicsContext.current?.imageInterpolation = .high
|
|
|
|
let bounds = NSRect(origin: .zero, size: imageSize)
|
|
let contentBounds = bounds.insetBy(dx: 1, dy: 1)
|
|
let backgroundPath = NSBezierPath(
|
|
roundedRect: contentBounds,
|
|
xRadius: 12,
|
|
yRadius: 12
|
|
)
|
|
NSColor.windowBackgroundColor.withAlphaComponent(0.96).setFill()
|
|
backgroundPath.fill()
|
|
NSColor.separatorColor.withAlphaComponent(0.28).setStroke()
|
|
backgroundPath.lineWidth = 1
|
|
backgroundPath.stroke()
|
|
|
|
let tintColor = NSColor(item.iconTint)
|
|
let iconBackgroundRect = NSRect(x: 9, y: 16, width: 30, height: 30)
|
|
NSColor(item.iconTint.opacity(0.14)).setFill()
|
|
NSBezierPath(roundedRect: iconBackgroundRect, xRadius: 10, yRadius: 10).fill()
|
|
|
|
drawSymbol(
|
|
item.iconName,
|
|
in: NSRect(x: 16, y: 23, width: 16, height: 16),
|
|
color: tintColor,
|
|
pointSize: 16
|
|
)
|
|
|
|
let trailingWidth: CGFloat = 74
|
|
let textX = iconBackgroundRect.maxX + 12
|
|
let textWidth = max(imageSize.width - textX - trailingWidth - 12, 80)
|
|
let titleRect = NSRect(x: textX, y: 34, width: textWidth, height: 18)
|
|
let descriptionRect = NSRect(x: textX, y: 13, width: textWidth, height: 17)
|
|
|
|
drawText(
|
|
item.title,
|
|
in: titleRect,
|
|
font: .systemFont(ofSize: 13, weight: .semibold),
|
|
color: .labelColor
|
|
)
|
|
|
|
drawText(
|
|
"\(item.description) · \(featureManagementPresentationText(for: item.presentation))",
|
|
in: descriptionRect,
|
|
font: .systemFont(ofSize: 11, weight: .medium),
|
|
color: .secondaryLabelColor
|
|
)
|
|
|
|
if item.isActive {
|
|
NSColor.systemGreen.setFill()
|
|
NSBezierPath(
|
|
ovalIn: NSRect(
|
|
x: imageSize.width - 72,
|
|
y: (imageSize.height - 8) / 2,
|
|
width: 8,
|
|
height: 8
|
|
)
|
|
)
|
|
.fill()
|
|
}
|
|
|
|
drawVisibilityCheckbox(
|
|
isOn: item.isVisible,
|
|
in: NSRect(x: imageSize.width - 50, y: 24, width: 14, height: 14)
|
|
)
|
|
drawSymbol(
|
|
"line.3.horizontal",
|
|
in: NSRect(x: imageSize.width - 20, y: 23, width: 13, height: 13),
|
|
color: .secondaryLabelColor,
|
|
pointSize: 13
|
|
)
|
|
|
|
return image
|
|
}
|
|
|
|
private static func drawText(
|
|
_ text: String,
|
|
in rect: NSRect,
|
|
font: NSFont,
|
|
color: NSColor
|
|
) {
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
paragraphStyle.lineBreakMode = .byTruncatingTail
|
|
|
|
NSString(string: text).draw(
|
|
in: rect,
|
|
withAttributes: [
|
|
.font: font,
|
|
.foregroundColor: color,
|
|
.paragraphStyle: paragraphStyle
|
|
]
|
|
)
|
|
}
|
|
|
|
private static func drawSymbol(
|
|
_ name: String,
|
|
in rect: NSRect,
|
|
color: NSColor,
|
|
pointSize: CGFloat
|
|
) {
|
|
guard
|
|
let symbol = NSImage(
|
|
systemSymbolName: name,
|
|
accessibilityDescription: nil
|
|
)?.withSymbolConfiguration(.init(pointSize: pointSize, weight: .semibold))
|
|
else {
|
|
return
|
|
}
|
|
|
|
let tintedSymbol = symbol.tinted(with: color)
|
|
tintedSymbol.draw(in: rect)
|
|
}
|
|
|
|
private static func drawVisibilityCheckbox(isOn: Bool, in rect: NSRect) {
|
|
let path = NSBezierPath(roundedRect: rect, xRadius: 3, yRadius: 3)
|
|
|
|
if isOn {
|
|
NSColor.controlAccentColor.setFill()
|
|
path.fill()
|
|
|
|
let checkPath = NSBezierPath()
|
|
checkPath.lineWidth = 1.5
|
|
checkPath.lineCapStyle = .round
|
|
checkPath.lineJoinStyle = .round
|
|
checkPath.move(to: NSPoint(
|
|
x: rect.minX + rect.width * 0.2,
|
|
y: rect.midY
|
|
))
|
|
checkPath.line(to: NSPoint(
|
|
x: rect.minX + rect.width * 0.42,
|
|
y: rect.minY + rect.height * 0.28
|
|
))
|
|
checkPath.line(to: NSPoint(
|
|
x: rect.maxX - rect.width * 0.18,
|
|
y: rect.maxY - rect.height * 0.22
|
|
))
|
|
NSColor.white.setStroke()
|
|
checkPath.stroke()
|
|
} else {
|
|
NSColor.windowBackgroundColor.setFill()
|
|
path.fill()
|
|
NSColor.separatorColor.withAlphaComponent(0.6).setStroke()
|
|
path.lineWidth = 1
|
|
path.stroke()
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class FeatureManagementTableCellView: NSTableCellView {
|
|
private let containerView = NSView()
|
|
private let iconBackgroundView = NSView()
|
|
private let iconImageView = NSImageView()
|
|
private let titleRowStackView = NSStackView()
|
|
private let titleLabel = NSTextField(labelWithString: "")
|
|
private let releaseChannelBadgeView = NSHostingView(rootView: PluginReleaseChannelBadge(releaseChannel: nil))
|
|
private let descriptionLabel = NSTextField(labelWithString: "")
|
|
private let activeDotView = NSView()
|
|
private let visibilityButton = NSButton(checkboxWithTitle: "", target: nil, action: nil)
|
|
private let handleImageView = NSImageView()
|
|
private var visibilityHandler: ((Bool) -> Void)?
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
buildViewHierarchy()
|
|
configureStyles()
|
|
configureLayout()
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configure(
|
|
item: PluginFeatureManagementItem,
|
|
showsHandle: Bool,
|
|
onVisibilityChange: @escaping (Bool) -> Void
|
|
) {
|
|
visibilityHandler = onVisibilityChange
|
|
|
|
titleLabel.stringValue = item.title
|
|
configureReleaseChannelBadge(item.releaseChannel)
|
|
descriptionLabel.stringValue = "\(item.description) · \(featureManagementPresentationText(for: item.presentation))"
|
|
iconImageView.image = NSImage(
|
|
systemSymbolName: item.iconName,
|
|
accessibilityDescription: item.title
|
|
)
|
|
iconImageView.contentTintColor = NSColor(item.iconTint)
|
|
iconBackgroundView.layer?.backgroundColor = NSColor(item.iconTint.opacity(0.14)).cgColor
|
|
activeDotView.isHidden = !item.isActive
|
|
visibilityButton.state = item.isVisible ? .on : .off
|
|
handleImageView.isHidden = !showsHandle
|
|
toolTip = item.title
|
|
visibilityButton.toolTip = item.title
|
|
}
|
|
|
|
private func buildViewHierarchy() {
|
|
wantsLayer = true
|
|
containerView.wantsLayer = true
|
|
iconBackgroundView.wantsLayer = true
|
|
activeDotView.wantsLayer = true
|
|
|
|
addSubview(containerView)
|
|
containerView.addSubview(iconBackgroundView)
|
|
iconBackgroundView.addSubview(iconImageView)
|
|
containerView.addSubview(titleRowStackView)
|
|
titleRowStackView.addArrangedSubview(titleLabel)
|
|
titleRowStackView.addArrangedSubview(releaseChannelBadgeView)
|
|
containerView.addSubview(descriptionLabel)
|
|
containerView.addSubview(activeDotView)
|
|
containerView.addSubview(visibilityButton)
|
|
containerView.addSubview(handleImageView)
|
|
}
|
|
|
|
private func configureStyles() {
|
|
containerView.layer?.cornerRadius = 12
|
|
containerView.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
|
iconBackgroundView.layer?.cornerRadius = 10
|
|
|
|
titleRowStackView.orientation = .horizontal
|
|
titleRowStackView.alignment = .centerY
|
|
titleRowStackView.spacing = 6
|
|
titleRowStackView.distribution = .fill
|
|
|
|
titleLabel.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
titleLabel.lineBreakMode = .byTruncatingTail
|
|
titleLabel.maximumNumberOfLines = 1
|
|
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
|
releaseChannelBadgeView.setContentHuggingPriority(.required, for: .horizontal)
|
|
releaseChannelBadgeView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
|
descriptionLabel.font = .systemFont(ofSize: 11, weight: .medium)
|
|
descriptionLabel.textColor = .secondaryLabelColor
|
|
descriptionLabel.lineBreakMode = .byWordWrapping
|
|
descriptionLabel.maximumNumberOfLines = 2
|
|
descriptionLabel.usesSingleLineMode = false
|
|
|
|
activeDotView.layer?.cornerRadius = 4
|
|
activeDotView.layer?.backgroundColor = NSColor.systemGreen.cgColor
|
|
|
|
visibilityButton.setButtonType(.switch)
|
|
visibilityButton.title = ""
|
|
visibilityButton.target = self
|
|
visibilityButton.action = #selector(handleVisibilityToggle(_:))
|
|
|
|
handleImageView.image = NSImage(
|
|
systemSymbolName: "line.3.horizontal",
|
|
accessibilityDescription: AppL10n.plugins(
|
|
"plugin.management.reorderAccessibility",
|
|
defaultValue: "拖拽调整顺序"
|
|
)
|
|
)
|
|
handleImageView.contentTintColor = .secondaryLabelColor
|
|
handleImageView.symbolConfiguration = .init(pointSize: 13, weight: .semibold)
|
|
}
|
|
|
|
private func configureLayout() {
|
|
[
|
|
containerView,
|
|
iconBackgroundView,
|
|
iconImageView,
|
|
titleRowStackView,
|
|
descriptionLabel,
|
|
activeDotView,
|
|
visibilityButton,
|
|
handleImageView
|
|
].forEach {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
}
|
|
|
|
NSLayoutConstraint.activate([
|
|
containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
containerView.topAnchor.constraint(equalTo: topAnchor),
|
|
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
|
iconBackgroundView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
|
|
iconBackgroundView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
|
iconBackgroundView.widthAnchor.constraint(equalToConstant: 30),
|
|
iconBackgroundView.heightAnchor.constraint(equalToConstant: 30),
|
|
|
|
iconImageView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
|
|
iconImageView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor),
|
|
|
|
titleRowStackView.leadingAnchor.constraint(equalTo: iconBackgroundView.trailingAnchor, constant: 12),
|
|
titleRowStackView.trailingAnchor.constraint(lessThanOrEqualTo: activeDotView.leadingAnchor, constant: -10),
|
|
titleRowStackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10),
|
|
|
|
descriptionLabel.leadingAnchor.constraint(equalTo: titleRowStackView.leadingAnchor),
|
|
descriptionLabel.trailingAnchor.constraint(lessThanOrEqualTo: visibilityButton.leadingAnchor, constant: -12),
|
|
descriptionLabel.topAnchor.constraint(equalTo: titleRowStackView.bottomAnchor, constant: 4),
|
|
|
|
activeDotView.widthAnchor.constraint(equalToConstant: 8),
|
|
activeDotView.heightAnchor.constraint(equalToConstant: 8),
|
|
activeDotView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
|
activeDotView.trailingAnchor.constraint(equalTo: visibilityButton.leadingAnchor, constant: -14),
|
|
|
|
visibilityButton.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
|
visibilityButton.trailingAnchor.constraint(equalTo: handleImageView.leadingAnchor, constant: -12),
|
|
|
|
handleImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
|
handleImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
|
|
handleImageView.widthAnchor.constraint(equalToConstant: 16),
|
|
handleImageView.heightAnchor.constraint(equalToConstant: 16)
|
|
])
|
|
}
|
|
|
|
@objc
|
|
private func handleVisibilityToggle(_ sender: NSButton) {
|
|
visibilityHandler?(sender.state == .on)
|
|
}
|
|
|
|
private func configureReleaseChannelBadge(_ rawValue: String?) {
|
|
releaseChannelBadgeView.rootView = PluginReleaseChannelBadge(releaseChannel: rawValue)
|
|
releaseChannelBadgeView.isHidden = PluginReleaseChannel(rawString: rawValue) == nil
|
|
}
|
|
}
|
|
|
|
private func featureManagementPresentationText(for presentation: PluginFeaturePresentation) -> String {
|
|
switch presentation {
|
|
case .featurePanel:
|
|
return AppL10n.plugins("plugin.presentation.featurePanel", defaultValue: "操作面板")
|
|
case .componentPanel:
|
|
return AppL10n.plugins("plugin.presentation.componentPanel", defaultValue: "组件")
|
|
case .featureAndComponentPanel:
|
|
return AppL10n.plugins("plugin.presentation.featureAndComponentPanel", defaultValue: "操作面板与组件")
|
|
}
|
|
}
|
|
|
|
private extension NSImage {
|
|
func tinted(with color: NSColor) -> NSImage {
|
|
let image = NSImage(size: size)
|
|
image.lockFocus()
|
|
defer { image.unlockFocus() }
|
|
|
|
let rect = NSRect(origin: .zero, size: size)
|
|
color.setFill()
|
|
rect.fill()
|
|
draw(in: rect, from: rect, operation: .destinationIn, fraction: 1)
|
|
|
|
image.isTemplate = false
|
|
return image
|
|
}
|
|
}
|