From 0f195fb80162ed3c81a771b846056cae56dbd77f Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 12 May 2024 19:01:14 +0200 Subject: [PATCH 01/13] prepare input element support Signed-off-by: Tassilo Karge --- .../OpenHABCore/Model/OpenHABWidget.swift | 1 + openHAB.xcodeproj/project.pbxproj | 4 +++ openHAB/Main.storyboard | 36 ++++++++++++++++++- openHAB/OpenHABSitemapViewController.swift | 15 ++++---- openHAB/TextInputUITableViewCell.swift | 22 ++++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 openHAB/TextInputUITableViewCell.swift diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift index 66488044..1fb9949b 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift @@ -55,6 +55,7 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { case frame = "Frame" case group = "Group" case image = "Image" + case input = "Input" case mapview = "Mapview" case selection = "Selection" case setpoint = "Setpoint" diff --git a/openHAB.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj index 3fc9f5af..bfa31248 100644 --- a/openHAB.xcodeproj/project.pbxproj +++ b/openHAB.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1224F78F228A89FD00750965 /* WatchMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1224F78D228A89FC00750965 /* WatchMessageService.swift */; }; + 2FEFD8F62BE7C5BE00E387B9 /* TextInputUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FEFD8F52BE7C5BE00E387B9 /* TextInputUITableViewCell.swift */; }; 4D6470DA2561F935007B03FC /* openHABIntents.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4D6470D32561F935007B03FC /* openHABIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 653B54C0285C0AC700298ECD /* OpenHABRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653B54BF285C0AC700298ECD /* OpenHABRootViewController.swift */; }; 653B54C2285E714900298ECD /* OpenHABViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653B54C1285E714900298ECD /* OpenHABViewController.swift */; }; @@ -269,6 +270,7 @@ /* Begin PBXFileReference section */ 1224F78D228A89FC00750965 /* WatchMessageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchMessageService.swift; sourceTree = ""; }; + 2FEFD8F52BE7C5BE00E387B9 /* TextInputUITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputUITableViewCell.swift; sourceTree = ""; }; 4D38D951256897490039DA6E /* SetNumberValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNumberValueIntentHandler.swift; sourceTree = ""; }; 4D38D959256897770039DA6E /* SetStringValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetStringValueIntentHandler.swift; sourceTree = ""; }; 4D38D9612568978E0039DA6E /* SetColorValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetColorValueIntentHandler.swift; sourceTree = ""; }; @@ -909,6 +911,7 @@ DFA16EBA18883DE500EDB0BB /* SliderUITableViewCell.swift */, DA50C7BE2B0A652F0009F716 /* SliderWithSwitchSupportUITableViewCell.swift */, DFA13CB318872EBD006355C3 /* SwitchUITableViewCell.swift */, + 2FEFD8F52BE7C5BE00E387B9 /* TextInputUITableViewCell.swift */, DAA42BA921DC983B00244B2A /* VideoUITableViewCell.swift */, DAA42BAB21DC984A00244B2A /* WebUITableViewCell.swift */, DAEAA89C21E6B06300267EA3 /* ReusableView.swift */, @@ -1560,6 +1563,7 @@ 938BF9D324EFD0B700E6B52F /* UIViewController+Localization.swift in Sources */, DAA42BA821DC97E000244B2A /* NotificationTableViewCell.swift in Sources */, DAF0A28F2C56F1EE00A14A6A /* ColorPickerCell.swift in Sources */, + 2FEFD8F62BE7C5BE00E387B9 /* TextInputUITableViewCell.swift in Sources */, 938EDCE122C4FEB800661CA1 /* ScaleAspectFitImageView.swift in Sources */, DAEAA89F21E6B16600267EA3 /* UITableView.swift in Sources */, DFB2624418830A3600D3244D /* OpenHABSitemapViewController.swift in Sources */, diff --git a/openHAB/Main.storyboard b/openHAB/Main.storyboard index 74fd991d..18c7d5dc 100644 --- a/openHAB/Main.storyboard +++ b/openHAB/Main.storyboard @@ -49,9 +49,43 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index 56a45262..ae88aa19 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -643,11 +643,13 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let widget: OpenHABWidget? = relevantWidget(indexPath: indexPath) + guard let widget: OpenHABWidget? = relevantWidget(indexPath: indexPath) else { + return tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell + } let cell: UITableViewCell - switch widget?.type { + switch widget.type { case .frame: cell = tableView.dequeueReusableCell(for: indexPath) as FrameUITableViewCell case .switchWidget: @@ -692,16 +694,17 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour cell = tableView.dequeueReusableCell(for: indexPath) as MapViewTableViewCell case .group, .text: cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell - default: - cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell + case .input: + cell = tableView.dequeueReusableCell(for: IndexPath) + as TextInputUITableViewCell } - var iconColor = widget?.iconColor + var iconColor = widget.iconColor if iconColor == nil || iconColor!.isEmpty, traitCollection.userInterfaceStyle == .dark { iconColor = "white" } // No icon is needed for image, video, frame and web widgets - if widget?.icon != nil, !((cell is NewImageUITableViewCell) || (cell is VideoUITableViewCell) || (cell is FrameUITableViewCell) || (cell is WebUITableViewCell)) { + if widget.icon != nil, !((cell is NewImageUITableViewCell) || (cell is VideoUITableViewCell) || (cell is FrameUITableViewCell) || (cell is WebUITableViewCell)) { if let urlc = Endpoint.icon( rootUrl: openHABRootUrl, version: appData?.openHABVersion ?? 2, diff --git a/openHAB/TextInputUITableViewCell.swift b/openHAB/TextInputUITableViewCell.swift new file mode 100644 index 00000000..de181f8a --- /dev/null +++ b/openHAB/TextInputUITableViewCell.swift @@ -0,0 +1,22 @@ +// +// TextInputUITableViewCell.swift +// openHAB +// +// Created by Tassilo Karge on 05.05.24. +// Copyright © 2024 openHAB e.V. All rights reserved. +// + +import Foundation + +class TextInputUITableViewCell: GenericUITableViewCell { + override var widget: OpenHABWidget! { + get { + super.widget + } + set(widget) { + super.widget = widget + accessoryType = .disclosureIndicator + selectionStyle = .blue + } + } +} From 873c9df6578b4cf92cdcfaf4d79631fe0fde4c75 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 12 May 2024 22:24:47 +0200 Subject: [PATCH 02/13] use input widget table view cell Signed-off-by: Tassilo Karge --- openHAB/OpenHABSitemapViewController.swift | 40 ++++++++++++---------- openHAB/TextInputUITableViewCell.swift | 14 +++++--- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index ae88aa19..80f21c46 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -643,24 +643,29 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let widget: OpenHABWidget? = relevantWidget(indexPath: indexPath) else { - return tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell + guard let widget: OpenHABWidget = relevantWidget(indexPath: indexPath), let widgetType = widget.type else { + // this should never be the case + let cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell + cell.displayWidget() + cell.touchEventDelegate = self + cell.separatorInset = UIEdgeInsets(top: 0, left: 60, bottom: 0, right: 0) + return cell } let cell: UITableViewCell - switch widget.type { + switch widgetType { case .frame: cell = tableView.dequeueReusableCell(for: indexPath) as FrameUITableViewCell case .switchWidget: // Reflecting the discussion held in https://github.com/openhab/openhab-core/issues/952 - if !(widget?.mappings ?? []).isEmpty { + if !widget.mappings.isEmpty { cell = tableView.dequeueReusableCell(for: indexPath) as SegmentedUITableViewCell - } else if widget?.item?.isOfTypeOrGroupType(.switchItem) ?? false { + } else if widget.item?.isOfTypeOrGroupType(.switchItem) ?? false { cell = tableView.dequeueReusableCell(for: indexPath) as SwitchUITableViewCell - } else if widget?.item?.isOfTypeOrGroupType(.rollershutter) ?? false { + } else if widget.item?.isOfTypeOrGroupType(.rollershutter) ?? false { cell = tableView.dequeueReusableCell(for: indexPath) as RollershutterCell - } else if !(widget?.mappingsOrItemOptions ?? []).isEmpty { + } else if !widget.mappingsOrItemOptions.isEmpty { cell = tableView.dequeueReusableCell(for: indexPath) as SegmentedUITableViewCell } else { cell = tableView.dequeueReusableCell(for: indexPath) as SwitchUITableViewCell @@ -668,7 +673,7 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour case .setpoint: cell = tableView.dequeueReusableCell(for: indexPath) as SetpointCell case .slider: - if let switchSupport = widget?.switchSupport, switchSupport { + if widget.switchSupport { cell = tableView.dequeueReusableCell(for: indexPath) as SliderWithSwitchSupportUITableViewCell } else { cell = tableView.dequeueReusableCell(for: indexPath) as SliderUITableViewCell @@ -692,26 +697,25 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour cell = tableView.dequeueReusableCell(for: indexPath) as WebUITableViewCell case .mapview: cell = tableView.dequeueReusableCell(for: indexPath) as MapViewTableViewCell - case .group, .text: - cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell case .input: - cell = tableView.dequeueReusableCell(for: IndexPath) - as TextInputUITableViewCell + cell = tableView.dequeueReusableCell(for: indexPath) as TextInputUITableViewCell + case .group, .text, .defaultWidget, .unknown: + cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell } var iconColor = widget.iconColor - if iconColor == nil || iconColor!.isEmpty, traitCollection.userInterfaceStyle == .dark { + if iconColor.isEmpty, traitCollection.userInterfaceStyle == .dark { iconColor = "white" } // No icon is needed for image, video, frame and web widgets - if widget.icon != nil, !((cell is NewImageUITableViewCell) || (cell is VideoUITableViewCell) || (cell is FrameUITableViewCell) || (cell is WebUITableViewCell)) { + if !((cell is NewImageUITableViewCell) || (cell is VideoUITableViewCell) || (cell is FrameUITableViewCell) || (cell is WebUITableViewCell)) { if let urlc = Endpoint.icon( rootUrl: openHABRootUrl, version: appData?.openHABVersion ?? 2, - icon: widget?.icon, - state: widget?.iconState() ?? "", + icon: widget.icon, + state: widget.iconState(), iconType: iconType, - iconColor: iconColor! + iconColor: iconColor ).url { var imageRequest = URLRequest(url: urlc) imageRequest.timeoutInterval = 10.0 @@ -749,7 +753,7 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour let nextWidget: OpenHABWidget? = relevantPage?.widgets[indexPath.row + 1] if let type = nextWidget?.type, type.isAny(of: .frame, .image, .video, .webview, .chart) { cell.separatorInset = UIEdgeInsets.zero - } else if !(widget?.type == .frame) { + } else if !(widget.type == .frame) { cell.separatorInset = UIEdgeInsets(top: 0, left: 60, bottom: 0, right: 0) } } diff --git a/openHAB/TextInputUITableViewCell.swift b/openHAB/TextInputUITableViewCell.swift index de181f8a..830a0645 100644 --- a/openHAB/TextInputUITableViewCell.swift +++ b/openHAB/TextInputUITableViewCell.swift @@ -1,12 +1,16 @@ +// Copyright (c) 2010-2024 Contributors to the openHAB project // -// TextInputUITableViewCell.swift -// openHAB +// See the NOTICE file(s) distributed with this work for additional +// information. // -// Created by Tassilo Karge on 05.05.24. -// Copyright © 2024 openHAB e.V. All rights reserved. +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0 // +// SPDX-License-Identifier: EPL-2.0 -import Foundation +import OpenHABCore +import UIKit class TextInputUITableViewCell: GenericUITableViewCell { override var widget: OpenHABWidget! { From db022c2d11e7a61d31682ffdfd1779884c3ace12 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 12 May 2024 22:33:14 +0200 Subject: [PATCH 03/13] WidgetType is unknownCaseRepresentable and does not need to be optional Signed-off-by: Tassilo Karge --- OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift | 2 +- OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift | 2 +- openHAB/OpenHABSitemapViewController.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift index 1fb9949b..138b09ba 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift @@ -73,7 +73,7 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { public var widgetId = "" public var label = "" public var icon = "" - public var type: WidgetType? + public var type: WidgetType = .unknownCase public var url = "" public var period = "" public var minValue = 0.0 diff --git a/OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift b/OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift index 7ff82430..718e2fe6 100644 --- a/OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift +++ b/OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift @@ -77,7 +77,7 @@ public extension String { return OpenHABItem.ItemType(rawValue: typeString) } - internal func toWidgetType() -> OpenHABWidget.WidgetType? { + internal func toWidgetType() -> OpenHABWidget.WidgetType { OpenHABWidget.WidgetType(rawValue: self) } diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index 80f21c46..725de4f9 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -643,7 +643,7 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let widget: OpenHABWidget = relevantWidget(indexPath: indexPath), let widgetType = widget.type else { + guard let widget: OpenHABWidget = relevantWidget(indexPath: indexPath) else { // this should never be the case let cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell cell.displayWidget() @@ -654,7 +654,7 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour let cell: UITableViewCell - switch widgetType { + switch widget.type { case .frame: cell = tableView.dequeueReusableCell(for: indexPath) as FrameUITableViewCell case .switchWidget: From f6067f81acbc554b583f593916cd0d6870eaf672 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 19 May 2024 08:51:13 +0200 Subject: [PATCH 04/13] add dialog to input value for input widget Signed-off-by: Tassilo Karge --- openHAB/OpenHABSitemapViewController.swift | 50 ++++++++++++------- .../Model/ObservableOpenHABWidget.swift | 3 ++ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index 725de4f9..4bf5b071 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -772,37 +772,49 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let widget: OpenHABWidget? = relevantWidget(indexPath: indexPath) - if widget?.linkedPage != nil { - if let link = widget?.linkedPage?.link { + if let index = widgetTableView.indexPathForSelectedRow { + widgetTableView.deselectRow(at: index, animated: false) + } + + guard let widget: OpenHABWidget = relevantWidget(indexPath: indexPath) else { + return + } + + if widget.linkedPage != nil { + if let link = widget.linkedPage?.link { os_log("Selected %{PUBLIC}@", log: .viewCycle, type: .info, link) } - - selectedWidgetRow = indexPath.row let newViewController = (storyboard?.instantiateViewController(withIdentifier: "OpenHABPageViewController") as? OpenHABSitemapViewController)! - newViewController.title = widget?.linkedPage?.title.components(separatedBy: "[")[0] - newViewController.pageUrl = widget?.linkedPage?.link ?? "" + newViewController.title = widget.linkedPage?.title.components(separatedBy: "[")[0] + newViewController.pageUrl = widget.linkedPage?.link ?? "" newViewController.openHABRootUrl = openHABRootUrl navigationController?.pushViewController(newViewController, animated: true) - } else if widget?.type == .selection { - selectedWidgetRow = indexPath.row - let selectedWidget: OpenHABWidget? = relevantWidget(indexPath: indexPath) - let selectionItemState = selectedWidget?.item?.state + } else if widget.type == .selection { + let selectionItemState = widget.item?.state logger.info("Selected selection widget in status: \(selectionItemState ?? "unknown")") let hostingController = UIHostingController(rootView: SelectionView( - mappings: selectedWidget?.mappingsOrItemOptions ?? [], + mappings: widget.mappingsOrItemOptions, selectionItemState: selectionItemState, onSelection: { selectedMappingIndex in - let selectedWidget: OpenHABWidget? = self.relevantPage?.widgets[self.selectedWidgetRow] - let selectedMapping: OpenHABWidgetMapping? = selectedWidget?.mappingsOrItemOptions[selectedMappingIndex] - self.sendCommand(selectedWidget?.item, commandToSend: selectedMapping?.command) + let selectedMapping: OpenHABWidgetMapping = widget.mappingsOrItemOptions[selectedMappingIndex] + self.sendCommand(widget.item, commandToSend: selectedMapping.command) } )) - hostingController.title = widget?.labelText + hostingController.title = widget.labelText navigationController?.pushViewController(hostingController, animated: true) - } - if let index = widgetTableView.indexPathForSelectedRow { - widgetTableView.deselectRow(at: index, animated: false) + } else if widget.type == .input { + // TODO: proper texts instead of hardcoded values + let alert = UIAlertController(title: "Enter new value", message: "Current value for \(widget.label) is \(widget.state)", preferredStyle: .alert) + alert.addTextField { _ in + // TODO: configure (set current value, validation, allow clearing, set delegate...) + } + let sendAction = UIAlertAction(title: "Set value", style: .destructive, handler: { [weak self] _ in + self?.sendCommand(widget.item, commandToSend: alert.textFields?[0].text) // TODO: sanitize / convert text? + }) + alert.addAction(sendAction) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.preferredAction = sendAction + present(alert, animated: true) } } diff --git a/openHABWatch/Model/ObservableOpenHABWidget.swift b/openHABWatch/Model/ObservableOpenHABWidget.swift index ee4c9b87..811c8ef9 100644 --- a/openHABWatch/Model/ObservableOpenHABWidget.swift +++ b/openHABWatch/Model/ObservableOpenHABWidget.swift @@ -33,6 +33,7 @@ enum WidgetTypeEnum { case video case webview case mapview + case input var boolState: Bool { guard case let .switcher(value) = self else { return false } @@ -162,6 +163,8 @@ class ObservableOpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableO .webview case "Mapview": .mapview + case "Input": + .input default: .unassigned } From 56fa379d374237a5919319eea4f23e6aec01868e Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 19 May 2024 10:00:24 +0200 Subject: [PATCH 05/13] add inputHint field to widget for input type Signed-off-by: Tassilo Karge --- .../OpenHABCore/Model/OpenHABWidget.swift | 21 +++++++++++-------- openHAB/OpenHABSitemapViewController.swift | 15 +++++++++++-- .../Model/ObservableOpenHABWidget.swift | 11 ++++++++-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift index 138b09ba..ab1f7625 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift @@ -48,7 +48,8 @@ protocol Widget: AnyObject { } public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { - public enum WidgetType: String { + public enum WidgetType: String, Decodable, UnknownCaseRepresentable { + static var unknownCase: OpenHABWidget.WidgetType = .unknown case chart = "Chart" case colorpicker = "Colorpicker" case defaultWidget = "Default" @@ -67,6 +68,11 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { case unknown = "Unknown" } + public enum InputHint: String, Decodable, UnknownCaseRepresentable { + static var unknownCase: OpenHABWidget.InputHint = .text + case text, number, date, time, datetime + } + public var id: String = "" public var sendCommand: ((_ item: OpenHABItem, _ command: String?) -> Void)? @@ -89,6 +95,7 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { public var state = "" public var text = "" public var legend: Bool? + public var inputHint = InputHint.unknownCase public var encoding = "" public var forceAsItem: Bool? public var item: OpenHABItem? @@ -204,15 +211,9 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { } } -extension OpenHABWidget.WidgetType: Decodable {} - -extension OpenHABWidget.WidgetType: UnknownCaseRepresentable { - static var unknownCase: OpenHABWidget.WidgetType = .unknown -} - extension OpenHABWidget { // This is an ugly initializer - convenience init(widgetId: String, label: String, icon: String, type: WidgetType, url: String?, period: String?, minValue: Double?, maxValue: Double?, step: Double?, refresh: Int?, height: Double?, isLeaf: Bool?, iconColor: String?, labelColor: String?, valueColor: String?, service: String?, state: String?, text: String?, legend: Bool?, encoding: String?, item: OpenHABItem?, linkedPage: OpenHABSitemapPage?, mappings: [OpenHABWidgetMapping], widgets: [OpenHABWidget], visibility: Bool?, switchSupport: Bool?, forceAsItem: Bool?) { + convenience init(widgetId: String, label: String, icon: String, type: WidgetType, url: String?, period: String?, minValue: Double?, maxValue: Double?, step: Double?, refresh: Int?, height: Double?, isLeaf: Bool?, iconColor: String?, labelColor: String?, valueColor: String?, service: String?, state: String?, text: String?, legend: Bool?, inputHint: InputHint, encoding: String?, item: OpenHABItem?, linkedPage: OpenHABSitemapPage?, mappings: [OpenHABWidgetMapping], widgets: [OpenHABWidget], visibility: Bool?, switchSupport: Bool?, forceAsItem: Bool?) { self.init() id = widgetId self.widgetId = widgetId @@ -239,6 +240,7 @@ extension OpenHABWidget { self.state = state ?? "" self.text = text ?? "" self.legend = legend + self.inputHint = inputHint self.encoding = encoding ?? "" self.item = item self.linkedPage = linkedPage @@ -276,6 +278,7 @@ public extension OpenHABWidget { let state: String? let text: String? let legend: Bool? + let inputHint: InputHint let encoding: String? let groupType: String? let item: OpenHABItem.CodingData? @@ -292,7 +295,7 @@ extension OpenHABWidget.CodingData { var openHABWidget: OpenHABWidget { let mappedWidgets = widgets.map(\.openHABWidget) // swiftlint:disable:next line_length - return OpenHABWidget(widgetId: widgetId, label: label, icon: icon, type: type, url: url, period: period, minValue: minValue, maxValue: maxValue, step: step, refresh: refresh, height: height, isLeaf: isLeaf, iconColor: iconcolor, labelColor: labelcolor, valueColor: valuecolor, service: service, state: state, text: text, legend: legend, encoding: encoding, item: item?.openHABItem, linkedPage: linkedPage?.openHABSitemapPage, mappings: mappings, widgets: mappedWidgets, visibility: visibility, switchSupport: switchSupport, forceAsItem: forceAsItem) + return OpenHABWidget(widgetId: widgetId, label: label, icon: icon, type: type, url: url, period: period, minValue: minValue, maxValue: maxValue, step: step, refresh: refresh, height: height, isLeaf: isLeaf, iconColor: iconcolor, labelColor: labelcolor, valueColor: valuecolor, service: service, state: state, text: text, legend: legend, inputHint: inputHint, encoding: encoding, item: item?.openHABItem, linkedPage: linkedPage?.openHABSitemapPage, mappings: mappings, widgets: mappedWidgets, visibility: visibility, switchSupport: switchSupport, forceAsItem: forceAsItem) } } diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index 4bf5b071..b066452d 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -803,10 +803,21 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour hostingController.title = widget.labelText navigationController?.pushViewController(hostingController, animated: true) } else if widget.type == .input { + let hint = widget.inputHint // TODO: proper texts instead of hardcoded values - let alert = UIAlertController(title: "Enter new value", message: "Current value for \(widget.label) is \(widget.state)", preferredStyle: .alert) - alert.addTextField { _ in + let alert = UIAlertController( + title: "Enter new value", + message: "Current value for \(widget.label) is \(widget.state)", + preferredStyle: .alert + ) + alert.addTextField { textField in // TODO: configure (set current value, validation, allow clearing, set delegate...) + textField.clearButtonMode = .always + // TODO: change text field propoerties according to hint + switch hint { + default: + textField.keyboardType = .alphabet + } } let sendAction = UIAlertAction(title: "Set value", style: .destructive, handler: { [weak self] _ in self?.sendCommand(widget.item, commandToSend: alert.textFields?[0].text) // TODO: sanitize / convert text? diff --git a/openHABWatch/Model/ObservableOpenHABWidget.swift b/openHABWatch/Model/ObservableOpenHABWidget.swift index 811c8ef9..6e472224 100644 --- a/openHABWatch/Model/ObservableOpenHABWidget.swift +++ b/openHABWatch/Model/ObservableOpenHABWidget.swift @@ -41,6 +41,10 @@ enum WidgetTypeEnum { } } +enum InputHint: String, Decodable, CaseIterable { + case text, number, date, time, datetime +} + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) class ObservableOpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObject { var id: String = "" @@ -65,6 +69,7 @@ class ObservableOpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableO @Published var state = "" var text = "" var legend: Bool? + var inputHint: InputHint = .text var encoding = "" @Published var item: OpenHABItem? var linkedPage: OpenHABSitemapPage? @@ -213,7 +218,7 @@ class ObservableOpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableO extension ObservableOpenHABWidget { // This is an ugly initializer - convenience init(widgetId: String, label: String, icon: String, type: String, url: String?, period: String?, minValue: Double?, maxValue: Double?, step: Double?, refresh: Int?, height: Double?, isLeaf: Bool?, iconColor: String?, labelColor: String?, valueColor: String?, service: String?, state: String?, text: String?, legend: Bool?, encoding: String?, item: OpenHABItem?, linkedPage: OpenHABSitemapPage?, mappings: [OpenHABWidgetMapping], widgets: [ObservableOpenHABWidget], forceAsItem: Bool?) { + convenience init(widgetId: String, label: String, icon: String, type: String, url: String?, period: String?, minValue: Double?, maxValue: Double?, step: Double?, refresh: Int?, height: Double?, isLeaf: Bool?, iconColor: String?, labelColor: String?, valueColor: String?, service: String?, state: String?, text: String?, legend: Bool?, inputHint: InputHint, encoding: String?, item: OpenHABItem?, linkedPage: OpenHABSitemapPage?, mappings: [OpenHABWidgetMapping], widgets: [ObservableOpenHABWidget], forceAsItem: Bool?) { self.init() id = widgetId @@ -242,6 +247,7 @@ extension ObservableOpenHABWidget { self.state = state ?? "" self.text = text ?? "" self.legend = legend + self.inputHint = inputHint self.encoding = encoding ?? "" self.item = item self.linkedPage = linkedPage @@ -279,6 +285,7 @@ extension ObservableOpenHABWidget { let state: String? let text: String? let legend: Bool? + let inputHint: InputHint let encoding: String? let groupType: String? let item: OpenHABItem.CodingData? @@ -293,7 +300,7 @@ extension ObservableOpenHABWidget.CodingData { var openHABWidget: ObservableOpenHABWidget { let mappedWidgets = widgets.map(\.openHABWidget) // swiftlint:disable:next line_length - return ObservableOpenHABWidget(widgetId: widgetId, label: label, icon: icon, type: type, url: url, period: period, minValue: minValue, maxValue: maxValue, step: step, refresh: refresh, height: height, isLeaf: isLeaf, iconColor: iconColor, labelColor: labelcolor, valueColor: valuecolor, service: service, state: state, text: text, legend: legend, encoding: encoding, item: item?.openHABItem, linkedPage: linkedPage?.openHABSitemapPage, mappings: mappings, widgets: mappedWidgets, forceAsItem: forceAsItem) + return ObservableOpenHABWidget(widgetId: widgetId, label: label, icon: icon, type: type, url: url, period: period, minValue: minValue, maxValue: maxValue, step: step, refresh: refresh, height: height, isLeaf: isLeaf, iconColor: iconColor, labelColor: labelcolor, valueColor: valuecolor, service: service, state: state, text: text, legend: legend, inputHint: inputHint, encoding: encoding, item: item?.openHABItem, linkedPage: linkedPage?.openHABSitemapPage, mappings: mappings, widgets: mappedWidgets, forceAsItem: forceAsItem) } } From ea7aff2d794819f476f9432fb5ef9a756ad55de7 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Fri, 24 May 2024 01:05:40 +0200 Subject: [PATCH 06/13] configure textfield in input field alert for input type Signed-off-by: Tassilo Karge --- openHAB/OpenHABSitemapViewController.swift | 81 +++++++++++++++++++--- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index b066452d..b27f962a 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -605,7 +605,7 @@ extension OpenHABSitemapViewController: ColorPickerCellDelegate { // MARK: - UITableViewDelegate, UITableViewDataSource -extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSource { +extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if currentPage != nil { if isFiltering { @@ -810,17 +810,49 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour message: "Current value for \(widget.label) is \(widget.state)", preferredStyle: .alert ) - alert.addTextField { textField in - // TODO: configure (set current value, validation, allow clearing, set delegate...) - textField.clearButtonMode = .always - // TODO: change text field propoerties according to hint - switch hint { - default: - textField.keyboardType = .alphabet + + let textExtractor: () -> String? + switch hint { + case .date: + let datePicker = UIDatePicker() + datePicker.datePickerMode = .date + alert.view.addSubview(datePicker) + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .full + dateFormatter.timeStyle = .none + textExtractor = { dateFormatter.string(from: datePicker.date) } + case .datetime: + let datePicker = UIDatePicker() + datePicker.datePickerMode = .dateAndTime + alert.view.addSubview(datePicker) + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .full + dateFormatter.timeStyle = .full + textExtractor = { dateFormatter.string(from: datePicker.date) } + case .time: + let datePicker = UIDatePicker() + datePicker.datePickerMode = .time + alert.view.addSubview(datePicker) + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .full + textExtractor = { dateFormatter.string(from: datePicker.date) } + case .number: + alert.addTextField { textField in + textField.clearButtonMode = .always + textField.delegate = self + textField.keyboardType = .numbersAndPunctuation + } + textExtractor = { alert.textFields?[0].text } + case .text: + alert.addTextField { textField in + textField.clearButtonMode = .always + textField.keyboardType = .default } + textExtractor = { alert.textFields?[0].text } } let sendAction = UIAlertAction(title: "Set value", style: .destructive, handler: { [weak self] _ in - self?.sendCommand(widget.item, commandToSend: alert.textFields?[0].text) // TODO: sanitize / convert text? + self?.sendCommand(widget.item, commandToSend: textExtractor()) // TODO: sanitize / convert text? }) alert.addAction(sendAction) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) @@ -852,6 +884,37 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour return nil } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let decimalSeparator = NSLocale.current.decimalSeparator ?? "" + let oldString = (textField.text ?? "") + let replacement: NSString = string as NSString + let wholeNumberRegex = "^-?[0-9]*$" + + // check for deletion + return string.isEmpty + // check for new negative sign + || ( + !string.starts(with: "-") // new string does not add negative sign + || range.location == 0 // new string adds negative sign to beginning + && ( + !oldString.starts(with: "-") // old string does not contain negative sign + || range.length > 0 + ) + ) // new string replaces negative sign in old string + // check for old negative sign + && ( + !oldString.starts(with: "-") // old string does not start with negative sign + || range.location > 0 // new string starts after negative sign in old string + || range.length > 0 + ) // new string replaces negative sign in old string + // check for decimal points + && ( + string.range(of: wholeNumberRegex) != nil // new string is whole number + || replacement.replacingCharacters(in: replacement.range(of: decimalSeparator), with: "").range(of: wholeNumberRegex) != nil // new string is valid decimal number + && !(oldString as NSString).replacingCharacters(in: range, with: "").contains(decimalSeparator) + ) // old string without replaced range not yet contains decimal separator + } } // MARK: Kingfisher authentication with NSURLCredential From 5da208064e6962af732f3a65e116bee37ddefc4a Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 10 Nov 2024 15:51:36 +0100 Subject: [PATCH 07/13] input hint is an optional widget attribute Signed-off-by: Tassilo Karge --- OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift | 6 +++--- openHABWatch/Model/ObservableOpenHABWidget.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift index ab1f7625..c929f9ff 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift @@ -213,7 +213,7 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { extension OpenHABWidget { // This is an ugly initializer - convenience init(widgetId: String, label: String, icon: String, type: WidgetType, url: String?, period: String?, minValue: Double?, maxValue: Double?, step: Double?, refresh: Int?, height: Double?, isLeaf: Bool?, iconColor: String?, labelColor: String?, valueColor: String?, service: String?, state: String?, text: String?, legend: Bool?, inputHint: InputHint, encoding: String?, item: OpenHABItem?, linkedPage: OpenHABSitemapPage?, mappings: [OpenHABWidgetMapping], widgets: [OpenHABWidget], visibility: Bool?, switchSupport: Bool?, forceAsItem: Bool?) { + convenience init(widgetId: String, label: String, icon: String, type: WidgetType, url: String?, period: String?, minValue: Double?, maxValue: Double?, step: Double?, refresh: Int?, height: Double?, isLeaf: Bool?, iconColor: String?, labelColor: String?, valueColor: String?, service: String?, state: String?, text: String?, legend: Bool?, inputHint: InputHint?, encoding: String?, item: OpenHABItem?, linkedPage: OpenHABSitemapPage?, mappings: [OpenHABWidgetMapping], widgets: [OpenHABWidget], visibility: Bool?, switchSupport: Bool?, forceAsItem: Bool?) { self.init() id = widgetId self.widgetId = widgetId @@ -240,7 +240,7 @@ extension OpenHABWidget { self.state = state ?? "" self.text = text ?? "" self.legend = legend - self.inputHint = inputHint + self.inputHint = inputHint ?? .text self.encoding = encoding ?? "" self.item = item self.linkedPage = linkedPage @@ -278,7 +278,7 @@ public extension OpenHABWidget { let state: String? let text: String? let legend: Bool? - let inputHint: InputHint + let inputHint: InputHint? let encoding: String? let groupType: String? let item: OpenHABItem.CodingData? diff --git a/openHABWatch/Model/ObservableOpenHABWidget.swift b/openHABWatch/Model/ObservableOpenHABWidget.swift index 6e472224..efecca5a 100644 --- a/openHABWatch/Model/ObservableOpenHABWidget.swift +++ b/openHABWatch/Model/ObservableOpenHABWidget.swift @@ -218,7 +218,7 @@ class ObservableOpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableO extension ObservableOpenHABWidget { // This is an ugly initializer - convenience init(widgetId: String, label: String, icon: String, type: String, url: String?, period: String?, minValue: Double?, maxValue: Double?, step: Double?, refresh: Int?, height: Double?, isLeaf: Bool?, iconColor: String?, labelColor: String?, valueColor: String?, service: String?, state: String?, text: String?, legend: Bool?, inputHint: InputHint, encoding: String?, item: OpenHABItem?, linkedPage: OpenHABSitemapPage?, mappings: [OpenHABWidgetMapping], widgets: [ObservableOpenHABWidget], forceAsItem: Bool?) { + convenience init(widgetId: String, label: String, icon: String, type: String, url: String?, period: String?, minValue: Double?, maxValue: Double?, step: Double?, refresh: Int?, height: Double?, isLeaf: Bool?, iconColor: String?, labelColor: String?, valueColor: String?, service: String?, state: String?, text: String?, legend: Bool?, inputHint: InputHint?, encoding: String?, item: OpenHABItem?, linkedPage: OpenHABSitemapPage?, mappings: [OpenHABWidgetMapping], widgets: [ObservableOpenHABWidget], forceAsItem: Bool?) { self.init() id = widgetId @@ -247,7 +247,7 @@ extension ObservableOpenHABWidget { self.state = state ?? "" self.text = text ?? "" self.legend = legend - self.inputHint = inputHint + self.inputHint = inputHint ?? .text self.encoding = encoding ?? "" self.item = item self.linkedPage = linkedPage @@ -285,7 +285,7 @@ extension ObservableOpenHABWidget { let state: String? let text: String? let legend: Bool? - let inputHint: InputHint + let inputHint: InputHint? let encoding: String? let groupType: String? let item: OpenHABItem.CodingData? From 9b552d8576b1d9a9779ece475ca0237eb0fc7234 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 10 Nov 2024 18:53:58 +0100 Subject: [PATCH 08/13] correct number processing for ios16 Signed-off-by: Tassilo Karge --- openHAB/OpenHABSitemapViewController.swift | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index b27f962a..c86981d9 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -843,7 +843,8 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour textField.delegate = self textField.keyboardType = .numbersAndPunctuation } - textExtractor = { alert.textFields?[0].text } + //replace expected decimal separator + textExtractor = { alert.textFields?[0].text?.replacingOccurrences(of: NSLocale.current.decimalSeparator ?? "", with: ".") } case .text: alert.addTextField { textField in textField.clearButtonMode = .always @@ -852,7 +853,7 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour textExtractor = { alert.textFields?[0].text } } let sendAction = UIAlertAction(title: "Set value", style: .destructive, handler: { [weak self] _ in - self?.sendCommand(widget.item, commandToSend: textExtractor()) // TODO: sanitize / convert text? + self?.sendCommand(widget.item, commandToSend: textExtractor()) }) alert.addAction(sendAction) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) @@ -888,8 +889,7 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let decimalSeparator = NSLocale.current.decimalSeparator ?? "" let oldString = (textField.text ?? "") - let replacement: NSString = string as NSString - let wholeNumberRegex = "^-?[0-9]*$" + let wholeNumberRegex = /^-?[0-9]*$/ // check for deletion return string.isEmpty @@ -904,15 +904,19 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour ) // new string replaces negative sign in old string // check for old negative sign && ( - !oldString.starts(with: "-") // old string does not start with negative sign + oldString.isEmpty + || !oldString.starts(with: "-") // old string does not start with negative sign || range.location > 0 // new string starts after negative sign in old string || range.length > 0 ) // new string replaces negative sign in old string - // check for decimal points + // check for decimal signs && ( - string.range(of: wholeNumberRegex) != nil // new string is whole number - || replacement.replacingCharacters(in: replacement.range(of: decimalSeparator), with: "").range(of: wholeNumberRegex) != nil // new string is valid decimal number - && !(oldString as NSString).replacingCharacters(in: range, with: "").contains(decimalSeparator) + string.firstRange(of: wholeNumberRegex) != nil // new string is whole number + || ( + string.replacing(decimalSeparator, with: "", maxReplacements: 1) + .firstRange(of: wholeNumberRegex) != nil // new string is valid decimal number + && !(oldString as NSString).replacingCharacters(in: range, with: "").contains(decimalSeparator) + ) ) // old string without replaced range not yet contains decimal separator } } From e2a3e0a8d10d529c71b3741c748ea7e7abcd1bc5 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Mon, 11 Nov 2024 03:32:04 +0100 Subject: [PATCH 09/13] Autoformat Signed-off-by: Tassilo Karge --- openHAB/OpenHABSitemapViewController.swift | 2 +- openHABWatch/Views/LogsViewer.swift | 149 +++++++++++---------- 2 files changed, 79 insertions(+), 72 deletions(-) diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index c86981d9..b8779f62 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -843,7 +843,7 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour textField.delegate = self textField.keyboardType = .numbersAndPunctuation } - //replace expected decimal separator + // replace expected decimal separator textExtractor = { alert.textFields?[0].text?.replacingOccurrences(of: NSLocale.current.decimalSeparator ?? "", with: ".") } case .text: alert.addTextField { textField in diff --git a/openHABWatch/Views/LogsViewer.swift b/openHABWatch/Views/LogsViewer.swift index d4ac8cbc..dc1b9063 100644 --- a/openHABWatch/Views/LogsViewer.swift +++ b/openHABWatch/Views/LogsViewer.swift @@ -1,10 +1,13 @@ +// Copyright (c) 2010-2024 Contributors to the openHAB project // -// LogView.swift -// openHABWatch +// See the NOTICE file(s) distributed with this work for additional +// information. // -// Created by Tim Müller-Seydlitz on 31.10.24. -// Copyright © 2024 openHAB e.V. All rights reserved. +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0 // +// SPDX-License-Identifier: EPL-2.0 import Foundation import OSLog @@ -12,96 +15,100 @@ import SwiftUI // Thanks to https://useyourloaf.com/blog/fetching-oslog-messages-in-swift/ -extension OSLogEntryLog.Level { - fileprivate var description: String { - switch self { - case .undefined: "undefined" - case .debug: "debug" - case .info: "info" - case .notice: "notice" - case .error: "error" - case .fault: "fault" - @unknown default: "default" +private extension OSLogEntryLog.Level { + var description: String { + switch self { + case .undefined: "undefined" + case .debug: "debug" + case .info: "info" + case .notice: "notice" + case .error: "error" + case .fault: "fault" + @unknown default: "default" + } } - } } -extension Logger { - static public func fetch(since date: Date, - predicateFormat: String) async throws -> [String] { - let store = try OSLogStore(scope: .currentProcessIdentifier) - let position = store.position(date: date) - let predicate = NSPredicate(format: predicateFormat) - let entries = try store.getEntries( - at: position, - matching: predicate - ) - - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - - var logs: [String] = [] - for entry in entries { - try Task.checkCancellation() - if let log = entry as? OSLogEntryLog { - var attributedMessage = AttributedString(dateFormatter.string(from: entry.date)) - attributedMessage.font = .headline - - logs.append(""" - \(dateFormatter.string(from: entry.date)): \ - \(log.category):\(log.level.description): \ - \(entry.composedMessage)\n - """) - } else { - logs.append("\(entry.date): \(entry.composedMessage)\n") - } +public extension Logger { + static func fetch(since date: Date, + predicateFormat: String) async throws -> [String] { + let store = try OSLogStore(scope: .currentProcessIdentifier) + let position = store.position(date: date) + let predicate = NSPredicate(format: predicateFormat) + let entries = try store.getEntries( + at: position, + matching: predicate + ) + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + + var logs: [String] = [] + for entry in entries { + try Task.checkCancellation() + if let log = entry as? OSLogEntryLog { + var attributedMessage = AttributedString(dateFormatter.string(from: entry.date)) + attributedMessage.font = .headline + + logs.append(""" + \(dateFormatter.string(from: entry.date)): \ + \(log.category):\(log.level.description): \ + \(entry.composedMessage)\n + """) + } else { + logs.append("\(entry.date): \(entry.composedMessage)\n") + } + } + + if logs.isEmpty { logs = ["Nothing found"] } + return logs } - - if logs.isEmpty { logs = ["Nothing found"] } - return logs - } } struct LogsViewer: View { @State private var text = "Loading..." - - static private let template = NSPredicate(format: - "(subsystem BEGINSWITH $PREFIX)") + + private static let template = NSPredicate(format: + "(subsystem BEGINSWITH $PREFIX)") let myFont = Font - .system(size: 10) - .monospaced() - + .system(size: 10) + .monospaced() + private func fetchLogs() async -> String { let calendar = Calendar.current - guard let dayAgo = calendar.date(byAdding: .day, - value: -1, to: Date.now) else { - return "Invalid calendar" + guard let dayAgo = calendar.date( + byAdding: .day, + value: -1, + to: Date.now + ) else { + return "Invalid calendar" } - + do { let predicate = Self.template.withSubstitutionVariables( - [ - "PREFIX": "org.openhab" - ]) + [ + "PREFIX": "org.openhab" + ]) - let logs = try await Logger.fetch(since: dayAgo, - predicateFormat: predicate.predicateFormat) - return logs.joined() + let logs = try await Logger.fetch( + since: dayAgo, + predicateFormat: predicate.predicateFormat + ) + return logs.joined() } catch { - return error.localizedDescription + return error.localizedDescription } - } + } var body: some View { - ScrollView { - Text(text) - .font(myFont) - .padding() + Text(text) + .font(myFont) + .padding() } .task { - text = await fetchLogs() + text = await fetchLogs() } } } From 36b3645329885e72ebbbf6570c5616a922e621fb Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Wed, 13 Nov 2024 21:09:25 +0100 Subject: [PATCH 10/13] inline date time picker for sitemap Signed-off-by: Tassilo Karge --- openHAB.xcodeproj/project.pbxproj | 4 ++ openHAB/DatePickerUITableViewCell.swift | 46 +++++++++++++++ openHAB/Main.storyboard | 32 +++++++++- openHAB/OpenHABSitemapViewController.swift | 68 ++++++++++------------ 4 files changed, 111 insertions(+), 39 deletions(-) create mode 100644 openHAB/DatePickerUITableViewCell.swift diff --git a/openHAB.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj index bfa31248..81c802ec 100644 --- a/openHAB.xcodeproj/project.pbxproj +++ b/openHAB.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1224F78F228A89FD00750965 /* WatchMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1224F78D228A89FC00750965 /* WatchMessageService.swift */; }; + 2F6412EE2CE494A80039FB28 /* DatePickerUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F6412ED2CE494A80039FB28 /* DatePickerUITableViewCell.swift */; }; 2FEFD8F62BE7C5BE00E387B9 /* TextInputUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FEFD8F52BE7C5BE00E387B9 /* TextInputUITableViewCell.swift */; }; 4D6470DA2561F935007B03FC /* openHABIntents.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4D6470D32561F935007B03FC /* openHABIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 653B54C0285C0AC700298ECD /* OpenHABRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653B54BF285C0AC700298ECD /* OpenHABRootViewController.swift */; }; @@ -270,6 +271,7 @@ /* Begin PBXFileReference section */ 1224F78D228A89FC00750965 /* WatchMessageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchMessageService.swift; sourceTree = ""; }; + 2F6412ED2CE494A80039FB28 /* DatePickerUITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerUITableViewCell.swift; sourceTree = ""; }; 2FEFD8F52BE7C5BE00E387B9 /* TextInputUITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputUITableViewCell.swift; sourceTree = ""; }; 4D38D951256897490039DA6E /* SetNumberValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNumberValueIntentHandler.swift; sourceTree = ""; }; 4D38D959256897770039DA6E /* SetStringValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetStringValueIntentHandler.swift; sourceTree = ""; }; @@ -897,6 +899,7 @@ DF4B84101886DA9900F34902 /* Widgets */ = { isa = PBXGroup; children = ( + 2F6412ED2CE494A80039FB28 /* DatePickerUITableViewCell.swift */, DAF0A28E2C56F1EE00A14A6A /* ColorPickerCell.swift */, DF06F1FB18FEC2020011E7B9 /* ColorPickerViewController.swift */, DF4B84121886DAC400F34902 /* FrameUITableViewCell.swift */, @@ -1547,6 +1550,7 @@ 935B484625342B8E00E44CF0 /* URL+Static.swift in Sources */, B7D5ECE121499E55001B0EC6 /* MapViewTableViewCell.swift in Sources */, DA6B2EF52C89F8F200DF77CF /* ColorPickerView.swift in Sources */, + 2F6412EE2CE494A80039FB28 /* DatePickerUITableViewCell.swift in Sources */, DAA42BAA21DC983B00244B2A /* VideoUITableViewCell.swift in Sources */, DFB2623B18830A3600D3244D /* AppDelegate.swift in Sources */, DA6B2EF72C8B92E800DF77CF /* SelectionView.swift in Sources */, diff --git a/openHAB/DatePickerUITableViewCell.swift b/openHAB/DatePickerUITableViewCell.swift new file mode 100644 index 00000000..51598f32 --- /dev/null +++ b/openHAB/DatePickerUITableViewCell.swift @@ -0,0 +1,46 @@ +// Copyright (c) 2010-2024 Contributors to the openHAB project +// +// See the NOTICE file(s) distributed with this work for additional +// information. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0 +// +// SPDX-License-Identifier: EPL-2.0 + +import OpenHABCore +import UIKit + +class DatePickerUITableViewCell: GenericUITableViewCell { + override var widget: OpenHABWidget! { + get { + super.widget + } + set(widget) { + super.widget = widget + switch widget.inputHint { + case .date: + datePicker.datePickerMode = .date + case .time: + datePicker.datePickerMode = .time + case .datetime: + datePicker.datePickerMode = .dateAndTime + default: + fatalError("Must not use this cell for input other than date and time") + } + datePicker.date = ISO8601DateFormatter().date(from: widget.state) ?? Date.now + } + } + + weak var controller: OpenHABSitemapViewController! + + @IBOutlet private(set) var datePicker: UIDatePicker! { + didSet { + datePicker.addAction(UIAction { [weak self] _ in + guard let self else { return } + controller?.sendCommand(widget.item, commandToSend: datePicker.date.ISO8601Format()) + }, for: .valueChanged) + } + } +} diff --git a/openHAB/Main.storyboard b/openHAB/Main.storyboard index 18c7d5dc..9826427d 100644 --- a/openHAB/Main.storyboard +++ b/openHAB/Main.storyboard @@ -49,9 +49,38 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -69,7 +98,6 @@ - diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index b8779f62..368e1c65 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -698,7 +698,13 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour case .mapview: cell = tableView.dequeueReusableCell(for: indexPath) as MapViewTableViewCell case .input: - cell = tableView.dequeueReusableCell(for: indexPath) as TextInputUITableViewCell + if [.date, .time, .datetime].contains(widget.inputHint) { + let pickerCell = tableView.dequeueReusableCell(for: indexPath) as DatePickerUITableViewCell + pickerCell.controller = self + cell = pickerCell + } else { + cell = tableView.dequeueReusableCell(for: indexPath) as TextInputUITableViewCell + } case .group, .text, .defaultWidget, .unknown: cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell } @@ -804,56 +810,44 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour navigationController?.pushViewController(hostingController, animated: true) } else if widget.type == .input { let hint = widget.inputHint - // TODO: proper texts instead of hardcoded values - let alert = UIAlertController( - title: "Enter new value", - message: "Current value for \(widget.label) is \(widget.state)", - preferredStyle: .alert - ) + let textExtractor: ((UIAlertController) -> String?)? + let textFieldAdder: ((UITextField) -> Void)? - let textExtractor: () -> String? switch hint { - case .date: - let datePicker = UIDatePicker() - datePicker.datePickerMode = .date - alert.view.addSubview(datePicker) - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .full - dateFormatter.timeStyle = .none - textExtractor = { dateFormatter.string(from: datePicker.date) } - case .datetime: - let datePicker = UIDatePicker() - datePicker.datePickerMode = .dateAndTime - alert.view.addSubview(datePicker) - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .full - dateFormatter.timeStyle = .full - textExtractor = { dateFormatter.string(from: datePicker.date) } - case .time: - let datePicker = UIDatePicker() - datePicker.datePickerMode = .time - alert.view.addSubview(datePicker) - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .none - dateFormatter.timeStyle = .full - textExtractor = { dateFormatter.string(from: datePicker.date) } + case .date, .time, .datetime: + // value setting is handeled by the cell itself + textExtractor = nil + textFieldAdder = nil case .number: - alert.addTextField { textField in + textFieldAdder = { textField in + textField.text = widget.state textField.clearButtonMode = .always textField.delegate = self textField.keyboardType = .numbersAndPunctuation } // replace expected decimal separator - textExtractor = { alert.textFields?[0].text?.replacingOccurrences(of: NSLocale.current.decimalSeparator ?? "", with: ".") } + textExtractor = { $0.textFields?[0].text?.replacingOccurrences(of: NSLocale.current.decimalSeparator ?? "", with: ".") } case .text: - alert.addTextField { textField in + textFieldAdder = { textField in + textField.text = widget.state textField.clearButtonMode = .always textField.keyboardType = .default } - textExtractor = { alert.textFields?[0].text } + textExtractor = { $0.textFields?[0].text } } + guard let textExtractor, let textFieldAdder else { + return + } + + // TODO: proper texts instead of hardcoded values + let alert = UIAlertController( + title: "Enter new value", + message: "Current value for \(widget.label) is \(widget.state)", + preferredStyle: .alert + ) + alert.addTextField(configurationHandler: textFieldAdder) let sendAction = UIAlertAction(title: "Set value", style: .destructive, handler: { [weak self] _ in - self?.sendCommand(widget.item, commandToSend: textExtractor()) + self?.sendCommand(widget.item, commandToSend: textExtractor(alert)) }) alert.addAction(sendAction) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) From 0a70d0eba180b8b4b846b5e477dfd979195237b0 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Wed, 13 Nov 2024 21:24:29 +0100 Subject: [PATCH 11/13] improve layout of datepicker Signed-off-by: Tassilo Karge --- openHAB/Main.storyboard | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openHAB/Main.storyboard b/openHAB/Main.storyboard index 9826427d..8c12b76d 100644 --- a/openHAB/Main.storyboard +++ b/openHAB/Main.storyboard @@ -56,13 +56,13 @@ - From 861a2b351399e475cde58eea1c5d36593b27f389 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Fri, 15 Nov 2024 23:56:30 +0100 Subject: [PATCH 12/13] correct date parsing for sitemap input element Signed-off-by: Tassilo Karge --- openHAB/DatePickerUITableViewCell.swift | 12 +++++++++++- openHAB/Main.storyboard | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/openHAB/DatePickerUITableViewCell.swift b/openHAB/DatePickerUITableViewCell.swift index 51598f32..f9806216 100644 --- a/openHAB/DatePickerUITableViewCell.swift +++ b/openHAB/DatePickerUITableViewCell.swift @@ -13,6 +13,12 @@ import OpenHABCore import UIKit class DatePickerUITableViewCell: GenericUITableViewCell { + static let dateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return dateFormatter + }() + override var widget: OpenHABWidget! { get { super.widget @@ -29,7 +35,11 @@ class DatePickerUITableViewCell: GenericUITableViewCell { default: fatalError("Must not use this cell for input other than date and time") } - datePicker.date = ISO8601DateFormatter().date(from: widget.state) ?? Date.now + guard let date = widget.labelValue else { + datePicker.date = Date() + return + } + datePicker.date = Self.dateFormatter.date(from: date) ?? Date.now } } diff --git a/openHAB/Main.storyboard b/openHAB/Main.storyboard index 8c12b76d..b3caea89 100644 --- a/openHAB/Main.storyboard +++ b/openHAB/Main.storyboard @@ -74,6 +74,7 @@ + From 53b5431d8ab349edced7ed0e8d459de78d6691b3 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sat, 16 Nov 2024 00:32:59 +0100 Subject: [PATCH 13/13] use iso8601Full for reading and writing dates from and to date item Signed-off-by: Tassilo Karge --- openHAB/DatePickerUITableViewCell.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openHAB/DatePickerUITableViewCell.swift b/openHAB/DatePickerUITableViewCell.swift index f9806216..7072c544 100644 --- a/openHAB/DatePickerUITableViewCell.swift +++ b/openHAB/DatePickerUITableViewCell.swift @@ -35,11 +35,11 @@ class DatePickerUITableViewCell: GenericUITableViewCell { default: fatalError("Must not use this cell for input other than date and time") } - guard let date = widget.labelValue else { + guard let date = widget.item?.state else { datePicker.date = Date() return } - datePicker.date = Self.dateFormatter.date(from: date) ?? Date.now + datePicker.date = DateFormatter.iso8601Full.date(from: date) ?? Date.now } } @@ -49,7 +49,7 @@ class DatePickerUITableViewCell: GenericUITableViewCell { didSet { datePicker.addAction(UIAction { [weak self] _ in guard let self else { return } - controller?.sendCommand(widget.item, commandToSend: datePicker.date.ISO8601Format()) + controller?.sendCommand(widget.item, commandToSend: DateFormatter.iso8601Full.string(from: datePicker.date)) }, for: .valueChanged) } }