Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 13016ea7 authored by stefan-niedermann's avatar stefan-niedermann
Browse files

Implement menu for context based formatting without selected range

parent 985a6cd1
Loading
Loading
Loading
Loading
+5 −3
Original line number Diff line number Diff line
@@ -37,11 +37,12 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.model.CloudNote;
import it.niedermann.owncloud.notes.util.DisplayUtils;
import it.niedermann.owncloud.notes.model.ISyncCallback;
import it.niedermann.owncloud.notes.util.ContextBasedFormattingCallback;
import it.niedermann.owncloud.notes.util.ContextBasedRangeFormattingCallback;
import it.niedermann.owncloud.notes.util.DisplayUtils;
import it.niedermann.owncloud.notes.util.MarkDownUtil;
import it.niedermann.owncloud.notes.util.NotesTextWatcher;
import it.niedermann.owncloud.notes.util.ContextBasedFormattingCallback;

public class NoteEditFragment extends SearchableBaseNoteFragment {

@@ -175,7 +176,8 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
            markdownProcessor.factory(EditFactory.create());
            markdownProcessor.live(editContent);

            editContent.setCustomSelectionActionModeCallback(new ContextBasedFormattingCallback(this.editContent));
            editContent.setCustomSelectionActionModeCallback(new ContextBasedRangeFormattingCallback(this.editContent));
            editContent.setOnCreateContextMenuListener(new ContextBasedFormattingCallback(this.editContent));
            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext().getApplicationContext());
            editContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(sp));
            if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
+4 −160
Original line number Diff line number Diff line
package it.niedermann.owncloud.notes.util;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ContextMenu;
import android.view.View;
import android.widget.EditText;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;

import it.niedermann.owncloud.notes.R;

import static android.content.Context.CLIPBOARD_SERVICE;

public class ContextBasedFormattingCallback implements ActionMode.Callback {
public class ContextBasedFormattingCallback implements View.OnCreateContextMenuListener {

    private static final String TAG = ContextBasedFormattingCallback.class.getCanonicalName();

@@ -34,144 +15,7 @@ public class ContextBasedFormattingCallback implements ActionMode.Callback {
    }

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        MenuInflater inflater = mode.getMenuInflater();
        inflater.inflate(R.menu.style, menu);
        menu.removeItem(android.R.id.selectAll);

        SparseIntArray styleFormatMap = new SparseIntArray();
        styleFormatMap.append(R.id.bold, Typeface.BOLD);
        styleFormatMap.append(R.id.italic, Typeface.ITALIC);

        MenuItem item;
        CharSequence title;
        SpannableStringBuilder ssb;

        for (int i = 0; i < styleFormatMap.size(); i++) {
            item = menu.findItem(styleFormatMap.keyAt(i));
            title = item.getTitle();
            ssb = new SpannableStringBuilder(title);
            ssb.setSpan(new StyleSpan(styleFormatMap.valueAt(i)), 0, title.length(), 0);
            item.setTitle(ssb);
        }

        return true;
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        return false;
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        int start = editText.getSelectionStart();
        int end = editText.getSelectionEnd();
        SpannableStringBuilder ssb = new SpannableStringBuilder(editText.getText());
        final String markdown;


