/**************************************************************************************** * Copyright (c) 2015 Ryan Annis <squeenix@live.ca> * * Copyright (c) 2015 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.os.Bundle; import android.support.v7.widget.Toolbar; import android.text.InputType; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; import android.widget.Toast; import com.afollestad.materialdialogs.MaterialDialog; import com.ichi2.anim.ActivityTransitionAnimation; import com.ichi2.anki.dialogs.ConfirmationDialog; import com.ichi2.anki.dialogs.ModelEditorContextMenu; import com.ichi2.anki.exception.ConfirmModSchemaException; import com.ichi2.async.DeckTask; import com.ichi2.libanki.Collection; import com.ichi2.themes.StyledProgressDialog; import com.ichi2.widget.WidgetStatus; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; public class ModelFieldEditor extends AnkiActivity { private final static int NORMAL_EXIT = 100001; //Position of the current field selected private int mCurrentPos; private ListView mFieldLabelView; private ArrayList<String> mFieldLabels; private MaterialDialog mProgressDialog; private Collection mCol; private JSONArray mNoteFields; private JSONObject mMod; private ModelEditorContextMenu mContextMenu; private EditText mFieldNameInput; private Runnable mConfirmDialogCancel = new Runnable() { @Override public void run() { dismissContextMenu(); } }; // ---------------------------------------------------------------------------- // ANDROID METHODS // ---------------------------------------------------------------------------- @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.model_field_editor); startLoadingCollection(); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); mFieldLabelView = (ListView) findViewById(R.id.note_type_editor_fields); if (toolbar != null) { setSupportActionBar(toolbar); } if (getSupportActionBar() != null) { getSupportActionBar().setTitle(R.string.model_field_editor_title); getSupportActionBar().setSubtitle(getIntent().getStringExtra("title")); } } @Override protected void onStop() { super.onStop(); if (!isFinishing()) { WidgetStatus.update(this); UIUtils.saveCollectionInBackground(this); } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.model_editor, menu); return true; } // ---------------------------------------------------------------------------- // ANKI METHODS // ---------------------------------------------------------------------------- @Override protected void onCollectionLoaded(Collection col) { this.mCol = col; setupLabels(); createfieldLabels(); } // ---------------------------------------------------------------------------- // UI SETUP // ---------------------------------------------------------------------------- /* * Sets up the main ListView and ArrayAdapters * Containing clickable labels for the fields */ private void createfieldLabels() { ArrayAdapter<String> mFieldLabelAdapter = new ArrayAdapter<>(this, R.layout.model_field_editor_list_item, mFieldLabels); mFieldLabelView.setAdapter(mFieldLabelAdapter); mFieldLabelView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { mContextMenu = ModelEditorContextMenu.newInstance(mFieldLabels.get(position), mContextMenuListener); showDialogFragment(mContextMenu); mCurrentPos = position; } }); } /* * Sets up the ArrayList containing the text for the main ListView */ private void setupLabels() { long noteTypeID = getIntent().getLongExtra("noteTypeID", 0); mMod = mCol.getModels().get(noteTypeID); mFieldLabels = new ArrayList<>(); try { mNoteFields = mMod.getJSONArray("flds"); for (int i = 0; i < mNoteFields.length(); i++) { JSONObject o = mNoteFields.getJSONObject(i); mFieldLabels.add(o.getString("name")); } } catch (JSONException e) { throw new RuntimeException(e); } } // ---------------------------------------------------------------------------- // CONTEXT MENU DIALOGUES // ---------------------------------------------------------------------------- /* * Creates a dialog to rename the currently selected field, short loading ti * Processing time scales with number of items */ private void addFieldDialog() { mFieldNameInput = new EditText(this); mFieldNameInput.setSingleLine(true); new MaterialDialog.Builder(this) .title(R.string.model_field_editor_add) .positiveText(R.string.dialog_ok) .customView(mFieldNameInput, true) .callback(new MaterialDialog.ButtonCallback() { @Override public void onPositive(MaterialDialog dialog) { String fieldName = mFieldNameInput.getText().toString() .replaceAll("[\'\"\\n\\r\\[\\]\\(\\)]", ""); if (fieldName.length() == 0) { showToast(getResources().getString(R.string.toast_empty_name)); } else if (containsField(fieldName)) { showToast(getResources().getString(R.string.toast_duplicate_field)); } else { //Name is valid, now field is added try { mCol.modSchema(); DeckTask.launchDeckTask(DeckTask.TASK_TYPE_ADD_FIELD, mChangeFieldHandler, new DeckTask.TaskData(new Object[]{mMod, fieldName})); } catch (ConfirmModSchemaException e) { //Create dialogue to for schema change ConfirmationDialog c = new ConfirmationDialog(); c.setArgs(getResources().getString(R.string.full_sync_confirmation)); Runnable confirm = new Runnable() { @Override public void run() { mCol.modSchemaNoCheck(); String fieldName = mFieldNameInput.getText().toString() .replaceAll("[\'\"\\n\\r\\[\\]\\(\\)]", ""); DeckTask.launchDeckTask(DeckTask.TASK_TYPE_ADD_FIELD, mChangeFieldHandler, new DeckTask.TaskData(new Object[]{mMod, fieldName})); dismissContextMenu(); } }; c.setConfirm(confirm); c.setCancel(mConfirmDialogCancel); ModelFieldEditor.this.showDialogFragment(c); } mCol.getModels().update(mMod); fullRefreshList(); } } }) .negativeText(R.string.dialog_cancel) .show(); } /* * Creates a dialog to rename the currently selected field, short loading ti * Processing time scales with number of items */ private void deleteFieldDialog() { Runnable confirm = new Runnable() { @Override public void run() { try { mCol.modSchema(false); deleteField(); } catch (ConfirmModSchemaException e) { //This should never be reached because modSchema() didn't throw an exception } dismissContextMenu(); } }; if (mFieldLabels.size() < 2) { showToast(getResources().getString(R.string.toast_last_field)); } else { try { mCol.modSchema(); ConfirmationDialog d = new ConfirmationDialog(); d.setArgs(getResources().getString(R.string.field_delete_warning)); d.setConfirm(confirm); d.setCancel(mConfirmDialogCancel); showDialogFragment(d); } catch (ConfirmModSchemaException e) { ConfirmationDialog c = new ConfirmationDialog(); c.setConfirm(confirm); c.setCancel(mConfirmDialogCancel); c.setArgs(getResources().getString(R.string.full_sync_confirmation)); showDialogFragment(c); } } } private void deleteField() { try { DeckTask.launchDeckTask(DeckTask.TASK_TYPE_DELETE_FIELD, mChangeFieldHandler, new DeckTask.TaskData(new Object[]{mMod, mNoteFields.getJSONObject(mCurrentPos)})); } catch (JSONException e) { throw new RuntimeException(e); } } /* * Creates a dialog to rename the currently selected field, short loading ti * Processing time is constant */ private void renameFieldDialog() { mFieldNameInput = new EditText(this); mFieldNameInput.setSingleLine(true); mFieldNameInput.setText(mFieldLabels.get(mCurrentPos)); mFieldNameInput.setSelection(mFieldNameInput.getText().length()); new MaterialDialog.Builder(this) .title(R.string.rename_model) .positiveText(R.string.rename) .customView(mFieldNameInput, true) .callback(new MaterialDialog.ButtonCallback() { @Override public void onPositive(MaterialDialog dialog) { String fieldLabel = mFieldNameInput.getText().toString() .replaceAll("[\'\"\\n\\r\\[\\]\\(\\)]", ""); if (fieldLabel.length() == 0) { showToast(getResources().getString(R.string.toast_empty_name)); } else if (containsField(fieldLabel)) { showToast(getResources().getString(R.string.toast_duplicate_field)); } else { //Field is valid, now rename try { renameField(); } catch (ConfirmModSchemaException e) { // Handler mod schema confirmation ConfirmationDialog c = new ConfirmationDialog(); c.setArgs(getResources().getString(R.string.full_sync_confirmation)); Runnable confirm = new Runnable() { @Override public void run() { try { mCol.modSchema(false); renameField(); } catch (ConfirmModSchemaException e) { //This should never be thrown } dismissContextMenu(); } }; c.setConfirm(confirm); c.setCancel(mConfirmDialogCancel); ModelFieldEditor.this.showDialogFragment(c); } } } }) .negativeText(R.string.dialog_cancel) .show(); } /* * Allows the user to select a number less than the number of fields in the current model to * reposition the current field to * Processing time is scales with number of items */ private void repositionFieldDialog() { mFieldNameInput = new EditText(this); mFieldNameInput.setRawInputType(InputType.TYPE_CLASS_NUMBER); new MaterialDialog.Builder(this) .title(String.format(getResources().getString(R.string.model_field_editor_reposition), 1, mFieldLabels.size())) .positiveText(R.string.dialog_ok) .customView(mFieldNameInput, true) .callback(new MaterialDialog.ButtonCallback() { @Override public void onPositive(MaterialDialog dialog) { String newPosition = mFieldNameInput.getText().toString(); int pos; try { pos = Integer.parseInt(newPosition); } catch (NumberFormatException n) { showToast(getResources().getString(R.string.toast_out_of_range)); return; } if (pos < 1 || pos > mFieldLabels.size()) { showToast(getResources().getString(R.string.toast_out_of_range)); } else { // Input is valid, now attempt to modify try { mCol.modSchema(); DeckTask.launchDeckTask(DeckTask.TASK_TYPE_REPOSITION_FIELD, mChangeFieldHandler, new DeckTask.TaskData(new Object[]{mMod, mNoteFields.getJSONObject(mCurrentPos), pos - 1})); } catch (ConfirmModSchemaException e) { // Handle mod schema confirmation ConfirmationDialog c = new ConfirmationDialog(); c.setArgs(getResources().getString(R.string.full_sync_confirmation)); Runnable confirm = new Runnable() { @Override public void run() { try { mCol.modSchemaNoCheck(); String newPosition = mFieldNameInput.getText().toString(); int pos = Integer.parseInt(newPosition); DeckTask.launchDeckTask(DeckTask.TASK_TYPE_REPOSITION_FIELD, mChangeFieldHandler, new DeckTask.TaskData(new Object[]{mMod, mNoteFields.getJSONObject(mCurrentPos),pos - 1})); dismissContextMenu(); } catch (JSONException e) { throw new RuntimeException(e); } } }; c.setConfirm(confirm); c.setCancel(mConfirmDialogCancel); ModelFieldEditor.this.showDialogFragment(c); } catch (JSONException e) { throw new RuntimeException(e); } } } }) .negativeText(R.string.dialog_cancel) .show(); } // ---------------------------------------------------------------------------- // HELPER METHODS // ---------------------------------------------------------------------------- /* * Useful when a confirmation dialog is created within another dialog */ private void dismissContextMenu() { if (mContextMenu != null) { mContextMenu.dismiss(); mContextMenu = null; } } private void dismissProgressBar() { if (mProgressDialog != null) { mProgressDialog.dismiss(); } mProgressDialog = null; } /* * Renames the current field */ private void renameField() throws ConfirmModSchemaException { try { String fieldLabel = mFieldNameInput.getText().toString() .replaceAll("[\'\"\\n\\r\\[\\]\\(\\)]", ""); JSONObject field = (JSONObject) mNoteFields.get(mCurrentPos); mCol.getModels().renameField(mMod, field, fieldLabel); mCol.getModels().save(); fullRefreshList(); } catch (JSONException e) { throw new RuntimeException(); } } /* * Changes the sort field (that displays in card browser) to the current field */ private void sortByField() { try { mCol.modSchema(); DeckTask.launchDeckTask(DeckTask.TASK_TYPE_CHANGE_SORT_FIELD, mChangeFieldHandler, new DeckTask.TaskData(new Object[]{mMod, mCurrentPos})); } catch (ConfirmModSchemaException e) { // Handler mMod schema confirmation ConfirmationDialog c = new ConfirmationDialog(); c.setArgs(getResources().getString(R.string.full_sync_confirmation)); Runnable confirm = new Runnable() { @Override public void run() { mCol.modSchemaNoCheck(); DeckTask.launchDeckTask(DeckTask.TASK_TYPE_CHANGE_SORT_FIELD, mChangeFieldHandler, new DeckTask.TaskData(new Object[]{mMod, mCurrentPos})); dismissContextMenu(); } }; c.setConfirm(confirm); c.setCancel(mConfirmDialogCancel); ModelFieldEditor.this.showDialogFragment(c); } } /* * Reloads everything */ private void fullRefreshList() { setupLabels(); createfieldLabels(); } /* * Checks if there exists a field with this name in the current model */ private boolean containsField(String field) { for (String s : mFieldLabels) { if (field.compareTo(s) == 0) { return true; } } return false; } private void showToast(CharSequence text) { int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(this, text, duration); toast.show(); } // ---------------------------------------------------------------------------- // HANDLERS // ---------------------------------------------------------------------------- /* * Called during the desk task when any field is modified */ private DeckTask.TaskListener mChangeFieldHandler = new DeckTask.TaskListener() { @Override public void onCancelled() { //This decktask can not be interrupted } @Override public void onPreExecute() { if (mProgressDialog == null) { mProgressDialog = StyledProgressDialog.show(ModelFieldEditor.this, getIntent().getStringExtra("title"), getResources().getString(R.string.model_field_editor_changing), false); } } @Override public void onPostExecute(DeckTask.TaskData result) { if (!result.getBoolean()) { closeActivity(DeckPicker.RESULT_DB_ERROR); } dismissProgressBar(); fullRefreshList(); } @Override public void onProgressUpdate(DeckTask.TaskData... values) { //This decktask does not publish updates } }; @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: onBackPressed(); return true; case R.id.action_add_new_model: addFieldDialog(); return true; default: return super.onOptionsItemSelected(item); } } public void closeActivity() { closeActivity(NORMAL_EXIT); } private void closeActivity(int reason) { switch (reason) { case NORMAL_EXIT: finishWithAnimation(ActivityTransitionAnimation.RIGHT); break; default: finishWithAnimation(ActivityTransitionAnimation.RIGHT); break; } } @Override public void onBackPressed() { closeActivity(); } private MaterialDialog.ListCallback mContextMenuListener = new MaterialDialog.ListCallback() { @Override public void onSelection(MaterialDialog materialDialog, View view, int selection, CharSequence charSequence) { switch (selection) { case ModelEditorContextMenu.SORT_FIELD: sortByField(); break; case ModelEditorContextMenu.FIELD_REPOSITION: repositionFieldDialog(); break; case ModelEditorContextMenu.FIELD_DELETE: deleteFieldDialog(); break; case ModelEditorContextMenu.FIELD_RENAME: renameFieldDialog(); break; } } }; }