package com.orgzly.android.ui.fragments; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; import android.support.design.widget.TextInputLayout; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.content.LocalBroadcastManager; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.MultiAutoCompleteTextView; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; import android.widget.ToggleButton; import android.widget.ViewFlipper; import com.orgzly.BuildConfig; import com.orgzly.R; import com.orgzly.android.Book; import com.orgzly.android.Broadcasts; import com.orgzly.android.Note; import com.orgzly.android.Shelf; import com.orgzly.android.prefs.AppPreferences; import com.orgzly.android.ui.FragmentListener; import com.orgzly.android.ui.CommonActivity; import com.orgzly.android.ui.NotePrioritySpinner; import com.orgzly.android.ui.Place; import com.orgzly.android.ui.NoteStateSpinner; import com.orgzly.android.ui.NotePlace; import com.orgzly.android.ui.dialogs.TimestampDialogFragment; import com.orgzly.android.ui.util.ActivityUtils; import com.orgzly.android.util.LogUtils; import com.orgzly.android.util.NoteContentParser; import com.orgzly.android.util.UserTimeFormatter; import com.orgzly.android.util.SpaceTokenizer; import com.orgzly.android.util.MiscUtils; import com.orgzly.org.OrgProperty; import com.orgzly.org.datetime.OrgDateTime; import com.orgzly.org.datetime.OrgRange; import com.orgzly.org.OrgHead; import com.orgzly.android.StateChangeLogic; import com.orgzly.org.parser.OrgParserWriter; import java.util.ArrayList; import java.util.Calendar; import java.util.NoSuchElementException; import java.util.TreeSet; /** * Note editor. */ public class NoteFragment extends Fragment implements View.OnClickListener, TimestampDialogFragment.OnDateTimeSetListener { private static final String TAG = NoteFragment.class.getName(); /** Name used for {@link android.app.FragmentManager}. */ public static final String FRAGMENT_TAG = NoteFragment.class.getName(); private static final String ARG_ORIGINAL_NOTE_HASH = "original_note_hash"; private static final String ARG_IS_NEW = "is_new"; private static final String ARG_BOOK_ID = "book_id"; private static final String ARG_NOTE_ID = "note_id"; private static final String ARG_PLACE = "place"; private static final String ARG_TITLE = "title"; private static final String ARG_CONTENT = "content"; /* Bundle keys for saving note. */ private static final String ARG_CURRENT_STATE = "current_state"; private static final String ARG_CURRENT_PRIORITY = "current_priority"; private static final String ARG_CURRENT_TITLE = "current_title"; private static final String ARG_CURRENT_TAGS = "current_tags"; private static final String ARG_CURRENT_SCHEDULED = "current_scheduled"; private static final String ARG_CURRENT_DEADLINE = "current_deadline"; private static final String ARG_CURRENT_CLOSED = "current_closed"; private static final String ARG_CURRENT_PROPERTIES = "current_properties"; private static final String ARG_CURRENT_CONTENT = "current_content"; private NoteFragmentListener mListener; private Shelf mShelf; /* Arguments. */ private boolean mIsNew; private long mBookId; private long mNoteId; /* Could be null if new note is being created. */ private Place place; /* Relative location, used for new notes. */ private String mInitialTitle; /* Initial title (used for when sharing to Orgzly) */ private String mInitialContent; /* Initial content (used when sharing to Orgzly) */ private Note mNote; private Book mBook; private ScrollView scrollView; private NoteStateSpinner mState; private NotePrioritySpinner mPriority; private TextInputLayout titleInputLayout; private EditText mTitleView; private MultiAutoCompleteTextView mTagsView; private Button mScheduledButton; private Button mDeadlineButton; private Button mClosedButton; private LinearLayout propertyList; private Button addProperty; private ToggleButton editSwitch; private EditText bodyEdit; private TextView bodyView; /** Used to switch to note-does-not-exist view, if the note has been deleted. */ private ViewFlipper mViewFlipper; private UserTimeFormatter mUserTimeFormatter; public static NoteFragment getInstance(boolean isNew, long bookId, long noteId, Place place, String initialTitle, String initialContent) { NoteFragment fragment = new NoteFragment(); Bundle args = new Bundle(); args.putBoolean(ARG_IS_NEW, isNew); args.putLong(ARG_BOOK_ID, bookId); if (noteId > 0) args.putLong(ARG_NOTE_ID, noteId); args.putString(ARG_PLACE, place.toString()); if (initialTitle != null) args.putString(ARG_TITLE, initialTitle); if (initialContent != null) args.putString(ARG_CONTENT, initialContent); fragment.setArguments(args); return fragment; } /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). */ public NoteFragment() { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); } @Override public void onAttach(Context context) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, getActivity()); super.onAttach(context); /* This makes sure that the container activity has implemented * the callback interface. If not, it throws an exception */ try { mListener = (NoteFragmentListener) getActivity(); } catch (ClassCastException e) { throw new ClassCastException(getActivity().toString() + " must implement " + NoteFragmentListener.class); } mShelf = new Shelf(getActivity().getApplicationContext()); parseArguments(); mUserTimeFormatter = new UserTimeFormatter(getActivity().getApplicationContext()); } @Override public void onCreate(Bundle savedInstanceState) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, savedInstanceState); super.onCreate(savedInstanceState); /* Would like to add items to the Options Menu. * Required (for fragments only) to receive onCreateOptionsMenu() call. */ setHasOptionsMenu(true); } private boolean mStateSpinnerReady = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, inflater, container, savedInstanceState); final View top = inflater.inflate(R.layout.fragment_note, container, false); scrollView = (ScrollView) top.findViewById(R.id.fragment_note_container); mPriority = new NotePrioritySpinner(getActivity(), (Spinner) top.findViewById(R.id.fragment_note_priority)); mState = new NoteStateSpinner(getActivity(), (Spinner) top.findViewById(R.id.fragment_note_state)); /* * Act after state change only if there was a touch (ie user clicked on the spinner). */ mState.getSpinner().setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { mStateSpinnerReady = true; // Load your spinner here } return false; } }); /* * On state change - update state and timestamps. * * There could be issues with onItemSelected called on initialization, not as a result * of user selection, which is why mStateSpinnerReady is being used. * * http://stackoverflow.com/questions/2562248/android-how-to-keep-onitemselected-from-firing-off-on-a-newly-instantiated-spin */ mState.getSpinner().setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (mStateSpinnerReady) { String state = parent.getItemAtPosition(position).toString(); updateNoteForStateChange(getActivity(), mNote, state); } mStateSpinnerReady = false; } @Override public void onNothingSelected(AdapterView<?> parent) { } }); titleInputLayout = (TextInputLayout) top.findViewById(R.id.fragment_note_title_input_layout); mTitleView = (EditText) top.findViewById(R.id.fragment_note_title); MiscUtils.clearErrorOnTextChange(mTitleView, titleInputLayout); /* * Only works when set from code. * We want imeOptions="actionDone", so we can't use textMultiLine. */ mTitleView.setHorizontallyScrolling(false); mTitleView.setMaxLines(3); mTitleView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { save(); return true; } }); mTagsView = (MultiAutoCompleteTextView) top.findViewById(R.id.fragment_note_tags); /* Hint causes minimum width - when tags' width is smaller then hint's, there is empty space. */ mTagsView.addTextChangedListener(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) { if (!TextUtils.isEmpty(mTagsView.getText().toString())) { mTagsView.setHint(""); } else { mTagsView.setHint(R.string.fragment_note_tags_hint); } } @Override public void afterTextChanged(Editable s) { } }); mScheduledButton = (Button) top.findViewById(R.id.fragment_note_scheduled_button); mScheduledButton.setOnClickListener(this); mDeadlineButton = (Button) top.findViewById(R.id.fragment_note_deadline_button); mDeadlineButton.setOnClickListener(this); mClosedButton = (Button) top.findViewById(R.id.fragment_note_closed_button); mClosedButton.setOnClickListener(this); propertyList = (LinearLayout) top.findViewById(R.id.property_list); addProperty = (Button) top.findViewById(R.id.add_property); addProperty.setOnClickListener(this); bodyEdit = (EditText) top.findViewById(R.id.body_edit); bodyView = (TextView) top.findViewById(R.id.body_view); // bodyView.setOnTouchListener(new View.OnTouchListener() { // @Override // public boolean onTouch(View v, MotionEvent event) { // editMode(true, true); // return false; // } // }); // bodyView.setOnClickListener(new View.OnClickListener() { // @Override // public void onClick(View v) { // editMode(true, true); // } // }); if (getActivity() != null && AppPreferences.isFontMonospaced(getContext())) { bodyEdit.setTypeface(Typeface.MONOSPACE); bodyView.setTypeface(Typeface.MONOSPACE); } editSwitch = (ToggleButton) top.findViewById(R.id.edit_content_toggle); editSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, buttonView, isChecked); if (isChecked) { bodyView.setVisibility(View.GONE); bodyEdit.setVisibility(View.VISIBLE); } else { bodyEdit.setVisibility(View.GONE); bodyView.setText(NoteContentParser.fromOrg(bodyEdit.getText().toString())); bodyView.setVisibility(View.VISIBLE); ActivityUtils.closeSoftKeyboard(getActivity()); } } }); editSwitch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, editSwitch.isChecked()); if (editSwitch.isChecked()) { // Clicked to edit content ActivityUtils.openSoftKeyboard(getActivity(), bodyEdit); // new Handler().postDelayed(new Runnable() { // @Override // public void run() { // scrollView.requestChildFocus(bodyEdit, bodyEdit); // } // }, 500); } else { // Clicked to finish editing content scrollView.smoothScrollTo(0, 0); } } }); mViewFlipper = (ViewFlipper) top.findViewById(R.id.fragment_note_view_flipper); return top; } /** * Note -> Bundle */ private void updateBundleFromNote(Bundle outState) { OrgHead head = mNote.getHead(); outState.putString(ARG_CURRENT_STATE, head.getState()); outState.putString(ARG_CURRENT_PRIORITY, head.getPriority()); outState.putString(ARG_CURRENT_TITLE, head.getTitle()); outState.putString(ARG_CURRENT_TAGS, TextUtils.join(" ", head.getTags())); if (head.getScheduled() != null) { outState.putString(ARG_CURRENT_SCHEDULED, head.getScheduled().toString()); } if (head.getDeadline() != null) { outState.putString(ARG_CURRENT_DEADLINE, head.getDeadline().toString()); } if (head.getClosed() != null) { outState.putString(ARG_CURRENT_CLOSED, head.getClosed().toString()); } /* Store properties as an array of strings: name1 value1 name2 value2 ... */ if (head.hasProperties()) { ArrayList<String> array = new ArrayList<>(); for (OrgProperty property: head.getProperties()) { array.add(property.getName()); array.add(property.getValue()); } outState.putStringArrayList(ARG_CURRENT_PROPERTIES, array); } else { outState.remove(ARG_CURRENT_PROPERTIES); } outState.putString(ARG_CURRENT_CONTENT, head.getContent()); } /** * Bundle -> Note */ private void updateNoteFromBundle(Bundle savedInstanceState) { OrgHead head = mNote.getHead(); head.setState(savedInstanceState.getString(ARG_CURRENT_STATE)); head.setPriority(savedInstanceState.getString(ARG_CURRENT_PRIORITY)); head.setTitle(savedInstanceState.getString(ARG_CURRENT_TITLE)); if (savedInstanceState.getString(ARG_CURRENT_TAGS) != null) { head.setTags(savedInstanceState.getString(ARG_CURRENT_TAGS).split("\\s+")); } else { head.setTags(new String[] {}); } if (TextUtils.isEmpty(savedInstanceState.getString(ARG_CURRENT_SCHEDULED))) { head.setScheduled(null); } else { head.setScheduled(OrgRange.getInstance(savedInstanceState.getString(ARG_CURRENT_SCHEDULED))); } if (TextUtils.isEmpty(savedInstanceState.getString(ARG_CURRENT_DEADLINE))) { head.setDeadline(null); } else { head.setDeadline(OrgRange.getInstance(savedInstanceState.getString(ARG_CURRENT_DEADLINE))); } if (TextUtils.isEmpty(savedInstanceState.getString(ARG_CURRENT_CLOSED))) { head.setClosed(null); } else { head.setClosed(OrgRange.getInstance(savedInstanceState.getString(ARG_CURRENT_CLOSED))); } head.removeProperties(); if (savedInstanceState.containsKey(ARG_CURRENT_PROPERTIES)) { ArrayList<String> array = savedInstanceState.getStringArrayList(ARG_CURRENT_PROPERTIES); for (int i = 0; i < array.size(); i += 2) { head.addProperty(new OrgProperty(array.get(i), array.get(i + 1))); } } head.setContent(savedInstanceState.getString(ARG_CURRENT_CONTENT)); } /** * Note -> Views */ private void updateViewsFromNote() { OrgHead head = mNote.getHead(); /* State. */ mState.setCurrentValue(head.getState()); /* Priority. */ mPriority.setCurrentValue(head.getPriority()); /* Title. */ mTitleView.setText(head.getTitle()); /* Tags. */ if (head.hasTags()) { mTagsView.setText(TextUtils.join(" ", head.getTags())); } else { mTagsView.setText(null); } /* Times. */ updateTimestampView(TimeType.SCHEDULED, mScheduledButton, head.getScheduled()); updateTimestampView(TimeType.DEADLINE, mDeadlineButton, head.getDeadline()); updateTimestampView(TimeType.CLOSED, mClosedButton, head.getClosed()); /* Properties. */ propertyList.removeAllViews(); if (head.hasProperties()) { for (OrgProperty property: head.getProperties()) { addPropertyToList(property); } } /* Content. */ bodyEdit.setText(head.getContent()); bodyView.setText(NoteContentParser.fromOrg(head.getContent())); } private void addPropertyToList(OrgProperty property) { final ViewGroup propView = (ViewGroup) View.inflate(getActivity(), R.layout.note_property, null); final TextView name = (TextView) propView.findViewById(R.id.name); final TextView value = (TextView) propView.findViewById(R.id.value); final View delete = propView.findViewById(R.id.delete); if (property != null) { // Existing property name.setText(property.getName()); value.setText(property.getValue()); } else { // User creating new property Activity activity = getActivity(); if (activity != null) { ActivityUtils.openSoftKeyboard(activity, name); } } delete.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { propertyList.removeView(propView); } }); propertyList.addView(propView); } /** * Views -> Note (Only those fields which are not updated when views are.) */ private void updateNoteFromViews() { OrgHead head = mNote.getHead(); head.setState(mState.getCurrentValue()); head.setPriority(mPriority.getCurrentValue()); /* Replace new lines with spaces, in case multi-line text has been pasted. */ head.setTitle(mTitleView.getText().toString().replaceAll("\n", " ").trim()); head.setTags(mTagsView.getText().toString().split("\\s+")); /* Add properties. */ head.removeProperties(); for (int i = 0; i < propertyList.getChildCount(); i++){ View property = propertyList.getChildAt(i); CharSequence name = ((TextView) property.findViewById(R.id.name)).getText(); CharSequence value = ((TextView) property.findViewById(R.id.value)).getText(); if (!TextUtils.isEmpty(name)) { // Ignore property with no name head.addProperty(new OrgProperty(name.toString(), value.toString())); } } head.setContent(bodyEdit.getText().toString()); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, view, savedInstanceState); super.onViewCreated(view, savedInstanceState); } @Override public void onActivityCreated(Bundle savedInstanceState) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, savedInstanceState); super.onActivityCreated(savedInstanceState); setupTagsView(); if (mBookId != 0) { mBook = mShelf.getBook(mBookId); } if (mIsNew) { /* Creating new note. */ mNote = new Note(); mNote.getPosition().setBookId(mBookId); updateNewNoteValues(); mViewFlipper.setDisplayedChild(0); editSwitch.setChecked(true); /* Open keyboard for new notes, unless fragment was given * some initial values (for example from ShareActivity). */ if (TextUtils.isEmpty(mInitialTitle) && TextUtils.isEmpty(mInitialContent)) { ActivityUtils.openSoftKeyboard(getActivity(), mTitleView); } } else { /* Get existing note from database. */ // TODO: Cleanup: getNote(id, withProperties) or such try { mNote = mShelf.getNote(mNoteId); mNote.getHead().setProperties(mShelf.getNoteProperties(mNoteId)); mViewFlipper.setDisplayedChild(0); } catch (NoSuchElementException e) { mNote = null; mViewFlipper.setDisplayedChild(1); } } if (mNote != null) { /* Get current values from saved Bundle and populate all views. */ if (savedInstanceState != null) { updateNoteFromBundle(savedInstanceState); } /* Update views from note. */ updateViewsFromNote(); } /* Store the hash value of original note. */ if (!getArguments().containsKey(ARG_ORIGINAL_NOTE_HASH) && mNote != null) { getArguments().putLong(ARG_ORIGINAL_NOTE_HASH, noteHash(mNote)); } /* Refresh action bar items (hide or display, depending on if book is loaded. */ if (getActivity() != null) { getActivity().supportInvalidateOptionsMenu(); } } /* * Auto-complete tags using all known tags from database. */ private void setupTagsView() { /* Collect all known tags. */ String[] knownTags = mShelf.getAllTags(0); /* White text on light gray background, when using android.R.layout.simple_dropdown_item_1line * See https://code.google.com/p/android/issues/detail?id=5237#c8 */ // ArrayAdapter <String> adapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_dropdown_item_1line, knownTags); ArrayAdapter <String> adapter = new ArrayAdapter<>(getActivity(), R.layout.dropdown_item, knownTags); mTagsView.setAdapter(adapter); mTagsView.setTokenizer(new SpaceTokenizer()); } @Override public void onResume() { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); super.onResume(); announceChangesToActivity(); mState.updatePossibleValues(getActivity().getApplicationContext()); mPriority.updatePossibleValues(getActivity().getApplicationContext()); } private void announceChangesToActivity() { if (mListener != null) { mListener.announceChanges( NoteFragment.FRAGMENT_TAG, Book.getFragmentTitleForBook(mBook), Book.getFragmentSubtitleForBook(mBook), 0); } } @Override public void onDestroyView() { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); super.onDestroyView(); mViewFlipper.setDisplayedChild(0); } @Override public void onSaveInstanceState(Bundle outState) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, outState); super.onSaveInstanceState(outState); if (mNote != null) { updateNoteFromViews(); updateBundleFromNote(outState); } } @Override public void onDetach() { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); super.onDetach(); mListener = null; } private void parseArguments() { if (getArguments() == null) { throw new IllegalArgumentException("No arguments found to " + NoteFragment.class.getSimpleName()); } mIsNew = getArguments().getBoolean(ARG_IS_NEW); /* Book ID must exist. */ if (! getArguments().containsKey(ARG_BOOK_ID)) { throw new IllegalArgumentException(NoteFragment.class.getSimpleName() + " requires " + ARG_BOOK_ID + " argument passed"); } mBookId = getArguments().getLong(ARG_BOOK_ID); // /* Book ID must be valid. */ // if (mBookId <= 0) { // throw new IllegalArgumentException("Passed argument book id is not valid (" + mBookId + ")"); // } /* Note ID might or might not be passed - it depends if note is being edited or created. */ if (getArguments().containsKey(ARG_NOTE_ID)) { mNoteId = getArguments().getLong(ARG_NOTE_ID); /* Note ID must be valid if it exists. */ if (mNoteId <= 0) { throw new IllegalArgumentException("Note id is " + mNoteId); } } /* Location (used for new notes). */ place = Place.valueOf(getArguments().getString(ARG_PLACE)); mInitialTitle = getArguments().getString(ARG_TITLE); mInitialContent = getArguments().getString(ARG_CONTENT); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Arguments parsed for " + this); } public boolean isAskingForConfirmationForModifiedNote() { /* It's possible that note does not exist * if it has been deleted and the user went back to it. */ if (mNote != null && isNoteModified()) { new AlertDialog.Builder(getContext()) .setTitle(R.string.note_has_been_modified) .setMessage(R.string.discard_or_save_changes) .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { save(); } }) .setNegativeButton(R.string.discard, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { cancel(); } }) .setNeutralButton(R.string.cancel, null) .create() .show(); return true; } else { return false; } } public boolean isNoteModified() { updateNoteFromViews(); long currentHash = noteHash(mNote); long originalHash = getArguments().getLong(ARG_ORIGINAL_NOTE_HASH); return currentHash != originalHash; } /** * Hash used to detect note modifications. */ private long noteHash(Note note) { OrgParserWriter parserWriter = new OrgParserWriter(); String head = parserWriter.whiteSpacedHead(note.getHead(), note.getPosition().getLevel(), false); return MiscUtils.sha1(head); } private enum TimeType { SCHEDULED, DEADLINE, CLOSED, CLOCKED } private void updateTimestampView(TimeType timeType, TextView button, OrgRange range) { switch (timeType) { case SCHEDULED: if (range != null) { button.setText(mUserTimeFormatter.formatAll(range)); } else { button.setText(getString(R.string.schedule_button_hint)); } break; case CLOSED: /* * Do not display CLOSED button if it's not set. * It will be updated on state change. */ if (range != null) { button.setText(mUserTimeFormatter.formatAll(range)); button.setVisibility(View.VISIBLE); } else { button.setVisibility(View.GONE); } break; case DEADLINE: if (range != null) { button.setText(mUserTimeFormatter.formatAll(range)); } else { button.setText(getString(R.string.deadline_button_hint)); } break; } } /** * Set new Note's initial values. */ private void updateNewNoteValues() { OrgHead head = mNote.getHead(); /* Set scheduled time for a new note. */ if (AppPreferences.isNewNoteScheduled(getContext())) { Calendar cal = Calendar.getInstance(); OrgDateTime timestamp = new OrgDateTime.Builder() .setIsActive(true) .setYear(cal.get(Calendar.YEAR)) .setMonth(cal.get(Calendar.MONTH)) .setDay(cal.get(Calendar.DAY_OF_MONTH)) .build(); OrgRange time = OrgRange.getInstance(timestamp); head.setScheduled(time); } /* Set state for a new note. */ String stateKeyword = AppPreferences.newNoteState(getContext()); if (NoteStateSpinner.isSet(stateKeyword)) { head.setState(stateKeyword); } else { head.setState(null); } /* Initial title. */ if (mInitialTitle != null) { head.setTitle(mInitialTitle); } /* Content. */ StringBuilder content = new StringBuilder(); /* Prepend content with created-at property. */ if (AppPreferences.createdAt(getContext())) { String propertyName = AppPreferences.createdAtProperty(getContext()); String time = OrgDateTime.getInstance(false).toString(); /* Inactive time. */ head.addProperty(new OrgProperty(propertyName, time)); } /* Initial content. */ if (mInitialContent != null) { if (content.length() > 0) { content.append("\n\n"); } content.append(mInitialContent); } if (content.length() > 0) { head.setContent(content.toString()); } } @Override public void onClick(View view) { DialogFragment f = null; switch (view.getId()) { /* Setting scheduled time. */ case R.id.fragment_note_scheduled_button: f = TimestampDialogFragment.getInstance( R.id.fragment_note_scheduled_button, R.string.schedule, mNote.getId(), mNote.getHead().getScheduled() != null ? mNote.getHead().getScheduled().getStartTime() : null); break; /* Setting deadline time. */ case R.id.fragment_note_deadline_button: f = TimestampDialogFragment.getInstance( R.id.fragment_note_deadline_button, R.string.deadline, mNote.getId(), mNote.getHead().getDeadline() != null ? mNote.getHead().getDeadline().getStartTime() : null); break; /* Setting closed time. */ case R.id.fragment_note_closed_button: f = TimestampDialogFragment.getInstance( R.id.fragment_note_closed_button, R.string.closed, mNote.getId(), mNote.getHead().getClosed() != null ? mNote.getHead().getClosed().getStartTime() : null); break; /* New property. */ case R.id.add_property: /* Add a new property with empty name and value. */ addPropertyToList(null); break; } if (f != null) { f.setTargetFragment(this, 0); f.show(getActivity().getSupportFragmentManager(), TimestampDialogFragment.FRAGMENT_TAG); } } @Override /* TimestampDialog */ public void onDateTimeSet(int id, TreeSet<Long> noteIds, OrgDateTime time) { OrgRange range = OrgRange.getInstance(time); switch (id) { case R.id.fragment_note_scheduled_button: updateTimestampView(TimeType.SCHEDULED, mScheduledButton, range); mNote.getHead().setScheduled(range); break; case R.id.fragment_note_deadline_button: updateTimestampView(TimeType.DEADLINE, mDeadlineButton, range); mNote.getHead().setDeadline(range); /* Warn about alerts not being implemented yet. */ Activity activity = getActivity(); if (activity != null) { ((CommonActivity) activity).showSimpleSnackbarLong( R.string.fragment_note_deadline_alarms_not_implemented); } break; case R.id.fragment_note_closed_button: updateTimestampView(TimeType.CLOSED, mClosedButton, range); mNote.getHead().setClosed(range); break; } } @Override /* TimestampDialog */ public void onDateTimeCleared(int id, TreeSet<Long> noteIds) { switch (id) { case R.id.fragment_note_scheduled_button: updateTimestampView(TimeType.SCHEDULED, mScheduledButton, null); mNote.getHead().setScheduled(null); break; case R.id.fragment_note_deadline_button: updateTimestampView(TimeType.DEADLINE, mDeadlineButton, null); mNote.getHead().setDeadline(null); break; case R.id.fragment_note_closed_button: updateTimestampView(TimeType.CLOSED, mClosedButton, null); mNote.getHead().setClosed(null); break; } } @Override /* TimestampDialog */ public void onDateTimeAborted(int id, TreeSet<Long> noteIds) { } /* * Options Menu. */ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, menu, inflater); inflater.inflate(R.menu.note_actions, menu); /* Remove search item. */ menu.removeItem(R.id.activity_action_search); if (mNote == null) { // Displaying non-existent note. menu.removeItem(R.id.close); menu.removeItem(R.id.done); menu.removeItem(R.id.delete); } /* Newly created note cannot be deleted. */ if (mIsNew) { menu.removeItem(R.id.delete); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, item); switch (item.getItemId()) { case R.id.done: save(); return true; case R.id.close: if (!isAskingForConfirmationForModifiedNote()) { cancel(); } return true; case R.id.delete: delete(); return true; default: return super.onOptionsItemSelected(item); } } private void delete() { new AlertDialog.Builder(getContext()) .setTitle(R.string.delete_note) .setMessage(R.string.delete_note_and_all_subnotes) .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mListener.onNoteDeleteRequest(mNote); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }) .create() .show(); } private void cancel() { mListener.onNoteCancelRequest(mNote); } private void save() { /* Make sure notebook is set. */ if (mNote.getPosition().getBookId() == 0) { Activity activity = getActivity(); if (activity != null) { ((CommonActivity) activity).showSimpleSnackbarLong(R.string.note_book_not_set); } return; } if (updateNoteFromViewsAndVerify()) { if (mIsNew) { mListener.onNoteCreateRequest(mNote, place != Place.UNDEFINED ? new NotePlace(mNote.getPosition().getBookId(), mNoteId, place) : null); } else { mListener.onNoteUpdateRequest(mNote); } LocalBroadcastManager.getInstance(getContext()) .sendBroadcast(new Intent(Broadcasts.ACTION_NOTE_CHANGED)); } } private boolean updateNoteFromViewsAndVerify() { updateNoteFromViews(); Activity activity = getActivity(); if (TextUtils.isEmpty(mNote.getHead().getTitle())) { if (activity != null) { titleInputLayout.setError(getString(R.string.can_not_be_empty)); } return false; } return true; } /** * Updates the current book this note belongs to. Only makes sense for new notes. * TODO: Should be setPosition and allow filing under specific note */ public void setBook(Book book) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, book); mBook = book; mBookId = book.getId(); mNote.getPosition().setBookId(mBookId); getArguments().putLong(ARG_BOOK_ID, book.getId()); } private void updateNoteForStateChange(Context context, Note note, String state) { StateChangeLogic stateSetOp = new StateChangeLogic( AppPreferences.todoKeywordsSet(context), AppPreferences.doneKeywordsSet(context) ); stateSetOp.setState(state, note.getHead().getState(), note.getHead().getScheduled(), note.getHead().getDeadline()); /* Update note. */ note.getHead().setState(stateSetOp.getState()); note.getHead().setScheduled(stateSetOp.getScheduled()); note.getHead().setDeadline(stateSetOp.getDeadline()); note.getHead().setClosed(stateSetOp.getClosed()); /* Update views. */ mState.setCurrentValue(stateSetOp.getState()); updateTimestampView(TimeType.SCHEDULED, mScheduledButton, stateSetOp.getScheduled()); updateTimestampView(TimeType.DEADLINE, mDeadlineButton, stateSetOp.getDeadline()); updateTimestampView(TimeType.CLOSED, mClosedButton, stateSetOp.getClosed()); } public interface NoteFragmentListener extends FragmentListener { void onNoteCreateRequest(Note note, NotePlace notePlace); void onNoteUpdateRequest(Note note); void onNoteCancelRequest(Note note); void onNoteDeleteRequest(Note note); } }