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 eec86fba85..5fb539be39 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java @@ -883,7 +883,12 @@ private boolean isDisplayedAtMainActivity() { } public void updateViewModeText() { - _format.getConverter().convertMarkupShowInWebView(_document, getTextString(), getActivity(), _webView, _nextConvertToPrintMode, _hlEditor.isLineNumbersEnabled()); + // Don't let text to view mode crash app + try { + _format.getConverter().convertMarkupShowInWebView(_document, getTextString(), getActivity(), _webView, _nextConvertToPrintMode, _hlEditor.isLineNumbersEnabled()); + } catch (OutOfMemoryError e) { + _format.getConverter().convertMarkupShowInWebView(_document, "updateViewModeText getTextString(): OutOfMemory " + e, getActivity(), _webView, _nextConvertToPrintMode, _hlEditor.isLineNumbersEnabled()); + } } public void setViewModeVisibility(final boolean show) { 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 73c1a3e480..1e8f9469a1 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java @@ -17,6 +17,7 @@ import android.os.Build; import android.os.Bundle; import android.text.TextUtils; +import android.util.Log; import android.util.Pair; import android.util.Patterns; import android.util.TypedValue; @@ -433,7 +434,9 @@ public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) { @Override public void onFsViewerSelected(final String request, final File sel, final Integer lineNumber) { - if (sel.isDirectory()) { + if (sel == null) { + Log.e(getClass().getName(), "onFsViewerSelected: selected file is null"); + } else if (sel.isDirectory()) { NewFileDialog.newInstance(sel, false, f -> { if (f.isFile()) { appendToExistingDocumentAndClose(f, true); 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 28d8e6ed2e..f1d18d91a5 100644 --- a/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java +++ b/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java @@ -251,11 +251,6 @@ private List loadActionPreference(final String suffix) { public List getActionOrder() { final Set order = new LinkedHashSet<>(loadActionPreference(ORDER_SUFFIX)); - // Handle the case where order was stored without suffix. i.e. before this release. - if (order.isEmpty()) { - order.addAll(loadActionPreference("")); - } - final Set defined = new LinkedHashSet<>(getActiveActionKeys()); final Set disabled = new LinkedHashSet<>(getDisabledActions()); @@ -302,7 +297,7 @@ public void recreateActionButtons(final ViewGroup barLayout, final ActionItem.Di @SuppressLint("ClickableViewAccessibility") private void setupRepeat(final View btn) { // Velocity and acceleration parameters - final int INITIAL_DELAY = 400, DELTA_DELAY = 50, MIN_DELAY = 100; + final int INITIAL_DELAY = 300, DELTA_DELAY = 100, MIN_DELAY = 100; final Handler handler = new Handler(); final Runnable repeater = new Runnable() { diff --git a/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java b/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java index 2821db277d..f5da1cc7cb 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java @@ -162,6 +162,12 @@ private AlertDialog makeDialog(final File basedir, final boolean allowCreateDir, templateAdapter.addAll(GsCollectionUtils.map(templates, p -> p.first)); templateSpinner.setAdapter(templateAdapter); + templateSpinner.setOnItemSelectedListener(new GsAndroidSpinnerOnItemSelectedAdapter(pos -> { + final String template = templateAdapter.getItem(pos); + final String fmt = appSettings.getTemplateTitleFormat(template); + formatEdit.setText(fmt); + })); + // Setup type / format spinner and action // ----------------------------------------------------------------------------------------- final ArrayAdapter typeAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_dropdown_item); 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 2031acdf77..25a4b3c2eb 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 @@ -18,8 +18,11 @@ import android.text.Layout; import android.text.TextWatcher; import android.util.AttributeSet; +import android.view.ActionMode; import android.util.Log; import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; @@ -30,6 +33,7 @@ import androidx.appcompat.widget.AppCompatEditText; import net.gsantner.markor.ApplicationObject; +import net.gsantner.markor.R; import net.gsantner.markor.activity.MainActivity; import net.gsantner.markor.model.AppSettings; import net.gsantner.opoc.format.GsTextUtils; @@ -114,12 +118,19 @@ public void afterTextChanged(final Editable s) { // Fix for Android 12 perf issues - https://github.com/gsantner/markor/discussions/1794 setEmojiCompatEnabled(false); + + // Custom options + setupCustomOptions(); } @Override public boolean onPreDraw() { _lineNumbersDrawer.setTextSize(getTextSize()); - return super.onPreDraw(); + try { + return super.onPreDraw(); + } catch (OutOfMemoryError ignored) { + return false; // return false to cancel current drawing pass/round + } } @Override @@ -544,6 +555,11 @@ public void withAutoFormatDisabled(final GsCallback.a0 callback) { // Utility functions for interaction // --------------------------------------------------------------------------------------------- + public void selectLines() { + final int[] sel = TextViewUtils.getLineSelection(this); + setSelection(sel[0], sel[1]); + } + public void simulateKeyPress(int keyEvent_KEYCODE_SOMETHING) { dispatchKeyEvent(new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyEvent_KEYCODE_SOMETHING, 0)); dispatchKeyEvent(new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyEvent_KEYCODE_SOMETHING, 0)); @@ -764,4 +780,37 @@ public void reset() { _maxNumberDigits = 0; } } + + private void setupCustomOptions() { + setCustomSelectionActionModeCallback(new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // Add custom items programmatically + menu.add(0, R.string.option_select_lines, 0, "☰"); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // Modify menu items here if necessary + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.string.option_select_lines: + HighlightingEditor.this.selectLines(); + return true; + default: + return false; + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + // Cleanup if needed + } + }); + } } \ No newline at end of file diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/SyntaxHighlighterBase.java b/app/src/main/java/net/gsantner/markor/frontend/textview/SyntaxHighlighterBase.java index fdd5a36ba5..00756b5b21 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/textview/SyntaxHighlighterBase.java +++ b/app/src/main/java/net/gsantner/markor/frontend/textview/SyntaxHighlighterBase.java @@ -62,9 +62,9 @@ * Spans are further divided into two categories: dynamic and static. * - Dynamic spans are updated as one scrolls, as described above * - Static spans are applied once and never updated. These are typically used for - * spans which affect the text layout. - * - For example, a span which makes text bigger. - * - Updating these dynamically would make the text jump around as one scrolls + * spans which affect the text layout. + * - For example, a span which makes text bigger. + * - Updating these dynamically would make the text jump around as one scrolls *

* Fixup: * - As the user types we shift all spans to accomodate the changed text. @@ -75,19 +75,19 @@ * - Derived classes should override generateSpans() to generate all spans * - New spans are added by calling addSpanGroup() * - The HighlightingEditor will trigger the generation of spans when the text changes. - * - This is debounced so that changes are batched - * - Span generation is done on a background thread + * - This is debounced so that changes are batched + * - Span generation is done on a background thread *

* Other performance tips: * - Performance is heavily dependent on the number of spans applied to the text. * - Combine related spans into a single span if possible - * - HighlightSpan is a helper class which can be used to create a span with multiple attributes - * - For example, a span which makes text bold and italic + * - HighlightSpan is a helper class which can be used to create a span with multiple attributes + * - For example, a span which makes text bold and italic * - Absolutely minimize the number of spans implementing `UpdateLayout` - * - These spans trigger a text layout update when changed in any way - * - Instead consider using a span implementing `StaticSpan` - * - If StaticSpans are present, the text is reflowed after applying them - * - This happens once, and not for each span, which is much more efficient + * - These spans trigger a text layout update when changed in any way + * - Instead consider using a span implementing `StaticSpan` + * - If StaticSpans are present, the text is reflowed after applying them + * - This happens once, and not for each span, which is much more efficient */ public abstract class SyntaxHighlighterBase { @@ -230,7 +230,7 @@ public SyntaxHighlighterBase clearStatic() { boolean hasStatic = false; for (int i = _groups.size() - 1; i >= 0; i--) { final SpanGroup group = _groups.get(i); - if (group.isStatic) { + if (group != null && group.isStatic) { hasStatic = true; _spannable.removeSpan(group.span); } @@ -348,7 +348,7 @@ public SyntaxHighlighterBase applyDynamic(final int[] range) { for (int i = 0; i < _groups.size(); i++) { final SpanGroup group = _groups.get(i); - if (group.isStatic) { + if (group == null || group.isStatic) { continue; } @@ -368,7 +368,6 @@ public SyntaxHighlighterBase applyDynamic(final int[] range) { } - public SyntaxHighlighterBase applyStatic() { if (_spannable != null && !_staticApplied) { applyFixup(); diff --git a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserDialog.java b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserDialog.java index b0609cbd72..6ba6c1cd1e 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserDialog.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserDialog.java @@ -173,7 +173,6 @@ public void onViewCreated(final View root, final @Nullable Bundle savedInstanceS _filesystemViewerAdapter = new GsFileBrowserListAdapter(_dopt, activity); _recyclerList.setAdapter(_filesystemViewerAdapter); - _filesystemViewerAdapter.getFilter().filter(""); onFsViewerDoUiUpdate(_filesystemViewerAdapter); // Setup callbacks diff --git a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserFragment.java b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserFragment.java index 58ef93e0d7..f0bb0a4f8c 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserFragment.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserFragment.java @@ -124,7 +124,6 @@ public void onViewCreated(@NonNull View root, @Nullable Bundle savedInstanceStat _filesystemViewerAdapter = new GsFileBrowserListAdapter(_dopt, context); _recyclerList.setAdapter(_filesystemViewerAdapter); - _filesystemViewerAdapter.getFilter().filter(""); onFsViewerDoUiUpdate(_filesystemViewerAdapter); _swipe.setOnRefreshListener(() -> { diff --git a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java index 8cccb29124..1dee2e5ae8 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java @@ -56,6 +56,7 @@ import java.util.Set; import java.util.Stack; import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -88,7 +89,7 @@ public class GsFileBrowserListAdapter extends RecyclerView.Adapter _virtualMapping; @@ -138,6 +139,7 @@ public GsFileBrowserListAdapter(GsFileBrowserOptions.Options options, Context co _virtualMapping = Collections.unmodifiableMap(getVirtualFolders()); _reverseVirtualMapping = Collections.unmodifiableMap(GsCollectionUtils.reverse(_virtualMapping)); loadFolder(_dopt.startFolder != null ? _dopt.startFolder : _dopt.rootFolder, null); + _filter = new StringFilter(this); } public Map getVirtualFolders() { @@ -366,9 +368,6 @@ public int getItemCount() { @Override public Filter getFilter() { - if (_filter == null) { - _filter = new StringFilter(this, _adapterData); - } return _filter; } @@ -599,7 +598,7 @@ public void showFile(final File file) { return; } - if (getFilePosition(file) < 0) { + if (!_adapterDataFiltered.contains(file)) { final File dir = file.getParentFile(); if (dir != null) { loadFolder(dir, file); @@ -619,13 +618,19 @@ public void onLayoutChange(View v, int l, int t, int r, int b, int ol, int ot, i }); } + private void postScrollToAndFlash(final File file) { + if (_recyclerView != null && file != null) { + _recyclerView.post(() -> scrollToAndFlash(file)); + } + } + /** * Scroll to a file in current folder and flash * * @param file File to blink */ public boolean scrollToAndFlash(final File file) { - final int pos = getFilePosition(file); + final int pos = _adapterDataFiltered.indexOf(file); if (pos >= 0 && _layoutManager != null) { _layoutManager.scrollToPosition(pos); _recyclerView.post(() -> @@ -640,19 +645,6 @@ public boolean scrollToAndFlash(final File file) { return false; } - // Get the position of a file in the current view - // -1 if file is not a child of the current directory - public int getFilePosition(final File file) { - if (file != null) { - for (int i = 0; i < _adapterDataFiltered.size(); i++) { - if (_adapterDataFiltered.get(i).equals(file)) { - return i; - } - } - } - return -1; - } - private static final ExecutorService executorService = new ThreadPoolExecutor(0, 3, 60, TimeUnit.SECONDS, new SynchronousQueue<>()); private void loadFolder(final File folder, final File show) { @@ -687,11 +679,19 @@ private void loadFolder(final File folder, final File show) { final File toShow = show == null ? _fileToShowAfterNextLoad : show; _fileToShowAfterNextLoad = null; - executorService.execute(() -> _loadFolder(toLoad, toShow)); + try { + executorService.execute(() -> _loadFolder(toLoad, toShow)); + } catch (RejectedExecutionException ignored) { // during exit + } } // This function is not called on the main thread, so post to the UI thread private synchronized void _loadFolder(final @NonNull File folder, final @Nullable File toShow) { + + if (_recyclerView == null) { + return; + } + final boolean folderChanged = !folder.equals(_currentFolder); final List newData = new ArrayList<>(); @@ -713,7 +713,6 @@ private synchronized void _loadFolder(final @NonNull File folder, final @Nullabl newData.add(new File(folder, "0")); } - if (folder.equals(VIRTUAL_STORAGE_RECENTS)) { newData.addAll(_dopt.recentFiles); } else if (folder.equals(VIRTUAL_STORAGE_POPULAR)) { @@ -752,16 +751,17 @@ private synchronized void _loadFolder(final @NonNull File folder, final @Nullabl } } - if (_recyclerView == null) { - //noinspection UnnecessaryReturnStatement - return; - } else if (folderChanged || modSumChanged || !newData.equals(_adapterData)) { + if (folderChanged || modSumChanged || !newData.equals(_adapterData)) { + final ArrayList filteredData = new ArrayList<>(); + _filter._filter(newData, filteredData); + _recyclerView.post(() -> { // Modify all these values in the UI thread _adapterData.clear(); _adapterData.addAll(newData); + _adapterDataFiltered.clear(); + _adapterDataFiltered.addAll(filteredData); _currentSelection.retainAll(_adapterData); - _filter.filter(_filter._lastFilter); _currentFolder = folder; _prevModSum = modSum; @@ -778,18 +778,18 @@ private synchronized void _loadFolder(final @NonNull File folder, final @Nullabl _layoutManager.onRestoreInstanceState(_folderScrollMap.remove(_currentFolder)); } - _recyclerView.post(() -> scrollToAndFlash(toShow)); + postScrollToAndFlash(toShow); }); - } else if (toShow != null && _adapterDataFiltered.contains(toShow)) { - _recyclerView.post(() -> scrollToAndFlash(toShow)); + } else { + postScrollToAndFlash(toShow); } if (_dopt.listener != null) { _dopt.listener.onFsViewerDoUiUpdate(GsFileBrowserListAdapter.this); } }); - } else if (toShow != null && _adapterDataFiltered.contains(toShow)) { - _recyclerView.post(() -> scrollToAndFlash(toShow)); + } else { + postScrollToAndFlash(toShow); } } @@ -831,39 +831,40 @@ public boolean isCurrentFolderHome() { //######################## private static class StringFilter extends Filter { private final GsFileBrowserListAdapter _adapter; - private final List _originalList; private final List _filteredList; - public CharSequence _lastFilter = ""; + public String _lastFilter = ""; - private StringFilter(GsFileBrowserListAdapter adapter, List adapterData) { + private StringFilter(final GsFileBrowserListAdapter adapter) { super(); _adapter = adapter; - _originalList = adapterData; _filteredList = new ArrayList<>(); } @Override protected FilterResults performFiltering(CharSequence constraint) { final FilterResults results = new FilterResults(); - constraint = constraint.toString().toLowerCase(Locale.getDefault()).trim(); - _filteredList.clear(); - if (constraint.length() == 0) { - _filteredList.addAll(_originalList); - } else { - for (File file : _originalList) { - if (file.getName().toLowerCase(Locale.getDefault()).contains(constraint)) { - _filteredList.add(file); - } - } - } + _lastFilter = constraint.toString().toLowerCase().trim(); + _filter(_adapter._adapterData, _filteredList); - _lastFilter = constraint; results.values = _filteredList; results.count = _filteredList.size(); return results; } + public void _filter(final List all, final List filtered) { + filtered.clear(); + if (_lastFilter.isEmpty()) { + filtered.addAll(all); + } else { + for (final File file : all) { + if (file.getName().toLowerCase().contains(_lastFilter)) { + filtered.add(file); + } + } + } + } + @Override @SuppressWarnings("unchecked") protected void publishResults(CharSequence constraint, FilterResults results) { diff --git a/app/src/main/res/layout/new_file_dialog.xml b/app/src/main/res/layout/new_file_dialog.xml index a6cc48837a..bb9cbfa1ec 100644 --- a/app/src/main/res/layout/new_file_dialog.xml +++ b/app/src/main/res/layout/new_file_dialog.xml @@ -150,7 +150,7 @@ android:text="@string/template" android:textAppearance="@style/TextAppearance.AppCompat.Caption" /> - . Square Brackets CSV OrgMode + option_select_lines