@@ -19,19 +19,31 @@ package com.ichi2.anki
1919import android.content.Context
2020import android.content.Intent
2121import android.os.Bundle
22+ import androidx.core.view.isVisible
2223import androidx.fragment.app.Fragment
24+ import androidx.fragment.app.FragmentContainerView
2325import androidx.fragment.app.commit
2426import com.ichi2.anki.NoteEditorActivity.Companion.FRAGMENT_ARGS_EXTRA
2527import com.ichi2.anki.NoteEditorActivity.Companion.FRAGMENT_NAME_EXTRA
2628import com.ichi2.anki.android.input.ShortcutGroup
2729import com.ichi2.anki.android.input.ShortcutGroupProvider
30+ import com.ichi2.anki.databinding.NoteEditorBinding
2831import com.ichi2.anki.libanki.Collection
32+ import com.ichi2.anki.noteeditor.NoteEditorFragmentDelegate
2933import com.ichi2.anki.noteeditor.NoteEditorLauncher
34+ import com.ichi2.anki.previewer.TemplatePreviewerArguments
35+ import com.ichi2.anki.previewer.TemplatePreviewerFragment
36+ import com.ichi2.anki.settings.Prefs
3037import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
3138import com.ichi2.anki.snackbar.SnackbarBuilder
39+ import com.ichi2.anki.ui.ResizablePaneManager
40+ import com.ichi2.themes.Themes
41+ import kotlinx.coroutines.Job
42+ import kotlinx.coroutines.delay
3243import timber.log.Timber
3344import kotlin.reflect.KClass
3445import kotlin.reflect.jvm.jvmName
46+ import kotlin.time.Duration.Companion.milliseconds
3547
3648/* *
3749 * To find the actual note Editor, @see [NoteEditorFragment]
@@ -44,14 +56,37 @@ class NoteEditorActivity :
4456 AnkiActivity (),
4557 BaseSnackbarBuilderProvider ,
4658 DispatchKeyEventListener ,
47- ShortcutGroupProvider {
59+ ShortcutGroupProvider ,
60+ NoteEditorFragmentDelegate {
4861 override val baseSnackbarBuilder: SnackbarBuilder = { }
4962
5063 lateinit var noteEditorFragment: NoteEditorFragment
5164
5265 private val mainToolbar: androidx.appcompat.widget.Toolbar
5366 get() = findViewById(R .id.toolbar)
5467
68+ /* *
69+ * Reference to the previewer container that exists only on larger screens.
70+ * Non-null if and only if the layout is x-large and includes the previewer frame
71+ *
72+ * Unlike lateinit variables, this will remain null throughout the activity
73+ * lifecycle on smaller screens that don't include the previewer frame.
74+ *
75+ * Fragmentation is determined by this view's visibility after inflation.
76+ */
77+ private var previewerFrame: FragmentContainerView ? = null
78+
79+ /* *
80+ * Job for managing delayed previewer refresh operations.
81+ * Automatically cancelled when the lifecycle scope is destroyed, preventing memory leaks.
82+ */
83+ private var refreshPreviewerJob: Job ? = null
84+
85+ val fragmented: Boolean
86+ get() = previewerFrame?.isVisible == true
87+
88+ private lateinit var binding: NoteEditorBinding
89+
5590 override fun onCreate (savedInstanceState : Bundle ? ) {
5691 if (showedActivityFailedScreen(savedInstanceState)) {
5792 return
@@ -61,7 +96,11 @@ class NoteEditorActivity :
6196 return
6297 }
6398
64- setContentView(R .layout.note_editor)
99+ binding = NoteEditorBinding .inflate(layoutInflater)
100+ setContentView(binding.root)
101+
102+ previewerFrame = binding.previewerFrame
103+ Timber .i(" Note Editor is in %s mode" , if (fragmented) " split" else " single-pane" )
65104
66105 val launcher = NoteIntentParser .parse(intent)
67106
@@ -78,10 +117,12 @@ class NoteEditorActivity :
78117 */
79118 runOnCommit {
80119 noteEditorFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG ) as NoteEditorFragment
120+ noteEditorFragment.setDelegate(this @NoteEditorActivity)
81121 }
82122 }
83123 } else {
84124 noteEditorFragment = existingFragment as NoteEditorFragment
125+ noteEditorFragment.setDelegate(this )
85126 }
86127
87128 enableToolbar()
@@ -93,9 +134,153 @@ class NoteEditorActivity :
93134 onBackPressedDispatcher.onBackPressed()
94135 }
95136
137+ if (fragmented) {
138+ // Defer previewer loading to avoid blocking onCreate
139+ binding.previewerFrame!! .post {
140+ loadNoteEditorPreviewer(true )
141+ }
142+ val parentLayout = binding.noteEditorXlView!!
143+ val divider = binding.noteEditorResizingDivider!!
144+ val noteEditorPane = binding.noteEditorFragmentFrame
145+ val previewerPane = binding.previewerFrameLayout!!
146+ ResizablePaneManager (
147+ parentLayout = parentLayout,
148+ divider = divider,
149+ leftPane = noteEditorPane,
150+ rightPane = previewerPane,
151+ sharedPrefs = Prefs .getUiConfig(this ),
152+ leftPaneWeightKey = PREF_NOTE_EDITOR_PANE_WEIGHT ,
153+ rightPaneWeightKey = PREF_PREVIEWER_PANE_WEIGHT ,
154+ )
155+ }
156+
96157 startLoadingCollection()
97158 }
98159
160+ /* *
161+ * Loads and configures the note editor previewer.
162+ *
163+ * This method orchestrates the entire preview process including:
164+ * - Processing the current note fields and tags
165+ * - Setting up the previewer fragment with the appropriate configuration
166+ * - Configuring the tab layout for card template navigation
167+ *
168+ * The preview will reflect the current state of the note being edited,
169+ * allowing users to see how their cards will appear during review.
170+ *
171+ * BUG: Fragment replacement loses user state
172+ * - Scroll position is reset when user has scrolled through long card content
173+ *
174+ * Ideally, we should:
175+ * Preserve scroll position when possible
176+ *
177+ * State preservation behavior:
178+ * - Regular templates: Content-only updates preserve tab selection and front/back state
179+ * - Cloze notes: Fragment recreation with preserved tab selection to handle dynamic cloze changes
180+ */
181+ fun loadNoteEditorPreviewer (forceReplace : Boolean ) {
182+ if (! fragmented) {
183+ return
184+ }
185+
186+ // Check if noteEditorFragment is initialized before proceeding
187+ if (! ::noteEditorFragment.isInitialized) {
188+ Timber .w(" loadNoteEditorPreviewer called before noteEditorFragment was initialized" )
189+ return
190+ }
191+
192+ // Check if editorNote is available before proceeding
193+ val note =
194+ noteEditorFragment.editorNote ? : run {
195+ Timber .w(" loadNoteEditorPreviewer called before editorNote was available" )
196+ return
197+ }
198+
199+ launchCatchingTask {
200+ try {
201+ val fields = noteEditorFragment.prepareNoteFields()
202+ val tags = noteEditorFragment.selectedTags ? : mutableListOf ()
203+
204+ fun updatePreviewerFragment (ord : Int ) {
205+ val previewerFragment = createPreviewerFragment(fields, tags, ord)
206+ supportFragmentManager.commit {
207+ replace(R .id.previewer_frame, previewerFragment)
208+ runOnCommit {
209+ configurePreviewerTabs(previewerFragment)
210+ }
211+ }
212+ }
213+
214+ val existingPreviewer = supportFragmentManager.findFragmentById(R .id.previewer_frame)
215+
216+ when {
217+ forceReplace -> {
218+ updatePreviewerFragment(ord = noteEditorFragment.determineCardOrdinal(fields))
219+ }
220+ existingPreviewer is TemplatePreviewerFragment -> {
221+ if (note.notetype.isCloze) {
222+ // For cloze notes, force recreation to handle dynamic cloze changes
223+ // but preserve the user's selected tab
224+ updatePreviewerFragment(ord = existingPreviewer.getSafeClozeOrd())
225+ } else {
226+ // For regular templates, just update content
227+ existingPreviewer.updateContent(fields, tags)
228+ }
229+ }
230+ }
231+ } catch (e: Exception ) {
232+ Timber .w(e, " Failed to load note editor previewer" )
233+ }
234+ }
235+ }
236+
237+ /* *
238+ * Creates and configures the template previewer fragment.
239+ *
240+ * @param fields The processed note fields
241+ * @param tags The selected tags for the note
242+ * @param ord The ordinal (position) of the card template to display
243+ * @return The configured previewer fragment
244+ */
245+ private fun createPreviewerFragment (
246+ fields : List <String >,
247+ tags : List <String >,
248+ ord : Int ,
249+ ): TemplatePreviewerFragment {
250+ val args =
251+ TemplatePreviewerArguments (
252+ notetypeFile =
253+ NotetypeFile (
254+ this @NoteEditorActivity,
255+ noteEditorFragment.editorNote!! .notetype,
256+ ),
257+ fields = fields,
258+ tags = tags,
259+ id = noteEditorFragment.editorNote!! .id,
260+ ord = ord,
261+ fillEmpty = false ,
262+ )
263+
264+ val backgroundColor = Themes .getColorFromAttr(this @NoteEditorActivity, R .attr.alternativeBackgroundColor)
265+ val previewerFragment = TemplatePreviewerFragment .newInstance(args, backgroundColor)
266+ return previewerFragment
267+ }
268+
269+ /* *
270+ * Configures the tab layout for the previewer with appropriate tabs for each card template.
271+ * Delegates the tab setup responsibility to the TemplatePreviewerFragment
272+ *
273+ * @param previewerFragment The previewer fragment that will manage the tabs
274+ */
275+ private fun configurePreviewerTabs (previewerFragment : TemplatePreviewerFragment ) {
276+ // Post to ensure the fragment is attached before accessing its viewModel
277+ binding.previewerFrame?.post {
278+ if (! previewerFragment.isAdded) return @post
279+ val previewerTabLayout = binding.previewerTabLayout!!
280+ previewerFragment.setupTabs(previewerTabLayout)
281+ }
282+ }
283+
99284 override fun onCollectionLoaded (col : Collection ) {
100285 super .onCollectionLoaded(col)
101286 Timber .d(" onCollectionLoaded()" )
@@ -108,11 +293,58 @@ class NoteEditorActivity :
108293 override val shortcuts: ShortcutGroup
109294 get() = noteEditorFragment.shortcuts
110295
296+ override fun onResume () {
297+ super .onResume()
298+ // Refresh the previewer when activity resumes, if needed
299+ if (fragmented) {
300+ loadNoteEditorPreviewer(false )
301+ }
302+ }
303+
304+ // region NoteEditorFragmentDelegate Protocol Methods
305+
306+ override fun onNoteEditorReady () {
307+ // Load the if fragmented, else does nothing
308+ if (! fragmented) return
309+
310+ loadNoteEditorPreviewer(false )
311+ }
312+
313+ override fun onNoteTextChanged () {
314+ if (! fragmented) return
315+
316+ refreshPreviewerJob?.cancel()
317+ refreshPreviewerJob =
318+ launchCatchingTask {
319+ delay(REFRESH_NOTE_EDITOR_PREVIEW_DELAY )
320+ loadNoteEditorPreviewer(false )
321+ }
322+ }
323+
324+ override fun onNoteSaved () {
325+ if (! fragmented) return
326+
327+ loadNoteEditorPreviewer(true )
328+ }
329+
330+ override fun onNoteTypeChanged () {
331+ if (! fragmented) return
332+
333+ loadNoteEditorPreviewer(true )
334+ }
335+ // endregion
336+
111337 companion object {
112338 const val FRAGMENT_ARGS_EXTRA = " fragmentArgs"
113339 const val FRAGMENT_NAME_EXTRA = " fragmentName"
114340 const val FRAGMENT_TAG = " NoteEditorFragmentTag"
115341
342+ // Keys for saving pane weights in SharedPreferences
343+ private const val PREF_NOTE_EDITOR_PANE_WEIGHT = " noteEditorPaneWeight"
344+ private const val PREF_PREVIEWER_PANE_WEIGHT = " previewerPaneWeight"
345+
346+ private val REFRESH_NOTE_EDITOR_PREVIEW_DELAY = 100 .milliseconds
347+
116348 /* *
117349 * Creates an Intent to launch the NoteEditor activity with a specific fragment class and arguments.
118350 *
0 commit comments