mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-06 08:55:34 +00:00
345 lines
13 KiB
Swift
345 lines
13 KiB
Swift
//
|
|
// KeyboardMenuView.swift
|
|
// KMEI
|
|
//
|
|
// Created by Gabriel Wong on 2017-09-28.
|
|
// Copyright © 2017 SIL International. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
// Used by the system keyboard as its globe key menu.
|
|
class KeyboardMenuView: UIView, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate {
|
|
private let bgColor = Colors.systemBackground
|
|
private let bgColor2 = Colors.systemBackground
|
|
private let borderColor = Colors.popupBorder
|
|
private var borderRadius: CGFloat = 5.0
|
|
private var strokeWidth: CGFloat = 0.75
|
|
|
|
private var menuFrame: CGRect = .zero
|
|
private let keyFrame: CGRect
|
|
private let rowHeight: CGFloat
|
|
private let fontSize: CGFloat
|
|
private let xLength: CGFloat
|
|
private var adjX: CGFloat = 0
|
|
private var tableView: UITableView?
|
|
private let closeButtonTitle: String?
|
|
|
|
private var _inputViewController: InputViewController?
|
|
override var inputViewController: InputViewController? {
|
|
return _inputViewController
|
|
}
|
|
|
|
// TODO: Only contain the keyboards and not the close button text
|
|
private var _tableList: [Any]?
|
|
var tableList: [Any] {
|
|
if let tableList = _tableList {
|
|
return tableList
|
|
}
|
|
|
|
let keyboardList = Storage.active.userDefaults.userKeyboards
|
|
|
|
let titleCloseButton: String
|
|
if let closeButtonTitle = closeButtonTitle {
|
|
titleCloseButton = closeButtonTitle
|
|
} else {
|
|
let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "Keyman"
|
|
let format = NSLocalizedString("keyboard-menu-exit", bundle: engineBundle, comment: "")
|
|
titleCloseButton = String.localizedStringWithFormat(format, appName)
|
|
}
|
|
|
|
if let keyboardList = keyboardList {
|
|
_tableList = keyboardList
|
|
_tableList!.append(titleCloseButton)
|
|
} else {
|
|
_tableList = [Defaults.keyboard]
|
|
_tableList!.append(titleCloseButton)
|
|
}
|
|
return _tableList!
|
|
}
|
|
|
|
init(keyFrame frame: CGRect, inputViewController: InputViewController, closeButtonTitle: String?) {
|
|
let isPortrait: Bool
|
|
if Manager.shared.isSystemKeyboard {
|
|
isPortrait = InputViewController.isPortrait
|
|
} else {
|
|
isPortrait = UIDevice.current.orientation.isPortrait
|
|
}
|
|
|
|
_inputViewController = inputViewController
|
|
self.closeButtonTitle = closeButtonTitle
|
|
keyFrame = frame
|
|
rowHeight = UIDevice.current.userInterfaceIdiom == .phone ? 30 : 60
|
|
fontSize = UIDevice.current.userInterfaceIdiom == .phone ? 14 : 21
|
|
xLength = keyFrame.size.width * (isPortrait ? 1.1 : 0.6)
|
|
|
|
super.init(frame: inputViewController.view.frame)
|
|
|
|
let screenWidth = UIScreen.main.bounds.width
|
|
let maxWidth = CGFloat.minimum(getMaxWidth(), screenWidth)
|
|
let baseHeight = keyFrame.size.height
|
|
let containerWidth = maxWidth - strokeWidth * 2
|
|
var containerHeight = CGFloat(tableList.count) * rowHeight
|
|
|
|
let vHeight = Manager.shared.inputViewController.kmwHeight
|
|
let bY = Manager.shared.inputViewController.kmwHeight - (keyFrame.origin.y + baseHeight)
|
|
|
|
if containerHeight + baseHeight > vHeight - bY {
|
|
let maxRows = (vHeight - baseHeight - bY) / rowHeight
|
|
containerHeight = maxRows.rounded(.towardZero) * rowHeight
|
|
}
|
|
|
|
let containerFrame = CGRect(x: strokeWidth, y: strokeWidth, width: containerWidth, height: containerHeight)
|
|
let viewWidth = maxWidth
|
|
let viewHeight = baseHeight + containerHeight + strokeWidth
|
|
var viewPosX = keyFrame.origin.x - (viewWidth - keyFrame.size.width) / 2.0
|
|
let viewPosY = (keyFrame.origin.y) - (viewHeight - keyFrame.size.height)
|
|
|
|
if viewPosX < 0 {
|
|
if (keyFrame.origin.x - borderRadius * 1.0) < 0 {
|
|
adjX = keyFrame.origin.x - viewPosX
|
|
viewPosX = keyFrame.origin.x
|
|
} else {
|
|
adjX = -viewPosX
|
|
viewPosX = 0
|
|
}
|
|
} else if (viewPosX + viewWidth) > screenWidth {
|
|
if ((keyFrame.origin.x + keyFrame.size.width) + borderRadius * 1.0) > screenWidth {
|
|
adjX = viewPosX - keyFrame.origin.x
|
|
viewPosX = keyFrame.origin.x + 2 * adjX
|
|
} else {
|
|
adjX = (screenWidth - viewWidth) - viewPosX
|
|
viewPosX = adjX + viewPosX
|
|
}
|
|
}
|
|
|
|
menuFrame = CGRect(x: viewPosX, y: viewPosY, width: viewWidth, height: viewHeight)
|
|
|
|
backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
|
tag = 1
|
|
isUserInteractionEnabled = true
|
|
let menuView = UIView(frame: menuFrame)
|
|
menuView.tag = 2
|
|
menuView.backgroundColor = UIColor.clear
|
|
tableView = UITableView(frame: containerFrame)
|
|
tableView?.delegate = self
|
|
tableView?.dataSource = self
|
|
tableView?.separatorStyle = .none
|
|
tableView?.backgroundColor = UIColor.clear
|
|
tableView?.layer.cornerRadius = (borderRadius * 1.0 - strokeWidth)
|
|
menuView.addSubview(tableView!)
|
|
addSubview(menuView)
|
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapAction))
|
|
tapGesture.delegate = self
|
|
tapGesture.numberOfTapsRequired = 1
|
|
tapGesture.numberOfTouchesRequired = 1
|
|
addGestureRecognizer(tapGesture)
|
|
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func flashScrollIndicators() {
|
|
tableView?.flashScrollIndicators()
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
let keyWidth = keyFrame.width
|
|
let keyHeight = keyFrame.height
|
|
let viewWidth = menuFrame.width
|
|
let viewHeight = menuFrame.height
|
|
|
|
guard let context = UIGraphicsGetCurrentContext() else {
|
|
return
|
|
}
|
|
UIGraphicsPushContext(context)
|
|
context.setLineJoin(.round)
|
|
context.setLineWidth(strokeWidth * 2)
|
|
context.setStrokeColor(borderColor.cgColor)
|
|
context.setFillColor(bgColor.cgColor)
|
|
var keyLeft = menuFrame.origin.x + (viewWidth - keyWidth) / 2.0 + strokeWidth - adjX
|
|
var keyRight = menuFrame.origin.x + (viewWidth - keyWidth) / 2.0 + keyWidth - strokeWidth - adjX
|
|
keyLeft = (keyLeft * 10).rounded(.up) / 10
|
|
keyRight = (keyRight * 10).rounded(.down) / 10
|
|
|
|
let midY = menuFrame.origin.y + viewHeight - keyHeight
|
|
let viewLeft = menuFrame.origin.x + strokeWidth
|
|
let viewRight = menuFrame.origin.x + viewWidth - strokeWidth
|
|
let viewTop = menuFrame.origin.y + strokeWidth
|
|
let viewBottom = menuFrame.origin.y + viewHeight - strokeWidth * 1.25
|
|
let viewMid = menuFrame.origin.x + viewWidth / 2.0
|
|
let r = borderRadius - strokeWidth
|
|
let r2 = borderRadius * 1.0 - strokeWidth
|
|
let r3 = borderRadius * 1.0 - strokeWidth
|
|
|
|
context.beginPath()
|
|
context.move(to: CGPoint(x: viewMid, y: viewTop))
|
|
context.addArc(tangent1End: CGPoint(x: viewLeft, y: viewTop),
|
|
tangent2End: CGPoint(x: viewLeft, y: midY), radius: r2)
|
|
context.addArc(tangent1End: CGPoint(x: viewLeft, y: midY),
|
|
tangent2End: CGPoint(x: keyLeft, y: midY), radius: r2)
|
|
context.addArc(tangent1End: CGPoint(x: keyLeft, y: midY),
|
|
tangent2End: CGPoint(x: keyLeft, y: viewBottom), radius: r3)
|
|
context.addArc(tangent1End: CGPoint(x: keyLeft, y: viewBottom),
|
|
tangent2End: CGPoint(x: keyRight, y: viewBottom), radius: r)
|
|
context.addArc(tangent1End: CGPoint(x: keyRight, y: viewBottom),
|
|
tangent2End: CGPoint(x: keyRight, y: midY), radius: r)
|
|
context.addArc(tangent1End: CGPoint(x: keyRight, y: midY),
|
|
tangent2End: CGPoint(x: viewRight, y: midY), radius: r3)
|
|
context.addArc(tangent1End: CGPoint(x: viewRight, y: midY),
|
|
tangent2End: CGPoint(x: viewRight, y: viewTop), radius: r2)
|
|
context.addArc(tangent1End: CGPoint(x: viewRight, y: viewTop),
|
|
tangent2End: CGPoint(x: viewMid, y: viewTop), radius: r2)
|
|
context.closePath()
|
|
context.drawPath(using: .stroke)
|
|
|
|
context.beginPath()
|
|
context.move(to: CGPoint(x: viewMid, y: viewTop))
|
|
context.addArc(tangent1End: CGPoint(x: viewLeft, y: viewTop),
|
|
tangent2End: CGPoint(x: viewLeft, y: midY), radius: r2)
|
|
context.addArc(tangent1End: CGPoint(x: viewLeft, y: midY),
|
|
tangent2End: CGPoint(x: keyLeft, y: midY), radius: r2)
|
|
context.addArc(tangent1End: CGPoint(x: keyLeft, y: midY),
|
|
tangent2End: CGPoint(x: keyLeft, y: viewBottom), radius: r3)
|
|
context.addArc(tangent1End: CGPoint(x: keyLeft, y: viewBottom),
|
|
tangent2End: CGPoint(x: keyRight, y: viewBottom), radius: r)
|
|
context.addArc(tangent1End: CGPoint(x: keyRight, y: viewBottom),
|
|
tangent2End: CGPoint(x: keyRight, y: midY), radius: r)
|
|
context.addArc(tangent1End: CGPoint(x: keyRight, y: midY),
|
|
tangent2End: CGPoint(x: viewRight, y: midY), radius: r3)
|
|
context.addArc(tangent1End: CGPoint(x: viewRight, y: midY),
|
|
tangent2End: CGPoint(x: viewRight, y: viewTop), radius: r2)
|
|
context.addArc(tangent1End: CGPoint(x: viewRight, y: viewTop),
|
|
tangent2End: CGPoint(x: viewMid, y: viewTop), radius: r2)
|
|
context.closePath()
|
|
context.clip()
|
|
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
let gradientColors = [bgColor.cgColor, bgColor2.cgColor] as CFArray
|
|
let gradientLocations: [CGFloat] = [0, 1]
|
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: gradientLocations)!
|
|
|
|
let startPoint = CGPoint(x: rect.midX, y: rect.minY)
|
|
let endPoint = CGPoint(x: rect.midX, y: rect.maxY)
|
|
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: [])
|
|
|
|
UIGraphicsPopContext()
|
|
}
|
|
|
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
let point = gestureRecognizer.location(in: gestureRecognizer.view)
|
|
guard let view = gestureRecognizer.view?.hitTest(point, with: nil) else {
|
|
return false
|
|
}
|
|
return view.tag > 0
|
|
}
|
|
|
|
@objc func tapAction(_ sender: UITapGestureRecognizer) {
|
|
Manager.shared.inputViewController.dismissKeyboardMenu()
|
|
}
|
|
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
return 1
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return tableList.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let cellIdentifier = "Cell"
|
|
if let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) {
|
|
return cell
|
|
}
|
|
|
|
let cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
|
|
let selectionColor = UIView()
|
|
selectionColor.backgroundColor = Colors.popupKeyTint
|
|
cell.selectedBackgroundView = selectionColor
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
return rowHeight
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
cell.textLabel?.textColor = UIColor.darkGray
|
|
cell.textLabel?.textAlignment = .left
|
|
cell.textLabel?.font = UIFont.systemFont(ofSize: fontSize)
|
|
cell.selectionStyle = .default
|
|
cell.backgroundColor = UIColor.clear
|
|
if indexPath.row == (tableList.count - 1) {
|
|
cell.textLabel?.text = tableList[indexPath.row] as? String
|
|
cell.tag = indexPath.row
|
|
cell.textLabel?.textColor = UIColor.gray
|
|
cell.textLabel?.textAlignment = .right
|
|
cell.isSelected = false
|
|
cell.accessoryType = .disclosureIndicator
|
|
} else {
|
|
let keyboard = tableList[indexPath.row] as! InstallableKeyboard
|
|
cell.textLabel?.text = keyboard.name
|
|
cell.tag = indexPath.row
|
|
if Manager.shared.currentKeyboardID == keyboard.fullID {
|
|
cell.selectionStyle = .none
|
|
cell.isSelected = true
|
|
cell.accessoryType = .checkmark
|
|
} else {
|
|
cell.isSelected = false
|
|
cell.accessoryType = .none
|
|
}
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
switchKeyboard(indexPath.row)
|
|
|
|
Manager.shared.inputViewController.dismissKeyboardMenu()
|
|
}
|
|
|
|
func getMaxWidth() -> CGFloat {
|
|
var w: CGFloat = 0
|
|
for obj in tableList {
|
|
let text: String?
|
|
if let kb = obj as? InstallableKeyboard {
|
|
text = kb.name
|
|
} else {
|
|
text = obj as? String
|
|
}
|
|
let tw = getTextWidth(text ?? "")
|
|
if tw > w {
|
|
w = tw
|
|
}
|
|
}
|
|
return w + ((UIDevice.current.userInterfaceIdiom == .phone) ? 75 : 150)
|
|
}
|
|
|
|
func getTextWidth(_ text: String) -> CGFloat {
|
|
return text.size(withAttributes: [.font: UIFont.systemFont(ofSize: fontSize)]).width
|
|
}
|
|
|
|
func switchKeyboard(_ index: Int) {
|
|
if index == (tableList.count - 1) {
|
|
inputViewController?.advanceToNextInputMode()
|
|
return
|
|
}
|
|
guard let kb = tableList[index] as? InstallableKeyboard else {
|
|
return
|
|
}
|
|
if Manager.shared.setKeyboard(kb) {
|
|
tableView?.reloadData()
|
|
|
|
// If we allow the system keyboard to show no banners, these lines are needed
|
|
// for properly-managed variable system keyboard height.
|
|
//
|
|
// It's not quite perfect yet, though - the rescale only works if the first
|
|
// keyboard loaded does not provide suggestions. Parking it here as it's not
|
|
// worth pursuing further at this stage.
|
|
inputViewController?.updateViewConstraints()
|
|
return
|
|
}
|
|
}
|
|
}
|