/* * @copyright 2013 Philip Warner * @license GNU General Public License * * This file is part of Book Catalogue. * * Book Catalogue 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. * * Book Catalogue 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 Book Catalogue. If not, see <http://www.gnu.org/licenses/>. */ package com.eleybourn.bookcatalogue.dialogs; import java.util.Calendar; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.text.Editable; import android.text.Selection; import android.text.TextWatcher; import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.Toast; import com.eleybourn.bookcatalogue.R; /** * Dialog class to allow for selection of partial dates from 0AD to 9999AD. * * The constructors and interface are now protected because this really should * only be called as part of the fragment version. * * @author pjw */ public class PartialDatePicker extends AlertDialog { /** Calling context */ private Context mContext; /** Currently displayed year; null if empty/invalid */ private Integer mYear; /** Currently displayed month; null if empty/invalid */ private Integer mMonth; /** Currently displayed day; null if empty/invalid */ private Integer mDay; /** Listener to be called when date is set or dialog cancelled */ private OnDateSetListener mListener; /** Local ref to month spinner */ private Spinner mMonthSpinner; /** Local ref to day spinner */ private Spinner mDaySpinner; /** Local ref to day spinner adapter */ private ArrayAdapter<String> mDayAdapter; /** Local ref to year text view */ private EditText mYearView; /** * Listener to receive notifications when dialog is closed by any means. * * @author pjw */ protected static interface OnDateSetListener { public void onDateSet(PartialDatePicker dialog, Integer year, Integer month, Integer day); public void onCancel(PartialDatePicker dialog); } /** * Constructor * * @param context Calling context * @param listener Listener for dialog events */ protected PartialDatePicker(Context context) { this(context, null, null, null); } /** * Constructor * * @param context Calling context * @param listener Listener for dialog events * @param year Starting year * @param month Starting month * @param day Starting day */ protected PartialDatePicker(Context context, Integer year, Integer month, Integer day) { super(context); mContext = context; mYear = year; mMonth = month; mDay = day; // Get the layout LayoutInflater inf = this.getLayoutInflater(); View root = inf.inflate(R.layout.date_picker, null); // Ensure components match current locale order reorderPickers(root); // Set the view setView(root); // Get UI components for later use mYearView = (EditText)root.findViewById(R.id.year); mMonthSpinner = (Spinner)root.findViewById(R.id.month); mDaySpinner = (Spinner)root.findViewById(R.id.day); // Create month spinner adapter ArrayAdapter<String> monthAdapter = new ArrayAdapter<String>(context, android.R.layout.simple_spinner_item); monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mMonthSpinner.setAdapter(monthAdapter); // Create day spinner adapter mDayAdapter = new ArrayAdapter<String>(context, android.R.layout.simple_spinner_item); mDayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mDaySpinner.setAdapter(mDayAdapter); // First entry is 'unknown' monthAdapter.add("---"); mDayAdapter.add("--"); // Make sure that the spinner can initially take any 'day' value. Otherwise, when a dialog is // reconstructed after rotation, the 'day' field will not be restorable by Android. regenDaysOfMonth(31); // Get a calendar for locale-related info Calendar cal = Calendar.getInstance(); // Set the day to 1...so avoid wrap on short months (default to current date) cal.set(Calendar.DAY_OF_MONTH, 1); // Add all month named (abbreviated) for(int i = 0; i < 12; i++) { cal.set(Calendar.MONTH, i); monthAdapter.add(String.format("%tb",cal)); } // Handle selections from the MONTH spinner mMonthSpinner.setOnItemSelectedListener(new OnItemSelectedListener(){ @Override public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) { int pos = mMonthSpinner.getSelectedItemPosition(); handleMonth(pos); } @Override public void onNothingSelected(AdapterView<?> arg0) { handleMonth(null); }} ); // Handle selections from the DAY spinner mDaySpinner.setOnItemSelectedListener(new OnItemSelectedListener(){ @Override public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) { int pos = mDaySpinner.getSelectedItemPosition(); handleDay(pos); } @Override public void onNothingSelected(AdapterView<?> arg0) { handleDay(null); }} ); // Handle all changes to the YEAR text mYearView.addTextChangedListener(new TextWatcher() { @Override public void afterTextChanged(Editable s) { handleYear(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { }}); // Handle YEAR + ((Button)root.findViewById(R.id.plus)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mYear != null) { mYearView.setText((++mYear).toString()); } else { mYearView.setText(Calendar.getInstance().get(Calendar.YEAR) + ""); } }} ); // Handle YEAR - ((Button)root.findViewById(R.id.minus)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mYear != null) { // We can't support negatvive years yet because of sorting issues and the fact that // the Calendar object bugs out with them. To fix the calendar object interface we // would need to translate -ve years to Epoch settings throughout the app. For now, // not many people have books written before 0AD, so it's a low priority. if (mYear > 0) { mYearView.setText((--mYear).toString()); } } else { mYearView.setText(Calendar.getInstance().get(Calendar.YEAR) + ""); } }} ); // Handle MONTH + ((Button)root.findViewById(R.id.plusMonth)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = (mMonthSpinner.getSelectedItemPosition() + 1) % mMonthSpinner.getCount(); mMonthSpinner.setSelection(pos); }} ); // Handle MONTH - ((Button)root.findViewById(R.id.minusMonth)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = (mMonthSpinner.getSelectedItemPosition() - 1 + mMonthSpinner.getCount()) % mMonthSpinner.getCount(); mMonthSpinner.setSelection(pos); }} ); // Handle DAY + ((Button)root.findViewById(R.id.plusDay)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = (mDaySpinner.getSelectedItemPosition() + 1) % mDaySpinner.getCount(); mDaySpinner.setSelection(pos); }} ); // Handle DAY - ((Button)root.findViewById(R.id.minusDay)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = (mDaySpinner.getSelectedItemPosition() - 1 + mDaySpinner.getCount()) % mDaySpinner.getCount(); mDaySpinner.setSelection(pos); }} ); // Handle OK ((Button)root.findViewById(R.id.ok)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Ensure the date is 'hierarchically valid'; require year, if month is non-null, require month if day non-null if (mDay != null && mDay > 0 && (mMonth == null || mMonth == 0)) { Toast.makeText(mContext, R.string.if_day_is_specified_month_and_year_must_be, Toast.LENGTH_LONG).show(); } else if (mMonth != null && mMonth > 0 && mYear == null) { Toast.makeText(mContext, R.string.if_month_is_specified_year_must_be, Toast.LENGTH_LONG).show(); } else { if (mListener != null) mListener.onDateSet(PartialDatePicker.this, mYear, mMonth, mDay); } }} ); // Handle Cancel ((Button)root.findViewById(R.id.cancel)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mListener != null) mListener.onCancel(PartialDatePicker.this); }} ); // Handle any other form of cancellation this.setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface arg0) { if (mListener != null) mListener.onCancel(PartialDatePicker.this); }}); // We are all set up! // Set the initial date setDate(year, month, day); } /** * Set the date to display * * @param year Year (or null) * @param month Month (or null) * @param day Day (or null) */ public void setDate(Integer year, Integer month, Integer day) { mYear = year; mMonth = month; mDay = day; String yearVal; if (year != null) { yearVal = year.toString(); } else { yearVal = ""; } mYearView.setText(yearVal); Editable e = mYearView.getEditableText(); Selection.setSelection(e, e.length(), e.length()); if (yearVal.equals("")) { mYearView.requestFocus(); getWindow().setSoftInputMode (WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); } if (month == null || month == 0) { mMonthSpinner.setSelection(0); } else { mMonthSpinner.setSelection(month); } if (day == null || day == 0) { mDaySpinner.setSelection(0); } else { mDaySpinner.setSelection(day); } } /** Accessor */ public Integer getYear() { return mYear; } /** Accessor */ public Integer getMonth() { return mMonth; } /** Accessor */ public Integer getDay() { return mDay; } /** * Handle changes to the YEAR field. */ private void handleYear() { // Try to convert to integer String val = mYearView.getText().toString(); try { int year = Integer.parseInt(val); mYear = year; } catch (Exception e) { mYear = null; } // Seems reasonable to disable other spinners if year invalid, but it actually // not very friendly when entering data for new books. regenDaysOfMonth(null); // Handle the result //if (mYear == null) { // mMonthSpinner.setEnabled(false); // mDaySpinner.setEnabled(false); //} else { // // Enable other spinners as appropriate // mMonthSpinner.setEnabled(true); // mDaySpinner.setEnabled(mMonthSpinner.getSelectedItemPosition() > 0); // regenDaysOfMonth(null); //} } /** * Handle changes to the MONTH field * @param pos */ private void handleMonth(Integer pos) { // See if we got a valid month boolean isMonth = (pos != null && pos > 0); // Seems reasonable to disable other spinners if year invalid, but it actually // not very friendly when entering data for new books. if (!isMonth) { // If not, disable DAY spinner; we leave current value intact in case a valid month is set later //mDaySpinner.setEnabled(false); mMonth = null; } else { // Set the month and make sure DAY spinner is valid mMonth = pos; //mDaySpinner.setEnabled(true); //regenDaysOfMonth(null); } regenDaysOfMonth(null); } /** * Handle changes to the DAY spinner * * @param pos */ private void handleDay(Integer pos) { boolean isSelected = (pos != null && pos > 0); if (!isSelected) { mDay = null; } else { mDay = pos; } } /** * Depending on year/month selected, generate the DAYS spinner values */ private void regenDaysOfMonth(Integer totalDays) { // Save the current day in case the regen alters it Integer daySave = mDay; //ArrayAdapter<String> days = (ArrayAdapter<String>)mDaySpinner.getAdapter(); // Make sure we have the 'no-day' value in the dialog if (mDayAdapter.getCount() == 0) mDayAdapter.add("--"); // Determine the total days if not passed to us if (totalDays == null || totalDays == 0) { if (mYear != null && mMonth != null && mMonth > 0) { // Get a calendar for the year/month Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, mYear); cal.set(Calendar.MONTH, mMonth-1); // Add appropriae days totalDays = cal.getActualMaximum(Calendar.DAY_OF_MONTH); } else { totalDays = 31; } } // If we have a valid total number of days, then update the list if (totalDays != null) { // Don't forget we have a '--' in the adapter if (mDayAdapter.getCount() <= totalDays) { for(int i = mDayAdapter.getCount(); i <= totalDays; i++) { mDayAdapter.add(i + ""); } } else { for(int i = mDayAdapter.getCount() - 1; i > totalDays; i--) { mDayAdapter.remove(i + ""); } } // Ensure selected day is valid if (daySave == null || daySave == 0) { mDaySpinner.setSelection(0); } else { if (daySave > totalDays) daySave = totalDays; mDaySpinner.setSelection(daySave); } } } /** * Reorder the views in the dialog to suit the curret locale. * * @param root Root view */ private void reorderPickers(View root) { char[] order; try { // This actually throws exception in some versions of Android, specifically when // the locale-specific date format has the day name (EEE) in it. So we exit and // just use our default order in these cases. // See Issue 712. order = DateFormat.getDateFormatOrder(mContext); } catch(Exception e) { return; } /* Default order is {year, month, date} so if that's the order then * do nothing. */ if ((order[0] == DateFormat.YEAR) && (order[1] == DateFormat.MONTH)) { return; } /* Remove the 3 pickers from their parent and then add them back in the * required order. */ LinearLayout parent = (LinearLayout) root.findViewById(R.id.dateSelector); // Get the three views View y = root.findViewById(R.id.yearSelector); View m = root.findViewById(R.id.monthSelector); View d = root.findViewById(R.id.daySelector); // Remove them parent.removeAllViews(); // Re-add in the correct order. for (char c : order) { if (c == DateFormat.DATE) { parent.addView(d); } else if (c == DateFormat.MONTH) { parent.addView(m); } else { parent.addView (y); } } } public void setOnDateSetListener(OnDateSetListener listener) { mListener = listener; } }