Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 378cad4bf1 | |||
| f9644d4afd | |||
| 1980c9dbc1 | |||
| 6085cbd217 |
@@ -106,6 +106,8 @@ Rates are shared across queries and refreshed on the next currency query after t
|
||||
|
||||
Search for **Conjure Emoji**, identified by its centered 🤔 icon, and open it. Browse the grid or search by name, keyword, `:shortcode:`, or emoji. Use the category menu to filter results or show **Recently Used**. Your last 30 selections are saved locally. Favorites appear first, followed by recent selections when browsing All Emoji.
|
||||
|
||||
Square 64-point emoji tiles use compact 1-point grid spacing and show the same subtle rounded border used elsewhere in Conjure only when selected or hovered; the selected tile retains its accent fill.
|
||||
|
||||
Use the arrow keys or Control-H/J/K/L to navigate left/down/up/right; the grid scrolls to keep the selected emoji visible. Return, clicking an emoji, or **Copy and Paste** copies it, dismisses Conjure, and tries to paste into the app that was focused when the launcher opened. Automatic pasting requires **System Settings → Privacy & Security → Accessibility → Conjure**; macOS prompts if permission is missing. After granting it, select an emoji again. If permission is denied, the previous app is unavailable, or focus cannot be restored promptly, the emoji stays copied for manual Command-V. The Back chevron returns to the launcher and restores your search.
|
||||
|
||||
If macOS keeps prompting despite an enabled Accessibility entry, quit Conjure, remove its stale entry with the minus button, add the exact app you launch (normally `/Applications/Conjure.app`), enable it, then reopen Conjure and retry. Avoid switching between installed and `.build` copies. For permission that survives rebuilds, configure certificate signing as described above. **Copy Emoji** followed by manual Command-V works without this permission.
|
||||
|
||||
@@ -37,9 +37,9 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
category.target = self
|
||||
category.action = #selector(categoryChanged)
|
||||
let layout = NSCollectionViewFlowLayout()
|
||||
layout.itemSize = NSSize(width: 64, height: 56)
|
||||
layout.minimumInteritemSpacing = 6
|
||||
layout.minimumLineSpacing = 6
|
||||
layout.itemSize = NSSize(width: 64, height: 64)
|
||||
layout.minimumInteritemSpacing = 1
|
||||
layout.minimumLineSpacing = 1
|
||||
layout.sectionInset = NSEdgeInsets(top: LauncherScrollView.fadeHeight, left: 16, bottom: 12, right: 16)
|
||||
collection.collectionViewLayout = layout
|
||||
collection.backgroundColors = [.clear]
|
||||
@@ -149,7 +149,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
collection.scrollToItems(at: [indexPath(for: selected)], scrollPosition: .nearestHorizontalEdge)
|
||||
}
|
||||
|
||||
private var columns: Int { max(1, Int((collection.bounds.width - 32 + 6) / 70)) }
|
||||
private var columns: Int { max(1, Int((collection.bounds.width - 32 + 1) / 65)) }
|
||||
|
||||
private func indexPath(for index: Int) -> IndexPath {
|
||||
splitFavorites && index >= favoriteCount
|
||||
@@ -284,7 +284,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
|
||||
@MainActor
|
||||
private final class EmojiItem: NSCollectionViewItem {
|
||||
let button = NSButton()
|
||||
let button = EmojiButton()
|
||||
var clicked: (() -> Void)?
|
||||
|
||||
override func loadView() {
|
||||
@@ -294,13 +294,40 @@ private final class EmojiItem: NSCollectionViewItem {
|
||||
button.action = #selector(copyEmoji)
|
||||
button.wantsLayer = true
|
||||
button.layer?.cornerRadius = 10
|
||||
button.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
view = button
|
||||
}
|
||||
|
||||
func highlight(_ selected: Bool) {
|
||||
button.layer?.backgroundColor = selected ? SunsetPalette.accent.withAlphaComponent(0.22).cgColor : nil
|
||||
button.setAccessibilitySelected(selected)
|
||||
button.selected = selected
|
||||
}
|
||||
|
||||
@objc private func copyEmoji() { clicked?() }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class EmojiButton: NSButton {
|
||||
var selected = false { didSet { updateBorder() } }
|
||||
private var hovered = false { didSet { updateBorder() } }
|
||||
private var hoverTracking: NSTrackingArea?
|
||||
|
||||
override func updateTrackingAreas() {
|
||||
super.updateTrackingAreas()
|
||||
if let hoverTracking { removeTrackingArea(hoverTracking) }
|
||||
let tracking = NSTrackingArea(rect: .zero,
|
||||
options: [.mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect],
|
||||
owner: self, userInfo: nil)
|
||||
addTrackingArea(tracking)
|
||||
hoverTracking = tracking
|
||||
hovered = window.map { visibleRect.contains(convert($0.mouseLocationOutsideOfEventStream, from: nil)) } ?? false
|
||||
}
|
||||
|
||||
override func mouseEntered(with event: NSEvent) { hovered = true }
|
||||
override func mouseExited(with event: NSEvent) { hovered = false }
|
||||
|
||||
private func updateBorder() {
|
||||
layer?.borderWidth = selected || hovered ? 0.5 : 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
- support switching between notes
|
||||
- highlight markdown syntax
|
||||
- allow images
|
||||
- [ ] put the border already in use on some elements around the emoji containers in the emoji picker.
|
||||
- [x] put the border already in use on some elements around the emoji containers in the emoji picker.
|
||||
- [ ] improve on the conjure translation command
|
||||
@@ -0,0 +1,43 @@
|
||||
import AppKit
|
||||
import Testing
|
||||
@testable import Conjure
|
||||
|
||||
@Test @MainActor func emojiBordersFollowSelectionAndHover() throws {
|
||||
_ = NSApplication.shared
|
||||
let suite = "EmojiBorders.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let picker = EmojiPickerController(defaults: defaults)
|
||||
picker.input = { "face" }
|
||||
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 400),
|
||||
styleMask: [.borderless], backing: .buffered, defer: false)
|
||||
window.contentView = picker.view
|
||||
picker.view.layoutSubtreeIfNeeded()
|
||||
let scroll = try #require(picker.view.subviews.compactMap { $0 as? NSScrollView }.first)
|
||||
let grid = try #require(scroll.documentView as? NSCollectionView)
|
||||
let first = try #require(grid.item(at: IndexPath(item: 0, section: 0))?.view as? NSButton)
|
||||
let second = try #require(grid.item(at: IndexPath(item: 1, section: 0))?.view as? NSButton)
|
||||
let enter = try #require(NSEvent.enterExitEvent(with: .mouseEntered, location: .zero,
|
||||
modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber,
|
||||
context: nil, eventNumber: 0, trackingNumber: 0, userData: nil))
|
||||
let exit = try #require(NSEvent.enterExitEvent(with: .mouseExited, location: .zero,
|
||||
modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber,
|
||||
context: nil, eventNumber: 0, trackingNumber: 0, userData: nil))
|
||||
first.mouseExited(with: exit)
|
||||
second.mouseExited(with: exit)
|
||||
#expect(first.layer?.borderWidth == 0.5)
|
||||
#expect(second.layer?.borderWidth == 0)
|
||||
second.mouseEntered(with: enter)
|
||||
#expect(second.layer?.borderWidth == 0.5)
|
||||
picker.moveSelection(by: 1)
|
||||
#expect(first.layer?.borderWidth == 0)
|
||||
second.mouseExited(with: exit)
|
||||
#expect(second.layer?.borderWidth == 0.5)
|
||||
picker.moveSelection(by: -1)
|
||||
#expect(second.layer?.borderWidth == 0)
|
||||
first.mouseEntered(with: enter)
|
||||
picker.moveSelection(by: 1)
|
||||
#expect(first.layer?.borderWidth == 0.5)
|
||||
first.mouseExited(with: exit)
|
||||
#expect(first.layer?.borderWidth == 0)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import AppKit
|
||||
import ConjureKit
|
||||
import Testing
|
||||
@testable import Conjure
|
||||
|
||||
@Test @MainActor func emojiDownNavigationFollowsRenderedColumns() throws {
|
||||
_ = NSApplication.shared
|
||||
let suite = "EmojiGrid.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
let pasteboard = NSPasteboard.withUniqueName()
|
||||
defer { defaults.removePersistentDomain(forName: suite); pasteboard.releaseGlobally() }
|
||||
let picker = EmojiPickerController(defaults: defaults, pasteboard: pasteboard)
|
||||
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 400),
|
||||
styleMask: [.borderless], backing: .buffered, defer: false)
|
||||
window.contentView = picker.view
|
||||
picker.view.layoutSubtreeIfNeeded()
|
||||
let scroll = try #require(picker.view.subviews.compactMap { $0 as? NSScrollView }.first)
|
||||
let grid = try #require(scroll.documentView as? NSCollectionView)
|
||||
let layout = try #require(grid.collectionViewLayout)
|
||||
let first = try #require(layout.layoutAttributesForItem(at: IndexPath(item: 0, section: 0)))
|
||||
let nextRow = try #require((1..<EmojiCatalog.all.count).first { index in
|
||||
guard let frame = layout.layoutAttributesForItem(at: IndexPath(item: index, section: 0))?.frame else { return false }
|
||||
return frame.minY > first.frame.minY
|
||||
})
|
||||
#expect(picker.handle(#selector(NSResponder.moveDown(_:))))
|
||||
picker.copySelection()
|
||||
#expect(pasteboard.string(forType: .string) == EmojiCatalog.all[nextRow].value)
|
||||
print("Emoji grid: \(nextRow) columns at 750 points; down navigation follows the rendered row.")
|
||||
}
|
||||
Reference in New Issue
Block a user