From 63e2e7b6ef3a5b7f8bb4b2604fe2c459d7865377 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 18 Jul 2022 21:18:25 +0200 Subject: [PATCH 01/22] feat(#168): add the possibility to update the end date from summary step --- .../ui/input/InputPagerFragmentActivity.kt | 2 +- .../ObserversAndDateInputFragment.kt | 280 ++---------- .../input/summary/InputTaxaSummaryFragment.kt | 59 ++- .../ui/shared/dialog/CommentDialogFragment.kt | 154 ------- .../shared/dialog/InputDateDialogFragment.kt | 153 +++++++ .../occtax/ui/shared/view/InputDateView.kt | 425 ++++++++++++++++++ .../main/res/drawable/ic_action_comment.xml | 10 - .../res/drawable/ic_action_edit_calendar.xml | 1 + .../main/res/drawable/ic_edit_calendar.xml | 10 + occtax/src/main/res/layout/dialog_comment.xml | 28 -- occtax/src/main/res/layout/dialog_date.xml | 14 + .../fragment_observers_and_date_input.xml | 77 +--- .../res/layout/list_item_taxon_summary.xml | 7 +- .../src/main/res/layout/view_input_date.xml | 74 +++ .../main/res/menu/{comment.xml => date.xml} | 6 +- occtax/src/main/res/values-fr/strings.xml | 32 +- occtax/src/main/res/values/attrs.xml | 4 + occtax/src/main/res/values/strings.xml | 32 +- 18 files changed, 788 insertions(+), 580 deletions(-) delete mode 100644 occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/CommentDialogFragment.kt create mode 100644 occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt create mode 100644 occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt delete mode 100644 occtax/src/main/res/drawable/ic_action_comment.xml create mode 100644 occtax/src/main/res/drawable/ic_edit_calendar.xml delete mode 100644 occtax/src/main/res/layout/dialog_comment.xml create mode 100644 occtax/src/main/res/layout/dialog_date.xml create mode 100644 occtax/src/main/res/layout/view_input_date.xml rename occtax/src/main/res/menu/{comment.xml => date.xml} (60%) diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt index c381c026..bf212713 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt @@ -126,7 +126,7 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit ) put( R.string.pager_fragment_summary_title, - InputTaxaSummaryFragment.newInstance() + InputTaxaSummaryFragment.newInstance(appSettings.inputSettings.dateSettings) ) } diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt index 553d5d47..ff5713fa 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt @@ -5,29 +5,21 @@ import android.content.Intent import android.database.Cursor import android.os.Bundle import android.text.Editable -import android.text.format.DateFormat -import android.text.format.DateFormat.is24HourFormat import android.util.Pair import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.EditText import android.widget.ListView import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.os.bundleOf import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader -import com.google.android.material.datepicker.CalendarConstraints -import com.google.android.material.datepicker.DateValidatorPointBackward -import com.google.android.material.datepicker.DateValidatorPointForward -import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.textfield.TextInputLayout -import com.google.android.material.timepicker.MaterialTimePicker -import com.google.android.material.timepicker.TimeFormat import dagger.hilt.android.AndroidEntryPoint import fr.geonature.commons.data.ContentProviderAuthority import fr.geonature.commons.data.GeoNatureModuleName @@ -39,8 +31,6 @@ import fr.geonature.commons.data.entity.NomenclatureType import fr.geonature.commons.data.helper.ProviderHelper.buildUri import fr.geonature.commons.input.AbstractInput import fr.geonature.commons.util.afterTextChanged -import fr.geonature.commons.util.get -import fr.geonature.commons.util.set import fr.geonature.occtax.R import fr.geonature.occtax.input.Input import fr.geonature.occtax.input.NomenclatureTypeViewType @@ -50,21 +40,16 @@ import fr.geonature.occtax.ui.dataset.DatasetListActivity import fr.geonature.occtax.ui.input.IInputFragment import fr.geonature.occtax.ui.input.InputPagerFragmentActivity import fr.geonature.occtax.ui.observers.InputObserverListActivity +import fr.geonature.occtax.ui.shared.view.InputDateView import fr.geonature.occtax.ui.shared.view.ListItemActionView import fr.geonature.occtax.util.SettingsUtils.getDefaultDatasetId import fr.geonature.occtax.util.SettingsUtils.getDefaultObserversId import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity import fr.geonature.viewpager.ui.IValidateFragment -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import org.tinylog.kotlin.Logger -import java.util.Calendar import java.util.Date import java.util.Locale import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine /** * Selected observer and current date as first {@code Fragment} used by [InputPagerFragmentActivity]. @@ -95,8 +80,7 @@ class ObserversAndDateInputFragment : Fragment(), private var selectedInputObserversActionView: ListItemActionView? = null private var selectedDatasetActionView: ListItemActionView? = null - private var dateStartTextInputLayout: TextInputLayout? = null - private var dateEndTextInputLayout: TextInputLayout? = null + private var inputDateView: InputDateView? = null private var commentTextInputLayout: TextInputLayout? = null private val loaderCallbacks = object : LoaderManager.LoaderCallbacks { @@ -337,105 +321,20 @@ class ObserversAndDateInputFragment : Fragment(), }) } - dateStartTextInputLayout = view.findViewById(R.id.dateStart)?.apply { - hint = getString( - if (dateSettings.endDateSettings == null) R.string.observers_and_date_date_hint - else R.string.observers_and_date_date_start_hint - ) - editText?.afterTextChanged { - error = checkStartDateConstraints() - dateEndTextInputLayout?.error = checkEndDateConstraints() - } - editText?.setOnClickListener { - CoroutineScope(Dispatchers.Main).launch { - val startDate = selectDateTime( - CalendarConstraints - .Builder() - .setValidator(DateValidatorPointBackward.now()) - .build(), - dateSettings.startDateSettings == InputDateSettings.DateSettings.DATETIME, - input?.startDate ?: Date() - ) - - input?.startDate = startDate - - if (dateSettings.endDateSettings == null) { - input?.endDate = startDate - } - - dateStartTextInputLayout?.editText?.apply { - updateDateEditText( - this, - dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE, - startDate - ) - } - dateEndTextInputLayout?.editText?.apply { - updateDateEditText( - this, - dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE, - input?.endDate - ) - } - - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + inputDateView = view.findViewById(R.id.input_date)?.apply { + setInputDateSettings(dateSettings) + setListener(object : InputDateView.OnInputDateViewListener { + override fun fragmentManager(): FragmentManager? { + return activity?.supportFragmentManager } - } - } - - dateEndTextInputLayout = view.findViewById(R.id.dateEnd)?.apply { - visibility = if (dateSettings.endDateSettings == null) View.GONE else View.VISIBLE - editText?.afterTextChanged { - error = checkEndDateConstraints() - dateStartTextInputLayout?.error = checkStartDateConstraints() - } - editText?.setOnClickListener { - CoroutineScope(Dispatchers.Main).launch { - val endDate = selectDateTime( - CalendarConstraints - .Builder() - .setValidator( - DateValidatorPointForward.from( - (input?.startDate ?: Date()) - .set( - Calendar.HOUR_OF_DAY, - 0 - ).set( - Calendar.MINUTE, - 0 - ).set( - Calendar.SECOND, - 0 - ).set( - Calendar.MILLISECOND, - 0 - ).time - ) - ) - .build(), - dateSettings.endDateSettings == InputDateSettings.DateSettings.DATETIME, - input?.endDate ?: input?.startDate ?: Date() - ) + override fun onDatesChanged(startDate: Date, endDate: Date) { + input?.startDate = startDate input?.endDate = endDate - dateStartTextInputLayout?.editText?.apply { - updateDateEditText( - this, - dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE, - input?.startDate - ) - } - dateEndTextInputLayout?.editText?.apply { - updateDateEditText( - this, - dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE, - endDate - ) - } (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() } - } + }) } commentTextInputLayout = view.findViewById(android.R.id.edit)?.apply { @@ -525,27 +424,20 @@ class ObserversAndDateInputFragment : Fragment(), loaderCallbacks ) - dateStartTextInputLayout?.editText?.apply { - updateDateEditText( - this, - dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE, - input?.startDate ?: Date() - ) - } - dateEndTextInputLayout?.editText?.apply { - updateDateEditText( - this, - dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE, - input?.endDate - ) - } - commentTextInputLayout?.hint = - getString( - if (input?.comment.isNullOrBlank()) R.string.observers_and_date_comment_add_hint - else R.string.observers_and_date_comment_edit_hint - ) - commentTextInputLayout?.editText?.apply { - text = input?.comment?.let { Editable.Factory.getInstance().newEditable(it) } + inputDateView?.setDates( + startDate = input?.startDate ?: Date(), + endDate = input?.endDate + ) + + commentTextInputLayout?.apply { + hint = + getString( + if (input?.comment.isNullOrBlank()) R.string.input_comment_add_hint + else R.string.input_comment_edit_hint + ) + editText?.apply { + text = input?.comment?.let { Editable.Factory.getInstance().newEditable(it) } + } } } @@ -618,118 +510,6 @@ class ObserversAndDateInputFragment : Fragment(), } } - /** - * Select a new date from given optional date through date/time pickers. - * If no date was given, use the current date. - */ - private suspend fun selectDateTime( - bounds: CalendarConstraints, - withTime: Boolean = false, - from: Date = Date() - ): Date = - suspendCoroutine { continuation -> - val supportFragmentManager = - activity?.supportFragmentManager - - if (supportFragmentManager == null) { - continuation.resume(from) - - return@suspendCoroutine - } - - val context = context - - if (context == null) { - continuation.resume(from) - - return@suspendCoroutine - } - - with( - MaterialDatePicker.Builder - .datePicker() - .setSelection(from.time) - .setCalendarConstraints(bounds) - .build() - ) { - addOnPositiveButtonClickListener { - val selectedDate = Date(it).set( - Calendar.HOUR_OF_DAY, - from.get(Calendar.HOUR_OF_DAY) - ).set( - Calendar.MINUTE, - from.get(Calendar.MINUTE) - ) - - if (!withTime) { - continuation.resume(selectedDate) - - return@addOnPositiveButtonClickListener - } - - with( - MaterialTimePicker.Builder() - .setTimeFormat(if (is24HourFormat(context)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H) - .setHour(selectedDate.get(if (is24HourFormat(context)) Calendar.HOUR_OF_DAY else Calendar.HOUR)) - .setMinute(selectedDate.get(Calendar.MINUTE)) - .build() - ) { - addOnPositiveButtonClickListener { - continuation.resume( - selectedDate.set( - if (is24HourFormat(context)) Calendar.HOUR_OF_DAY else Calendar.HOUR, - hour - ).set( - Calendar.MINUTE, - minute - ) - ) - } - addOnNegativeButtonClickListener { - continuation.resume(selectedDate) - } - addOnCancelListener { - continuation.resume(selectedDate) - } - show( - supportFragmentManager, - TIME_PICKER_DIALOG_FRAGMENT - ) - } - } - addOnNegativeButtonClickListener { - continuation.resume(from) - } - addOnCancelListener { - continuation.resume(from) - } - show( - supportFragmentManager, - DATE_PICKER_DIALOG_FRAGMENT - ) - } - } - - private fun updateDateEditText( - editText: EditText, - dateSettings: InputDateSettings.DateSettings, - date: Date? - ) { - editText.text = date?.let { - Editable.Factory - .getInstance() - .newEditable( - DateFormat.format( - getString( - if (dateSettings == InputDateSettings.DateSettings.DATETIME) R.string.observers_and_date_datetime_format - else R.string.observers_and_date_date_format - ), - it - ).toString() - ) - } - } - /** * Checks start date constraints from current [AbstractInput]. * @@ -741,10 +521,10 @@ class ObserversAndDateInputFragment : Fragment(), } val startDate = input?.startDate - ?: return getString(R.string.observers_and_date_error_date_start_not_set) + ?: return getString(R.string.input_error_date_start_not_set) if (startDate.after(Date())) { - return getString(R.string.observers_and_date_error_date_start_after_now) + return getString(R.string.input_error_date_start_after_now) } return null @@ -767,11 +547,11 @@ class ObserversAndDateInputFragment : Fragment(), } if (endDate == null) { - return getString(R.string.observers_and_date_error_date_end_not_set) + return getString(R.string.input_error_date_end_not_set) } if ((input?.startDate ?: Date()).after(endDate)) { - return getString(R.string.observers_and_date_error_date_end_before_start_date) + return getString(R.string.input_error_date_end_before_start_date) } return null @@ -781,8 +561,6 @@ class ObserversAndDateInputFragment : Fragment(), private const val ARG_DATE_SETTINGS = "arg_date_settings" - private const val DATE_PICKER_DIALOG_FRAGMENT = "date_picker_dialog_fragment" - private const val TIME_PICKER_DIALOG_FRAGMENT = "time_picker_dialog_fragment" private const val LOADER_OBSERVERS_IDS = 1 private const val LOADER_DATASET_ID = 2 private const val LOADER_DEFAULT_NOMENCLATURE_VALUES = 3 diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt index 9512d9ce..aab15cbd 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt @@ -3,7 +3,6 @@ package fr.geonature.occtax.ui.input.summary import android.os.Bundle import android.os.VibrationEffect import android.os.Vibrator -import android.text.TextUtils import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -13,7 +12,6 @@ import android.view.ViewGroup import android.view.animation.AnimationUtils import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat.getSystemService import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DividerItemDecoration @@ -25,10 +23,12 @@ import fr.geonature.commons.input.AbstractInputTaxon import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter import fr.geonature.occtax.R import fr.geonature.occtax.input.Input +import fr.geonature.occtax.settings.InputDateSettings import fr.geonature.occtax.ui.input.IInputFragment -import fr.geonature.occtax.ui.shared.dialog.CommentDialogFragment +import fr.geonature.occtax.ui.shared.dialog.InputDateDialogFragment import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity import fr.geonature.viewpager.ui.IValidateFragment +import java.util.Date /** * Summary of all edited taxa. @@ -39,27 +39,33 @@ class InputTaxaSummaryFragment : Fragment(), IValidateFragment, IInputFragment { + private lateinit var dateSettings: InputDateSettings + private var input: Input? = null private var adapter: InputTaxaSummaryRecyclerViewAdapter? = null private var recyclerView: RecyclerView? = null private var emptyTextView: TextView? = null private var fab: ExtendedFloatingActionButton? = null - private val onCommentDialogFragmentListener = - object : CommentDialogFragment.OnCommentDialogFragmentListener { - override fun onChanged(comment: String?) { - input?.comment = comment - activity?.invalidateOptionsMenu() + private val onInputDateDialogFragmentListener = + object : InputDateDialogFragment.OnInputDateDialogFragmentListener { + override fun onDatesChanged(startDate: Date, endDate: Date) { + input?.apply { + this.startDate = startDate + this.endDate = endDate + } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + dateSettings = arguments?.getParcelable(ARG_DATE_SETTINGS) ?: InputDateSettings.DEFAULT + val supportFragmentManager = activity?.supportFragmentManager ?: return - (supportFragmentManager.findFragmentByTag(COMMENT_DIALOG_FRAGMENT) as CommentDialogFragment?)?.also { - it.setOnCommentDialogFragmentListener(onCommentDialogFragmentListener) + (supportFragmentManager.findFragmentByTag(INPUT_DATE_DIALOG_FRAGMENT) as InputDateDialogFragment?)?.also { + it.setOnInputDateDialogFragmentListenerListener(onInputDateDialogFragmentListener) } } @@ -193,7 +199,7 @@ class InputTaxaSummaryFragment : Fragment(), ) inflater.inflate( - R.menu.comment, + R.menu.date, menu ) } @@ -201,24 +207,25 @@ class InputTaxaSummaryFragment : Fragment(), override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) - val commentItem = menu.findItem(R.id.menu_comment) - commentItem.title = - if (TextUtils.isEmpty(input?.comment)) getString(R.string.action_comment_add) else getString( - R.string.action_comment_edit - ) + val dateMenuItem = menu.findItem(R.id.menu_date) + dateMenuItem.isVisible = dateSettings.endDateSettings != null } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.menu_comment -> { + R.id.menu_date -> { val supportFragmentManager = activity?.supportFragmentManager ?: return true - CommentDialogFragment.newInstance(input?.comment) + InputDateDialogFragment.newInstance( + InputDateSettings(endDateSettings = dateSettings.endDateSettings), + input?.startDate ?: Date(), + input?.endDate + ) .apply { - setOnCommentDialogFragmentListener(onCommentDialogFragmentListener) + setOnInputDateDialogFragmentListenerListener(onInputDateDialogFragmentListener) show( supportFragmentManager, - COMMENT_DIALOG_FRAGMENT + INPUT_DATE_DIALOG_FRAGMENT ) } @@ -256,7 +263,8 @@ class InputTaxaSummaryFragment : Fragment(), companion object { - private const val COMMENT_DIALOG_FRAGMENT = "comment_dialog_fragment" + private const val INPUT_DATE_DIALOG_FRAGMENT = "input_date_dialog_fragment" + private const val ARG_DATE_SETTINGS = "arg_date_settings" /** * Use this factory method to create a new instance of [InputTaxaSummaryFragment]. @@ -264,6 +272,13 @@ class InputTaxaSummaryFragment : Fragment(), * @return A new instance of [InputTaxaSummaryFragment] */ @JvmStatic - fun newInstance() = InputTaxaSummaryFragment() + fun newInstance(dateSettings: InputDateSettings) = InputTaxaSummaryFragment().apply { + arguments = Bundle().apply { + putParcelable( + ARG_DATE_SETTINGS, + dateSettings + ) + } + } } } diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/CommentDialogFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/CommentDialogFragment.kt deleted file mode 100644 index e18030b5..00000000 --- a/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/CommentDialogFragment.kt +++ /dev/null @@ -1,154 +0,0 @@ -package fr.geonature.occtax.ui.shared.dialog - -import android.app.Dialog -import android.os.Bundle -import android.text.Editable -import android.text.TextUtils -import android.text.TextWatcher -import android.view.View -import android.view.ViewGroup -import android.widget.EditText -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment -import fr.geonature.commons.util.KeyboardUtils.hideSoftKeyboard -import fr.geonature.commons.util.KeyboardUtils.showSoftKeyboard -import fr.geonature.occtax.R - -/** - * Custom [Dialog] used to add comment. - * - * @author S. Grimault - */ -class CommentDialogFragment : DialogFragment() { - - private var comment: String? = null - private var onCommentDialogFragmentListener: OnCommentDialogFragmentListener? = null - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val context = requireContext() - - val view = View.inflate( - context, - R.layout.dialog_comment, - null - ) - val editText = view.findViewById(android.R.id.edit) - .also { - it.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - - override fun afterTextChanged(s: Editable?) { - } - - override fun onTextChanged( - s: CharSequence?, - start: Int, - before: Int, - count: Int - ) { - comment = s?.toString() - } - }) - } - - arguments?.getString(KEY_COMMENT) - ?.also { - comment = it - editText.text = Editable.Factory.getInstance() - .newEditable(it) - } - - // restore the previous state if any - savedInstanceState?.getString(KEY_COMMENT) - ?.also { - comment = it - editText.text = Editable.Factory.getInstance() - .newEditable(it) - } - - // show automatically the soft keyboard for the EditText - editText.post { - showSoftKeyboard(editText) - } - - return AlertDialog.Builder(context) - .setTitle(if (TextUtils.isEmpty(comment)) R.string.alert_dialog_add_comment_title else R.string.alert_dialog_edit_comment_title) - .setView(view) - .setPositiveButton(R.string.alert_dialog_ok) { _, _ -> - hideSoftKeyboard(editText) - onCommentDialogFragmentListener?.onChanged(comment) - } - .setNegativeButton( - R.string.alert_dialog_cancel, - null - ) - .create() - } - - override fun onStart() { - super.onStart() - - // resize the dialog width to match parent - dialog?.also { - it.window?.setLayout( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putSerializable( - KEY_COMMENT, - comment - ) - - super.onSaveInstanceState(outState) - } - - fun setOnCommentDialogFragmentListener(onCommentDialogFragmentListener: OnCommentDialogFragmentListener) { - - this.onCommentDialogFragmentListener = onCommentDialogFragmentListener - } - - companion object { - - const val KEY_COMMENT = "comment" - - /** - * Use this factory method to create a new instance of [CommentDialogFragment]. - * - * @return A new instance of [CommentDialogFragment] - */ - @JvmStatic - fun newInstance(comment: String?) = CommentDialogFragment().apply { - arguments = Bundle().apply { - putString( - KEY_COMMENT, - comment - ) - } - } - } - - /** - * The callback used by [CommentDialogFragment]. - * - * @author S. Grimault - */ - interface OnCommentDialogFragmentListener { - - /** - * Invoked when the positive button of the dialog is pressed. - * - * @param comment the string comment edited from this dialog - */ - fun onChanged(comment: String?) - } -} diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt new file mode 100644 index 00000000..9922894f --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt @@ -0,0 +1,153 @@ +package fr.geonature.occtax.ui.shared.dialog + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import fr.geonature.occtax.R +import fr.geonature.occtax.settings.InputDateSettings +import fr.geonature.occtax.ui.shared.view.InputDateView +import java.util.Date + +/** + * Custom [Dialog] used to edit input date. + * + * @author S. Grimault + */ +class InputDateDialogFragment : DialogFragment() { + + private var onInputDateDialogFragmentListener: OnInputDateDialogFragmentListener? = null + + private var dateSettings: InputDateSettings = + InputDateSettings(endDateSettings = InputDateSettings.DateSettings.DATE) + private var startDate: Date = Date() + private var endDate: Date = startDate + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + + val view = View.inflate( + context, + R.layout.dialog_date, + null + ) + + // restore the previous state if any + dateSettings = (savedInstanceState?.getParcelable(KEY_DATE_SETTINGS) + ?: arguments?.getParcelable(KEY_DATE_SETTINGS) + ?: InputDateSettings(endDateSettings = InputDateSettings.DateSettings.DATE)) + startDate = (savedInstanceState?.getSerializable(KEY_DATE_START) as Date?) + ?: (arguments?.getSerializable(KEY_DATE_START) as Date?) ?: Date() + endDate = (savedInstanceState?.getSerializable(KEY_DATE_END) as Date?) + ?: (arguments?.getSerializable(KEY_DATE_END) as Date?) ?: startDate + + val inputDateView = view.findViewById(R.id.input_date)?.also { + it.setInputDateSettings(dateSettings) + it.setDates( + startDate, + endDate + ) + it.setListener(object : InputDateView.OnInputDateViewListener { + override fun fragmentManager(): FragmentManager? { + return activity?.supportFragmentManager + } + + override fun onDatesChanged(startDate: Date, endDate: Date) { + this@InputDateDialogFragment.startDate = startDate + this@InputDateDialogFragment.endDate = endDate + } + }) + } + + return AlertDialog.Builder(context) + .setTitle( + if (dateSettings.startDateSettings != null && dateSettings.endDateSettings == null) R.string.input_date_start_hint + else if (dateSettings.startDateSettings == null && dateSettings.endDateSettings != null) R.string.input_date_end_hint + else R.string.input_date_hint + ) + .setView(view) + .setPositiveButton(R.string.alert_dialog_ok) { _, _ -> + onInputDateDialogFragmentListener?.onDatesChanged( + startDate, + endDate + ) + } + .setNegativeButton( + R.string.alert_dialog_cancel, + null + ) + .create() + } + + override fun onSaveInstanceState(outState: Bundle) { + with(outState) { + putParcelable( + KEY_DATE_SETTINGS, + dateSettings + ) + putSerializable( + KEY_DATE_START, + startDate + ) + putSerializable( + KEY_DATE_END, + endDate + ) + } + + super.onSaveInstanceState(outState) + } + + fun setOnInputDateDialogFragmentListenerListener(onInputDateDialogFragmentListener: OnInputDateDialogFragmentListener) { + this.onInputDateDialogFragmentListener = onInputDateDialogFragmentListener + } + + companion object { + + const val KEY_DATE_SETTINGS = "key_settings" + const val KEY_DATE_START = "key_date_start" + const val KEY_DATE_END = "key_date_end" + + /** + * Use this factory method to create a new instance of [InputDateDialogFragment]. + * + * @return A new instance of [InputDateDialogFragment] + */ + @JvmStatic + fun newInstance(dateSettings: InputDateSettings, startDate: Date, endDate: Date?) = + InputDateDialogFragment().apply { + arguments = Bundle().apply { + putParcelable( + KEY_DATE_SETTINGS, + dateSettings + ) + putSerializable( + KEY_DATE_START, + startDate + ) + putSerializable( + KEY_DATE_END, + endDate ?: startDate + ) + } + } + } + + /** + * The callback used by [InputDateDialogFragment]. + * + * @author S. Grimault + */ + interface OnInputDateDialogFragmentListener { + + /** + * Invoked when the positive button of the dialog is pressed. + * + * @param startDate the start date edited from this dialog + * @param endDate the end date edited from this dialog + */ + fun onDatesChanged(startDate: Date, endDate: Date) + } +} \ No newline at end of file diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt new file mode 100644 index 00000000..ae8c3c85 --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt @@ -0,0 +1,425 @@ +package fr.geonature.occtax.ui.shared.view + +import android.content.Context +import android.text.Editable +import android.text.format.DateFormat +import android.util.AttributeSet +import android.view.View +import android.widget.EditText +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.FragmentManager +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.DateValidatorPointBackward +import com.google.android.material.datepicker.DateValidatorPointForward +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.textfield.TextInputLayout +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat +import fr.geonature.commons.input.AbstractInput +import fr.geonature.commons.util.afterTextChanged +import fr.geonature.commons.util.get +import fr.geonature.commons.util.set +import fr.geonature.occtax.R +import fr.geonature.occtax.settings.InputDateSettings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.Calendar +import java.util.Date +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * Generic [View] about [AbstractInput] start and end date. + * + * @author S. Grimault + */ +class InputDateView : ConstraintLayout { + + private lateinit var titleTextView: TextView + private lateinit var dateStartTextInputLayout: TextInputLayout + private lateinit var dateEndTextInputLayout: TextInputLayout + + private var dateSettings: InputDateSettings = InputDateSettings.DEFAULT + private var startDate: Date = Date() + private var endDate: Date = startDate + + private var listener: OnInputDateViewListener? = null + + fun setListener(listener: OnInputDateViewListener) { + this.listener = listener + } + + fun setTitle(@StringRes titleResourceId: Int) { + setTitle(if (titleResourceId == 0) null else context.getString(titleResourceId)) + } + + fun setTitle(title: String?) { + titleTextView.text = title + titleTextView.visibility = if (title.isNullOrBlank()) GONE else VISIBLE + } + + fun setInputDateSettings(dateSettings: InputDateSettings) { + this.dateSettings = dateSettings + + with(dateStartTextInputLayout) { + visibility = if (dateSettings.startDateSettings == null) View.GONE else View.VISIBLE + hint = context.getString( + if (dateSettings.endDateSettings == null) R.string.input_date_hint + else R.string.input_date_start_hint + ) + } + dateEndTextInputLayout.visibility = + if (dateSettings.endDateSettings == null) View.GONE else View.VISIBLE + } + + fun setDates(startDate: Date, endDate: Date?) { + this.startDate = startDate + this.endDate = endDate ?: startDate + + dateStartTextInputLayout.editText?.apply { + updateDateEditText( + this, + dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE, + startDate + ) + } + dateEndTextInputLayout.editText?.apply { + updateDateEditText( + this, + dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE, + endDate + ) + } + } + + constructor(context: Context) : super(context) { + init( + null, + 0 + ) + } + + constructor(context: Context, attrs: AttributeSet) : super( + context, + attrs + ) { + init( + attrs, + 0 + ) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init( + attrs, + defStyle + ) + } + + private fun init(attrs: AttributeSet?, defStyle: Int) { + View.inflate( + context, + R.layout.view_input_date, + this + ) + + titleTextView = findViewById(android.R.id.title) + + dateStartTextInputLayout = findViewById(R.id.dateStart).apply { + visibility = if (dateSettings.startDateSettings == null) View.GONE else View.VISIBLE + hint = context.getString( + if (dateSettings.endDateSettings == null) R.string.input_date_hint + else R.string.input_date_start_hint + ) + editText?.afterTextChanged { + error = checkStartDateConstraints() + dateEndTextInputLayout.error = checkEndDateConstraints() + } + editText?.setOnClickListener { + CoroutineScope(Dispatchers.Main).launch { + val startDate = selectDateTime( + CalendarConstraints + .Builder() + .setValidator(DateValidatorPointBackward.now()) + .build(), + dateSettings.startDateSettings == InputDateSettings.DateSettings.DATETIME, + startDate + ) + + this@InputDateView.startDate = startDate + + if (dateSettings.endDateSettings == null) { + this@InputDateView.endDate = startDate + } + + dateStartTextInputLayout.editText?.apply { + updateDateEditText( + this, + dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE, + startDate + ) + } + dateEndTextInputLayout.editText?.apply { + updateDateEditText( + this, + dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE, + endDate + ) + } + + listener?.onDatesChanged( + startDate, + endDate + ) + } + } + } + + dateEndTextInputLayout = findViewById(R.id.dateEnd).apply { + visibility = if (dateSettings.endDateSettings == null) View.GONE else View.VISIBLE + editText?.afterTextChanged { + error = checkEndDateConstraints() + dateStartTextInputLayout.error = checkStartDateConstraints() + } + editText?.setOnClickListener { + CoroutineScope(Dispatchers.Main).launch { + val endDate = selectDateTime( + CalendarConstraints + .Builder() + .setValidator( + DateValidatorPointForward.from( + startDate + .set( + Calendar.HOUR_OF_DAY, + 0 + ).set( + Calendar.MINUTE, + 0 + ).set( + Calendar.SECOND, + 0 + ).set( + Calendar.MILLISECOND, + 0 + ).time + ) + ) + .build(), + dateSettings.endDateSettings == InputDateSettings.DateSettings.DATETIME, + endDate + ) + + this@InputDateView.endDate = endDate + dateStartTextInputLayout.editText?.apply { + updateDateEditText( + this, + dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE, + startDate + ) + } + dateEndTextInputLayout.editText?.apply { + updateDateEditText( + this, + dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE, + endDate + ) + } + + listener?.onDatesChanged( + startDate, + endDate + ) + } + } + } + + // Load attributes + val ta = context.obtainStyledAttributes( + attrs, + R.styleable.InputDateView, + defStyle, + 0 + ) + + ta.getString(R.styleable.InputDateView_title)?.also { + setTitle(it) + } + setTitle( + ta.getResourceId( + R.styleable.InputDateView_title, + 0 + ) + ) + + ta.recycle() + } + + /** + * Select a new date from given optional date through date/time pickers. + * If no date was given, use the current date. + */ + private suspend fun selectDateTime( + bounds: CalendarConstraints, + withTime: Boolean = false, + from: Date = Date() + ): Date = + suspendCoroutine { continuation -> + val fragmentManager = listener?.fragmentManager() + + if (fragmentManager == null) { + continuation.resume(from) + + return@suspendCoroutine + } + + val context = context + + if (context == null) { + continuation.resume(from) + + return@suspendCoroutine + } + + with( + MaterialDatePicker.Builder + .datePicker() + .setSelection(from.time) + .setCalendarConstraints(bounds) + .build() + ) { + addOnPositiveButtonClickListener { + val selectedDate = Date(it).set( + Calendar.HOUR_OF_DAY, + from.get(Calendar.HOUR_OF_DAY) + ).set( + Calendar.MINUTE, + from.get(Calendar.MINUTE) + ) + + if (!withTime) { + continuation.resume(selectedDate) + + return@addOnPositiveButtonClickListener + } + + with( + MaterialTimePicker.Builder() + .setTimeFormat(if (DateFormat.is24HourFormat(context)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H) + .setHour(selectedDate.get(if (DateFormat.is24HourFormat(context)) Calendar.HOUR_OF_DAY else Calendar.HOUR)) + .setMinute(selectedDate.get(Calendar.MINUTE)) + .build() + ) { + addOnPositiveButtonClickListener { + continuation.resume( + selectedDate.set( + if (DateFormat.is24HourFormat(context)) Calendar.HOUR_OF_DAY else Calendar.HOUR, + hour + ).set( + Calendar.MINUTE, + minute + ) + ) + } + addOnNegativeButtonClickListener { + continuation.resume(selectedDate) + } + addOnCancelListener { + continuation.resume(selectedDate) + } + show( + fragmentManager, + TIME_PICKER_DIALOG_FRAGMENT + ) + } + } + addOnNegativeButtonClickListener { + continuation.resume(from) + } + addOnCancelListener { + continuation.resume(from) + } + show( + fragmentManager, + DATE_PICKER_DIALOG_FRAGMENT + ) + } + } + + private fun updateDateEditText( + editText: EditText, + dateSettings: InputDateSettings.DateSettings, + date: Date? + ) { + editText.text = date?.let { + Editable.Factory + .getInstance() + .newEditable( + DateFormat.format( + context.getString( + if (dateSettings == InputDateSettings.DateSettings.DATETIME) R.string.input_datetime_format + else R.string.input_date_format + ), + it + ).toString() + ) + } + } + + /** + * Checks start date constraints from current [AbstractInput]. + * + * @return `null` if all constraints are valid, or an error message + */ + private fun checkStartDateConstraints(): CharSequence? { + if (startDate.after(Date())) { + return context.getString(R.string.input_error_date_start_after_now) + } + + return null + } + + /** + * Checks end date constraints from current [AbstractInput]. + * + * @return `null` if all constraints are valid, or an error message + */ + private fun checkEndDateConstraints(): CharSequence? { + if (dateSettings.endDateSettings == null) { + return null + } + + if (startDate.after(endDate)) { + return context.getString(R.string.input_error_date_end_before_start_date) + } + + return null + } + + /** + * Callback used by [InputDateView]. + */ + interface OnInputDateViewListener { + + /** + * Return the FragmentManager for interacting with fragments associated with this view. + */ + fun fragmentManager(): FragmentManager? + + /** + * Called when the start and end dates have been changed. + */ + fun onDatesChanged(startDate: Date, endDate: Date) + } + + companion object { + private const val DATE_PICKER_DIALOG_FRAGMENT = "date_picker_dialog_fragment" + private const val TIME_PICKER_DIALOG_FRAGMENT = "time_picker_dialog_fragment" + } +} \ No newline at end of file diff --git a/occtax/src/main/res/drawable/ic_action_comment.xml b/occtax/src/main/res/drawable/ic_action_comment.xml deleted file mode 100644 index 4aa3ca17..00000000 --- a/occtax/src/main/res/drawable/ic_action_comment.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/occtax/src/main/res/drawable/ic_action_edit_calendar.xml b/occtax/src/main/res/drawable/ic_action_edit_calendar.xml index 0a97af78..9f50ba54 100644 --- a/occtax/src/main/res/drawable/ic_action_edit_calendar.xml +++ b/occtax/src/main/res/drawable/ic_action_edit_calendar.xml @@ -2,6 +2,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="@color/actionbar_icon_tint" android:viewportWidth="24" android:viewportHeight="24"> + + diff --git a/occtax/src/main/res/layout/dialog_comment.xml b/occtax/src/main/res/layout/dialog_comment.xml deleted file mode 100644 index dba0daf2..00000000 --- a/occtax/src/main/res/layout/dialog_comment.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/occtax/src/main/res/layout/dialog_date.xml b/occtax/src/main/res/layout/dialog_date.xml new file mode 100644 index 00000000..95f34940 --- /dev/null +++ b/occtax/src/main/res/layout/dialog_date.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/occtax/src/main/res/layout/fragment_observers_and_date_input.xml b/occtax/src/main/res/layout/fragment_observers_and_date_input.xml index 98ae1ca8..a3834426 100644 --- a/occtax/src/main/res/layout/fragment_observers_and_date_input.xml +++ b/occtax/src/main/res/layout/fragment_observers_and_date_input.xml @@ -62,75 +62,11 @@ app:cardElevation="@dimen/cardview_elevation" app:contentPadding="@dimen/padding_default"> - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + app:title="@string/input_date" /> @@ -153,7 +89,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="@dimen/padding_default" - android:text="@string/observers_and_date_comment" + android:text="@string/input_comment" android:textAllCaps="false" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textStyle="bold" /> @@ -163,13 +99,12 @@ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="@dimen/padding_default" app:endIconMode="clear_text"> + android:paddingHorizontal="@dimen/padding_default"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/occtax/src/main/res/menu/comment.xml b/occtax/src/main/res/menu/date.xml similarity index 60% rename from occtax/src/main/res/menu/comment.xml rename to occtax/src/main/res/menu/date.xml index fe1cc23a..0a8d42c3 100644 --- a/occtax/src/main/res/menu/comment.xml +++ b/occtax/src/main/res/menu/date.xml @@ -3,9 +3,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> \ No newline at end of file diff --git a/occtax/src/main/res/values-fr/strings.xml b/occtax/src/main/res/values-fr/strings.xml index 73d48c1c..9ba0fe0d 100644 --- a/occtax/src/main/res/values-fr/strings.xml +++ b/occtax/src/main/res/values-fr/strings.xml @@ -54,8 +54,6 @@ %d sélectionnés Sauvegarder - Ajouter un commentaire - Supprimer un commentaire Nouvelle version disponible Une nouvelle version est disponible.\nVoulez vous la mettre à jour maintenant ? @@ -68,9 +66,6 @@ Souhaitez vous supprimer ce relevé ? Souhaitez vous supprimer ce dénombrement ? Souhaitez vous supprimer ce taxon ? - Ajouter un commentaire - Éditer le commentaire - Ajouter un commentaire OK Annuler @@ -106,19 +101,20 @@ Aucun observateur sélectionné.\nAjouter en un via le bouton "Ajouter". Jeu de données - Date - Date du relevé - Date de début - Date de fin - La date de début n\'a pas été définie - La date de début ne doit pas se situer dans le futur - La date de fin n\'a pas été définie - La date de fin doit être après celle du début - EEE dd MMM yyyy - EEE dd MMM yyyy \'à\' HH:mm - Commentaire du relevé - Ajouter un commentaire - Éditer le commentaire + + Date du relevé + Date du relevé + Date de début + Date de fin + La date de début n\'a pas été définie + La date de début ne doit pas se situer dans le futur + La date de fin n\'a pas été définie + La date de fin doit être après celle du début + EEE dd MMM yyyy + EEE dd MMM yyyy \'à\' HH:mm + Commentaire du relevé + Ajouter un commentaire + Éditer le commentaire %d taxon trouvé diff --git a/occtax/src/main/res/values/attrs.xml b/occtax/src/main/res/values/attrs.xml index 9726eb32..b0287854 100644 --- a/occtax/src/main/res/values/attrs.xml +++ b/occtax/src/main/res/values/attrs.xml @@ -1,6 +1,10 @@ + + + + diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml index 4764fc88..b1f162c9 100644 --- a/occtax/src/main/res/values/strings.xml +++ b/occtax/src/main/res/values/strings.xml @@ -55,8 +55,6 @@ %d selected Save - Add comment - Edit comment New version available A new version of this app is available.\nWould you like to upgrade now? @@ -69,9 +67,6 @@ Delete this input? Delete this counting? Delete this taxon? - Add a comment - Edit a comment - Add a comment OK Cancel @@ -106,19 +101,20 @@ No selected observer.\nAdd one by tapping the "Add" button. Dataset - Date - Input date - Start date - End date - Missing start date - Start date should be set before now - Missing end date - End date should be after start date - EEE dd MMM yyyy - EEE dd MMM yyyy \'at\' HH:mm - Input comment - Add a comment - Edit a comment + + Date + Input date + Start date + End date + Missing start date + Start date should be set before now + Missing end date + End date should be after start date + EEE dd MMM yyyy + EEE dd MMM yyyy \'at\' HH:mm + Input comment + Add a comment + Edit a comment %d taxon found From e8579b2b55a099308101b848caab7c9aff6a5d40 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 18 Jul 2022 21:28:38 +0200 Subject: [PATCH 02/22] feat(#167): show the number of taxa added as summary default subtitle --- .../occtax/ui/input/summary/InputTaxaSummaryFragment.kt | 8 +++++++- occtax/src/main/res/values-fr/strings.xml | 5 +++++ occtax/src/main/res/values/strings.xml | 4 ++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt index aab15cbd..2126c487 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt @@ -240,7 +240,13 @@ class InputTaxaSummaryFragment : Fragment(), } override fun getSubtitle(): CharSequence? { - return input?.getCurrentSelectedInputTaxon()?.taxon?.name + return input?.getInputTaxa()?.size?.let { + resources.getQuantityString( + R.plurals.summary_taxa_subtitle, + it, + it + ) + } } override fun pagingEnabled(): Boolean { diff --git a/occtax/src/main/res/values-fr/strings.xml b/occtax/src/main/res/values-fr/strings.xml index 9ba0fe0d..2d932f6e 100644 --- a/occtax/src/main/res/values-fr/strings.xml +++ b/occtax/src/main/res/values-fr/strings.xml @@ -162,6 +162,11 @@ Min Max + + %d taxon + %d taxons + %d taxons + Aucune saisie.\nAjouter un nouveau taxon via le bouton +. %1$s :]]> %2$s]]> diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml index b1f162c9..4fc0f0ff 100644 --- a/occtax/src/main/res/values/strings.xml +++ b/occtax/src/main/res/values/strings.xml @@ -158,6 +158,10 @@ Min Max + + %d taxon + %d taxa + No input taxon.\nCreate a new one by tapping the + button. %1$s:]]> %2$s]]> From 1c946afeaa543c493b386f5a3b0ee0827de85b93 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 18 Jul 2022 22:21:43 +0200 Subject: [PATCH 03/22] feat(#168): add start/end date validation constraints --- .../ObserversAndDateInputFragment.kt | 54 ++--------- .../shared/dialog/InputDateDialogFragment.kt | 20 ++++- .../occtax/ui/shared/view/InputDateView.kt | 90 ++++++++++++------- 3 files changed, 79 insertions(+), 85 deletions(-) diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt index ff5713fa..975fe83d 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt @@ -334,6 +334,10 @@ class ObserversAndDateInputFragment : Fragment(), (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() } + + override fun hasError(message: CharSequence) { + (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + } }) } @@ -363,8 +367,7 @@ class ObserversAndDateInputFragment : Fragment(), ?.isNotEmpty() ?: false && this.input?.datasetId != null && this.input?.properties?.isNotEmpty() == true && - checkStartDateConstraints() == null && - checkEndDateConstraints() == null + inputDateView?.hasErrors() == false } override fun refreshView() { @@ -510,53 +513,6 @@ class ObserversAndDateInputFragment : Fragment(), } } - /** - * Checks start date constraints from current [AbstractInput]. - * - * @return `null` if all constraints are valid, or an error message - */ - private fun checkStartDateConstraints(): CharSequence? { - if (input == null) { - return null - } - - val startDate = input?.startDate - ?: return getString(R.string.input_error_date_start_not_set) - - if (startDate.after(Date())) { - return getString(R.string.input_error_date_start_after_now) - } - - return null - } - - /** - * Checks end date constraints from current [AbstractInput]. - * - * @return `null` if all constraints are valid, or an error message - */ - private fun checkEndDateConstraints(): CharSequence? { - if (input == null) { - return null - } - - val endDate = input?.endDate - - if (dateSettings.endDateSettings == null) { - return null - } - - if (endDate == null) { - return getString(R.string.input_error_date_end_not_set) - } - - if ((input?.startDate ?: Date()).after(endDate)) { - return getString(R.string.input_error_date_end_before_start_date) - } - - return null - } - companion object { private const val ARG_DATE_SETTINGS = "arg_date_settings" diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt index 9922894f..2c4c686e 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt @@ -1,8 +1,10 @@ package fr.geonature.occtax.ui.shared.dialog import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import android.view.View +import android.widget.Button import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager @@ -24,6 +26,7 @@ class InputDateDialogFragment : DialogFragment() { InputDateSettings(endDateSettings = InputDateSettings.DateSettings.DATE) private var startDate: Date = Date() private var endDate: Date = startDate + private var buttonValidate: Button? = null override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val context = requireContext() @@ -43,7 +46,7 @@ class InputDateDialogFragment : DialogFragment() { endDate = (savedInstanceState?.getSerializable(KEY_DATE_END) as Date?) ?: (arguments?.getSerializable(KEY_DATE_END) as Date?) ?: startDate - val inputDateView = view.findViewById(R.id.input_date)?.also { + view.findViewById(R.id.input_date)?.also { it.setInputDateSettings(dateSettings) it.setDates( startDate, @@ -57,11 +60,18 @@ class InputDateDialogFragment : DialogFragment() { override fun onDatesChanged(startDate: Date, endDate: Date) { this@InputDateDialogFragment.startDate = startDate this@InputDateDialogFragment.endDate = endDate + + buttonValidate?.isEnabled = true + } + + override fun hasError(message: CharSequence) { + // disable validate button unless start and end date are valid + buttonValidate?.isEnabled = false } }) } - return AlertDialog.Builder(context) + val alertDialog = AlertDialog.Builder(context) .setTitle( if (dateSettings.startDateSettings != null && dateSettings.endDateSettings == null) R.string.input_date_start_hint else if (dateSettings.startDateSettings == null && dateSettings.endDateSettings != null) R.string.input_date_end_hint @@ -79,6 +89,12 @@ class InputDateDialogFragment : DialogFragment() { null ) .create() + + alertDialog.setOnShowListener { + buttonValidate = (it as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE) + } + + return alertDialog } override fun onSaveInstanceState(outState: Bundle) { diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt index ae8c3c85..aa873084 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt @@ -48,6 +48,34 @@ class InputDateView : ConstraintLayout { private var listener: OnInputDateViewListener? = null + constructor(context: Context) : super(context) { + init( + null, + 0 + ) + } + + constructor(context: Context, attrs: AttributeSet) : super( + context, + attrs + ) { + init( + attrs, + 0 + ) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init( + attrs, + defStyle + ) + } + fun setListener(listener: OnInputDateViewListener) { this.listener = listener } @@ -95,32 +123,9 @@ class InputDateView : ConstraintLayout { } } - constructor(context: Context) : super(context) { - init( - null, - 0 - ) - } - - constructor(context: Context, attrs: AttributeSet) : super( - context, - attrs - ) { - init( - attrs, - 0 - ) - } - - constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( - context, - attrs, - defStyle - ) { - init( - attrs, - defStyle - ) + fun hasErrors(): Boolean { + return checkStartDateConstraints() != null || + checkEndDateConstraints() != null } private fun init(attrs: AttributeSet?, defStyle: Int) { @@ -141,6 +146,10 @@ class InputDateView : ConstraintLayout { editText?.afterTextChanged { error = checkStartDateConstraints() dateEndTextInputLayout.error = checkEndDateConstraints() + + error?.also { + listener?.hasError(it) + } } editText?.setOnClickListener { CoroutineScope(Dispatchers.Main).launch { @@ -174,10 +183,12 @@ class InputDateView : ConstraintLayout { ) } - listener?.onDatesChanged( - startDate, - endDate - ) + if (error == null && dateEndTextInputLayout.editText?.error == null) { + listener?.onDatesChanged( + startDate, + endDate + ) + } } } } @@ -187,6 +198,10 @@ class InputDateView : ConstraintLayout { editText?.afterTextChanged { error = checkEndDateConstraints() dateStartTextInputLayout.error = checkStartDateConstraints() + + error?.also { + listener?.hasError(it) + } } editText?.setOnClickListener { CoroutineScope(Dispatchers.Main).launch { @@ -232,10 +247,12 @@ class InputDateView : ConstraintLayout { ) } - listener?.onDatesChanged( - startDate, - endDate - ) + if (error == null && dateStartTextInputLayout.editText?.error == null) { + listener?.onDatesChanged( + startDate, + endDate + ) + } } } } @@ -416,6 +433,11 @@ class InputDateView : ConstraintLayout { * Called when the start and end dates have been changed. */ fun onDatesChanged(startDate: Date, endDate: Date) + + /** + * Called when the current start or end dates is not valid. + */ + fun hasError(message: CharSequence) } companion object { From 8f67046bce83bac15fbac95c78c93e8c0a8a8cf3 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 18 Jul 2022 22:38:04 +0200 Subject: [PATCH 04/22] fix(#110): keep previous observers selection when refreshing the view for the first time --- .../ObserversAndDateInputFragment.kt | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt index 975fe83d..051bf4c9 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt @@ -373,24 +373,26 @@ class ObserversAndDateInputFragment : Fragment(), override fun refreshView() { setDefaultDatasetFromSettings() - val selectedInputObserverIds = - input?.getAllInputObserverIds() ?: context?.let { getDefaultObserversId(it) } - ?: emptyList() + input?.getAllInputObserverIds()?.also { selectedInputObserverIdsFromInput -> + val selectedInputObserverIds = selectedInputObserverIdsFromInput.ifEmpty { + context?.let { getDefaultObserversId(it) } ?: emptyList() + } - if (selectedInputObserverIds.isNotEmpty()) { - LoaderManager.getInstance(this) - .initLoader( - LOADER_OBSERVERS_IDS, - bundleOf( - kotlin.Pair( - KEY_SELECTED_INPUT_OBSERVER_IDS, - selectedInputObserverIds.toTypedArray().toLongArray() - ) - ), - loaderCallbacks - ) - } else { - updateSelectedObserversActionView(emptyList()) + if (selectedInputObserverIds.isNotEmpty()) { + LoaderManager.getInstance(this) + .initLoader( + LOADER_OBSERVERS_IDS, + bundleOf( + kotlin.Pair( + KEY_SELECTED_INPUT_OBSERVER_IDS, + selectedInputObserverIds.toTypedArray().toLongArray() + ) + ), + loaderCallbacks + ) + } else { + updateSelectedObserversActionView(emptyList()) + } } val selectedDatasetId = input?.datasetId From 3639934557f85525001a3be728dc61b671632c0f Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Wed, 20 Jul 2022 21:50:13 +0200 Subject: [PATCH 05/22] feat(#153): shows as well taxon common name --- .../InputTaxaSummaryRecyclerViewAdapter.kt | 8 ++++--- .../res/layout/list_item_taxon_summary.xml | 23 +++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt index b89db9f5..00e472a5 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt @@ -58,15 +58,17 @@ class InputTaxaSummaryRecyclerViewAdapter(listener: OnListItemRecyclerViewAdapte inner class ViewHolder(itemView: View) : AbstractListItemRecyclerViewAdapter.AbstractViewHolder(itemView) { private val title: TextView = itemView.findViewById(android.R.id.title) - private val filterChipGroup: ChipGroup = itemView.findViewById(R.id.chip_group_filter) private val text1: TextView = itemView.findViewById(android.R.id.text1) + private val filterChipGroup: ChipGroup = itemView.findViewById(R.id.chip_group_filter) + private val summary: TextView = itemView.findViewById(android.R.id.summary) private val text2: TextView = itemView.findViewById(android.R.id.text2) override fun onBind(item: AbstractInputTaxon) { title.text = item.taxon.name + text1.text = item.taxon.commonName buildTaxonomyChips(item.taxon.taxonomy) - text1.text = buildInformation(*(item as InputTaxon).properties.values.toTypedArray()) - text1.isSelected = true + summary.text = buildInformation(*(item as InputTaxon).properties.values.toTypedArray()) + summary.isSelected = true text2.text = buildCounting(item.getCounting().size) } diff --git a/occtax/src/main/res/layout/list_item_taxon_summary.xml b/occtax/src/main/res/layout/list_item_taxon_summary.xml index 9974e30c..28076bc3 100644 --- a/occtax/src/main/res/layout/list_item_taxon_summary.xml +++ b/occtax/src/main/res/layout/list_item_taxon_summary.xml @@ -23,6 +23,19 @@ app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/last_names" /> + + \ No newline at end of file From 3c1c5d2e0e7bf2a8f2a8aa236329dab7d62b99cc Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Aug 2022 14:17:20 +0200 Subject: [PATCH 06/22] fix(#91): improve a little bit the taxa search --- gn_mobile_core | 2 +- .../main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gn_mobile_core b/gn_mobile_core index cdf89b6d..8a219f7c 160000 --- a/gn_mobile_core +++ b/gn_mobile_core @@ -1 +1 @@ -Subproject commit cdf89b6d9894f2e2c9deb6237ae034bae1ec2593 +Subproject commit 8a219f7cb942746164950d9a3b6e19d3fc1476bf diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt index 1733b765..25eb9e32 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt @@ -126,7 +126,7 @@ class TaxaFragment : Fragment(), null, taxonFilter.first, taxonFilter.second.map { it.toString() }.toTypedArray(), - TaxonWithArea.OrderBy().by(AbstractTaxon.COLUMN_NAME).build() + TaxonWithArea.OrderBy().byName(args?.getString(KEY_FILTER_BY_NAME)).build() ) } LOADER_TAXON -> { From 2cbc836be37723d8dc87133bea6b9c6358647dd6 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Aug 2022 14:50:06 +0200 Subject: [PATCH 07/22] fix(#142): improve a little bit the input observers selection view --- gn_mobile_core | 2 +- .../ui/observers/InputObserverListFragment.kt | 96 +++++++++++++------ 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/gn_mobile_core b/gn_mobile_core index 8a219f7c..2fd84d2c 160000 --- a/gn_mobile_core +++ b/gn_mobile_core @@ -1 +1 @@ -Subproject commit 8a219f7cb942746164950d9a3b6e19d3fc1476bf +Subproject commit 2fd84d2c4f59e1fff9b799faf3a2b8d2a45022a7 diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverListFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverListFragment.kt index 62aaa830..0ce1c839 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverListFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverListFragment.kt @@ -41,6 +41,7 @@ class InputObserverListFragment : Fragment() { @Inject lateinit var authority: String + private lateinit var savedState: Bundle private var listener: OnInputObserverListFragmentListener? = null private var adapter: InputObserverRecyclerViewAdapter? = null @@ -52,7 +53,8 @@ class InputObserverListFragment : Fragment() { return when (id) { LOADER_OBSERVERS -> { - val selections = InputObserver.filter(args?.getString(KEY_FILTER)) + val observersFilter = + InputObserver.Filter().byName(args?.getString(KEY_FILTER)).build() CursorLoader( requireContext(), @@ -61,9 +63,9 @@ class InputObserverListFragment : Fragment() { InputObserver.TABLE_NAME ), null, - selections.first, - selections.second, - null + observersFilter.first, + observersFilter.second.map { it.toString() }.toTypedArray(), + InputObserver.OrderBy().byName(args?.getString(KEY_FILTER)).build() ) } @@ -96,6 +98,15 @@ class InputObserverListFragment : Fragment() { mode: ActionMode?, menu: Menu? ): Boolean { + mode?.menuInflater?.inflate( + R.menu.search, + menu + ) + + (menu?.findItem(R.id.action_search)?.actionView as SearchView?)?.apply { + configureSearchView(this) + } + return true } @@ -103,7 +114,17 @@ class InputObserverListFragment : Fragment() { mode: ActionMode?, menu: Menu? ): Boolean { - return false + val searchCriterion = savedState.getString(KEY_FILTER) + + (menu?.findItem(R.id.action_search)?.actionView as SearchView?)?.apply { + isIconified = searchCriterion.isNullOrEmpty() + setQuery( + searchCriterion, + false + ) + } + + return !searchCriterion.isNullOrEmpty() } override fun onActionItemClicked( @@ -119,6 +140,12 @@ class InputObserverListFragment : Fragment() { } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + savedState = savedInstanceState ?: Bundle() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -192,6 +219,10 @@ class InputObserverListFragment : Fragment() { setHasOptionsMenu(true) } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(savedState.apply { putAll(outState) }) + } + override fun onCreateOptionsMenu( menu: Menu, inflater: MenuInflater @@ -207,29 +238,9 @@ class InputObserverListFragment : Fragment() { menu ) - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - return true - } - - override fun onQueryTextChange(newText: String): Boolean { - LoaderManager.getInstance(this@InputObserverListFragment) - .restartLoader( - LOADER_OBSERVERS, - bundleOf( - Pair( - KEY_FILTER, - newText - ) - ), - loaderCallbacks - ) - - return true - } - }) + (menu.findItem(R.id.action_search)?.actionView as SearchView?)?.apply { + configureSearchView(this) + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -259,6 +270,35 @@ class InputObserverListFragment : Fragment() { listener = null } + private fun configureSearchView(searchView: SearchView) { + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + savedState.putString( + KEY_FILTER, + newText + ) + + LoaderManager.getInstance(this@InputObserverListFragment) + .restartLoader( + LOADER_OBSERVERS, + bundleOf( + Pair( + KEY_FILTER, + newText + ) + ), + loaderCallbacks + ) + + return true + } + }) + } + private fun updateActionMode(inputObservers: List) { if (inputObservers.isEmpty()) { actionMode?.finish() From bdff150b406dc9041f394450f6c404af3bd8c665 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Tue, 9 Aug 2022 21:27:08 +0200 Subject: [PATCH 08/22] fix(#120): UI improvement, give more space to show dataset name and description --- .../ui/dataset/DatasetRecyclerViewAdapter.kt | 16 ++++------------ ...lectable_item_3.xml => list_item_dataset.xml} | 15 +++------------ 2 files changed, 7 insertions(+), 24 deletions(-) rename occtax/src/main/res/layout/{list_selectable_item_3.xml => list_item_dataset.xml} (84%) diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt index 12fcf2a8..e9fa30d3 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt @@ -5,7 +5,6 @@ import android.text.format.DateFormat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.CheckBox import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.l4digital.fastscroll.FastScroller @@ -36,15 +35,9 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap val text1: TextView = v.findViewById(android.R.id.text1) text1.isSelected = true - val checkbox: CheckBox = v.findViewById(android.R.id.checkbox) - checkbox.isChecked = !checkbox.isChecked - val dataset = v.tag as Dataset - - if (checkbox.isChecked) { - selectedDataset = dataset - listener.onSelectedDataset(dataset) - } + selectedDataset = dataset + listener.onSelectedDataset(dataset) if (previousSelectedItemPosition >= 0) { notifyItemChanged(previousSelectedItemPosition) @@ -133,7 +126,7 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap inner class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context).inflate( - R.layout.list_selectable_item_3, + R.layout.list_item_dataset, parent, false ) @@ -142,7 +135,6 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap private val title: TextView = itemView.findViewById(android.R.id.title) private val text1: TextView = itemView.findViewById(android.R.id.text1) private val text2: TextView = itemView.findViewById(android.R.id.text2) - private val checkbox: CheckBox = itemView.findViewById(android.R.id.checkbox) fun bind(position: Int) { val cursor = cursor ?: return @@ -163,9 +155,9 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap dataset.createdAt ) ) - checkbox.isChecked = selectedDataset?.id == dataset.id with(itemView) { + isPressed = selectedDataset?.id == dataset.id tag = dataset setOnClickListener(onClickListener) } diff --git a/occtax/src/main/res/layout/list_selectable_item_3.xml b/occtax/src/main/res/layout/list_item_dataset.xml similarity index 84% rename from occtax/src/main/res/layout/list_selectable_item_3.xml rename to occtax/src/main/res/layout/list_item_dataset.xml index aaa44a44..c8432735 100644 --- a/occtax/src/main/res/layout/list_selectable_item_3.xml +++ b/occtax/src/main/res/layout/list_item_dataset.xml @@ -10,19 +10,11 @@ android:paddingStart="?attr/listPreferredItemPaddingStart" android:paddingEnd="?attr/listPreferredItemPaddingEnd"> - - @@ -43,8 +34,8 @@ android:layout_marginStart="?attr/listPreferredItemPaddingStart" android:ellipsize="marquee" android:marqueeRepeatLimit="marquee_forever" + android:lines="2" android:scrollHorizontally="true" - android:singleLine="true" android:textAppearance="?attr/textAppearanceListItemSecondary" app:layout_constraintEnd_toEndOf="@android:id/title" app:layout_constraintStart_toStartOf="@android:id/title" From b888157702a598b7e2229bea4f033756152a3a1d Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Tue, 9 Aug 2022 21:43:33 +0200 Subject: [PATCH 09/22] fix(#143): UI improvement --- gn_mobile_core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gn_mobile_core b/gn_mobile_core index 2fd84d2c..67f544d8 160000 --- a/gn_mobile_core +++ b/gn_mobile_core @@ -1 +1 @@ -Subproject commit 2fd84d2c4f59e1fff9b799faf3a2b8d2a45022a7 +Subproject commit 67f544d80a9bf78162ad02f4f5c75429f2522020 From 716599654150a0ddb20bc361649b223642b6a2b2 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Wed, 31 Aug 2022 22:41:35 +0200 Subject: [PATCH 10/22] fix: disable 'synchronize button' while data synchronization is still running --- occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt index f9ffe538..8fc53c44 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt @@ -421,6 +421,7 @@ class HomeActivity : AppCompatActivity() { vm.isSyncRunning.observe( this@HomeActivity ) { + appSyncView?.enableActionButton(!it) invalidateOptionsMenu() } vm From 9f72033adfaaba4e1623d25dd876c7b31dba2934 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 4 Sep 2022 15:55:07 +0200 Subject: [PATCH 11/22] feat(#177): new sequence of pages during input --- gn_mobile_core | 2 +- gn_mobile_maps | 2 +- occtax/src/main/AndroidManifest.xml | 1 + .../occtax/ui/input/AbstractInputFragment.kt | 59 +++++++ .../occtax/ui/input/IInputFragment.kt | 6 +- .../ui/input/InputPagerFragmentActivity.kt | 157 +++++++++++------- .../ui/input/OnInputPageFragmentListener.kt | 21 +++ .../ui/input/counting/CountingFragment.kt | 16 +- .../input/informations/InformationFragment.kt | 16 +- .../occtax/ui/input/map/InputMapFragment.kt | 46 +++-- .../ObserversAndDateInputFragment.kt | 29 +--- .../input/summary/InputTaxaSummaryFragment.kt | 62 ++++--- .../occtax/ui/input/taxa/TaxaFragment.kt | 42 ++--- occtax/src/main/res/values-fr/strings.xml | 4 +- occtax/src/main/res/values/strings.xml | 4 +- 15 files changed, 291 insertions(+), 176 deletions(-) create mode 100644 occtax/src/main/java/fr/geonature/occtax/ui/input/AbstractInputFragment.kt create mode 100644 occtax/src/main/java/fr/geonature/occtax/ui/input/OnInputPageFragmentListener.kt diff --git a/gn_mobile_core b/gn_mobile_core index 67f544d8..bededac5 160000 --- a/gn_mobile_core +++ b/gn_mobile_core @@ -1 +1 @@ -Subproject commit 67f544d80a9bf78162ad02f4f5c75429f2522020 +Subproject commit bededac524dd795d2e5c672bf578dbae0c52e0a0 diff --git a/gn_mobile_maps b/gn_mobile_maps index fbae4018..b67e4b3d 160000 --- a/gn_mobile_maps +++ b/gn_mobile_maps @@ -1 +1 @@ -Subproject commit fbae40182921bfbfa7e09fd955211fc051f00706 +Subproject commit b67e4b3dbac61878225aec98bda23ec7330a033f diff --git a/occtax/src/main/AndroidManifest.xml b/occtax/src/main/AndroidManifest.xml index 77e33c45..8e917ec8 100644 --- a/occtax/src/main/AndroidManifest.xml +++ b/occtax/src/main/AndroidManifest.xml @@ -49,6 +49,7 @@ android:theme="@style/AppTheme.NoActionBar" /> = Build.VERSION_CODES.R) { manageExternalStoragePermissionLifecycleObserver = ManageExternalStoragePermissionLifecycleObserver(this) @@ -84,74 +90,106 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit } Logger.info { "loading input: ${input.id}" } - - CoroutineScope(Dispatchers.Main).launch { - pagerManager.load(input.id) - } + inputViewModel.editInput(input) + + pageFragmentViewModel.set( + R.string.pager_fragment_observers_and_date_input_title to ObserversAndDateInputFragment.newInstance(appSettings.inputSettings.dateSettings), + R.string.pager_fragment_map_title to InputMapFragment.newInstance( + MapSettings.Builder.newInstance() + .from(appSettings.mapSettings!!) + .showCompass(showCompass(this)) + .showScale(showScale(this)) + .showZoom(showZoom(this)) + .build() + ), + R.string.pager_fragment_summary_title to InputTaxaSummaryFragment.newInstance(appSettings.inputSettings.dateSettings) + ) } override fun onPause() { super.onPause() - if (input.status == AbstractInput.Status.DRAFT) { - inputViewModel.saveInput(input) + inputViewModel.input.value?.takeIf { it.status == AbstractInput.Status.DRAFT }?.also { + inputViewModel.saveInput(it) } } - override val pagerFragments: Map - get() = LinkedHashMap().apply { - put( - R.string.pager_fragment_observers_and_date_input_title, - ObserversAndDateInputFragment.newInstance(appSettings.inputSettings.dateSettings) - ) - put( - R.string.pager_fragment_map_title, - InputMapFragment.newInstance(getMapSettings()) - ) - put( - R.string.pager_fragment_taxa_title, - TaxaFragment.newInstance(appSettings.areaObservationDuration) - ) - put( - R.string.pager_fragment_information_title, - InformationFragment.newInstance( - *appSettings.nomenclatureSettings?.information?.toTypedArray() ?: emptyArray() - ) - ) - put( - R.string.pager_fragment_counting_title, - CountingFragment.newInstance( - *appSettings.nomenclatureSettings?.counting?.toTypedArray() ?: emptyArray() - ) - ) - put( - R.string.pager_fragment_summary_title, - InputTaxaSummaryFragment.newInstance(appSettings.inputSettings.dateSettings) - ) - } + override fun getDefaultTitle(): CharSequence { + return getString(R.string.activity_input_title) + } + + override fun onNextAction(): Boolean { + return false + } override fun performFinishAction() { - inputViewModel.exportInput( - input, - appSettings - ) { - finish() + inputViewModel.input.value?.also { + inputViewModel.exportInput( + it, + appSettings + ) { + finish() + } } } override fun onPageSelected(position: Int) { super.onPageSelected(position) - val pageFragment = getCurrentPageFragment() + getCurrentPageFragment()?.also { page -> + if (page is IPageWithValidationFragment) { + // override the default next button color for the last page + nextButton.backgroundTintList = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + IntArray(0) + ), + intArrayOf( + ColorUtils.setAlphaComponent( + ThemeUtils.getColor( + this, + R.attr.colorOnSurface + ), + 32 + ), + if (position < ((viewPager.adapter?.itemCount + ?: 0) - 1) + ) ThemeUtils.getAccentColor(this) + else ThemeUtils.getPrimaryColor(this) + ) + ) - if (pageFragment is IInputFragment && ::input.isInitialized) { - pageFragment.setInput(input) - pageFragment.refreshView() - validateCurrentPage() - inputViewModel.saveInput(input) + hideKeyboard(page as Fragment) + } } } + override fun startEditTaxon() { + pageFragmentViewModel.add( + R.string.pager_fragment_taxa_title to TaxaFragment.newInstance(appSettings.areaObservationDuration), + R.string.pager_fragment_information_title to InformationFragment.newInstance( + *appSettings.nomenclatureSettings?.information?.toTypedArray() + ?: emptyArray() + ), + R.string.pager_fragment_counting_title to CountingFragment.newInstance( + *appSettings.nomenclatureSettings?.counting?.toTypedArray() + ?: emptyArray() + ), + R.string.pager_fragment_taxa_added_title to InputTaxaSummaryFragment.newInstance(appSettings.inputSettings.dateSettings) + ) + goToNextPage() + } + + override fun finishEditTaxon() { + input.clearCurrentSelectedInputTaxon() + removePage( + R.string.pager_fragment_taxa_title, + R.string.pager_fragment_information_title, + R.string.pager_fragment_counting_title, + R.string.pager_fragment_taxa_added_title + ) + } + override suspend fun onStoragePermissionsGranted() = suspendCancellableCoroutine { continuation -> lifecycleScope.launch { @@ -175,15 +213,6 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit } } - private fun getMapSettings(): MapSettings { - return MapSettings.Builder.newInstance() - .from(appSettings.mapSettings!!) - .showCompass(showCompass(this)) - .showScale(showScale(this)) - .showZoom(showZoom(this)) - .build() - } - companion object { private const val EXTRA_APP_SETTINGS = "extra_app_settings" diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/OnInputPageFragmentListener.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/OnInputPageFragmentListener.kt new file mode 100644 index 00000000..b1a7c2b0 --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/OnInputPageFragmentListener.kt @@ -0,0 +1,21 @@ +package fr.geonature.occtax.ui.input + +import fr.geonature.viewpager.ui.OnPageFragmentListener + +/** + * Callback used within pages to control [InputPagerFragmentActivity] view pager. + * + * @author S. Grimault + */ +interface OnInputPageFragmentListener : OnPageFragmentListener { + + /** + * Start taxon editing workflow. + */ + fun startEditTaxon() + + /** + * Finish taxon editing workflow. + */ + fun finishEditTaxon() +} \ No newline at end of file diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/CountingFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/CountingFragment.kt index 191954e3..66836622 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/CountingFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/CountingFragment.kt @@ -21,29 +21,23 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import fr.geonature.commons.data.entity.Taxonomy -import fr.geonature.commons.input.AbstractInput import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter import fr.geonature.occtax.R import fr.geonature.occtax.input.CountingMetadata import fr.geonature.occtax.input.Input import fr.geonature.occtax.input.InputTaxon import fr.geonature.occtax.settings.PropertySettings -import fr.geonature.occtax.ui.input.IInputFragment -import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity -import fr.geonature.viewpager.ui.IValidateFragment +import fr.geonature.occtax.ui.input.AbstractInputFragment /** * [Fragment] to let the user to add additional counting information for the given [Input]. * * @author S. Grimault */ -class CountingFragment : Fragment(), - IValidateFragment, - IInputFragment { +class CountingFragment : AbstractInputFragment() { private lateinit var editCountingResultLauncher: ActivityResultLauncher - private var input: Input? = null private var adapter: CountingRecyclerViewAdapter? = null private var recyclerView: RecyclerView? = null private var emptyTextView: TextView? = null @@ -125,7 +119,7 @@ class CountingFragment : Fragment(), ) { dialog, _ -> adapter?.remove(item) (input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.deleteCountingMetadata(item.index) - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() dialog.dismiss() } @@ -205,10 +199,6 @@ class CountingFragment : Fragment(), } } - override fun setInput(input: AbstractInput) { - this.input = input as Input - } - private fun launchEditCountingMetadataActivity(countingMetadata: CountingMetadata? = null) { val context = context ?: return diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/InformationFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/InformationFragment.kt index 19389919..fe56af3a 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/InformationFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/InformationFragment.kt @@ -19,15 +19,13 @@ import fr.geonature.commons.data.entity.Nomenclature import fr.geonature.commons.data.entity.NomenclatureType import fr.geonature.commons.data.entity.Taxonomy import fr.geonature.commons.data.helper.ProviderHelper.buildUri -import fr.geonature.commons.input.AbstractInput import fr.geonature.occtax.R import fr.geonature.occtax.input.Input import fr.geonature.occtax.input.InputTaxon import fr.geonature.occtax.input.PropertyValue import fr.geonature.occtax.settings.PropertySettings -import fr.geonature.occtax.ui.input.IInputFragment +import fr.geonature.occtax.ui.input.AbstractInputFragment import fr.geonature.occtax.ui.input.dialog.ChooseNomenclatureDialogFragment -import fr.geonature.viewpager.ui.IValidateFragment import org.tinylog.kotlin.Logger import javax.inject.Inject @@ -37,19 +35,17 @@ import javax.inject.Inject * @author S. Grimault */ @AndroidEntryPoint -class InformationFragment : Fragment(), - IValidateFragment, - IInputFragment, +class InformationFragment : AbstractInputFragment(), ChooseNomenclatureDialogFragment.OnChooseNomenclatureDialogFragmentListener { @ContentProviderAuthority @Inject lateinit var authority: String - private var input: Input? = null - private var adapter: NomenclatureTypesRecyclerViewAdapter? = null private lateinit var savedState: Bundle + private var adapter: NomenclatureTypesRecyclerViewAdapter? = null + private val loaderCallbacks = object : LoaderManager.LoaderCallbacks { override fun onCreateLoader( id: Int, @@ -240,10 +236,6 @@ class InformationFragment : Fragment(), ) } - override fun setInput(input: AbstractInput) { - this.input = input as Input - } - override fun onSelectedNomenclature( nomenclatureType: String, nomenclature: Nomenclature diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt index d5ec9fc2..8765a5a5 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt @@ -1,8 +1,10 @@ package fr.geonature.occtax.ui.input.map +import android.content.Context import android.os.Bundle import androidx.fragment.app.Fragment -import fr.geonature.commons.input.AbstractInput +import androidx.fragment.app.activityViewModels +import fr.geonature.commons.lifecycle.observeUntil import fr.geonature.commons.util.ThemeUtils import fr.geonature.maps.jts.geojson.GeometryUtils.fromPoint import fr.geonature.maps.jts.geojson.GeometryUtils.toPoint @@ -14,9 +16,10 @@ import fr.geonature.maps.ui.overlay.feature.filter.ContainsFeaturesFilter import fr.geonature.maps.ui.widget.EditFeatureButton import fr.geonature.occtax.R import fr.geonature.occtax.input.Input +import fr.geonature.occtax.input.InputViewModel import fr.geonature.occtax.ui.input.IInputFragment -import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity -import fr.geonature.viewpager.ui.IValidateFragment +import fr.geonature.viewpager.model.IPageWithValidationFragment +import fr.geonature.viewpager.ui.OnPageFragmentListener import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch @@ -30,9 +33,13 @@ import org.osmdroid.views.MapView * @author S. Grimault */ class InputMapFragment : MapFragment(), - IValidateFragment, + IPageWithValidationFragment, IInputFragment { + private val inputViewModel: InputViewModel by activityViewModels() + + private lateinit var listener: OnPageFragmentListener + private var input: Input? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -56,6 +63,29 @@ class InputMapFragment : MapFragment(), } } + override fun onAttach(context: Context) { + super.onAttach(context) + + if (context is OnPageFragmentListener) { + listener = context + } else { + throw RuntimeException("$context must implement ${OnPageFragmentListener::class.simpleName}") + } + } + + override fun onResume() { + super.onResume() + + inputViewModel.input.observeUntil( + viewLifecycleOwner, + { it != null }) { + if (it == null) return@observeUntil + + input = it + refreshView() + } + } + override fun onPause() { super.onPause() @@ -86,14 +116,10 @@ class InputMapFragment : MapFragment(), } } - override fun setInput(input: AbstractInput) { - this.input = input as Input - } - private fun clearInputSelection() { input?.geometry = null - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() CoroutineScope(Main).launch { getOverlays { overlay -> overlay is FeatureCollectionOverlay } @@ -131,7 +157,7 @@ class InputMapFragment : MapFragment(), .map { it.id } .firstOrNull() - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() } } diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt index 051bf4c9..deb68bf2 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt @@ -14,7 +14,6 @@ import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader @@ -29,7 +28,6 @@ import fr.geonature.commons.data.entity.DefaultNomenclatureWithType import fr.geonature.commons.data.entity.InputObserver import fr.geonature.commons.data.entity.NomenclatureType import fr.geonature.commons.data.helper.ProviderHelper.buildUri -import fr.geonature.commons.input.AbstractInput import fr.geonature.commons.util.afterTextChanged import fr.geonature.occtax.R import fr.geonature.occtax.input.Input @@ -37,29 +35,25 @@ import fr.geonature.occtax.input.NomenclatureTypeViewType import fr.geonature.occtax.input.PropertyValue import fr.geonature.occtax.settings.InputDateSettings import fr.geonature.occtax.ui.dataset.DatasetListActivity -import fr.geonature.occtax.ui.input.IInputFragment +import fr.geonature.occtax.ui.input.AbstractInputFragment import fr.geonature.occtax.ui.input.InputPagerFragmentActivity import fr.geonature.occtax.ui.observers.InputObserverListActivity import fr.geonature.occtax.ui.shared.view.InputDateView import fr.geonature.occtax.ui.shared.view.ListItemActionView import fr.geonature.occtax.util.SettingsUtils.getDefaultDatasetId import fr.geonature.occtax.util.SettingsUtils.getDefaultObserversId -import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity -import fr.geonature.viewpager.ui.IValidateFragment import org.tinylog.kotlin.Logger import java.util.Date import java.util.Locale import javax.inject.Inject /** - * Selected observer and current date as first {@code Fragment} used by [InputPagerFragmentActivity]. + * Selected observer and current date as first page used by [InputPagerFragmentActivity]. * * @author S. Grimault */ @AndroidEntryPoint -class ObserversAndDateInputFragment : Fragment(), - IValidateFragment, - IInputFragment { +class ObserversAndDateInputFragment : AbstractInputFragment() { @ContentProviderAuthority @Inject @@ -73,7 +67,6 @@ class ObserversAndDateInputFragment : Fragment(), private lateinit var datasetResultLauncher: ActivityResultLauncher private lateinit var dateSettings: InputDateSettings - private var input: Input? = null private val defaultInputObservers: MutableList = mutableListOf() private val selectedInputObservers: MutableList = mutableListOf() private var selectedDataset: Dataset? = null @@ -181,7 +174,7 @@ class ObserversAndDateInputFragment : Fragment(), if (data.count == 0) { selectedDataset = null input?.datasetId = null - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() } if (data.moveToFirst()) { @@ -219,7 +212,7 @@ class ObserversAndDateInputFragment : Fragment(), data.moveToNext() } - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() if (input?.properties?.isNotEmpty() == false) { val context = context ?: return @@ -332,11 +325,11 @@ class ObserversAndDateInputFragment : Fragment(), input?.startDate = startDate input?.endDate = endDate - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() } override fun hasError(message: CharSequence) { - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() } }) } @@ -446,10 +439,6 @@ class ObserversAndDateInputFragment : Fragment(), } } - override fun setInput(input: AbstractInput) { - this.input = input as Input - } - private fun updateSelectedObservers(selectedInputObservers: List) { this.selectedInputObservers.clear() this.selectedInputObservers.addAll(selectedInputObservers) @@ -461,7 +450,7 @@ class ObserversAndDateInputFragment : Fragment(), updateSelectedObserversActionView(selectedInputObservers) - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() } private fun updateSelectedDataset(selectedDataset: Dataset?) { @@ -471,7 +460,7 @@ class ObserversAndDateInputFragment : Fragment(), it.datasetId = selectedDataset?.id } - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() updateSelectedDatasetActionView(selectedDataset) } diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt index 2126c487..d734f120 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt @@ -1,6 +1,8 @@ package fr.geonature.occtax.ui.input.summary import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.os.VibrationEffect import android.os.Vibrator import android.view.LayoutInflater @@ -13,21 +15,16 @@ import android.view.animation.AnimationUtils import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat.getSystemService -import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import fr.geonature.commons.input.AbstractInput import fr.geonature.commons.input.AbstractInputTaxon import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter import fr.geonature.occtax.R -import fr.geonature.occtax.input.Input import fr.geonature.occtax.settings.InputDateSettings -import fr.geonature.occtax.ui.input.IInputFragment +import fr.geonature.occtax.ui.input.AbstractInputFragment import fr.geonature.occtax.ui.shared.dialog.InputDateDialogFragment -import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity -import fr.geonature.viewpager.ui.IValidateFragment import java.util.Date /** @@ -35,17 +32,15 @@ import java.util.Date * * @author S. Grimault */ -class InputTaxaSummaryFragment : Fragment(), - IValidateFragment, - IInputFragment { +class InputTaxaSummaryFragment : AbstractInputFragment() { private lateinit var dateSettings: InputDateSettings - private var input: Input? = null private var adapter: InputTaxaSummaryRecyclerViewAdapter? = null private var recyclerView: RecyclerView? = null private var emptyTextView: TextView? = null private var fab: ExtendedFloatingActionButton? = null + private var startEditTaxon = false private val onInputDateDialogFragmentListener = object : InputDateDialogFragment.OnInputDateDialogFragmentListener { @@ -103,19 +98,18 @@ class InputTaxaSummaryFragment : Fragment(), setText(R.string.action_add_taxon) extend() setOnClickListener { - ((activity as AbstractPagerFragmentActivity?))?.also { - input?.clearCurrentSelectedInputTaxon() - it.goToPreviousPage() - it.goToNextPage() - } + startEditTaxon = true + input?.clearCurrentSelectedInputTaxon() + listener.startEditTaxon() } } adapter = InputTaxaSummaryRecyclerViewAdapter(object : AbstractListItemRecyclerViewAdapter.OnListItemRecyclerViewAdapterListener { override fun onClick(item: AbstractInputTaxon) { + startEditTaxon = true input?.setCurrentSelectedInputTaxonId(item.taxon.id) - (activity as AbstractPagerFragmentActivity?)?.goToPageByKey(R.string.pager_fragment_information_title) + listener.startEditTaxon() } override fun onLongClicked( @@ -140,7 +134,7 @@ class InputTaxaSummaryFragment : Fragment(), ) { dialog, _ -> adapter?.remove(item) input?.removeInputTaxon(item.taxon.id) - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() dialog.dismiss() } @@ -188,6 +182,30 @@ class InputTaxaSummaryFragment : Fragment(), } } + override fun onResume() { + super.onResume() + + Handler(Looper.getMainLooper()).post { + // bypass this page and redirect to the previous one if we have started editing the first taxon + if (startEditTaxon && input?.getInputTaxa()?.isEmpty() == true) { + startEditTaxon = false + listener.goToPreviousPage() + return@post + } + + // no taxon added yet: redirect to the edit taxon pages + if (input?.getInputTaxa()?.isEmpty() == true) { + startEditTaxon = true + listener.startEditTaxon() + return@post + } + + // finish taxon editing workflow + startEditTaxon = false + listener.finishEditTaxon() + } + } + override fun onCreateOptionsMenu( menu: Menu, inflater: MenuInflater @@ -240,8 +258,10 @@ class InputTaxaSummaryFragment : Fragment(), } override fun getSubtitle(): CharSequence? { + val context = context ?: return null + return input?.getInputTaxa()?.size?.let { - resources.getQuantityString( + context.resources.getQuantityString( R.plurals.summary_taxa_subtitle, it, it @@ -254,7 +274,7 @@ class InputTaxaSummaryFragment : Fragment(), } override fun validate(): Boolean { - return this.input?.getCurrentSelectedInputTaxon() != null + return startEditTaxon || this.input?.getInputTaxa()?.isNotEmpty() ?: false } override fun refreshView() { @@ -263,10 +283,6 @@ class InputTaxaSummaryFragment : Fragment(), adapter?.setItems(input?.getInputTaxa() ?: emptyList()) } - override fun setInput(input: AbstractInput) { - this.input = input as Input - } - companion object { private const val INPUT_DATE_DIALOG_FRAGMENT = "input_date_dialog_fragment" diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt index 25eb9e32..94e9913b 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt @@ -15,6 +15,7 @@ import android.view.animation.AnimationUtils import android.widget.ProgressBar import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.core.view.get import androidx.fragment.app.Fragment @@ -33,15 +34,11 @@ import fr.geonature.commons.data.entity.Taxon import fr.geonature.commons.data.entity.TaxonWithArea import fr.geonature.commons.data.entity.Taxonomy import fr.geonature.commons.data.helper.ProviderHelper.buildUri -import fr.geonature.commons.input.AbstractInput import fr.geonature.commons.util.ThemeUtils import fr.geonature.occtax.R -import fr.geonature.occtax.input.Input import fr.geonature.occtax.input.InputTaxon import fr.geonature.occtax.settings.AppSettings -import fr.geonature.occtax.ui.input.IInputFragment -import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity -import fr.geonature.viewpager.ui.IValidateFragment +import fr.geonature.occtax.ui.input.AbstractInputFragment import org.tinylog.Logger import java.util.Locale import javax.inject.Inject @@ -52,9 +49,7 @@ import javax.inject.Inject * @author S. Grimault */ @AndroidEntryPoint -class TaxaFragment : Fragment(), - IValidateFragment, - IInputFragment { +class TaxaFragment : AbstractInputFragment() { @ContentProviderAuthority @Inject @@ -63,7 +58,6 @@ class TaxaFragment : Fragment(), private lateinit var savedState: Bundle private lateinit var taxaFilterResultLauncher: ActivityResultLauncher - private var input: Input? = null private var adapter: TaxaRecyclerViewAdapter? = null private var progressBar: ProgressBar? = null private var emptyTextView: View? = null @@ -167,7 +161,8 @@ class TaxaFragment : Fragment(), when (loader.id) { LOADER_TAXA -> { adapter?.bind(data) - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() + (activity as AppCompatActivity?)?.supportActionBar?.subtitle = getSubtitle() } LOADER_TAXON -> { if (data.moveToFirst()) { @@ -235,14 +230,14 @@ class TaxaFragment : Fragment(), Logger.info { "selected taxon (id: ${taxon.id}, name: '${taxon.name}', taxonomy: (kingdom='${taxon.taxonomy.kingdom}', group='${taxon.taxonomy.group}'))" } - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() } override fun onNoTaxonSelected() { input?.getCurrentSelectedInputTaxon() ?.also { input?.removeInputTaxon(it.taxon.id) } - (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + listener.validateCurrentPage() } override fun scrollToFirstSelectedItemPosition(position: Int) { @@ -381,13 +376,15 @@ class TaxaFragment : Fragment(), } override fun getSubtitle(): CharSequence? { + val context = context ?: return null + if (progressBar?.visibility == View.VISIBLE && adapter?.itemCount == 0) { return null } val taxaFound = adapter?.itemCount ?: return null - return resources.getQuantityString( + return context.resources.getQuantityString( R.plurals.taxa_found, taxaFound, taxaFound @@ -403,6 +400,12 @@ class TaxaFragment : Fragment(), } override fun refreshView() { + if (input?.selectedFeatureId.isNullOrEmpty()) savedState.remove(KEY_SELECTED_FEATURE_ID) + else savedState.putString( + KEY_SELECTED_FEATURE_ID, + input?.selectedFeatureId + ) + loadTaxa() val selectedInputTaxon = this.input?.getCurrentSelectedInputTaxon() @@ -424,19 +427,6 @@ class TaxaFragment : Fragment(), } } - override fun setInput(input: AbstractInput) { - this.input = input as Input - - savedState.putString( - KEY_SELECTED_FEATURE_ID, - input.selectedFeatureId - ) - - if (input.selectedFeatureId.isNullOrEmpty()) { - savedState.remove(KEY_SELECTED_FEATURE_ID) - } - } - private fun loadTaxa() { progressBar?.visibility = View.VISIBLE diff --git a/occtax/src/main/res/values-fr/strings.xml b/occtax/src/main/res/values-fr/strings.xml index 2d932f6e..84ceb6a9 100644 --- a/occtax/src/main/res/values-fr/strings.xml +++ b/occtax/src/main/res/values-fr/strings.xml @@ -3,6 +3,7 @@ Authentification Paramètres + Editer le relevé Jeu de données Observateurs Filtres sur les taxons @@ -88,7 +89,8 @@ Observateur & date Pointage - Taxons + Taxons ajoutés + Choix du taxon Informations Dénombrements Bilan de la saisie diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml index 4fc0f0ff..b969099e 100644 --- a/occtax/src/main/res/values/strings.xml +++ b/occtax/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Sign in Settings + Edit input Dataset Observers Taxa filters @@ -89,7 +90,8 @@ Observers & date Pointing - Taxa + Taxa added + Select taxon Information Counting Summary From 38df444d244bc5872e4ccf239a9329fa39065c28 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 4 Sep 2022 18:06:19 +0200 Subject: [PATCH 12/22] feat(#166): add taxonomy filter --- .../input/summary/InputTaxaSummaryFragment.kt | 190 ++++++++++++++++-- .../InputTaxaSummaryRecyclerViewAdapter.kt | 44 +--- .../res/layout/fragment_input_summary.xml | 65 ++++++ .../res/layout/list_item_taxon_summary.xml | 13 +- occtax/src/main/res/values-fr/strings.xml | 1 + occtax/src/main/res/values/strings.xml | 1 + 6 files changed, 243 insertions(+), 71 deletions(-) create mode 100644 occtax/src/main/res/layout/fragment_input_summary.xml diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt index d734f120..c215a999 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt @@ -1,5 +1,7 @@ package fr.geonature.occtax.ui.input.summary +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.Looper @@ -12,18 +14,28 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.animation.AnimationUtils -import android.widget.TextView +import android.widget.ProgressBar +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat.getSystemService +import androidx.core.view.get import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import fr.geonature.commons.data.entity.Taxonomy import fr.geonature.commons.input.AbstractInputTaxon import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter import fr.geonature.occtax.R +import fr.geonature.occtax.input.InputTaxon import fr.geonature.occtax.settings.InputDateSettings import fr.geonature.occtax.ui.input.AbstractInputFragment +import fr.geonature.occtax.ui.input.taxa.Filter +import fr.geonature.occtax.ui.input.taxa.FilterTaxonomy +import fr.geonature.occtax.ui.input.taxa.TaxaFilterActivity import fr.geonature.occtax.ui.shared.dialog.InputDateDialogFragment import java.util.Date @@ -34,12 +46,14 @@ import java.util.Date */ class InputTaxaSummaryFragment : AbstractInputFragment() { + private lateinit var savedState: Bundle private lateinit var dateSettings: InputDateSettings + private lateinit var taxaFilterResultLauncher: ActivityResultLauncher private var adapter: InputTaxaSummaryRecyclerViewAdapter? = null - private var recyclerView: RecyclerView? = null - private var emptyTextView: TextView? = null - private var fab: ExtendedFloatingActionButton? = null + private var progressBar: ProgressBar? = null + private var emptyTextView: View? = null + private var filterChipGroup: ChipGroup? = null private var startEditTaxon = false private val onInputDateDialogFragmentListener = @@ -55,6 +69,7 @@ class InputTaxaSummaryFragment : AbstractInputFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + savedState = savedInstanceState ?: Bundle() dateSettings = arguments?.getParcelable(ARG_DATE_SETTINGS) ?: InputDateSettings.DEFAULT val supportFragmentManager = activity?.supportFragmentManager ?: return @@ -62,6 +77,18 @@ class InputTaxaSummaryFragment : AbstractInputFragment() { (supportFragmentManager.findFragmentByTag(INPUT_DATE_DIALOG_FRAGMENT) as InputDateDialogFragment?)?.also { it.setOnInputDateDialogFragmentListenerListener(onInputDateDialogFragmentListener) } + + taxaFilterResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + if ((activityResult.resultCode != Activity.RESULT_OK) || (activityResult.data == null)) { + return@registerForActivityResult + } + + val selectedFilters = + activityResult.data?.getParcelableArrayExtra(TaxaFilterActivity.EXTRA_SELECTED_FILTERS) + ?.map { it as Filter<*> }?.toTypedArray() ?: emptyArray() + applyFilters(*selectedFilters) + } } override fun onCreateView( @@ -70,7 +97,7 @@ class InputTaxaSummaryFragment : AbstractInputFragment() { savedInstanceState: Bundle? ): View? { return inflater.inflate( - R.layout.fragment_recycler_view_fab, + R.layout.fragment_input_summary, container, false ) @@ -88,15 +115,12 @@ class InputTaxaSummaryFragment : AbstractInputFragment() { // we have a menu item to show in action bar setHasOptionsMenu(true) - recyclerView = view.findViewById(android.R.id.list) - fab = view.findViewById(R.id.fab) - + val recyclerView = view.findViewById(android.R.id.list) + progressBar = view.findViewById(android.R.id.progress) emptyTextView = view.findViewById(android.R.id.empty) - emptyTextView?.text = getString(R.string.summary_no_data) + filterChipGroup = view.findViewById(R.id.chip_group_filter) - fab?.apply { - setText(R.string.action_add_taxon) - extend() + view.findViewById(R.id.fab).apply { setOnClickListener { startEditTaxon = true input?.clearCurrentSelectedInputTaxon() @@ -182,6 +206,10 @@ class InputTaxaSummaryFragment : AbstractInputFragment() { } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(savedState.apply { putAll(outState) }) + } + override fun onResume() { super.onResume() @@ -216,10 +244,16 @@ class InputTaxaSummaryFragment : AbstractInputFragment() { inflater ) - inflater.inflate( - R.menu.date, - menu - ) + with(inflater) { + inflate( + R.menu.date, + menu + ) + inflate( + R.menu.filter, + menu + ) + } } override fun onPrepareOptionsMenu(menu: Menu) { @@ -249,6 +283,18 @@ class InputTaxaSummaryFragment : AbstractInputFragment() { return true } + R.id.menu_filter -> { + val context = context ?: return true + + taxaFilterResultLauncher.launch( + TaxaFilterActivity.newIntent( + context, + filter = getSelectedFilters().toTypedArray() + ) + ) + + true + } else -> super.onOptionsItemSelected(item) } } @@ -274,19 +320,127 @@ class InputTaxaSummaryFragment : AbstractInputFragment() { } override fun validate(): Boolean { - return startEditTaxon || this.input?.getInputTaxa()?.isNotEmpty() ?: false + return startEditTaxon || (this.input?.getInputTaxa() ?: emptyList()).any { + it is InputTaxon && it.properties.isNotEmpty() && it.getCounting().isNotEmpty() + } } override fun refreshView() { // FIXME: this is a workaround to refresh adapter's list as getInputTaxa() items are not immutable... if ((adapter?.itemCount ?: 0) > 0) adapter?.clear() - adapter?.setItems(input?.getInputTaxa() ?: emptyList()) + + val selectedFilters = + savedState.getParcelableArray(KEY_SELECTED_FILTERS)?.map { it as Filter<*> } + ?.toList() ?: emptyList() + val filterByTaxonomy = + selectedFilters.find { filter -> filter.type == Filter.FilterType.TAXONOMY }?.value as Taxonomy? + + adapter?.setItems((input?.getInputTaxa() ?: emptyList()).filter { + val taxonomy = filterByTaxonomy ?: return@filter true + + // filter by kingdom only + if (taxonomy.group == Taxonomy.ANY) { + return@filter it.taxon.taxonomy.kingdom == taxonomy.kingdom + } + + it.taxon.taxonomy == taxonomy + }) + } + + private fun applyFilters(vararg filter: Filter<*>) { + savedState.putParcelableArray( + KEY_SELECTED_FILTERS, + filter + ) + + val selectedTaxonomy = + filter.find { it.type == Filter.FilterType.TAXONOMY }?.value as Taxonomy? + + filterByTaxonomy(selectedTaxonomy) + refreshView() + } + + private fun filterByTaxonomy(selectedTaxonomy: Taxonomy?) { + val filterChipGroup = filterChipGroup ?: return + val context = context ?: return + + val taxonomyChipsToDelete = arrayListOf() + + for (i in 0 until filterChipGroup.childCount) { + with(filterChipGroup[i]) { + if (this is Chip && tag is Taxonomy) { + taxonomyChipsToDelete.add(this) + } + } + } + + taxonomyChipsToDelete.forEach { + filterChipGroup.removeView(it) + } + + filterChipGroup.visibility = if (filterChipGroup.childCount > 0) View.VISIBLE else View.GONE + + if (selectedTaxonomy != null) { + filterChipGroup.visibility = View.VISIBLE + + // build kingdom taxonomy filter chip + with( + LayoutInflater.from(context).inflate( + R.layout.chip, + filterChipGroup, + false + ) as Chip + ) { + tag = Taxonomy(selectedTaxonomy.kingdom) + text = selectedTaxonomy.kingdom + setOnClickListener { + applyFilters(*getSelectedFilters().filter { it.type != Filter.FilterType.TAXONOMY } + .toTypedArray()) + } + setOnCloseIconClickListener { + applyFilters(*getSelectedFilters().filter { it.type != Filter.FilterType.TAXONOMY } + .toTypedArray()) + } + + filterChipGroup.addView(this) + } + + // build group taxonomy filter chip + if (selectedTaxonomy.group != Taxonomy.ANY) { + with( + LayoutInflater.from(context).inflate( + R.layout.chip, + filterChipGroup, + false + ) as Chip + ) { + tag = selectedTaxonomy + text = selectedTaxonomy.group + setOnClickListener { + applyFilters(*(getSelectedFilters().filter { filter -> filter.type != Filter.FilterType.TAXONOMY } + .toTypedArray() + mutableListOf(FilterTaxonomy(Taxonomy((it.tag as Taxonomy).kingdom))))) + } + setOnCloseIconClickListener { + applyFilters(*(getSelectedFilters().filter { filter -> filter.type != Filter.FilterType.TAXONOMY } + .toTypedArray() + mutableListOf(FilterTaxonomy(Taxonomy((it.tag as Taxonomy).kingdom))))) + } + + filterChipGroup.addView(this) + } + } + } + } + + private fun getSelectedFilters(): List> { + return savedState.getParcelableArray(KEY_SELECTED_FILTERS)?.map { it as Filter<*> } + ?.toList() ?: emptyList() } companion object { private const val INPUT_DATE_DIALOG_FRAGMENT = "input_date_dialog_fragment" private const val ARG_DATE_SETTINGS = "arg_date_settings" + private const val KEY_SELECTED_FILTERS = "key_selected_filters" /** * Use this factory method to create a new instance of [InputTaxaSummaryFragment]. diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt index 00e472a5..b717a877 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt @@ -1,13 +1,10 @@ package fr.geonature.occtax.ui.input.summary import android.text.Spanned -import android.view.LayoutInflater +import android.text.SpannedString import android.view.View import android.widget.TextView import androidx.core.text.HtmlCompat -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup -import fr.geonature.commons.data.entity.Taxonomy import fr.geonature.commons.input.AbstractInputTaxon import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter import fr.geonature.occtax.R @@ -59,55 +56,20 @@ class InputTaxaSummaryRecyclerViewAdapter(listener: OnListItemRecyclerViewAdapte AbstractListItemRecyclerViewAdapter.AbstractViewHolder(itemView) { private val title: TextView = itemView.findViewById(android.R.id.title) private val text1: TextView = itemView.findViewById(android.R.id.text1) - private val filterChipGroup: ChipGroup = itemView.findViewById(R.id.chip_group_filter) private val summary: TextView = itemView.findViewById(android.R.id.summary) private val text2: TextView = itemView.findViewById(android.R.id.text2) override fun onBind(item: AbstractInputTaxon) { title.text = item.taxon.name text1.text = item.taxon.commonName - buildTaxonomyChips(item.taxon.taxonomy) summary.text = buildInformation(*(item as InputTaxon).properties.values.toTypedArray()) summary.isSelected = true text2.text = buildCounting(item.getCounting().size) } - private fun buildTaxonomyChips(taxonomy: Taxonomy) { - filterChipGroup.removeAllViews() - - // build kingdom taxonomy filter chip - with( - LayoutInflater.from(itemView.context).inflate( - R.layout.chip, - filterChipGroup, - false - ) as Chip - ) { - text = taxonomy.kingdom - filterChipGroup.addView(this) - isCloseIconVisible = false - isEnabled = false - } - - // build group taxonomy filter chip - if (taxonomy.group != Taxonomy.ANY) { - with( - LayoutInflater.from(itemView.context).inflate( - R.layout.chip, - filterChipGroup, - false - ) as Chip - ) { - text = taxonomy.group - filterChipGroup.addView(this) - isCloseIconVisible = false - isEnabled = false - } - } - } - private fun buildInformation(vararg propertyValue: PropertyValue): Spanned { - return HtmlCompat.fromHtml(propertyValue + return if (propertyValue.isEmpty()) SpannedString(itemView.context.getString(R.string.summary_taxon_information_empty)) + else HtmlCompat.fromHtml(propertyValue .asSequence() .filterNot { it.isEmpty() } .map { diff --git a/occtax/src/main/res/layout/fragment_input_summary.xml b/occtax/src/main/res/layout/fragment_input_summary.xml new file mode 100644 index 00000000..8806b88b --- /dev/null +++ b/occtax/src/main/res/layout/fragment_input_summary.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/occtax/src/main/res/layout/list_item_taxon_summary.xml b/occtax/src/main/res/layout/list_item_taxon_summary.xml index 28076bc3..b0f4a2cd 100644 --- a/occtax/src/main/res/layout/list_item_taxon_summary.xml +++ b/occtax/src/main/res/layout/list_item_taxon_summary.xml @@ -36,17 +36,6 @@ app:layout_constraintTop_toBottomOf="@android:id/title" tools:text="@tools:sample/first_names" /> - - Aucune saisie.\nAjouter un nouveau taxon via le bouton +. %1$s :]]> %2$s]]> + aucune information saisie %d dénombrement %d dénombrements diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml index b969099e..97af1edf 100644 --- a/occtax/src/main/res/values/strings.xml +++ b/occtax/src/main/res/values/strings.xml @@ -166,6 +166,7 @@ No input taxon.\nCreate a new one by tapping the + button. %1$s:]]> %2$s]]> + no information one counting %d counting From 2681534399710fd38d2b7b35e73a8042992d7ba2 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 5 Sep 2022 20:43:57 +0200 Subject: [PATCH 13/22] fix(#120): UI improvement, give more space to show dataset name and description --- .../geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt | 1 + occtax/src/main/res/layout/list_item_dataset.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt index e9fa30d3..01893ca6 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt @@ -146,6 +146,7 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap if (dataset != null) { title.text = dataset.name title.isSelected = selectedDataset?.id == dataset.id + title.isSelected = true text1.text = dataset.description text1.isSelected = selectedDataset?.id == dataset.id text2.text = itemView.context.getString( diff --git a/occtax/src/main/res/layout/list_item_dataset.xml b/occtax/src/main/res/layout/list_item_dataset.xml index c8432735..21d03682 100644 --- a/occtax/src/main/res/layout/list_item_dataset.xml +++ b/occtax/src/main/res/layout/list_item_dataset.xml @@ -34,7 +34,7 @@ android:layout_marginStart="?attr/listPreferredItemPaddingStart" android:ellipsize="marquee" android:marqueeRepeatLimit="marquee_forever" - android:lines="2" + android:lines="3" android:scrollHorizontally="true" android:textAppearance="?attr/textAppearanceListItemSecondary" app:layout_constraintEnd_toEndOf="@android:id/title" From 311f56f1e342eb99e93c72fa857e89d976add94c Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Tue, 6 Sep 2022 22:37:35 +0200 Subject: [PATCH 14/22] fix(#120): UI improvement, give more space to show dataset name and description --- .../ObserversAndDateInputFragment.kt | 24 +- .../occtax/ui/shared/view/ActionView.kt | 225 ++++++++++++++++++ .../fragment_observers_and_date_input.xml | 44 +++- occtax/src/main/res/layout/view_action.xml | 48 ++++ occtax/src/main/res/values/attrs.xml | 13 + 5 files changed, 340 insertions(+), 14 deletions(-) create mode 100644 occtax/src/main/java/fr/geonature/occtax/ui/shared/view/ActionView.kt create mode 100644 occtax/src/main/res/layout/view_action.xml diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt index deb68bf2..05c077e8 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt @@ -10,6 +10,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ListView +import android.widget.TextView import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -38,6 +39,7 @@ import fr.geonature.occtax.ui.dataset.DatasetListActivity import fr.geonature.occtax.ui.input.AbstractInputFragment import fr.geonature.occtax.ui.input.InputPagerFragmentActivity import fr.geonature.occtax.ui.observers.InputObserverListActivity +import fr.geonature.occtax.ui.shared.view.ActionView import fr.geonature.occtax.ui.shared.view.InputDateView import fr.geonature.occtax.ui.shared.view.ListItemActionView import fr.geonature.occtax.util.SettingsUtils.getDefaultDatasetId @@ -72,7 +74,7 @@ class ObserversAndDateInputFragment : AbstractInputFragment() { private var selectedDataset: Dataset? = null private var selectedInputObserversActionView: ListItemActionView? = null - private var selectedDatasetActionView: ListItemActionView? = null + private var selectedDatasetActionView: ActionView? = null private var inputDateView: InputDateView? = null private var commentTextInputLayout: TextInputLayout? = null @@ -299,8 +301,8 @@ class ObserversAndDateInputFragment : AbstractInputFragment() { } selectedDatasetActionView = - view.findViewById(R.id.selected_dataset_action_view)?.apply { - setListener(object : ListItemActionView.OnListItemActionViewListener { + view.findViewById(R.id.selected_dataset_action_view)?.apply { + setListener(object : ActionView.OnActionViewListener { override fun onAction() { val context = context ?: return @@ -482,15 +484,13 @@ class ObserversAndDateInputFragment : AbstractInputFragment() { } private fun updateSelectedDatasetActionView(selectedDataset: Dataset?) { - selectedDatasetActionView?.setItems( - if (selectedDataset == null) emptyList() - else listOf( - Pair.create( - selectedDataset.name, - selectedDataset.description - ) - ) - ) + selectedDatasetActionView?.getContentView()?.also { contentView -> + contentView.isSelected = true + contentView.findViewById(R.id.dataset_name)?.text = selectedDataset?.name + contentView.findViewById(R.id.dataset_description)?.text = + selectedDataset?.description + } + selectedDatasetActionView?.setContentViewVisibility(if (selectedDataset == null) View.GONE else View.VISIBLE) } private fun setDefaultDatasetFromSettings() { diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/ActionView.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/ActionView.kt new file mode 100644 index 00000000..a2ae664b --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/ActionView.kt @@ -0,0 +1,225 @@ +package fr.geonature.occtax.ui.shared.view + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.FrameLayout +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.children +import fr.geonature.occtax.R + +/** + * Generic [View] about adding custom view with an action button. + * + * @author S. Grimault + */ +class ActionView : ConstraintLayout { + + private var contentView: FrameLayout? = null + private lateinit var titleTextView: TextView + private lateinit var actionButton: Button + private lateinit var emptyTextView: TextView + private var listener: OnActionViewListener? = null + + private var contentViewVisibility: Int = View.GONE + + @StringRes + private var actionText: Int = 0 + + @StringRes + private var actionEmptyText: Int = 0 + + constructor(context: Context) : super(context) { + init( + null, + 0 + ) + } + + constructor( + context: Context, + attrs: AttributeSet + ) : super( + context, + attrs + ) { + init( + attrs, + 0 + ) + } + + constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int + ) : super( + context, + attrs, + defStyleAttr + ) { + init( + attrs, + defStyleAttr + ) + } + + override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { + if (contentView == null) { + super.addView( + child, + index, + params + ) + } else { + contentView?.children?.asSequence()?.filter { it.id != emptyTextView.id }?.forEach { + contentView?.removeView(it) + } + contentView?.addView( + child, + index, + params + ) + setContentViewVisibility(contentViewVisibility) + } + } + + fun getContentView(): View? { + return contentView?.children?.asSequence()?.firstOrNull { it.id != emptyTextView.id } + } + + fun setListener(listener: OnActionViewListener) { + this.listener = listener + } + + fun setTitle(@StringRes titleResourceId: Int) { + setTitle(if (titleResourceId == 0) null else context.getString(titleResourceId)) + } + + fun setTitle(title: String?) { + titleTextView.text = title + titleTextView.visibility = if (title.isNullOrBlank()) GONE else VISIBLE + } + + fun setEmptyText(@StringRes emptyTextResourceId: Int) { + emptyTextView.setText(if (emptyTextResourceId == 0) R.string.no_data else emptyTextResourceId) + } + + fun enableActionButton(enabled: Boolean = true) { + actionButton.isEnabled = enabled + } + + fun setActionText(@StringRes actionResourceId: Int) { + if (actionResourceId == 0) { + return + } + + actionText = actionResourceId + actionEmptyText = actionResourceId + } + + fun setActionEmptyText(@StringRes actionResourceId: Int) { + if (actionResourceId == 0) { + return + } + + actionEmptyText = actionResourceId + } + + fun setContentViewVisibility(visibility: Int) { + contentViewVisibility = visibility + actionButton.setText(if (visibility == View.VISIBLE) actionText else actionEmptyText.takeIf { it > 0 } + ?: actionText) + contentView?.children?.asSequence()?.forEach { + if (it.id == emptyTextView.id) it.visibility = + if (visibility == View.VISIBLE) View.GONE else View.VISIBLE + else it.visibility = if (visibility == View.VISIBLE) View.VISIBLE else View.GONE + } + } + + private fun init( + attrs: AttributeSet?, + defStyle: Int + ) { + View.inflate( + context, + R.layout.view_action, + this + ) + + titleTextView = findViewById(android.R.id.title) + actionButton = findViewById(android.R.id.button1) + actionButton.setOnClickListener { listener?.onAction() } + contentView = findViewById(android.R.id.content) + emptyTextView = findViewById(android.R.id.empty) + + // load attributes + val ta = context.obtainStyledAttributes( + attrs, + R.styleable.ActionView, + defStyle, + 0 + ) + + ta.getString(R.styleable.ActionView_title)?.also { + setTitle(it) + } + setTitle( + ta.getResourceId( + R.styleable.ActionView_title, + 0 + ) + ) + + setEmptyText( + ta.getResourceId( + R.styleable.ActionView_no_data, + R.string.no_data + ) + ) + + enableActionButton( + ta.getBoolean( + R.styleable.ActionView_action_enabled, + true + ) + ) + + setActionText( + ta.getResourceId( + R.styleable.ActionView_action, + 0 + ) + ) + setActionEmptyText( + ta.getResourceId( + R.styleable.ActionView_action_empty, + 0 + ) + ) + + setContentViewVisibility( + ta.getInt( + R.styleable.ActionView_content_visibility, + View.GONE + ) + ) + + ta.recycle() + } + + /** + * Callback used by [ActionView]. + */ + interface OnActionViewListener { + + /** + * Called when the action button has been clicked. + */ + fun onAction() + } +} \ No newline at end of file diff --git a/occtax/src/main/res/layout/fragment_observers_and_date_input.xml b/occtax/src/main/res/layout/fragment_observers_and_date_input.xml index a3834426..2dae7249 100644 --- a/occtax/src/main/res/layout/fragment_observers_and_date_input.xml +++ b/occtax/src/main/res/layout/fragment_observers_and_date_input.xml @@ -42,13 +42,53 @@ app:cardElevation="@dimen/cardview_elevation" app:contentPadding="@dimen/padding_default"> - + app:content_visibility="gone" + app:title="@string/observers_and_date_dataset"> + + + + + + + + + + diff --git a/occtax/src/main/res/layout/view_action.xml b/occtax/src/main/res/layout/view_action.xml new file mode 100644 index 00000000..2502f501 --- /dev/null +++ b/occtax/src/main/res/layout/view_action.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + +