From 696677169136e9648ddadc0eea4cf3f2abdd87e0 Mon Sep 17 00:00:00 2001 From: Li Guanglin <60415467+guanglinn@users.noreply.github.com> Date: Tue, 10 Oct 2023 23:35:44 +0800 Subject: [PATCH 1/2] Editor/viewer: Side margin improvements 2, closes #2111 (PR #2119 by @guanglinn) --- .../gsantner/markor/format/markdown/MarkdownTextConverter.java | 2 +- app/src/main/res/values/dimens.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 91b139b41..ad3f50de2 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 @@ -83,7 +83,7 @@ public class MarkdownTextConverter extends TextConverterBase { //######################## //## Injected CSS / JS / HTML //######################## - public static final String CSS_BODY = CSS_S + "body{margin:0;padding:2vw}" + CSS_E; + public static final String CSS_BODY = CSS_S + "body{margin:0;padding:2.5vw}" + CSS_E; public static final String CSS_HEADER_UNDERLINE = CSS_S + " .header_no_underline { text-decoration: none; color: " + TOKEN_BW_INVERSE_OF_THEME + "; } h1 < a.header_no_underline { border-bottom: 2px solid #eaecef; } " + CSS_E; public static final String CSS_H1_H2_UNDERLINE = CSS_S + " h1,h2 { border-bottom: 2px solid " + TOKEN_BW_INVERSE_OF_THEME_HEADER_UNDERLINE + "; } " + CSS_E; public static final String CSS_BLOCKQUOTE_VERTICAL_LINE = CSS_S + "blockquote{padding:0px 14px;border-" + TOKEN_TEXT_DIRECTION + ":3.5px solid #dddddd;margin:4px 0}" + CSS_E; diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 98114e2fc..5f011c4fd 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,6 +1,6 @@ - 12dp + 16dp 12dp 32dp 8dp From a20ea288c11ce57ee2e76b3177b06c6442916f54 Mon Sep 17 00:00:00 2001 From: Harshad Vedartham Date: Tue, 10 Oct 2023 08:43:40 -0700 Subject: [PATCH 2/2] Reworked attachments (PR #2106 by @harshad1) * Reworked how attachments are added * Preserve keyboard state, fix formatting * Tweaks to how keyboard is restored * Improvements to keeping keyboard open after dialog * Dramatic simplification to keyboard restore * Using built in voice recorder * Fixing issues with images not showing * Tweaks to maintaining keyboard state --------- Co-authored-by: Gregor Santner --- app/src/main/AndroidManifest.xml | 6 +- .../ActionButtonSettingsActivity.java | 15 +- .../activity/DocumentEditAndViewFragment.java | 44 +- .../markor/activity/MainActivity.java | 15 +- .../markor/activity/MarkorBaseActivity.java | 5 + .../markor/format/ActionButtonBase.java | 168 ++++---- .../markor/format/TextConverterBase.java | 17 +- .../asciidoc/AsciidocActionButtons.java | 172 +++----- .../markdown/MarkdownActionButtons.java | 16 +- .../format/todotxt/TodoTxtActionButtons.java | 7 +- .../wikitext/WikitextActionButtons.java | 16 +- .../frontend/AttachLinkOrFileDialog.java | 406 ++++++++++-------- .../markor/frontend/MarkorDialogFactory.java | 81 +--- .../gsantner/markor/model/AppSettings.java | 11 + .../opoc/frontend/GsAudioRecordOmDialog.java | 8 +- .../gsantner/opoc/util/GsCollectionUtils.java | 6 +- .../gsantner/opoc/util/GsContextUtils.java | 137 +++--- .../net/gsantner/opoc/util/GsFileUtils.java | 47 +- .../main/res/layout/select_path_dialog.xml | 4 + .../main/res/menu/document__edit__menu.xml | 36 -- .../res/values/string-not_translatable.xml | 12 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences_master.xml | 7 + .../writeily/model/WrMarkorSingleton.java | 16 +- 24 files changed, 596 insertions(+), 658 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 82329f24b..864198699 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -65,7 +65,7 @@ android:label="@string/app_name" android:launchMode="singleTop" android:taskAffinity=".activity.MainActivity" - android:windowSoftInputMode="stateAlwaysHidden|adjustResize"> + android:windowSoftInputMode="stateUnchanged|adjustResize"> @@ -1095,6 +1095,10 @@ + + + + diff --git a/app/src/main/java/net/gsantner/markor/activity/ActionButtonSettingsActivity.java b/app/src/main/java/net/gsantner/markor/activity/ActionButtonSettingsActivity.java index 2f5bca3ce..f4aa39f06 100644 --- a/app/src/main/java/net/gsantner/markor/activity/ActionButtonSettingsActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/ActionButtonSettingsActivity.java @@ -35,6 +35,7 @@ import net.gsantner.markor.format.plaintext.PlaintextActionButtons; import net.gsantner.markor.format.todotxt.TodoTxtActionButtons; import net.gsantner.markor.format.wikitext.WikitextActionButtons; +import net.gsantner.opoc.util.GsCollectionUtils; import java.util.ArrayList; import java.util.HashSet; @@ -110,14 +111,8 @@ public boolean onOptionsItemSelected(MenuItem item) { } private void saveNewOrder() { - final ArrayList reorderedKeys = new ArrayList<>(); - - for (final int i : _adapter.order) { - reorderedKeys.add(_keys.get(i)); - } - - _textActions.saveActionOrder(reorderedKeys); - _textActions.saveDisabledActions(new ArrayList<>(_adapter._disabled)); + _textActions.saveActionOrder(GsCollectionUtils.map(_adapter.order, i -> _keys.get(i))); + _textActions.saveDisabledActions(_adapter._disabled); } @Override @@ -238,8 +233,8 @@ private ReorderCallback(OrderAdapter adapter) { @Override public boolean onMove(@NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - final int from = viewHolder.getAdapterPosition(); - final int to = target.getAdapterPosition(); + final int from = viewHolder.getAbsoluteAdapterPosition(); + final int to = target.getAbsoluteAdapterPosition(); final int value = _adapter.order.get(from); _adapter.order.remove(from); 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 9068696ca..8500a0979 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java @@ -27,6 +27,8 @@ import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; import android.webkit.JavascriptInterface; import android.webkit.WebSettings; import android.webkit.WebView; @@ -43,8 +45,6 @@ import net.gsantner.markor.format.ActionButtonBase; import net.gsantner.markor.format.FormatRegistry; import net.gsantner.markor.format.TextConverterBase; -import net.gsantner.markor.frontend.AttachLinkOrFileDialog; -import net.gsantner.markor.frontend.DatetimeFormatDialog; import net.gsantner.markor.frontend.DraggableScrollbarScrollView; import net.gsantner.markor.frontend.FileInfoDialog; import net.gsantner.markor.frontend.MarkorDialogFactory; @@ -227,6 +227,27 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { updateUndoRedoIconStates(); }); _hlEditor.addTextChangedListener(GsTextWatcherAdapter.after(s -> debounced.run())); + + // We set the keyboard to be hidden if it was hidden when we lost focus + // This works well to preserve keyboard state. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + final Window window = activity.getWindow(); + final int adjustResize = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + final int unchanged = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED | adjustResize; + final int hidden = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN | adjustResize; + + _hlEditor.getViewTreeObserver().addOnWindowFocusChangeListener(hasFocus -> { + if (hasFocus) { + // Restore old state + _hlEditor.postDelayed(() -> window.setSoftInputMode(unchanged), 500); + } else { + final Boolean isOpen = TextViewUtils.isImeOpen(_hlEditor); + if (isOpen != null && !isOpen) { + window.setSoftInputMode(hidden); + } + } + }); + } } @Override @@ -294,7 +315,6 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // Edit / Preview switch menu.findItem(R.id.action_edit).setVisible(isText && _isPreviewVisible); - menu.findItem(R.id.submenu_attach).setVisible(false); menu.findItem(R.id.action_preview).setVisible(isText && !_isPreviewVisible); menu.findItem(R.id.action_search).setVisible(isText && !_isPreviewVisible); menu.findItem(R.id.action_search_view).setVisible(isText && _isPreviewVisible); @@ -518,24 +538,6 @@ public boolean onOptionsItemSelected(@NonNull final MenuItem item) { _cu.draftEmail(getActivity(), "Debug Log " + getString(R.string.app_name_real), text, "debug@localhost.lan"); return true; } - - case R.id.action_attach_color: { - _format.getActions().showColorPickerDialog(); - return true; - } - case R.id.action_attach_date: { - DatetimeFormatDialog.showDatetimeFormatDialog(activity, _hlEditor); - return true; - } - case R.id.action_attach_audio: - case R.id.action_attach_file: - case R.id.action_attach_image: - case R.id.action_attach_link: { - int actionId = (itemId == R.id.action_attach_audio ? 4 : (itemId == R.id.action_attach_image ? 2 : 3)); - AttachLinkOrFileDialog.showInsertImageOrLinkDialog(actionId, _document.getFormat(), activity, _hlEditor, _document.getFile()); - return true; - } - case R.id.action_load_epub: { MarkorFileBrowserFactory.showFileDialog(new GsFileBrowserOptions.SelectionListenerAdapter() { @Override 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 b5f5b949b..4ed688349 100644 --- a/app/src/main/java/net/gsantner/markor/activity/MainActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/MainActivity.java @@ -372,19 +372,18 @@ public String getPosTitle(final int pos) { return null; } - public void onViewPagerPageSelected(int pos) { + public void onViewPagerPageSelected(final int pos) { _bottomNav.getMenu().getItem(pos).setChecked(true); if (pos == tabIdToPos(R.id.nav_notebook)) { _fab.show(); + _cu.showSoftKeyboard(this, false); } else { _fab.hide(); - } - setTitle(getPosTitle(pos)); - - if (pos != tabIdToPos(R.id.nav_notebook)) { restoreDefaultToolbar(); } + + setTitle(getPosTitle(pos)); } private GsFileBrowserOptions.Options _filesystemDialogOptions = null; @@ -468,6 +467,12 @@ protected void onStop() { restoreDefaultToolbar(); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + _cu.extractResultFromActivityResult(this, requestCode, resultCode, data); + } + /** * Restores the default toolbar. Used when changing the tab or moving to another activity * while {@link GsFileBrowserFragment} action mode is active (e.g. when renaming a file) diff --git a/app/src/main/java/net/gsantner/markor/activity/MarkorBaseActivity.java b/app/src/main/java/net/gsantner/markor/activity/MarkorBaseActivity.java index 0f4e2d33f..9fac8c5f8 100644 --- a/app/src/main/java/net/gsantner/markor/activity/MarkorBaseActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/MarkorBaseActivity.java @@ -7,12 +7,17 @@ import android.view.WindowManager; import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; import net.gsantner.markor.ApplicationObject; import net.gsantner.markor.R; import net.gsantner.markor.model.AppSettings; import net.gsantner.markor.util.MarkorContextUtils; import net.gsantner.opoc.frontend.base.GsActivityBase; +import net.gsantner.opoc.util.GsCollectionUtils; + +import java.util.List; public abstract class MarkorBaseActivity extends GsActivityBase { 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 eefc9aa2a..968c0ded6 100644 --- a/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java +++ b/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java @@ -46,11 +46,14 @@ import net.gsantner.markor.model.Document; import net.gsantner.markor.util.MarkorContextUtils; import net.gsantner.opoc.format.GsTextUtils; +import net.gsantner.opoc.util.GsCollectionUtils; +import net.gsantner.opoc.util.GsContextUtils; import net.gsantner.opoc.util.GsFileUtils; import net.gsantner.opoc.wrapper.GsCallback; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -63,13 +66,13 @@ @SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) public abstract class ActionButtonBase { - private Activity m_activity; - private MarkorContextUtils m_cu; + private Activity _activity; + private MarkorContextUtils _cu; private final int _buttonHorizontalMargin; private String _lastSnip; protected HighlightingEditor _hlEditor; - protected WebView m_webView; + protected WebView _webView; protected Document _document; protected AppSettings _appSettings; protected int _indent; @@ -97,7 +100,7 @@ public boolean onActionLongClick(final @StringRes int action) { // Override to implement custom search action public boolean onSearch() { - MarkorDialogFactory.showSearchDialog(getActivity(), _hlEditor); + MarkorDialogFactory.showSearchDialog(_activity, _hlEditor); return true; } @@ -137,10 +140,10 @@ public List getDisabledActions() { * @return Map of String key -> Action */ public Map getActiveActionMap() { - List actionList = getActiveActionList(); - List keyList = getActiveActionKeys(); + final List actionList = getActiveActionList(); + final List keyList = getActiveActionKeys(); - 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)); @@ -154,14 +157,7 @@ public Map getActiveActionMap() { * @return List or resource strings */ public List getActiveActionKeys() { - final List actionList = getActiveActionList(); - final ArrayList keys = new ArrayList<>(); - - for (ActionItem item : actionList) { - keys.add(rstr(item.keyId)); - } - - return keys; + return GsCollectionUtils.map(getActiveActionList(), item -> rstr(item.keyId)); } /** @@ -172,7 +168,7 @@ public List getActiveActionKeys() { * * @param keys of keys (in order) to save */ - public void saveDisabledActions(final List keys) { + public void saveDisabledActions(final Collection keys) { saveActionPreference(DISABLED_SUFFIX, keys); } @@ -184,17 +180,13 @@ public void saveDisabledActions(final List keys) { * * @param keys of keys (in order) to save */ - public void saveActionOrder(final List keys) { + public void saveActionOrder(final Collection keys) { saveActionPreference(ORDER_SUFFIX, keys); } - private void saveActionPreference(final String suffix, List values) { - // Remove any values not in current actions - values = new ArrayList<>(values); - values.retainAll(getActiveActionKeys()); - - SharedPreferences settings = getContext().getSharedPreferences(ACTION_ORDER_PREF_NAME, Context.MODE_PRIVATE); - String formatKey = rstr(getFormatActionsKey()) + suffix; + private void saveActionPreference(final String suffix, final Collection values) { + final SharedPreferences settings = getContext().getSharedPreferences(ACTION_ORDER_PREF_NAME, Context.MODE_PRIVATE); + final String formatKey = rstr(getFormatActionsKey()) + suffix; settings.edit().putString(formatKey, TextUtils.join(",", values)).apply(); } @@ -220,30 +212,38 @@ private List loadActionPreference(final String suffix) { */ public List getActionOrder() { - ArrayList definedKeys = new ArrayList<>(getActiveActionKeys()); - List prefKeys = new ArrayList<>(loadActionPreference(ORDER_SUFFIX)); + final Set order = new LinkedHashSet<>(loadActionPreference(ORDER_SUFFIX)); // Handle the case where order was stored without suffix. i.e. before this release. - if (prefKeys.size() == 0) { - prefKeys = new ArrayList<>(loadActionPreference("")); + if (order.isEmpty()) { + order.addAll(loadActionPreference("")); } - Set prefSet = new LinkedHashSet<>(prefKeys); - Set defSet = new LinkedHashSet<>(definedKeys); + final Set defined = new LinkedHashSet<>(getActiveActionKeys()); + final Set disabled = new LinkedHashSet<>(getDisabledActions()); + + // Any definedKeys which are not in prefs or disabled keys are added to disabled + final Set existing = GsCollectionUtils.union(order, disabled); + final Set added = GsCollectionUtils.setDiff(defined, existing); + final Set removed = GsCollectionUtils.setDiff(existing, defined); + + // Disable any new actions unless none existing (i.e. first run) + if (!existing.isEmpty()) { + disabled.addAll(added); + } - // Add any defined keys which are not in prefs - defSet.removeAll(prefSet); - prefKeys.addAll(defSet); + // Add new ones to order + order.addAll(added); - // Remove any pref keys which are not defined - prefSet.removeAll(definedKeys); - prefKeys.removeAll(prefSet); + // Removed removed from order and disabled + disabled.removeAll(removed); + order.removeAll(removed); - if (defSet.size() > 0 || prefSet.size() > 0) { - saveActionOrder(prefKeys); + if (!added.isEmpty() || !removed.isEmpty()) { + saveActionOrder(order); } - return prefKeys; + return new ArrayList<>(order); } @SuppressWarnings("ConstantConditions") @@ -263,7 +263,7 @@ public void recreateActionButtons(ViewGroup barLayout, ActionItem.DisplayMode di } protected void appendActionButtonToBar(ViewGroup barLayout, @NonNull ActionItem action) { - final ImageView btn = (ImageView) getActivity().getLayoutInflater().inflate(R.layout.quick_keyboard_button, null); + final ImageView btn = (ImageView) _activity.getLayoutInflater().inflate(R.layout.quick_keyboard_button, null); btn.setImageResource(action.iconId); final String desc = rstr(action.stringId); btn.setContentDescription(desc); @@ -499,10 +499,10 @@ else if (((selectionEnd <= (_hlEditor.length() - _action.length())) && public ActionButtonBase setUiReferences(@Nullable final Activity activity, @Nullable final HighlightingEditor hlEditor, @Nullable final WebView webview) { - m_activity = activity; + _activity = activity; _hlEditor = hlEditor; - m_webView = webview; - m_cu = new MarkorContextUtils(m_activity); + _webView = webview; + _cu = new MarkorContextUtils(_activity); return this; } @@ -516,22 +516,22 @@ public ActionButtonBase setDocument(Document document) { } public Activity getActivity() { - return m_activity; + return _activity; } public Context getContext() { - return m_activity != null ? m_activity : _appSettings.getContext(); + return _activity != null ? _activity : _appSettings.getContext(); } public MarkorContextUtils getCu() { - return m_cu; + return _cu; } /** * Callable from background thread! */ public void setEditorTextAsync(final String text) { - getActivity().runOnUiThread(() -> _hlEditor.setText(text)); + _activity.runOnUiThread(() -> _hlEditor.setText(text)); } protected void runIndentLines(final boolean deIndent) { @@ -547,6 +547,7 @@ protected void runIndentLines(final boolean deIndent) { // Some actions common to multiple file types // Can be called _explicitly_ by a derived class protected final boolean runCommonAction(final @StringRes int action) { + final Editable text = _hlEditor.getText(); switch (action) { case R.string.abid_common_unordered_list_char: { runRegularPrefixAction(_appSettings.getUnorderedListCharacter() + " ", true); @@ -561,37 +562,23 @@ protected final boolean runCommonAction(final @StringRes int action) { return true; } case R.string.abid_common_time: { - DatetimeFormatDialog.showDatetimeFormatDialog(getActivity(), _hlEditor); + DatetimeFormatDialog.showDatetimeFormatDialog(_activity, _hlEditor); return true; - } - case R.string.abid_common_time_insert_timestamp: { - } case R.string.abid_common_accordion: { _hlEditor.insertOrReplaceTextOnCursor("
" + rstr(R.string.expand_collapse) + "\n" + HighlightingEditor.PLACE_CURSOR_HERE_TOKEN + "\n\n
"); return true; } - case R.string.abid_common_attach_something: { - MarkorDialogFactory.showAttachSomethingDialog(getActivity(), itemId -> { - switch (itemId) { - case R.id.action_attach_color: { - showColorPickerDialog(); - break; - } - case R.id.action_attach_date: { - DatetimeFormatDialog.showDatetimeFormatDialog(getActivity(), _hlEditor); - break; - } - case R.id.action_attach_audio: - case R.id.action_attach_file: - case R.id.action_attach_image: - case R.id.action_attach_link: { - int actionId = (itemId == R.id.action_attach_audio ? 4 : (itemId == R.id.action_attach_image ? 2 : 3)); - AttachLinkOrFileDialog.showInsertImageOrLinkDialog(actionId, _document.getFormat(), getActivity(), _hlEditor, _document.getFile()); - break; - } - } - }); + case R.string.abid_common_insert_audio: { + AttachLinkOrFileDialog.showInsertImageOrLinkDialog(AttachLinkOrFileDialog.AUDIO_ACTION, _document.getFormat(), _activity, text, _document.getFile()); + return true; + } + case R.string.abid_common_insert_link: { + AttachLinkOrFileDialog.showInsertImageOrLinkDialog(AttachLinkOrFileDialog.FILE_OR_LINK_ACTION, _document.getFormat(), _activity, text, _document.getFile()); + return true; + } + case R.string.abid_common_insert_image: { + AttachLinkOrFileDialog.showInsertImageOrLinkDialog(AttachLinkOrFileDialog.IMAGE_ACTION, _document.getFormat(), _activity, text, _document.getFile()); return true; } case R.string.abid_common_ordered_list_renumber: { @@ -611,7 +598,7 @@ protected final boolean runCommonAction(final @StringRes int action) { return true; } case R.string.abid_common_insert_snippet: { - MarkorDialogFactory.showInsertSnippetDialog(getActivity(), _hlEditor, (snip) -> { + MarkorDialogFactory.showInsertSnippetDialog(_activity, (snip) -> { _hlEditor.insertOrReplaceTextOnCursor(TextViewUtils.interpolateEscapedDateTime(snip)); _lastSnip = snip; }); @@ -619,11 +606,11 @@ protected final boolean runCommonAction(final @StringRes int action) { } case R.string.abid_common_open_link_browser: { String url; - if ((url = GsTextUtils.tryExtractUrlAroundPos(_hlEditor.getText().toString(), _hlEditor.getSelectionStart())) != null) { + if ((url = GsTextUtils.tryExtractUrlAroundPos(text.toString(), _hlEditor.getSelectionStart())) != null) { if (url.endsWith(")")) { url = url.substring(0, url.length() - 1); } - getCu().openWebpageInExternalBrowser(getContext(), url); + _cu.openWebpageInExternalBrowser(getContext(), url); } return true; } @@ -633,13 +620,12 @@ protected final boolean runCommonAction(final @StringRes int action) { } case R.string.abid_common_new_line_below: { // Go to end of line, works with wrapped lines too - _hlEditor.setSelection(TextViewUtils.getLineEnd(_hlEditor.getText(), TextViewUtils.getSelection(_hlEditor)[1])); + _hlEditor.setSelection(TextViewUtils.getLineEnd(text, TextViewUtils.getSelection(_hlEditor)[1])); _hlEditor.simulateKeyPress(KeyEvent.KEYCODE_ENTER); return true; } case R.string.abid_common_delete_lines: { final int[] sel = TextViewUtils.getLineSelection(_hlEditor); - final Editable text = _hlEditor.getText(); final boolean lastLine = sel[1] == text.length(); final boolean firstLine = sel[0] == 0; text.delete(sel[0] - (lastLine && !firstLine ? 1 : 0), sel[1] + (lastLine ? 0 : 1)); @@ -650,15 +636,15 @@ protected final boolean runCommonAction(final @StringRes int action) { return true; } case R.string.abid_common_web_jump_to_table_of_contents: { - m_webView.loadUrl("javascript:document.getElementsByClassName('toc')[0].scrollIntoView();"); + _webView.loadUrl("javascript:document.getElementsByClassName('toc')[0].scrollIntoView();"); return true; } case R.string.abid_common_view_file_in_other_app: { - getCu().viewFileInOtherApp(getContext(), _document.getFile(), GsFileUtils.getMimeType(_document.getFile())); + _cu.viewFileInOtherApp(getContext(), _document.getFile(), GsFileUtils.getMimeType(_document.getFile())); return true; } case R.string.abid_common_rotate_screen: { - getCu().nextScreenRotationSetting(getActivity()); + _cu.nextScreenRotationSetting(_activity); return true; } } @@ -672,7 +658,7 @@ protected final boolean runCommonLongPressAction(@StringRes int action) { switch (action) { case R.string.abid_common_deindent: case R.string.abid_common_indent: { - MarkorDialogFactory.showIndentSizeDialog(getActivity(), _indent, (size) -> { + MarkorDialogFactory.showIndentSizeDialog(_activity, _indent, (size) -> { _indent = Integer.parseInt(size); _appSettings.setDocumentIndentSize(_document.getPath(), _indent); }); @@ -707,6 +693,18 @@ protected final boolean runCommonLongPressAction(@StringRes int action) { } return true; } + case R.string.abid_common_insert_audio: { + AttachLinkOrFileDialog.insertAudioRecording(_activity, _document.getFormat(), _hlEditor.getText(), _document.getFile()); + return true; + } + case R.string.abid_common_insert_link: { + AttachLinkOrFileDialog.insertGalleryPhoto(_activity, _document.getFormat(), _hlEditor.getText(), _document.getFile()); + return true; + } + case R.string.abid_common_insert_image: { + AttachLinkOrFileDialog.insertCameraPhoto(_activity, _document.getFormat(), _hlEditor.getText(), _document.getFile()); + return true; + } } return false; } @@ -879,11 +877,11 @@ public void runJumpBottomTopAction(ActionItem.DisplayMode displayMode) { int pos = _hlEditor.getSelectionStart(); _hlEditor.setSelection(pos == 0 ? _hlEditor.getText().length() : 0); } else if (displayMode == ActionItem.DisplayMode.VIEW) { - boolean top = m_webView.getScrollY() > 100; - m_webView.scrollTo(0, top ? 0 : m_webView.getContentHeight()); + boolean top = _webView.getScrollY() > 100; + _webView.scrollTo(0, top ? 0 : _webView.getContentHeight()); if (!top) { - m_webView.scrollBy(0, 1000); - m_webView.scrollBy(0, 1000); + _webView.scrollBy(0, 1000); + _webView.scrollBy(0, 1000); } } } 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 e8ba5af29..90ddbcedc 100644 --- a/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java +++ b/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java @@ -98,7 +98,14 @@ public TextConverterBase() { * @param webView The WebView content to be shown in * @return Copy of converted html */ - public String convertMarkupShowInWebView(Document document, String content, Activity context, WebView webView, boolean lightMode, boolean lineNum) { + public String convertMarkupShowInWebView( + final Document document, + final String content, + final Activity context, + final WebView webView, + final boolean lightMode, + final boolean lineNum + ) { String html; try { html = convertMarkup(content, context, lightMode, lineNum, document.getFile()); @@ -106,10 +113,12 @@ public String convertMarkupShowInWebView(Document document, String content, Acti html = "Please report at project issue tracker: " + e; } - String baseFolder = document.getFile().getParent(); - if (baseFolder == null) { - baseFolder = "file://" + baseFolder + "/"; + String parent = document.getFile().getParent(); + if (parent == null) { + parent = _appSettings.getNotebookDirectory().getAbsolutePath(); } + final String baseFolder = "file://" + parent + "/"; + webView.loadDataWithBaseURL(baseFolder, html, getContentType(), UTF_CHARSET, null); // When TOKEN_TEXT_CONVERTER_MAX_ZOOM_OUT_BY_DEFAULT is contained in text zoom out as far possible diff --git a/app/src/main/java/net/gsantner/markor/format/asciidoc/AsciidocActionButtons.java b/app/src/main/java/net/gsantner/markor/format/asciidoc/AsciidocActionButtons.java index c989834dc..5d12fee47 100644 --- a/app/src/main/java/net/gsantner/markor/format/asciidoc/AsciidocActionButtons.java +++ b/app/src/main/java/net/gsantner/markor/format/asciidoc/AsciidocActionButtons.java @@ -14,7 +14,6 @@ import net.gsantner.markor.R; import net.gsantner.markor.format.ActionButtonBase; -import net.gsantner.markor.frontend.AttachLinkOrFileDialog; import net.gsantner.markor.frontend.MarkorDialogFactory; import net.gsantner.markor.frontend.textview.TextViewUtils; import net.gsantner.markor.model.Document; @@ -32,131 +31,68 @@ public AsciidocActionButtons(@NonNull Context context, Document document) { @Override public List getActiveActionList() { final ActionItem[] TMA_ACTIONS = { - new ActionItem(R.string.abid_asciidoc_checkbox_list, - R.drawable.ic_check_box_black_24dp, R.string.check_list), - new ActionItem( - R.string.abid_asciidoc_unordered_list_char, R.drawable.ic_list_black_24dp, - R.string.unordered_list), - new ActionItem(R.string.abid_asciidoc_ordered_list_char, - R.drawable.ic_format_list_numbered_black_24dp, R.string.ordered_list), - - new ActionItem(R.string.abid_asciidoc_indent_level, - R.drawable.ic_baseline_keyboard_double_arrow_right_24, - R.string.indent_level), - new ActionItem(R.string.abid_asciidoc_deindent_level, - R.drawable.ic_baseline_keyboard_double_arrow_left_24, - R.string.deindent_level), - - new ActionItem(R.string.abid_common_indent, - R.drawable.ic_format_indent_increase_black_24dp, R.string.indent), - new ActionItem(R.string.abid_common_deindent, - R.drawable.ic_format_indent_decrease_black_24dp, R.string.deindent), - new ActionItem(R.string.abid_asciidoc_squarebrackets, - R.drawable.ic_baseline_data_array_24, R.string.squarebrackets), - new ActionItem(R.string.abid_common_special_key, R.drawable.ic_keyboard_black_24dp, - R.string.special_key), + new ActionItem(R.string.abid_asciidoc_checkbox_list, R.drawable.ic_check_box_black_24dp, R.string.check_list), + new ActionItem(R.string.abid_asciidoc_unordered_list_char, R.drawable.ic_list_black_24dp, R.string.unordered_list), + new ActionItem(R.string.abid_asciidoc_ordered_list_char, R.drawable.ic_format_list_numbered_black_24dp, R.string.ordered_list), + new ActionItem(R.string.abid_asciidoc_indent_level, R.drawable.ic_baseline_keyboard_double_arrow_right_24, R.string.indent_level), + new ActionItem(R.string.abid_asciidoc_deindent_level, R.drawable.ic_baseline_keyboard_double_arrow_left_24, R.string.deindent_level), + new ActionItem(R.string.abid_common_indent, R.drawable.ic_format_indent_increase_black_24dp, R.string.indent), + new ActionItem(R.string.abid_common_deindent, R.drawable.ic_format_indent_decrease_black_24dp, R.string.deindent), + new ActionItem(R.string.abid_asciidoc_squarebrackets, R.drawable.ic_baseline_data_array_24, R.string.squarebrackets), + new ActionItem(R.string.abid_common_special_key, R.drawable.ic_keyboard_black_24dp, R.string.special_key), // similar to abid_common_special_key, but separate menu - new ActionItem(R.string.abid_asciidoc_special_key, - R.drawable.asciidoc_icon_black_24dp, R.string.asciidoc_special_key), - - new ActionItem(R.string.abid_common_insert_snippet, - R.drawable.ic_baseline_file_copy_24, R.string.insert_snippet), - new ActionItem(R.string.abid_asciidoc_h1, R.drawable.format_header_1, - R.string.heading_1), - new ActionItem(R.string.abid_asciidoc_h2, R.drawable.format_header_2, - R.string.heading_2), - new ActionItem(R.string.abid_asciidoc_h3, R.drawable.format_header_3, - R.string.heading_3), -// new ActionItem(R.string.abid_asciidoc_h4, R.drawable.format_header_4, -// R.string.heading_4), -// new ActionItem(R.string.abid_asciidoc_h5, R.drawable.format_header_5, -// R.string.heading_5), - - new ActionItem(R.string.abid_asciidoc_bold, R.drawable.ic_format_bold_black_24dp, - R.string.bold), - new ActionItem(R.string.abid_asciidoc_italic, - R.drawable.ic_format_italic_black_24dp, R.string.italic), - new ActionItem( - R.string.abid_asciidoc_monospace, R.drawable.ic_code_black_24dp, - R.string.inline_code), - new ActionItem(R.string.abid_asciidoc_underline, - R.drawable.ic_format_underlined_black_24dp, R.string.underline), - new ActionItem( - R.string.abid_asciidoc_highlight, R.drawable.ic_highlight_black_24dp, - R.string.highlighted), - new ActionItem(R.string.abid_asciidoc_linethrough, - R.drawable.ic_format_strikethrough_black_24dp, R.string.strikeout), - new ActionItem( - R.string.abid_asciidoc_overline, R.drawable.ic_baseline_format_overline_24, - R.string.inline_code), - new ActionItem(R.string.abid_asciidoc_superscript, R.drawable.ic_baseline_superscript_24, - R.string.inline_code), - new ActionItem(R.string.abid_asciidoc_subscript, - R.drawable.ic_baseline_subscript_24 - , R.string.inline_code), - new ActionItem( - R.string.abid_asciidoc_break_thematic, R.drawable.ic_more_horiz_black_24dp, - R.string.horizontal_line), - new ActionItem(R.string.abid_asciidoc_block_quote, - R.drawable.ic_format_quote_black_24dp, R.string.quote), + new ActionItem(R.string.abid_asciidoc_special_key, R.drawable.asciidoc_icon_black_24dp, R.string.asciidoc_special_key), + new ActionItem(R.string.abid_common_insert_snippet, R.drawable.ic_baseline_file_copy_24, R.string.insert_snippet), + new ActionItem(R.string.abid_asciidoc_h1, R.drawable.format_header_1, R.string.heading_1), + new ActionItem(R.string.abid_asciidoc_h2, R.drawable.format_header_2, R.string.heading_2), + new ActionItem(R.string.abid_asciidoc_h3, R.drawable.format_header_3, R.string.heading_3), + // new ActionItem(R.string.abid_asciidoc_h4, R.drawable.format_header_4, R.string.heading_4), + // new ActionItem(R.string.abid_asciidoc_h5, R.drawable.format_header_5, R.string.heading_5), + new ActionItem(R.string.abid_asciidoc_bold, R.drawable.ic_format_bold_black_24dp, R.string.bold), + new ActionItem(R.string.abid_asciidoc_italic, R.drawable.ic_format_italic_black_24dp, R.string.italic), + new ActionItem(R.string.abid_asciidoc_monospace, R.drawable.ic_code_black_24dp, R.string.inline_code), + new ActionItem(R.string.abid_asciidoc_underline, R.drawable.ic_format_underlined_black_24dp, R.string.underline), + new ActionItem(R.string.abid_asciidoc_highlight, R.drawable.ic_highlight_black_24dp, R.string.highlighted), + new ActionItem(R.string.abid_asciidoc_linethrough, R.drawable.ic_format_strikethrough_black_24dp, R.string.strikeout), + new ActionItem(R.string.abid_asciidoc_overline, R.drawable.ic_baseline_format_overline_24, R.string.inline_code), + new ActionItem(R.string.abid_asciidoc_superscript, R.drawable.ic_baseline_superscript_24, R.string.inline_code), + new ActionItem(R.string.abid_asciidoc_subscript, R.drawable.ic_baseline_subscript_24, R.string.inline_code), + new ActionItem(R.string.abid_asciidoc_break_thematic, R.drawable.ic_more_horiz_black_24dp, R.string.horizontal_line), + new ActionItem(R.string.abid_asciidoc_block_quote, R.drawable.ic_format_quote_black_24dp, R.string.quote), + new ActionItem(R.string.abid_common_insert_image, R.drawable.ic_image_black_24dp, R.string.insert_image), + new ActionItem(R.string.abid_common_insert_link, R.drawable.ic_link_black_24dp, R.string.insert_link), + new ActionItem(R.string.abid_common_insert_audio, R.drawable.ic_keyboard_voice_black_24dp, R.string.audio), // TODO: Implement later -// new ActionItem(R.string.abid_common_web_jump_to_table_of_contents, -// R.drawable.ic_list_black_24dp, R.string.table_of_contents, -// ActionItem.DisplayMode.VIEW), + // new ActionItem(R.string.abid_common_web_jump_to_table_of_contents, R.drawable.ic_list_black_24dp, R.string.table_of_contents, ActionItem.DisplayMode.VIEW), // TODO: Implement Table Generator later - // new ActionItem(R.string.abid_asciidoc_table_insert_columns, R.drawable - // .ic_view_module_black_24dp, R.string.table), - - //similar to other formats: - new ActionItem(R.string.abid_common_delete_lines, R.drawable.ic_delete_black_24dp, - R.string.delete_lines), - new ActionItem( - R.string.abid_common_open_link_browser, - R.drawable.ic_open_in_browser_black_24dp, - R.string.open_link), + // new ActionItem(R.string.abid_asciidoc_table_insert_columns, R.drawable.ic_view_module_black_24dp, R.string.table), + // similar to other formats: + new ActionItem(R.string.abid_common_delete_lines, R.drawable.ic_delete_black_24dp, R.string.delete_lines), + new ActionItem(R.string.abid_common_open_link_browser, R.drawable.ic_open_in_browser_black_24dp, R.string.open_link), // a different icon was used than in other formats, so that you can distinguish // it from opening in the browser - new ActionItem(R.string.abid_common_view_file_in_other_app, - R.drawable.ic_baseline_open_in_new_24, R.string.open_with, - ActionItem.DisplayMode.ANY), - new ActionItem( - R.string.abid_common_attach_something, R.drawable.ic_attach_file_black_24dp, - R.string.attach), - new ActionItem(R.string.abid_common_time, - R.drawable.ic_access_time_black_24dp, R.string.date_and_time), - new ActionItem( - R.string.abid_common_new_line_below, - R.drawable.ic_baseline_keyboard_return_24, - R.string.start_new_line_below), - new ActionItem( - R.string.abid_common_move_text_one_line_up, - R.drawable.ic_baseline_arrow_upward_24, - R.string.move_text_one_line_up), - new ActionItem( - R.string.abid_common_move_text_one_line_down, - R.drawable.ic_baseline_arrow_downward_24, R.string.move_text_one_line_down), - new ActionItem(R.string.abid_common_web_jump_to_very_top_or_bottom, - R.drawable.ic_vertical_align_center_black_24dp, R.string.jump_to_bottom, - ActionItem.DisplayMode.VIEW), - new ActionItem( - R.string.abid_common_rotate_screen, R.drawable.ic_rotate_left_black_24dp, - R.string.rotate, ActionItem.DisplayMode.ANY), - + new ActionItem(R.string.abid_common_view_file_in_other_app, R.drawable.ic_baseline_open_in_new_24, R.string.open_with, ActionItem.DisplayMode.ANY), + new ActionItem(R.string.abid_common_time, R.drawable.ic_access_time_black_24dp, R.string.date_and_time), + new ActionItem(R.string.abid_common_new_line_below, R.drawable.ic_baseline_keyboard_return_24, R.string.start_new_line_below), + new ActionItem(R.string.abid_common_move_text_one_line_up, R.drawable.ic_baseline_arrow_upward_24, R.string.move_text_one_line_up), + new ActionItem(R.string.abid_common_move_text_one_line_down, R.drawable.ic_baseline_arrow_downward_24, R.string.move_text_one_line_down), + new ActionItem(R.string.abid_common_web_jump_to_very_top_or_bottom, R.drawable.ic_vertical_align_center_black_24dp, R.string.jump_to_bottom, ActionItem.DisplayMode.VIEW), + new ActionItem(R.string.abid_common_rotate_screen, R.drawable.ic_rotate_left_black_24dp, R.string.rotate, ActionItem.DisplayMode.ANY), }; return Arrays.asList(TMA_ACTIONS); } -// indent deindent: + // indent deindent: -// implemented two different indent deindent + // implemented two different indent deindent -// a "normal" one for blank characters only -// another one that will change the level of headers and lists -// Pseudologics for the level changer: -// * indent level: the first character is added as a prefix -// * deindent level: the first character is removed, but only if something remains after -// '===' => '== ' => '= ' => '= ' (no further removal) + // a "normal" one for blank characters only + // another one that will change the level of headers and lists + // Pseudologics for the level changer: + // * indent level: the first character is added as a prefix + // * deindent level: the first character is removed, but only if something remains after + // '===' => '== ' => '= ' => '= ' (no further removal) @Override public boolean onActionClick(final @StringRes int action) { @@ -299,14 +235,6 @@ public boolean onActionClick(final @StringRes int action) { runAsciidocInlineAction("<<<\n", "", ""); return true; } - - case R.string.abid_asciidoc_insert_link: - case R.string.abid_asciidoc_insert_image: { - AttachLinkOrFileDialog.showInsertImageOrLinkDialog( - action == R.string.abid_asciidoc_insert_image ? 2 : 3, - _document.getFormat(), getActivity(), _hlEditor, _document.getFile()); - return true; - } //this is an additional extra menu, analogous to special_key menu case R.string.abid_asciidoc_special_key: { runAsciidocSpecialKeyAction(); 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 3405d092a..572d1d207 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 @@ -16,7 +16,6 @@ import net.gsantner.markor.R; import net.gsantner.markor.format.ActionButtonBase; -import net.gsantner.markor.frontend.AttachLinkOrFileDialog; import net.gsantner.markor.frontend.MarkorDialogFactory; import net.gsantner.markor.frontend.textview.AutoTextFormatter; import net.gsantner.markor.frontend.textview.TextViewUtils; @@ -52,7 +51,9 @@ public List getActiveActionList() { new ActionItem(R.string.abid_markdown_italic, R.drawable.ic_format_italic_black_24dp, R.string.italic), new ActionItem(R.string.abid_common_delete_lines, R.drawable.ic_delete_black_24dp, R.string.delete_lines), new ActionItem(R.string.abid_common_open_link_browser, R.drawable.ic_open_in_browser_black_24dp, R.string.open_link), - new ActionItem(R.string.abid_common_attach_something, R.drawable.ic_attach_file_black_24dp, R.string.attach), + new ActionItem(R.string.abid_common_insert_link, R.drawable.ic_link_black_24dp, R.string.insert_link), + new ActionItem(R.string.abid_common_insert_image, R.drawable.ic_image_black_24dp, R.string.insert_image), + new ActionItem(R.string.abid_common_insert_audio, R.drawable.ic_keyboard_voice_black_24dp, R.string.audio), new ActionItem(R.string.abid_common_special_key, R.drawable.ic_keyboard_black_24dp, R.string.special_key), new ActionItem(R.string.abid_common_time, R.drawable.ic_access_time_black_24dp, R.string.date_and_time), new ActionItem(R.string.abid_markdown_code_inline, R.drawable.ic_code_black_24dp, R.string.inline_code), @@ -138,11 +139,6 @@ public boolean onActionClick(final @StringRes int action) { MarkorDialogFactory.showInsertTableRowDialog(getActivity(), false, this::insertTableRow); return true; } - case R.string.abid_markdown_insert_link: - case R.string.abid_markdown_insert_image: { - AttachLinkOrFileDialog.showInsertImageOrLinkDialog(action == R.string.abid_markdown_insert_image ? 2 : 3, _document.getFormat(), getActivity(), _hlEditor, _document.getFile()); - return true; - } default: { return runCommonAction(action); } @@ -153,12 +149,6 @@ public boolean onActionClick(final @StringRes int action) { @Override public boolean onActionLongClick(final @StringRes int action) { switch (action) { - case R.string.abid_markdown_insert_image: { - int pos = _hlEditor.getSelectionStart(); - _hlEditor.getText().insert(pos, ""); - _hlEditor.setSelection(pos + 48); - return true; - } case R.string.abid_markdown_table_insert_columns: { MarkorDialogFactory.showInsertTableRowDialog(getActivity(), true, this::insertTableRow); return true; diff --git a/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtActionButtons.java b/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtActionButtons.java index c6d70829d..ac2a1be18 100644 --- a/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtActionButtons.java +++ b/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtActionButtons.java @@ -26,6 +26,7 @@ import net.gsantner.markor.frontend.textview.TextViewUtils; import net.gsantner.markor.model.Document; import net.gsantner.opoc.util.GsCollectionUtils; +import net.gsantner.opoc.util.GsContextUtils; import net.gsantner.opoc.util.GsFileUtils; import net.gsantner.opoc.wrapper.GsCallback; @@ -204,11 +205,11 @@ public boolean onActionLongClick(final @StringRes int action) { switch (action) { case R.string.abid_todotxt_add_context: { - MarkorDialogFactory.showSttKeySearchDialog(getActivity(), _hlEditor, R.string.browse_by_context, true, true, TextViewUtils.isImeOpen(_hlEditor), TodoTxtFilter.TYPE.CONTEXT); + MarkorDialogFactory.showSttKeySearchDialog(getActivity(), _hlEditor, R.string.browse_by_context, true, true, TodoTxtFilter.TYPE.CONTEXT); return true; } case R.string.abid_todotxt_add_project: { - MarkorDialogFactory.showSttKeySearchDialog(getActivity(), _hlEditor, R.string.browse_by_project, true, true, TextViewUtils.isImeOpen(_hlEditor), TodoTxtFilter.TYPE.PROJECT); + MarkorDialogFactory.showSttKeySearchDialog(getActivity(), _hlEditor, R.string.browse_by_project, true, true, TodoTxtFilter.TYPE.PROJECT); return true; } case R.string.abid_todotxt_sort_todo: { @@ -251,7 +252,7 @@ private void addRemoveItems(final String prefix, final GsCallback.r1 { final TextViewUtils.ChunkedEditable chunk = TextViewUtils.ChunkedEditable.wrap(_hlEditor.getText()); for (final String item : GsCollectionUtils.setDiff(current, updated)) { 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 6ade713c1..05cba2ff4 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 @@ -16,7 +16,6 @@ import net.gsantner.markor.R; import net.gsantner.markor.activity.DocumentActivity; import net.gsantner.markor.format.ActionButtonBase; -import net.gsantner.markor.frontend.AttachLinkOrFileDialog; import net.gsantner.markor.frontend.MarkorDialogFactory; import net.gsantner.markor.frontend.textview.AutoTextFormatter; import net.gsantner.markor.frontend.textview.TextViewUtils; @@ -70,8 +69,8 @@ public List getActiveActionList() { new ActionItem(R.string.abid_common_deindent, R.drawable.ic_format_indent_decrease_black_24dp, R.string.deindent), new ActionItem(R.string.abid_wikitext_h4, R.drawable.format_header_4, R.string.heading_4), new ActionItem(R.string.abid_wikitext_h5, R.drawable.format_header_5, R.string.heading_5), - new ActionItem(R.string.abid_wikitext_insert_image, R.drawable.ic_image_black_24dp, R.string.insert_image), - new ActionItem(R.string.abid_wikitext_insert_link, R.drawable.ic_link_black_24dp, R.string.insert_link), + new ActionItem(R.string.abid_common_insert_image, R.drawable.ic_image_black_24dp, R.string.insert_image), + new ActionItem(R.string.abid_common_insert_link, R.drawable.ic_link_black_24dp, R.string.insert_link), new ActionItem(R.string.abid_common_new_line_below, R.drawable.ic_baseline_keyboard_return_24, R.string.start_new_line_below), new ActionItem(R.string.abid_common_move_text_one_line_up, R.drawable.ic_baseline_arrow_upward_24, R.string.move_text_one_line_up), new ActionItem(R.string.abid_common_move_text_one_line_down, R.drawable.ic_baseline_arrow_downward_24, R.string.move_text_one_line_down), @@ -146,13 +145,6 @@ public boolean onActionClick(final @StringRes int action) { // runInlineAction("----\n"); // return true; // } - case R.string.abid_wikitext_insert_link: - AttachLinkOrFileDialog.showInsertImageOrLinkDialog(AttachLinkOrFileDialog.FILE_OR_LINK_ACTION, _document.getFormat(), getActivity(), _hlEditor, _document.getFile()); - return true; - case R.string.abid_wikitext_insert_image: { - AttachLinkOrFileDialog.showInsertImageOrLinkDialog(AttachLinkOrFileDialog.IMAGE_ACTION, _document.getFormat(), getActivity(), _hlEditor, _document.getFile()); - return true; - } case R.string.abid_common_indent: runRegexReplaceAction(WikitextReplacePatternGenerator.indentOneTab()); runRenumberOrderedListIfRequired(); @@ -179,13 +171,13 @@ public boolean onActionLongClick(final @StringRes int action) { runRegexReplaceAction(WikitextReplacePatternGenerator.removeCheckbox()); return true; } - case R.string.abid_wikitext_insert_link: { + case R.string.abid_common_insert_link: { int pos = _hlEditor.getSelectionStart(); _hlEditor.getText().insert(pos, "[[]]"); _hlEditor.setSelection(pos + 2); return true; } - case R.string.abid_wikitext_insert_image: { + case R.string.abid_common_insert_image: { int pos = _hlEditor.getSelectionStart(); _hlEditor.getText().insert(pos, "{{}}"); _hlEditor.setSelection(pos + 2); diff --git a/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java b/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java index 1dc12db7b..f765ca407 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java @@ -10,17 +10,18 @@ package net.gsantner.markor.frontend; import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.DialogInterface; import android.text.Editable; import android.view.View; -import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import net.gsantner.markor.ApplicationObject; @@ -28,27 +29,59 @@ import net.gsantner.markor.format.FormatRegistry; import net.gsantner.markor.format.markdown.MarkdownSyntaxHighlighter; import net.gsantner.markor.frontend.filebrowser.MarkorFileBrowserFactory; -import net.gsantner.markor.frontend.textview.HighlightingEditor; +import net.gsantner.markor.frontend.textview.TextViewUtils; import net.gsantner.markor.model.AppSettings; import net.gsantner.markor.util.MarkorContextUtils; import net.gsantner.opoc.frontend.GsAudioRecordOmDialog; import net.gsantner.opoc.frontend.filebrowser.GsFileBrowserOptions; import net.gsantner.opoc.util.GsFileUtils; import net.gsantner.opoc.wrapper.GsCallback; -import net.gsantner.opoc.wrapper.GsHashMap; import java.io.File; import java.util.regex.Matcher; -@SuppressWarnings({"UnusedReturnValue", "WeakerAccess"}) public class AttachLinkOrFileDialog { public final static int IMAGE_ACTION = 2, FILE_OR_LINK_ACTION = 3, AUDIO_ACTION = 4; - @SuppressWarnings("RedundantCast") - public static Dialog showInsertImageOrLinkDialog(final int action, final int textFormatId, final Activity activity, final HighlightingEditor _hlEditor, final File currentWorkingFile) { - final AppSettings _appSettings = ApplicationObject.settings(); + private static String getImageFormat(final int textFormatId) { + if (textFormatId == FormatRegistry.FORMAT_MARKDOWN) { + return "![TITLE](LINK)"; + } else if (textFormatId == FormatRegistry.FORMAT_WIKITEXT) { + return "{{LINK}}"; + } else if (textFormatId == FormatRegistry.FORMAT_ASCIIDOC) { + return "image::LINK[\"TITLE\"]"; + } else { + return "TITLE"; + } + } + + private static String getLinkFormat(final int textFormatId) { + if (textFormatId == FormatRegistry.FORMAT_MARKDOWN) { + return "[TITLE](LINK)"; + } else if (textFormatId == FormatRegistry.FORMAT_WIKITEXT) { + return "{{LINK|TITLE}}"; + } else if (textFormatId == FormatRegistry.FORMAT_ASCIIDOC) { + return "link:LINK[TITLE]"; + } else{ + return "TITLE"; + } + } + + private static String getAudioFormat(final int textFormatId) { + return ""; + } + + public static void showInsertImageOrLinkDialog( + final int action, + final int textFormatId, + final Activity activity, + final Editable edit, + final File currentFile + ) { + final int[] sel = TextViewUtils.getSelection(edit); + final androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(activity); - final View view = activity.getLayoutInflater().inflate(R.layout.select_path_dialog, (ViewGroup) null); + final View view = activity.getLayoutInflater().inflate(R.layout.select_path_dialog, null); final EditText inputPathName = view.findViewById(R.id.ui__select_path_dialog__name); final EditText inputPathUrl = view.findViewById(R.id.ui__select_path_dialog__url); final Button buttonBrowseFilesystem = view.findViewById(R.id.ui__select_path_dialog__browse_filesystem); @@ -57,211 +90,224 @@ public static Dialog showInsertImageOrLinkDialog(final int action, final int tex final Button buttonPictureEdit = view.findViewById(R.id.ui__select_path_dialog__edit_picture); final Button buttonAudioRecord = view.findViewById(R.id.ui__select_path_dialog__record_audio); - final int startCursorPos = _hlEditor.getSelectionStart(); - buttonAudioRecord.setVisibility(action == AUDIO_ACTION ? View.VISIBLE : View.GONE); - buttonPictureCamera.setVisibility(action == IMAGE_ACTION ? View.VISIBLE : View.GONE); - buttonPictureGallery.setVisibility(action == IMAGE_ACTION ? View.VISIBLE : View.GONE); - buttonPictureEdit.setVisibility(action == IMAGE_ACTION ? View.VISIBLE : View.GONE); final int actionTitle; - final String formatTemplate; - switch (action) { - default: - case FILE_OR_LINK_ACTION: { - actionTitle = R.string.insert_link; - formatTemplate = new GsHashMap().load( - FormatRegistry.FORMAT_MARKDOWN, "[{{ template.title }}]({{ template.link }})", - FormatRegistry.FORMAT_ASCIIDOC, "link:{{ template.link }}[{{ template.title }}]", - FormatRegistry.FORMAT_WIKITEXT, "[[{{ template.link }}|{{ template.title }}]]" - ).getOrDefault(textFormatId, "{{ template.title }}"); - break; - } - case IMAGE_ACTION: { - actionTitle = R.string.insert_image; - formatTemplate = new GsHashMap().load( - FormatRegistry.FORMAT_MARKDOWN, "![{{ template.title }}]({{ template.link }})", - FormatRegistry.FORMAT_ASCIIDOC, "image::{{ template.link }}[\"{{ template.title }}\"]", - FormatRegistry.FORMAT_WIKITEXT, "{{{{ template.link }}}}" - ).getOrDefault(textFormatId, "{{ template.title }}"); - break; - } - case AUDIO_ACTION: { - formatTemplate = ""; - actionTitle = R.string.audio; - break; - } - + if (action == IMAGE_ACTION) { + buttonPictureCamera.setVisibility(View.VISIBLE); + buttonPictureGallery.setVisibility(View.VISIBLE); + buttonPictureEdit.setVisibility(View.VISIBLE); + actionTitle = R.string.insert_image; + } else if (action == AUDIO_ACTION) { + actionTitle = R.string.audio; + buttonAudioRecord.setVisibility(View.VISIBLE); + } else { + actionTitle = R.string.insert_link; } // Extract filepath if using Markdown if (textFormatId == FormatRegistry.FORMAT_MARKDOWN) { - if (_hlEditor.hasSelection()) { - String selected_text = ""; - try { - selected_text = _hlEditor.getText().subSequence(_hlEditor.getSelectionStart(), _hlEditor.getSelectionEnd()).toString(); - } catch (Exception ignored) { - } - inputPathName.setText(selected_text); - } else if (_hlEditor.getText().toString().isEmpty()) { + if (sel[0] > 0 && sel[1] > 0 && sel[0] != sel[1]) { + inputPathName.setText(edit.subSequence(sel[0], sel[1])); + } else if (edit.length() == 0) { inputPathName.setText(""); } else { - final Editable contentText = _hlEditor.getText(); - int lineStartidx = Math.max(startCursorPos, 0); - int lineEndidx = Math.min(startCursorPos, contentText.length() - 1); - lineStartidx = Math.min(lineEndidx, lineStartidx); - for (; lineStartidx > 0; lineStartidx--) { - if (contentText.charAt(lineStartidx) == '\n') { - break; - } - } - for (; lineEndidx < contentText.length(); lineEndidx++) { - if (contentText.charAt(lineEndidx) == '\n') { - break; - } + final int[] lineSel = TextViewUtils.getLineSelection(edit, sel); + final String line = edit.subSequence(lineSel[0], lineSel[1]).toString(); + final Matcher m; + if (action == IMAGE_ACTION) { + m = MarkdownSyntaxHighlighter.ACTION_IMAGE_PATTERN.matcher(line); + } else if (action == FILE_OR_LINK_ACTION) { + m = MarkdownSyntaxHighlighter.ACTION_LINK_PATTERN.matcher(line); + } else { + m = null; } - - final String line = contentText.subSequence(lineStartidx, lineEndidx).toString(); - Matcher m = (action == FILE_OR_LINK_ACTION ? MarkdownSyntaxHighlighter.ACTION_LINK_PATTERN : MarkdownSyntaxHighlighter.ACTION_IMAGE_PATTERN).matcher(line); - if (m.find() && startCursorPos > lineStartidx + m.start() && startCursorPos < m.end() + lineStartidx) { - int stat = lineStartidx + m.start(); - int en = lineStartidx + m.end(); - _hlEditor.setSelection(stat, en); + if (m != null && m.find()) { inputPathName.setText(m.group(1)); inputPathUrl.setText((m.group(2))); + sel[0] = m.start() + lineSel[0]; + sel[1] = m.end() + lineSel[0]; } } } - // Inserts path relative if inside savedir, else absolute. asks to copy file if not in savedir - final GsFileBrowserOptions.SelectionListener fsListener = new GsFileBrowserOptions.SelectionListenerAdapter() { - @Override - public void onFsViewerSelected(final String request, final File file, final Integer lineNumber) { - final File saveDir = _appSettings.getNotebookDirectory(); - String text = null; - boolean isInSaveDir = GsFileUtils.isChild(saveDir, file) && GsFileUtils.isChild(saveDir, currentWorkingFile); - boolean isInCurrentDir = currentWorkingFile.getAbsolutePath().startsWith(file.getParentFile().getAbsolutePath()); - if (isInCurrentDir || isInSaveDir) { - text = GsFileUtils.relativePath(currentWorkingFile, file); - } else if ("abs_if_not_relative".equals(request)) { - text = file.getAbsolutePath(); - } else { - String filename = file.getName(); - if ("audio_record_om_dialog".equals(request)) { - filename = GsAudioRecordOmDialog.generateFilename(file).getName(); - } - File targetCopy = new File(currentWorkingFile.getParentFile(), filename); - showCopyFileToDirDialog(activity, file, targetCopy, false, (cbRetValSuccess, cbRestValTargetFile) -> onFsViewerSelected("abs_if_not_relative", cbRestValTargetFile, null)); - } - if (text == null) { - text = file.getAbsolutePath(); - } + final AlertDialog dialog = builder.setView(view).setTitle(actionTitle).create(); - inputPathUrl.setText(text); + // Helper func + final GsCallback.a1 _insertItem = (type) -> insertItem(type, textFormatId, activity, edit, currentFile, dialog); - if (inputPathName.getText().toString().isEmpty()) { - text = file.getName(); - text = text.contains(".") ? text.substring(0, text.lastIndexOf('.')) : text; - inputPathName.setText(text); - } - text = inputPathUrl.getText().toString(); - try { - if (text.startsWith("../assets/") && currentWorkingFile.getParentFile().getName().equals("_posts")) { - text = "{{ site.baseurl }}" + text.substring(2); - inputPathUrl.setText(text); - } - } catch (Exception ignored) { - } + // Done after dialog creation as we need a ref to dialog + final String ok = activity.getString(android.R.string.ok); + dialog.setButton(DialogInterface.BUTTON_POSITIVE, ok, (di, b) -> _insertItem.callback(TYPE_DIALOG_TEXT)); + dialog.show(); + + buttonPictureCamera.setOnClickListener(b -> _insertItem.callback(TYPE_CAMERA_PHOTO)); + buttonPictureGallery.setOnClickListener(v -> _insertItem.callback(TYPE_GALERY_IMAGE)); + buttonBrowseFilesystem.setOnClickListener(v -> _insertItem.callback(TYPE_FILE_LINK)); + buttonAudioRecord.setOnClickListener(v -> _insertItem.callback(TYPE_AUDIO_RECORDING)); + buttonPictureEdit.setOnClickListener(v -> _insertItem.callback(TYPE_EDIT_PICTURE)); + } + + private static final int TYPE_CAMERA_PHOTO = 0; + private static final int TYPE_AUDIO_RECORDING = 1; + private static final int TYPE_FILE_LINK = 2; + private static final int TYPE_GALERY_IMAGE = 3; + private static final int TYPE_EDIT_PICTURE = 4; + private static final int TYPE_DIALOG_TEXT = 5; + + private static void insertItem( + final int itemType, + final int textFormatId, + final Activity activity, + final Editable text, + final File currentFile, + @Nullable AlertDialog dialog + ) { + final int[] sel = TextViewUtils.getSelection(text); + + final AppSettings _appSettings = ApplicationObject.settings(); + final File attachmentDir = _appSettings.getAttachmentFolder(currentFile); + + final String template; + final GsCallback.b2 fileFilter; + if (itemType == TYPE_CAMERA_PHOTO || itemType == TYPE_GALERY_IMAGE) { + template = getImageFormat(textFormatId); + fileFilter = MarkorFileBrowserFactory.IsMimeImage; + } else if (itemType == TYPE_AUDIO_RECORDING) { + template = getAudioFormat(textFormatId); + fileFilter = MarkorFileBrowserFactory.IsMimeAudio; + } else { + template = getLinkFormat(textFormatId); + fileFilter = null; + } + + // Source, dest to be written when the user hits accept + final GsCallback.a2 insertLink = (title, path) -> { + if (TextViewUtils.isNullOrEmpty(path)) { + return; } - @Override - public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) { - if (currentWorkingFile != null) { - dopt.rootFolder = currentWorkingFile.getParentFile(); - } + String newText = template.replace("TITLE", title).replace("LINK", path); + + if (textFormatId == FormatRegistry.FORMAT_WIKITEXT && newText.endsWith("|]]")) { + newText = newText.replaceFirst("\\|]]$", "]]"); + } + + if (!text.subSequence(sel[0], sel[1]).equals(newText)) { + text.replace(sel[0], sel[1], newText); } + + if (dialog != null) { + dialog.dismiss(); + } + }; + + final GsCallback.a1 insertFileLink = (file) -> { + // If path is not under notebook, copy it to the res folder + if (!GsFileUtils.isChild(_appSettings.getNotebookDirectory(), file)) { + final File local = GsFileUtils.findNonConflictingDest(attachmentDir, file.getName()); + attachmentDir.mkdirs(); + GsFileUtils.copyFile(file, local); + file = local; + } + final String title = GsFileUtils.getFilenameWithoutExtension(file); + final String path = GsFileUtils.relativePath(currentFile, file); + insertLink.callback(title, path); }; - // Request camera / gallery picture button handling final MarkorContextUtils shu = new MarkorContextUtils(activity); - final BroadcastReceiver lbr = shu.receiveResultFromLocalBroadcast(activity, (intent, lbr_ref) -> { - fsListener.onFsViewerSelected("pic", new File(intent.getStringExtra(MarkorContextUtils.EXTRA_FILEPATH)), null); - }, - false, MarkorContextUtils.REQUEST_CAMERA_PICTURE + "", MarkorContextUtils.REQUEST_PICK_PICTURE + ""); - final File targetFolder = currentWorkingFile != null ? currentWorkingFile.getParentFile() : _appSettings.getNotebookDirectory(); - buttonPictureCamera.setOnClickListener(button -> shu.requestCameraPicture(activity, targetFolder)); - buttonPictureGallery.setOnClickListener(button -> shu.requestGalleryPicture(activity)); - - buttonBrowseFilesystem.setOnClickListener(button -> { - if (activity instanceof AppCompatActivity) { - AppCompatActivity a = (AppCompatActivity) activity; - GsCallback.b2 f = action == AUDIO_ACTION ? MarkorFileBrowserFactory.IsMimeAudio : (action == FILE_OR_LINK_ACTION ? null : MarkorFileBrowserFactory.IsMimeImage); - MarkorFileBrowserFactory.showFileDialog(fsListener, a.getSupportFragmentManager(), activity, f); + final BroadcastReceiver br = shu.receiveResultFromLocalBroadcast( + activity, + (intent, _br) -> insertFileLink.callback(new File(intent.getStringExtra(MarkorContextUtils.EXTRA_FILEPATH))), + true, + "" + MarkorContextUtils.REQUEST_CAMERA_PICTURE, + "" + MarkorContextUtils.REQUEST_PICK_PICTURE, + "" + MarkorContextUtils.REQUEST_RECORD_AUDIO + ); + + final EditText nameEdit, pathEdit; + if (dialog != null) { + nameEdit = dialog.findViewById(R.id.ui__select_path_dialog__name); + pathEdit = dialog.findViewById(R.id.ui__select_path_dialog__url); + dialog.setOnDismissListener(d -> LocalBroadcastManager.getInstance(activity).unregisterReceiver(br)); + } else { + nameEdit = pathEdit = null; + } + + // Perform the requested action + if (itemType == TYPE_CAMERA_PHOTO) { + shu.requestCameraPicture(activity); + } else if (itemType == TYPE_GALERY_IMAGE) { + shu.requestGalleryPicture(activity); + } else if (itemType == TYPE_AUDIO_RECORDING) { + if (!shu.requestAudioRecording(activity)) { + GsAudioRecordOmDialog.showAudioRecordDialog(activity, R.string.record_audio, insertFileLink); } - }); + } else if (itemType == TYPE_FILE_LINK && activity instanceof AppCompatActivity && nameEdit != null && pathEdit != null) { + final GsFileBrowserOptions.SelectionListener fsListener = new GsFileBrowserOptions.SelectionListenerAdapter() { + @Override + public void onFsViewerSelected(final String request, final File file, final Integer lineNumber) { + pathEdit.setText(GsFileUtils.relativePath(currentFile, file)); - // Audio Record -> fs listener with arg file,"audio_record" - buttonAudioRecord.setOnClickListener(v -> GsAudioRecordOmDialog.showAudioRecordDialog(activity, R.string.record_audio, cbValAudioRecordFilepath -> fsListener.onFsViewerSelected("audio_record_om_dialog", cbValAudioRecordFilepath, null))); + if (TextViewUtils.isNullOrEmpty(nameEdit.getText())) { + nameEdit.setText(GsFileUtils.getFilenameWithoutExtension(file)); + } + } + + @Override + public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) { + dopt.rootFolder = currentFile.getParentFile(); + } + }; + + final FragmentManager f = ((AppCompatActivity) activity).getSupportFragmentManager(); + MarkorFileBrowserFactory.showFileDialog(fsListener, f, activity, fileFilter); - buttonPictureEdit.setOnClickListener(v -> { - String filepath = inputPathUrl.getText().toString().replace("%20", " "); + } else if (itemType == TYPE_EDIT_PICTURE && pathEdit != null) { + + String filepath = pathEdit.getText().toString().replace("%20", " "); if (!filepath.startsWith("/")) { - filepath = new File(currentWorkingFile.getParent(), filepath).getAbsolutePath(); + filepath = new File(currentFile.getParent(), filepath).getAbsolutePath(); } File file = new File(filepath); if (file.exists() && file.isFile()) { shu.requestPictureEdit(activity, file); } - }); - - builder.setView(view) - .setTitle(actionTitle) - .setOnDismissListener(dialog -> LocalBroadcastManager.getInstance(activity).unregisterReceiver(lbr)) - .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { - if (_hlEditor.hasSelection()) { - _hlEditor.setSelection(startCursorPos); - } - }) - .setPositiveButton(android.R.string.ok, (dialog, id) -> { - try { - String title = inputPathName.getText().toString().replace(")", "\\)"); - String url = inputPathUrl.getText().toString().trim().replace(")", "\\)").replace(" ", "%20"); // Workaround for parser - cannot deal with spaces and have other entities problems - url = url.replace("{{%20site.baseurl%20}}", "{{ site.baseurl }}"); // Disable space encoding for Jekyll - String newText = formatTemplate.replace("{{ template.title }}", title).replace("{{ template.link }}", url); - if (textFormatId == FormatRegistry.FORMAT_WIKITEXT && newText.endsWith("|]]")) { - newText = newText.replaceFirst("\\|]]$", "]]"); - } - if (_hlEditor.hasSelection()) { - _hlEditor.getText().replace(_hlEditor.getSelectionStart(), _hlEditor.getSelectionEnd(), newText); - _hlEditor.setSelection(_hlEditor.getSelectionStart()); - } else { - _hlEditor.getText().insert(_hlEditor.getSelectionStart(), newText); - } - } catch (Exception ignored) { - } - }); - return builder.show(); - } + } else if (itemType == TYPE_DIALOG_TEXT && nameEdit != null && pathEdit != null) { - public static Dialog showCopyFileToDirDialog(final Activity activity, final File srcFile, final File tarFile, boolean disableCancel, final GsCallback.a2 copyFileFinishedCallback) { - final GsCallback.a1 copyToDirInvocation = cbValTargetFile -> new MarkorContextUtils(activity).writeFile(activity, cbValTargetFile, false, (wfCbValOpened, wfCbValStream) -> { - if (wfCbValOpened && GsFileUtils.copyFile(srcFile, wfCbValStream)) { - copyFileFinishedCallback.callback(true, cbValTargetFile); - } - }); + final String title = nameEdit.getText().toString().trim().replace(")", "\\)"); + final String link = pathEdit.getText().toString().trim() + .replace(")", "\\)") + .replace(" ", "%20") // Workaround for parser - cannot deal with spaces and have other entities problems + .replace("{{%20site.baseurl%20}}", "{{ site.baseurl }}"); // Disable space encoding for Jekyll + + insertLink.callback(title, link); + } + } - final File tarFileInAssetsDir = new File(ApplicationObject.settings().getNotebookDirectory(), tarFile.getName()); + public static void insertCameraPhoto( + final Activity activity, + final int textFormatId, + final Editable text, + final File currentFile + ) { + insertItem(TYPE_CAMERA_PHOTO, textFormatId, activity, text, currentFile, null); + } + public static void insertGalleryPhoto( + final Activity activity, + final int textFormatId, + final Editable text, + final File currentFile + ) { + insertItem(TYPE_GALERY_IMAGE, textFormatId, activity, text, currentFile, null); + } - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity) - .setTitle(R.string.copy_file) - .setMessage(R.string.file_not_in_current_folder_do_copy__appspecific) - .setPositiveButton(R.string.current, (dialogInterface, which) -> copyToDirInvocation.callback(tarFile)) - .setNeutralButton(R.string.notebook, (dialogInterface, which) -> copyToDirInvocation.callback(tarFileInAssetsDir)); - if (disableCancel) { - dialogBuilder.setCancelable(false); - } else { - dialogBuilder.setNegativeButton(android.R.string.no, null); - } - return dialogBuilder.show(); + public static void insertAudioRecording( + final Activity activity, + final int textFormatId, + final Editable text, + final File currentFile + ) { + insertItem(TYPE_AUDIO_RECORDING, textFormatId, activity, text, currentFile, null); } } 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 927fa2198..60646ec27 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java +++ b/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java @@ -26,13 +26,11 @@ import android.text.TextUtils; import android.util.Pair; import android.view.Gravity; -import android.view.Window; import android.view.WindowManager; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; -import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; @@ -102,35 +100,6 @@ public static void showAsciidocSpecialKeyDialog(Activity activity, GsCallback.a1 GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } - public static void showAttachSomethingDialog(final Activity activity, final GsCallback.a1 userCallback) { - final List availableData = new ArrayList<>(); - final List availableDataToActionMap = new ArrayList<>(); - final List availableDataToIconMap = new ArrayList<>(); - final GsCallback.a3 addToList = (strRes, actionRes, iconRes) -> { - availableData.add(activity.getString(strRes)); - availableDataToActionMap.add(actionRes); - availableDataToIconMap.add(iconRes); - }; - addToList.callback(R.string.color, R.id.action_attach_color, R.drawable.ic_format_color_fill_black_24dp); - addToList.callback(R.string.insert_link, R.id.action_attach_link, R.drawable.ic_link_black_24dp); - addToList.callback(R.string.file, R.id.action_attach_file, R.drawable.ic_attach_file_black_24dp); - addToList.callback(R.string.image, R.id.action_attach_image, R.drawable.ic_image_black_24dp); - addToList.callback(R.string.audio, R.id.action_attach_audio, R.drawable.ic_keyboard_voice_black_24dp); - addToList.callback(R.string.date, R.id.action_attach_date, R.drawable.ic_access_time_black_24dp); - - DialogOptions dopt = new DialogOptions(); - baseConf(activity, dopt); - dopt.callback = str -> userCallback.callback(availableDataToActionMap.get(availableData.indexOf(str))); - dopt.data = availableData; - dopt.iconsForData = availableDataToIconMap; - dopt.isSearchEnabled = false; - dopt.okButtonText = 0; - dopt.titleText = 0; - dopt.dialogWidthDp = WindowManager.LayoutParams.WRAP_CONTENT; - dopt.gravity = Gravity.BOTTOM | Gravity.END; - GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); - } - public static void showInsertTableRowDialog(final Activity activity, final boolean isHeader, GsCallback.a2 callback) { final DialogOptions dopt = new DialogOptions(); final String PREF_LAST_USED_TABLE_SIZE = "pref_key_last_used_table_size"; @@ -251,7 +220,6 @@ public static void showSttSortDialogue(Activity activity, final GsCallback.a2 options = new ArrayList<>(); final List icons = new ArrayList<>(); final List callbacks = new ArrayList<>(); options.add(activity.getString(R.string.priority)); icons.add(R.drawable.ic_star_black_24dp); - callbacks.add(() -> showSttKeySearchDialog(activity, text, R.string.browse_by_priority, false, false, showIme, TodoTxtFilter.TYPE.PRIORITY)); + callbacks.add(() -> showSttKeySearchDialog(activity, text, R.string.browse_by_priority, false, false, TodoTxtFilter.TYPE.PRIORITY)); options.add(activity.getString(R.string.due_date)); icons.add(R.drawable.ic_date_range_black_24dp); - callbacks.add(() -> showSttKeySearchDialog(activity, text, R.string.browse_by_due_date, false, false, showIme, TodoTxtFilter.TYPE.DUE)); + callbacks.add(() -> showSttKeySearchDialog(activity, text, R.string.browse_by_due_date, false, false, TodoTxtFilter.TYPE.DUE)); options.add(activity.getString(R.string.project)); icons.add(R.drawable.ic_new_label_black_24dp); - callbacks.add(() -> showSttKeySearchDialog(activity, text, R.string.browse_by_project, true, true, showIme, TodoTxtFilter.TYPE.PROJECT)); + callbacks.add(() -> showSttKeySearchDialog(activity, text, R.string.browse_by_project, true, true, TodoTxtFilter.TYPE.PROJECT)); options.add(activity.getString(R.string.context)); icons.add(R.drawable.gs_email_sign_black_24dp); - callbacks.add(() -> showSttKeySearchDialog(activity, text, R.string.browse_by_context, true, true, showIme, TodoTxtFilter.TYPE.CONTEXT)); + callbacks.add(() -> showSttKeySearchDialog(activity, text, R.string.browse_by_context, true, true, TodoTxtFilter.TYPE.CONTEXT)); options.add(activity.getString(R.string.advanced_filtering)); icons.add(R.drawable.ic_extension_black_24dp); @@ -299,7 +265,6 @@ public static void showSttFilteringDialog(final Activity activity, final EditTex return TodoTxtFilter.isMatchQuery(new TodoTxtTask(line), query); }; addSaveQuery(activity, dopt2, () -> queryHolder[0]); - addRestoreKeyboard(activity, dopt2, text, showIme); GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt2); }); @@ -357,7 +322,6 @@ public static void showSttFilteringDialog(final Activity activity, final EditTex * @param title Dialog title * @param enableSearch Whether key search is enabled * @param enableAnd Whether 'and' keys makes sense / is enabled - * @param showIme Whether to show IME when done (if == true) * @param queryType Key used with TodoTxtFilter */ public static void showSttKeySearchDialog( @@ -366,7 +330,6 @@ public static void showSttKeySearchDialog( final int title, final boolean enableSearch, final boolean enableAnd, - final Boolean showIme, final TodoTxtFilter.TYPE queryType ) { @@ -413,7 +376,6 @@ public static void showSttKeySearchDialog( dopt.isSearchEnabled = enableSearch; dopt.searchHintText = R.string.search; dopt.isMultiSelectEnabled = true; - addRestoreKeyboard(activity, dopt, text, showIme); // Callback to actually show tasks // ------------------------------------- @@ -429,7 +391,6 @@ public static void showSttKeySearchDialog( final DialogOptions doptSel = makeSttLineSelectionDialog(activity, text, t -> TodoTxtFilter.isMatchQuery(t, query)); setQueryTitle(doptSel, activity.getString(title), query); addSaveQuery(activity, doptSel, () -> query); - addRestoreKeyboard(activity, doptSel, text, showIme); GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, doptSel); }; @@ -513,28 +474,9 @@ public static void showSttSearchDialog(final Activity activity, final EditText t dialog.dismiss(); SearchAndReplaceTextDialog.showSearchReplaceDialog(activity, text.getText(), TextViewUtils.getSelection(text)); }; - addRestoreKeyboard(activity, dopt, text); GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } - // Restore the keyboard when dialog closes if keyboard is open when dialog was called - private static void addRestoreKeyboard(final Activity activity, final DialogOptions options, final EditText edit) { - addRestoreKeyboard(activity, options, edit, TextViewUtils.isImeOpen(edit)); - } - - // Restore the keyboard when dialog closes if required - // NOTE: This relies on the activity having unchanged as the default soft input mode - private static void addRestoreKeyboard(final Activity activity, final DialogOptions options, final EditText edit, final Boolean restore) { - if (restore != null && !restore) { - options.dismissCallback = (d) -> { - final Window w = activity.getWindow(); - // Suppress opening the keyboard automatically again - w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); - edit.postDelayed(() -> w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED), 1000); - }; - } - } - /** * Allow to choose between Hexcolor / foreground / background color, pass back stringid */ @@ -569,7 +511,6 @@ public static void showInsertItemsDialog( final Activity activity, final @StringRes int title, final List data, - final @Nullable EditText text, // Passed in here for keyboard restore final GsCallback.a1 insertCallback ) { GsSearchOrCustomTextDialog.DialogOptions dopt = new GsSearchOrCustomTextDialog.DialogOptions(); @@ -584,9 +525,6 @@ public static void showInsertItemsDialog( insertCallback.callback(dopt.data.get(pi).toString()); } }; - if (text != null) { - addRestoreKeyboard(activity, dopt, text); - } GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } @@ -596,7 +534,6 @@ public static void showUpdateItemsDialog( final @StringRes int title, final Set allKeys, final Set currentKeys, - final @Nullable EditText text, // Passed in here for keyboard restore final GsCallback.a1> callback ) { GsSearchOrCustomTextDialog.DialogOptions dopt = new GsSearchOrCustomTextDialog.DialogOptions(); @@ -611,10 +548,6 @@ public static void showUpdateItemsDialog( dopt.positionCallback = (newSel) -> callback.callback( GsCollectionUtils.map(newSel, pi -> dopt.data.get(pi).toString())); - if (text != null) { - addRestoreKeyboard(activity, dopt, text); - } - GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } @@ -639,7 +572,6 @@ public static void showSearchDialog(final Activity activity, final EditText text }; dopt.neutralButtonText = R.string.search_and_replace; dopt.positionCallback = (result) -> TextViewUtils.selectLines(text, result); - addRestoreKeyboard(activity, dopt, text); GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } @@ -716,7 +648,6 @@ public static void showHeadlineDialog( GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt2); }; dopt.gravity = Gravity.TOP; - addRestoreKeyboard(activity, dopt, edit); GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } @@ -852,12 +783,11 @@ public static Map getSnippets(final AppSettings as) { return texts; } - public static void showInsertSnippetDialog(final Activity activity, @Nullable final EditText edit, final GsCallback.a1 callback) { + public static void showInsertSnippetDialog(final Activity activity, final GsCallback.a1 callback) { final DialogOptions dopt = new DialogOptions(); baseConf(activity, dopt); final Map texts = getSnippets(as()); - final Boolean showIme = edit != null ? TextViewUtils.isImeOpen(edit) : null; final List data = new ArrayList<>(texts.keySet()); dopt.data = data; @@ -865,7 +795,6 @@ public static void showInsertSnippetDialog(final Activity activity, @Nullable fi dopt.titleText = R.string.insert_snippet; dopt.messageText = Html.fromHtml("" + as().getSnippetsFolder().getAbsolutePath() + ""); dopt.positionCallback = (ind) -> callback.callback(GsFileUtils.readTextFileFast(texts.get(data.get(ind.get(0)))).first); - addRestoreKeyboard(activity, dopt, edit, showIme); GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } diff --git a/app/src/main/java/net/gsantner/markor/model/AppSettings.java b/app/src/main/java/net/gsantner/markor/model/AppSettings.java index 3670000ba..17b2bc895 100644 --- a/app/src/main/java/net/gsantner/markor/model/AppSettings.java +++ b/app/src/main/java/net/gsantner/markor/model/AppSettings.java @@ -16,12 +16,14 @@ import androidx.annotation.ColorRes; import androidx.annotation.IdRes; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import net.gsantner.markor.BuildConfig; import net.gsantner.markor.R; import net.gsantner.markor.format.FormatRegistry; +import net.gsantner.markor.frontend.textview.TextViewUtils; import net.gsantner.markor.util.MarkorContextUtils; import net.gsantner.markor.util.ShortcutUtils; import net.gsantner.opoc.format.GsTextUtils; @@ -926,4 +928,13 @@ public boolean isOpenLinksWithChromeCustomTabs() { public String getShareIntoPrefix() { return getString(R.string.pref_key__share_into_format, "\\n----\\n{{text}}"); } + + public @NonNull File getAttachmentFolder(final File file) { + final File parent = file.getParentFile(); + if (parent == null) { + return getNotebookDirectory(); + } + final String child = getString(R.string.pref_key__attachment_folder_name, "_res").trim(); + return TextViewUtils.isNullOrEmpty(child) ? parent : new File(parent, child); + } } diff --git a/app/src/main/java/net/gsantner/opoc/frontend/GsAudioRecordOmDialog.java b/app/src/main/java/net/gsantner/opoc/frontend/GsAudioRecordOmDialog.java index cfae05ea3..76d1b6ca4 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/GsAudioRecordOmDialog.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/GsAudioRecordOmDialog.java @@ -79,7 +79,7 @@ public static void showAudioRecordDialog(final Activity activity, @StringRes fin final AtomicReference mediaPlayer = new AtomicReference<>(); final AtomicReference dialog = new AtomicReference<>(); final AtomicReference startTime = new AtomicReference<>(); - final File TMP_FILE_RECORDING = new File(activity.getCacheDir(), "recording.wav"); + final File TMP_FILE_RECORDING = generateFilename(activity.getCacheDir()); if (TMP_FILE_RECORDING.exists()) { TMP_FILE_RECORDING.delete(); } @@ -169,9 +169,9 @@ public static void showAudioRecordDialog(final Activity activity, @StringRes fin //////////////////////////////////// // Callback for OK & Cancel dialog button - DialogInterface.OnClickListener dialogOkAndCancelListener = (dialogInterface, dialogButtonCase) -> { + final DialogInterface.OnClickListener dialogOkAndCancelListener = (dialogInterface, dialogButtonCase) -> { final boolean isSavePressed = (dialogButtonCase == DialogInterface.BUTTON_POSITIVE); - if (isRecordSavedOnce.get()) { + if (isRecording.get() || isRecordSavedOnce.get()) { try { recorder.get().stopRecording(); } catch (Exception ignored) { @@ -219,7 +219,7 @@ public static void showAudioRecordDialog(final Activity activity, @StringRes fin } } - public static File generateFilename(File recordDirectory) { + public static File generateFilename(final File recordDirectory) { final String datestr = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.ENGLISH).format(new Date()); return new File(recordDirectory, datestr + "-record.wav"); } diff --git a/app/src/main/java/net/gsantner/opoc/util/GsCollectionUtils.java b/app/src/main/java/net/gsantner/opoc/util/GsCollectionUtils.java index a08706f23..887d947b2 100644 --- a/app/src/main/java/net/gsantner/opoc/util/GsCollectionUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/GsCollectionUtils.java @@ -11,6 +11,8 @@ import android.util.Pair; +import androidx.lifecycle.LifecycleKt; + import net.gsantner.opoc.wrapper.GsCallback; import java.util.ArrayList; @@ -140,7 +142,7 @@ public static boolean setEquals(Collection a, Collection b) { * Set union */ public static Set union(final Collection a, final Collection b) { - final Set ret = new HashSet<>(a); + final Set ret = new LinkedHashSet<>(a); ret.addAll(b); return ret; } @@ -149,7 +151,7 @@ public static Set union(final Collection a, final Collection b) { * Set intersection */ public static Set intersection(final Collection a, final Collection b) { - final Set ret = new HashSet<>(a); + final Set ret = new LinkedHashSet<>(a); ret.retainAll(b); return ret; } 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 0575788b6..3e1652506 100644 --- a/app/src/main/java/net/gsantner/opoc/util/GsContextUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/GsContextUtils.java @@ -143,12 +143,15 @@ import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Random; @@ -160,6 +163,8 @@ public class GsContextUtils { //######################## public static final GsContextUtils instance = new GsContextUtils(); + private static String _authority = null; + public GsContextUtils() { } @@ -174,6 +179,7 @@ protected T thisp() { @SuppressLint("ConstantLocale") public final static Locale INITIAL_LOCALE = Locale.getDefault(); public final static String EXTRA_FILEPATH = "real_file_path_2"; + public final static String EXTRA_URI = "real_uri_0"; public final static SimpleDateFormat DATEFORMAT_RFC3339ISH = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", INITIAL_LOCALE); public final static String MIME_TEXT_PLAIN = "text/plain"; public final static String PREF_KEY__SAF_TREE_URI = "pref_key__saf_tree_uri"; @@ -184,10 +190,11 @@ protected T thisp() { public final static int REQUEST_SAF = 50003; public final static int REQUEST_STORAGE_PERMISSION_M = 50004; public final static int REQUEST_STORAGE_PERMISSION_R = 50005; + public final static int REQUEST_RECORD_AUDIO = 50006; public static int TEXTFILE_OVERWRITE_MIN_TEXT_LENGTH = 2; protected static Pair>> m_cacheLastExtractFileMetadata; - protected static String m_lastCameraPictureFilepath; + protected static String _lastCameraPictureFilepath; protected static String m_chooserTitle = "➥"; @@ -1074,12 +1081,15 @@ public List getProvidersInfos(final Context context) { } public String getFileProvider(final Context context) { - for (final ProviderInfo info : getProvidersInfos(context)) { - if (info.name.matches("(?i).*fileprovider.*")) { - return info.authority; + if (_authority == null) { + for (final ProviderInfo info : getProvidersInfos(context)) { + if (info.name.matches("(?i).*fileprovider.*")) { + _authority = info.authority; + break; + } } } - return null; + return _authority; } /** @@ -1722,7 +1732,7 @@ public static String[] contentColumnData(final Context context, final Intent int * service, the image will get copied to app-cache folder and it's path returned. */ public void requestGalleryPicture(final Activity activity) { - Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + final Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); try { activity.startActivityForResult(intent, REQUEST_PICK_PICTURE); } catch (Exception ex) { @@ -1730,6 +1740,16 @@ public void requestGalleryPicture(final Activity activity) { } } + public boolean requestAudioRecording(final Activity activity) { + final Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + try { + activity.startActivityForResult(intent, REQUEST_RECORD_AUDIO); + return true; + } catch (Exception ignored) { + } + return false; + } + public String extractFileFromIntentStr(final Context context, final Intent receivingIntent) { File f = extractFileFromIntent(context, receivingIntent); return f != null ? f.getAbsolutePath() : null; @@ -1743,48 +1763,29 @@ public String extractFileFromIntentStr(final Context context, final Intent recei * it can be retrieved using {@link #extractResultFromActivityResult(Activity, int, int, Intent)} * returns null if an error happened. * - * @param target Path to file to write to, if folder the filename gets app_name + millis + random filename. If null DCIM folder is used. */ - @SuppressWarnings("RegExpRedundantEscape") - public String requestCameraPicture(final Activity context, final File target) { - final Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - final String timestampedFilename = GsFileUtils.getFilenameWithTimestamp("IMG", "", "jpg"); - final File storageDir = target != null ? target : new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera"); - - String cameraPictureFilepath = null; - if (takePictureIntent.resolveActivity(context.getPackageManager()) != null) { - File photoFile; - try { - // Create an image file name - if (target != null && !target.isDirectory()) { - photoFile = target; + public void requestCameraPicture(final Activity activity) { + try { + final Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + final File picDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + final String name = GsFileUtils.getFilenameWithTimestamp("IMG", "", ".jpg"); + final File imageTemp = GsFileUtils.findNonConflictingDest(picDir, name); + + if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null && imageTemp.createNewFile()) { + imageTemp.deleteOnExit(); + // Continue only if the File was successfully created + final Uri uri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + uri = FileProvider.getUriForFile(activity, getFileProviderAuthority(activity), imageTemp); } else { - photoFile = new File(storageDir, timestampedFilename); - if (photoFile.getParentFile() != null && !photoFile.getParentFile().exists() && !photoFile.getParentFile().mkdirs()) { - photoFile = File.createTempFile(timestampedFilename.replace(".jpg", "_"), ".jpg", storageDir); - } + uri = Uri.fromFile(imageTemp); } - - //noinspection StatementWithEmptyBody - if (photoFile.getParentFile() != null && !photoFile.getParentFile().exists() && photoFile.getParentFile().mkdirs()) - ; - - // Save a file: path for use with ACTION_VIEW intents - cameraPictureFilepath = photoFile.getAbsolutePath(); - } catch (IOException ex) { - return null; + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri).putExtra(Intent.EXTRA_RETURN_RESULT, true); + _lastCameraPictureFilepath = imageTemp.getAbsolutePath(); + activity.startActivityForResult(takePictureIntent, REQUEST_CAMERA_PICTURE); } - - // Continue only if the File was successfully created - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(context, getFileProviderAuthority(context), photoFile)); - } else { - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile)); - } - context.startActivityForResult(takePictureIntent, REQUEST_CAMERA_PICTURE); + } catch (IOException ignored) { } - m_lastCameraPictureFilepath = cameraPictureFilepath; - return cameraPictureFilepath; } /** @@ -1793,18 +1794,18 @@ public String requestCameraPicture(final Activity context, final File target) { * Also may forward results via local broadcast */ @SuppressLint("ApplySharedPref") - public Object extractResultFromActivityResult(final Activity context, final int requestCode, final int resultCode, final Intent data) { + public Object extractResultFromActivityResult(final Activity context, final int requestCode, final int resultCode, final Intent intent) { switch (requestCode) { case REQUEST_CAMERA_PICTURE: { - String picturePath = (resultCode == Activity.RESULT_OK) ? m_lastCameraPictureFilepath : null; + final String picturePath = (resultCode == Activity.RESULT_OK) ? _lastCameraPictureFilepath : null; if (picturePath != null) { sendLocalBroadcastWithStringExtra(context, REQUEST_CAMERA_PICTURE + "", EXTRA_FILEPATH, picturePath); } return picturePath; } case REQUEST_PICK_PICTURE: { - if (resultCode == Activity.RESULT_OK && data != null) { - Uri selectedImage = data.getData(); + if (resultCode == Activity.RESULT_OK && intent != null) { + Uri selectedImage = intent.getData(); String[] filePathColumn = {MediaStore.Images.Media.DATA}; String picturePath = null; @@ -1824,19 +1825,22 @@ public Object extractResultFromActivityResult(final Activity context, final int } // Try to grab via file extraction method - data.setAction(Intent.ACTION_VIEW); - picturePath = picturePath != null ? picturePath : extractFileFromIntentStr(context, data); + intent.setAction(Intent.ACTION_VIEW); + picturePath = picturePath != null ? picturePath : extractFileFromIntentStr(context, intent); // Retrieve image from file descriptor / Cloud, e.g.: Google Drive, Picasa if (picturePath == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { - ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(selectedImage, "r"); + final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(selectedImage, "r"); if (parcelFileDescriptor != null) { - FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - FileInputStream input = new FileInputStream(fileDescriptor); + final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); + final FileInputStream input = new FileInputStream(fileDescriptor); // Create temporary file in cache directory - picturePath = File.createTempFile("image", "tmp", context.getCacheDir()).getAbsolutePath(); + final File temp = File.createTempFile("image", "tmp", context.getCacheDir()); + temp.deleteOnExit(); + picturePath = temp.getAbsolutePath(); + GsFileUtils.writeFile(new File(picturePath), GsFileUtils.readCloseBinaryStream(input), null); } } catch (IOException ignored) { @@ -1848,23 +1852,36 @@ public Object extractResultFromActivityResult(final Activity context, final int if (picturePath != null) { sendLocalBroadcastWithStringExtra(context, REQUEST_CAMERA_PICTURE + "", EXTRA_FILEPATH, picturePath); } + return picturePath; } break; } - + case REQUEST_RECORD_AUDIO: { + final Uri uri = intent.getData(); + final String uriPath = uri.getPath(); + final String ext = uriPath.substring(uriPath.lastIndexOf(".")); + final String datestr = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.ENGLISH).format(new Date()); + final File temp = new File(context.getCacheDir(), datestr + ext); + GsFileUtils.copyUriToFile(context, uri, temp); + sendLocalBroadcastWithStringExtra(context, REQUEST_RECORD_AUDIO + "", EXTRA_FILEPATH, temp.getAbsolutePath()); + } case REQUEST_SAF: { - if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { - Uri treeUri = data.getData(); + if (resultCode == Activity.RESULT_OK && intent != null && intent.getData() != null) { + final Uri treeUri = intent.getData(); PreferenceManager.getDefaultSharedPreferences(context).edit().putString(PREF_KEY__SAF_TREE_URI, treeUri.toString()).commit(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - context.getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + final ContentResolver resolver = context.getContentResolver(); + try { + resolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } catch (SecurityException se) { + resolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } } return treeUri; } break; } - case REQUEST_STORAGE_PERMISSION_M: case REQUEST_STORAGE_PERMISSION_R: { return checkExternalStoragePermission(context); @@ -1920,8 +1937,8 @@ public void onReceive(Context context, Intent intent) { * @param file File that should be edited */ public void requestPictureEdit(final Context context, final File file) { - Uri uri = getUriByFileProviderAuthority(context, file); - Intent intent = new Intent(Intent.ACTION_EDIT); + final Uri uri = getUriByFileProviderAuthority(context, file); + final Intent intent = new Intent(Intent.ACTION_EDIT); intent.setDataAndType(uri, "image/*"); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); diff --git a/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java b/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java index 1919bd9ef..271b94ba0 100644 --- a/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java @@ -10,6 +10,8 @@ package net.gsantner.opoc.util; import android.annotation.SuppressLint; +import android.content.Context; +import android.net.Uri; import android.os.Build; import android.text.TextUtils; import android.util.Pair; @@ -682,6 +684,10 @@ public static String getFilenameWithTimestamp(final String... A0prefixA1postfixA return filename; } + public static String getPrefix(final String name) { + return name.split("_")[0]; + } + public static final String SORT_BY_NAME = "NAME", SORT_BY_FILESIZE = "FILESIZE", SORT_BY_MTIME = "MTIME", SORT_BY_MIMETYPE = "MIMETYPE"; /** @@ -691,7 +697,6 @@ public static String getFilenameWithTimestamp(final String... A0prefixA1postfixA * * @param sortBy String key of what to sort * @param file The file object to get the - * @param dirFirst Whether to sort directories first * @return A key which can be used for comparisons / sorting */ private static String makeSortKey(final String sortBy, final File file) { @@ -778,4 +783,44 @@ public static boolean isChild(final File parent, File test) { return false; } + + public static File findNonConflictingDest(final File destDir, final String name) { + File dest = new File(destDir, name); + final String[] splits = name.split("\\."); + final String baseName = splits[0]; + splits[0] = ""; + final String extension = String.join(".", splits); + int i = 1; + while (dest.exists()) { + dest = new File(destDir, String.format("%s_%d%s", baseName, i, extension)); + i++; + } + return dest; + } + + public static boolean isUriOrFilePath(final String path) { + try { + // Attempt to create a URI from the given path + // If the URI scheme is "file", it's a file path + return "file".equals(Uri.parse(path).getScheme()); + } catch (Exception e) { + // If parsing the path as a URI fails, it's not a valid URI + // So, assume it's a file path + return true; + } + } + + public static void copyUriToFile(final Context context, final Uri source, final File dest) { + + try (final OutputStream outputStream = new FileOutputStream(dest, false); + final InputStream inputStream = context.getContentResolver().openInputStream(source) + ) { + final byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } catch (IOException ignored) { + } + } } diff --git a/app/src/main/res/layout/select_path_dialog.xml b/app/src/main/res/layout/select_path_dialog.xml index 7c15bbd34..74346fef7 100644 --- a/app/src/main/res/layout/select_path_dialog.xml +++ b/app/src/main/res/layout/select_path_dialog.xml @@ -55,6 +55,7 @@ android:layout_height="wrap_content" android:drawableStart="@drawable/ic_photo_camera_black_24dp" android:drawableLeft="@drawable/ic_photo_camera_black_24dp" + android:visibility="gone" android:text="@string/camera_picture" />