Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

シフトキーで変わる記号をシフトキーを押しながら別のキーを打ったと見做せるようにする #226

Merged
merged 8 commits into from
Oct 20, 2024
Merged
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
4 changes: 4 additions & 0 deletions macSKK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
CE84A3E9295DA504009394C4 /* RomajiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE84A3E8295DA504009394C4 /* RomajiTests.swift */; };
CE84A3EB295DA715009394C4 /* Dict.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE84A3EA295DA715009394C4 /* Dict.swift */; };
CE84A3ED295DA818009394C4 /* MemoryDictTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE84A3EC295DA818009394C4 /* MemoryDictTests.swift */; };
CE8DFE092CBEAA2900A24230 /* Character+AdditionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8DFE082CBEAA1F00A24230 /* Character+AdditionsTests.swift */; };
CE97887B2A9B93EB00F9B196 /* DirectModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE97887A2A9B93EB00F9B196 /* DirectModeView.swift */; };
CEA78FAA295EBCAC00B67E25 /* StateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA78FA9295EBCAC00B67E25 /* StateMachineTests.swift */; };
CEA78FAC2960401F00B67E25 /* String+Transform.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA78FAB2960401F00B67E25 /* String+Transform.swift */; };
Expand Down Expand Up @@ -239,6 +240,7 @@
CE84A3E8295DA504009394C4 /* RomajiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RomajiTests.swift; sourceTree = "<group>"; };
CE84A3EA295DA715009394C4 /* Dict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dict.swift; sourceTree = "<group>"; };
CE84A3EC295DA818009394C4 /* MemoryDictTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryDictTests.swift; sourceTree = "<group>"; };
CE8DFE082CBEAA1F00A24230 /* Character+AdditionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Character+AdditionsTests.swift"; sourceTree = "<group>"; };
CE97887A2A9B93EB00F9B196 /* DirectModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectModeView.swift; sourceTree = "<group>"; };
CEA78FA9295EBCAC00B67E25 /* StateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateMachineTests.swift; sourceTree = "<group>"; };
CEA78FAB2960401F00B67E25 /* String+Transform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Transform.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -470,6 +472,7 @@
CED7CA3D2A8397E4004EF988 /* UpdateCheckerTests.swift */,
CE6DBA902A846C1700F5A227 /* ReleaseVersionTests.swift */,
CEA78FAD2961BA1D00B67E25 /* String+TransformTests.swift */,
CE8DFE082CBEAA1F00A24230 /* Character+AdditionsTests.swift */,
CE496C942B440BBD001C623C /* Data+EucJis2004Tests.swift */,
CE06CA332AAC199500E80E5E /* UserDict+Utilities.swift */,
CE39FA942BEB942B00E293F0 /* Character+KeyCode.swift */,
Expand Down Expand Up @@ -845,6 +848,7 @@
CEE2D9792A99FEC700A4CD76 /* CandidateTest.swift in Sources */,
CEF0823629685C0800646366 /* StateTests.swift in Sources */,
CED7CA3E2A8397E4004EF988 /* UpdateCheckerTests.swift in Sources */,
CE8DFE092CBEAA2900A24230 /* Character+AdditionsTests.swift in Sources */,
CE2F3B132C030C9A00CE342B /* KeyBindingTests.swift in Sources */,
CEA78FB229646CA100B67E25 /* UserDictTests.swift in Sources */,
CE06CA2B2AAC171B00E80E5E /* FileDictTests.swift in Sources */,
Expand Down
10 changes: 9 additions & 1 deletion macSKK/Character+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Foundation

