Skip to content

Commit

Permalink
Merge pull request #105 from mtgto/config-inline-count
Browse files Browse the repository at this point in the history
インラインで表示する変換候補の数を設定できるようにする
  • Loading branch information
mtgto authored Feb 2, 2024
2 parents 8e38cc0 + 5d1c5a8 commit 4dfa511
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 68 deletions.
8 changes: 8 additions & 0 deletions macSKK/InputController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ class InputController: IMKInputController {
}
}
}.store(in: &cancellables)

stateMachine.inlineCandidateCount = UserDefaults.standard.integer(forKey: UserDefaultsKeys.inlineCandidateCount)
NotificationCenter.default.publisher(for: notificationNameInlineCandidateCount)
.sink { [weak self] notification in
if let inlineCandidateCount = notification.object as? Int, inlineCandidateCount >= 0 {
self?.stateMachine.inlineCandidateCount = inlineCandidateCount
}
}.store(in: &cancellables)
}

override func handle(_ event: NSEvent!, client sender: Any!) -> Bool {
Expand Down
7 changes: 7 additions & 0 deletions macSKK/Settings/GeneralView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ struct GeneralView: View {
Toggle(isOn: $settingsViewModel.showAnnotation, label: {
Text("Show Annotation")
})
Section {
Picker("Number of inline candidates", selection: $settingsViewModel.inlineCandidateCount) {
ForEach(0..<10) { count in
Text("\(count)")
}
}
}
}
.formStyle(.grouped)
}.onAppear {
Expand Down
17 changes: 14 additions & 3 deletions macSKK/Settings/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ final class SettingsViewModel: ObservableObject {
@Published var selectedInputSourceId: InputSource.ID
/// 注釈を表示するかどうか
@Published var showAnnotation: Bool
/// インラインで表示する変換候補の数。
@Published var inlineCandidateCount: Int
// 辞書ディレクトリ
let dictionariesDirectoryUrl: URL
// バックグラウンドでの辞書を読み込みで読み込み状態が変わったときに通知される
Expand All @@ -118,7 +120,8 @@ final class SettingsViewModel: ObservableObject {
} else {
selectedInputSourceId = InputSource.defaultInputSourceId
}
self.showAnnotation = UserDefaults.standard.bool(forKey: UserDefaultsKeys.showAnnotation)
showAnnotation = UserDefaults.standard.bool(forKey: UserDefaultsKeys.showAnnotation)
inlineCandidateCount = UserDefaults.standard.integer(forKey: UserDefaultsKeys.inlineCandidateCount)

// SKK-JISYO.Lのようなファイルの読み込みが遅いのでバックグラウンドで処理
$dictSettings.filter({ !$0.isEmpty }).receive(on: DispatchQueue.global()).sink { dictSettings in
Expand Down Expand Up @@ -161,7 +164,7 @@ final class SettingsViewModel: ObservableObject {
self.dictLoadingStatuses[id] = status
}.store(in: &cancellables)

$directModeApplications.sink { applications in
$directModeApplications.dropFirst().sink { applications in
let bundleIdentifiers = applications.map { $0.bundleIdentifier }
UserDefaults.standard.set(bundleIdentifiers, forKey: "directModeBundleIdentifiers")
directModeBundleIdentifiers.send(bundleIdentifiers)
Expand Down Expand Up @@ -203,8 +206,15 @@ final class SettingsViewModel: ObservableObject {
}
}.store(in: &cancellables)

$showAnnotation.sink { showAnnotation in
$showAnnotation.dropFirst().sink { showAnnotation in
UserDefaults.standard.set(showAnnotation, forKey: UserDefaultsKeys.showAnnotation)
logger.log("注釈表示を\(showAnnotation ? "表示" : "非表示", privacy: .public)に変更しました")
}.store(in: &cancellables)

$inlineCandidateCount.dropFirst().sink { inlineCandidateCount in
UserDefaults.standard.set(inlineCandidateCount, forKey: UserDefaultsKeys.inlineCandidateCount)
NotificationCenter.default.post(name: notificationNameInlineCandidateCount, object: inlineCandidateCount)
logger.log("インラインで表示する変換候補の数を\(inlineCandidateCount)個に変更しました")
}.store(in: &cancellables)
}

Expand All @@ -218,6 +228,7 @@ final class SettingsViewModel: ObservableObject {
).appendingPathComponent("Dictionaries")
selectedInputSourceId = InputSource.defaultInputSourceId
showAnnotation = true
inlineCandidateCount = 3
}

// DictionaryViewのPreviewProvider用
Expand Down
1 change: 1 addition & 0 deletions macSKK/Settings/UserDefaultsKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ struct UserDefaultsKeys {
// 選択中のinputSourceID
static let selectedInputSource = "selectedInputSource"
static let showAnnotation = "showAnnotation"
static let inlineCandidateCount = "inlineCandidateCount"
}
7 changes: 4 additions & 3 deletions macSKK/StateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,18 @@ class StateMachine {
/// 読みの一部と補完結果(読み)のペア
var completion: (String, String)? = nil

// TODO: inlineCandidateCount, displayCandidateCountを環境設定にするかも
// TODO: displayCandidateCountを環境設定にするかも
/// 変換候補パネルを表示するまで表示する変換候補の数
let inlineCandidateCount = 3
var inlineCandidateCount: Int
/// 変換候補パネルに一度に表示する変換候補の数
let displayCandidateCount = 9

init(initialState: IMEState = IMEState()) {
init(initialState: IMEState = IMEState(), inlineCandidateCount: Int = 3) {
state = initialState
inputMethodEvent = inputMethodEventSubject.eraseToAnyPublisher()
candidateEvent = candidateEventSubject.removeDuplicates().eraseToAnyPublisher()
yomiEvent = yomiEventSubject.removeDuplicates().eraseToAnyPublisher()
self.inlineCandidateCount = inlineCandidateCount
}

func handle(_ action: Action) -> Bool {
Expand Down
19 changes: 15 additions & 4 deletions macSKK/View/CandidatesPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ final class CandidatesPanel: NSPanel {
showAnnotationPopover: showAnnotationPopover)
let rootView = CandidatesView(candidates: self.viewModel)
let viewController = NSHostingController(rootView: rootView)
super.init(contentRect: .zero, styleMask: [.nonactivatingPanel], backing: .buffered, defer: true)
// borderlessにしないとdeactivateServerが呼ばれてしまう
super.init(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: true)
backgroundColor = .clear
contentViewController = viewController
}

func setCandidates(_ candidates: CurrentCandidates, selected: Candidate?) {
viewModel.candidates = candidates
viewModel.selected = selected
viewModel.candidates = candidates
}

func setSystemAnnotation(_ systemAnnotation: String, for word: Word.Word) {
Expand All @@ -36,6 +38,9 @@ final class CandidatesPanel: NSPanel {

func setCursorPosition(_ cursorPosition: NSRect) {
self.cursorPosition = cursorPosition
if let mainScreen = NSScreen.main {
viewModel.maxWidth = mainScreen.visibleFrame.size.width - cursorPosition.origin.x
}
}

func setShowAnnotationPopover(_ showAnnotationPopover: Bool) {
Expand All @@ -59,17 +64,23 @@ final class CandidatesPanel: NSPanel {
print("preferredContentSize = \(viewController.preferredContentSize)")
print("sizeThatFits = \(viewController.sizeThatFits(in: CGSize(width: 10000, height: 10000)))")
#endif
let width = viewController.rootView.minWidth()
var origin = cursorPosition.origin
let width: CGFloat
let height: CGFloat
if case let .panel(words, _, _) = viewModel.candidates {
width = viewModel.showAnnotationPopover ? viewModel.minWidth + CandidatesView.annotationPopupWidth : viewModel.minWidth
height = CGFloat(words.count) * CandidatesView.lineHeight + CandidatesView.footerHeight
if viewModel.displayPopoverInLeft {
origin.x = origin.x - CandidatesView.annotationPopupWidth - CandidatesView.annotationMargin
}
} else {
// FIXME: 短い文のときにはそれに合わせて高さを縮める
width = viewModel.minWidth
height = 200
}
setContentSize(NSSize(width: width, height: height))
var origin = cursorPosition.origin
if let mainScreen = NSScreen.main {
// スクリーン右にはみ出す場合はスクリーン右端に接するように表示する
if origin.x + width > mainScreen.visibleFrame.size.width {
origin.x = mainScreen.frame.size.width - width
}
Expand Down
135 changes: 77 additions & 58 deletions macSKK/View/CandidatesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ struct CandidatesView: View {
/// 一行の高さ
static let lineHeight: CGFloat = 20
static let footerHeight: CGFloat = 20
/// 変換候補と注釈の間
static let annotationMargin: CGFloat = 8
/// パネル型の注釈ビューの幅
static let annotationPopupWidth: CGFloat = 300
private let font: Font = .body

var body: some View {
Expand All @@ -18,69 +22,71 @@ struct CandidatesView: View {
AnnotationView(annotations: $candidates.selectedAnnotations, systemAnnotation: $candidates.selectedSystemAnnotation)
.padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4))
.frame(width: 300, height: 200)
.background()
case let .panel(words, currentPage, totalPageCount):
VStack(spacing: 0) {
List(Array(words.enumerated()), id: \.element, selection: $candidates.selected) { index, candidate in
HStack {
Text("\(index + 1)")
.font(font)
.padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 0))
.frame(width: 16)
Text(candidate.word)
.font(font)
.fixedSize(horizontal: true, vertical: false)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 4))
Spacer() // popoverをListの右に表示するために余白を入れる
HStack(alignment: .top, spacing: Self.annotationMargin) {
if candidates.displayPopoverInLeft {
if candidates.popoverIsPresented {
AnnotationView(
annotations: $candidates.selectedAnnotations,
systemAnnotation: $candidates.selectedSystemAnnotation
)
.padding(EdgeInsets(top: 16, leading: 12, bottom: 16, trailing: 8))
.frame(width: Self.annotationPopupWidth, alignment: .topLeading)
.frame(maxHeight: 200)
.fixedSize(horizontal: false, vertical: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
.opacity(0.9)
} else {
Spacer(minLength: Self.annotationPopupWidth)
}
.listRowInsets(EdgeInsets())
.frame(height: Self.lineHeight)
// .border(Color.red) // Listの謎のInsetのデバッグ時に使用する
.contentShape(Rectangle())
}
.listStyle(.plain)
.environment(\.defaultMinListRowHeight, Self.lineHeight) // Listの行の上下の余白を削除
.scrollDisabled(true)
.frame(width: minWidth(), height: CGFloat(words.count) * Self.lineHeight)
HStack(alignment: .center, spacing: 0) {
VStack(spacing: 0) {
List(Array(words.enumerated()), id: \.element, selection: $candidates.selected) { index, candidate in
HStack {
Text("\(index + 1)")
.font(font)
.padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 0))
.frame(width: 16)
Text(candidate.word)
.font(font)
.fixedSize(horizontal: true, vertical: false)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 4))
Spacer() // popoverをListの右に表示するために余白を入れる
}
.listRowInsets(EdgeInsets())
.frame(height: Self.lineHeight)
// .border(Color.red) // Listの謎のInsetのデバッグ時に使用する
.contentShape(Rectangle())
}
.listStyle(.plain)
.environment(\.defaultMinListRowHeight, Self.lineHeight) // Listの行の上下の余白を削除
.scrollDisabled(true)
.frame(width: candidates.minWidth, height: CGFloat(words.count) * Self.lineHeight)
HStack(alignment: .center, spacing: 0) {
Spacer()
Text("\(currentPage + 1) / \(totalPageCount)")
.padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 4))
}
.frame(width: candidates.minWidth, height: Self.footerHeight)
.background()
Spacer()
Text("\(currentPage + 1) / \(totalPageCount)")
.padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 4))
}
.frame(width: minWidth(), height: Self.footerHeight)
}
.popover(
isPresented: $candidates.popoverIsPresented,
attachmentAnchor: .rect(.rect(CGRect(x: 0,
y: CGFloat(candidates.selectedIndex ?? 0) * Self.lineHeight,
width: minWidth(),
height: Self.lineHeight))),
arrowEdge: .trailing
) {
AnnotationView(
annotations: $candidates.selectedAnnotations,
systemAnnotation: $candidates.selectedSystemAnnotation
)
.frame(width: 300, alignment: .topLeading)
.padding()
if candidates.popoverIsPresented && !candidates.displayPopoverInLeft {
AnnotationView(
annotations: $candidates.selectedAnnotations,
systemAnnotation: $candidates.selectedSystemAnnotation
)
.padding(EdgeInsets(top: 16, leading: 28, bottom: 16, trailing: 4))
.frame(width: Self.annotationPopupWidth, alignment: .topLeading)
.frame(maxHeight: 200)
.fixedSize(horizontal: false, vertical: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
.opacity(0.9)
}
}
}
}

// 最長のテキストを表示するために必要なビューのサイズを返す
func minWidth() -> CGFloat {
if case let .panel(words, _, _) = candidates.candidates {
let width = words.map { candidate -> CGFloat in
let size = candidate.word.boundingRect(
with: CGSize(width: .greatestFiniteMagnitude, height: Self.lineHeight),
options: .usesLineFragmentOrigin,
attributes: [.font: NSFont.preferredFont(forTextStyle: .body)])
// 未解決の余白(8px) + 添字(16px) + 余白(4px) + テキスト + 余白(4px) + 未解決の余白(22px)
// @see https://forums.swift.org/t/swiftui-list-horizontal-insets-macos/52985/5
return 16 + 4 + size.width + 4 + 22
}.max()
return width ?? 0
} else {
return 300
.frame(maxHeight: 300, alignment: .topLeading)
.background(Color.clear)
}
}
}
Expand All @@ -94,7 +100,16 @@ struct CandidatesView_Previews: PreviewProvider {
private static func pageViewModel() -> CandidatesViewModel {
let viewModel = CandidatesViewModel(candidates: words, currentPage: 0, totalPageCount: 3, showAnnotationPopover: true)
viewModel.selected = words.first
viewModel.systemAnnotations = [words.first!.word: String(repeating: "これはシステム辞書の注釈です。", count: 10)]
viewModel.systemAnnotations = [words.first!.word: String(repeating: "これはシステム辞書の注釈です。", count: 20)]
viewModel.maxWidth = 1000
return viewModel
}

private static func pageViewModelLeftPopover() -> CandidatesViewModel {
let viewModel = CandidatesViewModel(candidates: words, currentPage: 0, totalPageCount: 3, showAnnotationPopover: true)
viewModel.selected = words.first
viewModel.systemAnnotations = [words.first!.word: String(repeating: "これはシステム辞書の注釈です。", count: 20)]
viewModel.maxWidth = 1
return viewModel
}

Expand All @@ -115,7 +130,11 @@ struct CandidatesView_Previews: PreviewProvider {

static var previews: some View {
CandidatesView(candidates: pageViewModel())
.background(Color.cyan)
.previewDisplayName("パネル表示")
CandidatesView(candidates: pageViewModelLeftPopover())
.background(Color.cyan)
.previewDisplayName("パネル表示 (注釈左)")
CandidatesView(candidates: pageWithoutPopoverViewModel())
.previewDisplayName("パネル表示 (注釈なし)")
CandidatesView(candidates: inlineViewModel())
Expand Down
Loading

0 comments on commit 4dfa511

Please sign in to comment.