import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'datamodel.dart'; import 'databaseconnector.dart'; import 'dart:async'; import 'package:geolocator/geolocator.dart'; import 'restdata.dart'; class AddEventKottasPegel extends StatefulWidget { const AddEventKottasPegel({Key? key}) : super(key: key); @override State<AddEventKottasPegel> createState() => _AddEventKottasPegelPageState(); } class _AddEventKottasPegelPageState extends State<AddEventKottasPegel> { RestData restConnector = RestData(); List<String> angleStatusItems = ['90° (vertical)', '80°', '70°', '60°', '50°', '40°', '30°', ]; //Parameters which shall be added to the measurement event as json string data List<String> measurementStatusItems = ['ok', 'broken', 'missing', 'tilted']; int measurementStatusId = -1; List<String> rodColorValues = ['black', 'red', 'blue', 'none']; int rodColorValuesIdOld = -1; int rodColorValuesIdNew = -1; late String angleStatus; int rawMeasurementValue = 0; int lengthOld = 0; int lengthNew = 0; bool syncGNSSData = true; bool _addButtonEnabled = true; bool _eventAdded = false; bool displayAngle = true; bool displayOldLength = true; bool displayNewLength = true; double accuracy = 0.0; late StreamSubscription<Position> streamHandler; late Timer restQueryTimer; final prefs = SharedPreferences.getInstance(); // Is async var database = DatabaseInstance(); late OverlayEntry _overlayEntry; //For event creation success notifications late Timer _overlayCloseTimer; TextEditingController labelEditingController = TextEditingController(); TextEditingController oldLengthEditingController = TextEditingController(); TextEditingController newLengthEditingController = TextEditingController(); Future startRest() async { final ConfigurationStoreInstance configuration = ConfigurationStoreInstance(); final EventStoreInstance event = EventStoreInstance(); /* Function which starts querying the URL configuration.restRequestUrl */ restQueryTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { // Query data from raspberry pi here try{ Map data = await restConnector.fetchData(configuration.restRequestUrl); debugPrint("Rest Query succeeded: $data"); event.currentEvent.latitude = data['lat']; event.currentEvent.longitude = data['lon']; event.currentEvent.elevation = data['elevation'].toString(); var date = DateTime.parse(data['timestamp']); var isoDate = date.toIso8601String(); event.currentEvent.startDate = isoDate; //TODO: test and validate event.currentEvent.endDate = isoDate; rawMeasurementValue = data['length']; //in mm //accept only int values accuracy = double.parse(data['precision'].toString()); //accept int and double and string values }catch(e){ //Set some useful error values. TODO: Ensure that the timeout is not to short otherwise this makes things worse: var date = DateTime.now().toUtc(); var isoDate = date.toIso8601String(); event.currentEvent.startDate = isoDate; event.currentEvent.endDate = isoDate; event.currentEvent.latitude = ""; event.currentEvent.longitude = ""; event.currentEvent.elevation = ""; rawMeasurementValue = 0; debugPrint("Rest Query failed: $e"); } if(mounted){ setState(() { //refresh UI on update });} }); } Future startGNSS() async { final EventStoreInstance event = EventStoreInstance(); debugPrint("Check Location Permission"); bool serviceStatus = false; bool hasPermission = false; serviceStatus = await Geolocator.isLocationServiceEnabled(); if(serviceStatus){ LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { debugPrint('Location permissions are denied'); }else if(permission == LocationPermission.deniedForever){ debugPrint('Location permissions are permanently denied'); }else{ hasPermission = true; } }else{ hasPermission = true; } if(hasPermission){ debugPrint('Location permissions granted'); if(mounted){ setState(() { //refresh the UI });} debugPrint('Starting location stream'); streamHandler = Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 0, )).listen((Position position) { debugPrint('Get Location: Lat:${position.latitude} Long:${position.longitude} Alt:${position.altitude}'); event.currentEvent.latitude = position.latitude.toString(); event.currentEvent.longitude = position.longitude.toString(); event.currentEvent.elevation = position.altitude.toString(); accuracy = position.accuracy; var date = DateTime.now().toUtc(); var isoDate = date.toIso8601String(); event.currentEvent.startDate = isoDate; event.currentEvent.endDate = isoDate; if(mounted){ setState(() { //refresh UI on update });} }); } }else{ debugPrint("GPS Service is not enabled, turn on GPS location"); } if(mounted){ setState(() { //refresh the UI });} } @override void initState() { final EventStoreInstance event = EventStoreInstance(); final ConfigurationStoreInstance configuration = ConfigurationStoreInstance(); // Init displayed parameters with some values to prevent GUI exceptions before the async REST/GNSS data is ready event.currentEvent.latitude = '0.0'; event.currentEvent.longitude = '0.0'; event.currentEvent.elevation = '0.0'; var date = DateTime.now().toUtc(); var isoDate = date.toIso8601String(); event.currentEvent.startDate = isoDate; //TODO: test and validate event.currentEvent.endDate = isoDate; if(event.gnssSync){ // Get data from REST / Raspberry PI startRest(); }else{ // Get Lat / Long ... from internal GNSS startGNSS(); } //Update current event with prefix and cnt event.currentEvent.label = configuration.labelConfig.prefix + configuration.labelConfig.cnt.toString().padLeft(3, '0'); labelEditingController.text = event.currentEvent.label; angleStatus = angleStatusItems[0]; oldLengthEditingController.text = lengthOld.toString(); newLengthEditingController.text = lengthNew.toString(); super.initState(); } @override void dispose() async { try { streamHandler.cancel(); debugPrint('Cancel location stream'); }catch(e){ debugPrint('Canceling location stream failed'); } try { _overlayEntry.remove(); }catch(e){ debugPrint('Dispose error on overlay remove: $e'); } try{ restQueryTimer.cancel(); }catch(e){ debugPrint('Timer cancel error on dispose: $e'); } /*Async update current event configuration to shared preferences*/ final EventStoreInstance event = EventStoreInstance(); event.storeToSharedPrefs(); super.dispose(); } bool _validateLatitude(value){ if (value == ""){ return true; //Empty string is valid } var number = num.tryParse(value); if(number != null){ if (number >= -90.0 && number <= 90.0){ return true; // Latitude valid } } return false; } bool _validateLongitude(value){ if (value == ""){ return true; //Empty string is valid } var number = num.tryParse(value); if(number != null){ if (number >= -180.0 && number <= 180.0){ return true; // Longitude valid } } return false; } bool _validateElevation(value){ if (value == ""){ return true; //Empty string is valid } var number = num.tryParse(value); if(number != null){ return true; // Any numerical value is valid for elevation } return false; } bool _validateCompleteInput(){ bool flag = true; if(displayOldLength == true) { if (rodColorValuesIdOld < 0 || rodColorValuesIdOld > rodColorValues.length) { flag = false; } if (lengthOld == 0){ flag = false; } } if (displayNewLength == true){ if (rodColorValuesIdNew < 0 || rodColorValuesIdNew > rodColorValues.length){ flag = false; } if (lengthNew == 0){ flag = false; } } return flag; } bool _validateInput(){ final EventStoreInstance event = EventStoreInstance(); if (RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch( event.currentEvent.label)) { if (RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch( event.currentEvent.description) || event.currentEvent.description == '') { if(_validateLatitude(event.currentEvent.latitude)){ if(_validateLongitude(event.currentEvent.longitude)){ if(_validateElevation(event.currentEvent.elevation)){ if(measurementStatusId >= 0 && measurementStatusId < measurementStatusItems.length) { if (_validateCompleteInput() == true) { return true; } } } } } } } return false; } bool _addButtonStatus(){ if((_validateInput() == true) && (_addButtonEnabled == true)){ return true; } return false; } Future<void> _showResultPopup(BuildContext context, String text, bool error) async { OverlayState? overlayState = Overlay.of(context); try { _overlayEntry.remove(); // Allow only one Overlay Popup at a time }catch(e){ debugPrint('Overlay already removed, during dispose: $e'); } _overlayEntry = OverlayEntry(builder: (context) { Color backGroundColor; Color textColor; if (error == true){ backGroundColor = Colors.redAccent; //Style for error message textColor = Colors.black; } else { backGroundColor = Colors.greenAccent; //Style for notification textColor = Colors.black; } return Stack( alignment: Alignment.center, children: [ Positioned( // Position at 10% of height from bottom bottom: MediaQuery.of(context).size.height * 0.1, child: Material( borderRadius: BorderRadius.circular(8.0), color: backGroundColor, //Some transparency remains child: Container( padding: const EdgeInsets.all(5.0), // Space between Text and Bubble width: MediaQuery.of(context).size.width * 0.95, child: TextFormField( minLines: 1, maxLines: 5, readOnly: true, autofocus: false, enabled: false, style: TextStyle(color: textColor), controller: TextEditingController( text: text, ), ), ), ), ), ], ); }); overlayState?.insert(_overlayEntry); try { _overlayCloseTimer.cancel(); // Kill old timers }catch(e){ debugPrint('Timer cancel error: $e'); } _overlayCloseTimer = Timer( const Duration(seconds: 3), () { try { _overlayEntry.remove(); // Allow only one Overlay Popup. NOTE: Is this a quick an dirty or a proper solution? }catch(e){ debugPrint('Overlay already removed, during dispose: $e'); } }, ); } void _resetSelectedFields(){ measurementStatusId = -1; rodColorValuesIdOld = -1; rodColorValuesIdNew = -1; angleStatus = angleStatusItems[0]; lengthOld = 0; lengthNew = 0; oldLengthEditingController.text = lengthOld.toString(); newLengthEditingController.text = lengthNew.toString(); } Future<void> _storeCurrentMeasurementEvent(BuildContext context, bool addAngle, bool addOldLength, bool addNewLength) async { final EventStoreInstance event = EventStoreInstance(); final ConfigurationStoreInstance configuration = ConfigurationStoreInstance(); //TODO: this is a prototype. URN and ID are hardcoded. Make configurable event.currentEvent.urn = 'station:neumayer_iii:snow_level_measure'; event.currentEvent.urnId = 9094; event.currentEvent.type = 'Deployment'; event.currentEvent.typeId = 187; event.currentEvent.status = "PENDING"; //Generate Payload Measurement Data Map<String, dynamic> data = { 'status' : measurementStatusItems[measurementStatusId], }; if(true == addAngle){ data['angle_old'] = angleStatus; } if(true == addOldLength) { data['length_old'] = lengthOld; data['color_old'] = rodColorValues[rodColorValuesIdOld]; } if (true == addNewLength) { data['length_new'] = lengthNew; data['color_new'] = rodColorValues[rodColorValuesIdNew]; } //Add payload as json to data field in db //event.currentEvent.data = data.toString(); event.currentEvent.data = jsonEncode(data); await database.addEvent(event.currentEvent); HapticFeedback.vibrate(); //Feedback that adding event succeeded //Get next Label prefix, id and event timestamp if (configuration.labelConfig.cntUp == true){ configuration.labelConfig.cnt++; }else{ configuration.labelConfig.cnt--; } event.currentEvent.label = configuration.labelConfig.prefix + configuration.labelConfig.cnt.toString().padLeft(3, '0'); labelEditingController.text = event.currentEvent.label; //Trigger label update event.currentEvent.description = ''; var date = DateTime.now().toUtc(); var isoDate = date.toIso8601String(); event.currentEvent.startDate = isoDate; event.currentEvent.endDate = isoDate; // Reset fields for next event _resetSelectedFields(); await event.storeToSharedPrefs(); await configuration.storeToSharedPrefs(); _addButtonEnabled = true; //Activate button to add more events _eventAdded = true; // Trigger popup in build method setState(() {}); } @override Widget build(BuildContext context) { /* Get singletons to access relevant data here.*/ final EventStoreInstance event = EventStoreInstance(); final ConfigurationStoreInstance configuration = ConfigurationStoreInstance(); String switchStatusText = ""; String switchStatusTextLine2 = ""; if (true == event.gnssSync){ switchStatusText = "External REST Data"; } else{ switchStatusText = "Internal GNSS"; // Use internal GNSS source } if (accuracy == 0.0){ switchStatusTextLine2 = 'No-Fix'; }else{ switchStatusTextLine2 = "Precision ${accuracy.toStringAsFixed(1)}m"; } // Set visibility parameters displayAngle = false; displayOldLength = false; displayNewLength = false; if(measurementStatusId == 0){ //ok displayAngle = true; displayOldLength = true; }else if (measurementStatusId == 1){ //broken displayNewLength = true; }else if (measurementStatusId == 2){ //missing displayNewLength = true; }else { //tilted / to short displayAngle = true; displayOldLength = true; displayNewLength = true; } if (true == _eventAdded){ _eventAdded = false; Future.delayed(Duration.zero,() { _showResultPopup(context, "Successfully created Event !", false); }); } //if (configuration.initialized == true) { if (true) { return Scaffold( appBar: AppBar( title: const Text("Kottaspegel"), actions: <Widget>[ Column( children: [ Text(switchStatusText, style: const TextStyle(fontStyle: FontStyle.italic)), Text(switchStatusTextLine2, style: const TextStyle(fontStyle: FontStyle.italic)), ], ), Switch( //Enable showing all or only pending events. Default is to show only pending events value: event.gnssSync, onChanged: (value) { event.gnssSync = value; debugPrint('Switched to:${event.gnssSync}'); accuracy = 0.0; if(event.gnssSync){ // Get data from rest / raspberry pi streamHandler.cancel(); startRest(); }else{ // Get data from internal GNSS restQueryTimer.cancel(); startGNSS(); rawMeasurementValue = 0; //Clear Raw Measurement value } setState(() {}); }), ], ), body: SingleChildScrollView( child: Container( margin: const EdgeInsets.symmetric(horizontal: 5.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ const SizedBox(height: 10.0), TextFormField( readOnly: false, enabled: false, keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.onUserInteraction, controller: TextEditingController( text: "" "${event.currentEvent.startDate.substring(11,19)} / " "${event.currentEvent.latitude} / " "${event.currentEvent.longitude} / " "${event.currentEvent.elevation}" ), decoration: const InputDecoration( labelText: 'UTC / Lat / Long / Elv ', //Example border: OutlineInputBorder(), ), ), const SizedBox(height: 15.0), Visibility( visible: event.gnssSync, child: TextFormField( readOnly: false, enabled: false, style: const TextStyle(fontSize: 30.0), keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.onUserInteraction, controller: TextEditingController( text: "Length: ${rawMeasurementValue.toString()} [mm]" ), decoration: const InputDecoration( labelText: '', //Example border: OutlineInputBorder(), ), ), ), const SizedBox(height: 15.0), TextFormField( controller: labelEditingController, autovalidateMode: AutovalidateMode.onUserInteraction, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Label', ), onChanged: (value){ event.currentEvent.label = value; setState(() {}); }, validator: (value) { if (!RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch( value!)) { return "Only: a-z , A-Z , _ , 0-9 , ,(Comma) , ( , ) , + , - , . , :"; } else { return null; // Entered Text is valid } }, ), const SizedBox(height: 15.0), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value primary: measurementStatusId == 0 ? Colors.black54 : Colors.white12, ), onPressed: () { measurementStatusId = 0; setState(() {}); }, child: const Text('Ok'), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value primary: measurementStatusId == 1 ? Colors.black54 : Colors.white12, ), onPressed: () { measurementStatusId = 1; setState(() {}); }, child: const Text('broken'), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value primary: measurementStatusId == 2 ? Colors.black54 : Colors.white12, ), onPressed: () { measurementStatusId = 2; setState(() {}); }, child: const Text('?'), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value primary: measurementStatusId == 3 ? Colors.black54 : Colors.white12, ), onPressed: () { measurementStatusId = 3; setState(() {}); }, child: const Text('tilt/short'), ), ] ), const SizedBox(height: 30.0), Visibility( visible: displayAngle, child: DropdownButtonFormField( iconDisabledColor: Colors.green, value: angleStatus, isExpanded: true, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'old angle', ), items: angleStatusItems.map((String angle) { return DropdownMenuItem( value: angle, //enabled: displayAngle, child: Text(angle), ); }).toList(), onChanged: (value) { angleStatus = value.toString(); } ), ), const SizedBox(height: 15.0), Visibility( visible: displayOldLength, child: TextFormField( readOnly: false, enabled: true, keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.onUserInteraction, inputFormatters:[FilteringTextInputFormatter.allow(RegExp("[0-9]"))], controller: oldLengthEditingController, decoration: const InputDecoration( labelText: 'old length', //Example border: OutlineInputBorder(), ), onChanged: (value) { lengthOld = int.parse(value); setState(() {}); }, ), ), const SizedBox(height: 5.0), Visibility( visible: displayOldLength, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), //TODO: find a more dynamic solution without a fixed value primary: rodColorValuesIdOld == 0 ? Colors.black54 : Colors.white12, ), onPressed: () { rodColorValuesIdOld = 0; setState(() {}); }, child: Text(rodColorValues[0]), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), //TODO: find a more dynamic solution without a fixed value primary: rodColorValuesIdOld == 1 ? Colors.black54 : Colors.white12, ), onPressed: () { rodColorValuesIdOld = 1; setState(() {}); }, child: Text(rodColorValues[1]), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), //TODO: find a more dynamic solution without a fixed value primary: rodColorValuesIdOld == 2 ? Colors.black54 : Colors.white12, ), onPressed: () { rodColorValuesIdOld = 2; setState(() {}); }, child: Text(rodColorValues[2]), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), //TODO: find a more dynamic solution without a fixed value primary: rodColorValuesIdOld == 3 ? Colors.black54 : Colors.white12, ), onPressed: () { rodColorValuesIdOld = 3; setState(() {}); }, child: Text(rodColorValues[3]), ), ] ), ), const SizedBox(height: 5.0), Visibility( visible: displayOldLength, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0), //TODO: find a more dynamic solution without a fixed value primary: event.gnssSync == true ? Colors.grey : Colors.white54, ), onPressed: () { lengthOld = rawMeasurementValue + 0; oldLengthEditingController.text = lengthOld.toString(); setState(() {}); }, child: const Text('+0m'), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0), //TODO: find a more dynamic solution without a fixed value primary: event.gnssSync == true ? Colors.grey : Colors.white54, ), onPressed: () { lengthOld = rawMeasurementValue + 2000; //2m offset oldLengthEditingController.text = lengthOld.toString(); setState(() {}); }, child: const Text('+2m'), ), ] ), ), const SizedBox(height: 30.0), Visibility( visible: displayNewLength, child: TextFormField( readOnly: false, enabled: true, keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.onUserInteraction, inputFormatters:[FilteringTextInputFormatter.allow(RegExp("[0-9]"))], controller: newLengthEditingController, decoration: const InputDecoration( labelText: 'new length', //Example border: OutlineInputBorder(), ), onChanged: (value) { lengthNew = int.parse(value); setState(() {}); }, ), ), const SizedBox(height: 5.0), Visibility( visible: displayNewLength, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), //TODO: find a more dynamic solution without a fixed value primary: rodColorValuesIdNew == 0 ? Colors.black54 : Colors.white12, ), onPressed: () { rodColorValuesIdNew = 0; setState(() {}); }, child: Text(rodColorValues[0]), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), //TODO: find a more dynamic solution without a fixed value primary: rodColorValuesIdNew == 1 ? Colors.black54 : Colors.white12, ), onPressed: () { rodColorValuesIdNew = 1; setState(() {}); }, child: Text(rodColorValues[1]), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), //TODO: find a more dynamic solution without a fixed value primary: rodColorValuesIdNew == 2 ? Colors.black54 : Colors.white12, ), onPressed: () { rodColorValuesIdNew = 2; setState(() {}); }, child: Text(rodColorValues[2]), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), //TODO: find a more dynamic solution without a fixed value primary: rodColorValuesIdNew == 3 ? Colors.black54 : Colors.white12, ), onPressed: () { rodColorValuesIdNew = 3; setState(() {}); }, child: Text(rodColorValues[3]), ), ] ), ), const SizedBox(height: 5.0), Visibility( visible: displayNewLength, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0), //TODO: find a more dynamic solution without a fixed value primary: event.gnssSync == true ? Colors.grey : Colors.white54, ), onPressed: () { lengthNew = rawMeasurementValue + 0; newLengthEditingController.text = lengthNew.toString(); setState(() {}); }, child: const Text('+0m'), ), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0), //TODO: find a more dynamic solution without a fixed value primary: event.gnssSync == true ? Colors.grey : Colors.white54, ), onPressed: () { lengthNew = rawMeasurementValue + 2000; //2m offset newLengthEditingController.text = lengthNew.toString(); setState(() {}); }, child: const Text('+2m'), ), ] ), ), const SizedBox(height: 15.0), TextFormField( initialValue: event.currentEvent.description, autovalidateMode: AutovalidateMode.always, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Description' ), onChanged: (value){ event.currentEvent.description = value; setState(() {}); }, validator: (value) { if (!RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch( value!)) { if(value == ''){ return null; //An empty description is also allowed. } return "Only: a-z , A-Z , _ , 0-9 , ,(Comma) , ( , ) , + , - , . , :"; } else { return null; // Entered Text is valid } }, ), ] ), ), ), bottomNavigationBar: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Container( margin: const EdgeInsets.symmetric(vertical: 10.0), child: _addButtonStatus() ? FloatingActionButton.extended( heroTag: null, tooltip: 'Create new event in local database', icon: null, label: const Text('Save'), onPressed: () { if (_validateInput()) { _addButtonEnabled = false; //Disable button until event is stored _storeCurrentMeasurementEvent(context, displayAngle, displayOldLength, displayNewLength); } setState(() {}); }, ): FloatingActionButton.extended( heroTag: null, tooltip: 'Input invalid', icon: null, backgroundColor: Colors.grey, label: const Text('Save'), onPressed: () { }, ), ), const SizedBox(width: 5.0), ], ), ); }else { return Scaffold( appBar: AppBar(title: const Text("Add Event")), body: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[ Container( margin: const EdgeInsets.all(10.0), child:const Text( 'Check Configuration Page for initial setup!', style: TextStyle(fontSize: 20) ), ), ], ), ); } } }