Skip to content

Commit

Permalink
Merge pull request #12571 from keymanapp/fix/ios/5897-adjustable-keyb…
Browse files Browse the repository at this point in the history
…oard-height

feat(ios): configurable keyboard height
  • Loading branch information
sgschantz authored Dec 16, 2024
2 parents a6af3c7 + 5135b03 commit faf2a5b
Show file tree
Hide file tree
Showing 27 changed files with 820 additions and 42 deletions.
8 changes: 8 additions & 0 deletions ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
1645D5952036C6FF0076C51B /* KeymanPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D5942036C6FF0076C51B /* KeymanPackage.swift */; };
1645D5972036C9F80076C51B /* KMPKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D5962036C9F80076C51B /* KMPKeyboard.swift */; };
165EB3A12098993900040A69 /* KeyboardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB3A02098993900040A69 /* KeyboardError.swift */; };
29084CAB2CD48B5D004070E7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 29084CAA2CD48B5D004070E7 /* Images.xcassets */; };
296EF2C72AFA26C700E3E384 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */; };
29B30C232B564F9900C342A4 /* KeymanEngineLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B30C222B564F9900C342A4 /* KeymanEngineLogger.swift */; };
29E202BD2CCB7541008B4740 /* KeyboardHeightViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E202BC2CCB7541008B4740 /* KeyboardHeightViewController.swift */; };
377D10DE26846B8900467431 /* SpacebarTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D10DD26846B8900467431 /* SpacebarTextViewController.swift */; };
6CD5DFAA150F6DC8007A5DDE /* icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 6CD5DFA8150F6DC8007A5DDE /* icon.png */; };
6CD5DFAB150F6DC8007A5DDE /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = 6CD5DFA9150F6DC8007A5DDE /* [email protected] */; };
Expand Down Expand Up @@ -298,6 +300,7 @@
1645D5942036C6FF0076C51B /* KeymanPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanPackage.swift; sourceTree = "<group>"; };
1645D5962036C9F80076C51B /* KMPKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPKeyboard.swift; sourceTree = "<group>"; };
165EB3A02098993900040A69 /* KeyboardError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardError.swift; sourceTree = "<group>"; };
29084CAA2CD48B5D004070E7 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
293EA3DB2705955300545EED /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/ResourceInfoView.strings; sourceTree = "<group>"; };
293EA3DC2705964200545EED /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/Localizable.strings; sourceTree = "<group>"; };
293EA3DD270596B700545EED /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ha; path = ha.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
Expand Down Expand Up @@ -333,6 +336,7 @@
29C1E17128001F7600759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/ResourceInfoView.strings"; sourceTree = "<group>"; };
29C1E17228001F8800759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
29C1E17328001FA200759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
29E202BC2CCB7541008B4740 /* KeyboardHeightViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardHeightViewController.swift; sourceTree = "<group>"; };
377D10DD26846B8900467431 /* SpacebarTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacebarTextViewController.swift; sourceTree = "<group>"; };
6C0A140E151EA930007FA4AD /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
6C0A140F151EA930007FA4AD /* Keyman.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; lineEnding = 0; path = Keyman.xcconfig; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.xcconfig; };
Expand Down Expand Up @@ -705,6 +709,7 @@
9A60763C22892485003BCFBA /* Settings.storyboard */,
9A60764322893A4E003BCFBA /* SettingsViewController.swift */,
377D10DD26846B8900467431 /* SpacebarTextViewController.swift */,
29E202BC2CCB7541008B4740 /* KeyboardHeightViewController.swift */,
);
name = Settings;
sourceTree = "<group>";
Expand Down Expand Up @@ -760,6 +765,7 @@
CEA1486F2407808F00C6ECD2 /* Localizable.strings */,
CE96E42D24D1229A005B8E5A /* Localizable.stringsdict */,
CE7A26D023CEE5790005955C /* Keyboard Colors.xcassets */,
29084CAA2CD48B5D004070E7 /* Images.xcassets */,
C06D372E1F81F4E100F61AE0 /* Info.plist */,
F273AB9615641D9300A47CEE /* Classes */,
);
Expand Down Expand Up @@ -1260,6 +1266,7 @@
buildActionMask = 2147483647;
files = (
C06D37601F82095200F61AE0 /* Keyman.bundle in Resources */,
29084CAB2CD48B5D004070E7 /* Images.xcassets in Resources */,
CEA1486C2407808F00C6ECD2 /* Localizable.strings in Resources */,
9ADC459F22E1895D004C78C6 /* LanguageLMDetailViewController.xib in Resources */,
CEA14870240780E100C6ECD2 /* ResourceInfoView.xib in Resources */,
Expand Down Expand Up @@ -1447,6 +1454,7 @@
CE67D961228A6F190029F2B5 /* KeyboardCommandStructs.swift in Sources */,
CE87751E24C68DA500B1475A /* KeyboardSearchViewController.swift in Sources */,
CE8B0BBF248764ED0045EB2E /* KMPResource.swift in Sources */,
29E202BD2CCB7541008B4740 /* KeyboardHeightViewController.swift in Sources */,
9AD4F53C229F85AC007992D3 /* LanguageSettingsViewController.swift in Sources */,
CE969BE8251AD8B500376D6A /* PackageWebViewController.swift in Sources */,
CE7A26DB23CEEF640005955C /* Colors.swift in Sources */,
Expand Down
5 changes: 5 additions & 0 deletions ios/engine/KMEI/KeymanEngine/Classes/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public enum Key {
static let synchronizeSWLexicalModel = "KeymanSynchronizeSWLexicalModel"

static let migrationLevel = "KeymanEngineMigrationLevel"
static let portraitKeyboardHeight = "PortraitKeyboardHeight"
static let landscapeKeyboardHeight = "LandscapeKeyboardHeight"

// JSON keys for language REST calls
static let options = "options"
Expand Down Expand Up @@ -93,6 +95,9 @@ public enum Defaults {
public static let lexicalModel: InstallableLexicalModel = {
return lexicalModelPackage.findResource(withID: lexicalModelID)!
}()

// default for ancient/unrecognized devices
static let unknownDeviceKeyboardHeight: CGFloat = 216.0
}

public enum Resources {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,27 @@ public extension UserDefaults {
set(prefs, forKey: Key.userCorrectSettings)
}
}


var portraitKeyboardHeight: Double {
get {
return double(forKey: Key.portraitKeyboardHeight)
}

set(height) {
set(height, forKey: Key.portraitKeyboardHeight)
}
}

var landscapeKeyboardHeight: Double {
get {
return double(forKey: Key.landscapeKeyboardHeight)
}

set(height) {
set(height, forKey: Key.landscapeKeyboardHeight)
}
}

func predictSettingForLanguage(languageID: String) -> Bool {
if let dict = predictionEnablements {
return dict[languageID] ?? true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ private class CustomInputView: UIInputView, UIInputViewAudioFeedback {
}

func setConstraints() {
os_log("CustomInputView setConstraints", log: KeymanEngineLogger.ui, type: .info)
let innerView = keymanWeb.view!
let guide = self.safeAreaLayoutGuide

Expand All @@ -88,18 +89,48 @@ private class CustomInputView: UIInputView, UIInputViewAudioFeedback {
kbdWidthConstraint.priority = .defaultHigh
kbdWidthConstraint.isActive = true

let bannerHeight = InputViewController.topBarHeight
self.buildKeyboardHeightConstraints(bannerHeight: InputViewController.topBarHeight)
}

/**
* Due to new custom keyboard height as chosen by the user.
* The value for the new keyboard height originates from KeyboardHeightViewController.
*/
func keyboardHeightChanged() {
os_log("CustomInputView keyboardHeightChanged", log: KeymanEngineLogger.ui, type: .info)

// deactivate constraints for both orientations (though one should already be inactive)
landscapeConstraint?.isActive = false
portraitConstraint?.isActive = false

// rebuild both portrait and landscape constraints
self.buildKeyboardHeightConstraints(bannerHeight: InputViewController.topBarHeight)

// activate constraints for the current orientation
if InputViewController.isPortrait {
portraitConstraint?.isActive = true
} else {
landscapeConstraint?.isActive = true
}

self.setNeedsLayout()
}

private func buildKeyboardHeightConstraints(bannerHeight: CGFloat) {
os_log("CustomInputView buildKeyboardHeightConstraints", log: KeymanEngineLogger.ui, type: .info)
let innerView = keymanWeb.view!

// Cannot be met by the in-app keyboard, but helps to 'force' height for the system keyboard.
let portraitHeight = innerView.heightAnchor.constraint(equalToConstant: bannerHeight + keymanWeb.constraintTargetHeight(isPortrait: true))
portraitHeight.identifier = "Height constraint for portrait mode"
portraitHeight.priority = .defaultHigh
let landscapeHeight = innerView.heightAnchor.constraint(equalToConstant: bannerHeight + keymanWeb.constraintTargetHeight(isPortrait: false))
landscapeHeight.identifier = "Height constraint for landscape mode"
landscapeHeight.priority = .defaultHigh

portraitConstraint = portraitHeight
landscapeConstraint = landscapeHeight
let portraitHeightConstraint = innerView.heightAnchor.constraint(equalToConstant: bannerHeight + keymanWeb.readKeyboardHeight(isPortrait: true)!)
portraitHeightConstraint.identifier = "Height constraint for portrait mode"
portraitHeightConstraint.priority = .defaultHigh

let landscapeHeightConstraint = innerView.heightAnchor.constraint(equalToConstant: bannerHeight + keymanWeb.readKeyboardHeight(isPortrait: false)!)
landscapeHeightConstraint.identifier = "Height constraint for landscape mode"
landscapeHeightConstraint.priority = .defaultHigh

portraitConstraint = portraitHeightConstraint
landscapeConstraint = landscapeHeightConstraint
// .isActive will be set according to the current portrait/landscape perspective.
}

Expand Down Expand Up @@ -153,7 +184,7 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
}

var expandedHeight: CGFloat {
return keymanWeb.keyboardHeight + InputViewController.topBarHeight
return keymanWeb.keyboardSize.height + InputViewController.topBarHeight
}

public convenience init() {
Expand Down Expand Up @@ -504,21 +535,32 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
}

public var kmwHeight: CGFloat {
return keymanWeb.keyboardHeight
return keymanWeb.keyboardSize.height
}

func clearModel() {
keymanWeb.activeModel = false
}

private func setInnerConstraints() {
let iv = self.inputView as! CustomInputView
iv.setConstraints()
let customInputView = self.inputView as! CustomInputView
customInputView.setConstraints()

self.updateViewConstraints()
fixLayout()
}

/**
* Due to new custom keyboard height as chosen by the user.
* The value for the new keyboard height originates from KeyboardHeightViewController.
*/
func keyboardHeightChanged() {
os_log("InputViewController keyboardHeightChanged", log: KeymanEngineLogger.ui, type: .debug)
if let customInputView = self.inputView as? CustomInputView {
customInputView.keyboardHeightChanged()
}
}

func fixLayout() {
view.setNeedsLayout()
view.layoutIfNeeded()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ class KeyboardScaleMap {
screenSize: CGSize = UIScreen.main.bounds.size,
asPhone: Bool? = nil) -> KeyboardSize? {
if let scaling = shared.scalings[KeyboardScaleMap.hashKey(for: device)] {
let kbHeight = (forPortrait ? scaling.portrait : scaling.landscape).keyboardHeight
os_log("KeyboardScaleMap getDeviceDefaultKeyboardScale keyboard height: %f", log:KeymanEngineLogger.ui, type: .debug, kbHeight)
return forPortrait ? scaling.portrait : scaling.landscape
}

Expand All @@ -240,6 +242,9 @@ class KeyboardScaleMap {
// keyboard dimensions. It's not a perfect rule, but should suffice for a stop-gap solution.
let scaling = shared.scalings[KeyboardScaleMap.hashKey(for: mappedDevice)]!

let kbHeight = (forPortrait ? scaling.portrait : scaling.landscape).keyboardHeight
os_log("KeyboardScaleMap getDeviceDefaultKeyboardScale keyboard height for missing device: %f", log:KeymanEngineLogger.ui, type: .debug, kbHeight)

return forPortrait ? scaling.portrait : scaling.landscape
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class KeymanWebViewController: UIViewController {
// after it has been replaced by KMW's OSK resizing operation.)

keyboardSize = view.bounds.size
os_log("KeymanWebViewController viewWillLayoutSubviews to keyboardSize %{public}s", log:KeymanEngineLogger.ui, type: .debug, NSCoder.string(for:keyboardSize))
}

open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
Expand Down Expand Up @@ -695,36 +696,100 @@ extension KeymanWebViewController: KeymanWebDelegate {
// MARK: - Manage views
extension KeymanWebViewController {
// MARK: - Sizing
public var keyboardHeight: CGFloat {
return keyboardSize.height
}

@objc func menuKeyHeld(_ keymanWeb: KeymanWebViewController) {
self.delegate?.menuKeyHeld(self)
}

func constraintTargetHeight(isPortrait: Bool) -> CGFloat {
return KeyboardScaleMap.getDeviceDefaultKeyboardScale(forPortrait: isPortrait)?.keyboardHeight ?? 216 // default for ancient devices
}
func determineDefaultKeyboardHeight(isPortrait: Bool) -> CGFloat {
os_log("determineDefaultKeyboardHeight", log:KeymanEngineLogger.ui, type: .info)

var keyboardWidth: CGFloat {
return keyboardSize.width
let keyboardHeight = KeyboardScaleMap.getDeviceDefaultKeyboardScale(forPortrait: isPortrait)?.keyboardHeight ?? Defaults.unknownDeviceKeyboardHeight

return keyboardHeight;
}

func initKeyboardSize() {
var width: CGFloat
var height: CGFloat
let width: CGFloat
width = UIScreen.main.bounds.width

if Util.isSystemKeyboard {
height = constraintTargetHeight(isPortrait: InputViewController.isPortrait)
var height: CGFloat

// get orientation differently if system or in-app keyboard
let portrait = Util.isSystemKeyboard ? InputViewController.isPortrait : UIDevice.current.orientation.isPortrait

/**
* If keyboard height is saved in UserDefaults, then use it
*/
if let savedHeight = self.readKeyboardHeight(isPortrait: portrait) {
height = savedHeight
} else {
height = constraintTargetHeight(isPortrait: UIDevice.current.orientation.isPortrait)
/**
* Otherwise, get default keyboard height for this orientation and write to UserDefaults
*/
height = self.determineDefaultKeyboardHeight(isPortrait: portrait)
self.writeKeyboardHeightIfDoesNotExist(isPortrait: portrait, height: height)

/**
* If we need to write out the keyboard height for one orientation, then we
* expect that the other must be written also.
* Write it out now, but only if a value for keyboard height does not already exist.
*/
height = self.determineDefaultKeyboardHeight(isPortrait: !portrait)
self.writeKeyboardHeightIfDoesNotExist(isPortrait: !portrait, height: height)
}

/**
* no need to check for Util.isSystemKeyboard because this is shared storage
* the UserDefaults are readable and writeable by both system and in-app
*/

keyboardSize = CGSize(width: width, height: height)
self.keyboardSize = CGSize(width: width, height: height)
os_log("KeymanWebViewController initKeyboardSize %{public}s", log:KeymanEngineLogger.ui, type: .default, NSCoder.string(for:keyboardSize))
}

/**
* reads and returns keyboard height if it is found in UserDefaults, otherwise returns nil
*/
func readKeyboardHeight(isPortrait: Bool) -> CGFloat? {
var height: CGFloat? = nil

if (isPortrait) {
if (Storage.active.userDefaults.object(forKey: Key.portraitKeyboardHeight) != nil) {
height = Storage.active.userDefaults.portraitKeyboardHeight
}
} else { // landscape
if (Storage.active.userDefaults.object(forKey: Key.landscapeKeyboardHeight) != nil) {
height = Storage.active.userDefaults.landscapeKeyboardHeight
}
}

let message = "readKeyboardHeight, for isPortrait \(isPortrait) value \(String(describing: height))"
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .info, message)

return height;
}

/**
* Write out the keyboard height to the UserDefaults but only if it does not exist there yet.
* If it exists, then we assume it was configured by the user and do not want to
* overwrite that value with a default value derived for this device.
*/
func writeKeyboardHeightIfDoesNotExist(isPortrait: Bool, height: CGFloat) {
let writeMessage = "writeKeyboardHeightIfDoesNotExist, isPortrait: \(isPortrait) height: \(height)"
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .info, writeMessage)
if (isPortrait) {
if (Storage.active.userDefaults.object(forKey: Key.portraitKeyboardHeight) == nil) {
Storage.active.userDefaults.portraitKeyboardHeight = height
os_log("portrait keyboardHeight default value written", log:KeymanEngineLogger.ui, type: .info, writeMessage)
}
} else {
if (Storage.active.userDefaults.object(forKey: Key.landscapeKeyboardHeight) == nil) {
Storage.active.userDefaults.landscapeKeyboardHeight = height
os_log("landscape keyboardHeight default value written", log:KeymanEngineLogger.ui, type: .info, writeMessage)
}
}
}

var keyboardSize: CGSize {
get {
if kbSize.equalTo(CGSize.zero) {
Expand All @@ -750,15 +815,6 @@ extension KeymanWebViewController {
}
}

@objc func resizeDelay() {
// + 1000 to work around iOS bug with resizing on landscape orientation. Technically we only
// need this for landscape but it doesn't hurt to do it with both. 1000 is a big number that
// should hopefully work on all devices.
let kbWidth = keyboardWidth
let kbHeight = keyboardHeight
view.frame = CGRect(x: 0.0, y: 0.0, width: kbWidth, height: kbHeight + 1000)
}

// Keyman interaction
func resizeKeyboard() {
fixLayout()
Expand All @@ -768,6 +824,7 @@ extension KeymanWebViewController {
// the first time.
setOskWidth(Int(kbSize.width))
setOskHeight(Int(kbSize.height))
os_log("KeymanWebViewController resizeKeyboard to kbSize %{public}s", log:KeymanEngineLogger.ui, type: .debug, NSCoder.string(for:kbSize))
}

func resetKeyboardState() {
Expand Down
Loading

0 comments on commit faf2a5b

Please sign in to comment.