        switch (item.getItemId()) {
            case R.id.bold: {
                markdown = "**";
                if (hasAlreadyMarkdown(start, end, markdown)) {
                    this.removeMarkdown(ssb, start, end, markdown);
                } else {
                    this.addMarkdown(ssb, start, end, markdown, Typeface.BOLD);
                }
                editText.setText(ssb);
                editText.setSelection(end + markdown.length() * 2);
                return true;
            }
            case R.id.italic: {
                markdown = "*";
                if (hasAlreadyMarkdown(start, end, markdown)) {
                    this.removeMarkdown(ssb, start, end, markdown);
                } else {
                    this.addMarkdown(ssb, start, end, markdown, Typeface.ITALIC);
                }
                editText.setText(ssb);
                editText.setSelection(end + markdown.length() * 2);
                return true;
            }
            case R.id.link: {
                boolean textToFormatIsLink = TextUtils.indexOf(editText.getText().subSequence(start, end), "http") == 0;
                if (textToFormatIsLink) {
                    ssb.insert(end, ")");
                    ssb.insert(start, "[](");
                } else {
                    String clipboardURL = getClipboardURLorNull(editText.getContext());
                    if (clipboardURL != null) {
                        ssb.insert(end, "](" + clipboardURL + ")");
                        end += clipboardURL.length();
                    } else {
                        ssb.insert(end, "]()");
                    }
                    ssb.insert(start, "[");
                }
                end++;
                ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1);
                editText.setText(ssb);
                if (textToFormatIsLink) {
                    editText.setSelection(start + 1);
                } else {
                    editText.setSelection(end + 2); // after <end>](
                }
                return true;
            }
            case android.R.id.cut: {
                // https://github.com/stefan-niedermann/nextcloud-notes/issues/604
                // https://github.com/stefan-niedermann/nextcloud-notes/issues/477
                try {
                    editText.onTextContextMenuItem(item.getItemId());
                    return true;
                } catch (IndexOutOfBoundsException e) {
                    e.printStackTrace();
                    editText.setSelection(0, 0);
                    editText.clearFocus();
                    return true;
                }
            }
        }
        return false;
    }

    private static String getClipboardURLorNull(Context context) {
        String clipboardURL = null;
        ClipData clipboardData = Objects.requireNonNull(((ClipboardManager) Objects.requireNonNull(context.getSystemService(CLIPBOARD_SERVICE))).getPrimaryClip());
        if (clipboardData.getItemCount() > 0) {
            try {
                clipboardURL = new URL(clipboardData.getItemAt(0).getText().toString()).toString();
            } catch (MalformedURLException e) {
                Log.d(TAG, "Clipboard does not contain a valid URL: " + clipboardURL);
            }
        }
        return clipboardURL;
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {

    }

    private boolean hasAlreadyMarkdown(int start, int end, String markdown) {
        return start > markdown.length() && markdown.contentEquals(editText.getText().subSequence(start - markdown.length(), start)) &&
                editText.getText().length() > end + markdown.length() && markdown.contentEquals(editText.getText().subSequence(end, end + markdown.length()));
    }

    private void removeMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown) {
        // FIXME disabled, because it does not work properly and might cause data loss
        // ssb.delete(start - markdown.length(), start);
        // ssb.delete(end - markdown.length(), end);
        // ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1);
    }
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {

    private void addMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown, int typeface) {
        ssb.insert(end, markdown);
        ssb.insert(start, markdown);
        editText.getText().charAt(start);
        editText.getText().charAt(start + 1);
        end += markdown.length() * 2;
        ssb.setSpan(new StyleSpan(typeface), start, end, 1);
    }
}
+177 −0
Original line number Diff line number Diff line
package it.niedermann.owncloud.notes.util;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.EditText;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;

import it.niedermann.owncloud.notes.R;

import static android.content.Context.CLIPBOARD_SERVICE;

public class ContextBasedRangeFormattingCallback implements ActionMode.Callback {

    private static final String TAG = ContextBasedRangeFormattingCallback.class.getCanonicalName();

    private EditText editText;

    public ContextBasedRangeFormattingCallback(EditText editText) {
        this.editText = editText;
    }

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        MenuInflater inflater = mode.getMenuInflater();
        inflater.inflate(R.menu.context_based_range_formatting, menu);
        menu.removeItem(android.R.id.selectAll);

        SparseIntArray styleFormatMap = new SparseIntArray();
        styleFormatMap.append(R.id.bold, Typeface.BOLD);
        styleFormatMap.append(R.id.italic, Typeface.ITALIC);

        MenuItem item;
        CharSequence title;
        SpannableStringBuilder ssb;

        for (int i = 0; i < styleFormatMap.size(); i++) {
            item = menu.findItem(styleFormatMap.keyAt(i));
            title = item.getTitle();
            ssb = new SpannableStringBuilder(title);
            ssb.setSpan(new StyleSpan(styleFormatMap.valueAt(i)), 0, title.length(), 0);
            item.setTitle(ssb);
        }

