feat: separate favorite application suggestions
This commit is contained in:
+1
-1
@@ -8,6 +8,6 @@ let package = Package(
|
||||
targets: [
|
||||
.target(name: "ConjureKit"),
|
||||
.executableTarget(name: "Conjure", dependencies: ["ConjureKit"]),
|
||||
.testTarget(name: "ConjureKitTests", dependencies: ["ConjureKit"]),
|
||||
.testTarget(name: "ConjureKitTests", dependencies: ["ConjureKit", "Conjure"]),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -71,7 +71,7 @@ Search is case- and accent-insensitive. Ranking is exact name, full-name prefix,
|
||||
|
||||
Words are runs of letters or numbers separated by spaces, punctuation, or symbols. For example, `settings` matches the second word of System Settings, `gc` matches Google Chrome, and `ss` matches System Settings. Initials must occur in order but can skip words: both `vsc` and `vc` match Visual Studio Code.
|
||||
|
||||
With an empty search, favorites appear first. Within both favorite and non-favorite groups, applications are ordered by successful launch count, then most recent launch, then alphabetically. Favorites and usage are stored locally by application bundle identity and survive restarts; copies with the same name remain independent. Only launches through Conjure count; failed launches do not. Applications without history remain alphabetical within their group.
|
||||
With an empty search, favorites appear first, separated from the remaining applications by a 12-point gap when both groups are present. Typed searches have no group spacing. Within both favorite and non-favorite groups, applications are ordered by successful launch count, then most recent launch, then alphabetically. Favorites and usage are stored locally by application bundle identity and survive restarts; copies with the same name remain independent. Only launches through Conjure count; failed launches do not. Applications without history remain alphabetical within their group.
|
||||
|
||||
Background discovery preserves the selected application even if its row changes. If that application disappears, the first remaining result is selected. Changing the query or reopening the launcher selects the first result.
|
||||
|
||||
|
||||
@@ -527,6 +527,18 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
resultCount
|
||||
}
|
||||
|
||||
private func spacingBeforeRow(_ row: Int) -> CGFloat {
|
||||
guard calculation == nil,
|
||||
searchField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
row > 0, results.indices.contains(row),
|
||||
history.isFavorite(results[row - 1]), !history.isFavorite(results[row]) else { return 0 }
|
||||
return 12
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
|
||||
LauncherRowView.rowHeight + spacingBeforeRow(row)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||
let identifier = NSUserInterfaceItemIdentifier("applicationCell")
|
||||
let cell = (tableView.makeView(withIdentifier: identifier, owner: self) as? ApplicationCell)
|
||||
@@ -564,6 +576,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
let view = (tableView.makeView(withIdentifier: identifier, owner: self) as? LauncherRowView)
|
||||
?? LauncherRowView()
|
||||
view.identifier = identifier
|
||||
view.topSpacing = spacingBeforeRow(row)
|
||||
return view
|
||||
}
|
||||
|
||||
@@ -1058,9 +1071,22 @@ final class LauncherRowView: NSTableRowView {
|
||||
static let textSize: CGFloat = 14
|
||||
static let iconSize: CGFloat = 22
|
||||
static let iconToTextSpacing: CGFloat = 12
|
||||
var topSpacing: CGFloat = 0 {
|
||||
didSet { needsLayout = true; needsDisplay = true }
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
for case let cell as ApplicationCell in subviews {
|
||||
cell.frame = NSRect(x: 0, y: topSpacing, width: bounds.width, height: bounds.height - topSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
override func drawSelection(in dirtyRect: NSRect) {
|
||||
let selection = NSBezierPath(roundedRect: bounds.insetBy(dx: 0.75, dy: 0.75), xRadius: 10, yRadius: 10)
|
||||
var selectionBounds = bounds
|
||||
selectionBounds.origin.y += topSpacing
|
||||
selectionBounds.size.height -= topSpacing
|
||||
let selection = NSBezierPath(roundedRect: selectionBounds.insetBy(dx: 0.75, dy: 0.75), xRadius: 10, yRadius: 10)
|
||||
SunsetPalette.selectionFill.setFill()
|
||||
selection.fill()
|
||||
let increasedContrast = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
- [x] Add Favorite / Remove Favorite actions and persist favorites by application bundle.
|
||||
- Empty search: favorites first, then usage ranking within each group.
|
||||
- Typed search: match quality first, then favorites, launch count, recency, and alphabetical order.
|
||||
- [x] add a star icon to the left of the "Application" type text in the selection entries for favorited items
|
||||
- [ ] add some spacing between the favorites grouping and the rest of the applications
|
||||
@@ -0,0 +1,18 @@
|
||||
import AppKit
|
||||
import Testing
|
||||
@testable import Conjure
|
||||
|
||||
@Test @MainActor
|
||||
func favoriteSpacingLeavesApplicationContentAtNormalHeightAndResetsOnReuse() {
|
||||
_ = NSApplication.shared
|
||||
let row = LauncherRowView(frame: NSRect(x: 0, y: 0, width: 650, height: 50))
|
||||
let cell = ApplicationCell(frame: row.bounds)
|
||||
row.addSubview(cell)
|
||||
row.topSpacing = 12
|
||||
row.layoutSubtreeIfNeeded()
|
||||
#expect(cell.frame == NSRect(x: 0, y: 12, width: 650, height: 38))
|
||||
row.frame.size.height = 38
|
||||
row.topSpacing = 0
|
||||
row.layoutSubtreeIfNeeded()
|
||||
#expect(cell.frame == NSRect(x: 0, y: 0, width: 650, height: 38))
|
||||
}
|
||||
Reference in New Issue
Block a user