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 380c475f85..0e0030adf7 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java @@ -703,7 +703,6 @@ private boolean isWrapped() { } private void setHorizontalScrollMode(final boolean wrap) { - final Context context = getContext(); if (context != null && _hlEditor != null && isWrapped() != wrap) { @@ -864,7 +863,7 @@ public void onAnimationEnd(Animator animation) { @Override protected void onToolbarClicked(View v) { - if (!_isPreviewVisible && _format != null) { + if (_format != null) { _format.getActions().runTitleClick(); } } @@ -892,7 +891,7 @@ public Document getDocument() { return _document; } - public WebView getWebview() { + public WebView getWebView() { return _webView; } 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 92489173ee..953104507d 100644 --- a/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java +++ b/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java @@ -149,8 +149,7 @@ public Map getActiveActionMap() { final List actionList = getActionList(); final List keyList = getActiveActionKeys(); - final Map map = new HashMap(); - + final Map map = new HashMap<>(); for (int i = 0; i < actionList.size(); i++) { map.put(keyList.get(i), actionList.get(i)); } @@ -251,7 +250,6 @@ private List loadActionPreference(final String suffix) { * @return List of Action Item keys in order specified by preferences */ 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. @@ -532,7 +530,6 @@ private static void runRegexReplaceAction(final Editable editable, final List 0) || (!isUp && linesEnd < text.length())) { - final CharSequence lines = text.subSequence(linesStart, linesEnd); final int altStart = isUp ? TextViewUtils.getLineStart(text, linesStart - 1) : linesEnd + 1; @@ -893,7 +892,6 @@ private String rstr(@StringRes int resKey) { } public void runSpecialKeyAction() { - // Needed to prevent selection from being overwritten on refocus final int[] sel = TextViewUtils.getSelection(_hlEditor); _hlEditor.clearFocus(); diff --git a/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java b/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java index 90ddbcedc3..59e1d84b6d 100644 --- a/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java +++ b/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java @@ -104,8 +104,7 @@ public String convertMarkupShowInWebView( final Activity context, final WebView webView, final boolean lightMode, - final boolean lineNum - ) { + final boolean lineNum) { String html; try { html = convertMarkup(content, context, lightMode, lineNum, document.getFile()); diff --git a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownActionButtons.java b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownActionButtons.java index 4a225e9bfe..7c120c60c5 100644 --- a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownActionButtons.java +++ b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownActionButtons.java @@ -304,7 +304,7 @@ private void insertTableRow(int cols, boolean isHeaderEnabled) { @Override public boolean runTitleClick() { final Matcher m = MarkdownReplacePatternGenerator.PREFIX_ATX_HEADING.matcher(""); - MarkorDialogFactory.showHeadlineDialog(getActivity(), _hlEditor, _disabledHeadings, (text, start, end) -> { + MarkorDialogFactory.showHeadlineDialog(getActivity(), _hlEditor, _webView, _disabledHeadings, (text, start, end) -> { if (m.reset(text.subSequence(start, end)).find()) { return m.end(2) - m.start(2) - 1; } diff --git a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextConverter.java b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextConverter.java index ecaff23170..5bb1d1a05a 100644 --- a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextConverter.java +++ b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextConverter.java @@ -31,8 +31,10 @@ import com.vladsch.flexmark.ext.yaml.front.matter.AbstractYamlFrontMatterVisitor; import com.vladsch.flexmark.ext.yaml.front.matter.YamlFrontMatterExtension; import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.html.renderer.HeaderIdGenerator; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.superscript.SuperscriptExtension; +import com.vladsch.flexmark.util.ast.Document; import com.vladsch.flexmark.util.builder.Extension; import com.vladsch.flexmark.util.options.MutableDataSet; @@ -153,17 +155,23 @@ public class MarkdownTextConverter extends TextConverterBase { public static final HtmlRenderer flexmarkRenderer = HtmlRenderer.builder().extensions(flexmarkExtensions).build(); //######################## - //## Methods + //## Others //######################## + private static String toDashChars = " -_"; // See HtmlRenderer.HEADER_ID_GENERATOR_TO_DASH_CHARS.getFrom(document) + private static final Pattern linkPattern = Pattern.compile("\\[(.*?)\\]\\((.*?)(\\s+\".*\")?\\)"); + + //######################## + //## Methods + //######################## @Override public String convertMarkup(String markup, Context context, boolean lightMode, boolean enableLineNumbers, File file) { - String converted = "", onLoadJs = "", head = ""; + String converted, onLoadJs = "", head = ""; final MutableDataSet options = new MutableDataSet(); options.set(Parser.EXTENSIONS, flexmarkExtensions); - options.set(Parser.SPACE_IN_LINK_URLS, true); // allow links like [this](some filename with spaces.md) + options.set(Parser.SPACE_IN_LINK_URLS, true); // Allow links like [this](some filename with spaces.md) // options.set(HtmlRenderer.SOFT_BREAK, "
\n"); // Add linefeed to HTML break @@ -198,7 +206,7 @@ public String convertMarkup(String markup, Context context, boolean lightMode, b head += CSS_PRESENTATION_BEAMER; } - // Frontmatter + // Front matter String fmaText = ""; final List fmaAllowedAttributes = _appSettings.getMarkdownShownYamlFrontMatterKeys(); Map> fma = Collections.EMPTY_MAP; @@ -293,7 +301,8 @@ public String convertMarkup(String markup, Context context, boolean lightMode, b //////////// // Markup parsing - afterwards = HTML - converted = fmaText + flexmarkRenderer.withOptions(options).render(flexmarkParser.parse(markup)); + Document document = flexmarkParser.parse(markup); + converted = fmaText + flexmarkRenderer.withOptions(options).render(document); // After render changes: Fixes for Footnotes (converter creates footnote +
+ ref#(click) --> remove line break) if (converted.contains("footnote-")) { @@ -329,14 +338,15 @@ public String convertMarkup(String markup, Context context, boolean lightMode, b return putContentIntoTemplate(context, converted, lightMode, file, onLoadJs, head); } - private static final Pattern linkPattern = Pattern.compile("\\[(.*?)\\]\\((.*?)(\\s+\".*\")?\\)"); + public static String generateHeaderId(String headerText) { + return HeaderIdGenerator.generateId(headerText, toDashChars, false, false); + } private String escapeSpacesInLink(final String markup) { final Matcher matcher = linkPattern.matcher(markup); if (!matcher.find()) { return markup; } - // 1) Walk through the text till finding a link in markdown syntax // 2) Add all text-before-link to buffer // 3) Extract [title](link to somehere) diff --git a/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextActionButtons.java b/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextActionButtons.java index 3def72f9e3..4a80be9ce4 100644 --- a/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextActionButtons.java +++ b/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextActionButtons.java @@ -263,7 +263,7 @@ public static String createWikitextHeaderAndTitleContents(String fileNameWithout @Override public boolean runTitleClick() { final Matcher m = WikitextSyntaxHighlighter.HEADING.matcher(""); - MarkorDialogFactory.showHeadlineDialog(getActivity(), _hlEditor, _disabledHeadings, (text, start, end) -> { + MarkorDialogFactory.showHeadlineDialog(getActivity(), _hlEditor, _webView, _disabledHeadings, (text, start, end) -> { if (m.reset(text.subSequence(start, end)).find()) { return 7 - (m.end(2) - m.start(2)); } 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 dfb45ce609..d0b9fb48e8 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java +++ b/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java @@ -27,6 +27,7 @@ import android.util.Pair; import android.view.Gravity; import android.view.WindowManager; +import android.webkit.WebView; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; @@ -39,6 +40,7 @@ import net.gsantner.markor.ApplicationObject; import net.gsantner.markor.R; import net.gsantner.markor.format.FormatRegistry; +import net.gsantner.markor.format.markdown.MarkdownTextConverter; import net.gsantner.markor.format.todotxt.TodoTxtBasicSyntaxHighlighter; import net.gsantner.markor.format.todotxt.TodoTxtFilter; import net.gsantner.markor.format.todotxt.TodoTxtTask; @@ -792,9 +794,9 @@ private static class Heading { public static void showHeadlineDialog( final Activity activity, final EditText edit, + final WebView webView, final Set disabled, - final GsCallback.r3 headingLevel - ) { + final GsCallback.r3 headingLevel) { // Get all headings and their levels final CharSequence text = edit.getText(); final List headings = new ArrayList<>(); @@ -819,12 +821,19 @@ public static void showHeadlineDialog( dopt.titleText = R.string.table_of_contents; dopt.searchHintText = R.string.search; dopt.isSearchEnabled = true; - dopt.neutralButtonText = R.string.filter; + dopt.isSoftInputVisible = false; + dopt.isDismissOnItemSelected = false; + dopt.isSaveItemPositionEnabled = true; + dopt.positionCallback = result -> { final int index = filtered.get(result.get(0)); TextViewUtils.selectLines(edit, headings.get(index).line); + String header = headings.get(index).str; + String id = MarkdownTextConverter.generateHeaderId(header.substring(header.lastIndexOf('#') + 1).trim()); + webView.loadUrl("javascript:document.getElementById('" + id + "').scrollIntoView();"); }; + dopt.neutralButtonText = R.string.filter; dopt.neutralButtonCallback = (dialog) -> { final DialogOptions dopt2 = new DialogOptions(); dopt2.preSelected = GsCollectionUtils.indices(levels, l -> !disabled.contains(l)); @@ -850,7 +859,11 @@ public static void showHeadlineDialog( }; GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt2); }; - dopt.gravity = Gravity.TOP; + + dopt.portraitAspectRatio = new float[]{0.95f, 0.8f}; + dopt.landscapeAspectRatio = new float[]{0.7f, 0.95f}; + dopt.gravity = Gravity.CENTER; + GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } 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 77c85a2ab6..7abf599fb2 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java @@ -146,7 +146,7 @@ private AlertDialog.Builder makeDialog(final File basedir, final boolean allowCr if (pos == 3) { // Jekyll prefix = TodoTxtTask.DATEF_YYYY_MM_DD.format(new Date()) + "-"; - } else if (pos == 9) { //ZettelKasten + } else if (pos == 9) { // ZettelKasten prefix = new SimpleDateFormat("yyyyMMddHHmm", Locale.ROOT).format(new Date()) + "-"; } if (!TextUtils.isEmpty(prefix) && !fileNameEdit.getText().toString().startsWith(prefix)) { @@ -325,7 +325,7 @@ private String getTemplateByPos( " line\";2059-12-24\n"; } case 12: { - // orgmode + // Org-mode return "OrgMode Reference\n" + "* Headline\n" + "** Nested headline\n" + "*** Deeper\n" + "\n" + "* Basic markup\n" + "This is the general building block for org-mode navigation.\n" + "- _underscores let you underline things_\n" + "- *stars add emphasis*\n" + "- /slashes are italics/\n" + "- +pluses are strikethrough+\n" + "- =equal signs are verbatim text=\n" + "- ~tildes can also be used~\n" + "\n" + "* List\n" + "** Unordered List\n" + "- Item 1\n" + "- Item 2\n" + " - Subitem 2.1\n" + " - Subitem 2.2\n" + "** Ordered List\n" + "1. First Item\n" + "2. Second Item\n" + " 1. Subitem 2.1\n" + " 2. Subitem 2.2\n" + "- [X] Completed Task\n" + "- [ ] Uncompleted Task\n" + "** Nested List\n" + " - Item A\n" + " - Subitem A.1\n" + " - Subitem A.2\n" + " - Item B\n" + "\n" + "* Tables\n" + "\n" + "| First Name | Last Name | Years using Emacs |\n" + "|----------------------------+---------------------+-------------------|\n" + "| Lee | Hinman | 5 |\n" + "| Mike | Hunsinger | 2 |\n" + "| Daniel | Glauser | 4 |\n" + "| Really-long-first-name-guy | long-last-name-pers | 1 |\n" + "\n" + "* Org-mode links\n" + "\n" + "#+BEGIN_SRC fundamental\n" + "[[http://google.com/][Google]]\n" + "#+END_SRC\n" + "\n" + "[[./images/pic1.png]]\n" + "\n" + "\n" + "* TODO List\n" + "** TODO This is a task that needs doing\n" + "** TODO Another todo task\n" + "- [ ] sub task one\n" + "- [X] sub task two\n" + "- [ ] sub task three\n" + "** DONE I've already finished this one\n" + "*** CANCELLED learn todos\n" + " CLOSED: [2023-10-16 Mon 08:39]\n" + "\n" + "* Code\n" + "#+BEGIN_LaTeX\n" + "$a + b$\n" + "#+END_LaTeX\n" + "\n" + "#+BEGIN_SRC emacs-lisp\n" + "(defun my/function ()\n" + " \"docstring\"\n" + " (interactive)\n" + " (progn\n" + " (+ 1 1)\n" + " (message \"Hi\")))\n" + "#+END_SRC\n" + "\n"; } } 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 3ca26d524c..30c29b56df 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/GsSearchOrCustomTextDialog.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/GsSearchOrCustomTextDialog.java @@ -23,6 +23,7 @@ import android.text.SpannableString; import android.text.TextUtils; import android.text.method.LinkMovementMethod; +import android.util.DisplayMetrics; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -88,18 +89,23 @@ public static class DialogOptions { public List iconsForData; public CharSequence messageText = ""; public String defaultText = ""; - public boolean isSearchEnabled = true; public boolean isDarkDialog = false; + public boolean isSearchEnabled = true; + public boolean isSoftInputVisible = true; + public boolean isDismissOnItemSelected = true; + public boolean isSaveItemPositionEnabled = false; + public int gravity = Gravity.NO_GRAVITY; public int dialogWidthDp = WindowManager.LayoutParams.MATCH_PARENT; public int dialogHeightDp = WindowManager.LayoutParams.WRAP_CONTENT; - public int gravity = Gravity.NO_GRAVITY; public int searchInputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; - public GsCallback.a1 highlighter = null; + public float[] portraitAspectRatio = {0.0f, 1.0f}; + public float[] landscapeAspectRatio = {0.0f, 1.0f}; public String extraFilter = null; public Collection preSelected = null; + public GsCallback.a1 highlighter = null; public GsCallback.a1 neutralButtonCallback = null; - public GsCallback.b2 searchFunction = GsSearchOrCustomTextDialog::standardSearch; public GsCallback.a1 dismissCallback = null; + public GsCallback.b2 searchFunction = GsSearchOrCustomTextDialog::standardSearch; @ColorInt public int textColor = 0xFF000000; @@ -154,7 +160,7 @@ private Adapter(final Context context, final DialogOptions dopt) { _dopt = dopt; _extraPattern = (_dopt.extraFilter == null ? null : Pattern.compile(_dopt.extraFilter).matcher("")); _selectedItems = new HashSet<>(_dopt.preSelected != null ? _dopt.preSelected : Collections.emptyList()); - _layoutHeight = (int) GsContextUtils.instance.convertDpToPx(context, 36); + _layoutHeight = GsContextUtils.instance.convertDpToPx(context, 36); } @NonNull @@ -273,6 +279,9 @@ public static void showMultiChoiceDialogWithSearchFilterUI(final Activity activi final ListView listView = new ListView(activity); listView.setId(LIST_VIEW_ID); listView.setAdapter(listAdapter); + if (dopt.isSaveItemPositionEnabled) { + listView.setSelection(activity.getIntent().getIntExtra("lastHeadingPosition", 0)); + } listView.setVisibility(dopt.data != null && !dopt.data.isEmpty() ? View.VISIBLE : View.GONE); final LinearLayout.LayoutParams listLayout = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0); listLayout.weight = 1; @@ -280,6 +289,8 @@ public static void showMultiChoiceDialogWithSearchFilterUI(final Activity activi if (dopt.dismissCallback != null) { dialogBuilder.setOnDismissListener(dopt.dismissCallback::callback); + } else { + dialogBuilder.setOnDismissListener(dialogInterface -> activity.getIntent().putExtra("lastHeadingPosition", listView.getFirstVisiblePosition())); } dialogBuilder.setView(mainLayout) @@ -318,24 +329,34 @@ public static void showMultiChoiceDialogWithSearchFilterUI(final Activity activi }); final Window win = dialog.getWindow(); - if (win != null && dopt.isSearchEnabled) { - win.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } - - dialog.show(); - if (win != null) { - int ds_w = dopt.dialogWidthDp < 100 ? dopt.dialogWidthDp : ((int) (dopt.dialogWidthDp * activity.getResources().getDisplayMetrics().density)); - int ds_h = dopt.dialogHeightDp < 100 ? dopt.dialogHeightDp : ((int) (dopt.dialogHeightDp * activity.getResources().getDisplayMetrics().density)); - ds_w = (ds_w * 1.1 > activity.getResources().getDisplayMetrics().widthPixels) ? ViewGroup.LayoutParams.MATCH_PARENT : ds_w; - ds_h = (ds_h * 1.1 > activity.getResources().getDisplayMetrics().heightPixels) ? ViewGroup.LayoutParams.MATCH_PARENT : ds_h; - win.setLayout(ds_w, ds_h); - } + if (dopt.isSearchEnabled) { + if (dopt.isSoftInputVisible) { + win.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } else { + win.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } + } + dialog.show(); + + DisplayMetrics displayMetrics = activity.getResources().getDisplayMetrics(); + if (dopt.portraitAspectRatio[0] > 0 && dopt.landscapeAspectRatio[0] > 0) { + GsContextUtils.windowAspectRatio(win, displayMetrics, dopt.portraitAspectRatio[0], dopt.portraitAspectRatio[1], dopt.landscapeAspectRatio[0], dopt.landscapeAspectRatio[1]); + } else { + int ds_w = dopt.dialogWidthDp < 100 ? dopt.dialogWidthDp : ((int) (dopt.dialogWidthDp * displayMetrics.density)); + int ds_h = dopt.dialogHeightDp < 100 ? dopt.dialogHeightDp : ((int) (dopt.dialogHeightDp * displayMetrics.density)); + ds_w = (ds_w * 1.1 > displayMetrics.widthPixels) ? ViewGroup.LayoutParams.MATCH_PARENT : ds_w; + ds_h = (ds_h * 1.1 > displayMetrics.heightPixels) ? ViewGroup.LayoutParams.MATCH_PARENT : ds_h; + win.setLayout(ds_w, ds_h); + } - if (win != null && dopt.gravity != Gravity.NO_GRAVITY) { - WindowManager.LayoutParams wlp = win.getAttributes(); - wlp.gravity = dopt.gravity; - win.setAttributes(wlp); + if (dopt.gravity != Gravity.NO_GRAVITY) { + WindowManager.LayoutParams wlp = win.getAttributes(); + wlp.gravity = dopt.gravity; + win.setAttributes(wlp); + } + } else { + dialog.show(); } final Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); @@ -356,7 +377,9 @@ public static void showMultiChoiceDialogWithSearchFilterUI(final Activity activi // Helper function to trigger callback with single item final GsCallback.b1 directActivate = (position) -> { final int index = listAdapter._filteredItems.get(position); - dialog.dismiss(); + if (dopt.isDismissOnItemSelected) { + dialog.dismiss(); + } if (dopt.callback != null) { dopt.callback.callback(dopt.data.get(index).toString()); } else if (dopt.positionCallback != null) { 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 a106a953b2..ac1ccb974b 100644 --- a/app/src/main/java/net/gsantner/opoc/util/GsContextUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/GsContextUtils.java @@ -2745,6 +2745,29 @@ public void dialogFullWidth(AlertDialog dialog, boolean fullWidth, boolean showK } } + public static void windowAspectRatio(final Window window, + final DisplayMetrics displayMetrics, + float portraitWidthRatio, + float portraitHeightRatio, + float landscapeWidthRatio, + float landscapeHeightRatio) { + if (window == null) { + return; + } + + WindowManager.LayoutParams params = window.getAttributes(); + final int width = displayMetrics.widthPixels; + final int height = displayMetrics.heightPixels; + if (width < height) { // Portrait + params.width = (int) (width * portraitWidthRatio); + params.height = (int) (height * portraitHeightRatio); + } else { // Landscape + params.width = (int) (width * landscapeWidthRatio); + params.height = (int) (height * landscapeHeightRatio); + } + window.setAttributes(params); + } + // Make activity/app not show up in the recents history - call before finish / System.exit public T removeActivityFromHistory(final Context activity) { try {