package com.door43.translationstudio.newui.translate; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; import android.content.ClipData; import android.content.ContentValues; import android.content.Context; import android.graphics.Bitmap; import android.os.Handler; import android.os.Looper; import android.support.design.widget.Snackbar; import android.support.design.widget.TabLayout; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.Selection; import android.text.TextUtils; import android.text.TextWatcher; import android.view.DragEvent; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.Switch; import android.widget.TextView; import com.door43.tools.reporting.Logger; import com.door43.translationstudio.R; import com.door43.translationstudio.core.Chapter; import com.door43.translationstudio.core.ChapterTranslation; import com.door43.translationstudio.core.CheckingQuestion; import com.door43.translationstudio.core.FileHistory; import com.door43.translationstudio.core.Frame; import com.door43.translationstudio.core.FrameTranslation; import com.door43.translationstudio.core.Library; import com.door43.translationstudio.core.LinedEditText; import com.door43.translationstudio.core.ProjectTranslation; import com.door43.translationstudio.core.TranslationNote; import com.door43.translationstudio.core.SourceLanguage; import com.door43.translationstudio.core.SourceTranslation; import com.door43.translationstudio.core.TargetLanguage; import com.door43.translationstudio.core.TargetTranslation; import com.door43.translationstudio.core.TranslationFormat; import com.door43.translationstudio.core.TranslationWord; import com.door43.translationstudio.core.Translator; import com.door43.translationstudio.core.Typography; import com.door43.translationstudio.dialogs.CustomAlertDialog; import com.door43.translationstudio.rendering.Clickables; import com.door43.translationstudio.rendering.DefaultRenderer; import com.door43.translationstudio.rendering.RenderingGroup; import com.door43.translationstudio.AppContext; import com.door43.translationstudio.rendering.ClickableRenderingEngine; import com.door43.translationstudio.spannables.NoteSpan; import com.door43.translationstudio.spannables.USFMNoteSpan; import com.door43.translationstudio.spannables.Span; import com.door43.translationstudio.spannables.USFMVerseSpan; import com.door43.translationstudio.spannables.VerseSpan; import com.door43.util.tasks.ThreadableUI; import com.door43.widget.ViewUtil; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.revwalk.RevCommit; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Created by joel on 9/18/2015. */ public class ReviewModeAdapter extends ViewModeAdapter<ReviewModeAdapter.ViewHolder> { private static final String TAG = ReviewModeAdapter.class.getSimpleName(); private static final int TAB_NOTES = 0; private static final int TAB_WORDS = 1; private static final int TAB_QUESTIONS = 2; public static final String UNDO = "Undo"; public static final String REDO = "Redo"; public static final String OPTIONS = "Options"; public static final String ISO_DATE_FORMAT = "yyyy-MM-dd HH:mm"; private final Library mLibrary; private final Translator mTranslator; private final Activity mContext; private final TargetTranslation mTargetTranslation; private HashMap<String, Chapter> mChapters; private HashMap<String, Frame> mFrames; private SourceTranslation mSourceTranslation; private SourceLanguage mSourceLanguage; private final TargetLanguage mTargetLanguage; private ListItem[] mListItems; private int mLayoutBuildNumber = 0; private boolean mResourcesOpened = false; private ContentValues[] mTabs; private int[] mOpenResourceTab; private boolean mAllowFootnote = true; // private boolean onBind = false; public ReviewModeAdapter(Activity context, String targetTranslationId, String sourceTranslationId, String startingChapterSlug, String startingFrameSlug, boolean resourcesOpened) { mLibrary = AppContext.getLibrary(); mTranslator = AppContext.getTranslator(); mContext = context; mTargetTranslation = mTranslator.getTargetTranslation(targetTranslationId); mSourceTranslation = mLibrary.getSourceTranslation(sourceTranslationId); boolean usfm = mTargetTranslation.getFormat() == TranslationFormat.USFM; mAllowFootnote = usfm; mSourceLanguage = mLibrary.getSourceLanguage(mSourceTranslation.projectSlug, mSourceTranslation.sourceLanguageSlug); mTargetLanguage = mLibrary.getTargetLanguage(mTargetTranslation.getTargetLanguageId()); mResourcesOpened = resourcesOpened; Chapter[] chapters = mLibrary.getChapters(mSourceTranslation); mFrames = new HashMap<>(); mChapters = new HashMap<>(); List<ListItem> listItems = new ArrayList<>(); // add project title card ListItem projectTitleItem = new ListItem(null, null); projectTitleItem.isProjectTitle = true; listItems.add(projectTitleItem); for(Chapter c:chapters) { // put in map for easier retrieval mChapters.put(c.getId(), c); String[] chapterFrameSlugs = mLibrary.getFrameSlugs(mSourceTranslation, c.getId()); boolean setStartPosition = startingChapterSlug != null && c.getId().equals(startingChapterSlug) && chapterFrameSlugs.length > 0; // default starting selection is first item in chapter if(setStartPosition) { setListStartPosition(listItems.size()); } // add title and reference cards for chapter if(!c.title.isEmpty()) { ListItem item = new ListItem(null, c.getId()); item.isChapterTitle = true; listItems.add(item); } if(!c.reference.isEmpty()) { ListItem item = new ListItem(null, c.getId()); item.isChapterReference = true; listItems.add(item); } // identify starting frame selection for(String frameSlug:chapterFrameSlugs) { if(setStartPosition && startingFrameSlug != null && frameSlug.equals(startingFrameSlug)) { setListStartPosition(listItems.size()); } listItems.add(new ListItem(frameSlug, c.getId())); } } mListItems = listItems.toArray(new ListItem[listItems.size()]); mOpenResourceTab = new int[listItems.size()]; loadTabInfo(); } @Override void rebuild() { mLayoutBuildNumber ++; notifyDataSetChanged(); } /** * Rebuilds the card tabs */ private void loadTabInfo() { List<ContentValues> tabContents = new ArrayList<>(); String[] sourceTranslationIds = AppContext.getOpenSourceTranslationIds(mTargetTranslation.getId()); for(String id:sourceTranslationIds) { SourceTranslation sourceTranslation = mLibrary.getSourceTranslation(id); if(sourceTranslation != null) { ContentValues values = new ContentValues(); // include the resource id if there are more than one if(mLibrary.getResources(sourceTranslation.projectSlug, sourceTranslation.sourceLanguageSlug).length > 1) { values.put("title", sourceTranslation.getSourceLanguageTitle() + " " + sourceTranslation.resourceSlug.toUpperCase()); } else { values.put("title", sourceTranslation.getSourceLanguageTitle()); } values.put("tag", sourceTranslation.getId()); tabContents.add(values); } } mTabs = tabContents.toArray(new ContentValues[tabContents.size()]); } @Override void setSourceTranslation(String sourceTranslationId) { mSourceTranslation = mLibrary.getSourceTranslation(sourceTranslationId); mSourceLanguage = mLibrary.getSourceLanguage(mSourceTranslation.projectSlug, mSourceTranslation.sourceLanguageSlug); Chapter[] chapters = mLibrary.getChapters(mSourceTranslation); List<ListItem> listItems = new ArrayList<>(); // add project title card ListItem projectTitleItem = new ListItem(null, null); projectTitleItem.isProjectTitle = true; listItems.add(projectTitleItem); mFrames = new HashMap<>(); mChapters = new HashMap<>(); for(Chapter c:chapters) { // add title and reference cards for chapter if(!c.title.isEmpty()) { ListItem item = new ListItem(null, c.getId()); item.isChapterTitle = true; listItems.add(item); } if(!c.reference.isEmpty()) { ListItem item = new ListItem(null, c.getId()); item.isChapterReference = true; listItems.add(item); } // put in map for easier retrieval mChapters.put(c.getId(), c); String[] chapterFrameSlugs = mLibrary.getFrameSlugs(mSourceTranslation, c.getId()); for(String frameSlug:chapterFrameSlugs) { listItems.add(new ListItem(frameSlug, c.getId())); } } mListItems = listItems.toArray(new ListItem[listItems.size()]); mOpenResourceTab = new int[listItems.size()]; loadTabInfo(); notifyDataSetChanged(); } @Override void onCoordinate(final ViewHolder holder) { int durration = 400; float openWeight = 1f; float closedWeight = 0.765f; ObjectAnimator anim; if(mResourcesOpened) { holder.mResourceLayout.setVisibility(View.VISIBLE); anim = ObjectAnimator.ofFloat(holder.mMainContent, "weightSum", openWeight, closedWeight); } else { holder.mResourceLayout.setVisibility(View.INVISIBLE); anim = ObjectAnimator.ofFloat(holder.mMainContent, "weightSum", closedWeight, openWeight); } anim.setDuration(durration); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { holder.mMainContent.requestLayout(); } }); anim.start(); } @Override public String getFocusedFrameId(int position) { if(position >= 0 && position < mListItems.length) { return mListItems[position].frameSlug; } return null; } @Override public String getFocusedChapterId(int position) { if(position >= 0 && position < mListItems.length) { return mListItems[position].chapterSlug; } return null; } @Override public int getItemPosition(String chapterId, String frameId) { for(int i = 0; i < mListItems.length; i ++) { ListItem item = mListItems[i]; if(item.isFrame() && item.chapterSlug.equals(chapterId) && item.frameSlug.equals(frameId)) { return i; } } return -1; } @Override public void reload() { setSourceTranslation(mSourceTranslation.getId()); } @Override public ViewHolder onCreateManagedViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_review_list_item, parent, false); ViewHolder vh = new ViewHolder(parent.getContext(), v); return vh; } /** * Loads a frame from the index and caches it * @param chapterSlug * @param frameSlug * @return */ private Frame loadFrame(String chapterSlug, String frameSlug) { String complexSlug = chapterSlug + "-" + frameSlug; if(mFrames.containsKey(complexSlug)) { return mFrames.get(complexSlug); } else { Frame frame = mLibrary.getFrame(mSourceTranslation, chapterSlug, frameSlug); mFrames.put(complexSlug, frame); return frame; } } @Override public void onBindViewHolder(final ViewHolder holder, final int position) { // this.onBind = true; final ListItem item = mListItems[position]; // open/close resources if(mResourcesOpened) { holder.mMainContent.setWeightSum(.765f); } else { holder.mMainContent.setWeightSum(1f); } // fetch translation from disk item.loadTranslations(mSourceTranslation, mTargetTranslation, mChapters.get(item.chapterSlug), loadFrame(item.chapterSlug, item.frameSlug)); ViewUtil.makeLinksClickable(holder.mSourceBody); // render the cards renderSourceCard(position, item, holder); renderTargetCard(position, item, holder); renderResourceCard(position, item, holder); // set up fonts if(holder.mLayoutBuildNumber != mLayoutBuildNumber) { holder.mLayoutBuildNumber = mLayoutBuildNumber; Typography.format(mContext, holder.mSourceBody, mSourceLanguage.getId(), mSourceLanguage.getDirection()); Typography.formatSub(mContext, holder.mTargetTitle, mTargetLanguage.getId(), mTargetLanguage.getDirection()); Typography.format(mContext, holder.mTargetBody, mTargetLanguage.getId(), mTargetLanguage.getDirection()); Typography.format(mContext, holder.mTargetEditableBody, mTargetLanguage.getId(), mTargetLanguage.getDirection()); } // this.onBind = false; } /** * Returns the preferred translation notes. * if none exist in the source language it will return the english version * @param frame * @return */ private static TranslationNote[] getPreferredNotes(SourceTranslation sourceTranslation, Frame frame) { Library library = AppContext.getLibrary(); TranslationNote[] notes = library.getTranslationNotes(sourceTranslation, frame.getChapterId(), frame.getId()); if(notes.length == 0 && !sourceTranslation.sourceLanguageSlug.equals("en")) { SourceTranslation defaultSourceTranslation = library.getDefaultSourceTranslation(sourceTranslation.projectSlug, "en"); notes = library.getTranslationNotes(defaultSourceTranslation, frame.getChapterId(), frame.getId()); } return notes; } /** * Returns the preferred translation words. * if none exist in the source language it will return the english version * @param sourceTranslation * @param frame * @return */ private static TranslationWord[] getPreferredWords(SourceTranslation sourceTranslation, Frame frame) { Library library = AppContext.getLibrary(); TranslationWord[] words = library.getTranslationWords(sourceTranslation, frame.getChapterId(), frame.getId()); if(words.length == 0 && !sourceTranslation.sourceLanguageSlug.equals("en")) { SourceTranslation defaultSourceTranslation = library.getDefaultSourceTranslation(sourceTranslation.projectSlug, "en"); words = library.getTranslationWords(defaultSourceTranslation, frame.getChapterId(), frame.getId()); } return words; } /** * Returns the preferred checking questions. * if none exist in the source language it will return the english version * @param sourceTranslation * @param chapterId * @param frameId * @return */ private static CheckingQuestion[] getPreferredQuestions(SourceTranslation sourceTranslation, String chapterId, String frameId) { Library library = AppContext.getLibrary(); CheckingQuestion[] questions = library.getCheckingQuestions(sourceTranslation, chapterId, frameId); if(questions.length == 0 && !sourceTranslation.sourceLanguageSlug.equals("en")) { SourceTranslation defaultSourceTranslation = library.getDefaultSourceTranslation(sourceTranslation.projectSlug, "en"); questions = library.getCheckingQuestions(defaultSourceTranslation, chapterId, frameId); } return questions; } private void renderSourceCard(int position, final ListItem item, ViewHolder holder) { // render if(item.renderedSourceBody == null) { item.renderedSourceBody = renderSourceText(item.bodySource, mSourceTranslation.getFormat(), holder, item, false); } holder.mSourceBody.setText(item.renderedSourceBody); } private void renderTargetCard(int position, final ListItem item, final ViewHolder holder) { final Frame frame; if(item.isFrame()) { frame = loadFrame(item.chapterSlug, item.frameSlug); } else { frame = null; } final Chapter chapter; if(item.isFrame() || item.isChapter()) { chapter = mChapters.get(item.chapterSlug); } else { chapter = null; } // remove old text watcher if(holder.mEditableTextWatcher != null) { holder.mTargetEditableBody.removeTextChangedListener(holder.mEditableTextWatcher); } if(item.renderedTargetBody == null) { renderTargetBody(item, holder, frame); } // insert rendered text if(item.isEditing) { // editing mode holder.mTargetEditableBody.setText(item.renderedTargetBody); } else { // verse marker mode holder.mTargetBody.setText(item.renderedTargetBody); holder.mTargetBody.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { v.onTouchEvent(event); v.clearFocus(); return true; } }); ViewUtil.makeLinksClickable(holder.mTargetBody); } // render title String targetTitle = ""; if(item.isChapter()) { targetTitle = mSourceTranslation.getProjectTitle() + " " + Integer.parseInt(chapter.getId()) + " - " + mTargetLanguage.name; } else if(item.isFrame()) { ChapterTranslation chapterTranslation = mTargetTranslation.getChapterTranslation(mChapters.get(item.chapterSlug)); targetTitle = chapterTranslation.title; if(targetTitle.isEmpty()) { targetTitle = chapter.title; if (targetTitle.isEmpty()) { targetTitle = mSourceTranslation.getProjectTitle() + " " + Integer.parseInt(chapter.getId()); } } targetTitle += ":" + frame.getTitle() + " - " + mTargetLanguage.name; } else if(item.isProjectTitle) { targetTitle = mTargetTranslation.getTargetLanguageName(); } holder.mTargetTitle.setText(targetTitle); // set up text watcher holder.mEditableTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { String translation = applyChangedText(s, holder, item); // commit immediately if editing history FileHistory history = item.getFileHistory(mTargetTranslation); if(!history.isAtHead()) { history.reset(); prepareTranslationUI(holder, item); } } @Override public void afterTextChanged(Editable s) { } }; if(item.isEditing) { holder.mTargetEditableBody.addTextChangedListener(holder.mEditableTextWatcher); } holder.mUndoButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { undoTextInTarget(holder, item); } }); holder.mRedoButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { redoTextInTarget(holder, item); } }); // editing button final GestureDetector detector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { item.isEditing = !item.isEditing; prepareTranslationUI(holder, item); if(item.isEditing) { holder.mTargetEditableBody.requestFocus(); InputMethodManager mgr = (InputMethodManager)mContext.getSystemService(Context.INPUT_METHOD_SERVICE); mgr.showSoftInput(holder.mTargetEditableBody, InputMethodManager.SHOW_IMPLICIT); // TRICKY: there may be changes to translation item.loadTranslations(mSourceTranslation, mTargetTranslation, chapter, frame); // re-render for editing mode item.renderedTargetBody = renderSourceText(item.bodyTranslation, item.translationFormat, holder, item, true); holder.mTargetEditableBody.setText(item.renderedTargetBody); holder.mTargetEditableBody.addTextChangedListener(holder.mEditableTextWatcher); } else { if(holder.mEditableTextWatcher != null) { holder.mTargetEditableBody.removeTextChangedListener(holder.mEditableTextWatcher); } holder.mTargetBody.requestFocus(); getListener().closeKeyboard(); // TRICKY: there may be changes to translation item.loadTranslations(mSourceTranslation, mTargetTranslation, chapter, frame); // re-render for verse mode item.renderedTargetBody = renderTargetText(item.bodyTranslation, item.translationFormat, frame, item.frameTranslation, holder, item); holder.mTargetBody.setText(item.renderedTargetBody); } return true; } }); holder.mEditButton.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return detector.onTouchEvent(event); } }); holder.mAddNoteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createFootnoteAtSelection(holder, item); } }); prepareTranslationUI(holder, item); // disable listener holder.mDoneSwitch.setOnCheckedChangeListener(null); // display as finished if(item.isTranslationFinished) { holder.mEditButton.setVisibility(View.GONE); holder.mUndoButton.setVisibility(View.GONE); holder.mRedoButton.setVisibility(View.GONE); holder.mAddNoteButton.setVisibility(View.GONE); holder.mDoneSwitch.setChecked(true); holder.mTargetInnerCard.setBackgroundResource(R.color.white); } else { holder.mEditButton.setVisibility(View.VISIBLE); holder.mDoneSwitch.setChecked(false); } // display source language tabs renderTabs(holder); // done buttons holder.mDoneSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { // make sure to capture verse marker changes changes before dialog is displayed Editable changes = holder.mTargetEditableBody.getText(); item.renderedTargetBody = changes; String newBody = Translator.compileTranslation(changes); item.bodyTranslation = newBody; CustomAlertDialog.Create(mContext) .setTitle(R.string.chunk_checklist_title) .setMessageHtml(R.string.chunk_checklist_body) .setPositiveButton(R.string.confirm, new View.OnClickListener() { @Override public void onClick(View v) { boolean success = onConfirmChunk(item, chapter, frame, mTargetTranslation.getFormat()); holder.mDoneSwitch.setChecked(success); } } ) .setNegativeButton(R.string.title_cancel, new View.OnClickListener() { @Override public void onClick(View v) { holder.mDoneSwitch.setChecked(false); // force back off if not accepted } }) .show("Chunk2"); } else { // done button checked off boolean opened; if (item.isChapterReference) { opened = mTargetTranslation.reopenChapterReference(chapter); } else if (item.isChapterTitle) { opened = mTargetTranslation.reopenChapterTitle(chapter); } else if (item.isProjectTitle) { opened = mTargetTranslation.openProjectTitle(); } else { opened = mTargetTranslation.reopenFrame(frame); } if (opened) { item.renderedTargetBody = null; notifyDataSetChanged(); } else { // TODO: 10/27/2015 notify user the frame could not be completed. } } } }); } private void prepareTranslationUI(final ViewHolder holder, ListItem item) { if(item.isEditing) { final FileHistory history = item.getFileHistory(mTargetTranslation); ThreadableUI thread = new ThreadableUI(mContext) { @Override public void onStop() { } @Override public void run() { try { history.loadCommits(); } catch (IOException e) { e.printStackTrace(); } catch (GitAPIException e) { e.printStackTrace(); } } @Override public void onPostExecute() { if(history.hasNext()) { holder.mRedoButton.setVisibility(View.VISIBLE); } else { holder.mRedoButton.setVisibility(View.GONE); } if(history.hasPrevious()) { holder.mUndoButton.setVisibility(View.VISIBLE); } else { holder.mUndoButton.setVisibility(View.GONE); } } }; thread.start(); boolean allowFootnote = mAllowFootnote && item.isFrame(); holder.mEditButton.setImageResource(R.drawable.ic_done_black_24dp); holder.mAddNoteButton.setVisibility(allowFootnote ? View.VISIBLE : View.GONE); holder.mUndoButton.setVisibility(View.GONE); holder.mRedoButton.setVisibility(View.GONE); holder.mTargetBody.setVisibility(View.GONE); holder.mTargetEditableBody.setVisibility(View.VISIBLE); holder.mTargetEditableBody.setEnableLines(true); holder.mTargetInnerCard.setBackgroundResource(R.color.white); } else { holder.mEditButton.setImageResource(R.drawable.ic_mode_edit_black_24dp); holder.mUndoButton.setVisibility(View.GONE); holder.mRedoButton.setVisibility(View.GONE); holder.mAddNoteButton.setVisibility(View.GONE); holder.mTargetBody.setVisibility(View.VISIBLE); holder.mTargetEditableBody.setVisibility(View.GONE); holder.mTargetEditableBody.setEnableLines(false); holder.mTargetInnerCard.setBackgroundResource(R.color.white); } } private void renderTargetBody(ListItem item, ViewHolder holder, Frame frame) { // render body if(item.isTranslationFinished || item.isEditing) { item.renderedTargetBody = renderSourceText(item.bodyTranslation, item.translationFormat, holder, item, true); } else { item.renderedTargetBody = renderTargetText(item.bodyTranslation, item.translationFormat, frame, item.frameTranslation, holder, item); } } /** * create a new footnote at selected position in target text. Displays an edit dialog to enter footnote data. * @param holder * @param item */ private void createFootnoteAtSelection(final ViewHolder holder, final ListItem item) { final EditText editText = getEditText(holder, item); int endPos = editText.getSelectionEnd(); if (endPos < 0) { endPos = 0; } final int insertPos = endPos; editFootnote("", holder, item, insertPos, insertPos); } /** * edit contents of footnote at specified position * @param initialNote * @param holder * @param item * @param footnotePos * @param footnoteEndPos */ private void editFootnote(CharSequence initialNote, final ViewHolder holder, final ListItem item, final int footnotePos, final int footnoteEndPos ) { final EditText editText = getEditText(holder, item); final CharSequence original = editText.getText(); LayoutInflater inflater = LayoutInflater.from(mContext); final View footnoteFragment = inflater.inflate(R.layout.fragment_footnote_prompt, null); if(footnoteFragment != null) { final EditText footnoteText = (EditText) footnoteFragment.findViewById(R.id.footnote_text); if ((footnoteText != null)) { footnoteText.setText(initialNote); // pop up note prompt final CustomAlertDialog dialog = CustomAlertDialog.Create(mContext); dialog.setTitle(R.string.title_add_footnote) .setAutoDismiss(false) .setPositiveButton(R.string.label_ok, new View.OnClickListener() { @Override public void onClick(View v) { CharSequence footnote = footnoteText.getText(); boolean validated = verifyAndReplaceFootnote(footnote, original, footnotePos, footnoteEndPos, holder, item, editText); if(validated) { dialog.dismiss(); } } }) .setNegativeButton(R.string.title_cancel, new View.OnClickListener() { @Override public void onClick(View v) { dialog.dismiss(); } }) .setView(footnoteFragment) .show("add-footnote"); } } } /** * insert footnote into EditText or remove footnote from EditText if both footnote and * footnoteTitleText are null * @param footnote * @param original * @param insertPos * @param insertEndPos * @param item * @param editText */ private boolean verifyAndReplaceFootnote(CharSequence footnote, CharSequence original, int insertPos, final int insertEndPos, final ViewHolder holder, final ListItem item, EditText editText) { // sanity checks if ((null == footnote) || (footnote.length() <= 0)) { warnDialog(R.string.title_footnote_invalid, R.string.footnote_message_empty); return false; } placeFootnote(footnote, original, insertPos, insertEndPos, holder, item, editText); return true; } /** * display warning dialog * @param titleID * @param messageID */ private void warnDialog(int titleID, int messageID) { final CustomAlertDialog dialog = CustomAlertDialog.Create(mContext); dialog.setTitle(titleID) .setMessage(messageID) .setPositiveButton(R.string.dismiss, null) .show("warn-dialog"); } /** * insert footnote into EditText or remove footnote from EditText if both footnote and * footnoteTitleText are null * @param footnote * @param original * @param start * @param end * @param item * @param editText */ private void placeFootnote(CharSequence footnote, CharSequence original, int start, final int end, final ViewHolder holder, final ListItem item, EditText editText) { CharSequence footnotecode = ""; if(footnote != null) { // sanity checks if ((null == footnote) || (footnote.length() <= 0)) { footnote = mContext.getResources().getString(R.string.footnote_label); } USFMNoteSpan footnoteSpannable = USFMNoteSpan.generateFootnote(footnote); footnotecode = footnoteSpannable.getMachineReadable(); } CharSequence newText = TextUtils.concat(original.subSequence(0, start), footnotecode, original.subSequence(end, original.length())); editText.setText(newText); item.renderedTargetBody = newText; item.bodyTranslation = Translator.compileTranslation(editText.getText()); // get XML for footnote mTargetTranslation.applyFrameTranslation(item.frameTranslation, item.bodyTranslation); // save change Frame frame = null; if(item.isFrame()) { frame = loadFrame(item.chapterSlug, item.frameSlug); } renderTargetBody(item, holder, frame); // generate spannable again adding editText.setText(item.renderedTargetBody); editText.setSelection(editText.length(), editText.length()); } /** * save changed text * @param s A string or editable * @param item * @return */ private String applyChangedText(CharSequence s, ViewHolder holder, ListItem item) { String translation; if(s instanceof Editable) { translation = Translator.compileTranslation((Editable) s); } else { translation = s.toString(); } if (item.isChapterReference) { mTargetTranslation.applyChapterReferenceTranslation(item.chapterTranslation, translation); } else if (item.isChapterTitle) { mTargetTranslation.applyChapterTitleTranslation(item.chapterTranslation, translation); } else if (item.isProjectTitle) { try { mTargetTranslation.applyProjectTitleTranslation(s.toString()); } catch (IOException e) { Logger.e(ReviewModeAdapter.class.getName(), "Failed to save the project title translation", e); } } else if (item.isFrame()) { mTargetTranslation.applyFrameTranslation(item.frameTranslation, translation); } item.renderedTargetBody = renderSourceText(translation, item.translationFormat, holder, item, true); return translation; } /** * restore the text from previous commit for fragment * @param holder * @param item */ private void undoTextInTarget(final ViewHolder holder, final ListItem item) { holder.mUndoButton.setVisibility(View.INVISIBLE); holder.mRedoButton.setVisibility(View.INVISIBLE); final FileHistory history = item.getFileHistory(mTargetTranslation); ThreadableUI thread = new ThreadableUI(mContext) { RevCommit commit = null; @Override public void onStop() { } @Override public void run() { // commit changes before viewing history if(history.isAtHead()) { if(!mTargetTranslation.isClean()) { try { mTargetTranslation.commitSync(); history.loadCommits(); } catch (Exception e) { e.printStackTrace(); } } } // get previous commit = history.previous(); } @Override public void onPostExecute() { try { if(commit != null) { String text = history.read(commit); // save and update ui if (text != null) { // TRICKY: prevent history from getting rolled back soon after the user views it restartAutoCommitTimer(); applyChangedText(text, holder, item); holder.mTargetEditableBody.removeTextChangedListener(holder.mEditableTextWatcher); holder.mTargetEditableBody.setText(item.renderedTargetBody); holder.mTargetEditableBody.addTextChangedListener(holder.mEditableTextWatcher); } } } catch (IOException e) { e.printStackTrace(); } if(history.hasNext()) { holder.mRedoButton.setVisibility(View.VISIBLE); } else { holder.mRedoButton.setVisibility(View.GONE); } if(history.hasPrevious()) { holder.mUndoButton.setVisibility(View.VISIBLE); } else { holder.mUndoButton.setVisibility(View.GONE); } } }; thread.start(); } /** * restore the text from later commit for fragment * @param holder * @param item */ private void redoTextInTarget(final ViewHolder holder, final ListItem item) { holder.mUndoButton.setVisibility(View.INVISIBLE); holder.mRedoButton.setVisibility(View.INVISIBLE); final FileHistory history = item.getFileHistory(mTargetTranslation); ThreadableUI thread = new ThreadableUI(mContext) { RevCommit commit = null; @Override public void onStop() { } @Override public void run() { commit = history.next(); } @Override public void onPostExecute() { try { if(commit != null) { String text = history.read(commit); // save and update ui if (text != null) { // TRICKY: prevent history from getting rolled back soon after the user views it restartAutoCommitTimer(); applyChangedText(text, holder, item); holder.mTargetEditableBody.removeTextChangedListener(holder.mEditableTextWatcher); holder.mTargetEditableBody.setText(item.renderedTargetBody); holder.mTargetEditableBody.addTextChangedListener(holder.mEditableTextWatcher); } } } catch (IOException e) { e.printStackTrace(); } if(history.hasNext()) { holder.mRedoButton.setVisibility(View.VISIBLE); } else { holder.mRedoButton.setVisibility(View.GONE); } if(history.hasPrevious()) { holder.mUndoButton.setVisibility(View.VISIBLE); } else { holder.mUndoButton.setVisibility(View.GONE); } } }; thread.start(); } private static final Pattern USFM_CONSECUTIVE_VERSE_MARKERS = Pattern.compile("\\\\v\\s(\\d+(-\\d+)?)\\s*\\\\v\\s(\\d+(-\\d+)?)"); private static final Pattern USFM_VERSE_MARKER = Pattern.compile(USFMVerseSpan.PATTERN); private static final Pattern CONSECUTIVE_VERSE_MARKERS = Pattern.compile("(<verse [^>]+/>\\s*){2}"); private static final Pattern VERSE_MARKER = Pattern.compile("<verse\\s+number=\"(\\d+)\"[^>]*>"); /** * Performs some validation, and commits changes if ready. * @return true if the section was successfully confirmed; otherwise false. */ private boolean onConfirmChunk(final ListItem item, final Chapter chapter, final Frame frame, TranslationFormat format) { boolean success = true; // So far, so good. // Check for empty translation. if (item.bodyTranslation.isEmpty()) { Snackbar snack = Snackbar.make(mContext.findViewById(android.R.id.content), R.string.translate_first, Snackbar.LENGTH_LONG); ViewUtil.setSnackBarTextColor(snack, mContext.getResources().getColor(R.color.light_primary_text)); snack.show(); success = false; } if(frame != null) { Matcher matcher; int lowVerse = -1; int highVerse = 999999999; int[] range = frame.getVerseRange(); if ((range != null) && (range.length > 0)) { lowVerse = range[0]; highVerse = lowVerse; if (range.length > 1) { highVerse = range[1]; } } // Check for contiguous verse numbers. if (success) { if (format == TranslationFormat.USFM) { matcher = USFM_CONSECUTIVE_VERSE_MARKERS.matcher(item.bodyTranslation); } else { matcher = CONSECUTIVE_VERSE_MARKERS.matcher(item.bodyTranslation); } if (matcher.find()) { Snackbar snack = Snackbar.make(mContext.findViewById(android.R.id.content), R.string.consecutive_verse_markers, Snackbar.LENGTH_LONG); ViewUtil.setSnackBarTextColor(snack, mContext.getResources().getColor(R.color.light_primary_text)); snack.show(); success = false; } } // Check for out-of-order verse markers. if (success) { int error = 0; if (format == TranslationFormat.USFM) { matcher = USFM_VERSE_MARKER.matcher(item.bodyTranslation); } else { matcher = VERSE_MARKER.matcher(item.bodyTranslation); } int lastVerseSeen = 0; while (matcher.find()) { int currentVerse = Integer.valueOf(matcher.group(1)); if (currentVerse <= lastVerseSeen) { if (currentVerse == lastVerseSeen) { error = R.string.duplicate_verse_marker; success = false; break; } else { error = R.string.outoforder_verse_markers; success = false; break; } } else if ((currentVerse < lowVerse) || (currentVerse > highVerse)) { error = R.string.outofrange_verse_marker; success = false; break; } else { lastVerseSeen = currentVerse; } } if (!success) { Snackbar snack = Snackbar.make(mContext.findViewById(android.R.id.content), error, Snackbar.LENGTH_LONG); ViewUtil.setSnackBarTextColor(snack, mContext.getResources().getColor(R.color.light_primary_text)); snack.show(); } } } // Everything looks good so far. Try and commit. if (success) { if (item.isChapterReference) { success = mTargetTranslation.finishChapterReference(chapter); } else if (item.isChapterTitle) { success = mTargetTranslation.finishChapterTitle(chapter); } else if (item.isProjectTitle) { success = mTargetTranslation.closeProjectTitle(); } else if(frame != null){ success = mTargetTranslation.finishFrame(frame); } else { success = false; } if (!success) { // TODO: Use a more accurate (if potentially more opaque) error message. Snackbar snack = Snackbar.make(mContext.findViewById(android.R.id.content), R.string.failed_to_commit_chunk, Snackbar.LENGTH_LONG); ViewUtil.setSnackBarTextColor(snack, mContext.getResources().getColor(R.color.light_primary_text)); snack.show(); } } // Wrap up. if (success) { try { mTargetTranslation.commit(); } catch (Exception e) { String frameComplexId = frame == null ? "" : ":" + frame.getComplexId(); Logger.e(TAG, "Failed to commit translation of " + mTargetTranslation.getId() + frameComplexId, e); } item.isEditing = false; item.renderedTargetBody = null; notifyDataSetChanged(); } return success; } /** * Renders the source language tabs on the target card * @param holder */ private void renderTabs(ViewHolder holder) { holder.mTranslationTabs.setOnTabSelectedListener(null); holder.mTranslationTabs.removeAllTabs(); for(ContentValues values:mTabs) { TabLayout.Tab tab = holder.mTranslationTabs.newTab(); tab.setText(values.getAsString("title")); tab.setTag(values.getAsString("tag")); holder.mTranslationTabs.addTab(tab); } // open selected tab for(int i = 0; i < holder.mTranslationTabs.getTabCount(); i ++) { TabLayout.Tab tab = holder.mTranslationTabs.getTabAt(i); if(tab.getTag().equals(mSourceTranslation.getId())) { tab.select(); break; } } // tabs listener holder.mTranslationTabs.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { final String sourceTranslationId = (String) tab.getTag(); if (getListener() != null) { Handler hand = new Handler(Looper.getMainLooper()); hand.post(new Runnable() { @Override public void run() { getListener().onSourceTranslationTabClick(sourceTranslationId); } }); } } @Override public void onTabUnselected(TabLayout.Tab tab) { } @Override public void onTabReselected(TabLayout.Tab tab) { } }); // change tabs listener holder.mNewTabButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (getListener() != null) { getListener().onNewSourceTranslationTabClick(); } } }); } private void renderResourceCard(final int position, ListItem item, final ViewHolder holder) { // clean up view if(holder.mResourceList.getChildCount() > 0) { holder.mResourceList.removeAllViews(); } holder.mResourceTabs.setOnTabSelectedListener(null); holder.mResourceTabs.removeAllTabs(); // skip if chapter title/reference if(!item.isFrame()) { return; } Frame frame = loadFrame(item.chapterSlug, item.frameSlug); // resource tabs final TranslationNote[] notes = getPreferredNotes(mSourceTranslation, frame); if(notes.length > 0) { TabLayout.Tab tab = holder.mResourceTabs.newTab(); tab.setText(R.string.label_translation_notes); tab.setTag(TAB_NOTES); holder.mResourceTabs.addTab(tab); if(mOpenResourceTab[position] == TAB_NOTES) { tab.select(); } } final TranslationWord[] words = getPreferredWords(mSourceTranslation, frame); if(words.length > 0) { TabLayout.Tab tab = holder.mResourceTabs.newTab(); tab.setText(R.string.translation_words); tab.setTag(TAB_WORDS); holder.mResourceTabs.addTab(tab); if(mOpenResourceTab[position] == TAB_WORDS) { tab.select(); } } final CheckingQuestion[] questions = getPreferredQuestions(mSourceTranslation, frame.getChapterId(), frame.getId()); if(questions.length > 0) { TabLayout.Tab tab = holder.mResourceTabs.newTab(); tab.setText(R.string.questions); tab.setTag(TAB_QUESTIONS); holder.mResourceTabs.addTab(tab); if(mOpenResourceTab[position] == TAB_QUESTIONS) { tab.select(); } } // select default tab. first notes, then words, then questions if(mOpenResourceTab[position] == TAB_NOTES && notes.length == 0) { mOpenResourceTab[position] = TAB_WORDS; } if(mOpenResourceTab[position] == TAB_WORDS && words.length == 0) { mOpenResourceTab[position] = TAB_QUESTIONS; } holder.mResourceTabs.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { if ((int) tab.getTag() == TAB_NOTES && mOpenResourceTab[position] != TAB_NOTES) { mOpenResourceTab[position] = TAB_NOTES; // render notes renderResources(holder, position, notes, words, questions); } else if ((int) tab.getTag() == TAB_WORDS && mOpenResourceTab[position] != TAB_WORDS) { mOpenResourceTab[position] = TAB_WORDS; // render words renderResources(holder, position, notes, words, questions); } else if ((int) tab.getTag() == TAB_QUESTIONS && mOpenResourceTab[position] != TAB_QUESTIONS) { mOpenResourceTab[position] = TAB_QUESTIONS; // render questions renderResources(holder, position, notes, words, questions); } } @Override public void onTabUnselected(TabLayout.Tab tab) { } @Override public void onTabReselected(TabLayout.Tab tab) { } }); // resource list if(notes.length > 0 || words.length > 0 || questions.length > 0) { renderResources(holder, position, notes, words, questions); } // tap to open resources if(!mResourcesOpened) { holder.mResourceLayout.setVisibility(View.INVISIBLE); // TRICKY: we have to detect a single tap so that swipes do not trigger this final GestureDetector resourceCardDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { if (!mResourcesOpened) { openResources(); } return true; } }); holder.mResourceCard.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return resourceCardDetector.onTouchEvent(event); } }); } else { holder.mResourceLayout.setVisibility(View.VISIBLE); } } /** * Renders the resources card * @param holder * @param position * @param notes * @param words * @param questions */ private void renderResources(final ViewHolder holder, int position, TranslationNote[] notes, TranslationWord[] words, CheckingQuestion[] questions) { if(holder.mResourceList.getChildCount() > 0) { holder.mResourceList.removeAllViews(); } if(mOpenResourceTab[position] == TAB_NOTES) { // render notes for(final TranslationNote note:notes) { TextView noteView = (TextView) mContext.getLayoutInflater().inflate(R.layout.fragment_resources_list_item, null); noteView.setText(note.getTitle()); noteView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (getListener() != null) { getListener().onTranslationNoteClick(note.getChapterId(), note.getFrameId(), note.getId(), holder.getResourceCardWidth()); } } }); Typography.formatSub(mContext, noteView, mSourceLanguage.getId(), mSourceLanguage.getDirection()); holder.mResourceList.addView(noteView); } } else if(mOpenResourceTab[position] == TAB_WORDS) { // render words for(final TranslationWord word:words) { TextView wordView = (TextView) mContext.getLayoutInflater().inflate(R.layout.fragment_resources_list_item, null); wordView.setText(word.getTerm()); wordView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (getListener() != null) { getListener().onTranslationWordClick(word.getId(), holder.getResourceCardWidth()); } } }); Typography.formatSub(mContext, wordView, mSourceLanguage.getId(), mSourceLanguage.getDirection()); holder.mResourceList.addView(wordView); } } else if(mOpenResourceTab[position] == TAB_QUESTIONS) { // render questions for(final CheckingQuestion question:questions) { TextView questionView = (TextView) mContext.getLayoutInflater().inflate(R.layout.fragment_resources_list_item, null); questionView.setText(question.getQuestion()); questionView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (getListener() != null) { getListener().onCheckingQuestionClick(question.getChapterId(), question.getFrameId(), question.getId(), holder.getResourceCardWidth()); } } }); Typography.formatSub(mContext, questionView, mSourceLanguage.getId(), mSourceLanguage.getDirection()); holder.mResourceList.addView(questionView); } } } /** * generate spannable for target text. Will add click listener for notes and verses if they are supported * @param text * @param format * @param frame * @param frameTranslation * @param holder * @param item * @return */ private CharSequence renderTargetText(String text, TranslationFormat format, final Frame frame, final FrameTranslation frameTranslation, final ViewHolder holder, final ListItem item) { RenderingGroup renderingGroup = new RenderingGroup(); if(Clickables.isClickableFormat(format) && frame != null) { Span.OnClickListener verseClickListener = new Span.OnClickListener() { @Override public void onClick(View view, Span span, int start, int end) { Snackbar snack = Snackbar.make(mContext.findViewById(android.R.id.content), R.string.long_click_to_drag, Snackbar.LENGTH_SHORT); ViewUtil.setSnackBarTextColor(snack, mContext.getResources().getColor(R.color.light_primary_text)); snack.show(); ((EditText) view).setSelection(((EditText) view).getText().length()); } @Override public void onLongClick(final View view, Span span, int start, int end) { ClipData dragData = ClipData.newPlainText(frame.getComplexId(), span.getMachineReadable()); final VerseSpan pin = ((VerseSpan) span); // create drag shadow LayoutInflater inflater = (LayoutInflater)AppContext.context().getSystemService(Context.LAYOUT_INFLATER_SERVICE); FrameLayout verseLayout = (FrameLayout)inflater.inflate(R.layout.fragment_verse_marker, null); TextView verseTitle = (TextView)verseLayout.findViewById(R.id.verse); if(pin.getEndVerseNumber() > 0) { verseTitle.setText(pin.getStartVerseNumber() + "-" + pin.getEndVerseNumber()); } else { verseTitle.setText(pin.getStartVerseNumber() + ""); } Bitmap shadow = ViewUtil.convertToBitmap(verseLayout); View.DragShadowBuilder myShadow = CustomDragShadowBuilder.fromBitmap(mContext, shadow); int[] spanRange = {start, end}; view.startDrag(dragData, // the data to be dragged myShadow, // the drag shadow builder spanRange, // no need to use local data 0 // flags (not currently used, set to 0) ); view.setOnDragListener(new View.OnDragListener() { private boolean hasEntered = false; @Override public boolean onDrag(View v, DragEvent event) { EditText editText = ((EditText) view); // TODO: highlight the drop site. if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) { // delete old span int[] spanRange = (int[])event.getLocalState(); CharSequence in = editText.getText(); CharSequence out = TextUtils.concat(in.subSequence(0, spanRange[0]), in.subSequence(spanRange[1], in.length())); editText.setText(out); } else if(event.getAction() == DragEvent.ACTION_DROP) { int offset = editText.getOffsetForPosition(event.getX(), event.getY()); CharSequence text = editText.getText(); if(offset >= 0) { // insert the verse at the offset text = TextUtils.concat(text.subSequence(0, offset), pin.toCharSequence(), text.subSequence(offset, text.length())); } else { // place the verse back at the beginning text = TextUtils.concat(pin.toCharSequence(), text); } item.renderedTargetBody = text; editText.setText(text); String translation = Translator.compileTranslation((Editable)editText.getText()); mTargetTranslation.applyFrameTranslation(frameTranslation, translation); // Reload, so that bodyTranslation and other data are kept in sync. item.loadTranslations(mSourceTranslation, mTargetTranslation, null, frame); } else if(event.getAction() == DragEvent.ACTION_DRAG_ENDED) { view.setOnDragListener(null); editText.setSelection(editText.getSelectionEnd()); // reset verse if dragged off the view // TODO: 10/5/2015 perhaps we should confirm with the user? if(!hasEntered) { // place the verse back at the beginning CharSequence text = editText.getText(); text = TextUtils.concat(pin.toCharSequence(), text); item.renderedTargetBody = text; editText.setText(text); String translation = Translator.compileTranslation((Editable)editText.getText()); mTargetTranslation.applyFrameTranslation(frameTranslation, translation); // Reload, so that bodyTranslation and other data are kept in sync. item.loadTranslations(mSourceTranslation, mTargetTranslation, null, frame); } } else if(event.getAction() == DragEvent.ACTION_DRAG_ENTERED) { hasEntered = true; } else if(event.getAction() == DragEvent.ACTION_DRAG_EXITED) { hasEntered = false; editText.setSelection(editText.getSelectionEnd()); } else if(event.getAction() == DragEvent.ACTION_DRAG_LOCATION) { int offset = editText.getOffsetForPosition(event.getX(), event.getY()); if(offset >= 0) { Selection.setSelection(editText.getText(), offset); } else { editText.setSelection(editText.getSelectionEnd()); } } return true; } }); } }; Span.OnClickListener noteClickListener = new Span.OnClickListener() { @Override public void onClick(View view, Span span, int start, int end) { if (span instanceof NoteSpan) { showFootnote(holder, item, (NoteSpan) span, start, end, true); } } @Override public void onLongClick(View view, Span span, int start, int end) { } }; ClickableRenderingEngine renderer = Clickables.setupRenderingGroup(format, renderingGroup, verseClickListener, noteClickListener, true); renderer.setLinebreaksEnabled(true); renderer.setPopulateVerseMarkers(frame.getVerseRange()); } else { // TODO: add note click listener renderingGroup.addEngine(new DefaultRenderer(null)); } if(!text.trim().isEmpty()) { renderingGroup.init(text); return renderingGroup.start(); } else { return ""; } } /** * display selected footnote in dialog. If editable, then it adds options to delete and edit * the footnote * @param holder * @param item * @param span * @param editable */ private void showFootnote(final ViewHolder holder, final ListItem item, final NoteSpan span, final int start, final int end, boolean editable) { CharSequence marker = span.getPassage(); CharSequence title = mContext.getResources().getText(R.string.title_note); if(!marker.toString().isEmpty()) { title = title + ": " + marker; } CharSequence message = span.getNotes(); CustomAlertDialog dlg = CustomAlertDialog.Create(mContext); dlg.setTitle(title) .setMessage(message) .setPositiveButton(R.string.dismiss, null); if(editable && !item.isTranslationFinished) { dlg.setNeutralButton(R.string.edit, new View.OnClickListener() { @Override public void onClick(View v) { editFootnote(span.getNotes(), holder, item, start, end); } }); dlg.setNegativeButton(R.string.label_delete, new View.OnClickListener() { @Override public void onClick(View v) { deleteFootnote(span.getNotes(), holder, item, start, end); } }); } dlg.show("viewNote"); } /** * prompt to confirm removal of specific footnote at position * @param note * @param holder * @param item * @param start * @param end */ private void deleteFootnote(CharSequence note, final ViewHolder holder, final ListItem item, final int start, final int end ) { final EditText editText = getEditText(holder, item); final CharSequence original = editText.getText(); // pop up delete prompt final CustomAlertDialog dialog = CustomAlertDialog.Create(mContext); dialog.setTitle(R.string.footnote_confirm_delete) .setMessage(note) .setPositiveButton(R.string.label_delete, new View.OnClickListener() { @Override public void onClick(View v) { placeFootnote(null, original, start, end, holder, item, editText); } }) .setNegativeButton(R.string.title_cancel, null) .show("add-footnote"); } /** * get appropriate edit text - it is different when editing versus viewing * @param holder * @param item * @return */ private EditText getEditText(final ViewHolder holder, final ListItem item) { if (!item.isEditing) { return holder.mTargetBody; } else { return holder.mTargetEditableBody; } } /** * generate spannable for source text. Will add click listener for notes if supported * @param text * @param format * @param holder * @param item * @param editable * @return */ private CharSequence renderSourceText(String text, TranslationFormat format, final ViewHolder holder, final ListItem item, final boolean editable) { RenderingGroup renderingGroup = new RenderingGroup(); if (Clickables.isClickableFormat(format)) { // TODO: add click listeners for verses Span.OnClickListener noteClickListener = new Span.OnClickListener() { @Override public void onClick(View view, Span span, int start, int end) { if(span instanceof NoteSpan) { showFootnote(holder, item, (NoteSpan) span, start, end, editable); } } @Override public void onLongClick(View view, Span span, int start, int end) { } }; Clickables.setupRenderingGroup(format, renderingGroup, null, noteClickListener, false); if(editable) { if(!item.isTranslationFinished) { renderingGroup.setVersesEnabled(false); } renderingGroup.setLinebreaksEnabled(true); } } else { // TODO: add note click listener renderingGroup.addEngine(new DefaultRenderer(null)); } renderingGroup.init(text); return renderingGroup.start(); } @Override public int getItemCount() { return mListItems.length; } /** * opens the resources view */ public void openResources() { if(!mResourcesOpened) { mResourcesOpened = true; coordinateViewHolders(); } } /** * closes the resources view */ public void closeResources() { if(mResourcesOpened) { mResourcesOpened = false; coordinateViewHolders(); } } /** * Checks if the resources are open * @return */ public boolean isResourcesOpen() { return mResourcesOpened; } public static class ViewHolder extends RecyclerView.ViewHolder { public final ImageButton mAddNoteButton; public final ImageButton mUndoButton; public final ImageButton mRedoButton; public final ImageButton mEditButton; public final CardView mResourceCard; public final LinearLayout mMainContent; public final LinearLayout mResourceLayout; public final Switch mDoneSwitch; private final LinearLayout mTargetInnerCard; private final TabLayout mResourceTabs; private final LinearLayout mResourceList; public final LinedEditText mTargetEditableBody; public int mLayoutBuildNumber = -1; public TextWatcher mEditableTextWatcher; public final TextView mTargetTitle; public final EditText mTargetBody; public final CardView mTargetCard; public final CardView mSourceCard; public final TabLayout mTranslationTabs; public final ImageButton mNewTabButton; public TextView mSourceBody; public ViewHolder(Context context, View v) { super(v); mMainContent = (LinearLayout)v.findViewById(R.id.main_content); mSourceCard = (CardView)v.findViewById(R.id.source_translation_card); mSourceBody = (TextView)v.findViewById(R.id.source_translation_body); mResourceCard = (CardView)v.findViewById(R.id.resources_card); mResourceLayout = (LinearLayout)v.findViewById(R.id.resources_layout); mResourceTabs = (TabLayout)v.findViewById(R.id.resource_tabs); mResourceTabs.setTabTextColors(R.color.dark_disabled_text, R.color.dark_secondary_text); mResourceList = (LinearLayout)v.findViewById(R.id.resources_list); mTargetCard = (CardView)v.findViewById(R.id.target_translation_card); mTargetInnerCard = (LinearLayout)v.findViewById(R.id.target_translation_inner_card); mTargetTitle = (TextView)v.findViewById(R.id.target_translation_title); mTargetBody = (EditText)v.findViewById(R.id.target_translation_body); mTargetEditableBody = (LinedEditText)v.findViewById(R.id.target_translation_editable_body); mTranslationTabs = (TabLayout)v.findViewById(R.id.source_translation_tabs); mEditButton = (ImageButton)v.findViewById(R.id.edit_translation_button); mAddNoteButton = (ImageButton)v.findViewById(R.id.add_note_button); mUndoButton = (ImageButton)v.findViewById(R.id.undo_button); mRedoButton = (ImageButton)v.findViewById(R.id.redo_button); mDoneSwitch = (Switch)v.findViewById(R.id.done_button); mTranslationTabs.setTabTextColors(R.color.dark_disabled_text, R.color.dark_secondary_text); mNewTabButton = (ImageButton) v.findViewById(R.id.new_tab_button); } /** * Returns the full width of the resource card * @return */ public int getResourceCardWidth() { if(mResourceCard != null) { int rightMargin = ((ViewGroup.MarginLayoutParams)mResourceCard.getLayoutParams()).rightMargin; return mResourceCard.getWidth() + rightMargin; } else { return 0; } } } private static class ListItem { private final String frameSlug; private final String chapterSlug; private boolean isChapterReference = false; private boolean isChapterTitle = false; private boolean isProjectTitle = false; private boolean isEditing = false; private CharSequence renderedSourceBody; private CharSequence renderedTargetBody; private TranslationFormat translationFormat; private String bodyTranslation; private boolean isTranslationFinished; private String bodySource; private FrameTranslation frameTranslation; private ChapterTranslation chapterTranslation; private ProjectTranslation projectTranslation; private FileHistory fileHistory = null; public ListItem(String frameSlug, String chapterSlug) { this.frameSlug = frameSlug; this.chapterSlug = chapterSlug; } public boolean isFrame() { return this.frameSlug != null; } public boolean isChapter() { return this.frameSlug == null && this.chapterSlug != null; } /** * Loads the file history or returns it from the cache * @param targetTranslation * @return */ public FileHistory getFileHistory(TargetTranslation targetTranslation) { if(this.fileHistory != null) { return this.fileHistory; } FileHistory history = null; if(this.isChapterReference) { history = targetTranslation.getChapterReferenceHistory(this.chapterTranslation); } else if(this.isChapterTitle) { history = targetTranslation.getChapterTitleHistory(this.chapterTranslation); } else if(this.isProjectTitle) { history = targetTranslation.getProjectTitleHistory(); } else if(this.isFrame()) { history = targetTranslation.getFrameHistory(this.frameTranslation); } this.fileHistory = history; return history; } /** * Loads the correct translation information into the item * @param targetTranslation * @param chapter * @param frame */ public void loadTranslations(SourceTranslation sourceTranslation, TargetTranslation targetTranslation, Chapter chapter, Frame frame) { if(isChapterReference || isChapterTitle) { frameTranslation = null; chapterTranslation = targetTranslation.getChapterTranslation(chapter); translationFormat = targetTranslation.getFormat(); if (isChapterTitle) { bodyTranslation = chapterTranslation.title; bodySource = chapter.title; isTranslationFinished = chapterTranslation.isTitleFinished(); } else { bodyTranslation = chapterTranslation.reference; bodySource = chapter.reference; isTranslationFinished = chapterTranslation.isReferenceFinished(); } } else if(isProjectTitle) { projectTranslation = targetTranslation.getProjectTranslation(); bodyTranslation = projectTranslation.getTitle(); bodySource = sourceTranslation.getProjectTitle(); isTranslationFinished = projectTranslation.isTitleFinished(); } else { chapterTranslation = null; frameTranslation = targetTranslation.getFrameTranslation(frame); translationFormat = targetTranslation.getFormat(); bodyTranslation = frameTranslation.body; bodySource = frame.body; isTranslationFinished = frameTranslation.isFinished(); } } } }