Skip to content

Commit c82a20a

Browse files
Merge pull request #47 from DeveloperAcademy-POSTECH/feat/#46-snap-shortcut-selection
Feat/#46 snap shortcut selection
2 parents 3658527 + 84464d0 commit c82a20a

1 file changed

Lines changed: 159 additions & 17 deletions

File tree

JeonstarLab Mac/Views/MacSnapEventListView.swift

Lines changed: 159 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55

66
import SwiftUI
7+
import AppKit
78

89
struct MacSnapEventListView: View {
910
let events: [WorkingSnapEvent]
@@ -63,13 +64,19 @@ struct MacSnapEventListView: View {
6364
}
6465

6566
HStack(alignment: .top, spacing: 10) {
66-
Picker("라벨", selection: labelBinding(for: key)) {
67-
ForEach(RecordingPackageLabel.allCases) { label in
68-
Text(label.displayName).tag(label)
69-
}
67+
HStack(spacing: 6) {
68+
Text("라벨")
69+
.font(.caption)
70+
.foregroundStyle(.secondary)
71+
.frame(width: 28, alignment: .leading)
72+
73+
NumberShortcutMenuButton(
74+
title: currentLabel(for: event).displayName,
75+
options: labelShortcutOptions(for: key)
76+
)
77+
.frame(width: 132)
7078
}
71-
.pickerStyle(.menu)
72-
.frame(width: 130)
79+
.padding(.top, 1)
7380

7481
TextField("스냅 노트", text: notesBinding(for: key), axis: .vertical)
7582
.textFieldStyle(.roundedBorder)
@@ -141,22 +148,33 @@ struct MacSnapEventListView: View {
141148
.font(.caption)
142149
.foregroundStyle(.secondary)
143150
} else {
144-
Menu("폴더에 추가") {
145-
if folders.isEmpty {
146-
Text("생성된 폴더가 없습니다.")
147-
} else {
148-
ForEach(folders) { folder in
149-
Button(folder.name) {
150-
onAddToFolder(event, folder)
151-
}
152-
}
153-
}
154-
}
151+
NumberShortcutMenuButton(
152+
title: "폴더에 추가",
153+
emptyMessage: "생성된 폴더가 없습니다.",
154+
options: folderShortcutOptions(for: event)
155+
)
156+
.frame(width: 104)
155157
}
156158
}
157159
}
158160
}
159161

162+
private func labelShortcutOptions(for key: String) -> [NumberShortcutMenuOption] {
163+
RecordingPackageLabel.allCases.enumerated().map { index, label in
164+
NumberShortcutMenuOption(index: index + 1, title: label.displayName) {
165+
labelBinding(for: key).wrappedValue = label
166+
}
167+
}
168+
}
169+
170+
private func folderShortcutOptions(for event: WorkingSnapEvent) -> [NumberShortcutMenuOption] {
171+
folders.enumerated().map { index, folder in
172+
NumberShortcutMenuOption(index: index + 1, title: folder.name) {
173+
onAddToFolder(event, folder)
174+
}
175+
}
176+
}
177+
160178
private func currentLabel(for event: WorkingSnapEvent) -> RecordingPackageLabel {
161179
snapEventLabels[event.snapID]?.label ?? event.label
162180
}
@@ -203,3 +221,127 @@ struct MacSnapEventListView: View {
203221
return String(format: "%.2f%@", locale: Locale(identifier: "en_US_POSIX"), value, suffix)
204222
}
205223
}
224+
225+
private struct NumberShortcutMenuOption: Identifiable {
226+
let index: Int
227+
let title: String
228+
let action: () -> Void
229+
230+
var id: Int { index }
231+
var hasShortcut: Bool { (1...9).contains(index) }
232+
}
233+
234+
private struct NumberShortcutMenuButton: View {
235+
let title: String
236+
var emptyMessage: String = "선택할 항목이 없습니다."
237+
let options: [NumberShortcutMenuOption]
238+
239+
@State private var isPresented = false
240+
241+
var body: some View {
242+
Button {
243+
isPresented = true
244+
} label: {
245+
HStack(spacing: 6) {
246+
Text(title)
247+
.lineLimit(1)
248+
Image(systemName: "chevron.down")
249+
.font(.caption2)
250+
.foregroundStyle(.secondary)
251+
}
252+
.frame(maxWidth: .infinity)
253+
}
254+
.buttonStyle(.bordered)
255+
.controlSize(.small)
256+
.popover(isPresented: $isPresented, arrowEdge: .bottom) {
257+
NumberShortcutMenuContent(
258+
emptyMessage: emptyMessage,
259+
options: options,
260+
isPresented: $isPresented
261+
)
262+
}
263+
}
264+
}
265+
266+
private struct NumberShortcutMenuContent: View {
267+
let emptyMessage: String
268+
let options: [NumberShortcutMenuOption]
269+
@Binding var isPresented: Bool
270+
271+
@State private var keyMonitor: Any?
272+
273+
var body: some View {
274+
VStack(alignment: .leading, spacing: 4) {
275+
if options.isEmpty {
276+
Text(emptyMessage)
277+
.font(.callout)
278+
.foregroundStyle(.secondary)
279+
.padding(.horizontal, 10)
280+
.padding(.vertical, 8)
281+
} else {
282+
ForEach(options) { option in
283+
Button {
284+
select(option)
285+
} label: {
286+
HStack {
287+
Text(option.title)
288+
.lineLimit(1)
289+
Spacer()
290+
if option.hasShortcut {
291+
Text("\(option.index)")
292+
.font(.caption)
293+
.foregroundStyle(.secondary)
294+
}
295+
}
296+
.contentShape(Rectangle())
297+
}
298+
.buttonStyle(.plain)
299+
.padding(.horizontal, 10)
300+
.padding(.vertical, 5)
301+
}
302+
}
303+
}
304+
.frame(minWidth: 180, alignment: .leading)
305+
.padding(.vertical, 6)
306+
.onAppear(perform: installKeyMonitor)
307+
.onDisappear(perform: removeKeyMonitor)
308+
}
309+
310+
private func select(_ option: NumberShortcutMenuOption) {
311+
option.action()
312+
isPresented = false
313+
}
314+
315+
private func installKeyMonitor() {
316+
removeKeyMonitor()
317+
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
318+
guard let number = shortcutNumber(from: event),
319+
let option = options.first(where: { $0.index == number }) else {
320+
return event
321+
}
322+
323+
select(option)
324+
return nil
325+
}
326+
}
327+
328+
private func removeKeyMonitor() {
329+
if let keyMonitor {
330+
NSEvent.removeMonitor(keyMonitor)
331+
self.keyMonitor = nil
332+
}
333+
}
334+
335+
private func shortcutNumber(from event: NSEvent) -> Int? {
336+
let modifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
337+
guard modifiers.isEmpty,
338+
let characters = event.charactersIgnoringModifiers,
339+
characters.count == 1,
340+
let number = Int(characters),
341+
(1...9).contains(number) else {
342+
return nil
343+
}
344+
345+
return number
346+
}
347+
}

0 commit comments

Comments
 (0)