diff --git a/app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml b/app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml index ea9854579f..a85f4f9a2a 100644 --- a/app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml +++ b/app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml @@ -2,16 +2,16 @@ xmlns:tools="http://schemas.android.com/tools" tools:targetApi="n" android:width="108dp" - android:height="108dp" - android:viewportWidth="800.7087" - android:viewportHeight="800.7087"> + android:height="108dp" + android:viewportWidth="800.7087" + android:viewportHeight="800.7087"> + android:strokeAlpha="1" /> + android:strokeAlpha="1" /> + android:strokeAlpha="1" /> + android:strokeAlpha="1" /> + android:strokeWidth="0.49996799" /> + android:strokeWidth="1.06330121" /> + android:strokeWidth="1.07910931" /> + android:strokeWidth="1.0553019" /> + android:strokeWidth="0.96706837" /> + android:strokeWidth="0.94961631" /> + android:strokeWidth="0.9483121" /> + android:strokeAlpha="1" /> diff --git a/app/src/flavorGplay/AndroidManifest.xml b/app/src/flavorGplay/AndroidManifest.xml index c9a29c587d..95eb936659 100644 --- a/app/src/flavorGplay/AndroidManifest.xml +++ b/app/src/flavorGplay/AndroidManifest.xml @@ -14,6 +14,8 @@ tools:ignore="ObsoleteSdkInt" tools:targetApi="jelly_bean"> - + diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java b/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java index 449e829a2b..03d49265b6 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java @@ -35,7 +35,6 @@ import android.webkit.WebView; import android.widget.HorizontalScrollView; import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; @@ -73,6 +72,7 @@ import net.gsantner.opoc.util.GsContextUtils; import net.gsantner.opoc.util.GsCoolExperimentalStuff; import net.gsantner.opoc.web.GsWebViewChromeClient; +import net.gsantner.opoc.wrapper.GsCallback; import net.gsantner.opoc.wrapper.GsTextWatcherAdapter; import java.io.File; @@ -110,6 +110,7 @@ public static DocumentEditAndViewFragment newInstance(final @NonNull Document do private DraggableScrollbarScrollView _verticalScrollView; private HorizontalScrollView _horizontalScrollView; private LineNumbersView _lineNumbersView; + private TextView _searchResultTextView; private Document _document; private FormatRegistry _format; private MarkorContextUtils _cu; @@ -151,6 +152,10 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { _lineNumbersView = view.findViewById(R.id.document__fragment__edit__line_numbers_view); _cu = new MarkorContextUtils(activity); _editTextUndoRedoHelper = new TextViewUndoRedo(); + _editorHolder.setOnClickListener(v -> { + _hlEditor.requestFocus(); + _cu.showSoftKeyboard(activity, true, _hlEditor); + }); // Using `if (_document != null)` everywhere is dangerous // It may cause reads or writes to _silently fail_ @@ -188,7 +193,9 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { _hlEditor.setLineSpacing(0, _appSettings.getEditorLineSpacing()); _hlEditor.setTextSize(TypedValue.COMPLEX_UNIT_SP, _appSettings.getDocumentFontSize(_document.path)); _hlEditor.setTypeface(GsFontPreferenceCompat.typeface(getContext(), _appSettings.getFontFamily(), Typeface.NORMAL)); - _hlEditor.setBackgroundColor(_appSettings.getEditorBackgroundColor()); + final int editorBackgroundColor = _appSettings.getEditorBackgroundColor(); + _hlEditor.setBackgroundColor(editorBackgroundColor); + _editorHolder.setBackgroundColor(editorBackgroundColor); _hlEditor.setTextColor(_appSettings.getEditorForegroundColor()); _hlEditor.setGravity(_appSettings.isEditorStartEditingInCenter() ? Gravity.CENTER : Gravity.NO_GRAVITY); _hlEditor.setHighlightingEnabled(_appSettings.getDocumentHighlightState(_document.path, _hlEditor.getText())); @@ -238,41 +245,21 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { }); } - // Keep this as a one-shot min-height sync. Do not use a persistent layout listener here, - // otherwise large documents trigger repeated relayout work during startup. - syncEditorMinHeightOnce(_verticalScrollView); } @Override protected void onFragmentFirstTimeVisible() { - // Restore cursor - final int lastSelection = _appSettings.getLastEditPosition(_document.path, _hlEditor.length()); - _hlEditor.setSelection(lastSelection); - - // Restore scroll position for edit-mode - _hlEditor.setReflowCallback(() -> { - // Must be called after HighlightingEditor reflow to prevent scroll position being reset - int lastEditHeight = _appSettings.getLastEditHeight(_document.path, 0); - int lastEditScrollY = _appSettings.getLastEditScrollY(_document.path, 0); - if (lastEditScrollY > 0 && lastEditHeight == _verticalScrollView.getHeight()) { - // Set scroll position by scroll Y if last scroll Y is valid - // This way is precise, not as imprecise as using line number - _hlEditor.postDelayed(() -> { - _verticalScrollView.scrollTo(0, lastEditScrollY); - _hlEditor.requestFocus(); - }, 600); - } else { - // Set scroll position by line number if last scroll Y is invalid - final Bundle args = getArguments(); - if (args != null && args.containsKey(Document.EXTRA_FILE_LINE_NUMBER)) { - final int lineNumber = args.getInt(Document.EXTRA_FILE_LINE_NUMBER); - int selection = lineNumber >= 0 ? TextViewUtils.getIndexFromLineOffset(_hlEditor.getText(), lineNumber, 0) : _hlEditor.length(); - TextViewUtils.setSelectionAndShow(_hlEditor, selection); - } else { - _hlEditor.requestFocus(); - } - } - }); + final Bundle args = getArguments(); + final boolean hasLineNumber = args != null && args.containsKey(Document.EXTRA_FILE_LINE_NUMBER); + int startPos = _appSettings.getLastEditPosition(_document.path, _hlEditor.length()); + if (hasLineNumber) { + final int lineNumber = args.getInt(Document.EXTRA_FILE_LINE_NUMBER); + startPos = lineNumber >= 0 + ? TextViewUtils.getIndexFromLineOffset(_hlEditor.getText(), lineNumber, 0) + : _hlEditor.length(); + } else { + _hlEditor.setSelection(startPos); + } // Restore scroll position for view-mode if (_webView != null) { @@ -284,13 +271,41 @@ protected void onFragmentFirstTimeVisible() { } _hlEditor.recomputeHighlighting(); - - // One-shot floor for first render after content/highlighting setup. - // Do not replace with per-layout updates; they regress big-file open performance. - syncEditorMinHeightOnce(_editorHolder); + if (hasLineNumber) { + TextViewUtils.setSelectionAndShow(_hlEditor, startPos); + } else { + final int lastEditHeight = _appSettings.getLastEditHeight(_document.path, 0); + final int lastEditScrollY = _appSettings.getLastEditScrollY(_document.path, 0); + final int fallbackPos = startPos; + _verticalScrollView.post(() -> { + if (lastEditHeight > 0 && lastEditHeight == _verticalScrollView.getHeight()) { + _verticalScrollView.scrollTo(0, lastEditScrollY); + } else { + TextViewUtils.setSelectionAndShow(_hlEditor, fallbackPos); + } + }); + } // Fade in to hide initial jank _hlEditor.post(() -> _hlEditor.animate().alpha(1).setDuration(250).start()); + setupHighlightingScrollRestore(); + } + + private void setupHighlightingScrollRestore() { + _hlEditor.setScrollCallbacks( + () -> new int[]{ + _horizontalScrollView != null ? _horizontalScrollView.getScrollX() : 0, + _verticalScrollView != null ? _verticalScrollView.getScrollY() : 0 + }, + (x, y) -> { + if (_horizontalScrollView != null) { + _horizontalScrollView.scrollTo(x, 0); + } + if (_verticalScrollView != null) { + _verticalScrollView.scrollTo(0, y); + } + } + ); } @Override @@ -778,20 +793,10 @@ private void setActionBarVisibility() { return; } - final boolean visible = _format.getActions().loadActionBarVisible() && _textActionsBar.getChildCount() > 0; - if (visible && parent.getVisibility() == View.VISIBLE) { - return; - } - final View bar = view.findViewById(R.id.document__fragment__edit__text_actions_bar); if (bar != null && _verticalScrollView != null) { + final boolean visible = _format.getActions().loadActionBarVisible() && _textActionsBar.getChildCount() > 0; parent.setVisibility(visible ? View.VISIBLE : View.GONE); - final int marginBottom = visible ? (int) getResources().getDimension(R.dimen.text_actions_bar_height) : 0; - setMarginBottom(_verticalScrollView, marginBottom); - final View viewScroll = view.findViewById(R.id.document__fragment_view_webview); - if (viewScroll != null) { - setMarginBottom(viewScroll, marginBottom); - } } } @@ -804,10 +809,6 @@ private void setupSearchView(SearchView searchView) { if (searchView == null) { return; } - // Only setup SearchView for view-mode, to avoid unnecessary setup for edit-mode - if (!_isPreviewVisible || _webView == null) { - return; - } searchView.setQueryHint(getString(R.string.search)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @@ -841,22 +842,25 @@ public boolean onQueryTextChange(String text) { return search(text); } }); - searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(@NonNull View v) { - } - - @Override - public void onViewDetachedFromWindow(@NonNull View v) { - // Clear search when SearchView is closed abnormally, e.g. switch from QuickNote to To-Do when SearchView is opened - if (searchView.getQuery().length() > 0) { - searchView.setQuery("", false); // This will make onQueryTextChange be called back + if (searchView.getTag(R.id.action_search_view) == null) { + searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View v) { } - if (!searchView.isIconified()) { - searchView.setIconified(true); + + @Override + public void onViewDetachedFromWindow(@NonNull View v) { + // Clear search when SearchView is closed abnormally, e.g. switch from QuickNote to To-Do when SearchView is opened + if (searchView.getQuery().length() > 0) { + searchView.setQuery("", false); // This will make onQueryTextChange be called back + } + if (!searchView.isIconified()) { + searchView.setIconified(true); + } } - } - }); + }); + searchView.setTag(R.id.action_search_view, Boolean.TRUE); + } // Because SearchView doesn't provide a public API to add custom buttons // We must get the searchPlate (the layout containing the text field and close button) from SearchView @@ -868,46 +872,68 @@ public void onViewDetachedFromWindow(@NonNull View v) { return; } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); - layoutParams.gravity = Gravity.CENTER; + final GsCallback.r0 makeLayoutParams = () -> { + final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + params.gravity = Gravity.CENTER; + return params; + }; Context searchViewContext = searchView.getContext(); - LinearLayout linearLayout = new LinearLayout(searchViewContext); - linearLayout.setLayoutParams(layoutParams); - - // Add search result TextView - TextView resultTextView = new TextView(searchViewContext); - LinearLayout.LayoutParams textViewLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT); - textViewLayoutParams.setMarginEnd(30); - resultTextView.setLayoutParams(textViewLayoutParams); - resultTextView.setGravity(Gravity.CENTER); - resultTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); - linearLayout.addView(resultTextView); - - // Add previous match Button - ImageView previousButton = new ImageView(searchViewContext); - previousButton.setImageResource(R.drawable.ic_baseline_keyboard_arrow_up_24); - previousButton.setLayoutParams(layoutParams); - previousButton.setPadding(24, 24, 24, 24); - TextViewUtils.setSelectableItemBackgroundBorderless(previousButton, searchViewContext); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - previousButton.setTooltipText(getString(R.string.previous_match)); - } - linearLayout.addView(previousButton); + LinearLayout linearLayout = searchPlate.findViewWithTag("markor_search_nav_controls"); + TextView resultTextView; + ImageButton previousButton; + ImageButton nextButton; + if (linearLayout == null) { + linearLayout = new LinearLayout(searchViewContext); + linearLayout.setTag("markor_search_nav_controls"); + linearLayout.setLayoutParams(makeLayoutParams.callback()); + + // Add search result TextView + resultTextView = new TextView(searchViewContext); + resultTextView.setTag("markor_search_nav_result"); + LinearLayout.LayoutParams textViewLayoutParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.MATCH_PARENT + ); + textViewLayoutParams.setMarginEnd(30); + resultTextView.setLayoutParams(textViewLayoutParams); + resultTextView.setGravity(Gravity.CENTER); + resultTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + linearLayout.addView(resultTextView); + + // Add previous match Button + previousButton = new ImageButton(searchViewContext); + previousButton.setImageResource(R.drawable.ic_baseline_keyboard_arrow_up_24); + previousButton.setLayoutParams(makeLayoutParams.callback()); + previousButton.setPadding(24, 24, 24, 24); + TextViewUtils.setSelectableItemBackgroundBorderless(previousButton, searchViewContext); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + previousButton.setTooltipText(getString(R.string.previous_match)); + } + linearLayout.addView(previousButton); + + // Add next match Button + nextButton = new ImageButton(searchViewContext); + nextButton.setImageResource(R.drawable.ic_baseline_keyboard_arrow_down_24); + nextButton.setLayoutParams(makeLayoutParams.callback()); + nextButton.setPadding(24, 24, 24, 24); + TextViewUtils.setSelectableItemBackgroundBorderless(nextButton, searchViewContext); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + nextButton.setTooltipText(getString(R.string.next_match)); + } + linearLayout.addView(nextButton); - // Add next match Button - ImageButton nextButton = new ImageButton(searchViewContext); - nextButton.setImageResource(R.drawable.ic_baseline_keyboard_arrow_down_24); - nextButton.setLayoutParams(layoutParams); - nextButton.setPadding(24, 24, 24, 24); - TextViewUtils.setSelectableItemBackgroundBorderless(nextButton, searchViewContext); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - nextButton.setTooltipText(getString(R.string.next_match)); + // Apply to SearchView + searchPlate.addView(linearLayout, 1); + } else { + resultTextView = linearLayout.findViewWithTag("markor_search_nav_result"); + previousButton = (ImageButton) linearLayout.getChildAt(1); + nextButton = (ImageButton) linearLayout.getChildAt(2); } - linearLayout.addView(nextButton); - - // Apply to SearchView - searchPlate.addView(linearLayout, 1); + _searchResultTextView = resultTextView; // Set listeners previousButton.setOnClickListener(v -> { @@ -920,13 +946,20 @@ public void onViewDetachedFromWindow(@NonNull View v) { _webView.findNext(true); } }); + bindWebViewSearchListener(); + } + + private void bindWebViewSearchListener() { + if (_webView == null || _searchResultTextView == null) { + return; + } _webView.setFindListener((activeMatchOrdinal, numberOfMatches, isDoneCounting) -> { if (isDoneCounting) { String searchResult = ""; if (numberOfMatches > 0) { searchResult = (activeMatchOrdinal + 1) + "/" + numberOfMatches; } - resultTextView.setText(searchResult); + _searchResultTextView.setText(searchResult); } }); } @@ -958,26 +991,6 @@ public boolean isSearchViewIconified() { } } - private void setMarginBottom(final View view, final int marginBottom) { - final ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); - if (params != null) { - params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, marginBottom); - view.setLayoutParams(params); - } - } - - private void syncEditorMinHeightOnce(final View parent) { - if (parent == null) { - return; - } - parent.post(() -> { - final int parentHeight = parent.getHeight(); - if (parentHeight > 0 && parentHeight != _hlEditor.getMinHeight()) { - _hlEditor.setMinHeight(parentHeight); - } - }); - } - private void updateMenuToggleStates(final int selectedFormatActionId) { MenuItem mi; if ((mi = _fragmentMenu.findItem(R.id.action_wrap_words)) != null) { @@ -1013,7 +1026,7 @@ private ViewGroup.LayoutParams makeLinearLayoutChildParams() { } private ViewGroup.LayoutParams makeScrollViewChildParams() { - return new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + return new ScrollView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } private void setWrapState(final boolean wrap) { @@ -1045,7 +1058,6 @@ private void setWrapState(final boolean wrap) { } _hlEditor.requestLayout(); - syncEditorMinHeightOnce(_editorHolder); _hlEditor.setHighlightingEnabled(hlEnabled); _hlEditor.post(() -> { @@ -1201,6 +1213,10 @@ private void setupWebViewIfNeeded(final Activity activity) { return false; }); } + if (_format != null) { + _format.getActions().setUiReferences(activity, _hlEditor, _webView); + } + bindWebViewSearchListener(); } @SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"}) @@ -1261,7 +1277,7 @@ protected boolean onToolbarLongClicked(View v) { } @Override - public void onDestroy() { + public void onDestroyView() { if (_webView != null) { try { _webView.loadUrl("about:blank"); @@ -1269,7 +1285,16 @@ public void onDestroy() { } catch (Exception ignored) { } } - super.onDestroy(); + _webView = null; + _webViewClient = null; + _searchResultTextView = null; + if (_hlEditor != null) { + _hlEditor.setScrollCallbacks(null, null); + } + if (_format != null) { + _format.getActions().setUiReferences(getActivity(), _hlEditor, null); + } + super.onDestroyView(); } public Document getDocument() { diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java b/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java index d39cb0865c..8c7df200f8 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java @@ -321,7 +321,7 @@ private void attachOrCopyAndClose(final File dest, final boolean show) { _appSettings.addRecentFile(dest); // Only if not forced link due to attachment - if (attachment == null) { + if (attachment == null && _linkCheckBox != null && _linkCheckBox.getVisibility() == View.VISIBLE) { _appSettings.setFormatShareAsLink(asLink); } diff --git a/app/src/main/java/net/gsantner/markor/activity/MainActivity.java b/app/src/main/java/net/gsantner/markor/activity/MainActivity.java index 9b74acf1e1..1df6964b26 100644 --- a/app/src/main/java/net/gsantner/markor/activity/MainActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/MainActivity.java @@ -476,6 +476,8 @@ public void onViewPagerPageSelected(final int pos) { } else { _fab.hide(); } + + setTitle(getPosTitle(pos)); } private GsFileBrowserOptions.Options _filesystemDialogOptions = null; diff --git a/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java b/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java index 7b0deda3e7..a73b4d750e 100644 --- a/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java +++ b/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java @@ -898,7 +898,11 @@ protected final boolean runCommonAction(final @StringRes int action) { return true; } case R.string.abid_common_web_jump_to_table_of_contents: { - runTitleClick(); + if (_appSettings.isMarkdownTableOfContentsEnabled() && _webView != null) { + _webView.loadUrl("javascript:document.getElementsByClassName('toc')[0].scrollIntoView();"); + } else { + runTitleClick(); + } return true; } case R.string.abid_common_view_file_in_other_app: { diff --git a/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java b/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java index 9c86f61248..ee4ad5eef9 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java +++ b/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java @@ -17,7 +17,6 @@ import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.os.Build; import android.text.Editable; @@ -33,6 +32,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.view.WindowManager; import android.view.animation.LinearInterpolator; import android.webkit.WebView; @@ -46,6 +46,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import net.gsantner.markor.R; @@ -470,12 +471,23 @@ public static DialogOptions makeSttLineSelectionDialog( return dopt; } + @Nullable + private static Editable getCurrentSearchText(final AlertDialog dialog) { + final Window window = dialog.getWindow(); + if (window == null) { + return null; + } + final View view = window.getDecorView().findViewWithTag("EDIT"); + return view instanceof EditText ? ((EditText) view).getText() : null; + } + // Search dialog for todo.txt public static void showSttSearchDialog(final Activity activity, final EditText text) { final DialogOptions dopt = makeSttLineSelectionDialog(activity, text, t -> true); dopt.titleText = R.string.search_documents; dopt.neutralButtonText = R.string.replace; - dopt.neutralButtonCallback2 = (dialog, searchText) -> { + dopt.neutralButtonCallback = dialog -> { + final Editable searchText = getCurrentSearchText(dialog); dialog.dismiss(); SearchAndReplaceTextDialog.showSearchReplaceDialog(activity, text.getText(), searchText, TextViewUtils.getSelection(text)); }; @@ -762,10 +774,11 @@ public static void showSearchDialog(final Activity activity, final EditText edit dopt.dataFilter = "[^\\s]+"; // Line must have one or more non-whitespace to display dopt.titleText = R.string.search_documents; dopt.searchHintText = R.string.search; - dopt.searchText = searchText; + dopt.state.searchText = searchText; dopt.neutralButtonCallback = null; - dopt.neutralButtonCallback2 = (dialog, searchText2) -> { + dopt.neutralButtonCallback = dialog -> { dialog.dismiss(); + final Editable searchText2 = getCurrentSearchText(dialog); SearchAndReplaceTextDialog.showSearchReplaceDialog(activity, edit, searchText2, TextViewUtils.getSelection(editText)); }; dopt.neutralButtonText = R.string.replace; diff --git a/app/src/main/java/net/gsantner/markor/frontend/SearchAndReplaceTextDialog.java b/app/src/main/java/net/gsantner/markor/frontend/SearchAndReplaceTextDialog.java index c81e0c37a0..58f39915c7 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/SearchAndReplaceTextDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/SearchAndReplaceTextDialog.java @@ -75,7 +75,7 @@ public class SearchAndReplaceTextDialog { private final List recentReplaces; - public static void showSearchReplaceDialog(final Activity activity, final Editable edit, Editable searchText, final int[] sel) { + public static void showSearchReplaceDialog(final Activity activity, final Editable edit, @Nullable CharSequence searchText, final int[] sel) { new SearchAndReplaceTextDialog(activity, edit, searchText, sel); } @@ -83,7 +83,7 @@ public static void showSearchReplaceDialog(final Activity activity, final Editab showSearchReplaceDialog(activity, edit, null, sel); } - private SearchAndReplaceTextDialog(final Activity activity, final Editable edit, Editable searchText, final int[] sel) { + private SearchAndReplaceTextDialog(final Activity activity, final Editable edit, @Nullable CharSequence searchText, final int[] sel) { _activity = activity; _edit = edit; @@ -404,4 +404,4 @@ public JSONObject toJson() { return obj; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java b/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java index 611e4f2ca7..ad24dfadc9 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java +++ b/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java @@ -79,6 +79,9 @@ public class HighlightingEditor extends AppCompatEditText { private int _textChangedNumber; private final Runnable _textChangedRecorder = TextViewUtils.makeDebounced(getHandler(), 1000, () -> _textChangedNumber++); private StaticCursorDrawer _staticCursorDrawer; + private GsCallback.r0 _getScrollCallback; + private GsCallback.a2 _applyScrollCallback; + private int[] _savedScrollPosition; public HighlightingEditor(Context context, AttributeSet attrs) { super(context, attrs); @@ -138,17 +141,27 @@ private void batch(final Runnable runnable) { beginBatchEdit(); runnable.run(); } finally { - endBatchEdit(); // This triggers a reflow which will bring focus back to the cursor and reset scroll position - if (reflowCallback != null) { - reflowCallback.callback(); - } + endBatchEdit(); // This can trigger reflow which will bring focus back to the cursor and reset scroll position } } - private GsCallback.a0 reflowCallback; + public void setScrollCallbacks(final GsCallback.r0 get, final GsCallback.a2 apply) { + _getScrollCallback = get; + _applyScrollCallback = apply; + } + + private void saveScrollPositionForLayout() { + if (_savedScrollPosition == null && _getScrollCallback != null) { + _savedScrollPosition = _getScrollCallback.callback(); + } + } - public void setReflowCallback(GsCallback.a0 callback) { - this.reflowCallback = callback; + private void applySavedScrollPosition() { + final int[] position = _savedScrollPosition; + _savedScrollPosition = null; + if (position != null && position.length >= 2 && _applyScrollCallback != null) { + _applyScrollCallback.callback(position[0], position[1]); + } } private boolean isScrollSignificant() { @@ -177,6 +190,7 @@ private void updateHighlighting() { public void recomputeHighlighting() { if (_hlEnabled && runHighlight(true)) { + this.saveScrollPositionForLayout(); batch(() -> _hl .clearDynamic() .clearStatic(false) @@ -185,6 +199,7 @@ public void recomputeHighlighting() { .applyStatic() .applyDynamic(hlRegion()) ); + this.applySavedScrollPosition(); } } diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java b/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java index 641252e4b2..4f257517b1 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java +++ b/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java @@ -425,9 +425,9 @@ public static void showSelection(final EditText editText, final int startSelecti int line = layout.getLineForOffset(startSelection); int lineHeight = editText.getLineHeight(); if (layout.getLineTop(line) < visible.top - lineHeight) { - showSelection(editText, visible, startSelection, startSelection, -lineHeight); + showSelection(editText, visible, startSelection, startSelection, -lineHeight * 3); } else if (layout.getLineBottom(line) > visible.bottom - lineHeight) { - showSelection(editText, visible, startSelection, startSelection, lineHeight * 4); + showSelection(editText, visible, startSelection, startSelection, lineHeight * 2); } } diff --git a/app/src/main/java/net/gsantner/opoc/frontend/GsSearchOrCustomTextDialog.java b/app/src/main/java/net/gsantner/opoc/frontend/GsSearchOrCustomTextDialog.java index 1406753327..8a39cd047f 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/GsSearchOrCustomTextDialog.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/GsSearchOrCustomTextDialog.java @@ -21,7 +21,6 @@ import android.graphics.drawable.RippleDrawable; import android.os.Build; import android.os.Parcelable; -import android.text.Editable; import android.text.InputFilter; import android.text.InputType; import android.text.Spannable; @@ -55,6 +54,7 @@ import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.TooltipCompat; import androidx.core.graphics.ColorUtils; +import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; import androidx.core.widget.TextViewCompat; @@ -133,7 +133,6 @@ public enum SelectionMode { public String dataFilter = null; // Regex pattern to filter data public GsCallback.a1 highlighter = null; public GsCallback.a1 neutralButtonCallback = null; - public GsCallback.a2 neutralButtonCallback2 = null; public GsCallback.a1 dismissCallback = null; public @Nullable InputFilter searchInputFilter = null; @@ -156,8 +155,6 @@ public enum SelectionMode { @StyleRes public int dialogStyle = 0; - public String searchText; - /** * Initial state of the dialog. Will be updated when the dialog is dismissed. */ @@ -388,22 +385,15 @@ public static void showMultiChoiceDialogWithSearchFilterUI(final Activity activi } }; - searchEditText.addTextChangedListener(GsTextWatcherAdapter.after(new GsCallback.a1() { - private Runnable searchTask; - private CharSequence searchText = new SpannableString(""); - - @Override - public void callback(Editable constraint) { - if (searchTask == null) { - searchTask = TextViewUtils.makeDebounced(searchEditText.getHandler(), 400, () -> { - listAdapter.filter(searchText); - setSelectAllButtonState.callback(); - }); - } - searchText = constraint; - searchTask.run(); - } - })); + // Filter when the user stops typing if the list is long + final Runnable _filterList = () -> { + listAdapter.filter(searchEditText.getText()); + setSelectAllButtonState.callback(); + }; + final Runnable _changeListener = dopt.data == null || dopt.data.size() < 1000 ? + _filterList : + TextViewUtils.makeDebounced(searchEditText.getHandler(), 400, _filterList); + searchEditText.addTextChangedListener(GsTextWatcherAdapter.after(e -> _changeListener.run())); // Ok button only present under these circumstances final boolean isSearchOk = dopt.callback != null && dopt.isSearchEnabled; @@ -437,10 +427,24 @@ public void callback(Editable constraint) { return false; }); + dialog.show(); + final Window win = dialog.getWindow(); if (win != null) { WindowCompat.setDecorFitsSystemWindows(win, true); + win.setLayout( + dopt.dialogWidthDp < 0 ? dopt.dialogWidthDp : GsContextUtils.instance.convertDpToPx(activity, dopt.dialogWidthDp), + dopt.dialogHeightDp < 0 ? dopt.dialogHeightDp : GsContextUtils.instance.convertDpToPx(activity, dopt.dialogHeightDp) + ); + + final View decorView = win.getDecorView(); + ViewCompat.setOnApplyWindowInsetsListener(decorView, (view, insets) -> { + decorView.requestLayout(); + listView.post(listView::requestLayout); + return insets; + }); + if (dopt.isSearchEnabled) { if (dopt.isSoftInputVisible) { win.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); @@ -452,15 +456,6 @@ public void callback(Editable constraint) { } } - dialog.show(); - - if (win != null) { - win.setLayout( - dopt.dialogWidthDp < 0 ? dopt.dialogWidthDp : GsContextUtils.instance.convertDpToPx(activity, dopt.dialogWidthDp), - dopt.dialogHeightDp < 0 ? dopt.dialogHeightDp : GsContextUtils.instance.convertDpToPx(activity, dopt.dialogHeightDp) - ); - } - final Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); if (neutralButton != null && dopt.neutralButtonText != 0) { neutralButton.setVisibility(Button.VISIBLE); @@ -468,9 +463,6 @@ public void callback(Editable constraint) { if (dopt.neutralButtonCallback != null) { neutralButton.setOnClickListener((button) -> dopt.neutralButtonCallback.callback(dialog)); - } else if (dopt.neutralButtonCallback2 != null) { - // Open search & replace dialog with search text of current search dialog, no need to input it again - neutralButton.setOnClickListener((button) -> dopt.neutralButtonCallback2.callback(dialog, searchEditText.getText())); } } @@ -645,7 +637,6 @@ public static View makeSearchView(final Context context, final DialogOptions dop searchEditText.setTextColor(dopt.textColor); searchEditText.setHintTextColor(ColorUtils.setAlphaComponent(dopt.textColor, 0x99)); searchEditText.setHint(dopt.searchHintText); - searchEditText.setText(dopt.searchText); searchEditText.setInputType(dopt.searchInputType == 0 ? searchEditText.getInputType() : dopt.searchInputType); searchEditText.setTag("EDIT"); // So we can easily find the search edit text @@ -657,7 +648,7 @@ public static View makeSearchView(final Context context, final DialogOptions dop final ImageView clearButton = new ImageView(context); clearButton.setImageResource(dopt.clearInputIcon); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - RippleDrawable rippleDrawable = new RippleDrawable(AppCompatResources.getColorStateList(context, R.color.accent), null, null); + final RippleDrawable rippleDrawable = new RippleDrawable(AppCompatResources.getColorStateList(context, R.color.accent), null, null); rippleDrawable.setRadius(60); clearButton.setBackground(rippleDrawable); } diff --git a/app/src/main/java/net/gsantner/opoc/util/AlphanumComparator.java b/app/src/main/java/net/gsantner/opoc/util/AlphanumComparator.java index a8ff47f5e0..71f3991a57 100644 --- a/app/src/main/java/net/gsantner/opoc/util/AlphanumComparator.java +++ b/app/src/main/java/net/gsantner/opoc/util/AlphanumComparator.java @@ -19,7 +19,7 @@ private static boolean isDigit(char ch) { * Find the end of a chunk starting at the given index. A chunk is either a sequence of digits * or a sequence of non-digits. * - * @param s The string to scan + * @param s The string to scan * @param index The index to start scanning from * @return The index of the first character not belonging to the chunk */ @@ -37,9 +37,9 @@ private static int getChunkEnd(String s, int index) { * Count the number of leading zeros in a numeric chunk, leaving at least one significant digit. * For example, "001" has 2 leading zeros, and '0' is the single remaining. * - * @param s The string containing the chunk + * @param s The string containing the chunk * @param start The start index of the numeric chunk - * @param end The end index of the numeric chunk + * @param end The end index of the numeric chunk * @return The number of leading zeros that can be ignored for numeric comparison */ private static int countLeadingZeros(String s, int start, int end) { @@ -56,12 +56,12 @@ private static int countLeadingZeros(String s, int start, int end) { * Compares two regions of strings for order, optionally ignoring case. Mimics String.compareTo * and String.compareToIgnoreCase without allocations. * - * @param s1 First string - * @param start1 Start index in s1 - * @param end1 End index in s1 - * @param s2 Second string - * @param start2 Start index in s2 - * @param end2 End index in s2 + * @param s1 First string + * @param start1 Start index in s1 + * @param end1 End index in s1 + * @param s2 Second string + * @param start2 Start index in s2 + * @param end2 End index in s2 * @param ignoreCase Whether to perform case-insensitive comparison * @return Negative if s1 region < s2 region, positive if >, zero if equal */ diff --git a/app/src/main/java/net/gsantner/opoc/util/GsContextUtils.java b/app/src/main/java/net/gsantner/opoc/util/GsContextUtils.java index 1b2f1581fc..6f071636eb 100644 --- a/app/src/main/java/net/gsantner/opoc/util/GsContextUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/GsContextUtils.java @@ -2719,6 +2719,21 @@ public T setActivityNavigationBarBackgroundColor(fina final Window window = context.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.setNavigationBarColor(color); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + final View decorView = window.getDecorView(); + final boolean useDarkNavIcons = !shouldColorOnTopBeLight(color); + final WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(window, decorView); + controller.setAppearanceLightNavigationBars(useDarkNavIcons); + + // Keep the legacy flag in sync for OEMs that still depend on decor view UI flags. + int systemUiVisibility = decorView.getSystemUiVisibility(); + if (useDarkNavIcons) { + systemUiVisibility |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } else { + systemUiVisibility &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } + decorView.setSystemUiVisibility(systemUiVisibility); + } } } catch (Exception ignored) { } diff --git a/app/src/main/res/layout/document__fragment__edit.xml b/app/src/main/res/layout/document__fragment__edit.xml index 540935228a..301bceb3dc 100644 --- a/app/src/main/res/layout/document__fragment__edit.xml +++ b/app/src/main/res/layout/document__fragment__edit.xml @@ -19,7 +19,6 @@ @@ -29,74 +28,81 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="2.5dp" /> - + android:layout_height="0dp" + android:layout_weight="1"> - + android:layout_height="match_parent" + android:background="@android:color/transparent" + android:fillViewport="true"> - - - - - - + android:clickable="true" + android:orientation="horizontal"> - + - + + + + + + + - + android:elevation="4dp" + android:paddingTop="4dp" + android:scrollbars="none" + android:translationZ="4dp"> - + + + - \ No newline at end of file +