diff --git a/.gitignore b/.gitignore index 3160b584..1e054964 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ Credits.rtf # that can be built by make -C Source/Data/bin/C_Version # C_count.occ.exe .idea +.swiftpm diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 250733f4..ab3928ab 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -9,17 +9,8 @@ /* Begin PBXBuildFile section */ 6A0421A815FEF3F50061ED63 /* FastLM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6A0421A615FEF3F50061ED63 /* FastLM.cpp */; }; 6A0D4EA715FC0D2D00ABF4B3 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; }; - 6A0D4ED015FC0D6400ABF4B3 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EC415FC0D6400ABF4B3 /* AppDelegate.m */; }; 6A0D4ED215FC0D6400ABF4B3 /* InputMethodController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EC715FC0D6400ABF4B3 /* InputMethodController.mm */; }; 6A0D4ED315FC0D6400ABF4B3 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EC815FC0D6400ABF4B3 /* main.m */; }; - 6A0D4ED415FC0D6400ABF4B3 /* OVInputSourceHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4ECA15FC0D6400ABF4B3 /* OVInputSourceHelper.m */; }; - 6A0D4ED515FC0D6400ABF4B3 /* PreferencesWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4ECC15FC0D6400ABF4B3 /* PreferencesWindowController.m */; }; - 6A0D4EFE15FC0DA600ABF4B3 /* VTCandidateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */; }; - 6A0D4EFF15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EDC15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m */; }; - 6A0D4F0015FC0DA600ABF4B3 /* VTHorizontalCandidateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EDE15FC0DA600ABF4B3 /* VTHorizontalCandidateView.m */; }; - 6A0D4F0115FC0DA600ABF4B3 /* VTVerticalCandidateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EE015FC0DA600ABF4B3 /* VTVerticalCandidateController.m */; }; - 6A0D4F0215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EE215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m */; }; - 6A0D4F0315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EE415FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m */; }; 6A0D4F0815FC0DA600ABF4B3 /* Bopomofo.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EEF15FC0DA600ABF4B3 /* Bopomofo.tiff */; }; 6A0D4F0915FC0DA600ABF4B3 /* Bopomofo@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EF015FC0DA600ABF4B3 /* Bopomofo@2x.tiff */; }; 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F2015FC0EB100ABF4B3 /* Mandarin.cpp */; }; @@ -33,7 +24,6 @@ 6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; }; 6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; }; 6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */; }; - 6AB3620D274CA50700AC7547 /* OVInputSourceHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4ECA15FC0D6400ABF4B3 /* OVInputSourceHelper.m */; }; 6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; }; 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; }; 6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; }; @@ -45,10 +35,15 @@ 6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6AD7CBC715FE555000691B5B /* data-plain-bpmf.txt */; }; 6AE210B215FC63CC003659FE /* PlainBopomofo.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 6AE210B015FC63CC003659FE /* PlainBopomofo.tiff */; }; 6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 6AE210B115FC63CC003659FE /* PlainBopomofo@2x.tiff */; }; - 6AE30A491F7F40B7008735BD /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6AE30A471F7F40B7008735BD /* UserOverrideModel.cpp */; }; - 6AFF97F2253B299E007F1C49 /* OVNonModalAlertWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6AFF97F0253B299E007F1C49 /* OVNonModalAlertWindowController.xib */; }; - 6AFF97F3253B299E007F1C49 /* OVNonModalAlertWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6AFF97F1253B299E007F1C49 /* OVNonModalAlertWindowController.m */; }; + 6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */; }; D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */; }; + D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */ = {isa = PBXBuildFile; productRef = D427F769278C9E29004A2160 /* CandidateUI */; }; + D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427F76B278CA1BA004A2160 /* AppDelegate.swift */; }; + D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; }; + D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; }; + D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; }; + D47F7DD5278C25A0002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; }; + D47F7DD6278C3075002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; }; D48550A325EBE689006A204C /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = D48550A225EBE689006A204C /* OpenCC */; }; /* End PBXBuildFile section */ @@ -76,27 +71,9 @@ 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 6A0D4EA915FC0D2D00ABF4B3 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 6A0D4EAB15FC0D2D00ABF4B3 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; - 6A0D4EC315FC0D6400ABF4B3 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 6A0D4EC415FC0D6400ABF4B3 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 6A0D4EC615FC0D6400ABF4B3 /* InputMethodController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InputMethodController.h; sourceTree = ""; }; 6A0D4EC715FC0D6400ABF4B3 /* InputMethodController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = InputMethodController.mm; sourceTree = ""; }; 6A0D4EC815FC0D6400ABF4B3 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 6A0D4EC915FC0D6400ABF4B3 /* OVInputSourceHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OVInputSourceHelper.h; sourceTree = ""; }; - 6A0D4ECA15FC0D6400ABF4B3 /* OVInputSourceHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OVInputSourceHelper.m; sourceTree = ""; }; - 6A0D4ECB15FC0D6400ABF4B3 /* PreferencesWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PreferencesWindowController.h; sourceTree = ""; }; - 6A0D4ECC15FC0D6400ABF4B3 /* PreferencesWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PreferencesWindowController.m; sourceTree = ""; }; - 6A0D4ED915FC0DA600ABF4B3 /* VTCandidateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTCandidateController.h; sourceTree = ""; }; - 6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTCandidateController.m; sourceTree = ""; }; - 6A0D4EDB15FC0DA600ABF4B3 /* VTHorizontalCandidateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTHorizontalCandidateController.h; sourceTree = ""; }; - 6A0D4EDC15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTHorizontalCandidateController.m; sourceTree = ""; }; - 6A0D4EDD15FC0DA600ABF4B3 /* VTHorizontalCandidateView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTHorizontalCandidateView.h; sourceTree = ""; }; - 6A0D4EDE15FC0DA600ABF4B3 /* VTHorizontalCandidateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTHorizontalCandidateView.m; sourceTree = ""; }; - 6A0D4EDF15FC0DA600ABF4B3 /* VTVerticalCandidateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTVerticalCandidateController.h; sourceTree = ""; }; - 6A0D4EE015FC0DA600ABF4B3 /* VTVerticalCandidateController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTVerticalCandidateController.m; sourceTree = ""; }; - 6A0D4EE115FC0DA600ABF4B3 /* VTVerticalCandidateTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTVerticalCandidateTableView.h; sourceTree = ""; }; - 6A0D4EE215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTVerticalCandidateTableView.m; sourceTree = ""; }; - 6A0D4EE315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTVerticalKeyLabelStripView.h; sourceTree = ""; }; - 6A0D4EE415FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTVerticalKeyLabelStripView.m; sourceTree = ""; }; 6A0D4EEF15FC0DA600ABF4B3 /* Bopomofo.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = Bopomofo.tiff; sourceTree = ""; }; 6A0D4EF015FC0DA600ABF4B3 /* Bopomofo@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "Bopomofo@2x.tiff"; sourceTree = ""; }; 6A0D4EF515FC0DA600ABF4B3 /* McBopomofo-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "McBopomofo-Info.plist"; sourceTree = ""; }; @@ -177,13 +154,16 @@ 6AD7CBC715FE555000691B5B /* data-plain-bpmf.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "data-plain-bpmf.txt"; sourceTree = ""; }; 6AE210B015FC63CC003659FE /* PlainBopomofo.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = PlainBopomofo.tiff; sourceTree = ""; }; 6AE210B115FC63CC003659FE /* PlainBopomofo@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "PlainBopomofo@2x.tiff"; sourceTree = ""; }; - 6AE30A471F7F40B7008735BD /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = ""; }; - 6AE30A481F7F40B7008735BD /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = ""; }; - 6AFF97EF253B299E007F1C49 /* OVNonModalAlertWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OVNonModalAlertWindowController.h; sourceTree = ""; }; - 6AFF97F0253B299E007F1C49 /* OVNonModalAlertWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OVNonModalAlertWindowController.xib; sourceTree = ""; }; - 6AFF97F1253B299E007F1C49 /* OVNonModalAlertWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OVNonModalAlertWindowController.m; sourceTree = ""; }; + 6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NonModalAlertWindowController.xib; sourceTree = ""; }; D427A9BF25ED28CC005D43E0 /* McBopomofo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "McBopomofo-Bridging-Header.h"; sourceTree = ""; }; D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenCCBridge.swift; sourceTree = ""; }; + D427F768278C9D0D004A2160 /* CandidateUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CandidateUI; path = Packages/CandidateUI; sourceTree = ""; }; + D427F76B278CA1BA004A2160 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; + D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = ""; }; + D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = ""; }; + D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = ""; }; + D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSourceHelper.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -193,6 +173,7 @@ files = ( 6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */, D48550A325EBE689006A204C /* OpenCC in Frameworks */, + D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */, 6A0D4EA715FC0D2D00ABF4B3 /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -211,6 +192,7 @@ 6A0D4E9215FC0CFA00ABF4B3 = { isa = PBXGroup; children = ( + D427F766278C9CBD004A2160 /* Packages */, 6A0D4EC215FC0D3C00ABF4B3 /* Source */, 6A0D4EA515FC0D2D00ABF4B3 /* Frameworks */, 6A0D4EA315FC0D2D00ABF4B3 /* Products */, @@ -240,50 +222,24 @@ 6A0D4EC215FC0D3C00ABF4B3 /* Source */ = { isa = PBXGroup; children = ( - 6A0D4ED815FC0DA600ABF4B3 /* CandidateUI */, 6A38BBDD15FC115800A8A51F /* Data */, 6A0D4F1215FC0EB100ABF4B3 /* Engine */, 6ACA41E715FC1D9000935EF6 /* Installer */, 6A0D4F4715FC0EB900ABF4B3 /* Resources */, - 6A0D4EC315FC0D6400ABF4B3 /* AppDelegate.h */, - 6A0D4EC415FC0D6400ABF4B3 /* AppDelegate.m */, 6A0D4EC615FC0D6400ABF4B3 /* InputMethodController.h */, 6A0D4EC715FC0D6400ABF4B3 /* InputMethodController.mm */, 6A0D4EC815FC0D6400ABF4B3 /* main.m */, - 6A0D4EF615FC0DA600ABF4B3 /* McBopomofo-Prefix.pch */, - 6AFF97EF253B299E007F1C49 /* OVNonModalAlertWindowController.h */, - 6AFF97F1253B299E007F1C49 /* OVNonModalAlertWindowController.m */, - 6A0D4EC915FC0D6400ABF4B3 /* OVInputSourceHelper.h */, - 6A0D4ECA15FC0D6400ABF4B3 /* OVInputSourceHelper.m */, - 6A0D4ECB15FC0D6400ABF4B3 /* PreferencesWindowController.h */, - 6A0D4ECC15FC0D6400ABF4B3 /* PreferencesWindowController.m */, + D427F76B278CA1BA004A2160 /* AppDelegate.swift */, + D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */, + D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */, + D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */, D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */, + 6A0D4EF615FC0DA600ABF4B3 /* McBopomofo-Prefix.pch */, D427A9BF25ED28CC005D43E0 /* McBopomofo-Bridging-Header.h */, - 6AE30A471F7F40B7008735BD /* UserOverrideModel.cpp */, - 6AE30A481F7F40B7008735BD /* UserOverrideModel.h */, ); path = Source; sourceTree = ""; }; - 6A0D4ED815FC0DA600ABF4B3 /* CandidateUI */ = { - isa = PBXGroup; - children = ( - 6A0D4ED915FC0DA600ABF4B3 /* VTCandidateController.h */, - 6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */, - 6A0D4EDB15FC0DA600ABF4B3 /* VTHorizontalCandidateController.h */, - 6A0D4EDC15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m */, - 6A0D4EDD15FC0DA600ABF4B3 /* VTHorizontalCandidateView.h */, - 6A0D4EDE15FC0DA600ABF4B3 /* VTHorizontalCandidateView.m */, - 6A0D4EDF15FC0DA600ABF4B3 /* VTVerticalCandidateController.h */, - 6A0D4EE015FC0DA600ABF4B3 /* VTVerticalCandidateController.m */, - 6A0D4EE115FC0DA600ABF4B3 /* VTVerticalCandidateTableView.h */, - 6A0D4EE215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m */, - 6A0D4EE315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.h */, - 6A0D4EE415FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m */, - ); - path = CandidateUI; - sourceTree = ""; - }; 6A0D4EEE15FC0DA600ABF4B3 /* Images */ = { isa = PBXGroup; children = ( @@ -304,6 +260,8 @@ 6A0D4F2215FC0EB100ABF4B3 /* OpenVanilla */, 6A0421A615FEF3F50061ED63 /* FastLM.cpp */, 6A0421A715FEF3F50061ED63 /* FastLM.h */, + D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */, + D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */, ); path = Engine; sourceTree = ""; @@ -377,7 +335,7 @@ 6A0D4F4715FC0EB900ABF4B3 /* Resources */ = { isa = PBXGroup; children = ( - 6AFF97F0253B299E007F1C49 /* OVNonModalAlertWindowController.xib */, + 6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */, 6A0D4EEE15FC0DA600ABF4B3 /* Images */, 6A0D4EF515FC0DA600ABF4B3 /* McBopomofo-Info.plist */, 6A0D4F4815FC0EE100ABF4B3 /* InfoPlist.strings */, @@ -416,6 +374,14 @@ path = Installer; sourceTree = ""; }; + D427F766278C9CBD004A2160 /* Packages */ = { + isa = PBXGroup; + children = ( + D427F768278C9D0D004A2160 /* CandidateUI */, + ); + name = Packages; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXLegacyTarget section */ @@ -452,6 +418,7 @@ name = McBopomofo; packageProductDependencies = ( D48550A225EBE689006A204C /* OpenCC */, + D427F769278C9E29004A2160 /* CandidateUI */, ); productName = McBopomofo; productReference = 6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */; @@ -525,7 +492,7 @@ 6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */, 6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */, 6A38BC1515FC117A00A8A51F /* data.txt in Resources */, - 6AFF97F2253B299E007F1C49 /* OVNonModalAlertWindowController.xib in Resources */, + 6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */, 6AE210B215FC63CC003659FE /* PlainBopomofo.tiff in Resources */, 6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */, 6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */, @@ -574,20 +541,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6A0D4ED015FC0D6400ABF4B3 /* AppDelegate.m in Sources */, - 6A0D4ED215FC0D6400ABF4B3 /* InputMethodController.mm in Sources */, + D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */, 6A0D4ED315FC0D6400ABF4B3 /* main.m in Sources */, - 6A0D4ED415FC0D6400ABF4B3 /* OVInputSourceHelper.m in Sources */, - 6A0D4ED515FC0D6400ABF4B3 /* PreferencesWindowController.m in Sources */, - 6A0D4EFE15FC0DA600ABF4B3 /* VTCandidateController.m in Sources */, - 6A0D4EFF15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m in Sources */, - 6A0D4F0015FC0DA600ABF4B3 /* VTHorizontalCandidateView.m in Sources */, - 6AFF97F3253B299E007F1C49 /* OVNonModalAlertWindowController.m in Sources */, - 6A0D4F0115FC0DA600ABF4B3 /* VTVerticalCandidateController.m in Sources */, - 6AE30A491F7F40B7008735BD /* UserOverrideModel.cpp in Sources */, - 6A0D4F0215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m in Sources */, - 6A0D4F0315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m in Sources */, + D47F7DD5278C25A0002F9DD7 /* InputSourceHelper.swift in Sources */, + D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */, D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */, + D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */, + 6A0D4ED215FC0D6400ABF4B3 /* InputMethodController.mm in Sources */, + D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */, 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */, 6A0421A815FEF3F50061ED63 /* FastLM.cpp in Sources */, ); @@ -599,8 +560,8 @@ files = ( 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */, 6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */, - 6AB3620D274CA50700AC7547 /* OVInputSourceHelper.m in Sources */, 6ACA41FF15FC1D9000935EF6 /* main.m in Sources */, + D47F7DD6278C3075002F9DD7 /* InputSourceHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -985,6 +946,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.openvanilla.McBopomofo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = app; }; name = Debug; @@ -1017,6 +979,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.openvanilla.McBopomofo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = app; }; name = Release; @@ -1074,6 +1037,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + D427F769278C9E29004A2160 /* CandidateUI */ = { + isa = XCSwiftPackageProductDependency; + productName = CandidateUI; + }; D48550A225EBE689006A204C /* OpenCC */ = { isa = XCSwiftPackageProductDependency; package = D48550A125EBE689006A204C /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */; diff --git a/Packages/CandidateUI/.gitignore b/Packages/CandidateUI/.gitignore new file mode 100644 index 00000000..bb460e7b --- /dev/null +++ b/Packages/CandidateUI/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Packages/CandidateUI/Package.swift b/Packages/CandidateUI/Package.swift new file mode 100644 index 00000000..b31be3b0 --- /dev/null +++ b/Packages/CandidateUI/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "CandidateUI", + platforms: [.macOS(.v10_10)], + products: [ + .library( + name: "CandidateUI", + targets: ["CandidateUI"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "CandidateUI", + dependencies: []), + ] +) diff --git a/Packages/CandidateUI/README.md b/Packages/CandidateUI/README.md new file mode 100644 index 00000000..db9fec6f --- /dev/null +++ b/Packages/CandidateUI/README.md @@ -0,0 +1,3 @@ +# CandidateUI + +The candidate window for McBopomofo. diff --git a/Packages/CandidateUI/Sources/CandidateUI/CandidateController.swift b/Packages/CandidateUI/Sources/CandidateUI/CandidateController.swift new file mode 100644 index 00000000..dfa83d19 --- /dev/null +++ b/Packages/CandidateUI/Sources/CandidateUI/CandidateController.swift @@ -0,0 +1,150 @@ +// +// CandidateController.swift +// +// Copyright (c) 2011 The McBopomofo Project. +// +// Contributors: +// Mengjuei Hsieh (@mjhsieh) +// Weizhong Yang (@zonble) +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa + +@objc(VTCandidateControllerDelegate) +public protocol CandidateControllerDelegate: AnyObject { + func candidateCountForController(_ controller: CandidateController) -> UInt + func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String + func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) +} + +@objc(VTCandidateController) +public class CandidateController: NSWindowController { + @objc public weak var delegate: CandidateControllerDelegate? + @objc public var selectedCandidateIndex: UInt = UInt.max + @objc public var visible: Bool = false { + didSet { + if visible { + window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0) + } else { + window?.perform(#selector(NSWindow.orderOut(_:)), with: self, afterDelay: 0.0) + } + } + } + @objc public var windowTopLeftPoint: NSPoint { + get { + guard let frameRect = window?.frame else { + return NSPoint.zero + } + return NSPoint(x: frameRect.minX, y: frameRect.maxY) + } + set { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { + self.set(windowTopLeftPoint: newValue, bottomOutOfScreenAdjustmentHeight: 0) + } + } + } + + @objc public var keyLabels: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + @objc public var keyLabelFont: NSFont = NSFont.systemFont(ofSize: 14) + @objc public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18) + + @objc public func reloadData() { + } + + @objc public func showNextPage() -> Bool { + false + } + + @objc public func showPreviousPage() -> Bool { + false + } + + @objc public func highlightNextCandidate() -> Bool { + false + } + + @objc public func highlightPreviousCandidate() -> Bool { + false + } + + @objc public func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { + UInt.max + } + + @objc(setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:) + public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { + self.doSet(windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height) + } + } + + func doSet(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { + var adjustedPoint = windowTopLeftPoint + var adjustedHeight = height + + var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero + for screen in NSScreen.screens { + let frame = screen.visibleFrame + if windowTopLeftPoint.x >= frame.minX && + windowTopLeftPoint.x <= frame.maxX && + windowTopLeftPoint.y >= frame.minY && + windowTopLeftPoint.y <= frame.maxY { + screenFrame = frame + break + } + } + + if adjustedHeight > screenFrame.size.height / 2.0 { + adjustedHeight = 0.0 + } + + let windowSize = window?.frame.size ?? NSSize.zero + + // bottom beneath the screen? + if adjustedPoint.y - windowSize.height < screenFrame.minY { + adjustedPoint.y = windowTopLeftPoint.y + adjustedHeight + windowSize.height + } + + // top over the screen? + if adjustedPoint.y >= screenFrame.maxY { + adjustedPoint.y = screenFrame.maxY - 1.0 + } + + // right + if adjustedPoint.x + windowSize.width >= screenFrame.maxX { + adjustedPoint.x = screenFrame.maxX - windowSize.width + } + + // left + if adjustedPoint.x < screenFrame.minX { + adjustedPoint.x = screenFrame.minX + } + + window?.setFrameTopLeftPoint(adjustedPoint) + } + +} diff --git a/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift b/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift new file mode 100644 index 00000000..8f6b9b7a --- /dev/null +++ b/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift @@ -0,0 +1,418 @@ +// +// HorizontalCandidateController.swift +// +// Copyright (c) 2011 The McBopomofo Project. +// +// Contributors: +// Mengjuei Hsieh (@mjhsieh) +// Weizhong Yang (@zonble) +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa + +fileprivate class HorizontalCandidateView: NSView { + var highlightedIndex: UInt = 0 + var action: Selector? + weak var target: AnyObject? + + private var keyLabels: [String] = [] + private var displayedCandidates: [String] = [] + private var keyLabelHeight: CGFloat = 0 + private var candidateTextHeight: CGFloat = 0 + private var cellPadding: CGFloat = 0 + private var keyLabelAttrDict: [NSAttributedString.Key:AnyObject] = [:] + private var candidateAttrDict: [NSAttributedString.Key:AnyObject] = [:] + private var elementWidths: [CGFloat] = [] + private var trackingHighlightedIndex: UInt = UInt.max + + override var isFlipped: Bool { + true + } + + var sizeForView: NSSize { + var result = NSSize.zero + + if !elementWidths.isEmpty { + result.width = elementWidths.reduce(0, +) + result.width += CGFloat(elementWidths.count) + result.height = keyLabelHeight + candidateTextHeight + 1.0 + } + return result + } + + @objc(setKeyLabels:displayedCandidates:) + func set(keyLabels labels: [String], displayedCandidates candidates: [String]) { + let count = min(labels.count, candidates.count) + keyLabels = Array(labels[0.. UInt? { + let location = convert(event.locationInWindow, to: nil) + if !NSPointInRect(location, self.bounds) { + return nil + } + var accuWidth: CGFloat = 0.0 + for index in 0..= accuWidth && location.x <= accuWidth + currentWidth { + return UInt(index) + } + accuWidth += currentWidth + 1.0 + } + return nil + + } + + override func mouseUp(with event: NSEvent) { + trackingHighlightedIndex = highlightedIndex + guard let newIndex = findHitIndex(event: event) else { + return + } + highlightedIndex = newIndex + self.setNeedsDisplay(self.bounds) + } + + override func mouseDown(with event: NSEvent) { + guard let newIndex = findHitIndex(event: event) else { + return + } + var triggerAction = false + if newIndex == highlightedIndex { + triggerAction = true + } else { + highlightedIndex = trackingHighlightedIndex + } + + trackingHighlightedIndex = 0 + self.setNeedsDisplay(self.bounds) + if triggerAction { + if let target = target as? NSObject, let action = action { + target.perform(action, with: self) + } + } + } +} + +@objc(VTHorizontalCandidateController) +public class HorizontalCandidateController : CandidateController { + private var candidateView: HorizontalCandidateView + private var prevPageButton: NSButton + private var nextPageButton: NSButton + private var currentPage: UInt = 0 + + public init() { + var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) + let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel] + let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel)) + panel.hasShadow = true + + contentRect.origin = NSPoint.zero + candidateView = HorizontalCandidateView(frame: contentRect) + panel.contentView?.addSubview(candidateView) + + contentRect.size = NSSize(width: 36.0, height: 20.0) + nextPageButton = NSButton(frame: contentRect) + nextPageButton.setButtonType(.momentaryLight) + nextPageButton.bezelStyle = .smallSquare + nextPageButton.title = "»" + + prevPageButton = NSButton(frame: contentRect) + prevPageButton.setButtonType(.momentaryLight) + prevPageButton.bezelStyle = .smallSquare + prevPageButton.title = "«" + + panel.contentView?.addSubview(nextPageButton) + panel.contentView?.addSubview(prevPageButton) + + super.init(window: panel) + + candidateView.target = self + candidateView.action = #selector(candidateViewMouseDidClick(_:)) + + nextPageButton.target = self + nextPageButton.action = #selector(pageButtonAction(_:)) + + prevPageButton.target = self + prevPageButton.action = #selector(pageButtonAction(_:)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func reloadData() { + candidateView.highlightedIndex = 0 + currentPage = 0 + layoutCandidateView() + } + + public override func showNextPage() -> Bool { + guard delegate != nil else { + return false + } + + if currentPage + 1 >= pageCount { + return false + } + + currentPage += 1 + candidateView.highlightedIndex = 0 + layoutCandidateView() + return true + } + + public override func showPreviousPage() -> Bool { + guard delegate != nil else { + return false + } + + if currentPage == 0 { + return false + } + + currentPage -= 1 + candidateView.highlightedIndex = 0 + layoutCandidateView() + return true + } + + public override func highlightNextCandidate() -> Bool { + guard let delegate = delegate else { + return false + } + + let currentIndex = selectedCandidateIndex + if currentIndex + 1 >= delegate.candidateCountForController(self) { + return false + } + selectedCandidateIndex = currentIndex + 1 + return true + } + + public override func highlightPreviousCandidate() -> Bool { + guard delegate != nil else { + return false + } + + let currentIndex = selectedCandidateIndex + if currentIndex == 0 { + return false + } + + selectedCandidateIndex = currentIndex - 1 + return true + } + + public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { + guard let delegate = delegate else { + return UInt.max + } + + let result = currentPage * UInt(keyLabels.count) + index + return result < delegate.candidateCountForController(self) ? result : UInt.max + } + + @objc public override var selectedCandidateIndex: UInt { + get { + return currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex + } + set { + guard let delegate = delegate else { + return + } + let keyLabelCount = UInt(keyLabels.count) + if newValue < delegate.candidateCountForController(self) { + currentPage = newValue / keyLabelCount + candidateView.highlightedIndex = newValue % keyLabelCount + layoutCandidateView() + } + } + } +} + +extension HorizontalCandidateController { + + private var pageCount: UInt { + guard let delegate = delegate else { + return 0 + } + let totalCount = delegate.candidateCountForController(self) + let keyLabelCount = UInt(keyLabels.count) + return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) + } + + private func layoutCandidateView() { + guard let delegate = delegate else { + return + } + + candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) + var candidates = [String]() + let count = delegate.candidateCountForController(self) + let keyLabelCount = UInt(keyLabels.count) + + let begin = currentPage * keyLabelCount + for index in begin.. 1 { + var buttonRect = nextPageButton.frame + var spacing = 0.0 + + if newSize.height < 40.0 { + buttonRect.size.height = floor(newSize.height / 2) + } else { + buttonRect.size.height = 20.0 + } + + if newSize.height >= 60.0 { + spacing = ceil(newSize.height * 0.1) + } + + let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0 + buttonRect.origin = NSPoint(x: newSize.width + 8.0, y: buttonOriginY) + nextPageButton.frame = buttonRect + + buttonRect.origin = NSPoint(x: newSize.width + 8.0, y: buttonOriginY + buttonRect.size.height + spacing) + prevPageButton.frame = buttonRect + + newSize.width += 52.0 + nextPageButton.isHidden = false + prevPageButton.isHidden = false + } else { + nextPageButton.isHidden = true + prevPageButton.isHidden = true + } + + frameRect = window?.frame ?? NSRect.zero + + let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height) + frameRect.size = newSize + frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) + self.window?.setFrame(frameRect, display: false) + self.candidateView.setNeedsDisplay(candidateView.bounds) + } + + @objc fileprivate func pageButtonAction(_ sender: Any) { + guard let sender = sender as? NSButton else { + return + } + if sender == nextPageButton { + _ = showNextPage() + } else if sender == prevPageButton { + _ = showPreviousPage() + } + } + + @objc fileprivate func candidateViewMouseDidClick(_ sender: Any) { + delegate?.candidateController(self, didSelectCandidateAtIndex: self.selectedCandidateIndex) + } + +} diff --git a/Packages/CandidateUI/Sources/CandidateUI/VerticalCandidateController.swift b/Packages/CandidateUI/Sources/CandidateUI/VerticalCandidateController.swift new file mode 100644 index 00000000..495da285 --- /dev/null +++ b/Packages/CandidateUI/Sources/CandidateUI/VerticalCandidateController.swift @@ -0,0 +1,457 @@ +// +// VerticalCandidateController.swift +// +// Copyright (c) 2011 The McBopomofo Project. +// +// Contributors: +// Mengjuei Hsieh (@mjhsieh) +// Weizhong Yang (@zonble) +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa + +fileprivate class VerticalKeyLabelStripView: NSView { + var keyLabelFont: NSFont = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + var labelOffsetY: CGFloat = 0 + var keyLabels: [String] = [] + var highlightedIndex: UInt = UInt.max + + override var isFlipped: Bool { + true + } + + override func draw(_ dirtyRect: NSRect) { + let bounds = self.bounds + NSColor.white.setFill() + NSBezierPath.fill(bounds) + + let count = UInt(keyLabels.count) + if count == 0 { + return + } + let cellHeight: CGFloat = bounds.size.height / CGFloat(count) + let black = NSColor.black + let darkGray = NSColor(deviceWhite: 0.7, alpha: 1.0) + let lightGray = NSColor(deviceWhite: 0.8, alpha: 1.0) + + let paraStyle = NSMutableParagraphStyle() + paraStyle.setParagraphStyle(NSParagraphStyle.default) + paraStyle.alignment = .center + + let textAttr: [NSAttributedString.Key:AnyObject] = [ + .font: keyLabelFont, + .foregroundColor: black, + .paragraphStyle: paraStyle] + for index in 0..= count { + cellRect.size.height += 1.0 + } + + (index == highlightedIndex ? darkGray : lightGray).setFill() + NSBezierPath.fill(cellRect) + let text = keyLabels[Int(index)] + (text as NSString).draw(in: textRect, withAttributes: textAttr) + } + } +} + +fileprivate class VerticalCandidateTableView: NSTableView { + override func adjustScroll(_ newVisible: NSRect) -> NSRect { + var scrollRect = newVisible + let rowHeightPlusSpacing = rowHeight + intercellSpacing.height + scrollRect.origin.y = (scrollRect.origin.y / rowHeightPlusSpacing) * rowHeightPlusSpacing + return scrollRect + } +} + +private let kCandidateTextPadding = 24.0 +private let kCandidateTextLeftMargin = 8.0 +private let kCandidateTextPaddingWithMandatedTableViewPadding = 18.0 +private let kCandidateTextLeftMarginWithMandatedTableViewPadding = 0.0 + + +@objc(VTVerticalCandidateController) +public class VerticalCandidateController: CandidateController { + private var keyLabelStripView: VerticalKeyLabelStripView + private var scrollView: NSScrollView + private var tableView: NSTableView + private var candidateTextParagraphStyle: NSMutableParagraphStyle + private var candidateTextPadding: CGFloat = kCandidateTextPadding + private var candidateTextLeftMargin: CGFloat = kCandidateTextLeftMargin + private var maxCandidateAttrStringWidth: CGFloat = 0 + + public init() { + var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) + let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel] + let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) + panel.hasShadow = true + + contentRect.origin = NSPoint.zero + var stripRect = contentRect + stripRect.size.width = 10.0 + keyLabelStripView = VerticalKeyLabelStripView(frame: stripRect) + panel.contentView?.addSubview(keyLabelStripView) + + var scrollViewRect = contentRect + scrollViewRect.origin.x = stripRect.size.width + scrollViewRect.size.width -= stripRect.size.width + scrollView = NSScrollView(frame: scrollViewRect) + scrollView.verticalScrollElasticity = .none + + tableView = NSTableView(frame: contentRect) + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "candidate")) + column.dataCell = NSTextFieldCell() + column.isEditable = false + + candidateTextPadding = kCandidateTextPadding + candidateTextLeftMargin = kCandidateTextLeftMargin + + tableView.addTableColumn(column) + tableView.intercellSpacing = NSSize(width: 0.0, height: 1.0) + tableView.headerView = nil + tableView.allowsMultipleSelection = false + tableView.allowsEmptySelection = false + + scrollView.documentView = tableView + panel.contentView?.addSubview(scrollView) + + + let paraStyle = NSMutableParagraphStyle() + paraStyle.setParagraphStyle(NSParagraphStyle.default) + paraStyle.firstLineHeadIndent = candidateTextLeftMargin + paraStyle.lineBreakMode = .byClipping + + candidateTextParagraphStyle = paraStyle + + + if #available(macOS 10.16, *) { + tableView.style = .fullWidth + candidateTextPadding = kCandidateTextPaddingWithMandatedTableViewPadding + candidateTextLeftMargin = kCandidateTextLeftMarginWithMandatedTableViewPadding + } + + super.init(window: panel) + tableView.dataSource = self + tableView.delegate = self + tableView.doubleAction = #selector(rowDoubleClicked(_:)) + tableView.target = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func reloadData() { + maxCandidateAttrStringWidth = ceil(candidateFont.pointSize * 2.0 + candidateTextPadding) + tableView.reloadData() + self.layoutCandidateView() + if delegate?.candidateCountForController(self) ?? 0 > 0 { + selectedCandidateIndex = 0 + } + } + + public override func showNextPage() -> Bool { + scrollPageByOne(true) + } + + public override func showPreviousPage() -> Bool { + scrollPageByOne(false) + } + + public override func highlightNextCandidate() -> Bool { + moveSelectionByOne(true) + } + + public override func highlightPreviousCandidate() -> Bool { + moveSelectionByOne(false) + } + + public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { + guard let delegate = delegate else { + return UInt.max + } + + let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin) + if firstVisibleRow != -1 { + let result = UInt(firstVisibleRow) + index + if result < delegate.candidateCountForController(self) { + return result + } + } + + return UInt.max + } + + @objc public override var selectedCandidateIndex: UInt { + get { + let selectedRow = tableView.selectedRow + return selectedRow == -1 ? UInt.max : UInt(selectedRow) + + } + set { + guard let delegate = delegate else { + return + } + var newIndex = newValue + let selectedRow = tableView.selectedRow + let labelCount = keyLabels.count + let itemCount = delegate.candidateCountForController(self) + + if newIndex == UInt.max { + if itemCount == 0 { + tableView.deselectAll(self) + return + } + newIndex = 0 + } + + var lastVisibleRow = newValue + + if selectedRow != -1 && itemCount > 0 && itemCount > labelCount { + if newIndex > selectedRow && (Int(newIndex) - selectedRow) > 1 { + lastVisibleRow = min(newIndex + UInt(labelCount) - 1, itemCount - 1) + } + // no need to handle the backward case: (newIndex < selectedRow && selectedRow - newIndex > 1) + } + + if itemCount > labelCount { + tableView.scrollRowToVisible(Int(lastVisibleRow)) + } + tableView.selectRowIndexes(IndexSet(integer: Int(newIndex)), byExtendingSelection: false) + } + } +} + +extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegate { + + public func numberOfRows(in tableView: NSTableView) -> Int { + Int(delegate?.candidateCountForController(self) ?? 0) + } + + public func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { + guard let delegate = delegate else { + return nil + } + var candidate = "" + if row < delegate.candidateCountForController(self) { + candidate = delegate.candidateController(self, candidateAtIndex: UInt(row)) + } + let attrString = NSAttributedString(string: candidate, attributes: [ + .font: candidateFont, + .paragraphStyle: candidateTextParagraphStyle + ]) + + // we do more work than what this method is expected to; normally not a good practice, but for the amount of data (9 to 10 rows max), we can afford the overhead + + // expand the window width if text overflows + let boundingRect = attrString.boundingRect(with: NSSize(width: 10240.0, height: 10240.0) , options: .usesLineFragmentOrigin) + let textWidth = boundingRect.size.width + candidateTextPadding + if textWidth > maxCandidateAttrStringWidth { + maxCandidateAttrStringWidth = textWidth + layoutCandidateView() + } + + // keep track of the highlighted index in the key label strip + let count = UInt(keyLabels.count) + let selectedRow = tableView.selectedRow + + if selectedRow != -1 { + var newHilightIndex = 0 + + if keyLabelStripView.highlightedIndex != -1 && + (row >= selectedRow + Int(count) || (selectedRow > count && row <= selectedRow - Int(count))) { + newHilightIndex = -1 + } else { + let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin) + newHilightIndex = selectedRow - firstVisibleRow + if newHilightIndex < -1 { + newHilightIndex = -1 + } + } + + if newHilightIndex != keyLabelStripView.highlightedIndex && newHilightIndex >= 0 { + keyLabelStripView.highlightedIndex = UInt(newHilightIndex) + keyLabelStripView.setNeedsDisplay(keyLabelStripView.frame) + } + + } + return attrString + } + + public func tableViewSelectionDidChange(_ notification: Notification) { + let selectedRow = tableView.selectedRow + if selectedRow != -1 { + // keep track of the highlighted index in the key label strip + let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin) + keyLabelStripView.highlightedIndex = UInt(selectedRow - firstVisibleRow) + keyLabelStripView.setNeedsDisplay(keyLabelStripView.frame) + + // fix a subtle OS X "bug" that, since we force the scroller to appear, + // scrolling sometimes shows a temporarily "broken" scroll bar + // (but quickly disappears) + if scrollView.hasVerticalScroller { + scrollView.verticalScroller?.setNeedsDisplay() + } + } + } + + @objc func rowDoubleClicked(_ sender: Any) { + let clickedRow = tableView.clickedRow + if clickedRow != -1 { + delegate?.candidateController(self, didSelectCandidateAtIndex: UInt(clickedRow)) + } + } + + func scrollPageByOne(_ forward: Bool) -> Bool { + guard let delegate = delegate else { + return false + } + let labelCount = UInt(keyLabels.count) + let itemCount = delegate.candidateCountForController(self) + if 0 == itemCount { + return false + } + if itemCount <= labelCount { + return false + } + + var newIndex = selectedCandidateIndex + if forward { + if newIndex == itemCount - 1 { + return false + } + newIndex = min(newIndex + labelCount, itemCount - 1) + } else { + if newIndex == 0 { + return false + } + + if newIndex < labelCount { + newIndex = 0 + } else { + newIndex -= labelCount + } + } + selectedCandidateIndex = newIndex + return true + } + + private func moveSelectionByOne(_ forward: Bool) -> Bool { + guard let delegate = delegate else { + return false + } + let itemCount = delegate.candidateCountForController(self) + if 0 == itemCount { + return false + } + var newIndex = selectedCandidateIndex + if forward { + if newIndex == itemCount - 1 { + return false + } + newIndex += 1 + } else { + if 0 == newIndex { + return false + } + newIndex -= 1 + } + selectedCandidateIndex = newIndex + return true + } + + private func layoutCandidateView() { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { [self] in + self.doLayoutCanaditeView() + } + } + + private func doLayoutCanaditeView() { + guard let delegate = delegate else { + return + } + let count = delegate.candidateCountForController(self) + if 0 == count { + return + } + + let candidateFontSize = ceil(candidateFont.pointSize) + let keyLabelFontSize = ceil(keyLabelFont.pointSize) + let fontSize = max(candidateFontSize, keyLabelFontSize) + + let controlSize: NSControl.ControlSize = fontSize > 36.0 ? .regular : .small + + var keyLabelCount = UInt(keyLabels.count) + var scrollerWidth: CGFloat = 0.0 + if count <= keyLabelCount { + keyLabelCount = count + scrollView.hasVerticalScroller = false + } else { + scrollView.hasVerticalScroller = true + let verticalScroller = scrollView.verticalScroller + verticalScroller?.controlSize = controlSize + verticalScroller?.scrollerStyle = .legacy + scrollerWidth = NSScroller.scrollerWidth(for: controlSize, scrollerStyle: .legacy) + } + + keyLabelStripView.keyLabelFont = keyLabelFont + keyLabelStripView.keyLabels = Array(keyLabels[0..= candidateFontSize) ? 0.0 : floor((candidateFontSize - keyLabelFontSize) / 2.0) + + let rowHeight = ceil(fontSize * 1.25) + tableView.rowHeight = rowHeight + + var maxKeyLabelWidth = keyLabelFontSize + let textAttr: [NSAttributedString.Key:AnyObject] = [.font: keyLabelFont] + let boundingBox = NSSize(width: 1600.0, height: 1600.0) + + for label in keyLabels { + let rect = (label as NSString).boundingRect(with: boundingBox, options: .usesLineFragmentOrigin, attributes: textAttr) + maxKeyLabelWidth = max(rect.size.width, maxKeyLabelWidth) + } + + let rowSpacing = tableView.intercellSpacing.height + let stripWidth = ceil(maxKeyLabelWidth * 1.20) + let tableViewStartWidth = ceil(maxCandidateAttrStringWidth + scrollerWidth) + let windowWidth = stripWidth + 1.0 + tableViewStartWidth + let windowHeight = CGFloat(keyLabelCount) * (rowHeight + rowSpacing) + + var frameRect = self.window?.frame ?? NSRect.zero + let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height) + + frameRect.size = NSMakeSize(windowWidth, windowHeight) + frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) + + keyLabelStripView.frame = NSRect(x: 0.0, y: 0.0, width: stripWidth, height: windowHeight) + scrollView.frame = NSRect(x: stripWidth + 1.0, y: 0.0, width: tableViewStartWidth, height: windowHeight) + self.window?.setFrame(frameRect, display: false) + } +} diff --git a/Source/AppDelegate.h b/Source/AppDelegate.h deleted file mode 100644 index 96ee6e0c..00000000 --- a/Source/AppDelegate.h +++ /dev/null @@ -1,54 +0,0 @@ -// -// AppDelegate.h -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import - -@class PreferencesWindowController; - -@interface AppDelegate : NSObject -{ -@private - NSURLConnection *_updateCheckConnection; - BOOL _currentUpdateCheckIsForced; - NSMutableData *_receivingData; - NSURL *_updateNextStepURL; - PreferencesWindowController *_preferencesWindowController; -} - -- (void)checkForUpdate; -- (void)checkForUpdateForced:(BOOL)forced; -- (void)showPreferences; - -@property (weak, nonatomic) IBOutlet NSWindow *window; -@end diff --git a/Source/AppDelegate.m b/Source/AppDelegate.m deleted file mode 100644 index 98d3dfc0..00000000 --- a/Source/AppDelegate.m +++ /dev/null @@ -1,273 +0,0 @@ -// -// AppDelegate.m -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "AppDelegate.h" -#import "OVNonModalAlertWindowController.h" -#import "PreferencesWindowController.h" - -extern void LTLoadLanguageModel(void); -extern void LTLoadUserLanguageModelFile(void); - -static NSString *kCheckUpdateAutomatically = @"CheckUpdateAutomatically"; -static NSString *kNextUpdateCheckDateKey = @"NextUpdateCheckDate"; -static NSString *kUpdateInfoEndpointKey = @"UpdateInfoEndpoint"; -static NSString *kUpdateInfoSiteKey = @"UpdateInfoSite"; -static const NSTimeInterval kNextCheckInterval = 86400.0; -static const NSTimeInterval kTimeoutInterval = 60.0; - -@interface AppDelegate () -@end - -@implementation AppDelegate -@synthesize window = _window; - -- (void)dealloc -{ - _preferencesWindowController = nil; - _updateCheckConnection = nil; -} - -- (void)applicationDidFinishLaunching:(NSNotification *)inNotification -{ - LTLoadLanguageModel(); - LTLoadUserLanguageModelFile(); - - if (![[NSUserDefaults standardUserDefaults] objectForKey:kCheckUpdateAutomatically]) { - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kCheckUpdateAutomatically]; - [[NSUserDefaults standardUserDefaults] synchronize]; - } - - [self checkForUpdate]; -} - -- (void)checkForUpdate -{ - [self checkForUpdateForced:NO]; -} - -- (void)checkForUpdateForced:(BOOL)forced -{ - if (_updateCheckConnection) { - // busy - return; - } - - _currentUpdateCheckIsForced = forced; - - // time for update? - if (!forced) { - if (![[NSUserDefaults standardUserDefaults] boolForKey:kCheckUpdateAutomatically]) { - return; - } - - NSDate *now = [NSDate date]; - NSDate *date = [[NSUserDefaults standardUserDefaults] objectForKey:kNextUpdateCheckDateKey]; - if (![date isKindOfClass:[NSDate class]]) { - date = now; - } - - if ([now compare:date] == NSOrderedAscending) { - return; - } - } - - NSDate *nextUpdateDate = [NSDate dateWithTimeInterval:kNextCheckInterval sinceDate:[NSDate date]]; - [[NSUserDefaults standardUserDefaults] setObject:nextUpdateDate forKey:kNextUpdateCheckDateKey]; - - NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; - NSString *updateInfoURLString = [infoDict objectForKey:kUpdateInfoEndpointKey]; - if (![updateInfoURLString length]) { - return; - } - - NSURL *updateInfoURL = [NSURL URLWithString:updateInfoURLString]; - if (!updateInfoURL) { - return; - } - - NSURLRequest *request = [NSURLRequest requestWithURL:updateInfoURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:kTimeoutInterval]; - if (!request) { - return; - } -#if DEBUG - NSLog(@"about to request update url %@ ",updateInfoURL); -#endif - - if (_receivingData) { - _receivingData = nil; - } - - // create a new data buffer and connection - _receivingData = [[NSMutableData alloc] init]; - _updateCheckConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; - [_updateCheckConnection start]; -} - -- (void)showPreferences -{ - if (!_preferencesWindowController) { - _preferencesWindowController = [[PreferencesWindowController alloc] initWithWindowNibName:@"preferences"]; - } - [[_preferencesWindowController window] center]; - [[_preferencesWindowController window] orderFront:self]; -} - -- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error -{ - BOOL isForcedCheck = _currentUpdateCheckIsForced; - - _receivingData = nil; - _updateCheckConnection = nil; - _currentUpdateCheckIsForced = NO; - - if (isForcedCheck) { - [[OVNonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Update Check Failed", nil) content:[NSString stringWithFormat:NSLocalizedString(@"There may be no internet connection or the server failed to respond.\n\nError message: %@", nil), [error localizedDescription]] confirmButtonTitle:NSLocalizedString(@"Dismiss", nil) cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; - } -} - -- (void)showNoUpdateAvailableAlert -{ - [[OVNonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Check for Update Completed", nil) content:NSLocalizedString(@"You are already using the latest version of McBopomofo.", nil) confirmButtonTitle:NSLocalizedString(@"OK", nil) cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; -} - -- (void)connectionDidFinishLoading:(NSURLConnection *)connection -{ - id plist = [NSPropertyListSerialization propertyListWithData:_receivingData options:NSPropertyListImmutable format:NULL error:NULL]; -#if DEBUG - NSLog(@"plist %@",plist); -#endif - - BOOL isForcedCheck = _currentUpdateCheckIsForced; - - _receivingData = nil; - _updateCheckConnection = nil; - _currentUpdateCheckIsForced = NO; - - if (!plist) { - if (isForcedCheck) { - [self showNoUpdateAvailableAlert]; - } - return; - } - - NSString *remoteVersion = [plist objectForKey:(id)kCFBundleVersionKey]; -#if DEBUG - NSLog(@"the remoteversion is %@",remoteVersion); -#endif - if (!remoteVersion) { - if (isForcedCheck) { - [self showNoUpdateAvailableAlert]; - } - return; - } - - // TODO: Validate info (e.g. bundle identifier) - // TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this - - NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; - NSString *currentVersion = [infoDict objectForKey:(id)kCFBundleVersionKey]; - NSComparisonResult result = [currentVersion compare:remoteVersion options:NSNumericSearch]; - - if (result != NSOrderedAscending) { - if (isForcedCheck) { - [self showNoUpdateAvailableAlert]; - } - return; - } - - - NSString *siteInfoURLString = [plist objectForKey:kUpdateInfoSiteKey]; - if (!siteInfoURLString) { - if (isForcedCheck) { - [self showNoUpdateAvailableAlert]; - } - return; - } - - NSURL *siteInfoURL = [NSURL URLWithString:siteInfoURLString]; - if (!siteInfoURL) { - if (isForcedCheck) { - [self showNoUpdateAvailableAlert]; - } - return; - } - _updateNextStepURL = siteInfoURL; - - NSDictionary *versionDescriptions = [plist objectForKey:@"Description"]; - NSString *versionDescription = @""; - if ([versionDescriptions isKindOfClass:[NSDictionary class]]) { - NSString *locale = @"en"; - NSArray *supportedLocales = [NSArray arrayWithObjects:@"en", @"zh-Hant", @"zh-Hans", nil]; - NSArray *preferredTags = [NSBundle preferredLocalizationsFromArray:supportedLocales]; - if ([preferredTags count]) { - locale = [preferredTags objectAtIndex:0]; - } - versionDescription = [versionDescriptions objectForKey:locale]; - if (!versionDescription) { - versionDescription = [versionDescriptions objectForKey:@"en"]; - } - - if (!versionDescription) { - versionDescription = @""; - } - else { - versionDescription = [@"\n\n" stringByAppendingString:versionDescription]; - } - } - - NSString *content = [NSString stringWithFormat:NSLocalizedString(@"You're currently using McBopomofo %@ (%@), a new version %@ (%@) is now available. Do you want to visit McBopomofo's website to download the version?%@", nil), [infoDict objectForKey:@"CFBundleShortVersionString"], currentVersion, [plist objectForKey:@"CFBundleShortVersionString"], remoteVersion, versionDescription]; - - [[OVNonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"New Version Available", nil) content:content confirmButtonTitle:NSLocalizedString(@"Visit Website", nil) cancelButtonTitle:NSLocalizedString(@"Not Now", nil) cancelAsDefault:NO delegate:self]; -} - -- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data -{ - [_receivingData appendData:data]; -} - -- (void)nonModalAlertWindowControllerDidConfirm:(OVNonModalAlertWindowController *)controller -{ - if (_updateNextStepURL) { - [[NSWorkspace sharedWorkspace] openURL:_updateNextStepURL]; - } - - _updateNextStepURL = nil; -} - -- (void)nonModalAlertWindowControllerDidCancel:(OVNonModalAlertWindowController *)controller -{ - _updateNextStepURL = nil; -} - -@end diff --git a/Source/AppDelegate.swift b/Source/AppDelegate.swift new file mode 100644 index 00000000..620fbbfe --- /dev/null +++ b/Source/AppDelegate.swift @@ -0,0 +1,217 @@ +// +// AppDelegate.swift +// +// Copyright (c) 2011 The McBopomofo Project. +// +// Contributors: +// Mengjuei Hsieh (@mjhsieh) +// Weizhong Yang (@zonble) +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa +import InputMethodKit + +private let kCheckUpdateAutomatically = "CheckUpdateAutomatically" +private let kNextUpdateCheckDateKey = "NextUpdateCheckDate" +private let kUpdateInfoEndpointKey = "UpdateInfoEndpoint" +private let kUpdateInfoSiteKey = "UpdateInfoSite" +private let kNextCheckInterval: TimeInterval = 86400.0 +private let kTimeoutInterval: TimeInterval = 60.0 + +@objc(AppDelegate) +class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControllerDelegate { + + @IBOutlet weak var window: NSWindow? + private var preferencesWindowController: PreferencesWindowController? + private var checkTask: URLSessionTask? + private var updateNextStepURL: URL? + + func applicationDidFinishLaunching(_ notification: Notification) { + LTLoadLanguageModel() + LTLoadUserLanguageModelFile() + + if UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) == nil { + UserDefaults.standard.set(true, forKey: kCheckUpdateAutomatically) + UserDefaults.standard.synchronize() + } + checkForUpdate() + } + + @objc func showPreferences() { + if preferencesWindowController == nil { + preferencesWindowController = PreferencesWindowController(windowNibName: "preferences") + } + preferencesWindowController?.window?.center() + preferencesWindowController?.window?.orderFront(self) + } + + @objc(checkForUpdate) + func checkForUpdate() { + checkForUpdate(forced: false) + } + + @objc(checkForUpdateForced:) + func checkForUpdate(forced: Bool) { + + if checkTask != nil { + // busy + return + } + + // time for update? + if !forced { + if UserDefaults.standard.bool(forKey: kCheckUpdateAutomatically) == false { + return + } + let now = Date() + let date = UserDefaults.standard.object(forKey: kNextUpdateCheckDateKey) as? Date ?? now + if now.compare(date) == .orderedAscending { + return + } + } + + let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date()) + UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey) + + guard let infoDict = Bundle.main.infoDictionary, + let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String, + let updateInfoURL = URL(string: updateInfoURLString) else { + return + } + + let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval) + + func showNoUpdateAvailableAlert() { + NonModalAlertWindowController.shared.show(title: NSLocalizedString("Check for Update Completed", comment: ""), content: NSLocalizedString("You are already using the latest version of McBopomofo.", comment: ""), confirmButtonTitle: NSLocalizedString("OK", comment: "") , cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil) + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + defer { + self.checkTask = nil + } + + if let error = error { + if forced { + let title = NSLocalizedString("Update Check Failed", comment: "") + let content = String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), error.localizedDescription) + let buttonTitle = NSLocalizedString("Dismiss", comment: "") + + DispatchQueue.main.async { + NonModalAlertWindowController.shared.show(title:title , content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil) + } + } + return + } + + do { + guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any], + let remoteVersion = plist[kCFBundleVersionKey] as? String, + let infoDict = Bundle.main.infoDictionary + else { + if forced { + DispatchQueue.main.async { + showNoUpdateAvailableAlert() + } + } + return + } + + // TODO: Validate info (e.g. bundle identifier) + // TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this + + let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? "" + let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil) + + if result != .orderedAscending { + if forced { + DispatchQueue.main.async { + showNoUpdateAvailableAlert() + } + } + return + } + + guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String, + let siteInfoURL = URL(string: siteInfoURLString) else { + if forced { + DispatchQueue.main.async { + showNoUpdateAvailableAlert() + } + } + return + } + + self.updateNextStepURL = siteInfoURL + + var versionDescription = "" + let versionDescriptions = plist["Description"] as? [AnyHashable: Any] + if let versionDescriptions = versionDescriptions { + var locale = "en" + let supportedLocales = ["en", "zh-Hant", "zh-Hans"] + let preferredTags = Bundle.preferredLocalizations(from: supportedLocales) + if let first = preferredTags.first { + locale = first + } + versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? "" + if !versionDescription.isEmpty { + versionDescription = "\n\n" + versionDescription + } + } + + let content = String(format: NSLocalizedString("You're currently using McBopomofo %@ (%@), a new version %@ (%@) is now available. Do you want to visit McBopomofo's website to download the version?%@", comment: ""), + infoDict["CFBundleShortVersionString"] as? String ?? "", + currentVersion, + plist["CFBundleShortVersionString"] as? String ?? "", + remoteVersion, + versionDescription) + DispatchQueue.main.async { + NonModalAlertWindowController.shared.show(title: NSLocalizedString("New Version Available", comment: "") , content: content, confirmButtonTitle: NSLocalizedString("Visit Website", comment: ""), cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, delegate: self) + } + + } catch { + if forced { + DispatchQueue.main.async { + showNoUpdateAvailableAlert() + } + } + } + } + checkTask = task + task.resume() + } + + func nonModalAlertWindowControllerDidConfirm(_ controller: NonModalAlertWindowController) { + if let updateNextStepURL = updateNextStepURL { + NSWorkspace.shared.open(updateNextStepURL) + } + updateNextStepURL = nil + } + + func nonModalAlertWindowControllerDidCancel(_ controller: NonModalAlertWindowController) { + updateNextStepURL = nil + } +} diff --git a/Source/Base.lproj/preferences.xib b/Source/Base.lproj/preferences.xib index cfe934d0..f189ef01 100644 --- a/Source/Base.lproj/preferences.xib +++ b/Source/Base.lproj/preferences.xib @@ -8,10 +8,10 @@ - + - - + + diff --git a/Source/CandidateUI/VTCandidateController.h b/Source/CandidateUI/VTCandidateController.h deleted file mode 100644 index 41e36a5c..00000000 --- a/Source/CandidateUI/VTCandidateController.h +++ /dev/null @@ -1,68 +0,0 @@ -// -// VTCandidateController.h -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import - -@class VTCandidateController; - -@protocol VTCandidateControllerDelegate -- (NSUInteger)candidateCountForController:(VTCandidateController *)controller; -- (NSString *)candidateController:(VTCandidateController *)controller candidateAtIndex:(NSUInteger)index; -- (void)candidateController:(VTCandidateController *)controller didSelectCandidateAtIndex:(NSUInteger)index; -@end - -@interface VTCandidateController : NSWindowController -{ -@protected - __weak id _delegate; - NSArray *_keyLabels; - NSFont *_keyLabelFont; - NSFont *_candidateFont; - BOOL _visible; -} - -- (void)reloadData; - -- (BOOL)showNextPage; -- (BOOL)showPreviousPage; -- (BOOL)highlightNextCandidate; -- (BOOL)highlightPreviousCandidate; - -- (void)setWindowTopLeftPoint:(NSPoint)topLeftPoint bottomOutOfScreenAdjustmentHeight:(CGFloat)height; - -- (NSUInteger)candidateIndexAtKeyLabelIndex:(NSUInteger)index; - -@property (weak, nonatomic) id delegate; -@property (assign, nonatomic) NSUInteger selectedCandidateIndex; - -@property (assign, nonatomic) BOOL visible; -@property (assign, nonatomic) NSPoint windowTopLeftPoint; - -@property (copy, nonatomic) NSArray *keyLabels; -@property (copy, nonatomic) NSFont *keyLabelFont; -@property (copy, nonatomic) NSFont *candidateFont; -@end diff --git a/Source/CandidateUI/VTCandidateController.m b/Source/CandidateUI/VTCandidateController.m deleted file mode 100644 index 933d1018..00000000 --- a/Source/CandidateUI/VTCandidateController.m +++ /dev/null @@ -1,178 +0,0 @@ -// -// VTCandidateController.m -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "VTCandidateController.h" - - -@implementation VTCandidateController -@synthesize delegate = _delegate; -@synthesize keyLabels = _keyLabels; -@synthesize keyLabelFont = _keyLabelFont; -@synthesize candidateFont = _candidateFont; - -- (void)dealloc -{ - _keyLabels = nil; - _keyLabelFont = nil; - _candidateFont = nil; -} - -- (id)initWithWindow:(NSWindow *)window -{ - self = [super initWithWindow:window]; - if (self) { - // populate the default values - _keyLabels = @[@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9"]; - _keyLabelFont = [NSFont systemFontOfSize:14.0]; - _candidateFont = [NSFont systemFontOfSize:18.0]; - } - return self; -} - -- (void)reloadData -{ -} - -- (BOOL)showNextPage -{ - return NO; -} - -- (BOOL)showPreviousPage -{ - return NO; -} - -- (BOOL)highlightNextCandidate -{ - return NO; -} - -- (BOOL)highlightPreviousCandidate -{ - return NO; -} - -- (void)setWindowTopLeftPoint:(NSPoint)topLeftPoint bottomOutOfScreenAdjustmentHeight:(CGFloat)height -{ - // Since layout is now deferred, the origin setting should also be deferred so that - // the correct visible frame dimensions are used. - NSArray *params = [NSArray arrayWithObjects:[NSValue valueWithPoint:topLeftPoint], [NSNumber numberWithDouble:height], nil]; - [self performSelector:@selector(deferredSetWindowTopLeftPoint:) withObject:params afterDelay:0.0]; -} - -- (void)deferredSetWindowTopLeftPoint:(NSArray *)params -{ - NSPoint topLeftPoint = [[params objectAtIndex:0] pointValue]; - CGFloat height = [[params objectAtIndex:1] doubleValue]; - - NSPoint adjustedPoint = topLeftPoint; - CGFloat adjustedHeight = height; - - // first, locate the screen the point is in - NSRect screenFrame = [[NSScreen mainScreen] visibleFrame]; - - for (NSScreen *screen in [NSScreen screens]) { - NSRect frame = [screen visibleFrame]; - if (topLeftPoint.x >= NSMinX(frame) && topLeftPoint.x <= NSMaxX(frame) && topLeftPoint.y >= NSMinY(frame) && topLeftPoint.y <= NSMaxY(frame)) { - screenFrame = frame; - break; - } - } - - // make sure we don't have any erratic value - if (adjustedHeight > screenFrame.size.height / 2.0) { - adjustedHeight = 0.0; - } - - NSSize windowSize = [[self window] frame].size; - - // bottom beneath the screen? - if (adjustedPoint.y - windowSize.height < NSMinY(screenFrame)) { - adjustedPoint.y = topLeftPoint.y + adjustedHeight + windowSize.height; - } - - // top over the screen? - if (adjustedPoint.y >= NSMaxY(screenFrame)) { - adjustedPoint.y = NSMaxY(screenFrame) - 1.0; - } - - // right - if (adjustedPoint.x + windowSize.width >= NSMaxX(screenFrame)) { - adjustedPoint.x = NSMaxX(screenFrame) - windowSize.width; - } - - // left - if (adjustedPoint.x < NSMinX(screenFrame)) { - adjustedPoint.x = NSMinX(screenFrame); - } - - [[self window] setFrameTopLeftPoint:adjustedPoint]; -} - -- (NSUInteger)candidateIndexAtKeyLabelIndex:(NSUInteger)index -{ - return NSUIntegerMax; -} - -- (BOOL)visible -{ - // Because setVisible: defers its action, we need to use our own visible. Do not use [[self window] isVisible]. - return _visible; -} - -- (void)setVisible:(BOOL)visible -{ - _visible = visible; - if (visible) { - [[self window] performSelector:@selector(orderFront:) withObject:self afterDelay:0.0]; - } - else { - [[self window] performSelector:@selector(orderOut:) withObject:self afterDelay:0.0]; - } -} - -- (NSPoint)windowTopLeftPoint -{ - NSRect frameRect = [[self window] frame]; - return NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height); -} - -- (void)setWindowTopLeftPoint:(NSPoint)topLeftPoint -{ - [self setWindowTopLeftPoint:topLeftPoint bottomOutOfScreenAdjustmentHeight:0.0]; -} - -- (NSUInteger)selectedCandidateIndex -{ - return NSUIntegerMax; -} - -- (void)setSelectedCandidateIndex:(NSUInteger)newIndex -{ -} -@end diff --git a/Source/CandidateUI/VTHorizontalCandidateController.h b/Source/CandidateUI/VTHorizontalCandidateController.h deleted file mode 100644 index 23c9da4e..00000000 --- a/Source/CandidateUI/VTHorizontalCandidateController.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// VTHorizontalCandidateController.h -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "VTCandidateController.h" - -@class VTHorizontalCandidateView; - -@interface VTHorizontalCandidateController : VTCandidateController -{ -@protected - VTHorizontalCandidateView *_candidateView; - NSButton *_prevPageButton; - NSButton *_nextPageButton; - NSUInteger _currentPage; -} -@end diff --git a/Source/CandidateUI/VTHorizontalCandidateController.m b/Source/CandidateUI/VTHorizontalCandidateController.m deleted file mode 100644 index 4cc8795b..00000000 --- a/Source/CandidateUI/VTHorizontalCandidateController.m +++ /dev/null @@ -1,250 +0,0 @@ -// -// VTHorizontalCandidateController.m -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "VTHorizontalCandidateController.h" -#import "VTHorizontalCandidateView.h" - -@interface VTHorizontalCandidateController (Private) -- (NSUInteger)pageCount; -- (void)layoutCandidateView; -- (void)pageButtonAction:(id)sender; -- (void)candidateViewMouseDidClick:(id)sender; -@end - - -@implementation VTHorizontalCandidateController - -- (void)dealloc -{ - _candidateView = nil; - _prevPageButton = nil; - _nextPageButton = nil; -} - -- (id)init -{ - NSRect contentRect = NSMakeRect(128.0, 128.0, 0.0, 0.0); - NSUInteger styleMask = NSBorderlessWindowMask | NSNonactivatingPanelMask; - - NSPanel *panel = [[NSPanel alloc] initWithContentRect:contentRect styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]; - [panel setLevel:kCGPopUpMenuWindowLevel]; - [panel setHasShadow:YES]; - - self = [self initWithWindow:panel]; - if (self) { - contentRect.origin = NSMakePoint(0.0, 0.0); - _candidateView = [[VTHorizontalCandidateView alloc] initWithFrame:contentRect]; - _candidateView.target = self; - _candidateView.action = @selector(candidateViewMouseDidClick:); - [[panel contentView] addSubview:_candidateView]; - - contentRect.size = NSMakeSize(36.0, 20.0); - _nextPageButton = [[NSButton alloc] initWithFrame:contentRect]; - _prevPageButton = [[NSButton alloc] initWithFrame:contentRect]; - [_nextPageButton setButtonType:NSMomentaryLightButton]; - [_nextPageButton setBezelStyle:NSSmallSquareBezelStyle]; - [_nextPageButton setTitle:@"»"]; - [_nextPageButton setTarget:self]; - [_nextPageButton setAction:@selector(pageButtonAction:)]; - - [_prevPageButton setButtonType:NSMomentaryLightButton]; - [_prevPageButton setBezelStyle:NSSmallSquareBezelStyle]; - [_prevPageButton setTitle:@"«"]; - [_prevPageButton setTarget:self]; - [_prevPageButton setAction:@selector(pageButtonAction:)]; - - [[panel contentView] addSubview:_nextPageButton]; - [[panel contentView] addSubview:_prevPageButton]; - } - - return self; -} - -- (void)reloadData -{ - _candidateView.highlightedIndex = 0; - _currentPage = 0; - [self layoutCandidateView]; -} - -- (BOOL)showNextPage -{ - if (_currentPage + 1 >= [self pageCount]) { - return NO; - } - - _currentPage++; - _candidateView.highlightedIndex = 0; - [self layoutCandidateView]; - return YES; -} - -- (BOOL)showPreviousPage -{ - if (_currentPage == 0) { - return NO; - } - - _currentPage--; - _candidateView.highlightedIndex = 0; - [self layoutCandidateView]; - return YES; -} - -- (BOOL)highlightNextCandidate -{ - NSUInteger currentIndex = self.selectedCandidateIndex; - if (currentIndex + 1 >= [_delegate candidateCountForController:self]) { - return NO; - } - - self.selectedCandidateIndex = currentIndex + 1; - return YES; -} - -- (BOOL)highlightPreviousCandidate -{ - NSUInteger currentIndex = self.selectedCandidateIndex; - if (currentIndex == 0) { - return NO; - } - - self.selectedCandidateIndex = currentIndex - 1; - return YES; -} - -- (NSUInteger)candidateIndexAtKeyLabelIndex:(NSUInteger)index -{ - NSUInteger result = _currentPage * [_keyLabels count] + index; - return result < [_delegate candidateCountForController:self] ? result : NSUIntegerMax; -} - - -- (NSUInteger)selectedCandidateIndex -{ - return _currentPage * [_keyLabels count] + _candidateView.highlightedIndex; -} - -- (void)setSelectedCandidateIndex:(NSUInteger)newIndex -{ - NSUInteger keyLabelCount = [_keyLabels count]; - if (newIndex < [_delegate candidateCountForController:self]) { - _currentPage = newIndex / keyLabelCount; - _candidateView.highlightedIndex = newIndex % keyLabelCount; - [self layoutCandidateView]; - } -} -@end - - -@implementation VTHorizontalCandidateController (Private) -- (NSUInteger)pageCount -{ - NSUInteger totalCount = [_delegate candidateCountForController:self]; - NSUInteger keyLabelCount = [_keyLabels count]; - return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0); -} - -- (void)layoutCandidateView -{ - [_candidateView setKeyLabelFont:_keyLabelFont candidateFont:_candidateFont]; - - NSMutableArray *candidates = [NSMutableArray array]; - NSUInteger count = [_delegate candidateCountForController:self]; - NSUInteger keyLabelCount = [_keyLabels count]; - for (NSUInteger index = _currentPage * keyLabelCount, j = 0; index < count && j < keyLabelCount; index++, j++) { - [candidates addObject:[_delegate candidateController:self candidateAtIndex:index]]; - } - - [_candidateView setKeyLabels:_keyLabels displayedCandidates:candidates]; - NSSize newSize = _candidateView.sizeForView; - - NSRect frameRect = [_candidateView frame]; - frameRect.size = newSize; - [_candidateView setFrame:frameRect]; - - if ([self pageCount] > 1) { - NSRect buttonRect = [_nextPageButton frame]; - CGFloat spacing = 0.0; - - if (newSize.height < 40.0) { - buttonRect.size.height = floor(newSize.height / 2); - } - else { - buttonRect.size.height = 20.0; - } - - if (newSize.height >= 60.0) { - spacing = ceil(newSize.height * 0.1); - } - - CGFloat buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0; - buttonRect.origin = NSMakePoint(newSize.width + 8.0, buttonOriginY); - [_nextPageButton setFrame:buttonRect]; - - buttonRect.origin = NSMakePoint(newSize.width + 8.0, buttonOriginY + buttonRect.size.height + spacing); - [_prevPageButton setFrame:buttonRect]; - - [_nextPageButton setEnabled:(_currentPage + 1 < [self pageCount])]; - [_prevPageButton setEnabled:(_currentPage != 0)]; - - newSize.width += 52.0; - - [_nextPageButton setHidden:NO]; - [_prevPageButton setHidden:NO]; - } - else { - [_nextPageButton setHidden:YES]; - [_prevPageButton setHidden:YES]; - } - - frameRect = [[self window] frame]; - NSPoint topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height); - - frameRect.size = newSize; - frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height); - - [[self window] setFrame:frameRect display:NO]; - [_candidateView setNeedsDisplay:YES]; - -} - -- (void)pageButtonAction:(id)sender -{ - if (sender == _nextPageButton) { - [self showNextPage]; - } - else if (sender == _prevPageButton) { - [self showPreviousPage]; - } -} - -- (void)candidateViewMouseDidClick:(id)sender -{ - [_delegate candidateController:self didSelectCandidateAtIndex:self.selectedCandidateIndex]; -} -@end diff --git a/Source/CandidateUI/VTHorizontalCandidateView.h b/Source/CandidateUI/VTHorizontalCandidateView.h deleted file mode 100644 index 492b7c5c..00000000 --- a/Source/CandidateUI/VTHorizontalCandidateView.h +++ /dev/null @@ -1,54 +0,0 @@ -// -// VTHorizontalCandidateView.h -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import - -@interface VTHorizontalCandidateView : NSView -{ -@protected - NSArray *_keyLabels; - NSArray *_displayedCandidates; - CGFloat _keyLabelHeight; - CGFloat _candidateTextHeight; - CGFloat _cellPadding; - NSDictionary *_keyLabelAttrDict; - NSDictionary *_candidateAttrDict; - NSArray *_elementWidths; - NSUInteger _highlightedIndex; - NSUInteger _trackingHighlightedIndex; - SEL _action; - __weak id _target; -} - -- (void)setKeyLabels:(NSArray *)labels displayedCandidates:(NSArray *)candidates; -- (void)setKeyLabelFont:(NSFont *)labelFont candidateFont:(NSFont *)candidateFont; - -@property (readonly, nonatomic) NSSize sizeForView; -@property (assign, nonatomic) NSUInteger highlightedIndex; -@property (assign, nonatomic) SEL action; -@property (weak, nonatomic) id target; -@end diff --git a/Source/CandidateUI/VTHorizontalCandidateView.m b/Source/CandidateUI/VTHorizontalCandidateView.m deleted file mode 100644 index d24122f5..00000000 --- a/Source/CandidateUI/VTHorizontalCandidateView.m +++ /dev/null @@ -1,225 +0,0 @@ -// -// VTHorizontalCandidateView.m -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "VTHorizontalCandidateView.h" - -// use these instead of MIN/MAX macro to keep compilers happy with pedantic warnings on -NS_INLINE CGFloat min(CGFloat a, CGFloat b) { return a < b ? a : b; } -NS_INLINE CGFloat max(CGFloat a, CGFloat b) { return a > b ? a : b; } - -@implementation VTHorizontalCandidateView - -@synthesize highlightedIndex = _highlightedIndex; -@synthesize action = _action; -@synthesize target = _target; - -- (void)dealloc -{ - _keyLabels = nil; - _displayedCandidates = nil; - _keyLabelAttrDict = nil; - _candidateAttrDict = nil; - _elementWidths = nil; -} - -- (void)setKeyLabels:(NSArray *)labels displayedCandidates:(NSArray *)candidates -{ - NSUInteger count = min([labels count], [candidates count]); - _keyLabels = [labels subarrayWithRange:NSMakeRange(0, count)]; - _displayedCandidates = [candidates subarrayWithRange:NSMakeRange(0, count)]; - - NSMutableArray *newWidths = [NSMutableArray array]; - - NSSize baseSize = NSMakeSize(10240.0, 10240.0); - for (NSUInteger index = 0; index < count; index++) { - NSRect labelRect = [[_keyLabels objectAtIndex:index] boundingRectWithSize:baseSize options:NSStringDrawingUsesLineFragmentOrigin attributes:_keyLabelAttrDict]; - - NSRect candidateRect = [[_displayedCandidates objectAtIndex:index] boundingRectWithSize:baseSize options:NSStringDrawingUsesLineFragmentOrigin attributes:_candidateAttrDict]; - - CGFloat width = max(labelRect.size.width, candidateRect.size.width) + _cellPadding; - [newWidths addObject:[NSNumber numberWithDouble:width]]; - } - - _elementWidths = newWidths; -} - -- (void)setKeyLabelFont:(NSFont *)labelFont candidateFont:(NSFont *)candidateFont -{ - NSMutableParagraphStyle *paraStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - [paraStyle setAlignment:NSCenterTextAlignment]; - - _keyLabelAttrDict = [NSDictionary dictionaryWithObjectsAndKeys: - labelFont, NSFontAttributeName, - paraStyle, NSParagraphStyleAttributeName, - [NSColor blackColor], NSForegroundColorAttributeName, - nil]; - _candidateAttrDict = [NSDictionary dictionaryWithObjectsAndKeys: - candidateFont, NSFontAttributeName, - paraStyle, NSParagraphStyleAttributeName, - [NSColor textColor], NSForegroundColorAttributeName, - nil]; - - CGFloat labelFontSize = [labelFont pointSize]; - CGFloat candidateFontSize = [candidateFont pointSize]; - CGFloat biggestSize = max(labelFontSize, candidateFontSize); - - _keyLabelHeight = ceil(labelFontSize * 1.20); - _candidateTextHeight = ceil(candidateFontSize * 1.20); - _cellPadding = ceil(biggestSize / 2.0); -} - - -- (NSSize)sizeForView -{ - NSSize result = NSMakeSize(0.0, 0.0); - if ([_elementWidths count]) { - for (NSNumber *w in _elementWidths) { - result.width += [w doubleValue]; - } - - result.width += [_elementWidths count]; - result.height = _keyLabelHeight + _candidateTextHeight + 1.0; - } - - return result; -} - -- (BOOL)isFlipped -{ - return YES; -} - -- (void)drawRect:(NSRect)dirtyRect -{ - NSColor *backgroundColor = [NSColor controlBackgroundColor]; - NSColor *darkGray = [NSColor colorWithDeviceWhite:0.7 alpha:1.0]; - NSColor *lightGray = [NSColor colorWithDeviceWhite:0.8 alpha:1.0]; - - NSRect bounds = [self bounds]; - - [backgroundColor setFill]; - [NSBezierPath fillRect:bounds]; - - [[NSColor darkGrayColor] setStroke]; - [NSBezierPath strokeLineFromPoint:NSMakePoint(bounds.size.width, 0.0) toPoint:NSMakePoint(bounds.size.width, bounds.size.height)]; - - NSUInteger count = [_elementWidths count]; - CGFloat accuWidth = 0.0; - - for (NSUInteger index = 0; index < count; index++) { - NSDictionary *activeCandidateAttr = _candidateAttrDict; - CGFloat currentWidth = [[_elementWidths objectAtIndex:index] doubleValue]; - NSRect labelRect = NSMakeRect(accuWidth, 0.0, currentWidth, _keyLabelHeight); - NSRect candidateRect = NSMakeRect(accuWidth, _keyLabelHeight + 1.0, currentWidth, _candidateTextHeight); - - if (index == _highlightedIndex) { - [darkGray setFill]; - } - else { - [lightGray setFill]; - } - - [NSBezierPath fillRect:labelRect]; - [[_keyLabels objectAtIndex:index] drawInRect:labelRect withAttributes:_keyLabelAttrDict]; - - if (index == _highlightedIndex) { - [[NSColor selectedTextBackgroundColor] setFill]; - - activeCandidateAttr = [_candidateAttrDict mutableCopy]; - [(NSMutableDictionary *)activeCandidateAttr setObject:[NSColor selectedTextColor] forKey:NSForegroundColorAttributeName]; - } - else { - [backgroundColor setFill]; - } - - [NSBezierPath fillRect:candidateRect]; - [[_displayedCandidates objectAtIndex:index] drawInRect:candidateRect withAttributes:activeCandidateAttr]; - - accuWidth += currentWidth + 1.0; - } -} - -- (NSUInteger)findHitIndex:(NSEvent *)theEvent -{ - NSUInteger result = NSUIntegerMax; - - NSPoint location = [self convertPoint:[theEvent locationInWindow] toView:nil]; - if (!NSPointInRect(location, [self bounds])) { - return result; - } - - NSUInteger count = [_elementWidths count]; - CGFloat accuWidth = 0.0; - for (NSUInteger index = 0; index < count; index++) { - CGFloat currentWidth = [[_elementWidths objectAtIndex:index] doubleValue]; - - if (location.x >= accuWidth && location.x <= accuWidth + currentWidth) { - result = index; - break; - } - - accuWidth += currentWidth + 1.0; - } - - return result; -} - -- (void)mouseDown:(NSEvent *)theEvent -{ - NSUInteger newIndex = [self findHitIndex:theEvent]; - _trackingHighlightedIndex = _highlightedIndex; - - if (newIndex != NSUIntegerMax) { - _highlightedIndex = newIndex; - [self setNeedsDisplay:YES]; - } -} - -- (void)mouseUp:(NSEvent *)theEvent -{ - NSUInteger newIndex = [self findHitIndex:theEvent]; - BOOL triggerAction = NO; - - if (newIndex == _highlightedIndex) { - triggerAction = YES; - } - else { - _highlightedIndex = _trackingHighlightedIndex; - } - - _trackingHighlightedIndex = 0; - [self setNeedsDisplay:YES]; - -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Warc-performSelector-leaks" - if (triggerAction && _target && _action) { - [_target performSelector:_action withObject:self]; - } -# pragma clang diagnostic pop -} - -@end diff --git a/Source/CandidateUI/VTVerticalCandidateController.h b/Source/CandidateUI/VTVerticalCandidateController.h deleted file mode 100644 index 04bdb596..00000000 --- a/Source/CandidateUI/VTVerticalCandidateController.h +++ /dev/null @@ -1,41 +0,0 @@ -// -// VTVerticalCandidateController.h -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "VTCandidateController.h" - -@class VTVerticalKeyLabelStripView; - -@interface VTVerticalCandidateController : VTCandidateController -{ -@protected - VTVerticalKeyLabelStripView *_keyLabelStripView; - NSScrollView *_scrollView; - NSTableView *_tableView; - NSMutableParagraphStyle *_candidateTextParagraphStyle; - CGFloat _maxCandidateAttrStringWidth; -} -@end diff --git a/Source/CandidateUI/VTVerticalCandidateController.m b/Source/CandidateUI/VTVerticalCandidateController.m deleted file mode 100644 index 7150d45b..00000000 --- a/Source/CandidateUI/VTVerticalCandidateController.m +++ /dev/null @@ -1,434 +0,0 @@ -// -// VTVerticalCandidateController.m -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "VTVerticalCandidateController.h" -#import "VTVerticalKeyLabelStripView.h" -#import "VTVerticalCandidateTableView.h" - -// use these instead of MIN/MAX macro to keep compilers happy with pedantic warnings on -NS_INLINE CGFloat min(CGFloat a, CGFloat b) { return a < b ? a : b; } -NS_INLINE CGFloat max(CGFloat a, CGFloat b) { return a > b ? a : b; } - -static const CGFloat kCandidateTextPadding = 24.0; -static const CGFloat kCandidateTextLeftMargin = 8.0; - -#if defined(__MAC_10_16) -static const CGFloat kCandidateTextPaddingWithMandatedTableViewPadding = 18.0; -static const CGFloat kCandidateTextLeftMarginWithMandatedTableViewPadding = 0.0; -#endif - -@interface VTVerticalCandidateController (Private) -- (void)rowDoubleClicked:(id)sender; -- (BOOL)scrollPageByOne:(BOOL)forward; -- (BOOL)moveSelectionByOne:(BOOL)forward; -- (void)layoutCandidateView; -@end - -@implementation VTVerticalCandidateController -{ - // Total padding added to the left and the right of the table view cell text. - CGFloat _candidateTextPadding; - - // The indent of the table view cell text from the left. - CGFloat _candidateTextLeftMargin; -} - - -- (id)init -{ - NSRect contentRect = NSMakeRect(128.0, 128.0, 0.0, 0.0); - NSUInteger styleMask = NSBorderlessWindowMask | NSNonactivatingPanelMask; - - NSPanel *panel = [[NSPanel alloc] initWithContentRect:contentRect styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]; - [panel setLevel:kCGPopUpMenuWindowLevel]; - [panel setHasShadow:YES]; - - self = [self initWithWindow:panel]; - if (self) { - contentRect.origin = NSMakePoint(0.0, 0.0); - - NSRect stripRect = contentRect; - stripRect.size.width = 10.0; - _keyLabelStripView = [[VTVerticalKeyLabelStripView alloc] initWithFrame:stripRect]; - - [[panel contentView] addSubview:_keyLabelStripView]; - - NSRect scrollViewRect = contentRect; - scrollViewRect.origin.x = stripRect.size.width; - scrollViewRect.size.width -= stripRect.size.width; - - _scrollView = [[NSScrollView alloc] initWithFrame:scrollViewRect]; - - // >=10.7 only, elastic scroll causes some drawing issues with visible scroller, so we disable it - if ([_scrollView respondsToSelector:@selector(setVerticalScrollElasticity:)]) { - [_scrollView setVerticalScrollElasticity:NSScrollElasticityNone]; - } - - _tableView = [[VTVerticalCandidateTableView alloc] initWithFrame:contentRect]; - [_tableView setDataSource:self]; - [_tableView setDelegate:self]; - - NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"candidate"]; - [column setDataCell:[[NSTextFieldCell alloc] init]]; - [column setEditable:NO]; - - _candidateTextPadding = kCandidateTextPadding; - _candidateTextLeftMargin = kCandidateTextLeftMargin; - - [_tableView addTableColumn:column]; - [_tableView setIntercellSpacing:NSMakeSize(0.0, 1.0)]; - [_tableView setHeaderView:nil]; - [_tableView setAllowsMultipleSelection:NO]; - [_tableView setAllowsEmptySelection:YES]; - [_tableView setDoubleAction:@selector(rowDoubleClicked:)]; - [_tableView setTarget:self]; - - #if defined(__MAC_10_16) - if (@available(macOS 10.16, *)) { - [_tableView setStyle:NSTableViewStyleFullWidth]; - _candidateTextPadding = kCandidateTextPaddingWithMandatedTableViewPadding; - _candidateTextLeftMargin = kCandidateTextLeftMarginWithMandatedTableViewPadding; - } - #endif - - [_scrollView setDocumentView:_tableView]; - [[panel contentView] addSubview:_scrollView]; - - _candidateTextParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - [_candidateTextParagraphStyle setFirstLineHeadIndent:_candidateTextLeftMargin]; - [_candidateTextParagraphStyle setLineBreakMode:NSLineBreakByClipping]; - } - - return self; -} - -- (void)reloadData -{ - _maxCandidateAttrStringWidth = ceil([_candidateFont pointSize] * 2.0 + _candidateTextPadding); - - [_tableView reloadData]; - [self layoutCandidateView]; - - if ([_delegate candidateCountForController:self]) { - self.selectedCandidateIndex = 0; - } -} - -- (BOOL)showNextPage -{ - return [self scrollPageByOne:YES]; -} - -- (BOOL)showPreviousPage -{ - return [self scrollPageByOne:NO]; -} - -- (BOOL)highlightNextCandidate -{ - return [self moveSelectionByOne:YES]; -} - -- (BOOL)highlightPreviousCandidate -{ - return [self moveSelectionByOne:NO]; -} - -- (NSUInteger)candidateIndexAtKeyLabelIndex:(NSUInteger)index -{ - NSInteger firstVisibleRow = [_tableView rowAtPoint:[_scrollView documentVisibleRect].origin]; - if (firstVisibleRow != -1) { - NSUInteger result = firstVisibleRow + index; - if (result < [_delegate candidateCountForController:self]) { - return result; - } - } - - return NSUIntegerMax; -} - -- (NSUInteger)selectedCandidateIndex -{ - NSInteger selectedRow = [_tableView selectedRow]; - return (selectedRow == -1) ? NSUIntegerMax : selectedRow; -} - -- (void)setSelectedCandidateIndex:(NSUInteger)aNewIndex -{ - NSUInteger newIndex = aNewIndex; - - NSInteger selectedRow = [_tableView selectedRow]; - - NSUInteger labelCount = [_keyLabels count]; - NSUInteger itemCount = [_delegate candidateCountForController:self]; - - if (newIndex == NSUIntegerMax) { - if (itemCount == 0) { - [_tableView deselectAll:self]; - return; - } - newIndex = 0; - } - - NSUInteger lastVisibleRow = newIndex; - if (selectedRow != -1 && itemCount > 0 && itemCount > labelCount) { - if (newIndex > selectedRow && (newIndex - selectedRow) > 1) { - lastVisibleRow = min(newIndex + labelCount - 1, itemCount - 1); - } - - // no need to handle the backward case: (newIndex < selectedRow && selectedRow - newIndex > 1) - } - - if (itemCount > labelCount) { - [_tableView scrollRowToVisible:lastVisibleRow]; - } - - [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex] byExtendingSelection:NO]; -} - - -@end - - -@implementation VTVerticalCandidateController (Private) -- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView -{ - return [_delegate candidateCountForController:self]; -} - -- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row -{ - NSString *candidate = @""; - - // rendering can occur when the delegate is already gone or data goes stale; in that case we ignore it - - if (row < [_delegate candidateCountForController:self]) { - candidate = [_delegate candidateController:self candidateAtIndex:row]; - } - - NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:candidate attributes:[NSDictionary dictionaryWithObjectsAndKeys:_candidateFont, NSFontAttributeName, _candidateTextParagraphStyle, NSParagraphStyleAttributeName, nil]]; - - // we do more work than what this method is expected to; normally not a good practice, but for the amount of data (9 to 10 rows max), we can afford the overhead - - // expand the window width if text overflows - NSRect boundingRect = [attrString boundingRectWithSize:NSMakeSize(10240.0, 10240.0) options:NSStringDrawingUsesLineFragmentOrigin]; - CGFloat textWidth = boundingRect.size.width + _candidateTextPadding; - if (textWidth > _maxCandidateAttrStringWidth) { - _maxCandidateAttrStringWidth = textWidth; - [self layoutCandidateView]; - } - - // keep track of the highlighted index in the key label strip - NSUInteger count = [_keyLabels count]; - NSInteger selectedRow = [_tableView selectedRow]; - if (selectedRow != -1) { - // cast this into signed integer to make our life easier - NSInteger newHilightIndex; - - if (_keyLabelStripView.highlightedIndex != -1 && (row >= selectedRow + count || (selectedRow > count && row <= selectedRow - count))) { - newHilightIndex = -1; - } - else { - NSInteger firstVisibleRow = [_tableView rowAtPoint:[_scrollView documentVisibleRect].origin]; - - newHilightIndex = selectedRow - firstVisibleRow; - if (newHilightIndex < -1) { - newHilightIndex = -1; - } - } - - if (newHilightIndex != _keyLabelStripView.highlightedIndex && newHilightIndex >= 0) { - _keyLabelStripView.highlightedIndex = newHilightIndex; - [_keyLabelStripView setNeedsDisplay:YES]; - } - } - - return attrString; -} - -- (void)tableViewSelectionDidChange:(NSNotification *)aNotification -{ - NSInteger selectedRow = [_tableView selectedRow]; - if (selectedRow != -1) { - // keep track of the highlighted index in the key label strip - NSInteger firstVisibleRow = [_tableView rowAtPoint:[_scrollView documentVisibleRect].origin]; - _keyLabelStripView.highlightedIndex = selectedRow - firstVisibleRow; - [_keyLabelStripView setNeedsDisplay:YES]; - - // fix a subtle OS X "bug" that, since we force the scroller to appear, - // scrolling sometimes shows a temporarily "broken" scroll bar - // (but quickly disappears) - if ([_scrollView hasVerticalScroller]) { - [[_scrollView verticalScroller] setNeedsDisplay]; - } - } -} - -- (void)rowDoubleClicked:(id)sender -{ - NSInteger clickedRow = [_tableView clickedRow]; - if (clickedRow != -1) { - [_delegate candidateController:self didSelectCandidateAtIndex:clickedRow]; - } -} - -- (BOOL)scrollPageByOne:(BOOL)forward -{ - NSUInteger labelCount = [_keyLabels count]; - NSUInteger itemCount = [_delegate candidateCountForController:self]; - - if (0 == itemCount) { - return NO; - } - - if (itemCount <= labelCount) { - return NO; - } - - NSUInteger newIndex = self.selectedCandidateIndex; - - if (forward) { - if (newIndex == itemCount - 1) { - return NO; - } - - newIndex = min(newIndex + labelCount, itemCount - 1); - } - else { - if (newIndex == 0) { - return NO; - } - - if (newIndex < labelCount) { - newIndex = 0; - } - else { - newIndex -= labelCount; - } - } - - self.selectedCandidateIndex = newIndex; - return YES; -} - -- (BOOL)moveSelectionByOne:(BOOL)forward -{ - NSUInteger itemCount = [_delegate candidateCountForController:self]; - - if (0 == itemCount) { - return NO; - } - - NSUInteger newIndex = self.selectedCandidateIndex; - - if (forward) { - if (newIndex == itemCount - 1) { - return NO; - } - - newIndex++; - } - else { - if (0 == newIndex) { - return NO; - } - - newIndex--; - } - - self.selectedCandidateIndex = newIndex; - return YES; -} - -- (void)layoutCandidateView -{ - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(doLayoutCanaditeView) object:nil]; - [self performSelector:@selector(doLayoutCanaditeView) withObject:nil afterDelay:0.0]; -} - -- (void)doLayoutCanaditeView -{ - NSUInteger count = [_delegate candidateCountForController:self]; - if (!count) { - return; - } - - CGFloat candidateFontSize = ceil([_candidateFont pointSize]); - CGFloat keyLabelFontSize = ceil([_keyLabelFont pointSize]); - CGFloat fontSize = max(candidateFontSize, keyLabelFontSize); - - NSControlSize controlSize = (fontSize > 36.0) ? NSRegularControlSize : NSSmallControlSize; - - NSUInteger keyLabelCount = [_keyLabels count]; - CGFloat scrollerWidth = 0.0; - if (count <= keyLabelCount) { - keyLabelCount = count; - [_scrollView setHasVerticalScroller:NO]; - } - else { - [_scrollView setHasVerticalScroller:YES]; - - NSScroller *verticalScroller = [_scrollView verticalScroller]; - [verticalScroller setControlSize:controlSize]; - [verticalScroller setScrollerStyle:NSScrollerStyleLegacy]; - scrollerWidth = [NSScroller scrollerWidthForControlSize:controlSize scrollerStyle:NSScrollerStyleLegacy]; - } - - _keyLabelStripView.keyLabelFont = _keyLabelFont; - _keyLabelStripView.keyLabels = [_keyLabels subarrayWithRange:NSMakeRange(0, keyLabelCount)]; - _keyLabelStripView.labelOffsetY = (keyLabelFontSize >= candidateFontSize) ? 0.0 : floor((candidateFontSize - keyLabelFontSize) / 2.0); - - CGFloat rowHeight = ceil(fontSize * 1.25); - [_tableView setRowHeight:rowHeight]; - - CGFloat maxKeyLabelWidth = keyLabelFontSize; - NSDictionary *textAttr = [NSDictionary dictionaryWithObjectsAndKeys: - _keyLabelFont, NSFontAttributeName, - nil]; - NSSize boundingBox = NSMakeSize(1600.0, 1600.0); - for (NSString *label in _keyLabels) { - NSRect rect = [label boundingRectWithSize:boundingBox options:NSStringDrawingUsesLineFragmentOrigin attributes:textAttr]; - maxKeyLabelWidth = max(rect.size.width, maxKeyLabelWidth); - } - - CGFloat rowSpacing = [_tableView intercellSpacing].height; - CGFloat stripWidth = ceil(maxKeyLabelWidth * 1.20); - CGFloat tableViewStartWidth = ceil(_maxCandidateAttrStringWidth + scrollerWidth);; - CGFloat windowWidth = stripWidth + 1.0 + tableViewStartWidth; - CGFloat windowHeight = keyLabelCount * (rowHeight + rowSpacing); - - NSRect frameRect = [[self window] frame]; - NSPoint topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height); - - frameRect.size = NSMakeSize(windowWidth, windowHeight); - frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height); - - [_keyLabelStripView setFrame:NSMakeRect(0.0, 0.0, stripWidth, windowHeight)]; - [_scrollView setFrame:NSMakeRect(stripWidth + 1.0, 0.0, tableViewStartWidth, windowHeight)]; - [[self window] setFrame:frameRect display:NO]; -} -@end diff --git a/Source/CandidateUI/VTVerticalCandidateTableView.h b/Source/CandidateUI/VTVerticalCandidateTableView.h deleted file mode 100644 index 47bb37ac..00000000 --- a/Source/CandidateUI/VTVerticalCandidateTableView.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// VTVerticalCandidateTableView.h -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import - -@interface VTVerticalCandidateTableView : NSTableView -@end diff --git a/Source/CandidateUI/VTVerticalCandidateTableView.m b/Source/CandidateUI/VTVerticalCandidateTableView.m deleted file mode 100644 index bf326ac5..00000000 --- a/Source/CandidateUI/VTVerticalCandidateTableView.m +++ /dev/null @@ -1,38 +0,0 @@ -// -// VTVerticalCandidateTableView.m -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "VTVerticalCandidateTableView.h" - -@implementation VTVerticalCandidateTableView -- (NSRect)adjustScroll:(NSRect)newVisible -{ - NSRect scrollRect = newVisible; - CGFloat rowHeightPlusSpacing = [self rowHeight] + [self intercellSpacing].height; - scrollRect.origin.y = (NSInteger)(scrollRect.origin.y / rowHeightPlusSpacing) * rowHeightPlusSpacing; - return scrollRect; -} -@end diff --git a/Source/CandidateUI/VTVerticalKeyLabelStripView.h b/Source/CandidateUI/VTVerticalKeyLabelStripView.h deleted file mode 100644 index 8439ba1a..00000000 --- a/Source/CandidateUI/VTVerticalKeyLabelStripView.h +++ /dev/null @@ -1,43 +0,0 @@ -// -// VTVerticalKeyLabelStripView.m -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import - -@interface VTVerticalKeyLabelStripView : NSView -{ -@protected - NSFont *_keyLabelFont; - CGFloat _labelOffsetY; - NSArray *_keyLabels; - NSInteger _highlightedIndex; -} - -@property (retain, nonatomic) NSFont *keyLabelFont; -@property (assign, nonatomic) CGFloat labelOffsetY; -@property (retain, nonatomic) NSArray *keyLabels; -@property (assign, nonatomic) NSInteger highlightedIndex; -@end diff --git a/Source/CandidateUI/VTVerticalKeyLabelStripView.m b/Source/CandidateUI/VTVerticalKeyLabelStripView.m deleted file mode 100644 index 1a7d2a19..00000000 --- a/Source/CandidateUI/VTVerticalKeyLabelStripView.m +++ /dev/null @@ -1,104 +0,0 @@ -// -// VTVerticalKeyLabelStripView.h -// -// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "VTVerticalKeyLabelStripView.h" - -@implementation VTVerticalKeyLabelStripView -@synthesize keyLabelFont = _keyLabelFont; -@synthesize labelOffsetY = _labelOffsetY; -@synthesize keyLabels = _keyLabels; -@synthesize highlightedIndex = _highlightedIndex; - -- (void)dealloc -{ - _keyLabelFont = nil; - _keyLabels = nil; -} - -- (id)initWithFrame:(NSRect)frameRect -{ - self = [super initWithFrame:frameRect]; - if (self) { - _keyLabelFont = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]; - } - - return self; -} - -- (BOOL)isFlipped -{ - return YES; -} - -- (void)drawRect:(NSRect)dirtyRect -{ - NSRect bounds = [self bounds]; - [[NSColor whiteColor] setFill]; - [NSBezierPath fillRect:bounds]; - - NSUInteger count = [_keyLabels count]; - if (!count) { - return; - } - - CGFloat cellHeight = bounds.size.height / count; - NSColor *black = [NSColor blackColor]; - NSColor *darkGray = [NSColor colorWithDeviceWhite:0.7 alpha:1.0]; - NSColor *lightGray = [NSColor colorWithDeviceWhite:0.8 alpha:1.0]; - - NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - [style setAlignment:NSCenterTextAlignment]; - - NSDictionary *textAttr = [NSDictionary dictionaryWithObjectsAndKeys: - _keyLabelFont, NSFontAttributeName, - black, NSForegroundColorAttributeName, - style, NSParagraphStyleAttributeName, - nil]; - - for (NSUInteger index = 0; index < count; index++) { - NSRect textRect = NSMakeRect(0.0, index * cellHeight + _labelOffsetY, bounds.size.width, cellHeight - _labelOffsetY); - NSRect cellRect = NSMakeRect(0.0, index * cellHeight, bounds.size.width, cellHeight - 1); - - // fill in the last cell - if (index + 1 >= count) { - cellRect.size.height += 1.0; - } - - if (index == _highlightedIndex) { - [darkGray setFill]; - } - else { - [lightGray setFill]; - } - - [NSBezierPath fillRect:cellRect]; - - NSString *text = [_keyLabels objectAtIndex:index]; - [text drawInRect:textRect withAttributes:textAttr]; - } -} -@end diff --git a/Source/UserOverrideModel.cpp b/Source/Engine/UserOverrideModel.cpp similarity index 100% rename from Source/UserOverrideModel.cpp rename to Source/Engine/UserOverrideModel.cpp diff --git a/Source/UserOverrideModel.h b/Source/Engine/UserOverrideModel.h similarity index 100% rename from Source/UserOverrideModel.h rename to Source/Engine/UserOverrideModel.h diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index 7c53d37b..0b075cf9 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -38,12 +38,11 @@ #import #import "OVStringHelper.h" #import "OVUTF8Helper.h" -#import "AppDelegate.h" -#import "OVNonModalAlertWindowController.h" -#import "VTHorizontalCandidateController.h" -#import "VTVerticalCandidateController.h" #import "McBopomofo-Swift.h" +@import CandidateUI; +@import OpenCC; + //@import SwiftUI; // C++ namespace usages @@ -286,9 +285,6 @@ static double FindHighestScore(const vector& nodes, double epsilon) NSMenuItem *aboutMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"About McBopomofo…", @"") action:@selector(showAbout:) keyEquivalent:@""]; [menu addItem:aboutMenuItem]; - - NSLog(@"menu %@", menu); - return menu; } @@ -1589,7 +1585,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } NSLog(@"openUserPhrases called"); if (!LTCheckIfUserLanguageModelFileExists()) { NSString *content = [NSString stringWithFormat:NSLocalizedString(@"Please check the permission of at \"%@\".", @""), LTUserDataFolderPath()]; - [[OVNonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Unable to create the user phrase file.", @"") content:content confirmButtonTitle:NSLocalizedString(@"OK", @"") cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; + [[NonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Unable to create the user phrase file.", @"") content:content confirmButtonTitle:NSLocalizedString(@"OK", @"") cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; return; } diff --git a/Source/InputSourceHelper.swift b/Source/InputSourceHelper.swift new file mode 100644 index 00000000..e5c1bf83 --- /dev/null +++ b/Source/InputSourceHelper.swift @@ -0,0 +1,142 @@ +// +// OVInputSourceHelper.swift +// +// Copyright (c) 2011 The McBopomofo Project. +// +// Contributors: +// Mengjuei Hsieh (@mjhsieh) +// Weizhong Yang (@zonble) +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa +import Carbon + +public class InputSourceHelper: NSObject { + + @available(*, unavailable) + public override init() { + super.init() + } + + public static func allInstalledInputSources() -> [TISInputSource] { + TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] + } + + @objc(inputSourceForProperty:stringValue:) + public static func inputSource(for propertyKey: CFString, stringValue: String) -> TISInputSource? { + let stringID = CFStringGetTypeID() + for source in allInstalledInputSources() { + if let proprtyPtr = TISGetInputSourceProperty(source, propertyKey) { + let property = Unmanaged.fromOpaque(proprtyPtr).takeUnretainedValue() + let typeID = CFGetTypeID(property) + if typeID != stringID { + continue + } + if stringValue == property as? String { + return source + } + } + } + return nil + } + + @objc(inputSourceForInputSourceID:) + public static func inputSource(for sourceID: String) -> TISInputSource? { + inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID) + } + + @objc(inputSourceEnabled:) + public static func inputSourceEnabled(for source: TISInputSource) -> Bool { + if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) { + let value = Unmanaged.fromOpaque(valuePts).takeUnretainedValue() + return value == kCFBooleanTrue + } + return false + } + + @objc(enableInputSource:) + public static func enable(inputSource: TISInputSource) -> Bool { + let status = TISEnableInputSource(inputSource) + return status == noErr + } + + @objc(enableAllInputModesForInputSourceBundleID:) + public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool { + var enabled = false + for source in allInstalledInputSources() { + guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), + let _ = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else { + continue + } + let bundleID = Unmanaged.fromOpaque(bundleIDPtr).takeUnretainedValue() + if String(bundleID) == inputSourceBundleD { + let modeEnabled = self.enable(inputSource: source) + if !modeEnabled { + return false + } + enabled = true + } + } + + return enabled + } + + @objc(enableInputMode:forInputSourceBundleID:) + public static func enable(inputMode modeID: String, for bundleID: String) -> Bool { + for source in allInstalledInputSources() { + guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), + let modePtr = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else { + continue + } + let inputsSourceBundleID = Unmanaged.fromOpaque(bundleIDPtr).takeUnretainedValue() + let inputsSourceModeID = Unmanaged.fromOpaque(modePtr).takeUnretainedValue() + if modeID == String(inputsSourceModeID) && bundleID == String(inputsSourceBundleID) { + let enabled = enable(inputSource: source) + print("Attempt to enable input source of mode: \(modeID), bundle ID: \(bundleID), result: \(enabled)") + return enabled + } + + } + print("Failed to find any matching input source of mode: \(modeID), bundle ID: \(bundleID)") + return false + + } + + @objc(disableInputSource:) + public static func disable(inputSource: TISInputSource) -> Bool { + let status = TISDisableInputSource(inputSource) + return status == noErr + } + + @objc(registerInputSource:) + public static func registerTnputSource(at url: URL) -> Bool { + let status = TISRegisterInputSource(url as CFURL) + return status == noErr + } + +} + diff --git a/Source/Installer/AppDelegate.m b/Source/Installer/AppDelegate.m index 17485951..62f7acc5 100644 --- a/Source/Installer/AppDelegate.m +++ b/Source/Installer/AppDelegate.m @@ -27,7 +27,7 @@ #import "AppDelegate.h" #import -#import "OVInputSourceHelper.h" +#import "McBopomofoInstaller-Swift.h" static NSString *const kTargetBin = @"McBopomofo"; static NSString *const kTargetType = @"app"; @@ -182,13 +182,13 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { NSURL *imeBundleURL = imeBundle.bundleURL; NSString *imeIdentifier = imeBundle.bundleIdentifier; - TISInputSourceRef inputSource = [OVInputSourceHelper inputSourceForInputSourceID:imeIdentifier]; + TISInputSourceRef inputSource = [InputSourceHelper inputSourceForInputSourceID:imeIdentifier]; // if this IME name is not found in the list of available IMEs if (!inputSource) { NSLog(@"Registering input source %@ at %@.", imeIdentifier, imeBundleURL.absoluteString); // then register - BOOL status = [OVInputSourceHelper registerInputSource:imeBundleURL]; + BOOL status = [InputSourceHelper registerInputSource:imeBundleURL]; if (!status) { NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot register input source %@ at %@.", nil), imeIdentifier, imeBundleURL.absoluteString]; @@ -197,7 +197,7 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { return; } - inputSource = [OVInputSourceHelper inputSourceForInputSourceID:imeIdentifier]; + inputSource = [InputSourceHelper inputSourceForInputSourceID:imeIdentifier]; // if it still doesn't register successfully, bail. if (!inputSource) { NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot find input source %@ after registration.", nil), imeIdentifier]; @@ -219,10 +219,10 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { // as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not* // enabled in the user's current set of IMEs (which means the IME does not show up in // the user's input menu). - BOOL mainInputSourceEnabled = [OVInputSourceHelper inputSourceEnabled:inputSource]; + BOOL mainInputSourceEnabled = [InputSourceHelper inputSourceEnabled:inputSource]; if (!mainInputSourceEnabled || isMacOS12OrAbove) { - mainInputSourceEnabled = [OVInputSourceHelper enableInputSource:inputSource]; + mainInputSourceEnabled = [InputSourceHelper enableInputSource:inputSource]; if (mainInputSourceEnabled) { NSLog(@"Input method enabled: %@", imeIdentifier); } else { diff --git a/Source/McBopomofo-Bridging-Header.h b/Source/McBopomofo-Bridging-Header.h index 1b2cb5d6..032f1391 100644 --- a/Source/McBopomofo-Bridging-Header.h +++ b/Source/McBopomofo-Bridging-Header.h @@ -2,3 +2,7 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +extern void LTLoadLanguageModel(void); +extern void LTLoadUserLanguageModelFile(void); + + diff --git a/Source/NonModalAlertWindowController.swift b/Source/NonModalAlertWindowController.swift new file mode 100644 index 00000000..4f1b60eb --- /dev/null +++ b/Source/NonModalAlertWindowController.swift @@ -0,0 +1,132 @@ +// +// NonModalAlertWindowController.swift +// +// Copyright (c) 2011 The McBopomofo Project. +// +// Contributors: +// Mengjuei Hsieh (@mjhsieh) +// Weizhong Yang (@zonble) +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa + +@objc protocol NonModalAlertWindowControllerDelegate: AnyObject { + func nonModalAlertWindowControllerDidConfirm(_ controller: NonModalAlertWindowController) + func nonModalAlertWindowControllerDidCancel(_ controller: NonModalAlertWindowController) +} + +class NonModalAlertWindowController: NSWindowController { + @objc(sharedInstance) static let shared = NonModalAlertWindowController(windowNibName: "NonModalAlertWindowController") + + @IBOutlet weak var titleTextField: NSTextField! + @IBOutlet weak var contentTextField: NSTextField! + @IBOutlet weak var confirmButton: NSButton! + @IBOutlet weak var cancelButton: NSButton! + weak var delegate: NonModalAlertWindowControllerDelegate? + + @objc func show(title: String, content: String, confirmButtonTitle: String, cancelButtonTitle: String?, cancelAsDefault: Bool, delegate: NonModalAlertWindowControllerDelegate?) { + if window?.isVisible == true { + self.delegate?.nonModalAlertWindowControllerDidCancel(self) + } + + self.delegate = delegate + + var oldFrame = confirmButton.frame + confirmButton.title = confirmButtonTitle + confirmButton.sizeToFit() + + var newFrame = confirmButton.frame + newFrame.size.width = max(90, newFrame.size.width + 10) + newFrame.origin.x += oldFrame.size.width - newFrame.size.width + self.confirmButton.frame = newFrame + + if let cancelButtonTitle = cancelButtonTitle { + cancelButton.title = cancelButtonTitle + cancelButton.sizeToFit() + var adjustFrame = cancelButton.frame + adjustFrame.size.width = max(90, adjustFrame.size.width + 10) + adjustFrame.origin.x = newFrame.origin.x - adjustFrame.size.width + self.confirmButton.frame = adjustFrame + self.cancelButton.isHidden = false + } else { + self.cancelButton.isHidden = true + } + + cancelButton.nextKeyView = confirmButton + confirmButton.nextKeyView = cancelButton + + if cancelButtonTitle != nil { + if cancelAsDefault { + window?.defaultButtonCell = cancelButton.cell as? NSButtonCell + } else { + cancelButton.keyEquivalent = " " + window?.defaultButtonCell = confirmButton.cell as? NSButtonCell + } + } else { + window?.defaultButtonCell = confirmButton.cell as? NSButtonCell + } + + titleTextField.stringValue = title + + oldFrame = contentTextField.frame + contentTextField.stringValue = content + + var infiniteHeightFrame = oldFrame + infiniteHeightFrame.size.width -= 4.0 + infiniteHeightFrame.size.height = 10240 + newFrame = (content as NSString).boundingRect(with: infiniteHeightFrame.size, options: [.usesLineFragmentOrigin], attributes: [.font: contentTextField.font!]) + newFrame.size.width = max(newFrame.size.width, oldFrame.size.width) + newFrame.size.height += 4.0 + newFrame.origin = oldFrame.origin + newFrame.origin.y -= (newFrame.size.height - oldFrame.size.height) + contentTextField.frame = newFrame + + var windowFrame = window?.frame ?? NSRect.zero + windowFrame.size.height += (newFrame.size.height - oldFrame.size.height) + window?.level = NSWindow.Level(Int(CGShieldingWindowLevel()) + 1) + window?.setFrame(windowFrame, display: true) + window?.center() + window?.makeKeyAndOrderFront(self) + NSApp.activate(ignoringOtherApps: true) + } + + @IBAction func confirmButtonAction(_ sender: Any) { + delegate?.nonModalAlertWindowControllerDidConfirm(self) + window?.orderOut(self) + } + + @IBAction func cancelButtonAction(_ sender: Any) { + cancel(sender) + } + + func cancel(_ sender: Any) { + delegate?.nonModalAlertWindowControllerDidCancel(self) + delegate = nil + window?.orderOut(self) + } + +} diff --git a/Source/OVNonModalAlertWindowController.xib b/Source/NonModalAlertWindowController.xib similarity index 96% rename from Source/OVNonModalAlertWindowController.xib rename to Source/NonModalAlertWindowController.xib index 430b3b36..8c378007 100644 --- a/Source/OVNonModalAlertWindowController.xib +++ b/Source/NonModalAlertWindowController.xib @@ -1,12 +1,12 @@ - + - + - + @@ -21,7 +21,7 @@ - + diff --git a/Source/OVInputSourceHelper.h b/Source/OVInputSourceHelper.h deleted file mode 100644 index cc4c8919..00000000 --- a/Source/OVInputSourceHelper.h +++ /dev/null @@ -1,50 +0,0 @@ -// -// OVInputSourceHelper.h -// -// Copyright (c) 2010-2011 Lukhnos D. Liu (lukhnos at lukhnos dot org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import - -@interface OVInputSourceHelper : NSObject -// list all installed input sources -+ (NSArray *)allInstalledInputSources; - -// search for a certain input source -+ (TISInputSourceRef)inputSourceForProperty:(CFStringRef)inPropertyKey stringValue:(NSString *)inValue; - -// shorthand for -inputSourceForProerty:kTISPropertyInputSourceID stringValue: -+ (TISInputSourceRef)inputSourceForInputSourceID:(NSString *)inID; - -// enable/disable an input source (along with all its input modes) -+ (BOOL)inputSourceEnabled:(TISInputSourceRef)inInputSource; -+ (BOOL)enableInputSource:(TISInputSourceRef)inInputSource; -+ (BOOL)enableAllInputModesForInputSourceBundleID:(NSString *)inID; -+ (BOOL)enableInputMode:(NSString *)modeID forInputSourceBundleID:(NSString *)bundleID; -+ (BOOL)disableInputSource:(TISInputSourceRef)inInputSource; - -// register (i.e. make available to Input Source tab in Language & Text Preferences) -// an input source installed in (~)/Library/Input Methods or (~)/Library/Keyboard Layouts/ -+ (BOOL)registerInputSource:(NSURL *)inBundleURL; -@end diff --git a/Source/OVInputSourceHelper.m b/Source/OVInputSourceHelper.m deleted file mode 100644 index b7638a34..00000000 --- a/Source/OVInputSourceHelper.m +++ /dev/null @@ -1,122 +0,0 @@ -// -// OVInputSourceHelper.m -// -// Copyright (c) 2010-2011 Lukhnos D. Liu (lukhnos at lukhnos dot org) -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -#import "OVInputSourceHelper.h" - -@implementation OVInputSourceHelper -+ (NSArray *)allInstalledInputSources -{ - CFArrayRef list = TISCreateInputSourceList(NULL, true); - return (__bridge NSArray *)list; -// return [NSMakeCollectable(list) autorelease]; -} - -+ (TISInputSourceRef)inputSourceForProperty:(CFStringRef)inPropertyKey stringValue:(NSString *)inValue -{ - CFTypeID stringID = CFStringGetTypeID(); - - for (id source in [self allInstalledInputSources]) { - CFTypeRef property = TISGetInputSourceProperty((__bridge TISInputSourceRef)source, inPropertyKey); - if (!property || CFGetTypeID(property) != stringID) { - continue; - } - - if (inValue && [inValue compare:(__bridge NSString *)property] == NSOrderedSame) { - return (__bridge TISInputSourceRef)source; - } - } - return NULL; -} - -+ (TISInputSourceRef)inputSourceForInputSourceID:(NSString *)inID -{ - return [self inputSourceForProperty:kTISPropertyInputSourceID stringValue:inID]; -} - -+ (BOOL)inputSourceEnabled:(TISInputSourceRef)inInputSource -{ - CFBooleanRef value = TISGetInputSourceProperty(inInputSource, kTISPropertyInputSourceIsEnabled); - return value ? (BOOL)CFBooleanGetValue(value) : NO; -} - -+ (BOOL)enableInputSource:(TISInputSourceRef)inInputSource -{ - OSStatus status = TISEnableInputSource(inInputSource); - return status == noErr; -} - -+ (BOOL)enableAllInputModesForInputSourceBundleID:(NSString *)inID -{ - BOOL enabled = NO; - - for (id source in [self allInstalledInputSources]) { - TISInputSourceRef inputSource = (__bridge TISInputSourceRef)source; - NSString *bundleID = (__bridge NSString *)TISGetInputSourceProperty(inputSource, kTISPropertyBundleID); - NSString *mode = (NSString *)CFBridgingRelease(TISGetInputSourceProperty(inputSource, kTISPropertyInputModeID)); - if (mode && [bundleID isEqualToString:inID]) { - BOOL modeEnabled = [self enableInputSource:inputSource]; - if (!modeEnabled) { - return NO; - } - - enabled = YES; - } - } - - return enabled; -} - -+ (BOOL)enableInputMode:(NSString *)modeID forInputSourceBundleID:(NSString *)bundleID -{ - for (id source in [self allInstalledInputSources]) { - TISInputSourceRef inputSource = (__bridge TISInputSourceRef)source; - NSString *inputSoureBundleID = (__bridge NSString *)TISGetInputSourceProperty(inputSource, kTISPropertyBundleID); - NSString *inputSourceModeID = (NSString *)CFBridgingRelease(TISGetInputSourceProperty(inputSource, kTISPropertyInputModeID)); - - if ([modeID isEqual:inputSourceModeID] && [bundleID isEqual:inputSoureBundleID]) { - BOOL enabled = [self enableInputSource:inputSource]; - NSLog(@"Attempt to enable input source of mode: %@, bundle ID: %@, result: %d", modeID, bundleID, enabled); - return enabled; - } - } - - NSLog(@"Failed to find any matching input source of mode: %@, bundle ID: %@", modeID, bundleID); - return NO; -} - -+ (BOOL)disableInputSource:(TISInputSourceRef)inInputSource -{ - OSStatus status = TISDisableInputSource(inInputSource); - return status == noErr; -} - -+ (BOOL)registerInputSource:(NSURL *)inBundleURL -{ - OSStatus status = TISRegisterInputSource((__bridge CFURLRef)inBundleURL); - return status == noErr; -} -@end diff --git a/Source/OVNonModalAlertWindowController.h b/Source/OVNonModalAlertWindowController.h deleted file mode 100644 index e0eea1e2..00000000 --- a/Source/OVNonModalAlertWindowController.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// OVNonModalAlertWindowController.h -// OpenVanilla -// -// Created by Lukhnos Liu on 10/17/12. -// Copyright (c) 2012 The OpenVanilla Project. All rights reserved. -// - -#import - -@class OVNonModalAlertWindowController; - -@protocol OVNonModalAlertWindowControllerDelegate -- (void)nonModalAlertWindowControllerDidConfirm:(OVNonModalAlertWindowController *)controller; - -@optional -- (void)nonModalAlertWindowControllerDidCancel:(OVNonModalAlertWindowController *)controller; -@end - -@interface OVNonModalAlertWindowController : NSWindowController - -+ (OVNonModalAlertWindowController *)sharedInstance; -- (void)showWithTitle:(NSString *)title content:(NSString *)content confirmButtonTitle:(NSString *)confirmTitle cancelButtonTitle:(NSString *)cancelButtonTitle cancelAsDefault:(BOOL)cancelAsDefault delegate:(id)delegate; -- (IBAction)confirmButtonAction:(id)sender; -- (IBAction)cancelButtonAction:(id)sender; -@property (assign, nonatomic) IBOutlet NSTextField *titleTextField; -@property (assign, nonatomic) IBOutlet NSTextField *contentTextField; -@property (assign, nonatomic) IBOutlet NSButton *confirmButton; -@property (assign, nonatomic) IBOutlet NSButton *cancelButton; -@property (assign, nonatomic) id delegate; -@end diff --git a/Source/OVNonModalAlertWindowController.m b/Source/OVNonModalAlertWindowController.m deleted file mode 100644 index 48302d68..00000000 --- a/Source/OVNonModalAlertWindowController.m +++ /dev/null @@ -1,131 +0,0 @@ -// -// OVNonModalAlertWindowController.m -// OpenVanilla -// -// Created by Lukhnos Liu on 10/17/12. -// Copyright (c) 2012 The OpenVanilla Project. All rights reserved. -// - -#import "OVNonModalAlertWindowController.h" - -@implementation OVNonModalAlertWindowController -@synthesize titleTextField = _titleTextField; -@synthesize contentTextField = _contentTextField; -@synthesize confirmButton = _confirmButton; -@synthesize cancelButton = _cancelButton; -@synthesize delegate = _delegate; - -+ (OVNonModalAlertWindowController *)sharedInstance -{ - static OVNonModalAlertWindowController *instance; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[OVNonModalAlertWindowController alloc] initWithWindowNibName:@"OVNonModalAlertWindowController"]; - [instance window]; - }); - return instance; -} - -// Suppress the use of the MIN/MAX macros -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wgnu-statement-expression" - -- (void)showWithTitle:(NSString *)title content:(NSString *)content confirmButtonTitle:(NSString *)confirmTitle cancelButtonTitle:(NSString *)cancelButtonTitle cancelAsDefault:(BOOL)cancelAsDefault delegate:(id )delegate; -{ - // cancel previous alert - if (self.window.visible) { - if ([_delegate respondsToSelector:@selector(nonModalAlertWindowControllerDidCancel:)]) { - [_delegate nonModalAlertWindowControllerDidCancel:self]; - } - } - - _delegate = delegate; - - NSRect oldFrame = self.confirmButton.frame; - [self.confirmButton setTitle:confirmTitle]; - [self.confirmButton sizeToFit]; - - NSRect newFrame = self.confirmButton.frame; - - newFrame.size.width = MAX(90.0, (newFrame.size.width + 10.0)); - newFrame.origin.x += (oldFrame.size.width - newFrame.size.width); - [self.confirmButton setFrame:newFrame]; - - if (cancelButtonTitle) { - [self.cancelButton setTitle:cancelButtonTitle]; - [self.cancelButton sizeToFit]; - NSRect adjustedFrame = self.cancelButton.frame; - adjustedFrame.size.width = MAX(90.0, (adjustedFrame.size.width + 10.0)); - adjustedFrame.origin.x = newFrame.origin.x - adjustedFrame.size.width; - self.cancelButton.frame = adjustedFrame; - self.cancelButton.hidden = NO; - } - else { - self.cancelButton.hidden = YES; - } - - self.cancelButton.nextKeyView = self.confirmButton; - self.confirmButton.nextKeyView = self.cancelButton; - - if (cancelButtonTitle) { - if (cancelAsDefault) { - [self.window setDefaultButtonCell:self.cancelButton.cell]; - } - else { - self.cancelButton.keyEquivalent = @" "; - [self.window setDefaultButtonCell:self.confirmButton.cell]; - } - } - else { - [[self window] setDefaultButtonCell:self.confirmButton.cell]; - } - - self.titleTextField.stringValue = title; - - oldFrame = [self.contentTextField frame]; - self.contentTextField.stringValue = content; - - NSRect infiniteHeightFrame = oldFrame; - infiniteHeightFrame.size.width -= 4.0; - infiniteHeightFrame.size.height = 10240; - newFrame = [content boundingRectWithSize:infiniteHeightFrame.size options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: self.contentTextField.font}]; - newFrame.size.width = MAX(newFrame.size.width, oldFrame.size.width); - newFrame.size.height += 4.0; - newFrame.origin = oldFrame.origin; - newFrame.origin.y -= (newFrame.size.height - oldFrame.size.height); - [self.contentTextField setFrame:newFrame]; - - NSRect windowFrame = [[self window] frame]; - windowFrame.size.height += (newFrame.size.height - oldFrame.size.height); - - self.window.level = CGShieldingWindowLevel() + 1; - [self.window setFrame:windowFrame display:YES]; - [self.window center]; - [self.window makeKeyAndOrderFront:self]; - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; -} - -#pragma GCC diagnostic pop - -- (IBAction)confirmButtonAction:(id)sender -{ - [_delegate nonModalAlertWindowControllerDidConfirm:self]; - [self.window orderOut:self]; -} - -- (IBAction)cancelButtonAction:(id)sender -{ - [self cancel:sender]; -} - -- (void)cancel:(id)sender -{ - if ([_delegate respondsToSelector:@selector(nonModalAlertWindowControllerDidCancel:)]) { - [_delegate nonModalAlertWindowControllerDidCancel:self]; - } - - _delegate = nil; - [self.window orderOut:self]; -} - -@end diff --git a/Source/OpenCCBridge.swift b/Source/OpenCCBridge.swift index ad53ccee..582ec8e0 100644 --- a/Source/OpenCCBridge.swift +++ b/Source/OpenCCBridge.swift @@ -3,7 +3,7 @@ import OpenCC // Since SwiftyOpenCC only provide Swift classes, we create an NSObject subclass // in Swift in order to bridge the Swift classes into our Objective-C++ project. -class OpenCCBridge : NSObject { +class OpenCCBridge: NSObject { private static let shared = OpenCCBridge() private var converter: ChineseConverter? diff --git a/Source/PreferencesWindowController.h b/Source/PreferencesWindowController.h deleted file mode 100644 index 538ddd5b..00000000 --- a/Source/PreferencesWindowController.h +++ /dev/null @@ -1,50 +0,0 @@ -// -// PreferencesWindowController.h -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// -#import - -@interface PreferencesWindowController : NSWindowController -{ -@private - NSPopUpButton *__weak _fontSizePopUpButton; - NSPopUpButton *__weak _basisKeyboardLayoutButton; - NSComboBox *__weak _selectionKeyComboBox; -} - -- (IBAction)updateBasisKeyboardLayoutAction:(id)sender; -- (IBAction)changeSelectionKeyAction:(id)sender; - -@property (weak, nonatomic) IBOutlet NSPopUpButton *fontSizePopUpButton; -@property (weak, nonatomic) IBOutlet NSPopUpButton *basisKeyboardLayoutButton; -@property (weak, nonatomic) IBOutlet NSComboBox *selectionKeyComboBox; -@end diff --git a/Source/PreferencesWindowController.m b/Source/PreferencesWindowController.m deleted file mode 100644 index 29253196..00000000 --- a/Source/PreferencesWindowController.m +++ /dev/null @@ -1,139 +0,0 @@ -// -// PreferencesWindowController.m -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// -#import "PreferencesWindowController.h" -#import - -static NSString *const kBasisKeyboardLayoutPreferenceKey = @"BasisKeyboardLayout"; // alphanumeric ("ASCII") input basis -static NSString *const kCandidateKeys = @"CandidateKeys"; -static NSString *const kDefaultKeys = @"123456789"; - -@implementation PreferencesWindowController -@synthesize fontSizePopUpButton = _fontSizePopUpButton; -@synthesize basisKeyboardLayoutButton = _basisKeyboardLayoutButton; -@synthesize selectionKeyComboBox = _selectionKeyComboBox; - -- (void)awakeFromNib -{ - CFArrayRef list = TISCreateInputSourceList(NULL, true); - NSMenuItem *usKeyboardLayoutItem = nil; - NSMenuItem *chosenItem = nil; - - [self.basisKeyboardLayoutButton.menu removeAllItems]; - - NSString *basisKeyboardLayoutID = [[NSUserDefaults standardUserDefaults] stringForKey:kBasisKeyboardLayoutPreferenceKey]; - - for (int i = 0; i < CFArrayGetCount(list); i++) { - TISInputSourceRef source = (TISInputSourceRef)CFArrayGetValueAtIndex(list, i); - - CFStringRef category = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory); - if (CFStringCompare(category, kTISCategoryKeyboardInputSource, 0) != kCFCompareEqualTo) { - continue; - } - - CFBooleanRef asciiCapable = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsASCIICapable); - if (!CFBooleanGetValue(asciiCapable)) { - continue; - } - - CFStringRef sourceType = TISGetInputSourceProperty(source, kTISPropertyInputSourceType); - if (CFStringCompare(sourceType, kTISTypeKeyboardLayout, 0) != kCFCompareEqualTo) { - continue; - } - - NSString *sourceID = (__bridge NSString *)TISGetInputSourceProperty(source, kTISPropertyInputSourceID); - NSString *localizedName = (__bridge NSString *)TISGetInputSourceProperty(source, kTISPropertyLocalizedName); - - NSMenuItem *item = [[NSMenuItem alloc] init]; - item.title = localizedName; - item.representedObject = sourceID; - - if ([sourceID isEqualToString:@"com.apple.keylayout.US"]) { - usKeyboardLayoutItem = item; - } - - // false if nil - if ([basisKeyboardLayoutID isEqualToString:sourceID]) { - chosenItem = item; - } - - [self.basisKeyboardLayoutButton.menu addItem:item]; - } - - [self.basisKeyboardLayoutButton selectItem:(chosenItem ? chosenItem : usKeyboardLayoutItem)]; - CFRelease(list); - - self.selectionKeyComboBox.usesDataSource = NO; - [self.selectionKeyComboBox removeAllItems]; - [self.selectionKeyComboBox addItemsWithObjectValues:@[ - kDefaultKeys, - @"asdfghjkl", - @"asdfzxcvb" - ]]; - - NSString *ckeys = [[NSUserDefaults standardUserDefaults] stringForKey:kCandidateKeys]; - if (!ckeys || [ckeys stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length == 0) { - ckeys = kDefaultKeys; - } - - [self.selectionKeyComboBox setStringValue:ckeys]; -} - -- (IBAction)updateBasisKeyboardLayoutAction:(id)sender -{ - NSString *sourceID = [[self.basisKeyboardLayoutButton selectedItem] representedObject]; - if (sourceID) { - [[NSUserDefaults standardUserDefaults] setObject:sourceID forKey:kBasisKeyboardLayoutPreferenceKey]; - } -} - -- (IBAction)changeSelectionKeyAction:(id)sender -{ - NSString *keys = [[sender stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - if (keys.length != 9 || - ![keys canBeConvertedToEncoding:NSASCIIStringEncoding]) { - [self.selectionKeyComboBox setStringValue:kDefaultKeys]; - [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCandidateKeys]; - NSBeep(); - return; - } - - [self.selectionKeyComboBox setStringValue:keys]; - if ([keys isEqualToString:kDefaultKeys]) { - [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCandidateKeys]; - } else { - [[NSUserDefaults standardUserDefaults] setObject:keys forKey:kCandidateKeys]; - } -} - -@end diff --git a/Source/PreferencesWindowController.swift b/Source/PreferencesWindowController.swift new file mode 100644 index 00000000..59d490dd --- /dev/null +++ b/Source/PreferencesWindowController.swift @@ -0,0 +1,144 @@ +// +// PreferencesWindowController.swift +// +// Copyright (c) 2011 The McBopomofo Project. +// +// Contributors: +// Mengjuei Hsieh (@mjhsieh) +// Weizhong Yang (@zonble) +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa +import Carbon + +private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout" +private let kCandidateKeys = "CandidateKeys" +private let kDefaultKeys = "123456789" + +// Please note that the class should be exposed as "PreferencesWindowController" +// in Objective-C in order to let IMK to see the same class name as +// the "InputMethodServerPreferencesWindowControllerClass" in Info.plist. +@objc(PreferencesWindowController) class PreferencesWindowController: NSWindowController { + @IBOutlet weak var fontSizePopUpButton: NSPopUpButton! + @IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton! + @IBOutlet weak var selectionKeyComboBox: NSComboBox! + + override func awakeFromNib() { + let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] + var usKeyboardLayoutItem: NSMenuItem? = nil + var chosenItem: NSMenuItem? = nil + + basisKeyboardLayoutButton.menu?.removeAllItems() + + let basisKeyboardLayoutID = UserDefaults.standard.string(forKey: kBasisKeyboardLayoutPreferenceKey) + for source in list { + if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) { + let category = Unmanaged.fromOpaque(categoryPtr).takeUnretainedValue() + if category != kTISCategoryKeyboardInputSource { + continue + } + } else { + continue + } + + if let asciiCapablePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsASCIICapable) { + let asciiCapable = Unmanaged.fromOpaque(asciiCapablePtr).takeUnretainedValue() + if asciiCapable != kCFBooleanTrue { + continue + } + } else { + continue + } + + if let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) { + let sourceType = Unmanaged.fromOpaque(sourceTypePtr).takeUnretainedValue() + if sourceType != kTISTypeKeyboardLayout { + continue + } + } else { + continue + } + + guard let sourceIDPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), + let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName) else { + continue + } + + let sourceID = String(Unmanaged.fromOpaque(sourceIDPtr).takeUnretainedValue()) + let localizedName = String(Unmanaged.fromOpaque(localizedNamePtr).takeUnretainedValue()) + + let menuItem = NSMenuItem() + menuItem.title = localizedName + menuItem.representedObject = sourceID + + if sourceID == "com.apple.keylayout.US" { + usKeyboardLayoutItem = menuItem + } + if basisKeyboardLayoutID == sourceID { + chosenItem = menuItem + } + basisKeyboardLayoutButton.menu?.addItem(menuItem) + } + + basisKeyboardLayoutButton.select(chosenItem ?? usKeyboardLayoutItem) + selectionKeyComboBox.usesDataSource = false + selectionKeyComboBox.addItems(withObjectValues: [kDefaultKeys, "asdfghjkl", "asdfzxcvb"]) + + var candidateSelectionKeys = (UserDefaults.standard.string(forKey: kCandidateKeys) ?? kDefaultKeys) + .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + if candidateSelectionKeys.isEmpty { + candidateSelectionKeys = kDefaultKeys + } + + selectionKeyComboBox.stringValue = candidateSelectionKeys + } + + @IBAction func updateBasisKeyboardLayoutAction(_ sender:Any) { + if let sourceID = basisKeyboardLayoutButton.selectedItem?.representedObject { + UserDefaults.standard.set(sourceID, forKey: kBasisKeyboardLayoutPreferenceKey) + } + } + + @IBAction func changeSelectionKeyAction(_ sender: Any) { + let keys = (sender as AnyObject).stringValue.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + if keys.count != 9 || !keys.canBeConverted(to: .ascii) { + selectionKeyComboBox.stringValue = kDefaultKeys + UserDefaults.standard.removeObject(forKey: kCandidateKeys) + NSSound.beep() + return + } + + selectionKeyComboBox.stringValue = keys + if keys == kDefaultKeys { + UserDefaults.standard.removeObject(forKey: kCandidateKeys) + } else { + UserDefaults.standard.set(keys, forKey: kCandidateKeys) + } + } + +} + diff --git a/Source/main.m b/Source/main.m index bca49bd0..a98863b0 100644 --- a/Source/main.m +++ b/Source/main.m @@ -33,7 +33,8 @@ // #import -#import "OVInputSourceHelper.h" +#import +#import "McBopomofo-Swift.h" static NSString *const kConnectionName = @"McBopomofo_1_Connection"; @@ -54,20 +55,20 @@ int main(int argc, char *argv[]) bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]; } - TISInputSourceRef inputSource = [OVInputSourceHelper inputSourceForInputSourceID:bundleID]; + TISInputSourceRef inputSource = [InputSourceHelper inputSourceForInputSourceID:bundleID]; // if this IME name is not found in the list of available IMEs if (!inputSource) { NSLog(@"Registering input source %@ at %@.", bundleID, [bundleURL absoluteString]); // then register - BOOL status = [OVInputSourceHelper registerInputSource:bundleURL]; + BOOL status = [InputSourceHelper registerInputSource:bundleURL]; if (!status) { NSLog(@"Fatal error: Cannot register input source %@ at %@.", bundleID, [bundleURL absoluteString]); return -1; } - inputSource = [OVInputSourceHelper inputSourceForInputSourceID:bundleID]; + inputSource = [InputSourceHelper inputSourceForInputSourceID:bundleID]; // if it still doesn't register successfully, bail. if (!inputSource) { NSLog(@"Fatal error: Cannot find input source %@ after registration.", bundleID); @@ -76,22 +77,22 @@ int main(int argc, char *argv[]) } // if it's not enabled, just enabled it - if (inputSource && ![OVInputSourceHelper inputSourceEnabled:inputSource]) { + if (inputSource && ![InputSourceHelper inputSourceEnabled:inputSource]) { NSLog(@"Enabling input source %@ at %@.", bundleID, [bundleURL absoluteString]); - BOOL status = [OVInputSourceHelper enableInputSource:inputSource]; + BOOL status = [InputSourceHelper enableInputSource:inputSource]; if (!status) { NSLog(@"Fatal error: Cannot enable input source %@.", bundleID); return -1; } - if (![OVInputSourceHelper inputSourceEnabled:inputSource]){ + if (![InputSourceHelper inputSourceEnabled:inputSource]){ NSLog(@"Fatal error: Cannot enable input source %@.", bundleID); return -1; } } if (argc > 2 && !strcmp(argv[2], "--all")) { - BOOL enabled = [OVInputSourceHelper enableAllInputModesForInputSourceBundleID:bundleID]; + BOOL enabled = [InputSourceHelper enableAllInputModesForInputSourceBundleID:bundleID]; if (enabled) { NSLog(@"All input sources enabled for %@", bundleID); }