        return true;
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        return false;
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        int start = editText.getSelectionStart();
        int end = editText.getSelectionEnd();
        SpannableStringBuilder ssb = new SpannableStringBuilder(editText.getText());
        final String markdown;


        switch (item.getItemId()) {
            case R.id.bold: {
                markdown = "**";
                if (hasAlreadyMarkdown(start, end, markdown)) {
                    this.removeMarkdown(ssb, start, end, markdown);
                } else {
                    this.addMarkdown(ssb, start, end, markdown, Typeface.BOLD);
                }
                editText.setText(ssb);
                editText.setSelection(end + markdown.length() * 2);
                return true;
            }
            case R.id.italic: {
                markdown = "*";
                if (hasAlreadyMarkdown(start, end, markdown)) {
                    this.removeMarkdown(ssb, start, end, markdown);
                } else {
                    this.addMarkdown(ssb, start, end, markdown, Typeface.ITALIC);
                }
                editText.setText(ssb);
                editText.setSelection(end + markdown.length() * 2);
                return true;
            }
            case R.id.link: {
                boolean textToFormatIsLink = TextUtils.indexOf(editText.getText().subSequence(start, end), "http") == 0;
                if (textToFormatIsLink) {
                    ssb.insert(end, ")");
                    ssb.insert(start, "[](");
                } else {
                    String clipboardURL = getClipboardURLorNull(editText.getContext());
                    if (clipboardURL != null) {
                        ssb.insert(end, "](" + clipboardURL + ")");
                        end += clipboardURL.length();
                    } else {
                        ssb.insert(end, "]()");
                    }
                    ssb.insert(start, "[");
                }
                end++;
                ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1);
                editText.setText(ssb);
                if (textToFormatIsLink) {
                    editText.setSelection(start + 1);
                } else {
                    editText.setSelection(end + 2); // after <end>](
                }
                return true;
            }
            case android.R.id.cut: {
                // https://github.com/stefan-niedermann/nextcloud-notes/issues/604
                // https://github.com/stefan-niedermann/nextcloud-notes/issues/477
                try {
                    editText.onTextContextMenuItem(item.getItemId());
                    return true;
                } catch (IndexOutOfBoundsException e) {
                    e.printStackTrace();
                    editText.setSelection(0, 0);
                    editText.clearFocus();
                    return true;
                }
            }
        }
        return false;
    }

    private static String getClipboardURLorNull(Context context) {
        String clipboardURL = null;
        ClipData clipboardData = Objects.requireNonNull(((ClipboardManager) Objects.requireNonNull(context.getSystemService(CLIPBOARD_SERVICE))).getPrimaryClip());
        if (clipboardData.getItemCount() > 0) {
            try {
                clipboardURL = new URL(clipboardData.getItemAt(0).getText().toString()).toString();
            } catch (MalformedURLException e) {
                Log.d(TAG, "Clipboard does not contain a valid URL: " + clipboardURL);
            }
        }
        return clipboardURL;
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {

    }

    private boolean hasAlreadyMarkdown(int start, int end, String markdown) {
        return start > markdown.length() && markdown.contentEquals(editText.getText().subSequence(start - markdown.length(), start)) &&
                editText.getText().length() > end + markdown.length() && markdown.contentEquals(editText.getText().subSequence(end, end + markdown.length()));
    }

    private void removeMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown) {
        // FIXME disabled, because it does not work properly and might cause data loss
        // ssb.delete(start - markdown.length(), start);
        // ssb.delete(end - markdown.length(), end);
        // ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1);
    }

    private void addMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown, int typeface) {
        ssb.insert(end, markdown);
        ssb.insert(start, markdown);
        editText.getText().charAt(start);
        editText.getText().charAt(start + 1);
        end += markdown.length() * 2;
        ssb.setSpan(new StyleSpan(typeface), start, end, 1);
    }
}
+11 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/checkbox"
        android:title="@string/simple_checkbox"
        app:showAsAction="ifRoom" />
    <item android:id="@+id/link"
        android:icon="@drawable/ic_insert_link_black_24dp"
        android:title="@string/simple_link"
        app:showAsAction="ifRoom" />
</menu>
 No newline at end of file
Loading