diff --git a/Clipy/Sources/Constants.swift b/Clipy/Sources/Constants.swift index 87cafef..13bb5ed 100644 --- a/Clipy/Sources/Constants.swift +++ b/Clipy/Sources/Constants.swift @@ -72,6 +72,7 @@ struct Constants { static let pasteCounts = "kCPYPasteCounts" static let boardManUsePanelUI = "BoardManUsePanelUI" static let boardManShowRowNumbers = "BoardManShowRowNumbers" + static let boardManHistoryUsageFilter = "BoardManHistoryUsageFilter" static let boardManTimestampFormat = "BoardManTimestampFormat" static let boardManPanelHeight = "BoardManPanelHeight" static let boardManShowUsageCount = "BoardManShowUsageCount" diff --git a/Clipy/Sources/Extensions/Realm+Migration.swift b/Clipy/Sources/Extensions/Realm+Migration.swift index 7facfc7..da02f9e 100644 --- a/Clipy/Sources/Extensions/Realm+Migration.swift +++ b/Clipy/Sources/Extensions/Realm+Migration.swift @@ -53,6 +53,19 @@ extension Realm { } } } + if oldSchemaVersion < 8 { + migration.enumerateObjects(ofType: CPYClip.className()) { oldObject, newObject in + let updateTime = oldObject?["updateTime"] as? Int ?? 0 + var createdTime = updateTime * 1000 + if let dataPath = oldObject?["dataPath"] as? String, + !dataPath.isEmpty, + let attributes = try? FileManager.default.attributesOfItem(atPath: dataPath), + let fileDate = (attributes[.creationDate] as? Date) ?? (attributes[.modificationDate] as? Date) { + createdTime = Int(fileDate.timeIntervalSince1970 * 1000) + } + newObject?["createdTime"] = createdTime + } + } }) Realm.Configuration.defaultConfiguration = config @@ -73,7 +86,7 @@ enum LegacySnippetMigrationResult: Equatable { } enum LegacySnippetMigrationService { - static let schemaVersion: UInt64 = 7 + static let schemaVersion: UInt64 = 8 private struct SnippetSnapshot { let index: Int diff --git a/Clipy/Sources/Managers/MenuManager.swift b/Clipy/Sources/Managers/MenuManager.swift index f92a8f9..aafe793 100644 --- a/Clipy/Sources/Managers/MenuManager.swift +++ b/Clipy/Sources/Managers/MenuManager.swift @@ -233,10 +233,11 @@ extension MenuManager { fileprivate func boardManHistoryItems() -> [BoardManHistoryItem] { let startedAt = CFAbsoluteTimeGetCurrent() - let maxHistory = max(1, AppEnvironment.current.defaults.integer(forKey: Constants.UserDefaults.maxHistorySize)) - let ascending = !AppEnvironment.current.defaults.bool(forKey: Constants.UserDefaults.reorderClipsAfterPasting) - let clipResults = realm.objects(CPYClip.self).sorted(byKeyPath: #keyPath(CPYClip.updateTime), ascending: ascending) let defaults = AppEnvironment.current.defaults + let maxHistory = max(1, defaults.integer(forKey: Constants.UserDefaults.maxHistorySize)) + let usesRecentOrder = defaults.bool(forKey: Constants.UserDefaults.reorderClipsAfterPasting) + let sortKeyPath = usesRecentOrder ? #keyPath(CPYClip.updateTime) : #keyPath(CPYClip.createdTime) + let clipResults = realm.objects(CPYClip.self).sorted(byKeyPath: sortKeyPath, ascending: false) let showRowNumbers = defaults.object(forKey: Constants.UserDefaults.boardManShowRowNumbers) as? Bool ?? true let timestampFormat = BoardManPanel.allowedTimestampFormat(defaults.string(forKey: Constants.UserDefaults.boardManTimestampFormat)) let showUsageCount = defaults.object(forKey: Constants.UserDefaults.boardManShowUsageCount) as? Bool ?? true @@ -564,8 +565,9 @@ private extension MenuManager { var subMenuCount = placeInLine var subMenuIndex = 1 + placeInLine - let ascending = !AppEnvironment.current.defaults.bool(forKey: Constants.UserDefaults.reorderClipsAfterPasting) - let clipResults = realm.objects(CPYClip.self).sorted(byKeyPath: #keyPath(CPYClip.updateTime), ascending: ascending) + let usesRecentOrder = AppEnvironment.current.defaults.bool(forKey: Constants.UserDefaults.reorderClipsAfterPasting) + let sortKeyPath = usesRecentOrder ? #keyPath(CPYClip.updateTime) : #keyPath(CPYClip.createdTime) + let clipResults = realm.objects(CPYClip.self).sorted(byKeyPath: sortKeyPath, ascending: false) let currentSize = Int(clipResults.count) var i = 0 for clip in clipResults { @@ -1063,6 +1065,40 @@ fileprivate enum BoardManUIStyle: String, CaseIterable { } } +fileprivate enum BoardManHistoryUsageFilter: String, CaseIterable { + case all = "All" + case unused = "Unused" + case used = "Used" + + static func allowed(_ value: String?) -> BoardManHistoryUsageFilter { + return allCases.first(where: { $0.rawValue == value }) ?? .all + } + + var symbolName: String { + switch self { + case .all: return "square.stack.3d.up" + case .unused: return "circle" + case .used: return "checkmark.circle.fill" + } + } + + var toolTip: String { + switch self { + case .all: return "All — show every clipboard history item (default)." + case .unused: return "Unused — show items that have not been pasted yet." + case .used: return "Used — show items pasted at least once." + } + } + + func includes(_ item: BoardManHistoryItem) -> Bool { + switch self { + case .all: return true + case .unused: return item.pasteCount == 0 + case .used: return item.pasteCount > 0 + } + } +} + fileprivate enum BoardManFontChoice: String, CaseIterable { case system = "System" case rounded = "Rounded" @@ -1923,6 +1959,8 @@ class BoardManPanel: NSPanel { private var glassBackgroundView: NSVisualEffectView? private var searchField: NSSearchField? private var segmentedControl: BoardManHeaderSegmentedControl? + private var historyUsageFilterControl: NSSegmentedControl? + private var historySortButton: NSButton? private var settingsBackgroundView: NSView? private var settingsSidebarView: NSView? private var settingsCategoryButtons: [NSButton] = [] @@ -2488,6 +2526,40 @@ class BoardManPanel: NSPanel { contentView.addSubview(tabs) segmentedControl = tabs + let historyFilter = NSSegmentedControl(frame: .zero) + historyFilter.segmentCount = BoardManHistoryUsageFilter.allCases.count + historyFilter.trackingMode = .selectOne + historyFilter.segmentStyle = .rounded + historyFilter.controlSize = .small + historyFilter.target = self + historyFilter.action = #selector(historyUsageFilterChanged(_:)) + let selectedUsageFilter = BoardManHistoryUsageFilter.allowed( + AppEnvironment.current.defaults.string(forKey: Constants.UserDefaults.boardManHistoryUsageFilter) + ) + for (index, filter) in BoardManHistoryUsageFilter.allCases.enumerated() { + historyFilter.setLabel(filter.rawValue, forSegment: index) + historyFilter.setToolTip(filter.toolTip, forSegment: index) + if #available(macOS 11.0, *) { + historyFilter.setLabel("", forSegment: index) + historyFilter.setImage(NSImage(systemSymbolName: filter.symbolName, accessibilityDescription: filter.rawValue), forSegment: index) + } + } + historyFilter.selectedSegment = BoardManHistoryUsageFilter.allCases.firstIndex(of: selectedUsageFilter) ?? 0 + historyFilter.setAccessibilityLabel("History usage filter") + contentView.addSubview(historyFilter) + historyUsageFilterControl = historyFilter + + let historySort = NSButton(title: "", target: self, action: #selector(historySortOrderChanged(_:))) + historySort.setButtonType(.toggle) + historySort.bezelStyle = .texturedRounded + historySort.controlSize = .small + historySort.imagePosition = .imageOnly + historySort.state = AppEnvironment.current.defaults.bool(forKey: Constants.UserDefaults.reorderClipsAfterPasting) ? .on : .off + historySort.setAccessibilityLabel("History sort order") + contentView.addSubview(historySort) + historySortButton = historySort + updateHistorySortButton() + let settingsBackground = NSView(frame: .zero) settingsBackground.wantsLayer = true settingsBackground.layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor @@ -3341,6 +3413,7 @@ class BoardManPanel: NSPanel { ? surfaceTint.withAlphaComponent(0.26) : NSColor.controlBackgroundColor.withAlphaComponent(0.78) segmentedControl?.needsDisplay = true + updateHistorySortButton() settingsSidebarView?.layer?.backgroundColor = (useGlass ? surfaceTint.withAlphaComponent(0.28) : NSColor.controlBackgroundColor.withAlphaComponent(simpleStyle ? 0.42 : 0.72)).cgColor @@ -3510,6 +3583,22 @@ class BoardManPanel: NSPanel { updateSnippetActionButtons() let contentTop = headerY - 24 + let showsHistoryToolbar = activeTab == .history && !isSettings + historyUsageFilterControl?.isHidden = !showsHistoryToolbar + historySortButton?.isHidden = !showsHistoryToolbar + let historyToolbarY = contentTop - 30 + if showsHistoryToolbar { + let filterWidth: CGFloat = 114 + historyUsageFilterControl?.frame = NSRect(x: margin, y: historyToolbarY, width: filterWidth, height: 26) + if let filter = historyUsageFilterControl { + let segmentWidth = floor(filterWidth / CGFloat(max(1, filter.segmentCount))) + for segment in 0..= 0 && usageFilter.selectedSegment < 3) + for segment in 0..