From 22113b0fad5c69cb2d87e3e6eb002e3dc5751d6c Mon Sep 17 00:00:00 2001 From: Luffy Date: Fri, 26 Dec 2025 00:58:47 +0100 Subject: [PATCH 1/4] Fix #744 - Dialog view is destroyed (state not preserved) when rotating the screen --- .../dialogs/ChecklistItemDialogFragment.kt | 146 ++++++++++++++++++ .../fossify/notes/fragments/TasksFragment.kt | 72 ++++++--- 2 files changed, 194 insertions(+), 24 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/notes/dialogs/ChecklistItemDialogFragment.kt diff --git a/app/src/main/kotlin/org/fossify/notes/dialogs/ChecklistItemDialogFragment.kt b/app/src/main/kotlin/org/fossify/notes/dialogs/ChecklistItemDialogFragment.kt new file mode 100644 index 000000000..31ea56934 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/notes/dialogs/ChecklistItemDialogFragment.kt @@ -0,0 +1,146 @@ +package org.fossify.notes.dialogs + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText +import androidx.fragment.app.DialogFragment +import org.fossify.notes.R +import org.fossify.notes.databinding.DialogNewChecklistItemBinding +import org.fossify.notes.databinding.ItemAddChecklistBinding +import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.getContrastColor +import org.fossify.commons.extensions.getProperPrimaryColor +import org.fossify.commons.extensions.showKeyboard +import org.fossify.commons.R as CommonsR + +class ChecklistItemDialogFragment : DialogFragment() { + + private val activeInputFields = mutableListOf() + private var binding: DialogNewChecklistItemBinding? = null + + companion object { + const val DIALOG_TAG = "ChecklistItemDialogFragment" + const val REQUEST_KEY = "ChecklistItemRequest" + const val RESULT_TEXT_KEY = "ResultText" + const val RESULT_TASK_ID_KEY = "ResultTaskId" + + private const val ARG_TEXT = "ArgText" + private const val ARG_TASK_ID = "ArgTaskId" + private const val SAVED_STATE_TEXTS = "SavedStateTexts" + + fun newInstance(taskId: Int = -1, text: String = ""): ChecklistItemDialogFragment { + val fragment = ChecklistItemDialogFragment() + val args = Bundle() + args.putInt(ARG_TASK_ID, taskId) + args.putString(ARG_TEXT, text) + fragment.arguments = args + return fragment + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val activity = requireActivity() + val taskId = arguments?.getInt(ARG_TASK_ID) ?: -1 + + binding = DialogNewChecklistItemBinding.inflate(activity.layoutInflater) + + activeInputFields.clear() + + // Restore rows + if (savedInstanceState != null) { + val savedTexts = savedInstanceState.getStringArrayList(SAVED_STATE_TEXTS) + if (!savedTexts.isNullOrEmpty()) { + savedTexts.forEach { text -> addNewRow(text) } + } else { + addNewRow("") + } + } else { + val initialText = arguments?.getString(ARG_TEXT) ?: "" + addNewRow(initialText) + } + + val isNewTaskMode = (taskId == -1) + if (isNewTaskMode) { + val contrastColor = activity.getProperPrimaryColor().getContrastColor() + binding!!.addItem.setColorFilter(contrastColor) + + binding!!.addItem.setOnClickListener { + addNewRow("") + } + } else { + binding!!.addItem.visibility = View.GONE + binding!!.settingsAddChecklistTop.visibility = View.GONE + } + + val titleRes = if (isNewTaskMode) R.string.add_new_checklist_items else R.string.rename_note + + val builder = activity.getAlertDialogBuilder() + .setTitle(titleRes) + .setView(binding!!.root) + .setPositiveButton(CommonsR.string.ok, null) + .setNegativeButton(CommonsR.string.cancel, null) + + val dialog = builder.create() + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + + dialog.setOnShowListener { + if (activeInputFields.isNotEmpty()) { + dialog.showKeyboard(activeInputFields.last()) + } + + val positiveButton = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) + positiveButton.setOnClickListener { + val combinedText = activeInputFields + .map { it.text.toString().trim() } + .filter { it.isNotEmpty() } + .joinToString("\n") + + if (combinedText.isNotEmpty()) { + val resultBundle = Bundle().apply { + putString(RESULT_TEXT_KEY, combinedText) + putInt(RESULT_TASK_ID_KEY, taskId) + } + parentFragmentManager.setFragmentResult(REQUEST_KEY, resultBundle) + dialog.dismiss() + } else { + dialog.dismiss() + } + } + } + + return dialog + } + + private fun addNewRow(text: String) { + val rowBinding = ItemAddChecklistBinding.inflate(layoutInflater) + + // We disable automatic state saving for this view. + // This prevents Android from confusing the multiple EditTexts (which all share the same ID) and overwriting our manually restored text with the last view's text. + rowBinding.titleEditText.isSaveEnabled = false + + rowBinding.titleEditText.setText(text) + + if (text.isNotEmpty()) { + rowBinding.titleEditText.setSelection(text.length) + } + + val inputField = rowBinding.titleEditText as AppCompatEditText + activeInputFields.add(inputField) + + binding?.checklistHolder?.addView(rowBinding.root) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + val currentTexts = ArrayList(activeInputFields.map { it.text.toString() }) + outState.putStringArrayList(SAVED_STATE_TEXTS, currentTexts) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } +} diff --git a/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt b/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt index 11117e10d..5513a44b2 100644 --- a/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt +++ b/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt @@ -14,6 +14,7 @@ import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.notes.activities.SimpleActivity import org.fossify.notes.adapters.TasksAdapter import org.fossify.notes.databinding.FragmentChecklistBinding +import org.fossify.notes.dialogs.ChecklistItemDialogFragment import org.fossify.notes.dialogs.EditTaskDialog import org.fossify.notes.dialogs.NewChecklistItemDialog import org.fossify.notes.extensions.config @@ -43,6 +44,24 @@ class TasksFragment : NoteFragment(), TasksActionListener { return binding.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Listen for results from the ChecklistItemDialogFragment + childFragmentManager.setFragmentResultListener(ChecklistItemDialogFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle -> + val text = bundle.getString(ChecklistItemDialogFragment.RESULT_TEXT_KEY) ?: return@setFragmentResultListener + val taskId = bundle.getInt(ChecklistItemDialogFragment.RESULT_TASK_ID_KEY, -1) + + if (taskId == -1) { + // ID is -1, so we are adding a NEW item + addNewChecklistItems(text) + } else { + // ID exists, so we are EDITING an existing item + updateExistingTask(taskId, text) + } + } + } + override fun onResume() { super.onResume() loadNoteById(noteId) @@ -137,30 +156,30 @@ class TasksFragment : NoteFragment(), TasksActionListener { setupLockedViews(this.toCommonBinding(), note!!) } } - private fun showNewItemDialog() { - NewChecklistItemDialog(activity as SimpleActivity, noteId) { titles -> - var currentMaxId = tasks.maxByOrNull { item -> item.id }?.id ?: 0 - val newItems = ArrayList() - - titles.forEach { title -> - title.split("\n").map { it.trim() }.filter { it.isNotBlank() }.forEach { row -> - newItems.add(Task(currentMaxId + 1, System.currentTimeMillis(), row, false)) - currentMaxId++ - } - } + // Pass -1 to indicate a NEW item + ChecklistItemDialogFragment.newInstance(taskId = -1, text = "") + .show(childFragmentManager, ChecklistItemDialogFragment.DIALOG_TAG) + } - if (config?.addNewChecklistItemsTop == true) { - tasks.addAll(0, newItems) - } else { - tasks.addAll(newItems) - } + private fun addNewChecklistItems(text: String) { + var currentMaxId = tasks.maxByOrNull { item -> item.id }?.id ?: 0 + val newItems = ArrayList() - saveNote() - setupAdapter() + text.split("\n").map { it.trim() }.filter { it.isNotBlank() }.forEach { row -> + newItems.add(Task(currentMaxId + 1, System.currentTimeMillis(), row, false)) + currentMaxId++ + } + + if (config?.addNewChecklistItemsTop == true) { + tasks.addAll(0, newItems) + } else { + tasks.addAll(newItems) } - } + saveNote() + setupAdapter() + } private fun prepareTaskItems(): List { return if (config?.moveDoneChecklistItems == true) { mutableListOf().apply { @@ -272,12 +291,17 @@ class TasksFragment : NoteFragment(), TasksActionListener { fun getTasks() = Gson().toJson(tasks) override fun editTask(task: Task, callback: () -> Unit) { - EditTaskDialog(activity as SimpleActivity, task.title) { title -> - val editedTask = task.copy(title = title) - val index = tasks.indexOf(task) - tasks[index] = editedTask + ChecklistItemDialogFragment.newInstance(taskId = task.id, text = task.title) + .show(childFragmentManager, ChecklistItemDialogFragment.DIALOG_TAG) + } + + private fun updateExistingTask(taskId: Int, newTitle: String) { + val taskIndex = tasks.indexOfFirst { it.id == taskId } + if (taskIndex != -1) { + val task = tasks[taskIndex] + val editedTask = task.copy(title = newTitle) + tasks[taskIndex] = editedTask saveAndReload() - callback() } } From fb9e2468ff406c51b82f2dd07eb8d545d7a39445 Mon Sep 17 00:00:00 2001 From: Luffy Date: Sat, 27 Dec 2025 15:14:28 +0100 Subject: [PATCH 2/4] update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c202b8417..62888b116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed +- Fixed checklist dialog disappearing on screen rotation ([#744]) - Fixed inconsistent checklist sorting when the "Move checked items to the bottom" option is enabled ([#59]) ## [1.6.0] - 2025-10-29 @@ -105,6 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#178]: https://github.com/FossifyOrg/Notes/issues/178 [#190]: https://github.com/FossifyOrg/Notes/issues/190 [#201]: https://github.com/FossifyOrg/Notes/issues/201 +[#744]: https://github.com/FossifyOrg/General-Discussion/issues/744 [Unreleased]: https://github.com/FossifyOrg/Notes/compare/1.6.0...HEAD [1.6.0]: https://github.com/FossifyOrg/Notes/compare/1.5.0...1.6.0 From eae017690c40e40721c18809da508c9b2d81d9de Mon Sep 17 00:00:00 2001 From: Luffy Date: Sat, 27 Dec 2025 16:27:05 +0100 Subject: [PATCH 3/4] Fix for PR : reduced lines lenght + optimized imports --- .../fossify/notes/dialogs/ChecklistItemDialogFragment.kt | 3 ++- .../kotlin/org/fossify/notes/fragments/TasksFragment.kt | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/notes/dialogs/ChecklistItemDialogFragment.kt b/app/src/main/kotlin/org/fossify/notes/dialogs/ChecklistItemDialogFragment.kt index 31ea56934..b3edc00b3 100644 --- a/app/src/main/kotlin/org/fossify/notes/dialogs/ChecklistItemDialogFragment.kt +++ b/app/src/main/kotlin/org/fossify/notes/dialogs/ChecklistItemDialogFragment.kt @@ -118,7 +118,8 @@ class ChecklistItemDialogFragment : DialogFragment() { val rowBinding = ItemAddChecklistBinding.inflate(layoutInflater) // We disable automatic state saving for this view. - // This prevents Android from confusing the multiple EditTexts (which all share the same ID) and overwriting our manually restored text with the last view's text. + // This prevents Android from confusing the multiple EditTexts (which all share the same ID) + // and overwriting our manually restored text with the last view's text. rowBinding.titleEditText.isSaveEnabled = false rowBinding.titleEditText.setText(text) diff --git a/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt b/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt index 5df578725..62c04c0f7 100644 --- a/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt +++ b/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt @@ -15,8 +15,6 @@ import org.fossify.notes.activities.SimpleActivity import org.fossify.notes.adapters.TasksAdapter import org.fossify.notes.databinding.FragmentChecklistBinding import org.fossify.notes.dialogs.ChecklistItemDialogFragment -import org.fossify.notes.dialogs.EditTaskDialog -import org.fossify.notes.dialogs.NewChecklistItemDialog import org.fossify.notes.extensions.config import org.fossify.notes.extensions.updateWidgets import org.fossify.notes.helpers.NOTE_ID @@ -48,7 +46,9 @@ class TasksFragment : NoteFragment(), TasksActionListener { super.onViewCreated(view, savedInstanceState) // Listen for results from the ChecklistItemDialogFragment - childFragmentManager.setFragmentResultListener(ChecklistItemDialogFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle -> + childFragmentManager.setFragmentResultListener(ChecklistItemDialogFragment.REQUEST_KEY, + viewLifecycleOwner) + { _, bundle -> val text = bundle.getString(ChecklistItemDialogFragment.RESULT_TEXT_KEY) ?: return@setFragmentResultListener val taskId = bundle.getInt(ChecklistItemDialogFragment.RESULT_TASK_ID_KEY, -1) From 5404d92910a48b4cc63e01e6730b15ab2a17e1bf Mon Sep 17 00:00:00 2001 From: Luffy Date: Wed, 31 Dec 2025 02:08:56 +0100 Subject: [PATCH 4/4] Implementation of the PR feedbacks and previous features --- CHANGELOG.md | 3 +- .../notes/dialogs/EditTaskDialogFragment.kt | 78 +++++++ .../dialogs/NewChecklistItemDialogFragment.kt | 211 ++++++++++++++++++ .../fossify/notes/fragments/TasksFragment.kt | 44 ++-- 4 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/notes/dialogs/EditTaskDialogFragment.kt create mode 100644 app/src/main/kotlin/org/fossify/notes/dialogs/NewChecklistItemDialogFragment.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 62888b116..6896ac7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed -- Fixed checklist dialog disappearing on screen rotation ([#744]) +- Fixed the checklist dialog disappearing on screen rotation - Fixed inconsistent checklist sorting when the "Move checked items to the bottom" option is enabled ([#59]) ## [1.6.0] - 2025-10-29 @@ -106,7 +106,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#178]: https://github.com/FossifyOrg/Notes/issues/178 [#190]: https://github.com/FossifyOrg/Notes/issues/190 [#201]: https://github.com/FossifyOrg/Notes/issues/201 -[#744]: https://github.com/FossifyOrg/General-Discussion/issues/744 [Unreleased]: https://github.com/FossifyOrg/Notes/compare/1.6.0...HEAD [1.6.0]: https://github.com/FossifyOrg/Notes/compare/1.5.0...1.6.0 diff --git a/app/src/main/kotlin/org/fossify/notes/dialogs/EditTaskDialogFragment.kt b/app/src/main/kotlin/org/fossify/notes/dialogs/EditTaskDialogFragment.kt new file mode 100644 index 000000000..329fc22c1 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/notes/dialogs/EditTaskDialogFragment.kt @@ -0,0 +1,78 @@ +package org.fossify.notes.dialogs + +import android.content.DialogInterface +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.setupDialogStuff +import org.fossify.commons.extensions.showKeyboard +import org.fossify.commons.extensions.toast +import org.fossify.notes.databinding.DialogRenameChecklistItemBinding +import org.fossify.notes.extensions.maybeRequestIncognito +import org.fossify.notes.models.Task + +class EditTaskDialogFragment : DialogFragment() { + + companion object { + const val TAG = "EditTaskDialog" + const val ARG_TASK_ID = "arg_task_id" + const val ARG_OLD_TITLE = "arg_old_title" + const val REQUEST_KEY = "edit_task_request" + const val RESULT_TITLE = "result_title" + const val RESULT_TASK_ID = "result_task_id" + private const val STATE_TEXT = "state_text" + + fun show( + host: androidx.fragment.app.FragmentManager, + task: Task + ) = EditTaskDialogFragment().apply { + arguments = bundleOf(ARG_OLD_TITLE to task.title, ARG_TASK_ID to task.id) + }.show(host, TAG) + } + + private lateinit var binding: DialogRenameChecklistItemBinding + + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { + val activity = requireActivity() + binding = DialogRenameChecklistItemBinding.inflate(activity.layoutInflater).also { + val restored = savedInstanceState?.getString(STATE_TEXT) + it.checklistItemTitle.setText( + restored ?: requireArguments().getString(ARG_OLD_TITLE).orEmpty() + ) + it.checklistItemTitle.maybeRequestIncognito() + } + + val builder = activity.getAlertDialogBuilder() + .setPositiveButton(org.fossify.commons.R.string.ok, null) + .setNegativeButton(org.fossify.commons.R.string.cancel, null) + + var dialog: AlertDialog? = null + activity.setupDialogStuff(binding.root, builder) { alert -> + alert.showKeyboard(binding.checklistItemTitle) + alert.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { + val newTitle = binding.checklistItemTitle.text?.toString().orEmpty() + if (newTitle.isEmpty()) { + activity.toast(org.fossify.commons.R.string.empty_name) + } else { + val taskId = requireArguments().getInt(ARG_TASK_ID) + parentFragmentManager + .setFragmentResult( + REQUEST_KEY, bundleOf(RESULT_TASK_ID to taskId, RESULT_TITLE to newTitle) + ) + alert.dismiss() + } + } + dialog = alert + } + + return dialog!! + } + + override fun onSaveInstanceState(outState: Bundle) { + val text = binding.checklistItemTitle.text?.toString().orEmpty() + outState.putString(STATE_TEXT, text) + super.onSaveInstanceState(outState) + } +} diff --git a/app/src/main/kotlin/org/fossify/notes/dialogs/NewChecklistItemDialogFragment.kt b/app/src/main/kotlin/org/fossify/notes/dialogs/NewChecklistItemDialogFragment.kt new file mode 100644 index 000000000..781d261f7 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/notes/dialogs/NewChecklistItemDialogFragment.kt @@ -0,0 +1,211 @@ +package org.fossify.notes.dialogs + +import android.content.DialogInterface +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import org.fossify.commons.extensions.beVisibleIf +import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.getContrastColor +import org.fossify.commons.extensions.getProperPrimaryColor +import org.fossify.commons.extensions.setupDialogStuff +import org.fossify.commons.extensions.showKeyboard +import org.fossify.commons.extensions.toast +import org.fossify.commons.helpers.SORT_BY_CUSTOM +import org.fossify.notes.R +import org.fossify.notes.databinding.DialogNewChecklistItemBinding +import org.fossify.notes.databinding.ItemAddChecklistBinding +import org.fossify.notes.extensions.config +import org.fossify.notes.extensions.maybeRequestIncognito + +class NewChecklistItemDialogFragment : DialogFragment() { + + private val activeInputFields = mutableListOf() + private var binding: DialogNewChecklistItemBinding? = null + + // Track the index of the currently focused row + private var lastFocusedIndex = -1 + + companion object { + const val TAG = "NewChecklistItemDialogFragment" + const val REQUEST_KEY = "new_checklist_item_request" + const val RESULT_TEXT = "result_text" + const val RESULT_ADD_TOP = "result_add_top" + + private const val ARG_NOTE_ID = "arg_note_id" + private const val STATE_TEXTS = "state_texts" + + private const val STATE_FOCUSED_INDEX = "state_focused_index" + + + fun show( + host: androidx.fragment.app.FragmentManager, + noteId: Long + ) = NewChecklistItemDialogFragment().apply { + arguments = bundleOf(ARG_NOTE_ID to noteId) + }.show(host, TAG) + } + + + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { + val activity = requireActivity() + binding = DialogNewChecklistItemBinding.inflate(activity.layoutInflater) + activeInputFields.clear() + + // Restore state or add initial row + if (savedInstanceState != null) { + val savedTexts = savedInstanceState.getStringArrayList(STATE_TEXTS) + if (!savedTexts.isNullOrEmpty()) { + savedTexts.forEach { text -> addNewRow(text) } + } else { + addNewRow("") + } + // Restore the focus index + lastFocusedIndex = savedInstanceState.getInt(STATE_FOCUSED_INDEX, -1) + } else { + addNewRow("") + } + + // Setup UI + val noteId = requireArguments().getLong(ARG_NOTE_ID) + val contrastColor = activity.getProperPrimaryColor().getContrastColor() + binding!!.addItem.setColorFilter(contrastColor) + + // Insert after the currently focused row + binding!!.addItem.setOnClickListener { + val insertIndex = if (lastFocusedIndex != -1 && lastFocusedIndex < activeInputFields.size) { + lastFocusedIndex + 1 + } else { + null // Append to end if nothing is focused + } + addNewRow("", focus = true, position = insertIndex) + } + + val config = activity.config + binding!!.settingsAddChecklistTop.beVisibleIf(config.getSorting(noteId) == SORT_BY_CUSTOM) + binding!!.settingsAddChecklistTop.isChecked = config.addNewChecklistItemsTop + + val builder = activity.getAlertDialogBuilder() + .setTitle(R.string.add_new_checklist_items) + .setPositiveButton(org.fossify.commons.R.string.ok, null) + .setNegativeButton(org.fossify.commons.R.string.cancel, null) + + var dialog: AlertDialog? = null + activity.setupDialogStuff(binding!!.root, builder) { alert -> + + // Apply Focus : if we have a valid restored index, use it + if (lastFocusedIndex != -1 && lastFocusedIndex < activeInputFields.size) { + alert.showKeyboard(activeInputFields[lastFocusedIndex]) + } else if (activeInputFields.isNotEmpty()) { + // Default to the last + alert.showKeyboard(activeInputFields.last()) + } + + alert.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { + // Collect all texts + val combinedText = activeInputFields + .map { it.text.toString().trim() } + .filter { it.isNotEmpty() } + .joinToString("\n") + + if (combinedText.isEmpty()) { + activity.toast(org.fossify.commons.R.string.empty_name) + } else { + config.addNewChecklistItemsTop = binding!!.settingsAddChecklistTop.isChecked + + // Return result + parentFragmentManager.setFragmentResult( + REQUEST_KEY, + bundleOf( + RESULT_TEXT to combinedText, + RESULT_ADD_TOP to binding!!.settingsAddChecklistTop.isChecked + ) + ) + alert.dismiss() + } + } + dialog = alert + } + + return dialog!! + } + + private fun addNewRow(text: String, focus: Boolean = false, position: Int? = null) { + val rowBinding = ItemAddChecklistBinding.inflate(layoutInflater) + + // Disable state saving for individual views to avoid rotation conflict + rowBinding.titleEditText.isSaveEnabled = false + rowBinding.titleEditText.setText(text) + rowBinding.titleEditText.maybeRequestIncognito() + + if (text.isNotEmpty()) { + rowBinding.titleEditText.setSelection(text.length) + } + + // Track focus changes in real time + rowBinding.titleEditText.setOnFocusChangeListener { view, hasFocus -> + if (hasFocus) { + // When this view gets focus, remember its index + lastFocusedIndex = activeInputFields.indexOf(view) + } + } + + // Add "Enter" key listener to create new rows automatically + rowBinding.titleEditText.setOnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_NEXT || + actionId == EditorInfo.IME_ACTION_DONE || + event?.keyCode == KeyEvent.KEYCODE_ENTER) { + + val currentIndex = activeInputFields.indexOf(v) + addNewRow("", focus = true, position = currentIndex + 1) + true + } else { + false + } + } + + val inputField = rowBinding.titleEditText as AppCompatEditText + + // Insert into list and view hierarchy at correct position + if (position != null && position < activeInputFields.size) { + activeInputFields.add(position, inputField) + binding?.checklistHolder?.addView(rowBinding.root, position) + } else { + activeInputFields.add(inputField) + binding?.checklistHolder?.addView(rowBinding.root) + } + + + if (focus) { + binding?.dialogHolder?.post { + // Only scroll to bottom if appending to the end + if (position == null) { + binding?.dialogHolder?.fullScroll(View.FOCUS_DOWN) + } + + inputField.requestFocus() + requireActivity().showKeyboard(inputField) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + val currentTexts = ArrayList(activeInputFields.map { it.text.toString() }) + outState.putStringArrayList(STATE_TEXTS, currentTexts) + + // Save the index tracked via the listener + outState.putInt(STATE_FOCUSED_INDEX, lastFocusedIndex) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + activeInputFields.clear() + } +} diff --git a/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt b/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt index 62c04c0f7..58d5bb9a8 100644 --- a/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt +++ b/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt @@ -15,6 +15,8 @@ import org.fossify.notes.activities.SimpleActivity import org.fossify.notes.adapters.TasksAdapter import org.fossify.notes.databinding.FragmentChecklistBinding import org.fossify.notes.dialogs.ChecklistItemDialogFragment +import org.fossify.notes.dialogs.EditTaskDialogFragment +import org.fossify.notes.dialogs.NewChecklistItemDialogFragment import org.fossify.notes.extensions.config import org.fossify.notes.extensions.updateWidgets import org.fossify.notes.helpers.NOTE_ID @@ -35,6 +37,9 @@ class TasksFragment : NoteFragment(), TasksActionListener { var tasks = mutableListOf() + // Variable to track the callback function + private var editTaskCallback: (() -> Unit)? = null + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentChecklistBinding.inflate(inflater, container, false) noteId = requireArguments().getLong(NOTE_ID, 0L) @@ -45,19 +50,27 @@ class TasksFragment : NoteFragment(), TasksActionListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Listen for results from the ChecklistItemDialogFragment - childFragmentManager.setFragmentResultListener(ChecklistItemDialogFragment.REQUEST_KEY, + // Listen for results from the NewChecklistItemDialogFragment + childFragmentManager.setFragmentResultListener(NewChecklistItemDialogFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle -> - val text = bundle.getString(ChecklistItemDialogFragment.RESULT_TEXT_KEY) ?: return@setFragmentResultListener - val taskId = bundle.getInt(ChecklistItemDialogFragment.RESULT_TASK_ID_KEY, -1) - - if (taskId == -1) { - // ID is -1, so we are adding a NEW item - addNewChecklistItems(text) - } else { - // ID exists, so we are EDITING an existing item - updateExistingTask(taskId, text) + val text = bundle.getString(NewChecklistItemDialogFragment.RESULT_TEXT) ?: return@setFragmentResultListener + addNewChecklistItems(text) + } + + // Listen for EditTaskDialogFragment + childFragmentManager.setFragmentResultListener(EditTaskDialogFragment.REQUEST_KEY, + viewLifecycleOwner) + { _, bundle -> + val taskId = bundle.getInt(EditTaskDialogFragment.RESULT_TASK_ID) + val newTitle = bundle.getString(EditTaskDialogFragment.RESULT_TITLE) + + if (newTitle != null) { + updateExistingTask(taskId, newTitle) + + // Invoke the callback + editTaskCallback?.invoke() + editTaskCallback = null } } } @@ -157,9 +170,7 @@ class TasksFragment : NoteFragment(), TasksActionListener { } } private fun showNewItemDialog() { - // Pass -1 to indicate a NEW item - ChecklistItemDialogFragment.newInstance(taskId = -1, text = "") - .show(childFragmentManager, ChecklistItemDialogFragment.DIALOG_TAG) + NewChecklistItemDialogFragment.show(childFragmentManager, noteId) } private fun addNewChecklistItems(text: String) { @@ -291,8 +302,9 @@ class TasksFragment : NoteFragment(), TasksActionListener { fun getTasks() = Gson().toJson(tasks) override fun editTask(task: Task, callback: () -> Unit) { - ChecklistItemDialogFragment.newInstance(taskId = task.id, text = task.title) - .show(childFragmentManager, ChecklistItemDialogFragment.DIALOG_TAG) + // Save the callback to be used when the result arrives + this.editTaskCallback = callback + EditTaskDialogFragment.show(childFragmentManager, task) } private fun updateExistingTask(taskId: Int, newTitle: String) {