diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift index 66488044..c929f9ff 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift @@ -48,13 +48,15 @@ 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" case frame = "Frame" case group = "Group" case image = "Image" + case input = "Input" case mapview = "Mapview" case selection = "Selection" case setpoint = "Setpoint" @@ -66,13 +68,18 @@ 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)? 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 @@ -88,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? @@ -203,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 @@ -238,6 +240,7 @@ extension OpenHABWidget { self.state = state ?? "" self.text = text ?? "" self.legend = legend + self.inputHint = inputHint ?? .text self.encoding = encoding ?? "" self.item = item self.linkedPage = linkedPage @@ -275,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? @@ -291,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/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.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj index 3fc9f5af..81c802ec 100644 --- a/openHAB.xcodeproj/project.pbxproj +++ b/openHAB.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* 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 */; }; 653B54C2285E714900298ECD /* OpenHABViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653B54C1285E714900298ECD /* OpenHABViewController.swift */; }; @@ -269,6 +271,8 @@ /* 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 = ""; }; 4D38D9612568978E0039DA6E /* SetColorValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetColorValueIntentHandler.swift; sourceTree = ""; }; @@ -895,6 +899,7 @@ DF4B84101886DA9900F34902 /* Widgets */ = { isa = PBXGroup; children = ( + 2F6412ED2CE494A80039FB28 /* DatePickerUITableViewCell.swift */, DAF0A28E2C56F1EE00A14A6A /* ColorPickerCell.swift */, DF06F1FB18FEC2020011E7B9 /* ColorPickerViewController.swift */, DF4B84121886DAC400F34902 /* FrameUITableViewCell.swift */, @@ -909,6 +914,7 @@ DFA16EBA18883DE500EDB0BB /* SliderUITableViewCell.swift */, DA50C7BE2B0A652F0009F716 /* SliderWithSwitchSupportUITableViewCell.swift */, DFA13CB318872EBD006355C3 /* SwitchUITableViewCell.swift */, + 2FEFD8F52BE7C5BE00E387B9 /* TextInputUITableViewCell.swift */, DAA42BA921DC983B00244B2A /* VideoUITableViewCell.swift */, DAA42BAB21DC984A00244B2A /* WebUITableViewCell.swift */, DAEAA89C21E6B06300267EA3 /* ReusableView.swift */, @@ -1544,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 */, @@ -1560,6 +1567,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/DatePickerUITableViewCell.swift b/openHAB/DatePickerUITableViewCell.swift new file mode 100644 index 00000000..7072c544 --- /dev/null +++ b/openHAB/DatePickerUITableViewCell.swift @@ -0,0 +1,56 @@ +// 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 { + static let dateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return dateFormatter + }() + + 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") + } + guard let date = widget.item?.state else { + datePicker.date = Date() + return + } + datePicker.date = DateFormatter.iso8601Full.date(from: date) ?? 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: DateFormatter.iso8601Full.string(from: datePicker.date)) + }, for: .valueChanged) + } + } +} diff --git a/openHAB/Main.storyboard b/openHAB/Main.storyboard index 74fd991d..b3caea89 100644 --- a/openHAB/Main.storyboard +++ b/openHAB/Main.storyboard @@ -49,9 +49,72 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index 56a45262..368e1c65 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 { @@ -643,22 +643,29 @@ 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 { + // 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 widget.type { 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 @@ -666,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 @@ -690,25 +697,31 @@ 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 - default: + case .input: + 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 } - var iconColor = widget?.iconColor - if iconColor == nil || iconColor!.isEmpty, traitCollection.userInterfaceStyle == .dark { + var iconColor = widget.iconColor + 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 @@ -746,7 +759,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) } } @@ -765,37 +778,81 @@ 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 { + let hint = widget.inputHint + let textExtractor: ((UIAlertController) -> String?)? + let textFieldAdder: ((UITextField) -> Void)? + + switch hint { + case .date, .time, .datetime: + // value setting is handeled by the cell itself + textExtractor = nil + textFieldAdder = nil + case .number: + textFieldAdder = { textField in + textField.text = widget.state + textField.clearButtonMode = .always + textField.delegate = self + textField.keyboardType = .numbersAndPunctuation + } + // replace expected decimal separator + textExtractor = { $0.textFields?[0].text?.replacingOccurrences(of: NSLocale.current.decimalSeparator ?? "", with: ".") } + case .text: + textFieldAdder = { textField in + textField.text = widget.state + textField.clearButtonMode = .always + textField.keyboardType = .default + } + 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(alert)) + }) + alert.addAction(sendAction) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.preferredAction = sendAction + present(alert, animated: true) } } @@ -822,6 +879,40 @@ 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 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.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 signs + && ( + 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 + } } // MARK: Kingfisher authentication with NSURLCredential diff --git a/openHAB/TextInputUITableViewCell.swift b/openHAB/TextInputUITableViewCell.swift new file mode 100644 index 00000000..830a0645 --- /dev/null +++ b/openHAB/TextInputUITableViewCell.swift @@ -0,0 +1,26 @@ +// 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 TextInputUITableViewCell: GenericUITableViewCell { + override var widget: OpenHABWidget! { + get { + super.widget + } + set(widget) { + super.widget = widget + accessoryType = .disclosureIndicator + selectionStyle = .blue + } + } +} diff --git a/openHABWatch/Model/ObservableOpenHABWidget.swift b/openHABWatch/Model/ObservableOpenHABWidget.swift index ee4c9b87..efecca5a 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 } @@ -40,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 = "" @@ -64,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? @@ -162,6 +168,8 @@ class ObservableOpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableO .webview case "Mapview": .mapview + case "Input": + .input default: .unassigned } @@ -210,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 @@ -239,6 +247,7 @@ extension ObservableOpenHABWidget { self.state = state ?? "" self.text = text ?? "" self.legend = legend + self.inputHint = inputHint ?? .text self.encoding = encoding ?? "" self.item = item self.linkedPage = linkedPage @@ -276,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? @@ -290,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) } } 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() } } }