From ca6f4908a4c48e7115c15e8fea3b6e6123c0e0bb Mon Sep 17 00:00:00 2001 From: Harshad Vedartham Date: Fri, 24 Dec 2021 01:57:30 -0800 Subject: [PATCH] File change state indication & general improvements, by @harshad1 @gsantner (PR #1516) * Closes #1311 * Closes #1537 Co-authored-by: Gregor Santner --- .../markor/activity/DocumentActivity.java | 10 +- .../markor/activity/DocumentEditFragment.java | 227 +++++++++--------- .../activity/DocumentShareIntoFragment.java | 2 +- .../markor/activity/MainActivity.java | 89 ++++--- .../gsantner/markor/format/TextConverter.java | 2 +- .../format/markdown/MarkdownTextActions.java | 2 +- .../plaintext/PlaintextTextActions.java | 2 +- .../net/gsantner/markor/model/Document.java | 74 +++--- .../markor/ui/hleditor/Highlighter.java | 7 - .../markor/ui/hleditor/TextActions.java | 6 +- .../net/gsantner/markor/util/ShareUtil.java | 2 +- .../net/gsantner/opoc/util/FileUtils.java | 24 +- .../main/res/menu/document__edit__menu.xml | 3 +- app/src/main/res/values/styles.xml | 11 + 14 files changed, 241 insertions(+), 220 deletions(-) diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentActivity.java b/app/src/main/java/net/gsantner/markor/activity/DocumentActivity.java index 15f51f7e57..13d6493a27 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentActivity.java @@ -161,11 +161,7 @@ protected void onCreate(Bundle savedInstanceState) { } setSupportActionBar(_toolbar); - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setDisplayHomeAsUpEnabled(true); - ab.setDisplayShowTitleEnabled(false); - } + _toolbar.setOnClickListener(this::onToolbarTitleClicked); _fragManager = getSupportFragmentManager(); @@ -283,7 +279,7 @@ public void showTextEditor(@Nullable Document document, @Nullable File file, boo boolean sameDocumentRequested = false; if (currentFragment instanceof DocumentEditFragment) { String reqPath = (reqFile != null) ? reqFile.getPath() : ""; - sameDocumentRequested = reqPath.equals(((DocumentEditFragment) currentFragment).getPath()); + sameDocumentRequested = reqPath.equals(((DocumentEditFragment) currentFragment).getDocument().getPath()); } if (!sameDocumentRequested) { @@ -369,7 +365,7 @@ private GsFragmentBase getCurrentVisibleFragment() { public void setDocument(Document document) { _document = document; - _toolbarTitleText.setText(_document.getTitle()); + setDocumentTitle(_document.getTitle()); } private void onToolbarTitleClicked(View v) { diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentEditFragment.java b/app/src/main/java/net/gsantner/markor/activity/DocumentEditFragment.java index facb49194d..f36b64a528 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentEditFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentEditFragment.java @@ -17,13 +17,13 @@ import android.content.Context; import android.graphics.Color; import android.graphics.Typeface; -import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.text.Selection; import android.util.TypedValue; import android.view.Gravity; import android.view.Menu; @@ -135,6 +135,8 @@ public static DocumentEditFragment newInstance(File path, boolean pathIsFolder, private MarkorWebViewClient _webViewClient; private boolean _nextConvertToPrintMode = false; private long _loadModTime = 0; + private boolean _isTextChanged = false; + private MenuItem _saveMenuItem, _undoMenuItem, _redoMenuItem; public DocumentEditFragment() { super(); @@ -143,10 +145,6 @@ public DocumentEditFragment() { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - _appSettings = new AppSettings(getContext()); - if (_appSettings.getSetWebViewFulldrawing() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - WebView.enableSlowWholeDocumentDraw(); - } } @Override @@ -158,9 +156,16 @@ protected int getLayoutResId() { @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - _shareUtil = new ShareUtil(view.getContext()); + final Activity activity = getActivity(); + + _appSettings = new AppSettings(activity); + if (_appSettings.getSetWebViewFulldrawing() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + WebView.enableSlowWholeDocumentDraw(); + } + + _shareUtil = new ShareUtil(activity); - _webViewClient = new MarkorWebViewClient(getActivity()); + _webViewClient = new MarkorWebViewClient(activity); _webView.setBackgroundColor(ContextCompat.getColor(view.getContext(), _appSettings.isDarkThemeEnabled() ? R.color.dark__background : R.color.light__background)); _webView.setWebViewClient(_webViewClient); WebSettings webSettings = _webView.getSettings(); @@ -181,27 +186,24 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { if (savedInstanceState != null && savedInstanceState.containsKey(SAVESTATE_DOCUMENT)) { _document = (Document) savedInstanceState.getSerializable(SAVESTATE_DOCUMENT); } else { - _document = Document.fromArguments(getActivity(), getArguments()); + _document = Document.fromArguments(activity, getArguments()); intentLineNumber = _document.getIntentLineNumber(); } // Upon construction, the document format has been determined from extension etc // Here we replace it with the last saved format. - _document.setFormat(_appSettings.getDocumentFormat(getPath(), _document.getFormat())); + _document.setFormat(_appSettings.getDocumentFormat(_document.getPath(), _document.getFormat())); applyTextFormat(_document.getFormat()); _textFormat.getTextActions().setDocument(_document); loadDocument(); - Activity activity = getActivity(); if (activity instanceof DocumentActivity) { - DocumentActivity da = ((DocumentActivity) activity); - da.setDocumentTitle(_document.getTitle()); - da.setDocument(_document); + ((DocumentActivity) activity).setDocument(_document); } _editTextUndoRedoHelper = new TextViewUndoRedo(_hlEditor); - new ActivityUtils(getActivity()).hideSoftKeyboard().freeContextRef(); + new ActivityUtils(activity).hideSoftKeyboard().freeContextRef(); _hlEditor.clearFocus(); _hlEditor.setLineSpacing(0, _appSettings.getEditorLineSpacing()); setupAppearancePreferences(view); @@ -236,7 +238,6 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { _hlEditor.smoothMoveCursorToLine(StringUtils.getLineOffsetFromIndex(text, pos)[0]); _hlEditor.setSelection(pos); } - } } @@ -248,7 +249,7 @@ public void onResume() { _hlEditor.setGravity(_appSettings.isEditorStartEditingInCenter() ? Gravity.CENTER : Gravity.NO_GRAVITY); - if (_document != null && _document.getFile() != null) { + if (_document != null) { _document.testCreateParent(); boolean permok = _shareUtil.canWriteFile(_document.getFile(), false); if (!permok && !_document.getFile().isDirectory() && _shareUtil.canWriteFile(_document.getFile(), _document.getFile().isDirectory())) { @@ -261,7 +262,7 @@ public void onResume() { _textSdWarning.setVisibility(permok ? View.GONE : View.VISIBLE); } - if (_document != null && _document.getFile() != null && _document.getFile().getAbsolutePath().contains("mordor/1-epub-experiment.md") && getActivity() instanceof DocumentActivity) { + if (_document != null && _document.getFile().getAbsolutePath().contains("mordor/1-epub-experiment.md") && getActivity() instanceof DocumentActivity) { _hlEditor.setText(CoolExperimentalStuff.convertEpubToText(_document.getFile(), getString(R.string.page))); } @@ -281,16 +282,12 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.findItem(R.id.action_redo).setVisible(_appSettings.isEditorHistoryEnabled()); menu.findItem(R.id.action_send_debug_log).setVisible(MainActivity.IS_DEBUG_ENABLED && getActivity() instanceof DocumentActivity && !_isPreviewVisible); - final boolean canUndo = _editTextUndoRedoHelper.getCanUndo(); - final boolean canRedo = _editTextUndoRedoHelper.getCanRedo(); final boolean isExperimentalFeaturesEnabled = _appSettings.isExperimentalFeaturesEnabled(); // Undo / Redo / Save (keep visible, but deactivated and tinted grey if not executable) - Drawable drawable; - drawable = menu.findItem(R.id.action_undo).setEnabled(canUndo).setVisible(!_isPreviewVisible).getIcon(); - drawable.mutate().setAlpha(canUndo ? 255 : 40); - drawable = menu.findItem(R.id.action_redo).setEnabled(canRedo).setVisible(!_isPreviewVisible).getIcon(); - drawable.mutate().setAlpha(canRedo ? 255 : 40); + _undoMenuItem = menu.findItem(R.id.action_undo).setVisible(!_isPreviewVisible); + _redoMenuItem = menu.findItem(R.id.action_redo).setVisible(!_isPreviewVisible); + _saveMenuItem = menu.findItem(R.id.action_save).setVisible(!_isPreviewVisible); // Edit / Preview switch menu.findItem(R.id.action_edit).setVisible(_isPreviewVisible); @@ -328,26 +325,52 @@ public boolean onQueryTextChange(String text) { } }); + // Set various initial states updateMenuToggleStates(_document.getFormat()); + checkTextChangeState(); + updateUndoRedoIconStates(); + } + + private void updateUndoRedoIconStates() { + if (_editTextUndoRedoHelper == null) { + return; + } + + final boolean canUndo = _editTextUndoRedoHelper.getCanUndo(); + if (_undoMenuItem != null && _undoMenuItem.isEnabled() != canUndo) { + _undoMenuItem.setEnabled(canUndo).getIcon().mutate().setAlpha(canUndo ? 255 : 40); + } + + final boolean canRedo = _editTextUndoRedoHelper.getCanRedo(); + if (_redoMenuItem != null && _redoMenuItem.isEnabled() != canRedo) { + _redoMenuItem.setEnabled(canRedo).getIcon().mutate().setAlpha(canRedo ? 255 : 40); + } } public void loadDocument() { - final long modTime = _document.getFile().lastModified(); //Only trigger the load process if constructing or file updated + final long modTime = _document.lastModified(); if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED) || modTime > _loadModTime) { - final String content = _document.loadContent(getContext()); _loadModTime = modTime; - // Only setText if content changed - final CharSequence text = _hlEditor.getText(); - if (text == null || !content.contentEquals(text)) { + final String content = _document.loadContent(getContext()); + if (!_document.isContentSame(_hlEditor.getText())) { + + final int[] sel = StringUtils.getSelection(_hlEditor); + sel[0] = Math.min(sel[0], content.length()); + sel[1] = Math.min(sel[1], content.length()); + _hlEditor.setText(content); + + _hlEditor.setSelection(sel[0], sel[1]); // hleditor can handle invalid selections } + checkTextChangeState(); + if (_isPreviewVisible) { + setDocumentViewVisibility(true); _webViewClient.setRestoreScrollY(_webView.getScrollY()); - setDocumentViewVisibility(_isPreviewVisible); } } } @@ -357,7 +380,8 @@ public boolean onOptionsItemSelected(final MenuItem item) { if (item == null) { return true; } - _shareUtil.setContext(getActivity()); + _shareUtil.setContext(getContext()); + final Activity activity = getActivity(); final int itemId = item.getItemId(); switch (itemId) { case R.id.action_undo: { @@ -365,7 +389,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { _hlEditor.disableHighlighterAutoFormat(); _editTextUndoRedoHelper.undo(); _hlEditor.enableHighlighterAutoFormat(); - ((AppCompatActivity) getActivity()).supportInvalidateOptionsMenu(); + updateUndoRedoIconStates(); } return true; } @@ -374,7 +398,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { _hlEditor.disableHighlighterAutoFormat(); _editTextUndoRedoHelper.redo(); _hlEditor.enableHighlighterAutoFormat(); - ((AppCompatActivity) getActivity()).supportInvalidateOptionsMenu(); + updateUndoRedoIconStates(); } return true; } @@ -383,7 +407,12 @@ public boolean onOptionsItemSelected(final MenuItem item) { return true; } case R.id.action_reload: { + final long oldModTime = _loadModTime; loadDocument(); + // Use modtime to show toast if document updated + if (_loadModTime != oldModTime) { + Toast.makeText(activity, "✔", Toast.LENGTH_SHORT).show(); + } return true; } case R.id.action_preview: { @@ -417,7 +446,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { case R.id.action_share_html: case R.id.action_share_html_source: { if (saveDocument(false)) { - TextConverter converter = TextFormat.getFormat(_document.getFormat(), getActivity(), _document, _hlEditor).getConverter(); + TextConverter converter = TextFormat.getFormat(_document.getFormat(), activity, _document, _hlEditor).getConverter(); _shareUtil.shareText(converter.convertMarkup(_hlEditor.getText().toString(), _hlEditor.getContext(), false, _document.getFile()), "text/" + (item.getItemId() == R.id.action_share_html ? "html" : "plain")); } @@ -426,18 +455,11 @@ public boolean onOptionsItemSelected(final MenuItem item) { case R.id.action_share_calendar_event: { if (saveDocument(false)) { if (!_shareUtil.createCalendarAppointment(_document.getTitle(), _hlEditor.getText().toString(), null)) { - Toast.makeText(getActivity(), R.string.no_calendar_app_is_installed, Toast.LENGTH_SHORT).show(); + Toast.makeText(activity, R.string.no_calendar_app_is_installed, Toast.LENGTH_SHORT).show(); } } return true; } - case android.R.id.home: { - final Activity activity = getActivity(); - if (activity != null && saveDocument(false)) { - activity.onBackPressed(); - } - return true; - } case R.id.action_share_screenshot: case R.id.action_share_image: case R.id.action_share_pdf: { @@ -445,7 +467,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { if (saveDocument(false)) { _nextConvertToPrintMode = true; setDocumentViewVisibility(true); - Toast.makeText(getActivity(), R.string.please_wait, Toast.LENGTH_LONG).show(); + Toast.makeText(activity, R.string.please_wait, Toast.LENGTH_LONG).show(); _webView.postDelayed(() -> { if (item.getItemId() == R.id.action_share_pdf && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { _shareUtil.printOrCreatePdfFromWebview(_webView, _document, _hlEditor.getText().toString().contains("beamer\n")); @@ -465,7 +487,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { if (_document != null) { _document.setFormat(itemId); applyTextFormat(itemId); - _appSettings.setDocumentFormat(getPath(), _document.getFormat()); + _appSettings.setDocumentFormat(_document.getPath(), _document.getFormat()); } return true; } @@ -481,11 +503,11 @@ public boolean onOptionsItemSelected(final MenuItem item) { } case R.id.action_attach_color: { - new CommonTextActions(getActivity(), _hlEditor).runAction(CommonTextActions.ACTION_COLOR_PICKER); + new CommonTextActions(activity, _hlEditor).runAction(CommonTextActions.ACTION_COLOR_PICKER); return true; } case R.id.action_attach_date: { - DatetimeFormatDialog.showDatetimeFormatDialog(getActivity(), _hlEditor); + DatetimeFormatDialog.showDatetimeFormatDialog(activity, _hlEditor); return true; } case R.id.action_attach_audio: @@ -493,7 +515,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { 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)); - AttachImageOrLinkDialog.showInsertImageOrLinkDialog(actionId, _document.getFormat(), getActivity(), _hlEditor, _document.getFile()); + AttachImageOrLinkDialog.showInsertImageOrLinkDialog(actionId, _document.getFormat(), activity, _hlEditor, _document.getFile()); return true; } @@ -508,19 +530,19 @@ public void onFsViewerSelected(String request, File file, final Integer lineNumb public void onFsViewerConfig(FilesystemViewerData.Options dopt) { dopt.titleText = R.string.select; } - }, getFragmentManager(), getActivity(), + }, getFragmentManager(), activity, input -> input != null && input.getAbsolutePath().toLowerCase().endsWith(".epub") ); return true; } case R.id.action_speed_read: { - CoolExperimentalStuff.showSpeedReadDialog(getActivity(), _hlEditor.getText().toString()); + CoolExperimentalStuff.showSpeedReadDialog(activity, _hlEditor.getText().toString()); return true; } case R.id.action_wrap_words: { wrapText = !wrapText; wrapTextSetting = wrapText; - _appSettings.setDocumentWrapState(getPath(), wrapTextSetting); + _appSettings.setDocumentWrapState(_document.getPath(), wrapTextSetting); setHorizontalScrollMode(wrapText); updateMenuToggleStates(0); return true; @@ -528,21 +550,21 @@ public void onFsViewerConfig(FilesystemViewerData.Options dopt) { case R.id.action_enable_highlighting: { highlightText = !highlightText; _hlEditor.setHighlightingEnabled(highlightText); - _appSettings.setDocumentHighlightState(getPath(), highlightText); + _appSettings.setDocumentHighlightState(_document.getPath(), highlightText); updateMenuToggleStates(0); return true; } case R.id.action_info: { - if (_document != null && _document.getFile() != null) { + if (_document != null) { saveDocument(false); // In order to have the correct info displayed FileInfoDialog.show(_document.getFile(), getFragmentManager()); } return true; } case R.id.action_set_font_size: { - SearchOrCustomTextDialogCreator.showFontSizeDialog(getActivity(), _appSettings.getDocumentFontSize(getPath()), (newSize) -> { + SearchOrCustomTextDialogCreator.showFontSizeDialog(activity, _appSettings.getDocumentFontSize(_document.getPath()), (newSize) -> { _hlEditor.setTextSize(TypedValue.COMPLEX_UNIT_SP, (float) newSize); - _appSettings.setDocumentFontSize(getPath(), newSize); + _appSettings.setDocumentFontSize(_document.getPath(), newSize); }); } default: { @@ -551,24 +573,18 @@ public void onFsViewerConfig(FilesystemViewerData.Options dopt) { } } - private long _lastChangedThreadStart = 0; - - @OnTextChanged(value = R.id.document__fragment__edit__highlighting_editor, callback = OnTextChanged.Callback.TEXT_CHANGED) + @OnTextChanged(value = R.id.document__fragment__edit__highlighting_editor, callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED) public void onContentEditValueChanged(CharSequence text) { - if ((_lastChangedThreadStart + HISTORY_DELTA) < System.currentTimeMillis()) { - _lastChangedThreadStart = System.currentTimeMillis(); - _hlEditor.postDelayed(() -> { - Activity activity = getActivity(); - if (activity instanceof AppCompatActivity) { - ((AppCompatActivity) activity).supportInvalidateOptionsMenu(); - } - }, HISTORY_DELTA); - } - Activity activity = getActivity(); - if (activity != null && activity instanceof AppCompatActivity) { - ((AppCompatActivity) activity).supportInvalidateOptionsMenu(); - } + checkTextChangeState(); + updateUndoRedoIconStates(); + } + public void checkTextChangeState() { + _isTextChanged = !_document.isContentSame(_hlEditor.getText()); + + if (_saveMenuItem != null && _saveMenuItem.isEnabled() != _isTextChanged) { + _saveMenuItem.setEnabled(_isTextChanged).getIcon().mutate().setAlpha(_isTextChanged ? 255 : 40); + } } public void applyTextFormat(final int textFormatId) { @@ -584,7 +600,7 @@ public void applyTextFormat(final int textFormatId) { } private void setupAppearancePreferences(View fragmentView) { - _hlEditor.setTextSize(TypedValue.COMPLEX_UNIT_SP, _appSettings.getDocumentFontSize(getPath())); + _hlEditor.setTextSize(TypedValue.COMPLEX_UNIT_SP, _appSettings.getDocumentFontSize(_document.getPath())); _hlEditor.setTypeface(FontPreferenceCompat.typeface(getContext(), _appSettings.getFontFamily(), Typeface.NORMAL)); _hlEditor.setBackgroundColor(_appSettings.getEditorBackgroundColor()); @@ -593,11 +609,10 @@ private void setupAppearancePreferences(View fragmentView) { } private void initDocState() { - final String path = getPath(); - wrapTextSetting = _appSettings.getDocumentWrapState(path); + wrapTextSetting = _appSettings.getDocumentWrapState(_document.getPath()); wrapText = isDisplayedAtMainActivity() || wrapTextSetting; - highlightText = _appSettings.getDocumentHighlightState(path, _hlEditor.getText()); + highlightText = _appSettings.getDocumentHighlightState(_document.getPath(), _hlEditor.getText()); updateMenuToggleStates(0); setHorizontalScrollMode(wrapText); @@ -651,37 +666,20 @@ public String getFragmentTag() { return FRAGMENT_TAG; } - @Override - public boolean onBackPressed() { - final boolean preview = ( - getActivity().getIntent().getBooleanExtra(DocumentActivity.EXTRA_DO_PREVIEW, false) - || _appSettings.getDocumentPreviewState(getPath()) - || _document.getFile().getName().startsWith("index.")); - if (_isPreviewVisible && !preview) { - setDocumentViewVisibility(false); - return true; - } else if (!_isPreviewVisible && preview) { - setDocumentViewVisibility(true); - return true; - } else if (_menuSearchViewForViewMode != null && !_menuSearchViewForViewMode.isIconified()) { - _menuSearchViewForViewMode.clearFocus(); - return true; - } - return false; - } - // Save the file // Only supports java.io.File. TODO: Android Content public boolean saveDocument(boolean forceSaveEmpty) { - if (isAdded() && _hlEditor != null && _hlEditor.getText() != null) { + // Document is written iff content has changed + // _isTextChanged implies _document != null && _hlEditor != null && _hlEditor.getText() != null + if (_isTextChanged && isAdded()) { - if (_document != null && _document.getFile() != null) { - _appSettings.setLastEditPosition(_document.getFile(), _hlEditor.getSelectionStart()); - _appSettings.setDocumentPreviewState(getPath(), _isPreviewVisible); - } + _appSettings.setLastEditPosition(_document.getFile(), _hlEditor.getSelectionStart()); - updateLauncherWidgets(); - return _document.saveContent(getContext(), _hlEditor.getText().toString(), _shareUtil, forceSaveEmpty); + if (_document.saveContent(getContext(), _hlEditor.getText().toString(), _shareUtil, forceSaveEmpty)) { + updateLauncherWidgets(); + checkTextChangeState(); + return true; + } } return false; } @@ -704,8 +702,9 @@ public void onSaveInstanceState(@NonNull Bundle outState) { @Override public void onPause() { saveDocument(false); - if (_document != null && _document.getFile() != null) { + if (_document != null) { _appSettings.addRecentDocument(_document.getFile()); + _appSettings.setDocumentPreviewState(_document.getPath(), _isPreviewVisible); } super.onPause(); } @@ -719,6 +718,12 @@ private void updateLauncherWidgets() { @Override public void setUserVisibleHint(boolean isVisibleToUser) { + // This function can be called _outside_ the normal lifecycle! + // Do nothing if the fragment is not at least created! + if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.CREATED)) { + return; + } + super.setUserVisibleHint(isVisibleToUser); if (isVisibleToUser && isDisplayedAtMainActivity()) { loadDocument(); @@ -739,10 +744,10 @@ public void setUserVisibleHint(boolean isVisibleToUser) { @Override public void onFragmentFirstTimeVisible() { - final boolean initPreview = _appSettings.getDocumentPreviewState(getPath()); + final boolean initPreview = _appSettings.getDocumentPreviewState(_document.getPath()); if (_savedInstanceState == null || !_savedInstanceState.containsKey(SAVESTATE_CURSOR_POS) && _hlEditor.length() > 0) { int lastPos; - if (_document != null && _document.getFile() != null && (lastPos = _appSettings.getLastEditPositionChar(_document.getFile())) >= 0 && lastPos <= _hlEditor.length()) { + if (_document != null && (lastPos = _appSettings.getLastEditPositionChar(_document.getFile())) >= 0 && lastPos <= _hlEditor.length()) { if (!initPreview) { _hlEditor.requestFocus(); } @@ -757,23 +762,25 @@ public void onFragmentFirstTimeVisible() { } public void setDocumentViewVisibility(boolean show) { + final Activity activity = getActivity(); if (!show) { _webViewClient.setRestoreScrollY(_webView.getScrollY()); - } - if (show) { + } else { _textFormat.getConverter().convertMarkupShowInWebView(_document, _hlEditor.getText().toString(), _webView, _nextConvertToPrintMode); - new ActivityUtils(getActivity()).hideSoftKeyboard().freeContextRef(); + new ActivityUtils(activity).hideSoftKeyboard().freeContextRef(); _hlEditor.clearFocus(); - _hlEditor.postDelayed(() -> new ActivityUtils(getActivity()).hideSoftKeyboard().freeContextRef(), 300); + _hlEditor.postDelayed(() -> new ActivityUtils(activity).hideSoftKeyboard().freeContextRef(), 300); } + _nextConvertToPrintMode = false; _webView.setAlpha(0); _webView.setVisibility(show ? View.VISIBLE : View.GONE); if (show) { _webView.animate().setDuration(150).alpha(1.0f).setListener(null); } + _isPreviewVisible = show; - ((AppCompatActivity) getActivity()).supportInvalidateOptionsMenu(); + ((AppCompatActivity) activity).supportInvalidateOptionsMenu(); } final View.OnLongClickListener _longClickToTopOrBottom = new View.OnLongClickListener() { @@ -805,10 +812,6 @@ public Document getDocument() { return _document; } - public String getPath() { - return Document.getPath(_document); - } - public WebView getWebview() { return _webView; } diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java b/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java index e0b762a98a..1f42ad9c96 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java @@ -281,7 +281,7 @@ private void showInDocumentActivity(Document document) { if (getActivity() instanceof DocumentActivity) { DocumentActivity a = (DocumentActivity) getActivity(); a.setDocument(document); - a.showTextEditor(document, null, false, _appSettings.getDocumentPreviewState(Document.getPath(document)), null); + a.showTextEditor(document, null, false, _appSettings.getDocumentPreviewState(document.getPath()), null); } } 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 ff4c365e4b..f4b9543d13 100644 --- a/app/src/main/java/net/gsantner/markor/activity/MainActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/MainActivity.java @@ -79,7 +79,6 @@ public class MainActivity extends MarkorBaseActivity implements FilesystemViewer private SectionsPagerAdapter _viewPagerAdapter; private boolean _doubleBackToExitPressedOnce; - private MenuItem _lastBottomMenuItem; private AppSettings _appSettings; private ActivityUtils _contextUtils; @@ -136,11 +135,7 @@ protected void onCreate(Bundle savedInstanceState) { (new ActivityUtils(this)).applySpecialLaunchersVisibility(_appSettings.isSpecialFileLaunchersEnabled()); - _bottomNav.postDelayed(() -> { - if (_appSettings.getAppStartupTab() != R.id.nav_notebook) { - _bottomNav.setSelectedItemId(_appSettings.getAppStartupTab()); - } - }, 1); + _bottomNav.postDelayed(() -> _bottomNav.setSelectedItemId(_appSettings.getAppStartupTab()), 10); } private void optShowRate() { @@ -168,7 +163,7 @@ public boolean onOptionsItemSelected(MenuItem item) { AppSettings as = new AppSettings(this); switch (item.getItemId()) { case R.id.action_preview: { - File f = _bottomNav.getSelectedItemId() == R.id.nav_quicknote ? as.getQuickNoteFile() : as.getTodoFile(); + final File f = _bottomNav.getSelectedItemId() == R.id.nav_quicknote ? as.getQuickNoteFile() : as.getTodoFile(); DocumentActivity.launch(MainActivity.this, f, false, true, null, null); return true; } @@ -331,38 +326,12 @@ public void onBackPressed() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { - updateFabVisibility(item.getItemId() == R.id.nav_notebook); - PermissionChecker permc = new PermissionChecker(this); - - switch (item.getItemId()) { - case R.id.nav_notebook: { - _viewPager.setCurrentItem(0); - _toolbar.setTitle(getFileBrowserTitle()); - return true; - } + _viewPager.setCurrentItem(tabIdToPos(item.getItemId())); + return true; + } - case R.id.nav_todo: { - permc.doIfExtStoragePermissionGranted(); // cannot prevent bottom tab selection - restoreDefaultToolbar(); - _viewPager.setCurrentItem(1); - _toolbar.setTitle(R.string.todo); - return true; - } - case R.id.nav_quicknote: { - permc.doIfExtStoragePermissionGranted(); // cannot prevent bottom tab selection - restoreDefaultToolbar(); - _viewPager.setCurrentItem(2); - _toolbar.setTitle(R.string.quicknote); - return true; - } - case R.id.nav_more: { - restoreDefaultToolbar(); - _viewPager.setCurrentItem(3); - _toolbar.setTitle(R.string.more); - return true; - } - } - return false; + public void setMainTitle(final String title) { + _toolbar.setTitle(title); } public void updateFabVisibility(boolean visible) { @@ -382,17 +351,41 @@ public String getFileBrowserTitle() { return title; } + public int tabIdToPos(final int id) { + if (id == R.id.nav_notebook) return 0; + if (id == R.id.nav_todo) return 1; + if (id == R.id.nav_quicknote) return 2; + if (id == R.id.nav_more) return 3; + return 0; + } + + public int getCurrentPos() { + return _viewPager.getCurrentItem(); + } + + public String getPosTitle(final int pos) { + if (pos == 0) return getFileBrowserTitle(); + if (pos == 1) return getString(R.string.todo); + if (pos == 2) return getString(R.string.quicknote); + if (pos == 3) return getString(R.string.more); + return ""; + } + @OnPageChange(value = R.id.main__view_pager_container, callback = OnPageChange.Callback.PAGE_SELECTED) public void onViewPagerPageSelected(int pos) { - Menu menu = _bottomNav.getMenu(); - PermissionChecker permc = new PermissionChecker(this); - (_lastBottomMenuItem != null ? _lastBottomMenuItem : menu.getItem(0)).setChecked(false); - _lastBottomMenuItem = menu.getItem(pos).setChecked(true); - updateFabVisibility(pos == 0); - _toolbar.setTitle(new String[]{getFileBrowserTitle(), getString(R.string.todo), getString(R.string.quicknote), getString(R.string.more)}[pos]); + _bottomNav.getMenu().getItem(pos).setChecked(true); + + updateFabVisibility(pos == tabIdToPos(R.id.nav_notebook)); + + setMainTitle(getPosTitle(pos)); - if (pos > 0 && pos < 3) { - permc.doIfExtStoragePermissionGranted(); // cannot prevent bottom tab selection + if (pos != tabIdToPos(R.id.nav_notebook)) { + restoreDefaultToolbar(); + } + + if (pos == tabIdToPos(R.id.nav_quicknote) || pos == tabIdToPos(R.id.nav_todo)) { + // cannot prevent bottom tab selection + new PermissionChecker(this).doIfExtStoragePermissionGranted(); } } @@ -422,7 +415,9 @@ public void onFsViewerConfig(FilesystemViewerData.Options dopt) { public void onFsViewerDoUiUpdate(FilesystemViewerAdapter adapter) { if (adapter != null && adapter.getCurrentFolder() != null && !TextUtils.isEmpty(adapter.getCurrentFolder().getName())) { _appSettings.setFileBrowserLastBrowsedFolder(adapter.getCurrentFolder()); - _toolbar.setTitle(adapter.areItemsSelected() ? "" : getFileBrowserTitle()); + if (getCurrentPos() == tabIdToPos(R.id.nav_notebook)) { + _toolbar.setTitle(adapter.areItemsSelected() ? "" : getFileBrowserTitle()); + } invalidateOptionsMenu(); } } diff --git a/app/src/main/java/net/gsantner/markor/format/TextConverter.java b/app/src/main/java/net/gsantner/markor/format/TextConverter.java index 969ab758f2..4dcfabc170 100644 --- a/app/src/main/java/net/gsantner/markor/format/TextConverter.java +++ b/app/src/main/java/net/gsantner/markor/format/TextConverter.java @@ -84,7 +84,7 @@ public String convertMarkupShowInWebView(Document document, String content, WebV } String baseFolder = new AppSettings(context).getNotebookDirectoryAsStr(); - if (document.getFile() != null && document.getFile().getParentFile() != null) { + if (document.getFile().getParentFile() != null) { baseFolder = document.getFile().getParent(); } baseFolder = "file://" + baseFolder + "/"; diff --git a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextActions.java b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextActions.java index e82f849ba8..e08101b611 100644 --- a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextActions.java +++ b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextActions.java @@ -232,7 +232,7 @@ public boolean onLongClick(View view) { case R.string.tmaid_common_indent: { SearchOrCustomTextDialogCreator.showIndentSizeDialog(_activity, _indent, (size) -> { _indent = Integer.parseInt(size); - _appSettings.setDocumentIndentSize(getPath(), _indent); + _appSettings.setDocumentIndentSize(_document.getPath(), _indent); }); return true; } diff --git a/app/src/main/java/net/gsantner/markor/format/plaintext/PlaintextTextActions.java b/app/src/main/java/net/gsantner/markor/format/plaintext/PlaintextTextActions.java index abb73ced0e..c2468bc5e2 100644 --- a/app/src/main/java/net/gsantner/markor/format/plaintext/PlaintextTextActions.java +++ b/app/src/main/java/net/gsantner/markor/format/plaintext/PlaintextTextActions.java @@ -100,7 +100,7 @@ public boolean onLongClick(View v) { case R.string.tmaid_common_indent: { SearchOrCustomTextDialogCreator.showIndentSizeDialog(_activity, _indent, (size) -> { _indent = Integer.parseInt(size); - _appSettings.setDocumentIndentSize(getPath(), _indent); + _appSettings.setDocumentIndentSize(_document.getPath(), _indent); }); return true; } diff --git a/app/src/main/java/net/gsantner/markor/model/Document.java b/app/src/main/java/net/gsantner/markor/model/Document.java index 3330da3cf6..c3ac456b0f 100644 --- a/app/src/main/java/net/gsantner/markor/model/Document.java +++ b/app/src/main/java/net/gsantner/markor/model/Document.java @@ -13,6 +13,7 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.text.TextUtils; import android.util.Log; @@ -29,8 +30,11 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; import java.security.SecureRandom; import java.util.Locale; @@ -50,11 +54,15 @@ public class Document implements Serializable { private final String _fileExtension; private int _format = TextFormat.FORMAT_UNKNOWN; private String _title = ""; + private String _path = ""; private long _modTime = 0; private int _intentLineNumber = -1; - private String _lastHash = null; - public Document(File file) { + // Used to check if string changed + private long _lastHash = 0; + private int _lastLength = -1; + + public Document(@NonNull final File file) { _file = file; final String name = _file.getName(); final int doti = name.lastIndexOf("."); @@ -65,9 +73,10 @@ public Document(File file) { _fileExtension = name.substring(doti).toLowerCase(); _title = name.substring(0, doti); } + _path = _file.getAbsolutePath(); // Set initial format - final String fnlower = getFile().getName().toLowerCase(); + final String fnlower = _file.getName().toLowerCase(); if (TextFormat.CONVERTER_TODOTXT.isFileOutOfThisFormat(fnlower)) { setFormat(TextFormat.FORMAT_TODOTXT); } else if (TextFormat.CONVERTER_KEYVALUE.isFileOutOfThisFormat(fnlower)) { @@ -82,21 +91,21 @@ public Document(File file) { } public String getPath() { - return getPath(this); + return _path; } - public static String getPath(final Document document) { - if (document != null) { - final File file = document.getFile(); - if (file != null) { - return file.getAbsolutePath(); - } - } - return null; + public @NonNull File getFile() { + return _file; } - public File getFile() { - return _file; + public long lastModified() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Files.readAttributes(_file.toPath(), BasicFileAttributes.class).lastModifiedTime().toMillis(); + } + } catch (IOException ignored) { + } + return _file.lastModified(); } public String getTitle() { @@ -108,7 +117,7 @@ public void setTitle(String title) { } public String getName() { - return getFile().getName(); + return _file.getName(); } public int getIntentLineNumber() { @@ -119,7 +128,7 @@ public int getIntentLineNumber() { public boolean equals(Object obj) { if (obj instanceof Document) { Document other = ((Document) obj); - return equalsc(getFile(), other.getFile()) + return equalsc(_file, other._file) && equalsc(getTitle(), other.getTitle()) && (getFormat() == other.getFormat()); } @@ -147,7 +156,7 @@ public static boolean isEncrypted(File file) { } public boolean isEncrypted() { - return isEncrypted(getFile()); + return isEncrypted(_file); } // Try several fallbacks to get a valid file @@ -192,28 +201,37 @@ public static Document fromArguments(Context context, Bundle arguments) { return document; } + private void setContentHash(final CharSequence s) { + _lastLength = s.length(); + _lastHash = FileUtils.crc32(s.toString().getBytes()); + } + + public boolean isContentSame(final CharSequence s) { + return s != null && s.length() == _lastLength && _lastHash == (FileUtils.crc32(s.toString().getBytes())); + } + public synchronized String loadContent(final Context context) { String content; final char[] pw; if (isEncrypted() && (pw = getPasswordWithWarning(context)) != null) { try { - final byte[] encryptedContext = FileUtils.readCloseStreamWithSize(new FileInputStream(getFile()), (int) getFile().length()); + final byte[] encryptedContext = FileUtils.readCloseStreamWithSize(new FileInputStream(_file), (int) _file.length()); if (encryptedContext.length > JavaPasswordbasedCryption.Version.NAME_LENGTH) { content = JavaPasswordbasedCryption.getDecryptedText(encryptedContext, pw); } else { content = new String(encryptedContext, StandardCharsets.UTF_8); } } catch (FileNotFoundException e) { - Log.e(Document.class.getName(), "loadDocument: File " + getFile() + " not found."); + Log.e(Document.class.getName(), "loadDocument: File " + _file + " not found."); content = ""; } catch (JavaPasswordbasedCryption.EncryptionFailedException | IllegalArgumentException e) { Toast.makeText(context, R.string.could_not_decrypt_file_content_wrong_password_or_is_the_file_maybe_not_encrypted, Toast.LENGTH_LONG).show(); - Log.e(Document.class.getName(), "loadDocument: decrypt failed for File " + getFile() + ". " + e.getMessage(), e); + Log.e(Document.class.getName(), "loadDocument: decrypt failed for File " + _file + ". " + e.getMessage(), e); content = ""; } } else { - content = FileUtils.readTextFileFast(getFile()); + content = FileUtils.readTextFileFast(_file); } if (MainActivity.IS_DEBUG_ENABLED) { @@ -227,8 +245,8 @@ public synchronized String loadContent(final Context context) { } // Also set hash and time on load - should prevent unnecessary saves - _lastHash = FileUtils.sha512sum(content.getBytes()); - _modTime = _file.lastModified(); + setContentHash(content); + _modTime = lastModified(); return content; } @@ -272,10 +290,8 @@ public synchronized boolean saveContent(final Context context, final String cont } shareUtil = shareUtil != null ? shareUtil : new ShareUtil(context); - final String newHash = FileUtils.sha512sum(content.getBytes()); - // Don't write same content if base file not changed - if (newHash != null && newHash.equals(_lastHash) && _modTime >= _file.lastModified()) { + if (isContentSame(content) && _modTime >= lastModified()) { return true; } @@ -299,7 +315,7 @@ public synchronized boolean saveContent(final Context context, final String cont }); success = true; } else { - success = FileUtils.writeFile(getFile(), contentAsBytes); + success = FileUtils.writeFile(_file, contentAsBytes); } } catch (JavaPasswordbasedCryption.EncryptionFailedException e) { Log.e(Document.class.getName(), "writeContent: encrypt failed for File " + getPath() + ". " + e.getMessage(), e); @@ -308,8 +324,8 @@ public synchronized boolean saveContent(final Context context, final String cont } if (success) { - _lastHash = newHash; - _modTime = _file.lastModified(); + setContentHash(content); + _modTime = lastModified(); } return success; diff --git a/app/src/main/java/net/gsantner/markor/ui/hleditor/Highlighter.java b/app/src/main/java/net/gsantner/markor/ui/hleditor/Highlighter.java index 09f1f9fafd..efa31eaaa1 100644 --- a/app/src/main/java/net/gsantner/markor/ui/hleditor/Highlighter.java +++ b/app/src/main/java/net/gsantner/markor/ui/hleditor/Highlighter.java @@ -112,13 +112,6 @@ public void generalHighlightRun(final Spannable spannable) { } } - protected String getFilepath() { - if (_document != null && _document.getFile() != null) { - return _document.getFile().getAbsolutePath(); - } - return ""; - } - public AppSettings getAppSettings() { return _appSettings; } diff --git a/app/src/main/java/net/gsantner/markor/ui/hleditor/TextActions.java b/app/src/main/java/net/gsantner/markor/ui/hleditor/TextActions.java index e177740ddb..646fbcac78 100644 --- a/app/src/main/java/net/gsantner/markor/ui/hleditor/TextActions.java +++ b/app/src/main/java/net/gsantner/markor/ui/hleditor/TextActions.java @@ -70,11 +70,7 @@ public TextActions(Activity activity, Document document) { _context = activity != null ? activity : _hlEditor.getContext(); _appSettings = new AppSettings(_context); _textActionSidePadding = (int) (_appSettings.getEditorTextActionItemPadding() * _context.getResources().getDisplayMetrics().density); - _indent = _appSettings.getDocumentIndentSize(getPath()); - } - - public String getPath() { - return Document.getPath(_document); + _indent = _appSettings.getDocumentIndentSize(_document.getPath()); } /** diff --git a/app/src/main/java/net/gsantner/markor/util/ShareUtil.java b/app/src/main/java/net/gsantner/markor/util/ShareUtil.java index dbb201b47e..53e40300bb 100644 --- a/app/src/main/java/net/gsantner/markor/util/ShareUtil.java +++ b/app/src/main/java/net/gsantner/markor/util/ShareUtil.java @@ -34,7 +34,7 @@ public void createLauncherDesktopShortcut(Document document) { // This is only allowed to call when direct file access is possible!! // So basically only for java.io.File Objects. Virtual files, or content:// // in private/restricted space won't work - because of missing permission grant when re-launching - if (document != null && document.getFile() != null && !TextUtils.isEmpty(document.getTitle())) { + if (document != null && !TextUtils.isEmpty(document.getTitle())) { Intent shortcutIntent = new Intent(_context, OpenEditorFromShortcutOrWidgetActivity.class) .setData(Uri.fromFile(document.getFile())); super.createLauncherDesktopShortcut(shortcutIntent, R.drawable.ic_launcher, document.getTitle()); diff --git a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java index 98ba1ef34b..a99976c5aa 100644 --- a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java @@ -31,11 +31,13 @@ import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; +import java.util.zip.CRC32; @SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "deprecation", "TryFinallyCanBeTryWithResources"}) public class FileUtils { @@ -504,18 +506,28 @@ public static File join(File file, String... childSegments) { return file; } - public static String sha512sum(final byte[] bytes) { + private static String hash(final byte[] data, final String alg) { try { - final StringBuilder sb = new StringBuilder(); - for (final byte b : MessageDigest.getInstance("SHA-512").digest(bytes)) { - sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)); - } - return sb.toString(); + return Arrays.toString(MessageDigest.getInstance(alg).digest(data)); } catch (NoSuchAlgorithmException e) { return null; } } + public static String md5(final byte[] data) { + return hash(data, "MD5"); + } + + public static String sha512(final byte[] data) { + return hash(data, "SHA-512"); + } + + public static long crc32(final byte[] data) { + final CRC32 alg = new CRC32(); + alg.update(data); + return alg.getValue(); + } + // Return true if the target file exists, false if there is an issue with the file or it's parent directories public static boolean fileExists(final File checkFile) { File[] files; diff --git a/app/src/main/res/menu/document__edit__menu.xml b/app/src/main/res/menu/document__edit__menu.xml index e2a7697ad2..1e86cc8117 100644 --- a/app/src/main/res/menu/document__edit__menu.xml +++ b/app/src/main/res/menu/document__edit__menu.xml @@ -98,8 +98,7 @@ android:id="@+id/action_save" android:icon="@drawable/ic_save_black_24dp" android:title="@string/save" - app:showAsAction="never" /> - + app:showAsAction="always" /> @color/primary_dark @color/primary_dark @style/AppTheme.PreferenceTheme + @style/ActionButtonStyle @@ -71,4 +73,13 @@ +