Skip to content

Commit 442acc4

Browse files
committed
Move NoteEditor into Activity and Add Previewer to NoteEditor
- Introduce NoteEditor Activity which launches NoteEditorFragment into a FragmentContainerView - Fold the intent in NoteEditorLauncher to use NoteEditor (Activity) instead of a single fragment activity - Add previewer into a FragmentContainerView of NoteEditor with a 1 second refresh delay - Introduce a tab layout to switch between cards generated - Refresh Previewer when text changed, notetype changed, and note saved - Use ResizablePaneManager to make the layout resizable
1 parent e1f7e41 commit 442acc4

File tree

7 files changed

+590
-172
lines changed

7 files changed

+590
-172
lines changed

AnkiDroid/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,12 @@
452452
android:exported="false"
453453
android:configChanges="orientation|screenSize"
454454
/>
455+
<activity
456+
android:name="com.ichi2.anki.NoteEditor"
457+
android:exported="false"
458+
android:configChanges="keyboardHidden|orientation|screenSize"
459+
android:windowSoftInputMode="stateAlwaysHidden|adjustResize"
460+
/>
455461
<activity
456462
android:name="com.ichi2.anki.previewer.CardViewerActivity"
457463
android:exported="false"
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/*
2+
* Copyright (c) 2025 Hari Srinivasan <harisrini21@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki
18+
19+
import android.os.Bundle
20+
import android.os.Handler
21+
import android.os.Looper
22+
import android.text.Editable
23+
import android.text.TextWatcher
24+
import android.view.Menu
25+
import android.view.MenuItem
26+
import android.view.View
27+
import android.widget.LinearLayout
28+
import androidx.core.view.MenuProvider
29+
import androidx.fragment.app.FragmentContainerView
30+
import androidx.fragment.app.commit
31+
import androidx.lifecycle.lifecycleScope
32+
import com.google.android.material.tabs.TabLayout
33+
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
34+
import com.ichi2.anki.CollectionManager.withCol
35+
import com.ichi2.anki.NoteEditorFragment.Companion.shouldReplaceNewlines
36+
import com.ichi2.anki.android.input.ShortcutGroup
37+
import com.ichi2.anki.android.input.ShortcutGroupProvider
38+
import com.ichi2.anki.dialogs.DeckSelectionDialog
39+
import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener
40+
import com.ichi2.anki.dialogs.tags.TagsDialogListener
41+
import com.ichi2.anki.libanki.Collection
42+
import com.ichi2.anki.libanki.Note
43+
import com.ichi2.anki.model.CardStateFilter
44+
import com.ichi2.anki.noteeditor.NoteEditorLauncher
45+
import com.ichi2.anki.previewer.TemplatePreviewerArguments
46+
import com.ichi2.anki.previewer.TemplatePreviewerFragment
47+
import com.ichi2.anki.servicelayer.NoteService
48+
import com.ichi2.anki.settings.Prefs
49+
import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
50+
import com.ichi2.anki.snackbar.SnackbarBuilder
51+
import com.ichi2.anki.ui.ResizablePaneManager
52+
import com.ichi2.anki.utils.postDelayed
53+
import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener
54+
import com.ichi2.themes.Themes
55+
import kotlinx.coroutines.launch
56+
import timber.log.Timber
57+
import kotlin.time.Duration.Companion.seconds
58+
59+
class NoteEditor :
60+
AnkiActivity(),
61+
DeckSelectionListener,
62+
SubtitleListener,
63+
TagsDialogListener,
64+
BaseSnackbarBuilderProvider,
65+
DispatchKeyEventListener,
66+
MenuProvider,
67+
ShortcutGroupProvider {
68+
/**
69+
* The frame containing the Previewer. Non null only in layout x-large.
70+
*/
71+
private var previewerFrame: FragmentContainerView? = null
72+
private val refreshPreviewerFragmentHandler = Handler(Looper.getMainLooper())
73+
var fragmented: Boolean = false
74+
75+
override val baseSnackbarBuilder: SnackbarBuilder = { }
76+
77+
override fun onCreate(savedInstanceState: Bundle?) {
78+
if (showedActivityFailedScreen(savedInstanceState)) {
79+
return
80+
}
81+
super.onCreate(savedInstanceState)
82+
if (!ensureStoragePermissions()) {
83+
return
84+
}
85+
86+
setContentView(R.layout.note_editor)
87+
88+
previewerFrame = findViewById(R.id.previewer_frame)
89+
90+
/**
91+
* Check if previewerFrame is not null and if its visibility is set to VISIBLE.
92+
* If both conditions are true, assign true to the variable [fragmented], otherwise assign false.
93+
* [fragmented] will be true if the view size is large otherwise false
94+
*/
95+
fragmented = previewerFrame?.visibility == View.VISIBLE
96+
Timber.i("Using split Note Editor: %b", fragmented)
97+
98+
if (fragmented) {
99+
loadNoteEditorPreviewer()
100+
val parentLayout = findViewById<LinearLayout>(R.id.note_editor_xl_view)
101+
val divider = findViewById<View>(R.id.note_editor_resizing_divider)
102+
val leftPane = findViewById<View>(R.id.note_editor_frame)
103+
val rightPane = findViewById<View>(R.id.previewer_frame)
104+
if (parentLayout != null && divider != null && leftPane != null && rightPane != null) {
105+
ResizablePaneManager(
106+
parentLayout = parentLayout,
107+
divider = divider,
108+
leftPane = leftPane,
109+
rightPane = rightPane,
110+
sharedPrefs = Prefs.getUiConfig(this),
111+
leftPaneWeightKey = PREF_NOTE_EDITOR_PANE_WEIGHT,
112+
rightPaneWeightKey = PREF_PREVIEWER_PANE_WEIGHT,
113+
)
114+
}
115+
}
116+
117+
// Create and launch the NoteEditorFragment based on the launcher intent
118+
val launcher =
119+
intent.extras?.let { bundle ->
120+
// Convert intent extras to NoteEditorLauncher
121+
NoteEditorLauncher.PassArguments(bundle)
122+
} ?: NoteEditorLauncher.AddNote()
123+
124+
supportFragmentManager.commit {
125+
replace(R.id.note_editor_frame, NoteEditorFragment.newInstance(launcher))
126+
}
127+
128+
startLoadingCollection()
129+
}
130+
131+
/**
132+
* Loads or reloads editorNote in [previewerFrame] if the view is fragmented. Do nothing otherwise.
133+
*/
134+
fun loadNoteEditorPreviewer() {
135+
if (!fragmented) {
136+
return
137+
}
138+
launchCatchingTask {
139+
val noteEditorFragment = noteEditorFragment ?: return@launchCatchingTask
140+
141+
val convertNewlines = shouldReplaceNewlines()
142+
143+
fun String?.toFieldText(): String = NoteService.convertToHtmlNewline(this.toString(), convertNewlines)
144+
val fields = noteEditorFragment.editFields?.mapTo(mutableListOf()) { it.fieldText.toFieldText() } ?: mutableListOf()
145+
val tags = noteEditorFragment.selectedTags ?: mutableListOf()
146+
147+
val ord =
148+
if (noteEditorFragment.editorNote!!.notetype.isCloze) {
149+
val tempNote = withCol { Note.fromNotetypeId(this@withCol, noteEditorFragment.editorNote!!.notetype.id) }
150+
tempNote.fields = fields // makes possible to get the cloze numbers from the fields
151+
val clozeNumbers = withCol { clozeNumbersInNote(tempNote) }
152+
if (clozeNumbers.isNotEmpty()) {
153+
clozeNumbers.first() - 1
154+
} else {
155+
0
156+
}
157+
} else {
158+
noteEditorFragment.currentEditedCard?.ord ?: 0
159+
}
160+
161+
val args =
162+
TemplatePreviewerArguments(
163+
notetypeFile = NotetypeFile(this@NoteEditor, noteEditorFragment.editorNote!!.notetype),
164+
fields = fields,
165+
tags = tags,
166+
id = noteEditorFragment.editorNote!!.id,
167+
ord = ord,
168+
fillEmpty = true,
169+
)
170+
171+
val backgroundColor = Themes.getColorFromAttr(this@NoteEditor, R.attr.alternativeBackgroundColor)
172+
val previewerFragment = TemplatePreviewerFragment.newInstance(args, backgroundColor)
173+
174+
// Use commit and post to ensure proper fragment transaction ordering
175+
supportFragmentManager.commit {
176+
replace(R.id.previewer_frame, previewerFragment)
177+
}
178+
179+
// Post to ensure the fragment is attached before accessing its viewModel
180+
findViewById<View>(R.id.previewer_frame).post {
181+
val previewerTabLayout = findViewById<TabLayout>(R.id.previewer_tab_layout)
182+
previewerTabLayout.removeAllTabs()
183+
previewerTabLayout.setBackgroundColor(backgroundColor)
184+
185+
// Now that the previewerFragment is attached, we can safely access its viewModel
186+
val previewerViewModel = previewerFragment.viewModel
187+
188+
lifecycleScope.launch {
189+
val cardsWithEmptyFronts = previewerViewModel.cardsWithEmptyFronts?.await()
190+
for ((index, templateName) in previewerViewModel.getTemplateNames().withIndex()) {
191+
val tabTitle =
192+
if (cardsWithEmptyFronts?.get(index) == true) {
193+
getString(R.string.card_previewer_empty_front_indicator, templateName)
194+
} else {
195+
templateName
196+
}
197+
val newTab = previewerTabLayout.newTab().setText(tabTitle)
198+
previewerTabLayout.addTab(newTab)
199+
}
200+
previewerTabLayout.selectTab(previewerTabLayout.getTabAt(previewerViewModel.getCurrentTabIndex()))
201+
previewerTabLayout.addOnTabSelectedListener(
202+
object : OnTabSelectedListener {
203+
override fun onTabSelected(tab: TabLayout.Tab) {
204+
Timber.v("Selected tab %d", tab.position)
205+
previewerViewModel.onTabSelected(tab.position)
206+
}
207+
208+
override fun onTabUnselected(tab: TabLayout.Tab) {
209+
// do nothing
210+
}
211+
212+
override fun onTabReselected(tab: TabLayout.Tab) {
213+
// do nothing
214+
}
215+
},
216+
)
217+
}
218+
}
219+
}
220+
}
221+
222+
val noteEditorWatcher: TextWatcher =
223+
object : TextWatcher {
224+
/**
225+
* Declare a nullable variable refreshPreviewerFragmentRunnable of type Runnable.
226+
* This will hold a reference to the Runnable that refreshes the previewer noteEditorFragment.
227+
* It is used to manage delayed noteEditorFragment updates and can be null if no updates in card.
228+
*/
229+
private var refreshPreviewerFragmentRunnable: Runnable? = null
230+
231+
override fun afterTextChanged(arg0: Editable) {
232+
refreshPreviewerFragmentRunnable?.let { refreshPreviewerFragmentHandler.removeCallbacks(it) }
233+
val updateRunnable =
234+
Runnable {
235+
loadNoteEditorPreviewer()
236+
}
237+
refreshPreviewerFragmentRunnable = updateRunnable
238+
refreshPreviewerFragmentHandler.postDelayed(updateRunnable, REFRESH_NOTE_EDITOR_PREVIEW_DELAY)
239+
}
240+
241+
override fun beforeTextChanged(
242+
arg0: CharSequence,
243+
arg1: Int,
244+
arg2: Int,
245+
arg3: Int,
246+
) {
247+
// do nothing
248+
}
249+
250+
override fun onTextChanged(
251+
arg0: CharSequence,
252+
arg1: Int,
253+
arg2: Int,
254+
arg3: Int,
255+
) {
256+
// do nothing
257+
}
258+
}
259+
260+
/**
261+
* Retrieves the [NoteEditorFragment]
262+
*/
263+
val noteEditorFragment: NoteEditorFragment?
264+
get() = supportFragmentManager.findFragmentById(R.id.note_editor_frame) as? NoteEditorFragment
265+
266+
override fun onCollectionLoaded(col: Collection) {
267+
super.onCollectionLoaded(col)
268+
Timber.d("onCollectionLoaded()")
269+
registerReceiver()
270+
}
271+
272+
override fun onDeckSelected(deck: DeckSelectionDialog.SelectableDeck?) {
273+
noteEditorFragment?.onDeckSelected(deck)
274+
}
275+
276+
override val subtitleText: String
277+
get() = noteEditorFragment?.subtitleText ?: ""
278+
279+
override fun onSelectedTags(
280+
selectedTags: List<String>,
281+
indeterminateTags: List<String>,
282+
stateFilter: CardStateFilter,
283+
) {
284+
noteEditorFragment?.onSelectedTags(selectedTags, indeterminateTags, stateFilter)
285+
}
286+
287+
override fun dispatchKeyEvent(event: android.view.KeyEvent): Boolean =
288+
noteEditorFragment?.dispatchKeyEvent(event) ?: false || super.dispatchKeyEvent(event)
289+
290+
override fun onCreateMenu(
291+
menu: Menu,
292+
menuInflater: android.view.MenuInflater,
293+
) {
294+
noteEditorFragment?.onCreateMenu(menu, menuInflater)
295+
}
296+
297+
override fun onMenuItemSelected(item: MenuItem): Boolean = noteEditorFragment?.onMenuItemSelected(item) ?: false
298+
299+
override val shortcuts: ShortcutGroup
300+
get() = noteEditorFragment?.shortcuts ?: ShortcutGroup(emptyList(), 0)
301+
302+
companion object {
303+
// Keys for saving pane weights in SharedPreferences
304+
private const val PREF_NOTE_EDITOR_PANE_WEIGHT = "noteEditorPaneWeight"
305+
private const val PREF_PREVIEWER_PANE_WEIGHT = "previewerPaneWeight"
306+
307+
private val REFRESH_NOTE_EDITOR_PREVIEW_DELAY = 1.seconds
308+
}
309+
}

0 commit comments

Comments
 (0)