|
|
|
@@ -1,12 +1,47 @@
|
|
|
|
import AppKit
|
|
|
|
import AppKit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private struct ClipboardItemCardLayout: Equatable {
|
|
|
|
|
|
|
|
let width: CGFloat
|
|
|
|
|
|
|
|
let height: CGFloat
|
|
|
|
|
|
|
|
let inset: CGFloat
|
|
|
|
|
|
|
|
let headerHeight: CGFloat
|
|
|
|
|
|
|
|
let bodyHeight: CGFloat
|
|
|
|
|
|
|
|
let footerHeight: CGFloat
|
|
|
|
|
|
|
|
let actionButtonSize: CGFloat
|
|
|
|
|
|
|
|
let primaryActionButtonSize: CGFloat
|
|
|
|
|
|
|
|
let actionRailHeight: CGFloat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static let regular = ClipboardItemCardLayout(
|
|
|
|
|
|
|
|
width: 320,
|
|
|
|
|
|
|
|
height: 244,
|
|
|
|
|
|
|
|
inset: 16,
|
|
|
|
|
|
|
|
headerHeight: 56,
|
|
|
|
|
|
|
|
bodyHeight: 152,
|
|
|
|
|
|
|
|
footerHeight: 36,
|
|
|
|
|
|
|
|
actionButtonSize: 24,
|
|
|
|
|
|
|
|
primaryActionButtonSize: 30,
|
|
|
|
|
|
|
|
actionRailHeight: 34
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static let compact = ClipboardItemCardLayout(
|
|
|
|
|
|
|
|
width: 264,
|
|
|
|
|
|
|
|
height: 220,
|
|
|
|
|
|
|
|
inset: 13,
|
|
|
|
|
|
|
|
headerHeight: 50,
|
|
|
|
|
|
|
|
bodyHeight: 138,
|
|
|
|
|
|
|
|
footerHeight: 32,
|
|
|
|
|
|
|
|
actionButtonSize: 22,
|
|
|
|
|
|
|
|
primaryActionButtonSize: 28,
|
|
|
|
|
|
|
|
actionRailHeight: 32
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var isCompact: Bool {
|
|
|
|
|
|
|
|
self == Self.compact
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
private enum Metrics {
|
|
|
|
private enum Metrics {
|
|
|
|
static let cardRailHeight: CGFloat = 266
|
|
|
|
|
|
|
|
static let cardWidth: CGFloat = 320
|
|
|
|
|
|
|
|
static let cardHeight: CGFloat = 244
|
|
|
|
|
|
|
|
static let cardSpacing: CGFloat = 16
|
|
|
|
|
|
|
|
static let cardStackInset: CGFloat = 10
|
|
|
|
|
|
|
|
static let actionButtonSize: CGFloat = 30
|
|
|
|
static let actionButtonSize: CGFloat = 30
|
|
|
|
static let panelTopInset: CGFloat = 12
|
|
|
|
static let panelTopInset: CGFloat = 12
|
|
|
|
static let panelSideInset: CGFloat = 22
|
|
|
|
static let panelSideInset: CGFloat = 22
|
|
|
|
@@ -14,6 +49,49 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
static let panelStatusBarHeight: CGFloat = 24
|
|
|
|
static let panelStatusBarHeight: CGFloat = 24
|
|
|
|
static let minimumBottomInset: CGFloat = 20
|
|
|
|
static let minimumBottomInset: CGFloat = 20
|
|
|
|
static let panelCornerRadius: CGFloat = 0
|
|
|
|
static let panelCornerRadius: CGFloat = 0
|
|
|
|
|
|
|
|
static let compactCardThreshold: CGFloat = 760
|
|
|
|
|
|
|
|
static let emptyStateMinimumWidth: CGFloat = 760
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private enum CardDensity: String {
|
|
|
|
|
|
|
|
case regular
|
|
|
|
|
|
|
|
case compact
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static func fitting(width: CGFloat) -> CardDensity {
|
|
|
|
|
|
|
|
width > 0 && width < Metrics.compactCardThreshold ? .compact : .regular
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var layout: ClipboardItemCardLayout {
|
|
|
|
|
|
|
|
switch self {
|
|
|
|
|
|
|
|
case .regular: return .regular
|
|
|
|
|
|
|
|
case .compact: return .compact
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var cardSpacing: CGFloat {
|
|
|
|
|
|
|
|
switch self {
|
|
|
|
|
|
|
|
case .regular: return 16
|
|
|
|
|
|
|
|
case .compact: return 12
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var cardStackInset: CGFloat {
|
|
|
|
|
|
|
|
switch self {
|
|
|
|
|
|
|
|
case .regular: return 10
|
|
|
|
|
|
|
|
case .compact: return 8
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var railHeight: CGFloat {
|
|
|
|
|
|
|
|
layout.height + (cardStackInset * 2) + 2
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var emptyStateMinimumWidth: CGFloat {
|
|
|
|
|
|
|
|
switch self {
|
|
|
|
|
|
|
|
case .regular: return Metrics.emptyStateMinimumWidth
|
|
|
|
|
|
|
|
case .compact: return 420
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private enum Palette {
|
|
|
|
private enum Palette {
|
|
|
|
@@ -66,6 +144,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
private var mainStack: NSStackView?
|
|
|
|
private var mainStack: NSStackView?
|
|
|
|
private var bottomSafeInset = Metrics.minimumBottomInset
|
|
|
|
private var bottomSafeInset = Metrics.minimumBottomInset
|
|
|
|
private var currentStatusTone: StatusTone = .ready
|
|
|
|
private var currentStatusTone: StatusTone = .ready
|
|
|
|
|
|
|
|
private var cardDensity: CardDensity = .regular
|
|
|
|
|
|
|
|
private var scrollViewHeightConstraint: NSLayoutConstraint?
|
|
|
|
private var cardViews: [ClipboardItemCardView] = []
|
|
|
|
private var cardViews: [ClipboardItemCardView] = []
|
|
|
|
private var collectionButtons: [ClipboardSortMode: CollectionChipView] = [:]
|
|
|
|
private var collectionButtons: [ClipboardSortMode: CollectionChipView] = [:]
|
|
|
|
private var customCollectionButtons: [String: CollectionChipView] = [:]
|
|
|
|
private var customCollectionButtons: [String: CollectionChipView] = [:]
|
|
|
|
@@ -206,13 +286,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
|
|
|
|
|
|
|
|
itemsStack.orientation = .horizontal
|
|
|
|
itemsStack.orientation = .horizontal
|
|
|
|
itemsStack.alignment = .top
|
|
|
|
itemsStack.alignment = .top
|
|
|
|
itemsStack.spacing = Metrics.cardSpacing
|
|
|
|
applyCardDensity()
|
|
|
|
itemsStack.edgeInsets = NSEdgeInsets(
|
|
|
|
|
|
|
|
top: Metrics.cardStackInset,
|
|
|
|
|
|
|
|
left: Metrics.cardStackInset,
|
|
|
|
|
|
|
|
bottom: Metrics.cardStackInset,
|
|
|
|
|
|
|
|
right: Metrics.cardStackInset
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
itemsStack.translatesAutoresizingMaskIntoConstraints = true
|
|
|
|
itemsStack.translatesAutoresizingMaskIntoConstraints = true
|
|
|
|
scrollView.documentView = itemsStack
|
|
|
|
scrollView.documentView = itemsStack
|
|
|
|
scrollView.hasHorizontalScroller = true
|
|
|
|
scrollView.hasHorizontalScroller = true
|
|
|
|
@@ -223,7 +297,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
scrollView.borderType = .noBorder
|
|
|
|
scrollView.borderType = .noBorder
|
|
|
|
scrollView.setContentHuggingPriority(.required, for: .vertical)
|
|
|
|
scrollView.setContentHuggingPriority(.required, for: .vertical)
|
|
|
|
scrollView.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
|
|
scrollView.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
|
|
scrollView.heightAnchor.constraint(equalToConstant: Metrics.cardRailHeight).isActive = true
|
|
|
|
scrollViewHeightConstraint = scrollView.heightAnchor.constraint(equalToConstant: cardDensity.railHeight)
|
|
|
|
|
|
|
|
scrollViewHeightConstraint?.isActive = true
|
|
|
|
|
|
|
|
|
|
|
|
statusLabel.font = .systemFont(ofSize: NSFont.systemFontSize - 1)
|
|
|
|
statusLabel.font = .systemFont(ofSize: NSFont.systemFontSize - 1)
|
|
|
|
statusLabel.textColor = .secondaryLabelColor
|
|
|
|
statusLabel.textColor = .secondaryLabelColor
|
|
|
|
@@ -474,6 +549,29 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private func applyCardDensity() {
|
|
|
|
|
|
|
|
itemsStack.spacing = cardDensity.cardSpacing
|
|
|
|
|
|
|
|
let inset = cardDensity.cardStackInset
|
|
|
|
|
|
|
|
itemsStack.edgeInsets = NSEdgeInsets(
|
|
|
|
|
|
|
|
top: inset,
|
|
|
|
|
|
|
|
left: inset,
|
|
|
|
|
|
|
|
bottom: inset,
|
|
|
|
|
|
|
|
right: inset
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
scrollViewHeightConstraint?.constant = cardDensity.railHeight
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@discardableResult
|
|
|
|
|
|
|
|
private func updateCardDensityForCurrentWidth() -> Bool {
|
|
|
|
|
|
|
|
let targetDensity = CardDensity.fitting(width: bounds.width)
|
|
|
|
|
|
|
|
guard targetDensity != cardDensity else { return false }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cardDensity = targetDensity
|
|
|
|
|
|
|
|
applyCardDensity()
|
|
|
|
|
|
|
|
reloadItems()
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func contentInsets() -> NSEdgeInsets {
|
|
|
|
private func contentInsets() -> NSEdgeInsets {
|
|
|
|
NSEdgeInsets(
|
|
|
|
NSEdgeInsets(
|
|
|
|
top: Metrics.panelTopInset,
|
|
|
|
top: Metrics.panelTopInset,
|
|
|
|
@@ -531,11 +629,13 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
scrollView.documentView = itemsStack
|
|
|
|
scrollView.documentView = itemsStack
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let collectionNames = viewModel.collectionNames
|
|
|
|
let collectionNames = viewModel.collectionNames
|
|
|
|
|
|
|
|
let layout = cardDensity.layout
|
|
|
|
for (index, item) in items.enumerated() {
|
|
|
|
for (index, item) in items.enumerated() {
|
|
|
|
let card = ClipboardItemCardView(
|
|
|
|
let card = ClipboardItemCardView(
|
|
|
|
item: item,
|
|
|
|
item: item,
|
|
|
|
thumbnail: viewModel.thumbnail(for: item),
|
|
|
|
thumbnail: viewModel.thumbnail(for: item),
|
|
|
|
index: index,
|
|
|
|
index: index,
|
|
|
|
|
|
|
|
layout: layout,
|
|
|
|
collectionNames: collectionNames,
|
|
|
|
collectionNames: collectionNames,
|
|
|
|
isStacked: viewModel.isItemStacked(at: index),
|
|
|
|
isStacked: viewModel.isItemStacked(at: index),
|
|
|
|
stackCount: viewModel.stackCount
|
|
|
|
stackCount: viewModel.stackCount
|
|
|
|
@@ -689,7 +789,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
itemsStack.layoutSubtreeIfNeeded()
|
|
|
|
itemsStack.layoutSubtreeIfNeeded()
|
|
|
|
|
|
|
|
|
|
|
|
let frame = card.convert(card.bounds, to: itemsStack)
|
|
|
|
let frame = card.convert(card.bounds, to: itemsStack)
|
|
|
|
let paddedFrame = frame.insetBy(dx: -Metrics.cardSpacing, dy: 0)
|
|
|
|
let paddedFrame = frame.insetBy(dx: -cardDensity.cardSpacing, dy: 0)
|
|
|
|
itemsStack.scrollToVisible(paddedFrame)
|
|
|
|
itemsStack.scrollToVisible(paddedFrame)
|
|
|
|
scrollView.reflectScrolledClipView(scrollView.contentView)
|
|
|
|
scrollView.reflectScrolledClipView(scrollView.contentView)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -780,8 +880,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func emptyStateView() -> NSView {
|
|
|
|
private func emptyStateView() -> NSView {
|
|
|
|
let width = max(760, scrollView.contentView.bounds.width)
|
|
|
|
let width = max(cardDensity.emptyStateMinimumWidth, scrollView.contentView.bounds.width)
|
|
|
|
let container = NSView(frame: NSRect(x: 0, y: 0, width: width, height: Metrics.cardRailHeight))
|
|
|
|
let container = NSView(frame: NSRect(x: 0, y: 0, width: width, height: cardDensity.railHeight))
|
|
|
|
let copy = emptyStateCopy()
|
|
|
|
let copy = emptyStateCopy()
|
|
|
|
let title = NSTextField(labelWithString: copy.title)
|
|
|
|
let title = NSTextField(labelWithString: copy.title)
|
|
|
|
title.font = .systemFont(ofSize: 14, weight: .medium)
|
|
|
|
title.font = .systemFont(ofSize: 14, weight: .medium)
|
|
|
|
@@ -811,9 +911,9 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
|
|
|
|
|
|
|
|
private func sizeItemsDocument(itemCount: Int) {
|
|
|
|
private func sizeItemsDocument(itemCount: Int) {
|
|
|
|
let count = CGFloat(itemCount)
|
|
|
|
let count = CGFloat(itemCount)
|
|
|
|
let contentWidth = (count * Metrics.cardWidth)
|
|
|
|
let contentWidth = (count * cardDensity.layout.width)
|
|
|
|
+ max(0, count - 1) * Metrics.cardSpacing
|
|
|
|
+ max(0, count - 1) * cardDensity.cardSpacing
|
|
|
|
+ (Metrics.cardStackInset * 2)
|
|
|
|
+ (cardDensity.cardStackInset * 2)
|
|
|
|
let width = max(scrollView.contentView.bounds.width, contentWidth)
|
|
|
|
let width = max(scrollView.contentView.bounds.width, contentWidth)
|
|
|
|
lastScrollContentWidth = width
|
|
|
|
lastScrollContentWidth = width
|
|
|
|
itemsStack.frame = NSRect(x: 0, y: 0, width: width, height: currentListHeight())
|
|
|
|
itemsStack.frame = NSRect(x: 0, y: 0, width: width, height: currentListHeight())
|
|
|
|
@@ -822,7 +922,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func currentListHeight() -> CGFloat {
|
|
|
|
private func currentListHeight() -> CGFloat {
|
|
|
|
Metrics.cardHeight + (Metrics.cardStackInset * 2)
|
|
|
|
cardDensity.layout.height + (cardDensity.cardStackInset * 2)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func emptyStateCopy() -> (title: String, detail: String) {
|
|
|
|
private func emptyStateCopy() -> (title: String, detail: String) {
|
|
|
|
@@ -897,6 +997,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
|
|
|
|
|
|
|
|
override func layout() {
|
|
|
|
override func layout() {
|
|
|
|
super.layout()
|
|
|
|
super.layout()
|
|
|
|
|
|
|
|
_ = updateCardDensityForCurrentWidth()
|
|
|
|
let collectionViewportWidth = collectionScrollView.contentView.bounds.width
|
|
|
|
let collectionViewportWidth = collectionScrollView.contentView.bounds.width
|
|
|
|
if collectionViewportWidth != lastCollectionViewportWidth {
|
|
|
|
if collectionViewportWidth != lastCollectionViewportWidth {
|
|
|
|
lastCollectionViewportWidth = collectionViewportWidth
|
|
|
|
lastCollectionViewportWidth = collectionViewportWidth
|
|
|
|
@@ -913,7 +1014,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
if cardViews.isEmpty {
|
|
|
|
if cardViews.isEmpty {
|
|
|
|
guard let documentView = scrollView.documentView else { return }
|
|
|
|
guard let documentView = scrollView.documentView else { return }
|
|
|
|
documentView.frame.size = NSSize(
|
|
|
|
documentView.frame.size = NSSize(
|
|
|
|
width: max(760, scrollView.contentView.bounds.width),
|
|
|
|
width: max(cardDensity.emptyStateMinimumWidth, scrollView.contentView.bounds.width),
|
|
|
|
height: currentListHeight()
|
|
|
|
height: currentListHeight()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
@@ -997,6 +1098,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|
|
|
cardViews.map(\.debugPreviewStyle)
|
|
|
|
cardViews.map(\.debugPreviewStyle)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var debugCardDensity: String {
|
|
|
|
|
|
|
|
cardDensity.rawValue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var debugCardSizes: [NSSize] {
|
|
|
|
|
|
|
|
cardViews.map { $0.frame.size }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var debugCardHeaderBadgeSymbols: [String] {
|
|
|
|
var debugCardHeaderBadgeSymbols: [String] {
|
|
|
|
cardViews.map(\.debugHeaderBadgeSymbol)
|
|
|
|
cardViews.map(\.debugHeaderBadgeSymbol)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1464,15 +1573,6 @@ private final class AspectFillImageView: NSView {
|
|
|
|
|
|
|
|
|
|
|
|
private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
private enum Metrics {
|
|
|
|
private enum Metrics {
|
|
|
|
static let width: CGFloat = 320
|
|
|
|
|
|
|
|
static let height: CGFloat = 244
|
|
|
|
|
|
|
|
static let inset: CGFloat = 16
|
|
|
|
|
|
|
|
static let headerHeight: CGFloat = 56
|
|
|
|
|
|
|
|
static let bodyHeight: CGFloat = 152
|
|
|
|
|
|
|
|
static let footerHeight: CGFloat = 36
|
|
|
|
|
|
|
|
static let actionButtonSize: CGFloat = 24
|
|
|
|
|
|
|
|
static let primaryActionButtonSize: CGFloat = 30
|
|
|
|
|
|
|
|
static let actionRailHeight: CGFloat = 34
|
|
|
|
|
|
|
|
static let dragThreshold: CGFloat = 4
|
|
|
|
static let dragThreshold: CGFloat = 4
|
|
|
|
}
|
|
|
|
}
|
|
|
|
private enum Palette {
|
|
|
|
private enum Palette {
|
|
|
|
@@ -1507,6 +1607,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
|
|
|
|
|
|
|
|
private let index: Int
|
|
|
|
private let index: Int
|
|
|
|
private let itemID: UUID
|
|
|
|
private let itemID: UUID
|
|
|
|
|
|
|
|
private let layout: ClipboardItemCardLayout
|
|
|
|
private let itemKind: ClipboardItemKind
|
|
|
|
private let itemKind: ClipboardItemKind
|
|
|
|
private let itemIsPinned: Bool
|
|
|
|
private let itemIsPinned: Bool
|
|
|
|
private let itemIsStacked: Bool
|
|
|
|
private let itemIsStacked: Bool
|
|
|
|
@@ -1531,12 +1632,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
item: ClipboardItem,
|
|
|
|
item: ClipboardItem,
|
|
|
|
thumbnail: NSImage?,
|
|
|
|
thumbnail: NSImage?,
|
|
|
|
index: Int,
|
|
|
|
index: Int,
|
|
|
|
|
|
|
|
layout: ClipboardItemCardLayout = .regular,
|
|
|
|
collectionNames: [String] = [],
|
|
|
|
collectionNames: [String] = [],
|
|
|
|
isStacked: Bool = false,
|
|
|
|
isStacked: Bool = false,
|
|
|
|
stackCount: Int = 0
|
|
|
|
stackCount: Int = 0
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
self.index = index
|
|
|
|
self.index = index
|
|
|
|
self.itemID = item.id
|
|
|
|
self.itemID = item.id
|
|
|
|
|
|
|
|
self.layout = layout
|
|
|
|
self.itemKind = item.kind
|
|
|
|
self.itemKind = item.kind
|
|
|
|
self.itemIsPinned = item.isPinned
|
|
|
|
self.itemIsPinned = item.isPinned
|
|
|
|
self.itemIsStacked = isStacked
|
|
|
|
self.itemIsStacked = isStacked
|
|
|
|
@@ -1895,7 +1998,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
actionRail.spacing = 4
|
|
|
|
actionRail.spacing = 4
|
|
|
|
actionRail.edgeInsets = NSEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
|
|
|
|
actionRail.edgeInsets = NSEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
|
|
|
|
actionRail.wantsLayer = true
|
|
|
|
actionRail.wantsLayer = true
|
|
|
|
actionRail.layer?.cornerRadius = Metrics.actionRailHeight / 2
|
|
|
|
actionRail.layer?.cornerRadius = layout.actionRailHeight / 2
|
|
|
|
actionRail.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.44).cgColor
|
|
|
|
actionRail.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.44).cgColor
|
|
|
|
actionRail.layer?.borderWidth = 0.5
|
|
|
|
actionRail.layer?.borderWidth = 0.5
|
|
|
|
actionRail.layer?.borderColor = NSColor.white.withAlphaComponent(0.18).cgColor
|
|
|
|
actionRail.layer?.borderColor = NSColor.white.withAlphaComponent(0.18).cgColor
|
|
|
|
@@ -1904,7 +2007,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
actionRail.layer?.shadowRadius = 10
|
|
|
|
actionRail.layer?.shadowRadius = 10
|
|
|
|
actionRail.layer?.shadowOffset = NSSize(width: 0, height: 4)
|
|
|
|
actionRail.layer?.shadowOffset = NSSize(width: 0, height: 4)
|
|
|
|
actionRail.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
actionRail.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
actionRail.heightAnchor.constraint(equalToConstant: Metrics.actionRailHeight).isActive = true
|
|
|
|
actionRail.heightAnchor.constraint(equalToConstant: layout.actionRailHeight).isActive = true
|
|
|
|
actionRail.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
actionRail.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
actionRail.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
|
actionRail.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1934,8 +2037,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let buttonCount = CGFloat(actionRailButtons.count)
|
|
|
|
let buttonCount = CGFloat(actionRailButtons.count)
|
|
|
|
let secondaryCount = CGFloat(max(0, actionRailButtons.count - 1))
|
|
|
|
let secondaryCount = CGFloat(max(0, actionRailButtons.count - 1))
|
|
|
|
let contentWidth = Metrics.primaryActionButtonSize
|
|
|
|
let contentWidth = layout.primaryActionButtonSize
|
|
|
|
+ secondaryCount * Metrics.actionButtonSize
|
|
|
|
+ secondaryCount * layout.actionButtonSize
|
|
|
|
+ max(0, buttonCount - 1) * actionRail.spacing
|
|
|
|
+ max(0, buttonCount - 1) * actionRail.spacing
|
|
|
|
+ actionRail.edgeInsets.left
|
|
|
|
+ actionRail.edgeInsets.left
|
|
|
|
+ actionRail.edgeInsets.right
|
|
|
|
+ actionRail.edgeInsets.right
|
|
|
|
@@ -1957,7 +2060,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
button.imageScaling = .scaleProportionallyDown
|
|
|
|
button.imageScaling = .scaleProportionallyDown
|
|
|
|
button.isBordered = false
|
|
|
|
button.isBordered = false
|
|
|
|
button.wantsLayer = true
|
|
|
|
button.wantsLayer = true
|
|
|
|
let size = isPrimary ? Metrics.primaryActionButtonSize : Metrics.actionButtonSize
|
|
|
|
let size = isPrimary ? layout.primaryActionButtonSize : layout.actionButtonSize
|
|
|
|
button.layer?.cornerRadius = size / 2
|
|
|
|
button.layer?.cornerRadius = size / 2
|
|
|
|
button.layer?.backgroundColor = isPrimary
|
|
|
|
button.layer?.backgroundColor = isPrimary
|
|
|
|
? NSColor.controlAccentColor.cgColor
|
|
|
|
? NSColor.controlAccentColor.cgColor
|
|
|
|
@@ -2094,8 +2197,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
setAccessibilityRole(.button)
|
|
|
|
setAccessibilityRole(.button)
|
|
|
|
setAccessibilityLabel(accessibilityTitle(for: item))
|
|
|
|
setAccessibilityLabel(accessibilityTitle(for: item))
|
|
|
|
setAccessibilityHelp("Selects this clipboard item. Double-click to paste.")
|
|
|
|
setAccessibilityHelp("Selects this clipboard item. Double-click to paste.")
|
|
|
|
widthAnchor.constraint(equalToConstant: Metrics.width).isActive = true
|
|
|
|
widthAnchor.constraint(equalToConstant: layout.width).isActive = true
|
|
|
|
heightAnchor.constraint(equalToConstant: Metrics.height).isActive = true
|
|
|
|
heightAnchor.constraint(equalToConstant: layout.height).isActive = true
|
|
|
|
|
|
|
|
|
|
|
|
contentView.wantsLayer = true
|
|
|
|
contentView.wantsLayer = true
|
|
|
|
contentView.layer?.cornerRadius = 8
|
|
|
|
contentView.layer?.cornerRadius = 8
|
|
|
|
@@ -2142,17 +2245,17 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
let header = NSView()
|
|
|
|
let header = NSView()
|
|
|
|
header.wantsLayer = true
|
|
|
|
header.wantsLayer = true
|
|
|
|
header.layer?.backgroundColor = accentColor(for: item.kind).cgColor
|
|
|
|
header.layer?.backgroundColor = accentColor(for: item.kind).cgColor
|
|
|
|
header.heightAnchor.constraint(equalToConstant: Metrics.headerHeight).isActive = true
|
|
|
|
header.heightAnchor.constraint(equalToConstant: layout.headerHeight).isActive = true
|
|
|
|
|
|
|
|
|
|
|
|
let kind = NSTextField(labelWithString: kindLabel(for: item.kind))
|
|
|
|
let kind = NSTextField(labelWithString: kindLabel(for: item.kind))
|
|
|
|
kind.font = .systemFont(ofSize: 16, weight: .bold)
|
|
|
|
kind.font = .systemFont(ofSize: layout.isCompact ? 15 : 16, weight: .bold)
|
|
|
|
kind.textColor = .white
|
|
|
|
kind.textColor = .white
|
|
|
|
kind.lineBreakMode = .byTruncatingTail
|
|
|
|
kind.lineBreakMode = .byTruncatingTail
|
|
|
|
kind.maximumNumberOfLines = 1
|
|
|
|
kind.maximumNumberOfLines = 1
|
|
|
|
kind.toolTip = kind.stringValue
|
|
|
|
kind.toolTip = kind.stringValue
|
|
|
|
|
|
|
|
|
|
|
|
let source = NSTextField(labelWithString: Self.relativeDateText(for: item.createdAt))
|
|
|
|
let source = NSTextField(labelWithString: Self.relativeDateText(for: item.createdAt))
|
|
|
|
source.font = .systemFont(ofSize: 11, weight: .regular)
|
|
|
|
source.font = .systemFont(ofSize: layout.isCompact ? 10 : 11, weight: .regular)
|
|
|
|
source.textColor = NSColor.white.withAlphaComponent(0.72)
|
|
|
|
source.textColor = NSColor.white.withAlphaComponent(0.72)
|
|
|
|
source.lineBreakMode = .byTruncatingTail
|
|
|
|
source.lineBreakMode = .byTruncatingTail
|
|
|
|
source.maximumNumberOfLines = 1
|
|
|
|
source.maximumNumberOfLines = 1
|
|
|
|
@@ -2191,15 +2294,15 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
source.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
source.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
|
|
|
|
|
|
|
var constraints: [NSLayoutConstraint] = [
|
|
|
|
var constraints: [NSLayoutConstraint] = [
|
|
|
|
labelStack.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: Metrics.inset),
|
|
|
|
labelStack.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: layout.inset),
|
|
|
|
labelStack.centerYAnchor.constraint(equalTo: header.centerYAnchor),
|
|
|
|
labelStack.centerYAnchor.constraint(equalTo: header.centerYAnchor),
|
|
|
|
labelStack.trailingAnchor.constraint(lessThanOrEqualTo: badge.leadingAnchor, constant: -12),
|
|
|
|
labelStack.trailingAnchor.constraint(lessThanOrEqualTo: badge.leadingAnchor, constant: -12),
|
|
|
|
badge.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
badge.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -layout.inset),
|
|
|
|
badge.centerYAnchor.constraint(equalTo: header.centerYAnchor),
|
|
|
|
badge.centerYAnchor.constraint(equalTo: header.centerYAnchor),
|
|
|
|
badge.widthAnchor.constraint(equalToConstant: 42),
|
|
|
|
badge.widthAnchor.constraint(equalToConstant: layout.isCompact ? 36 : 42),
|
|
|
|
badge.heightAnchor.constraint(equalToConstant: 42),
|
|
|
|
badge.heightAnchor.constraint(equalToConstant: layout.isCompact ? 36 : 42),
|
|
|
|
separator.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: Metrics.inset),
|
|
|
|
separator.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: layout.inset),
|
|
|
|
separator.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
separator.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -layout.inset),
|
|
|
|
separator.bottomAnchor.constraint(equalTo: header.bottomAnchor),
|
|
|
|
separator.bottomAnchor.constraint(equalTo: header.bottomAnchor),
|
|
|
|
separator.heightAnchor.constraint(equalToConstant: 1)
|
|
|
|
separator.heightAnchor.constraint(equalToConstant: 1)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
@@ -2246,7 +2349,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
let body = NSView()
|
|
|
|
let body = NSView()
|
|
|
|
body.wantsLayer = true
|
|
|
|
body.wantsLayer = true
|
|
|
|
body.layer?.backgroundColor = Palette.bodyBackground
|
|
|
|
body.layer?.backgroundColor = Palette.bodyBackground
|
|
|
|
body.heightAnchor.constraint(equalToConstant: Metrics.bodyHeight).isActive = true
|
|
|
|
body.heightAnchor.constraint(equalToConstant: layout.bodyHeight).isActive = true
|
|
|
|
|
|
|
|
|
|
|
|
let content = previewView(for: item, thumbnail: thumbnail)
|
|
|
|
let content = previewView(for: item, thumbnail: thumbnail)
|
|
|
|
body.addSubview(content)
|
|
|
|
body.addSubview(content)
|
|
|
|
@@ -2311,8 +2414,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
title.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
|
title.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
|
detail.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
|
detail.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
|
|
|
|
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
|
|
|
|
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
|
|
|
|
stack.topAnchor.constraint(equalTo: container.topAnchor, constant: 16),
|
|
|
|
stack.topAnchor.constraint(equalTo: container.topAnchor, constant: 16),
|
|
|
|
stack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -14)
|
|
|
|
stack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -14)
|
|
|
|
])
|
|
|
|
])
|
|
|
|
@@ -2377,8 +2480,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
globe.widthAnchor.constraint(equalToConstant: 28),
|
|
|
|
globe.widthAnchor.constraint(equalToConstant: 28),
|
|
|
|
globe.heightAnchor.constraint(equalToConstant: 28),
|
|
|
|
globe.heightAnchor.constraint(equalToConstant: 28),
|
|
|
|
host.widthAnchor.constraint(lessThanOrEqualTo: hero.widthAnchor, constant: -48),
|
|
|
|
host.widthAnchor.constraint(lessThanOrEqualTo: hero.widthAnchor, constant: -48),
|
|
|
|
textStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
|
|
|
|
textStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
|
|
|
|
textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
|
|
|
|
textStack.topAnchor.constraint(equalTo: hero.bottomAnchor, constant: 11),
|
|
|
|
textStack.topAnchor.constraint(equalTo: hero.bottomAnchor, constant: 11),
|
|
|
|
textStack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -10),
|
|
|
|
textStack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -10),
|
|
|
|
title.widthAnchor.constraint(equalTo: textStack.widthAnchor),
|
|
|
|
title.widthAnchor.constraint(equalTo: textStack.widthAnchor),
|
|
|
|
@@ -2427,8 +2530,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
imageView.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
|
imageView.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
|
hostPill.leadingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: 12),
|
|
|
|
hostPill.leadingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: 12),
|
|
|
|
hostPill.bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -10),
|
|
|
|
hostPill.bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -10),
|
|
|
|
textStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
|
|
|
|
textStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
|
|
|
|
textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
|
|
|
|
textStack.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 10),
|
|
|
|
textStack.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 10),
|
|
|
|
textStack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -8),
|
|
|
|
textStack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -8),
|
|
|
|
title.widthAnchor.constraint(equalTo: textStack.widthAnchor),
|
|
|
|
title.widthAnchor.constraint(equalTo: textStack.widthAnchor),
|
|
|
|
@@ -2495,8 +2598,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
container.addSubview(row)
|
|
|
|
container.addSubview(row)
|
|
|
|
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
row.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
|
|
|
|
row.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
|
|
|
|
row.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
row.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
|
|
|
|
row.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
|
|
|
row.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
|
|
|
iconBox.widthAnchor.constraint(equalToConstant: thumbnail == nil ? 72 : 96),
|
|
|
|
iconBox.widthAnchor.constraint(equalToConstant: thumbnail == nil ? 72 : 96),
|
|
|
|
iconBox.heightAnchor.constraint(equalToConstant: thumbnail == nil ? 84 : 104),
|
|
|
|
iconBox.heightAnchor.constraint(equalToConstant: thumbnail == nil ? 84 : 104),
|
|
|
|
@@ -2567,8 +2670,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
note.heightAnchor.constraint(equalToConstant: 28),
|
|
|
|
note.heightAnchor.constraint(equalToConstant: 28),
|
|
|
|
waveform.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
|
|
waveform.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
|
|
waveform.topAnchor.constraint(equalTo: note.bottomAnchor, constant: 8),
|
|
|
|
waveform.topAnchor.constraint(equalTo: note.bottomAnchor, constant: 8),
|
|
|
|
labels.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
|
|
|
|
labels.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
|
|
|
|
labels.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
labels.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
|
|
|
|
labels.topAnchor.constraint(equalTo: waveform.bottomAnchor, constant: 10),
|
|
|
|
labels.topAnchor.constraint(equalTo: waveform.bottomAnchor, constant: 10),
|
|
|
|
title.widthAnchor.constraint(equalTo: labels.widthAnchor),
|
|
|
|
title.widthAnchor.constraint(equalTo: labels.widthAnchor),
|
|
|
|
detail.widthAnchor.constraint(equalTo: labels.widthAnchor)
|
|
|
|
detail.widthAnchor.constraint(equalTo: labels.widthAnchor)
|
|
|
|
@@ -2668,7 +2771,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
let footer = NSView()
|
|
|
|
let footer = NSView()
|
|
|
|
footer.wantsLayer = true
|
|
|
|
footer.wantsLayer = true
|
|
|
|
footer.layer?.backgroundColor = Palette.footerBackground
|
|
|
|
footer.layer?.backgroundColor = Palette.footerBackground
|
|
|
|
footer.heightAnchor.constraint(equalToConstant: Metrics.footerHeight).isActive = true
|
|
|
|
footer.heightAnchor.constraint(equalToConstant: layout.footerHeight).isActive = true
|
|
|
|
|
|
|
|
|
|
|
|
let source = NSTextField(labelWithString: sourceText(for: item))
|
|
|
|
let source = NSTextField(labelWithString: sourceText(for: item))
|
|
|
|
source.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .medium)
|
|
|
|
source.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .medium)
|
|
|
|
@@ -2707,12 +2810,12 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|
|
|
footer.addSubview(divider)
|
|
|
|
footer.addSubview(divider)
|
|
|
|
footer.addSubview(stack)
|
|
|
|
footer.addSubview(stack)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
divider.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: Metrics.inset),
|
|
|
|
divider.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: layout.inset),
|
|
|
|
divider.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
divider.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -layout.inset),
|
|
|
|
divider.topAnchor.constraint(equalTo: footer.topAnchor),
|
|
|
|
divider.topAnchor.constraint(equalTo: footer.topAnchor),
|
|
|
|
divider.heightAnchor.constraint(equalToConstant: 1),
|
|
|
|
divider.heightAnchor.constraint(equalToConstant: 1),
|
|
|
|
stack.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: Metrics.inset),
|
|
|
|
stack.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: layout.inset),
|
|
|
|
stack.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -Metrics.inset),
|
|
|
|
stack.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -layout.inset),
|
|
|
|
stack.centerYAnchor.constraint(equalTo: footer.centerYAnchor)
|
|
|
|
stack.centerYAnchor.constraint(equalTo: footer.centerYAnchor)
|
|
|
|
])
|
|
|
|
])
|
|
|
|
return footer
|
|
|
|
return footer
|
|
|
|
|