com.ichi2.anki.CardTemplateEditor.java Source code

Java tutorial

Introduction

Here is the source code for com.ichi2.anki.CardTemplateEditor.java

Source

/***************************************************************************************
 *                                                                                      *
 * Copyright (c) 2014 Timothy Rae <perceptualchaos2@gmail.com>                          *
 *                                                                                      *
 * This program is free software; you can redistribute it and/or modify it under        *
 * the terms of the GNU General Public License as published by the Free Software        *
 * Foundation; either version 3 of the License, or (at your option) any later           *
 * version.                                                                             *
 *                                                                                      *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
 *                                                                                      *
 * You should have received a copy of the GNU General Public License along with         *
 * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
 ****************************************************************************************/

package com.ichi2.anki;

import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;

import com.afollestad.materialdialogs.MaterialDialog;
import com.ichi2.anim.ActivityTransitionAnimation;
import com.ichi2.anki.dialogs.ConfirmationDialog;
import com.ichi2.anki.exception.ConfirmModSchemaException;
import com.ichi2.async.DeckTask;
import com.ichi2.libanki.Card;
import com.ichi2.libanki.Collection;
import com.ichi2.libanki.Models;
import com.ichi2.libanki.Note;
import com.ichi2.themes.Themes;
import com.ichi2.ui.SlidingTabLayout;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import timber.log.Timber;

/**
 * Allows the user to view the template for the current note type
 */
public class CardTemplateEditor extends AnkiActivity {
    private TemplatePagerAdapter mTemplateAdapter;
    private JSONObject mModelBackup = null;
    private ViewPager mViewPager;
    private SlidingTabLayout mSlidingTabLayout;
    private long mModelId;
    private long mNoteId;
    private static final int REQUEST_PREVIEWER = 0;
    private static final String DUMMY_TAG = "DUMMY_NOTE_TO_DELETE_x0-90-fa";

    // ----------------------------------------------------------------------------
    // Listeners
    // ----------------------------------------------------------------------------

    /* Used for updating the collection when a reverse card is added */
    private DeckTask.TaskListener mUpdateTemplateHandler = new DeckTask.TaskListener() {
        @Override
        public void onPreExecute() {
            showProgressBar();
        }

        @Override
        public void onProgressUpdate(DeckTask.TaskData... values) {
        }

        @Override
        public void onPostExecute(DeckTask.TaskData result) {
            hideProgressBar();
            if (result.getBoolean()) {
                // Refresh the GUI -- setting the last template as the active tab
                try {
                    selectTemplate(getCol().getModels().get(mModelId).getJSONArray("tmpls").length());
                } catch (JSONException e) {
                    throw new RuntimeException(e);
                }
            } else if (result.getString() != null && result.getString().equals("removeTemplateFailed")) {
                // Failed to remove template
                String message = getResources().getString(R.string.card_template_editor_would_delete_note);
                Themes.showThemedToast(CardTemplateEditor.this, message, false);
            } else {
                // RuntimeException occurred
                setResult(RESULT_CANCELED);
                finishWithoutAnimation();
            }
        }

        @Override
        public void onCancelled() {
            hideProgressBar();
        }
    };

