Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion Maccy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@
DAFE2DDA268A521B00990986 /* String+Shortened.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAFE2DD9268A521A00990986 /* String+Shortened.swift */; };
DAFE2DE9268A9B1B00990986 /* HistoryItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAFE2DE8268A9B1B00990986 /* HistoryItemTests.swift */; };
DAFEF0B8249D7DEE006029E8 /* KeyboardShortcuts.Name+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAFEF0B7249D7DEE006029E8 /* KeyboardShortcuts.Name+Shortcuts.swift */; };
AA00000100000001AAAAAAAA /* Workspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000200000001AAAAAAAA /* Workspace.swift */; };
AA00000100000002AAAAAAAA /* WorkspaceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000200000002AAAAAAAA /* WorkspaceManager.swift */; };
AA00000100000003AAAAAAAA /* WorkspaceTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000200000003AAAAAAAA /* WorkspaceTabsView.swift */; };
AA00000100000004AAAAAAAA /* WorkspaceSettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000200000004AAAAAAAA /* WorkspaceSettingsPane.swift */; };
AA00000100000005AAAAAAAA /* WorkspaceSettings.strings in Resources */ = {isa = PBXBuildFile; fileRef = AA00000300000005AAAAAAAA /* WorkspaceSettings.strings */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -555,6 +560,11 @@
DAFE2DD9268A521A00990986 /* String+Shortened.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Shortened.swift"; sourceTree = "<group>"; };
DAFE2DE8268A9B1B00990986 /* HistoryItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItemTests.swift; sourceTree = "<group>"; };
DAFEF0B7249D7DEE006029E8 /* KeyboardShortcuts.Name+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyboardShortcuts.Name+Shortcuts.swift"; sourceTree = "<group>"; };
AA00000200000001AAAAAAAA /* Workspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workspace.swift; sourceTree = "<group>"; };
AA00000200000002AAAAAAAA /* WorkspaceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceManager.swift; sourceTree = "<group>"; };
AA00000200000003AAAAAAAA /* WorkspaceTabsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceTabsView.swift; sourceTree = "<group>"; };
AA00000200000004AAAAAAAA /* WorkspaceSettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSettingsPane.swift; sourceTree = "<group>"; };
AA00000200000005AAAAAAAA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/WorkspaceSettings.strings; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -634,6 +644,7 @@
children = (
DA05B5132C234DCF006980FE /* HistoryItem.swift */,
DA05B5122C234DCF006980FE /* HistoryItemContent.swift */,
AA00000200000001AAAAAAAA /* Workspace.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -662,6 +673,8 @@
DA689FAE2C1CD3B00009B887 /* StorageSettings.strings */,
DAA365CD2C4BF9DD00A394F8 /* PinsSettingsPane.swift */,
DAA365CF2C4C147400A394F8 /* PinsSettings.strings */,
AA00000200000004AAAAAAAA /* WorkspaceSettingsPane.swift */,
AA00000300000005AAAAAAAA /* WorkspaceSettings.strings */,
);
path = Settings;
sourceTree = "<group>";
Expand Down Expand Up @@ -699,6 +712,7 @@
2F9B42F02F1A4999006548E5 /* ToolbarView.swift */,
DAA072D02C4089D3006DDFD2 /* VisualEffectView.swift */,
DA555F112CF155A2009608BD /* WrappingTextView.swift */,
AA00000200000003AAAAAAAA /* WorkspaceTabsView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -740,6 +754,7 @@
DA3BCB8D2C3E015000B01BC1 /* ModifierFlags.swift */,
DAE8F5D32C43262B00851CA9 /* Popup.swift */,
2F578C3E2EFFE8720088B759 /* SlideoutController.swift */,
AA00000200000002AAAAAAAA /* WorkspaceManager.swift */,
);
path = Observables;
sourceTree = "<group>";
Expand Down Expand Up @@ -1039,6 +1054,7 @@
DA5E62802C39E53F00F4C710 /* PreviewItemView.strings in Resources */,
DA009932256411F90030E697 /* README.md in Resources */,
DA9C3C4A2C20D4B40056795D /* IgnoreSettings.strings in Resources */,
AA00000100000005AAAAAAAA /* WorkspaceSettings.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1176,6 +1192,10 @@
DA7A753E26A52F0F00DC16EF /* NSImage+Names.swift in Sources */,
2F0EC7852E774083003E2EA9 /* PasteStackView.swift in Sources */,
DA20FA782B082E1A00056DD5 /* NSSound+Named.swift in Sources */,
AA00000100000001AAAAAAAA /* Workspace.swift in Sources */,
AA00000100000002AAAAAAAA /* WorkspaceManager.swift in Sources */,
AA00000100000003AAAAAAAA /* WorkspaceTabsView.swift in Sources */,
AA00000100000004AAAAAAAA /* WorkspaceSettingsPane.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1499,6 +1519,14 @@
name = GeneralSettings.strings;
sourceTree = "<group>";
};
AA00000300000005AAAAAAAA /* WorkspaceSettings.strings */ = {
isa = PBXVariantGroup;
children = (
AA00000200000005AAAAAAAA /* en */,
);
name = WorkspaceSettings.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */

/* Begin XCBuildConfiguration section */
Expand Down Expand Up @@ -1957,4 +1985,4 @@
/* End XCSwiftPackageProductDependency section */
};
rootObject = DAEE383B1E3DBEB100DD2966 /* Project object */;
}
}
6 changes: 5 additions & 1 deletion Maccy/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Bridge FloatingPanel via AppDelegate.
AppState.shared.appDelegate = self