extension Character {
/**
* アルファベットだけで構成されているかを返す
* アルファベットで構成されているかを返す
*/
var isAlphabet: Bool {
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".contains(self)
Expand All @@ -14,4 +14,12 @@ extension Character {
var isNumber: Bool {
"0123456789".contains(self)
}

/**
* ひらがなで構成されているかを返す。
*/
var isHiragana: Bool {
guard let first = self.unicodeScalars.first else { return false }
return 0x3041 <= first.value && first.value <= 0x309f
}
}
59 changes: 58 additions & 1 deletion macSKK/Romaji.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2022 mtgto <hogerappa@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation
import AppKit

/**
* ローマ字かな(記号も可)変換ルール
Expand Down Expand Up @@ -77,6 +77,13 @@ struct Romaji: Equatable, Sendable {
/// ローマ字かな変換テーブル
let table: [String: Moji]

/**
* シフトキーで別の記号に変換される文字についてシフトを押しながら押したとするための対応表。
* 日本語配列で "+" (シフト + ;) を入力したときに "+" ではなく シフトを押しながら;として押したと扱うために使用する。
* AZIKで余っているキーに処理を割り当てたいときなどに使用する。
*/
let lowercaseMap: [String: String]

/**
* 未確定文字列のままになることができる文字列の集合。
*
Expand All @@ -93,6 +100,7 @@ struct Romaji: Equatable, Sendable {
init(source: String) throws {
var table: [String: Moji] = [:]
var undecidedInputs: Set<String> = []
var lowercaseMap: [String: String] = [:]
var error: RomajiError? = nil
var lineNumber = 0
source.enumerateLines { line, stop in
Expand All @@ -111,6 +119,19 @@ struct Romaji: Equatable, Sendable {
stop = true
return
}
// 第二要素だけがあり、第二要素が "<shift>" + 一文字 のような形式の場合、lowercaseMapの設定として扱う
if elements.count == 2 && elements[1].hasPrefix("<shift>") && elements[1].count == 8 {
let converted = String(elements[1].dropFirst(7))
// 簡単な無限ループになってないかの検査
guard elements[0] != converted else {
logger.error("ローマ字変換定義ファイルの \(lineNumber) 行目のlowercaseMap記述が壊れているため読み込みできません")
error = RomajiError.invalid
stop = true
return
}
lowercaseMap[elements[0]] = converted
return
}
let firstRomaji = elements.count == 5 ? elements[4] : String(Self.firstRomajis[elements[1].first!] ?? elements[0].first!)
let hiragana = elements[1]
let katakana = elements.count > 2 ? elements[2] : nil
Expand All @@ -130,6 +151,7 @@ struct Romaji: Equatable, Sendable {
}
self.table = table
self.undecidedInputs = undecidedInputs
self.lowercaseMap = lowercaseMap
}

/**
Expand Down Expand Up @@ -262,4 +284,39 @@ struct Romaji: Equatable, Sendable {
}
return ConvertedMoji(input: input, kakutei: nil)
}

/**
* キー入力を別のキーとして扱う設定があれば変換する
*
* 変換後もNSEvent.keyCodeは変更されないので、もし非印字キー (Backspace、英数キーなど) として扱うキーに変換したい場合は
* lowercaseMapの値の型を変更すること。
*
* 今はシフトキーが押されているときのみに対応する
* https://github.com/mtgto/macSKK/issues/225
*/
func convertKeyEvent(_ event: NSEvent) -> NSEvent? {
// シフトキーが押されてなければ無視する
if !event.modifierFlags.contains(.shift) {
return nil
}
guard let characters = event.characters else {
return nil
}
guard let charactersIgnoringCharacters = event.charactersIgnoringModifiers else {
return nil
}
if let mapped = lowercaseMap[characters] {
return NSEvent.keyEvent(with: event.type,
location: event.locationInWindow,
modifierFlags: event.modifierFlags,
timestamp: event.timestamp,
windowNumber: event.windowNumber,
context: nil,
characters: mapped,
charactersIgnoringModifiers: charactersIgnoringCharacters,
isARepeat: event.isARepeat,
keyCode: event.keyCode)
}
return nil
}
}
30 changes: 27 additions & 3 deletions macSKK/StateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,23 @@ final class StateMachine {
} else {
// Option-Shift-2のような入力のときには€が入力されるようにする
if let characters = action.characters() {
// lowercaseMapにエントリがある場合はエントリの方のキーが入力されたと見做す
if let mappedEvent = Global.kanaRule.convertKeyEvent(action.event) {
return handleNormal(
Action(keyBind: Global.keyBinding.action(event: mappedEvent),
event: mappedEvent,
cursorPosition: action.cursorPosition),
specialState: specialState)
}
let result = Global.kanaRule.convert(characters)
if let moji = result.kakutei {
addFixedText(moji.string(for: state.inputMode))
if moji.kana.isHiragana {
state.inputMethod = .composing(
ComposingState(isShift: true, text: moji.kana.map { String($0) }, romaji: result.input))
updateMarkedText()
} else {
addFixedText(moji.string(for: state.inputMode))
}
} else {
addFixedText(characters)
}
Expand Down Expand Up @@ -786,6 +800,16 @@ final class StateMachine {
}
switch state.inputMode {
case .hiragana, .katakana, .hankaku:
// lowercaseMapにエントリがある場合はエントリの方のキーが入力されたと見做す
if let mappedEvent = Global.kanaRule.convertKeyEvent(action.event) {
return handleComposing(
Action(keyBind: Global.keyBinding.action(event: mappedEvent),
event: mappedEvent,
cursorPosition: action.cursorPosition),
composing: composing,
specialState: specialState
)
}
// ローマ字が確定してresult.inputがない
// StickyShiftでokuriが[]になっている、またはShift押しながら入力した
if let moji = converted.kakutei {
Expand All @@ -794,8 +818,8 @@ final class StateMachine {
// まだ読み部分が空ならば常に送り仮名ではない
// シフトを押しながら入力した文字がアルファベットじゃないなら送り仮名ではない (記号なので)
// 未確定文字列の先頭にカーソルがあるときはシフト押していてもいなくても送り仮名ではない
if text.isEmpty || (okuri == nil && !(action.shiftIsPressed() && input.isAlphabet)) || composing.cursor == 0 {
if isShift || (action.shiftIsPressed() && input.isAlphabet) {
if text.isEmpty || (okuri == nil && !(action.shiftIsPressed() && moji.kana.isHiragana)) || composing.cursor == 0 {
if isShift || (action.shiftIsPressed() && moji.kana.isHiragana) {
state.inputMethod = .composing(composing.appendText(moji).resetRomaji().with(isShift: true))
} else {
state.inputMethod = .normal
Expand Down
5 changes: 5 additions & 0 deletions macSKK/String+Transform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ extension String {
*/
var isAlphabet: Bool { self.allSatisfy { $0.isAlphabet } }

/**
* ひらがなだけで構成されているかを返す。
*/
var isHiragana: Bool { self.allSatisfy { $0.isHiragana } }

/**
* 自身が見出し語のとき、送り仮名ありの見出し語かどうかを返す。
*
Expand Down
8 changes: 7 additions & 1 deletion macSKK/kana-rule.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# 1つ目: ローマ字入力で確定入力されるまでに入力される文字列を指定してください。
# 2つ目: ひらがなモードで入力される文字を指定してください。
# 3つ目: カタカナモードで入力される文字を指定してください。
# 省略時はひらがなモードで入力される文字自動でカタカナに変換します
# 省略時はひらがなモードで入力される文字を自動でカタカナに変換します
# 記号などはカタカナに変換できないのでそのまま使用されます。
# 4つ目: 半角カナ入力モードで入力される文字を指定してください。
# 省略時はひらがなモードで入力される文字を自動で半角に変換します。
Expand Down Expand Up @@ -251,3 +251,9 @@ zl,→
z , 
z(,(
z),)

# 2つ目の要素を "<shift>" + 入力したいキーで書くと、特殊な設定としてシフトキーを押しながら入力したと見做す
# JIS配列の "+" や英字配列の ":" のように、シフトキーで変わるキーを元のキーをシフト入力したと見做すことができる
# 例えばAZIKで ; を「っ」入力に割り当てている場合に下記のような設定をすることで送り仮名の「っ」を入力できる
#+,<shift>;
#:,<shift>;
20 changes: 20 additions & 0 deletions macSKKTests/Character+AdditionsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2024 mtgto <hogerappa@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later

import XCTest

@testable import macSKK

final class CharacterAdditionsTests: XCTestCase {
func testIsHiragana() {
XCTAssertTrue(Character("あ").isHiragana)
XCTAssertTrue(Character("ぁ").isHiragana)
XCTAssertTrue(Character("っ").isHiragana)
XCTAssertTrue(Character("ゔ").isHiragana)
XCTAssertTrue(Character("ん").isHiragana)
XCTAssertFalse(Character("ー").isHiragana)
XCTAssertFalse(Character("ア").isHiragana)
XCTAssertFalse(Character("ア").isHiragana)
XCTAssertFalse(Character("a").isHiragana)
}
}
11 changes: 11 additions & 0 deletions macSKKTests/RomajiTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class RomajiTests: XCTestCase {
XCTAssertThrowsError(try Romaji(source: ",あ"), "1要素目が空")
XCTAssertThrowsError(try Romaji(source: "a,"), "2要素目が空")
XCTAssertNoThrow(try Romaji(source: "&comma;,あ"), "カンマを使いたい場合は &comma; と書く")
XCTAssertNoThrow(try Romaji(source: "+,<shift>っ"), "シフトキーを押しているときの記号のルール")
XCTAssertThrowsError(try Romaji(source: "+,<shift>+"), "シフトキーを押しているときの記号のルールで左辺と右辺が一致")
}

func testConvert() throws {
Expand All @@ -36,6 +38,15 @@ class RomajiTests: XCTestCase {
XCTAssertEqual(kanaRule.convert("@"), Romaji.ConvertedMoji(input: "@", kakutei: nil), "ルールにない文字は変換されない")
XCTAssertEqual(kanaRule.convert("a;"), Romaji.ConvertedMoji(input: "", kakutei: Romaji.Moji(firstRomaji: "a", kana:"あせみころん")), "システム用の文字を含むことができる")
XCTAssertEqual(kanaRule.convert("ca"), Romaji.ConvertedMoji(input: "", kakutei: Romaji.Moji(firstRomaji: "k", kana:"か")), "実際に入力した一文字目(c)ではなく「か」からローマ字(k)に変換する")
XCTAssertEqual(kanaRule.lowercaseMap["+"], ";")
XCTAssertEqual(kanaRule.lowercaseMap[":"], ";")
}

func testConvertSpecialCharacters() throws {
// AZIKのセミコロンを促音(っ)として扱う設定
let kanaRule = try Romaji(source: ";,っ")
// 入力はセミコロンでもfirstRomajiは "t" になる
XCTAssertEqual(kanaRule.convert(";"), Romaji.ConvertedMoji(input: "", kakutei: Romaji.Moji(firstRomaji: "t", kana: "っ")))
}

func testVu() throws {
Expand Down
39 changes: 36 additions & 3 deletions macSKKTests/StateMachineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ final class StateMachineTests: XCTestCase {
wait(for: [expectation], timeout: 1.0)
}

@MainActor func testHandleNormalRomajiKanaRuleAzik() {
let stateMachine = StateMachine(initialState: IMEState(inputMode: .direct))
Global.kanaRule = try! Romaji(source: [";,っ", ":,<shift>;"].joined(separator: "\n"))
let expectation = XCTestExpectation()
stateMachine.inputMethodEvent.collect(3).sink { events in
XCTAssertEqual(events[0], .fixedText(":"))
XCTAssertEqual(events[1], .modeChanged(.hiragana, .zero))
XCTAssertEqual(events[2], .markedText(MarkedText([.markerCompose, .plain("っ")])))
expectation.fulfill()
}.store(in: &cancellables)
// direct時はローマ字かな変換テーブルは関係なく ":" が入力される
XCTAssertTrue(stateMachine.handle(printableKeyEventAction(character: ":", characterIgnoringModifier: ";", withShift: true)))
XCTAssertTrue(stateMachine.handle(hiraganaAction))
// ひらがなモード時はローマ字かな変換テーブルが参照され "っ" がシフトを押しながら入力されたとする
XCTAssertTrue(stateMachine.handle(printableKeyEventAction(character: ":", characterIgnoringModifier: ";", withShift: true)))
wait(for: [expectation], timeout: 1.0)
}

@MainActor func testHandleNormalSpace() {
let stateMachine = StateMachine(initialState: IMEState(inputMode: .hiragana))
let expectation = XCTestExpectation()
Expand Down Expand Up @@ -1320,7 +1338,7 @@ final class StateMachineTests: XCTestCase {
XCTAssertTrue(stateMachine.handle(printableKeyEventAction(character: " ")))
wait(for: [expectation], timeout: 1.0)
}

@MainActor func testHandleComposingStickyShiftAfterPrintable() {
Global.kanaRule = try! Romaji(source: "a;,あせみころん")
let stateMachine = StateMachine(initialState: IMEState(inputMode: .hiragana))
Expand Down Expand Up @@ -1351,6 +1369,21 @@ final class StateMachineTests: XCTestCase {
wait(for: [expectation], timeout: 1.0)
}

@MainActor func testHandleComposingRomajiKanaRuleAzik() {
let stateMachine = StateMachine(initialState: IMEState(inputMode: .hiragana))
Global.kanaRule = try! Romaji(source: ["a,あ", ";,っ", ":,<shift>;"].joined(separator: "\n"))
let expectation = XCTestExpectation()
stateMachine.inputMethodEvent.collect(3).sink { events in
XCTAssertEqual(events[0], .markedText(MarkedText([.markerCompose, .plain("あ")])))
XCTAssertEqual(events[1], .modeChanged(.hiragana, .zero))
XCTAssertEqual(events[2], .markedText(MarkedText([.plain("[登録:あ*っ]")])))
expectation.fulfill()
}.store(in: &cancellables)
XCTAssertTrue(stateMachine.handle(printableKeyEventAction(character: "a", withShift: true)))
XCTAssertTrue(stateMachine.handle(printableKeyEventAction(character: ":", characterIgnoringModifier: ";", withShift: true)))
wait(for: [expectation], timeout: 1.0)
}

@MainActor func testHandleComposingPrintableAndL() {
let stateMachine = StateMachine(initialState: IMEState(inputMode: .hiragana))
let expectation = XCTestExpectation()
Expand Down Expand Up @@ -2957,8 +2990,8 @@ final class StateMachineTests: XCTestCase {
return Action(
keyBind: keyBind(character: characterIgnoringModifiers, withShift: withShift),
event: generateNSEvent(character: character,
characterIgnoringModifiers: characterIgnoringModifier,
modifierFlags: [.shift]),
characterIgnoringModifiers: characterIgnoringModifier,
modifierFlags: [.shift]),
cursorPosition: .zero
)
} else {
Expand Down
Loading