    // ----------------------------------------------------------------------------
    // ANDROID METHODS
    // ----------------------------------------------------------------------------

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Timber.d("onCreate()");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.card_template_editor_activity);
        // Load the args either from the intent or savedInstanceState bundle
        if (savedInstanceState == null) {
            // get model id
            mModelId = getIntent().getLongExtra("modelId", -1L);
            if (mModelId == -1) {
                Timber.e("CardTemplateEditor :: no model ID was provided");
                finishWithoutAnimation();
                return;
            }
            // get id for currently edited card (optional)
            mNoteId = getIntent().getLongExtra("noteId", -1L);
        } else {
            mModelId = savedInstanceState.getLong("modelId");
            mNoteId = savedInstanceState.getLong("noteId");
            try {
                mModelBackup = new JSONObject(savedInstanceState.getString("modelBackup"));
            } catch (JSONException e) {
                Timber.e(e, "Malformed model in savedInstanceState");
            }
        }

        // Disable the home icon
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        if (toolbar != null) {
            setSupportActionBar(toolbar);
        }
        startLoadingCollection();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        if (mModelBackup != null) {
            outState.putString("modelBackup", mModelBackup.toString());
        }
        outState.putLong("modelId", mModelId);
        outState.putLong("noteId", mNoteId);
        super.onSaveInstanceState(outState);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home: {
            if (modelHasChanged()) {
                showDiscardChangesDialog();
            } else {
                finishWithAnimation(ActivityTransitionAnimation.RIGHT);
            }
            return true;
        }
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void showProgressBar() {
        super.showProgressBar();
        findViewById(R.id.progress_description).setVisibility(View.VISIBLE);
        findViewById(R.id.fragment_parent).setVisibility(View.INVISIBLE);
    }

    @Override
    public void hideProgressBar() {
        super.hideProgressBar();
        findViewById(R.id.progress_description).setVisibility(View.INVISIBLE);
        findViewById(R.id.fragment_parent).setVisibility(View.VISIBLE);
    }

    /**
     * Callback used to finish initializing the activity after the collection has been correctly loaded
     * @param col Collection which has been loaded
     */
    @Override
    protected void onCollectionLoaded(Collection col) {
        super.onCollectionLoaded(col);
        // Create the adapter that will return a fragment for each of the three
        // primary sections of the activity.
        mTemplateAdapter = new TemplatePagerAdapter(getSupportFragmentManager());
        mTemplateAdapter.setModel(col.getModels().get(mModelId));
        // Set up the ViewPager with the sections adapter.
        mViewPager = (ViewPager) findViewById(R.id.pager);
        mViewPager.setAdapter(mTemplateAdapter);
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(final int position, final float v, final int i2) {
            }

            @Override
            public void onPageSelected(final int position) {
                CardTemplateFragment fragment = (CardTemplateFragment) mTemplateAdapter.instantiateItem(mViewPager,
                        position);
                if (fragment != null) {
                    fragment.updateCss();
                }
            }

            @Override
            public void onPageScrollStateChanged(final int position) {
            }
        });
        mSlidingTabLayout = (SlidingTabLayout) findViewById(R.id.sliding_tabs);
        mSlidingTabLayout.setViewPager(mViewPager);
        // Set activity title
        if (getSupportActionBar() != null) {
            getSupportActionBar().setTitle(R.string.title_activity_template_editor);
            getSupportActionBar().setSubtitle(col.getModels().get(mModelId).optString("name"));
        }
        // Make backup of the model for cancellation purposes
        try {
            if (mModelBackup == null) {
                mModelBackup = new JSONObject(col.getModels().get(mModelId).toString());
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        // Close collection opening dialog if needed
        Timber.i("CardTemplateEditor:: Card template editor successfully started for model id %d", mModelId);
    }

    public boolean modelHasChanged() {
        JSONObject newModel = getCol().getModels().get(mModelId);
        return mModelBackup != null && !mModelBackup.toString().equals(newModel.toString());
    }

    private void showDiscardChangesDialog() {
        new MaterialDialog.Builder(this).content(R.string.discard_unsaved_changes).positiveText(R.string.dialog_ok)
                .negativeText(R.string.dialog_cancel).callback(new MaterialDialog.ButtonCallback() {
                    @Override
                    public void onPositive(MaterialDialog dialog) {
                        Timber.i("TemplateEditor:: OK button pressed to confirm discard changes");
                        getCol().getModels().update(CardTemplateEditor.this.mModelBackup);
                        getCol().getModels().flush();
                        getCol().reset();
                        finishWithAnimation(ActivityTransitionAnimation.RIGHT);
                    }
                }).build().show();
    }

    // ----------------------------------------------------------------------------
    // CUSTOM METHODS
    // ----------------------------------------------------------------------------

    /**
     * Refresh list of templates and select position
     * @param idx index of template to select
     */
    public void selectTemplate(int idx) {
        // invalidate all existing fragments
        mTemplateAdapter.notifyChangeInPosition(1);
        // notify of new data set
        mTemplateAdapter.notifyDataSetChanged();
        // reload the list of tabs
        mSlidingTabLayout.setViewPager(mViewPager);
        // select specified tab
        mViewPager.setCurrentItem(idx);
    }

    // ----------------------------------------------------------------------------
    // INNER CLASSES
    // ----------------------------------------------------------------------------

    /**
     * A {@link android.support.v4.app.FragmentPagerAdapter} that returns a fragment corresponding to
     * one of the tabs.
     */
    public class TemplatePagerAdapter extends FragmentPagerAdapter {
        private JSONObject mModel;
        private long baseId = 0;

        public TemplatePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        //this is called when notifyDataSetChanged() is called
        @Override
        public int getItemPosition(Object object) {
            // refresh all tabs when data set changed
            return PagerAdapter.POSITION_NONE;
        }

        @Override
        public Fragment getItem(int position) {
            return CardTemplateFragment.newInstance(position, mModelId, mNoteId);
        }

        @Override
        public long getItemId(int position) {
            // give an ID different from position when position has been changed
            return baseId + position;
        }

        @Override
        public int getCount() {
            try {
                return mModel.getJSONArray("tmpls").length();
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public CharSequence getPageTitle(int position) {
            try {
                return mModel.getJSONArray("tmpls").getJSONObject(position).getString("name");
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Notify that the position of a fragment has been changed.
         * Create a new ID for each position to force recreation of the fragment
         * @see <a href="http://stackoverflow.com/questions/10396321/remove-fragment-page-from-viewpager-in-android/26944013#26944013">stackoverflow</a>
         * @param n number of items which have been changed
         */
        public void notifyChangeInPosition(int n) {
            // shift the ID returned by getItemId outside the range of all previous fragments
            baseId += getCount() + n;
        }

        public void setModel(JSONObject model) {
            mModel = model;
        }
    }

    public static class CardTemplateFragment extends Fragment {
        EditText mFront;
        EditText mCss;
        EditText mBack;
        JSONObject mModel;

        public static CardTemplateFragment newInstance(int position, long modelId, long noteId) {
            CardTemplateFragment f = new CardTemplateFragment();
            Bundle args = new Bundle();
            args.putInt("position", position);
            args.putLong("modelId", modelId);
            args.putLong("noteId", noteId);
            f.setArguments(args);
            return f;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View mainView = inflater.inflate(R.layout.card_template_editor_item, container, false);
            final int position = getArguments().getInt("position");
            try {
                // Load template
                long mid = getArguments().getLong("modelId");
                mModel = ((AnkiActivity) getActivity()).getCol().getModels().get(mid);
                final JSONArray tmpls = mModel.getJSONArray("tmpls");
                final JSONObject template = tmpls.getJSONObject(position);
                // Load EditText Views
                mFront = ((EditText) mainView.findViewById(R.id.front_edit));
                mCss = ((EditText) mainView.findViewById(R.id.styling_edit));
                mBack = ((EditText) mainView.findViewById(R.id.back_edit));
                // Set EditText content
                mFront.setText(template.getString("qfmt"));
                mCss.setText(mModel.getString("css"));
                mBack.setText(template.getString("afmt"));
                // Set text change listeners
                TextWatcher templateEditorWatcher = new TextWatcher() {
                    @Override
                    public void afterTextChanged(Editable arg0) {
                        try {
                            template.put("qfmt", mFront.getText());
                            template.put("afmt", mBack.getText());
                            mModel.put("css", mCss.getText());
                            tmpls.put(position, template);
                            mModel.put("tmpls", tmpls);
                        } catch (JSONException e) {
                            Timber.e(e, "Could not update card template");
                        }
                    }

                    @Override
                    public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
                    }

                    @Override
                    public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
                    }
                };
                mFront.addTextChangedListener(templateEditorWatcher);
                mCss.addTextChangedListener(templateEditorWatcher);
                mBack.addTextChangedListener(templateEditorWatcher);
                // Enable menu
                setHasOptionsMenu(true);
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
            return mainView;
        }

        @Override
        public void onResume() {
            super.onResume();
        }

        private void updateCss() {
            if (mCss != null && mModel != null) {
                try {
                    mCss.setText(mModel.getString("css"));
                } catch (JSONException e) {
                    // do nothing
                }
            }
        }

        @Override
        public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
            inflater.inflate(R.menu.card_template_editor, menu);
            super.onCreateOptionsMenu(menu, inflater);
        }

        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            final Collection col = ((AnkiActivity) getActivity()).getCol();
            final JSONObject model = getModel();
            switch (item.getItemId()) {
            case R.id.action_add:
                Timber.i("CardTemplateEditor:: Add template button pressed");
                addNewTemplateWithCheck(getModel());
                return true;
            case R.id.action_delete: {
                Timber.i("CardTemplateEditor:: Delete template button pressed");
                Resources res = getResources();
                int position = getArguments().getInt("position");
                try {
                    JSONArray tmpls = model.getJSONArray("tmpls");
                    final JSONObject template = tmpls.getJSONObject(position);
                    // Don't do anything if only one template
                    if (tmpls.length() < 2) {
                        Themes.showThemedToast(getActivity(),
                                res.getString(R.string.card_template_editor_cant_delete), false);
                        return true;
                    }
                    // Show confirmation dialog
                    int numAffectedCards = col.getModels().tmplUseCount(model, position);
                    confirmDeleteCards(template, model, numAffectedCards);
                } catch (JSONException e) {
                    throw new RuntimeException(e);
                }
                return true;
            }
            case R.id.action_preview: {
                Timber.i("CardTemplateEditor:: Preview model button pressed");
                // Save the model if necessary
                if (modelHasChanged()) {
                    col.getModels().save(model, false);
                }
                // Create intent for the previewer and add some arguments
                Intent i = new Intent(getActivity(), Previewer.class);
                int pos = getArguments().getInt("position");
                long cid;
                if (getArguments().getLong("noteId") != -1L
                        && pos < col.getNote(getArguments().getLong("noteId")).cards().size()) {
                    // Give the card ID if we started from an actual note and it has a card generated in this pos
                    cid = col.getNote(getArguments().getLong("noteId")).cards().get(pos).getId();
                } else {
                    // Otherwise create a dummy card to show the effect of formatting
                    Card dummyCard = getDummyCard();
                    if (dummyCard != null) {
                        cid = dummyCard.getId();
                    } else {
                        ((AnkiActivity) getActivity()).showSimpleSnackbar(R.string.invalid_template, false);
                        return true;
                    }
                }
                // Launch intent
                i.putExtra("currentCardId", cid);
                startActivityForResult(i, REQUEST_PREVIEWER);
                return true;
            }
            case R.id.action_confirm:
                Timber.i("CardTemplateEditor:: Save model button pressed");
                if (modelHasChanged()) {
                    // regenerate the cards of the model
                    DeckTask.TaskData args = new DeckTask.TaskData(new Object[] { model });
                    DeckTask.launchDeckTask(DeckTask.TASK_TYPE_SAVE_MODEL, mSaveModelAndExitHandler, args);
                } else {
                    ((AnkiActivity) getActivity()).finishWithAnimation(ActivityTransitionAnimation.RIGHT);
                }

                return true;
            default:
                return super.onOptionsItemSelected(item);
            }
        }

        /**
         * Get a dummy card
         * @return
         */
        private Card getDummyCard() {
            Timber.d("Creating dummy note");
            JSONObject model = getCol().getModels().get(getArguments().getLong("modelId"));
            Note n = getCol().newNote(model);
            ArrayList<String> fieldNames = getCol().getModels().fieldNames(model);
            for (int i = 0; i < fieldNames.size(); i++) {
                n.setField(i, fieldNames.get(i));
            }
            n.addTag(DUMMY_TAG);
            getCol().addNote(n);
            if (n.cards().size() <= getArguments().getInt("position")) {
                return null;
            }
            return getCol().getCard(n.cards().get(getArguments().getInt("position")).getId());
        }

        private void deleteDummyCards() {
            // TODO: make into an async task
            List<Long> remnantNotes = getCol().findNotes("tag:" + DUMMY_TAG);
            if (remnantNotes.size() > 0) {
                long[] nids = new long[remnantNotes.size()];
                for (int i = 0; i < remnantNotes.size(); i++) {
                    nids[i] = remnantNotes.get(i);
                }
                getCol().remNotes(nids);
                getCol().save();
            }
        }

        @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            deleteDummyCards();
        }

        /* Used for updating the collection when a model has been edited */
        private DeckTask.TaskListener mSaveModelAndExitHandler = new DeckTask.TaskListener() {
            @Override
            public void onPreExecute() {
                ((AnkiActivity) getActivity()).showProgressBar();
                final InputMethodManager imm = (InputMethodManager) getActivity()
                        .getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
            }

            @Override
            public void onProgressUpdate(DeckTask.TaskData... values) {
            }

            @Override
            public void onPostExecute(DeckTask.TaskData result) {
                if (result.getBoolean()) {
                    getActivity().setResult(RESULT_OK);
                    ((AnkiActivity) getActivity()).finishWithAnimation(ActivityTransitionAnimation.RIGHT);
                } else {
                    // RuntimeException occurred
                    getActivity().setResult(RESULT_CANCELED);
                    ((AnkiActivity) getActivity()).finishWithoutAnimation();
                }
            }

            @Override
            public void onCancelled() {
            }
        };

        private boolean modelHasChanged() {
            return ((CardTemplateEditor) getActivity()).modelHasChanged();
        }

        /**
         * Load the model from the collection
         * @return the model we are editing
         */
        private JSONObject getModel() {
            long mid = getArguments().getLong("modelId");
            return ((AnkiActivity) getActivity()).getCol().getModels().get(mid);
        }

        /**
         * Get the collection
         * @return
         */
        private Collection getCol() {
            return ((AnkiActivity) getActivity()).getCol();
        }

        /**
         * Confirm if the user wants to delete all the cards associated with current template
         *
         * @param tmpl template to remove
         * @param model model to remove from
         * @param numAffectedCards number of cards which will be affected
         */
        private void confirmDeleteCards(final JSONObject tmpl, final JSONObject model, int numAffectedCards) {
            ConfirmationDialog d = new ConfirmationDialog() {
                @Override
                public void confirm() {
                    deleteTemplateWithCheck(tmpl, model);
                }
            };
            Resources res = getResources();
            String msg = String.format(
                    res.getQuantityString(R.plurals.card_template_editor_confirm_delete, numAffectedCards),
                    numAffectedCards, tmpl.optString("name"));
            d.setArgs(msg);
            ((AnkiActivity) getActivity()).showDialogFragment(d);
        }

        /**
         * Delete tmpl from model, asking user to confirm again if it's going to require a full sync
         *
         * @param tmpl template to remove
         * @param model model to remove from
         */
        private void deleteTemplateWithCheck(final JSONObject tmpl, final JSONObject model) {
            try {
                ((CardTemplateEditor) getActivity()).getCol().modSchema(true);
                deleteTemplate(tmpl, model);
            } catch (ConfirmModSchemaException e) {
                ConfirmationDialog d = new ConfirmationDialog() {
                    @Override
                    public void confirm() {
                        deleteTemplate(tmpl, model);
                    }
                };
                d.setArgs(getResources().getString(R.string.full_sync_confirmation));
                ((AnkiActivity) getActivity()).showDialogFragment(d);
            }
        }

        /**
         * Launch background task to delete tmpl from model
         * @param tmpl template to remove
         * @param model model to remove from
         */
        private void deleteTemplate(JSONObject tmpl, JSONObject model) {
            CardTemplateEditor activity = ((CardTemplateEditor) getActivity());
            activity.getCol().modSchemaNoCheck();
            Object[] args = new Object[] { model, tmpl };
            DeckTask.launchDeckTask(DeckTask.TASK_TYPE_REMOVE_TEMPLATE, activity.mUpdateTemplateHandler,
                    new DeckTask.TaskData(args));
            activity.dismissAllDialogFragments();
        }

        /**
         * Add new template to model, asking user to confirm if it's going to require a full sync
         *
         * @param model model to add new template to
         */
        private void addNewTemplateWithCheck(final JSONObject model) {
            try {
                ((CardTemplateEditor) getActivity()).getCol().modSchema(true);
                addNewTemplate(model);
            } catch (ConfirmModSchemaException e) {
                ConfirmationDialog d = new ConfirmationDialog() {
                    @Override
                    public void confirm() {
                        addNewTemplate(model);
                    }
                };
                d.setArgs(getResources().getString(R.string.full_sync_confirmation));
                ((AnkiActivity) getActivity()).showDialogFragment(d);
            }
        }

        /**
         * Launch background task to add new template to model
         * @param model model to add new template to
         */
        private void addNewTemplate(JSONObject model) {
            CardTemplateEditor activity = ((CardTemplateEditor) getActivity());
            activity.getCol().modSchemaNoCheck();
            Models mm = activity.getCol().getModels();
            // Build new template
            JSONObject newTemplate;
            try {
                int oldPosition = getArguments().getInt("position");
                JSONArray templates = model.getJSONArray("tmpls");
                JSONObject oldTemplate = templates.getJSONObject(oldPosition);
                newTemplate = mm.newTemplate(newCardName(templates));
                // Set up question & answer formats
                newTemplate.put("qfmt", oldTemplate.get("qfmt"));
                newTemplate.put("afmt", oldTemplate.get("afmt"));
                // Reverse the front and back if only one template
                if (templates.length() == 1) {
                    flipQA(newTemplate);
                }
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
            // Add new template to the current model via AsyncTask
            Object[] args = new Object[] { model, newTemplate };
            DeckTask.launchDeckTask(DeckTask.TASK_TYPE_ADD_TEMPLATE, activity.mUpdateTemplateHandler,
                    new DeckTask.TaskData(args));
            activity.dismissAllDialogFragments();
        }

        /**
         * Flip the question and answer side of the template
         * @param template template to flip
         */
        private void flipQA(JSONObject template) {
            try {
                String qfmt = template.getString("qfmt");
                String afmt = template.getString("afmt");
                Matcher m = Pattern.compile("(?s)(.+)<hr id=answer>(.+)").matcher(afmt);
                if (!m.find()) {
                    template.put("qfmt", afmt.replace("{{FrontSide}}", ""));
                } else {
                    template.put("qfmt", m.group(2).trim());
                }
                template.put("afmt", "{{FrontSide}}\n\n<hr id=answer>\n\n" + qfmt);
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Get name for new template
         * @param templates array of templates which is being added to
         * @return name for new template
         */
        private String newCardName(JSONArray templates) {
            String name;
            // Start by trying to set the name to "Card n" where n is the new num of templates
            int n = templates.length() + 1;
            // If the starting point for name already exists, iteratively increase n until we find a unique name
            while (true) {
                // Get new name
                name = "Card " + Integer.toString(n);
                // Cycle through all templates checking if new name exists
                boolean exists = false;
                for (int i = 0; i < templates.length(); i++) {
                    try {
                        exists = exists || name.equals(templates.getJSONObject(i).getString("name"));
                    } catch (JSONException e) {
                        throw new RuntimeException(e);
                    }
                }
                if (!exists) {
                    break;
                }
                n += 1;
            }
            return name;
        }
    }
}