// Ensure default workspace exists and migrate orphaned items before clipboard starts.
Storage.shared.ensureDefaultWorkspace()
WorkspaceManager.shared.load()

Clipboard.shared.onNewCopy { History.shared.add($0) }
Clipboard.shared.start()

Expand Down Expand Up @@ -109,7 +113,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

func applicationWillTerminate(_ notification: Notification) {
if Defaults[.clearOnQuit] {
AppState.shared.history.clear()
AppState.shared.history.clearEverything()
}
}

Expand Down
1 change: 1 addition & 0 deletions Maccy/Extensions/Defaults.Keys+Names.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct StorageType {
}

extension Defaults.Keys {
static let activeWorkspaceId = Key<String?>("activeWorkspaceId")
static let clearOnQuit = Key<Bool>("clearOnQuit", default: false)
static let clearSystemClipboard = Key<Bool>("clearSystemClipboard", default: false)
static let clipboardCheckInterval = Key<Double>("clipboardCheckInterval", default: 0.5)
Expand Down
1 change: 1 addition & 0 deletions Maccy/Extensions/NSImage+Names.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ extension NSImage {
static let pincircle = NSImage(systemSymbolName: "pin.circle", accessibilityDescription: "pin.cirlce")
static let nosign = NSImage(systemSymbolName: "nosign", accessibilityDescription: "nosign")
static let gearshape2 = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: "gearshape2")
static let squareStack = NSImage(systemSymbolName: "square.stack.3d.up", accessibilityDescription: "workspaces")
}

