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 e5ed59f21d..c552a8efb6 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java @@ -605,8 +605,11 @@ public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) { return true; } case R.id.action_show_file_browser: { - final Intent intent = new Intent(activity, MainActivity.class).putExtra(Document.EXTRA_FILE, _document.getFile()); - GsContextUtils.instance.animateToActivity(activity, intent, false, null); + // Delay because I want menu to close before we open the file browser + _hlEditor.postDelayed(() -> { + final Intent intent = new Intent(activity, MainActivity.class).putExtra(Document.EXTRA_FILE, _document.getFile()); + GsContextUtils.instance.animateToActivity(activity, intent, false, null); + }, 250); return true; } default: { diff --git a/app/src/main/java/net/gsantner/markor/activity/MarkorBaseFragment.java b/app/src/main/java/net/gsantner/markor/activity/MarkorBaseFragment.java index 14fb745bbb..d14740c96b 100644 --- a/app/src/main/java/net/gsantner/markor/activity/MarkorBaseFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/MarkorBaseFragment.java @@ -1,7 +1,9 @@ package net.gsantner.markor.activity; import android.content.Context; +import android.view.Menu; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import net.gsantner.markor.ApplicationObject; 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 ede2fdd247..909e4e96b6 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 @@ -25,6 +25,7 @@ import net.gsantner.opoc.util.GsFileUtils; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -227,18 +228,27 @@ private boolean followLinkUnderCursor() { final int cursor = sel - TextViewUtils.getLineStart(_hlEditor.getText(), sel); final Matcher m = MARKDOWN_LINK.matcher(line); + + final ArrayList linksUnderCursor = new ArrayList<>(); while (m.find()) { final String group = m.group(2); - if (m.start() <= cursor && m.end() > cursor && group != null) { - if (WEB_URL.matcher(group).matches()) { - GsContextUtils.instance.openWebpageInExternalBrowser(getActivity(), group); + if (m.start() <= cursor && m.end() >= cursor && group != null) { + linksUnderCursor.add(group); + } + } + + // We want to search the line backwards in order to find the link closest to the cursor + // This helps us to match a link right after the cursor when there is one right before + for (int i = linksUnderCursor.size() - 1; i >= 0; i--) { + final String group = linksUnderCursor.get(i); + if (WEB_URL.matcher(group).matches()) { + GsContextUtils.instance.openWebpageInExternalBrowser(getActivity(), group); + return true; + } else { + final File f = GsFileUtils.makeAbsolute(group, _document.getFile().getParentFile()); + if (GsFileUtils.canCreate(f)) { + DocumentActivity.handleFileClick(getActivity(), f, null); return true; - } else { - final File f = GsFileUtils.makeAbsolute(group, _document.getFile().getParentFile()); - if (GsFileUtils.canCreate(f)) { - DocumentActivity.handleFileClick(getActivity(), f, null); - return true; - } } } } 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 0636aebfbe..65c4a39520 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java @@ -12,6 +12,7 @@ import android.app.Activity; import android.content.Context; import android.content.DialogInterface; +import android.os.Build; import android.text.Editable; import android.view.View; import android.widget.Button; @@ -27,11 +28,15 @@ 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.filesearch.FileSearchDialog; +import net.gsantner.markor.frontend.filesearch.FileSearchEngine; +import net.gsantner.markor.frontend.filesearch.FileSearchResultSelectorDialog; import net.gsantner.markor.frontend.textview.TextViewUtils; import net.gsantner.markor.model.AppSettings; import net.gsantner.markor.util.MarkorContextUtils; import net.gsantner.opoc.format.GsTextUtils; import net.gsantner.opoc.frontend.GsAudioRecordOmDialog; +import net.gsantner.opoc.frontend.filebrowser.GsFileBrowserListAdapter; import net.gsantner.opoc.frontend.filebrowser.GsFileBrowserOptions; import net.gsantner.opoc.util.GsFileUtils; import net.gsantner.opoc.wrapper.GsCallback; @@ -84,6 +89,8 @@ public static void showInsertImageOrLinkDialog( 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); + final Button buttonSelectSpecial = view.findViewById(R.id.ui__select_path_dialog__select_special); + final Button buttonSearch = view.findViewById(R.id.ui__select_path_dialog__search); final Button buttonPictureGallery = view.findViewById(R.id.ui__select_path_dialog__gallery_picture); final Button buttonPictureCamera = view.findViewById(R.id.ui__select_path_dialog__camera_picture); final Button buttonPictureEdit = view.findViewById(R.id.ui__select_path_dialog__edit_picture); @@ -137,6 +144,8 @@ public static void showInsertImageOrLinkDialog( okType = InsertType.AUDIO_DIALOG; } else { dialog.setTitle(R.string.insert_link); + buttonSelectSpecial.setVisibility(View.VISIBLE); + buttonSearch.setVisibility(View.VISIBLE); browseType = InsertType.LINK_BROWSE; okType = InsertType.LINK_DIALOG; } @@ -144,6 +153,8 @@ public static void showInsertImageOrLinkDialog( final String ok = activity.getString(android.R.string.ok); dialog.setButton(DialogInterface.BUTTON_POSITIVE, ok, (di, b) -> _insertItem.callback(okType)); buttonBrowseFilesystem.setOnClickListener(v -> _insertItem.callback(browseType)); + buttonSelectSpecial.setOnClickListener(v -> _insertItem.callback(InsertType.LINK_SPECIAL)); + buttonSearch.setOnClickListener(v -> _insertItem.callback(InsertType.LINK_SEARCH)); buttonPictureCamera.setOnClickListener(b -> _insertItem.callback(InsertType.IMAGE_CAMERA)); buttonPictureGallery.setOnClickListener(v -> _insertItem.callback(InsertType.IMAGE_GALLERY)); buttonAudioRecord.setOnClickListener(v -> _insertItem.callback(InsertType.AUDIO_RECORDING)); @@ -163,6 +174,8 @@ private enum InsertType { AUDIO_DIALOG, LINK_BROWSE, LINK_DIALOG, + LINK_SPECIAL, + LINK_SEARCH } private static String getTemplateForAction(final InsertType action, final int textFormatId) { @@ -181,6 +194,8 @@ private static String getTemplateForAction(final InsertType action, final int te } case LINK_DIALOG: case LINK_BROWSE: + case LINK_SPECIAL: + case LINK_SEARCH: default: { return getLinkFormat(textFormatId); } @@ -203,6 +218,8 @@ private static GsCallback.b2 getFilterForAction(final InsertType } case LINK_DIALOG: case LINK_BROWSE: + case LINK_SPECIAL: + case LINK_SEARCH: default: { return null; } @@ -300,6 +317,16 @@ private static void insertItem( final MarkorContextUtils cu = new MarkorContextUtils(activity); + final GsCallback.a1 setFields = file -> { + if (pathEdit != null) { + pathEdit.setText(GsFileUtils.relativePath(currentFile, file)); + } + + if (nameEdit != null && GsTextUtils.isNullOrEmpty(nameEdit.getText())) { + nameEdit.setText(GsFileUtils.getNameWithoutExtension(file.getName())); + } + }; + // Do each thing as necessary switch (action) { case IMAGE_CAMERA: { @@ -340,11 +367,7 @@ private static void insertItem( 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)); - - if (GsTextUtils.isNullOrEmpty(nameEdit.getText())) { - nameEdit.setText(GsFileUtils.getFilenameWithoutExtension(file)); - } + setFields.callback(file); } @Override @@ -358,6 +381,24 @@ public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) { } break; } + case LINK_SPECIAL: { + MarkorDialogFactory.showSelectSpecialFileDialog(activity, setFields); + break; + } + case LINK_SEARCH: { + final File nb = _appSettings.getNotebookDirectory(); + final FileSearchDialog.Options options = new FileSearchDialog.Options(); + options.enableSearchInContent = false; + options.searchLocation = R.string.notebook; + if (!FileSearchEngine.isSearchExecuting.get()) { + FileSearchDialog.showDialog(activity, options, searchOptions -> { + searchOptions.rootSearchDir = nb; + FileSearchEngine.queueFileSearch(activity, searchOptions, searchResults -> + FileSearchResultSelectorDialog.showDialog(activity, searchResults, (file, line, isLong) -> + setFields.callback(new File(nb, file)))); + }); + } + } case LINK_DIALOG: case AUDIO_DIALOG: case IMAGE_DIALOG: { 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 97f9faa912..00c92046d7 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java +++ b/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java @@ -32,6 +32,7 @@ import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; @@ -141,12 +142,11 @@ public static void showSearchFilesDialog( } if (!FileSearchEngine.isSearchExecuting.get()) { - GsCallback.a1 fileSearchDialogCallback = (searchOptions) -> { + FileSearchDialog.showDialog(activity, searchOptions -> { searchOptions.rootSearchDir = searchDir; - FileSearchEngine.queueFileSearch(activity, searchOptions, (searchResults) -> - FileSearchResultSelectorDialog.showDialog(activity, searchResults, callback)); - }; - FileSearchDialog.showDialog(activity, fileSearchDialogCallback); + FileSearchEngine.queueFileSearch(activity, searchOptions, searchResults -> + FileSearchResultSelectorDialog.showDialog(activity, searchResults, () -> callback)); + }); } } @@ -585,24 +585,79 @@ public static void showDocumentChecklistDialog( GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } - // Insert items - public static void showInsertItemsDialog( + public static void showSelectSpecialFileDialog(final Activity activity, final GsCallback.a1 callback) { + GsSearchOrCustomTextDialog.DialogOptions dopt = new GsSearchOrCustomTextDialog.DialogOptions(); + baseConf(activity, dopt); + dopt.titleText = R.string.special_documents; + final ArrayList data = new ArrayList<>(); + data.add(activity.getString(R.string.recently_viewed_documents)); + data.add(activity.getString(R.string.popular_documents)); + data.add(activity.getString(R.string.favourites)); + dopt.data = data; + dopt.isSearchEnabled = false; + final AppSettings as = ApplicationObject.settings(); + + dopt.positionCallback = i -> { + switch (i.get(0)) { + default: + case 0: + selectItemDialog(activity, R.string.recently_viewed_documents, as.getRecentFiles(), File::getName, callback); + break; + case 1: + selectItemDialog(activity, R.string.popular_documents, as.getPopularFiles(), File::getName, callback); + break; + case 2: + selectItemDialog(activity, R.string.favourites, as.getFavouriteFiles(), File::getName, callback); + break; + } + }; + + GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); + } + + /* Dialog to select an item from a list of items */ + public static void selectItemDialog( final Activity activity, - final @StringRes int title, - final List data, - final GsCallback.a1 insertCallback + final int title, + final Collection items, + final GsCallback.s1 toString, + final GsCallback.a1 callback ) { GsSearchOrCustomTextDialog.DialogOptions dopt = new GsSearchOrCustomTextDialog.DialogOptions(); baseConf(activity, dopt); - dopt.data = new ArrayList<>(new TreeSet<>(data)); - dopt.callback = insertCallback; dopt.titleText = title; - dopt.searchHintText = R.string.search_or_custom; - dopt.isMultiSelectEnabled = true; - dopt.positionCallback = (result) -> { - for (final Integer pi : result) { - insertCallback.callback(dopt.data.get(pi).toString()); - } + final List data = items instanceof List ? (List) items : new ArrayList<>(items); + dopt.data = GsCollectionUtils.map(data, toString::callback); + dopt.positionCallback = i -> callback.callback(data.get(i.get(0))); + dopt.isSearchEnabled = true; + GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static void showGlobFilesDialog( + final Activity activity, + final File searchDir, + final GsCallback.a1 callback + ) { + GsSearchOrCustomTextDialog.DialogOptions dopt = new GsSearchOrCustomTextDialog.DialogOptions(); + baseConf(activity, dopt); + dopt.titleText = R.string.search_documents; + dopt.isSearchEnabled = true; + dopt.defaultText = "**/[!.]*.*"; + dopt.callback = (query) -> { + final List found = GsFileUtils.searchFiles(searchDir, query); + GsSearchOrCustomTextDialog.DialogOptions dopt2 = new GsSearchOrCustomTextDialog.DialogOptions(); + baseConf(activity, dopt2); + dopt2.titleText = R.string.select; + dopt2.isSearchEnabled = true; + dopt2.data = GsCollectionUtils.map(found, File::getPath); + dopt2.positionCallback = (result) -> callback.callback(found.get(result.get(0))); + dopt2.neutralButtonText = R.string.search; + dopt2.neutralButtonCallback = dialog2 -> { + dialog2.dismiss(); + showGlobFilesDialog(activity, searchDir, callback); + }; + GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt2); }; GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } diff --git a/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchDialog.java b/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchDialog.java index e5159f996e..6b126e5f0e 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchDialog.java @@ -4,6 +4,7 @@ import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; +import android.view.Window; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; @@ -13,6 +14,7 @@ import android.widget.Spinner; import android.widget.TextView; +import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatEditText; import androidx.core.content.ContextCompat; @@ -20,26 +22,28 @@ import net.gsantner.markor.ApplicationObject; import net.gsantner.markor.R; import net.gsantner.markor.model.AppSettings; +import net.gsantner.opoc.format.GsTextUtils; import net.gsantner.opoc.util.GsContextUtils; import net.gsantner.opoc.wrapper.GsCallback; -import java.util.concurrent.atomic.AtomicReference; - public class FileSearchDialog { + public static final class Options { + public boolean enableRegex = true; + public boolean enableSearchInContent = true; + public @StringRes int searchLocation = R.string.directory; + } + public static void showDialog(final Activity activity, final GsCallback.a1 dialogCallback) { - final AtomicReference dialogRef = new AtomicReference<>(); - dialogRef.set(buildDialog(activity, dialogRef, dialogCallback).create()); - if (dialogRef.get().getWindow() != null) { - dialogRef.get().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } - dialogRef.get().show(); - if (dialogRef.get().getWindow() != null) { - dialogRef.get().getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); - } + showDialog(activity, new Options(), dialogCallback); } - private static AlertDialog.Builder buildDialog(final Activity activity, final AtomicReference dialog, final GsCallback.a1 dialogCallback) { + public static void showDialog( + final Activity activity, + final FileSearchDialog.Options options, + final GsCallback.a1 dialogCallback + ) { final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog); + final AppSettings appSettings = ApplicationObject.settings(); final ScrollView scrollView = new ScrollView(activity); final LinearLayout dialogLayout = new LinearLayout(activity); @@ -61,31 +65,9 @@ private static AlertDialog.Builder buildDialog(final Activity activity, final At final CheckBox searchInContentCheckBox = new CheckBox(activity); final CheckBox onlyFirstContentMatchCheckBox = new CheckBox(activity); - final AppSettings appSettings = ApplicationObject.settings(); - final GsCallback.a0 submit = () -> { - final String query = searchEditText.getText().toString(); - if (dialogCallback != null && !TextUtils.isEmpty(query)) { - FileSearchEngine.SearchOptions opt = new FileSearchEngine.SearchOptions(); - opt.query = query; - opt.isRegexQuery = regexCheckBox.isChecked(); - opt.isCaseSensitiveQuery = caseSensitivityCheckBox.isChecked(); - opt.isSearchInContent = searchInContentCheckBox.isChecked(); - opt.isOnlyFirstContentMatch = onlyFirstContentMatchCheckBox.isChecked(); - opt.ignoredDirectories = appSettings.getFileSearchIgnorelist(); - opt.maxSearchDepth = appSettings.getSearchMaxDepth(); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - opt.password = appSettings.getDefaultPassword(); - } - appSettings.setSearchQueryRegexUsing(opt.isRegexQuery); - appSettings.setSearchQueryCaseSensitivity(opt.isCaseSensitiveQuery); - appSettings.setSearchInContent(opt.isSearchInContent); - appSettings.setOnlyFirstContentMatch(opt.isOnlyFirstContentMatch); - dialogCallback.callback(opt); - } - }; - // TextView - messageTextView.setText(R.string.recursive_search_in_current_directory); + final String loc = activity.getString(options.searchLocation != 0 ? options.searchLocation : R.string.directory); + messageTextView.setText(activity.getString(R.string.recursive_search_in_location, loc)); dialogLayout.addView(messageTextView, margins); // EdiText: Search query input @@ -94,16 +76,6 @@ private static AlertDialog.Builder buildDialog(final Activity activity, final At searchEditText.setMaxLines(1); searchEditText.setTextColor(textColor); searchEditText.setHintTextColor((textColor & 0x00FFFFFF) | 0x99000000); - searchEditText.setOnKeyListener((keyView, keyCode, keyEvent) -> { - if ((keyEvent.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { - if (dialog != null && dialog.get() != null) { - dialog.get().dismiss(); - } - submit.callback(); - return true; - } - return false; - }); dialogLayout.addView(searchEditText, margins); // Spinner: History @@ -128,9 +100,14 @@ public void onNothingSelected(AdapterView parent) { } // Checkbox: Regex search - regexCheckBox.setText(R.string.regex_search); - regexCheckBox.setChecked(appSettings.isSearchQueryUseRegex()); - dialogLayout.addView(regexCheckBox, margins); + if (options.enableRegex) { + regexCheckBox.setText(R.string.regex_search); + regexCheckBox.setChecked(appSettings.isSearchQueryUseRegex()); + dialogLayout.addView(regexCheckBox, margins); + } else { + regexCheckBox.setChecked(false); + regexCheckBox.setVisibility(View.GONE); + } // Checkbox: Case sensitive caseSensitivityCheckBox.setText(R.string.case_sensitive); @@ -138,32 +115,78 @@ public void onNothingSelected(AdapterView parent) { dialogLayout.addView(caseSensitivityCheckBox, margins); // Checkbox: Search in content - searchInContentCheckBox.setText(R.string.search_in_content); - searchInContentCheckBox.setChecked(appSettings.isSearchInContent()); - searchInContentCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - onlyFirstContentMatchCheckBox.setVisibility(isChecked ? View.VISIBLE : View.INVISIBLE); - }); - dialogLayout.addView(searchInContentCheckBox, margins); - - // Checkbox: Only first content match - onlyFirstContentMatchCheckBox.setText(R.string.stop_search_after_first_match); - onlyFirstContentMatchCheckBox.setChecked(appSettings.isOnlyFirstContentMatch()); - onlyFirstContentMatchCheckBox.setVisibility(searchInContentCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); - dialogLayout.addView(onlyFirstContentMatchCheckBox, subCheckBoxMargins); + if (options.enableSearchInContent) { + searchInContentCheckBox.setText(R.string.search_in_content); + searchInContentCheckBox.setChecked(appSettings.isSearchInContent()); + searchInContentCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + onlyFirstContentMatchCheckBox.setVisibility(isChecked ? View.VISIBLE : View.INVISIBLE); + }); + dialogLayout.addView(searchInContentCheckBox, margins); + + // Checkbox: Only first content match + onlyFirstContentMatchCheckBox.setText(R.string.stop_search_after_first_match); + onlyFirstContentMatchCheckBox.setChecked(appSettings.isOnlyFirstContentMatch()); + onlyFirstContentMatchCheckBox.setVisibility(searchInContentCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); + dialogLayout.addView(onlyFirstContentMatchCheckBox, subCheckBoxMargins); + } else { + searchInContentCheckBox.setChecked(false); + searchInContentCheckBox.setVisibility(View.GONE); + onlyFirstContentMatchCheckBox.setChecked(false); + onlyFirstContentMatchCheckBox.setVisibility(View.GONE); + } // ScrollView scrollView.addView(dialogLayout); // Configure dialog - dialogBuilder.setTitle(R.string.search) + final AlertDialog dialog = dialogBuilder + .setTitle(R.string.search) .setOnCancelListener(null) .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> dialogInterface.dismiss()) - .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { - dialogInterface.dismiss(); - submit.callback(); - }) - .setView(scrollView); + .setView(scrollView) + .create(); + + final Window window = dialog.getWindow(); + if (window != null) { + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); + } + + final GsCallback.a0 submit = () -> { + final String query = searchEditText.getText().toString(); + if (dialogCallback != null && !TextUtils.isEmpty(query)) { + FileSearchEngine.SearchOptions opt = new FileSearchEngine.SearchOptions(); + opt.query = query; + opt.isRegexQuery = regexCheckBox.isChecked(); + opt.isCaseSensitiveQuery = caseSensitivityCheckBox.isChecked(); + opt.isSearchInContent = searchInContentCheckBox.isChecked(); + opt.isOnlyFirstContentMatch = onlyFirstContentMatchCheckBox.isChecked(); + opt.ignoredDirectories = appSettings.getFileSearchIgnorelist(); + opt.maxSearchDepth = appSettings.getSearchMaxDepth(); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + opt.password = appSettings.getDefaultPassword(); + } + appSettings.setSearchQueryRegexUsing(opt.isRegexQuery); + appSettings.setSearchQueryCaseSensitivity(opt.isCaseSensitiveQuery); + appSettings.setSearchInContent(opt.isSearchInContent); + appSettings.setOnlyFirstContentMatch(opt.isOnlyFirstContentMatch); + + dialog.dismiss(); + dialogCallback.callback(opt); + } + }; + + dialog.setButton(AlertDialog.BUTTON_POSITIVE, activity.getString(android.R.string.ok), (di, i) -> submit.callback()); + + // Enter button callback set after creation to get ref to dialog + searchEditText.setOnKeyListener((keyView, keyCode, keyEvent) -> { + if ((keyEvent.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { + submit.callback(); + return true; + } + return false; + }); - return dialogBuilder; + dialog.show(); } } diff --git a/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchEngine.java b/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchEngine.java index 14575b017a..c8a778ae88 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchEngine.java +++ b/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchEngine.java @@ -27,9 +27,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Queue; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; @@ -71,6 +73,7 @@ public static class SearchOptions { public boolean isShowMatchPreview = true; public boolean isShowResultOnCancel = true; public char[] password = new char[0]; + public int message = 0; } public static class FitFile { @@ -119,8 +122,8 @@ public static class QueueSearchFilesTask extends AsyncTask _result = new ArrayList<>(); - private final List _ignoredRegexDirs = new ArrayList<>(); - private final List _ignoredExactDirs = new ArrayList<>(); + private final Set _ignoredRegexDirs = new HashSet<>(); + private final Set _ignoredExactDirs = new HashSet<>(); public QueueSearchFilesTask(final SearchOptions config, final GsCallback.a1> callback) { _config = config; @@ -286,7 +289,7 @@ protected void onCancelled() { FileSearchEngine.isSearchExecuting.set(false); } - public void splitRegexExactFiles(final List list, final List exactList, final List regexList) { + private void splitRegexExactFiles(final List list, final Set exactList, final Set regexList) { for (String pattern : (list != null ? list : new ArrayList())) { if (pattern.isEmpty()) { continue; diff --git a/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchResultSelectorDialog.java b/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchResultSelectorDialog.java index 0704191f70..ec95e78db6 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchResultSelectorDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchResultSelectorDialog.java @@ -2,56 +2,53 @@ import android.annotation.SuppressLint; import android.app.Activity; +import android.app.Dialog; import android.content.Context; import android.database.DataSetObserver; import android.text.Editable; -import android.text.TextWatcher; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.view.WindowManager; import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatEditText; import androidx.core.content.ContextCompat; import net.gsantner.markor.R; +import net.gsantner.markor.frontend.MarkorDialogFactory; import net.gsantner.markor.frontend.filesearch.FileSearchEngine.FitFile; import net.gsantner.opoc.util.GsContextUtils; import net.gsantner.opoc.wrapper.GsCallback; +import net.gsantner.opoc.wrapper.GsTextWatcherAdapter; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; public class FileSearchResultSelectorDialog { + /** + * Show a file system selector dialog + * + * @param activity Activity to use + * @param searchResults Search results to filter + * @param callback Callback to call when a item is selected + * callback.first: Path to file (relative) + * callback.second: Line number (null if not applicable) + * callback.third: True if the dialog was dismissed by long clicking on a file + */ public static void showDialog( final Activity activity, final List searchResults, - final GsCallback.a3 dialogCallback - ) { - final AtomicReference dialog = new AtomicReference<>(); - dialog.set(buildDialog(activity, dialog, searchResults, dialogCallback).create()); - if (dialog.get().getWindow() != null) { - dialog.get().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } - dialog.get().show(); - if (dialog.get().getWindow() != null) { - dialog.get().getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); - } - } - - private static AlertDialog.Builder buildDialog( - final Activity activity, - final AtomicReference dialog, - final List searchResults, - final GsCallback.a3 dialogCallback + final GsCallback.a3 callback ) { final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog); @@ -80,7 +77,7 @@ private static AlertDialog.Builder buildDialog( // List filling final List groupItemsData = filter(searchResults, ""); final ExpandableSearchResultsListAdapter adapter = new ExpandableSearchResultsListAdapter(activity, groupItemsData); - searchEditText.addTextChangedListener(new TextWatcher() { + searchEditText.addTextChangedListener(new GsTextWatcherAdapter() { @Override public void afterTextChanged(final Editable arg0) { String filterText = searchEditText.getText() == null ? "" : searchEditText.getText().toString(); @@ -88,30 +85,32 @@ public void afterTextChanged(final Editable arg0) { ExpandableSearchResultsListAdapter adapter = new ExpandableSearchResultsListAdapter(activity, filteredGroups); expandableListView.setAdapter(adapter); } - - @Override - public void onTextChanged(final CharSequence arg0, final int arg1, final int arg2, final int arg3) { - } - - @Override - public void beforeTextChanged(final CharSequence arg0, final int arg1, final int arg2, final int arg3) { - } }); expandableListView.setGroupIndicator(null); expandableListView.setAdapter(adapter); + dialogLayout.addView(expandableListView); - final GsCallback.a0 dismiss = () -> { - if (dialog != null && dialog.get() != null) { - dialog.get().dismiss(); - } - }; + // Configure dialog + final AlertDialog dialog = dialogBuilder + .setView(dialogLayout) + .setTitle(R.string.select) + .setOnCancelListener(null) + .setMessage(searchResults.isEmpty() ? " ¯\\_(ツ)_/¯ " : null) + .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> dialogInterface.dismiss()) + .create(); + + final Window window = dialog.getWindow(); + if (window != null) { + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); + } expandableListView.setOnGroupClickListener((parent, view, groupPosition, id) -> { final FitFile groupItem = (FitFile) parent.getExpandableListAdapter().getGroup(groupPosition); if (groupItem.children.isEmpty()) { - dismiss.callback(); - dialogCallback.callback(groupItem.path, null, false); + dialog.dismiss(); + callback.callback(groupItem.path, null, false); } return false; }); @@ -121,7 +120,8 @@ public void beforeTextChanged(final CharSequence arg0, final int arg1, final int final FitFile groupItem = (FitFile) _adapter.getGroup(groupPos); final Pair childItem = (Pair) _adapter.getChild(groupPos, childPos); if (childItem != null && childItem.second != null && childItem.second >= 0) { - dialogCallback.callback(groupItem.path, childItem.second, false); + dialog.dismiss(); + callback.callback(groupItem.path, childItem.second, false); } return false; }; @@ -135,8 +135,8 @@ public void beforeTextChanged(final CharSequence arg0, final int arg1, final int if (ExpandableListView.getPackedPositionType(packed) == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { final int group = ExpandableListView.getPackedPositionGroup(packed); final String path = ((FitFile) expandableListView.getExpandableListAdapter().getGroup(group)).path; - dismiss.callback(); - dialogCallback.callback(path, null, true); + dialog.dismiss(); + callback.callback(path, null, true); } else { final int groupPos = ExpandableListView.getPackedPositionGroup(packed); final int childPos = ExpandableListView.getPackedPositionChild(packed); @@ -147,16 +147,7 @@ public void beforeTextChanged(final CharSequence arg0, final int arg1, final int return true; }); - dialogLayout.addView(expandableListView); - - // Configure dialog - dialogBuilder.setView(dialogLayout) - .setTitle(R.string.select) - .setOnCancelListener(null) - .setMessage(searchResults.isEmpty() ? " ¯\\_(ツ)_/¯ " : null) - .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> dialogInterface.dismiss()); - - return dialogBuilder; + dialog.show(); } private static List filter(final List searchResults, String query) { 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 43ea3b79f5..e6186a588b 100644 --- a/app/src/main/java/net/gsantner/markor/model/AppSettings.java +++ b/app/src/main/java/net/gsantner/markor/model/AppSettings.java @@ -588,9 +588,9 @@ public ArrayList getAsFileList(List list) { return r; } - public Set getFavouriteFiles() { + public static Set getFileSet(final List paths) { final Set set = new LinkedHashSet<>(); - for (final String fp : getStringList(R.string.pref_key__favourite_files)) { + for (final String fp : paths) { final File f = new File(fp); if (f.exists() || GsFileBrowserListAdapter.isVirtualStorage(f)) { set.add(f); @@ -599,6 +599,18 @@ public Set getFavouriteFiles() { return set; } + public Set getFavouriteFiles() { + return getFileSet(getStringList(R.string.pref_key__favourite_files)); + } + + public Set getRecentFiles() { + return getFileSet(getStringList(R.string.pref_key__recent_documents)); + } + + public Set getPopularFiles() { + return getFileSet(getPopularDocuments()); + } + public String getInjectedHeader() { return getString(R.string.pref_key__inject_to_head, rstr(R.string.inject_to_head_default)); } diff --git a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserDialog.java b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserDialog.java index 84f68c5a42..77e12253c1 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserDialog.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserDialog.java @@ -22,6 +22,8 @@ import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; diff --git a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserFragment.java b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserFragment.java index d3e255bec2..d7a4287ebb 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserFragment.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserFragment.java @@ -522,9 +522,9 @@ public boolean onOptionsItemSelected(final MenuItem item) { private void executeSearchAction() { final File currentFolder = getCurrentFolder(); - MarkorDialogFactory.showSearchFilesDialog(getActivity(), currentFolder, (relPath, lineNumber, isLong) -> { + MarkorDialogFactory.showSearchFilesDialog(getActivity(), currentFolder, (relPath, lineNumber, longPress) -> { final File load = new File(currentFolder, relPath); - if (!isLong) { + if (!longPress) { if (load.isDirectory()) { _filesystemViewerAdapter.setCurrentFolder(load); } else { diff --git a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java index 0ae19dbcf9..56e0de0ab4 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java @@ -55,10 +55,11 @@ public class GsFileBrowserListAdapter extends RecyclerView.Adapter newData = new ArrayList<>(); - if (_currentFolder.isDirectory()) { - GsCollectionUtils.addAll(newData, _currentFolder.listFiles(GsFileBrowserListAdapter.this)); - } else if (_currentFolder.equals(VIRTUAL_STORAGE_RECENTS)) { - newData.addAll(_dopt.recentFiles); - } else if (_currentFolder.equals(VIRTUAL_STORAGE_POPULAR)) { - newData.addAll(_dopt.popularFiles); - } else if (_currentFolder.equals(VIRTUAL_STORAGE_FAVOURITE)) { - GsCollectionUtils.addAll(newData, _dopt.favouriteFiles); - } else if (folder.getAbsolutePath().equals("/storage/emulated")) { - newData.add(new File(folder, "0")); - } else if (folder.getAbsolutePath().equals("/")) { - newData.add(new File(folder, "storage")); - } else if (folder.equals(_context.getFilesDir().getParentFile())) { - // Private AppStorage: Allow to access to files directory only - // (don't allow access to internals like shared_preferences & databases) - newData.add(new File(folder, "files")); - } else if (folder.getAbsolutePath().equals("/storage")) { + if (folder.equals(VIRTUAL_STORAGE_ROOT)) { // Scan for /storage/emulated/{0,1,2,..} for (int i = 0; i < 10; i++) { final File file = new File("/storage/emulated/" + i); @@ -600,9 +585,25 @@ private void loadFolder(final File folder) { _virtualMapping.put(VIRTUAL_STORAGE_APP_DATA_PRIVATE, appDataFolder); newData.add(VIRTUAL_STORAGE_APP_DATA_PRIVATE); } + } else if (_currentFolder.isDirectory()) { + GsCollectionUtils.addAll(newData, _currentFolder.listFiles(GsFileBrowserListAdapter.this)); + } else if (_currentFolder.equals(VIRTUAL_STORAGE_RECENTS)) { + newData.addAll(_dopt.recentFiles); + } else if (_currentFolder.equals(VIRTUAL_STORAGE_POPULAR)) { + newData.addAll(_dopt.popularFiles); + } else if (_currentFolder.equals(VIRTUAL_STORAGE_FAVOURITE)) { + GsCollectionUtils.addAll(newData, _dopt.favouriteFiles); + } else if (folder.getAbsolutePath().equals("/storage/emulated")) { + newData.add(new File(folder, "0")); + } else if (folder.getAbsolutePath().equals("/")) { + newData.add(new File(folder, "storage")); + } else if (folder.equals(_context.getFilesDir().getParentFile())) { + // Private AppStorage: Allow to access to files directory only + // (don't allow access to internals like shared_preferences & databases) + newData.add(new File(folder, "files")); } - for (final File externalFileDir : ContextCompat.getExternalFilesDirs(_context, null)) { + for (final File externalFileDir : ContextCompat.getExternalFilesDirs(_context, null)) { for (int i = 0; i < newData.size(); i++) { final File file = newData.get(i); if (!canWrite(file) && !file.getAbsolutePath().equals("/") && externalFileDir != null && externalFileDir.getAbsolutePath().startsWith(file.getAbsolutePath())) { 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 1353f5b33a..8d592f4fcd 100644 --- a/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java @@ -14,8 +14,11 @@ import android.net.Uri; import android.os.Build; import android.text.TextUtils; +import android.util.Log; import android.util.Pair; +import androidx.annotation.RequiresApi; + import net.gsantner.opoc.format.GsTextUtils; import java.io.BufferedInputStream; @@ -33,7 +36,13 @@ import java.io.OutputStream; import java.io.Serializable; import java.net.URLConnection; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; @@ -843,4 +852,28 @@ public static File makeAbsolute(final File file, final File base) { return null; } } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static List searchFiles(final File root, String glob) { + try { + glob = glob.trim(); + glob = glob.startsWith("glob:") ? glob : "glob:" + glob; + + final PathMatcher matcher = FileSystems.getDefault().getPathMatcher(glob); + final List found = new ArrayList<>(); + Files.walkFileTree(root.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(final Path path, final BasicFileAttributes attrs) { + if (matcher.matches(path)) { + found.add(path.toFile()); + } + return FileVisitResult.CONTINUE; + } + }); + return found; + } catch (IOException e) { + Log.d(GsFileUtils.class.getName(), e.toString()); + } + return null; + } } diff --git a/app/src/main/res/layout/select_path_dialog.xml b/app/src/main/res/layout/select_path_dialog.xml index 74346fef71..517b40c86d 100644 --- a/app/src/main/res/layout/select_path_dialog.xml +++ b/app/src/main/res/layout/select_path_dialog.xml @@ -49,6 +49,24 @@ android:drawableLeft="@drawable/ic_insert_drive_file_black_24dp" android:text="@string/browse_filesystem" /> +