iOS/Sources/App/WebView/Extensions/WebViewGestureHandler.swift
Copilot 0570085873
Add quick search gesture action with Ctrl+K support for HA 2026.2+ (#4257)
## Summary

iOS equivalent of [Android PR
#6337](https://github.com/home-assistant/android/pull/6337). Adds a new
`quickSearch` gesture action that triggers the quickbar introduced in
Home Assistant 2026.2.

**Changes:**
- New `quickSearch` case in `HAGestureAction` enum
- Version-aware keyboard dispatch: `Ctrl+K` for HA 2026.2+, falls back
to `E` key for older versions
- New `quickSearchKeyEvent` JavaScript command with `ctrlKey: true`
- Version constant `quickSearchKeyboardShortcut` (2026.2)
- Localization strings for "Quick search"
- Test case for `quickSearchKeyEvent` in
`WebViewJavascriptCommandsTests.swift`

Existing `searchEntities` action unchanged for backward compatibility.

```swift
private func showQuickSearch() {
    let command: String
    if let serverVersion = webView?.server.info.version,
       serverVersion >= .quickSearchKeyboardShortcut {
        command = WebViewJavascriptCommands.quickSearchKeyEvent  // Ctrl+K
    } else {
        command = WebViewJavascriptCommands.searchEntitiesKeyEvent  // E key fallback
    }
    webView?.evaluateJavaScript(command) { ... }
}
```

## Screenshots

N/A - gesture action appears in existing settings UI via
`HAGestureAction.allCases`

## Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#

## Any other notes

Mirrors Android behavior: new quickbar (Ctrl+K) on 2026.2+, legacy
entity search (E) on older versions.

<!-- START COPILOT CODING AGENT SUFFIX -->



<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> Check this android PR
https://github.com/home-assistant/android/pull/6337
> We want the same on iOS, check WebViewController gesture handling to
understand how gestures that trigger javascript key event works and also
check GestureView to understand how the user configure it.


</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/home-assistant/iOS/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bgoncal <5808343+bgoncal@users.noreply.github.com>
2026-01-27 16:21:35 +01:00

156 lines
5.1 KiB
Swift

import Foundation
import MBProgressHUD
import Shared
import Version
// MARK: - Gestures
final class WebViewGestureHandler {
weak var webView: WebViewControllerProtocol?
func handleGestureAction(_ action: HAGestureAction) {
switch action {
case .assist:
showAssistThroughKeyEvent()
case .showSidebar:
showSidebar()
case .backPage:
webViewNavigateBack()
case .nextPage:
webViewNavigateForward()
case .showServersList:
showServersList()
case .nextServer:
moveToServer(next: true)
case .previousServer:
moveToServer(next: false)
case .showSettings:
webView?.showSettingsViewController()
case .openDebug:
webView?.openDebug()
case .quickSearch:
showQuickSearch()
case .searchEntities:
showSearchEntities()
case .searchDevices:
showSearchDevices()
case .searchCommands:
showSearchCommands()
case .none:
/* no-op */
break
}
}
private func showSidebar() {
webView?.webViewExternalMessageHandler
.sendExternalBus(message: .init(command: WebViewExternalBusOutgoingMessage.showSidebar.rawValue))
}
private func webViewNavigateBack() {
if webView?.canGoBack ?? false {
webView?.goBack()
}
}
private func webViewNavigateForward() {
if webView?.canGoForward ?? false {
webView?.goForward()
}
}
private func showServersList() {
Current.sceneManager.webViewWindowControllerPromise.done { controller in
controller.selectServer(includeSettings: true) { server in
controller.open(server: server)
}
}
}
private func showQuickSearch() {
// Use Ctrl+K for HA 2026.2+, fallback to E key for older versions
let command: String
if let serverVersion = webView?.server.info.version,
serverVersion >= .quickSearchKeyboardShortcut {
command = WebViewJavascriptCommands.quickSearchKeyEvent
} else {
command = WebViewJavascriptCommands.searchEntitiesKeyEvent
}
webView?.evaluateJavaScript(command) { _, error in
if let error {
Current.Log.error("JavaScript error while trying to open quick search: \(error)")
} else {
Current.Log.info("Open quick search command sent to webview")
}
}
}
private func showSearchEntities() {
webView?.evaluateJavaScript(WebViewJavascriptCommands.searchEntitiesKeyEvent) { _, error in
if let error {
Current.Log.error("JavaScript error while trying to open entities search: \(error)")
} else {
Current.Log.info("Open entities search command sent to webview")
}
}
}
private func showSearchDevices() {
webView?.evaluateJavaScript(WebViewJavascriptCommands.searchDevicesKeyEvent) { _, error in
if let error {
Current.Log.error("JavaScript error while trying to open devices search: \(error)")
} else {
Current.Log.info("Open devices search command sent to webview")
}
}
}
private func showSearchCommands() {
webView?.evaluateJavaScript(WebViewJavascriptCommands.searchCommandsKeyEvent) { _, error in
if let error {
Current.Log.error("JavaScript error while trying to open commands search: \(error)")
} else {
Current.Log.info("Open commands search command sent to webview")
}
}
}
private func showAssistThroughKeyEvent() {
webView?.evaluateJavaScript(WebViewJavascriptCommands.assistKeyEvent) { _, error in
if let error {
Current.Log.error("JavaScript error while trying to open assist: \(error)")
} else {
Current.Log.info("Open assist command sent to webview")
}
}
}
private func moveToServer(next: Bool) {
guard let server = webView?.server else {
Current.Log.error("No server available to switch")
return
}
let servers = Current.servers.all
guard servers.count > 1, let currentIndex = servers.firstIndex(of: server) else { return }
let nextIndex: Int
if next {
nextIndex = (currentIndex - 1 + servers.count) % servers.count
} else {
nextIndex = (currentIndex + 1) % servers.count
}
let nextServer = servers[nextIndex]
Current.sceneManager.webViewWindowControllerPromise.done { controller in
controller.open(server: nextServer).done { controller in
let hud = MBProgressHUD.showAdded(to: controller.view, animated: true)
hud.isUserInteractionEnabled = false
hud.mode = .text
hud.label.text = nextServer.info.name
hud.hide(animated: true, afterDelay: 1.0)
}
}
}
}