extension NSImage.Name {
Expand Down
1 change: 1 addition & 0 deletions Maccy/Extensions/Settings.PaneIdentifier+Panes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ extension Settings.PaneIdentifier {
static let ignore = Self("ignore")
static let pins = Self("pins")
static let storage = Self("storage")
static let workspaces = Self("workspaces")
}
3 changes: 3 additions & 0 deletions Maccy/Models/HistoryItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class HistoryItem {
@Relationship(deleteRule: .cascade, inverse: \HistoryItemContent.item)
var contents: [HistoryItemContent] = []

@Relationship(inverse: \Workspace.items)
var workspace: Workspace?

init(contents: [HistoryItemContent] = []) {
self.firstCopiedAt = firstCopiedAt
self.lastCopiedAt = lastCopiedAt
Expand Down
22 changes: 22 additions & 0 deletions Maccy/Models/Workspace.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation
import SwiftData

@Model
class Workspace {
static let defaultName = "Default"

var id: UUID
var name: String = Workspace.defaultName
var createdAt: Date
var sortOrder: Int = 0

@Relationship(deleteRule: .nullify)
var items: [HistoryItem] = []

init(name: String, sortOrder: Int = 0) {
self.id = UUID()
self.name = name
self.sortOrder = sortOrder
self.createdAt = Date.now
}
}
11 changes: 11 additions & 0 deletions Maccy/Observables/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ class AppState: Sendable {
static let shared = AppState(history: History.shared, footer: Footer())

let multiSelectionEnabled = false
var isPopoverEditing = false

var appDelegate: AppDelegate?
var popup: Popup
var history: History
var footer: Footer
var navigator: NavigationManager
var preview: SlideoutController
var workspaceManager: WorkspaceManager

var searchVisible: Bool {
if !Defaults[.showSearch] { return false }
Expand All @@ -40,6 +42,7 @@ class AppState: Sendable {
self.footer = footer
popup = Popup()
navigator = NavigationManager(history: history, footer: footer)
workspaceManager = WorkspaceManager.shared
preview = SlideoutController(
onContentResize: { contentWidth in
Defaults[.windowSize].width = contentWidth
Expand Down Expand Up @@ -124,6 +127,14 @@ class AppState: Sendable {
) {
StorageSettingsPane()
},
Settings.Pane(
identifier: Settings.PaneIdentifier.workspaces,
title: NSLocalizedString("Title", tableName: "WorkspaceSettings", comment: ""),
toolbarIcon: NSImage.squareStack!
) {
WorkspaceSettingsPane()
.environment(self)
},
Settings.Pane(
identifier: Settings.PaneIdentifier.appearance,
title: NSLocalizedString("Title", tableName: "AppearanceSettings", comment: ""),
Expand Down
70 changes: 56 additions & 14 deletions Maccy/Observables/History.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length

@MainActor
func load() async throws {
let descriptor = FetchDescriptor<HistoryItem>()
guard let workspaceId = WorkspaceManager.shared.activeWorkspace?.id else { return }

let descriptor = FetchDescriptor<HistoryItem>(
predicate: #Predicate { $0.workspace?.id == workspaceId }
)

let results = try Storage.shared.context.fetch(descriptor)
all = sorter.sort(results).map { HistoryItemDecorator($0) }
items = all
Expand Down Expand Up @@ -136,6 +141,11 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length
@discardableResult
@MainActor
func add(_ item: HistoryItem) -> HistoryItemDecorator {
// Assign the item to the currently active workspace.
if item.workspace == nil {
item.workspace = WorkspaceManager.shared.activeWorkspace
}

if #available(macOS 15.0, *) {
try? History.shared.insertIntoStorage(item)
} else {
Expand Down Expand Up @@ -217,20 +227,16 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length
cleanup(item)
}
}

let unpinnedToDelete = all.filter(\.isUnpinned)
for item in unpinnedToDelete {
Storage.shared.context.delete(item.item)
}

all.removeAll(where: \.isUnpinned)
sessionLog.removeValues { $0.pin == nil }
items = all

try? Storage.shared.context.transaction {
try? Storage.shared.context.delete(
model: HistoryItem.self,
where: #Predicate { $0.pin == nil }
)
try? Storage.shared.context.delete(
model: HistoryItemContent.self,
where: #Predicate { $0.item?.pin == nil }
)
}
Storage.shared.context.processPendingChanges()
try? Storage.shared.context.save()
}
Expand All @@ -248,11 +254,15 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length
all.forEach { item in
cleanup(item)
}

for item in all {
Storage.shared.context.delete(item.item)
}

all.removeAll()
sessionLog.removeAll()
items = all

try? Storage.shared.context.delete(model: HistoryItem.self)
Storage.shared.context.processPendingChanges()
try? Storage.shared.context.save()
}
Expand All @@ -264,6 +274,33 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length
}
}

/// Clears history items in the active workspace. Preserves other workspaces' data.
/// Fetches directly from storage since `all` may not be loaded at quit time.
@MainActor
func clearEverything() {
guard let workspaceId = WorkspaceManager.shared.activeWorkspace?.id else { return }

withLogging("Clearing active workspace history on quit") {
all.forEach { cleanup($0) }
all.removeAll()
sessionLog.removeAll()
items = all

let descriptor = FetchDescriptor<HistoryItem>(
predicate: #Predicate { $0.workspace?.id == workspaceId }
)
if let toDelete = try? Storage.shared.context.fetch(descriptor) {
for item in toDelete {
Storage.shared.context.delete(item)
}
}
Storage.shared.context.processPendingChanges()
try? Storage.shared.context.save()
}

Clipboard.shared.clear()
}

@MainActor
func delete(_ item: HistoryItemDecorator?) {
guard let item else { return }
Expand Down Expand Up @@ -445,7 +482,12 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length

@MainActor
private func findSimilarItem(_ item: HistoryItem) -> HistoryItem? {
let descriptor = FetchDescriptor<HistoryItem>()
guard let workspaceId = WorkspaceManager.shared.activeWorkspace?.id else { return nil }

let descriptor = FetchDescriptor<HistoryItem>(
predicate: #Predicate { $0.workspace?.id == workspaceId }
)

if let all = try? Storage.shared.context.fetch(descriptor) {
let duplicates = all.filter({ $0 == item || $0.supersedes(item) })
if duplicates.count > 1 {
Expand All @@ -455,7 +497,7 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length
}
}

return item
return nil
}

private func isModified(_ item: HistoryItem) -> HistoryItem? {
Expand Down
6 changes: 4 additions & 2 deletions Maccy/Observables/Popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Popup {
var extraTopHeight: CGFloat = 0
var extraBottomHeight: CGFloat = 0
var footerHeight: CGFloat = 0
var workspaceTabsHeight: CGFloat = 0

private var eventsMonitor: Any?

Expand Down Expand Up @@ -97,15 +98,16 @@ class Popup {
if AppState.shared.preview.state.isOpen && AppState.shared.navigator.leadSelection != nil {
minimumHeight += Self.minimumPreviewHeight
}
minimumHeight = max(headerHeight + Self.verticalPadding, minimumHeight)
let chromeHeight = headerHeight + workspaceTabsHeight + footerHeight + Self.verticalPadding
minimumHeight = max(chromeHeight, minimumHeight)

height = max(height, minimumHeight)
height = min(height, Defaults[.windowSize].height)
return height
}

func resize(height: CGFloat) {
self.height = height + headerHeight + extraTopHeight + extraBottomHeight + footerHeight
self.height = height + headerHeight + workspaceTabsHeight + extraTopHeight + extraBottomHeight + footerHeight
AppState.shared.appDelegate?.panel.verticallyResize(to: preferredHeight(for: self.height))
needsResize = false
}
Expand Down
Loading