package de.blau.android.propertyeditor; import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.List; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.DialogFragment; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.ActionMenuView; import android.support.v7.widget.AppCompatButton; import android.text.Editable; import android.text.Spannable; import android.text.SpannableString; import android.text.TextWatcher; import android.text.style.ForegroundColorSpan; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.Window; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; import ch.poole.conditionalrestrictionparser.Condition; import ch.poole.conditionalrestrictionparser.Condition.CompOp; import ch.poole.conditionalrestrictionparser.ConditionalRestrictionParser; import ch.poole.conditionalrestrictionparser.Conditions; import ch.poole.conditionalrestrictionparser.ParseException; import ch.poole.conditionalrestrictionparser.Restriction; import ch.poole.conditionalrestrictionparser.TokenMgrError; import ch.poole.conditionalrestrictionparser.Util; import de.blau.android.R; import de.blau.android.util.Snack; import de.blau.android.util.ThemeUtils; public class ConditionalRestrictionFragment extends DialogFragment { private static final int LINEARLAYOUT_ID = 12345; private static final String KEY_KEY = "key"; private static final String VALUE_KEY = "value"; private static final String TEMPLATES_KEY = "templates"; private static final String OH_TEMPLATES_KEY = "oh_templates"; private static final String DEBUG_TAG = ConditionalRestrictionFragment.class.getSimpleName(); private LayoutInflater inflater = null; private String key; private String conditionalRestrictionValue; private ArrayList<String> templates; private ArrayList<String> ohTemplates; private ArrayList<Restriction> restrictions = null; private EditText text; /** * Lists of possible values generated from templates */ private ArrayList<String> restrictionValues = null; private ArrayList<String> simpleConditionValues = null; private ArrayList<String> expressionConditionValues = null; private ScrollView sv; private OnSaveListener saveListener = null; /** */ static public ConditionalRestrictionFragment newInstance(String key, String value, ArrayList<String> templates, ArrayList<String> ohTemplates) { ConditionalRestrictionFragment f = new ConditionalRestrictionFragment(); Bundle args = new Bundle(); args.putSerializable(KEY_KEY, key); args.putSerializable(VALUE_KEY, value); args.putSerializable(TEMPLATES_KEY, templates); args.putSerializable(OH_TEMPLATES_KEY, ohTemplates); f.setArguments(args); // f.setShowsDialog(true); return f; } @Override public void onAttach(Activity activity) { super.onAttach(activity); Log.d(DEBUG_TAG, "onAttach"); try { saveListener = (OnSaveListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnSaveListener"); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate"); } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); // request a window without the title dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); return dialog; } @SuppressLint("InflateParams") @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(DEBUG_TAG, "onCreateView"); this.inflater = ThemeUtils.getLayoutInflater(getActivity()); LinearLayout conditionalRestrictionLayout = (LinearLayout) inflater.inflate(R.layout.conditionalrestriction, null); if (savedInstanceState == null) { key = getArguments().getString(KEY_KEY); conditionalRestrictionValue = getArguments().getString(VALUE_KEY); templates = getArguments().getStringArrayList(TEMPLATES_KEY); ohTemplates = getArguments().getStringArrayList(OH_TEMPLATES_KEY); } else { key = savedInstanceState.getString(KEY_KEY); conditionalRestrictionValue = savedInstanceState.getString(VALUE_KEY); templates = savedInstanceState.getStringArrayList(TEMPLATES_KEY); ohTemplates = savedInstanceState.getStringArrayList(OH_TEMPLATES_KEY); } if (conditionalRestrictionValue == null) { conditionalRestrictionValue = ""; } // generate the list of possible values from the templates for (String t:templates) { Log.d(DEBUG_TAG, "Parsing template " + t ); ConditionalRestrictionParser parser = new ConditionalRestrictionParser(new ByteArrayInputStream(t.getBytes())); try { ArrayList<Restriction> list = parser.restrictions(); for (Restriction r:list) { String v = r.getValue(); try { //noinspection ResultOfMethodCallIgnored Integer.parseInt(v); } catch (NumberFormatException nfex) { // not a number add it to list if (restrictionValues==null) { restrictionValues = new ArrayList<String>(); } restrictionValues.add(v); } for (Condition c:r.getConditions()) { if (c.isExpression()) { try { //noinspection ResultOfMethodCallIgnored Integer.parseInt(c.term1()); if (expressionConditionValues==null) { expressionConditionValues = new ArrayList<String>(); } expressionConditionValues.add(c.term2()); } catch (NumberFormatException nfex) { // not a number add it to list if (expressionConditionValues==null) { expressionConditionValues = new ArrayList<String>(); } expressionConditionValues.add(c.term1()); } } else if (!c.isOpeningHours()) { if (simpleConditionValues==null) { simpleConditionValues = new ArrayList<String>(); } simpleConditionValues.add(c.term1()); } } } } catch (Exception ex) { Log.e(DEBUG_TAG, "Parsing template " + t + " raised " + ex); } } buildLayout(conditionalRestrictionLayout, conditionalRestrictionValue); // add callbacks for the buttons AppCompatButton cancel = (AppCompatButton) conditionalRestrictionLayout.findViewById(R.id.cancel); cancel.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { dismiss(); }}); AppCompatButton save = (AppCompatButton) conditionalRestrictionLayout.findViewById(R.id.save); save.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { saveListener.save(key,text.getText().toString()); dismiss(); }}); return conditionalRestrictionLayout; } @Override public boolean onOptionsItemSelected(final MenuItem item) { Log.d(DEBUG_TAG, "onOptionsItemSelected"); return true; } private final Runnable rebuild = new Runnable() { @Override public void run() { ConditionalRestrictionParser parser = new ConditionalRestrictionParser(new ByteArrayInputStream(text.getText().toString().getBytes())); text.removeTextChangedListener(watcher); // avoid infinite loop try { restrictions = parser.restrictions(); removeHighlight(text); } catch (ParseException pex) { Log.d(DEBUG_TAG, pex.getMessage()); highlightParseError(text, pex); } catch (TokenMgrError err) { // we currently can't do anything reasonable here except ignore Log.e(DEBUG_TAG, err.getMessage()); } text.addTextChangedListener(watcher); if (restrictions == null) { // couldn't parse anything restrictions = new ArrayList<Restriction>(); } buildForm(sv,restrictions); } }; private final TextWatcher watcher = new TextWatcher() { @Override public void afterTextChanged(Editable s) { text.removeCallbacks(rebuild); text.postDelayed(rebuild, 500); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } }; /** * Initial setup of layout * @param conditionalRestrictionLayout * @param conditionalRestrictionValue */ private void buildLayout(final LinearLayout conditionalRestrictionLayout, final String conditionalRestrictionValue) { text = (EditText) conditionalRestrictionLayout.findViewById(R.id.conditional_restriction_string_edit); sv = (ScrollView) conditionalRestrictionLayout.findViewById(R.id.conditional_restriction_view); if (text != null && sv != null) { text.addTextChangedListener(watcher); text.setText(conditionalRestrictionValue); sv.removeAllViews(); FloatingActionButton add = (FloatingActionButton) conditionalRestrictionLayout.findViewById(R.id.add); add.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { LinearLayout ll = (LinearLayout) conditionalRestrictionLayout.findViewById(LINEARLAYOUT_ID); ArrayList<Condition> c = new ArrayList<Condition>(); c.add(new Condition("",false)); Restriction r = new Restriction("", new Conditions(c,false)); restrictions.add(r); addRestriction(ll, r); }} ); // initial build text.removeCallbacks(rebuild); text.post(rebuild); } } /** * Highlight the position of a parse error * @param text * @param pex */ private synchronized void highlightParseError(EditText text, ParseException pex) { if (text.length() > 0) { int c = pex.currentToken.next.beginColumn-1; // starts at 1 int pos = text.getSelectionStart(); Spannable spannable = new SpannableString(text.getText()); spannable.setSpan(new ForegroundColorSpan(Color.RED), c, Math.max(c,Math.min(c+1,spannable.length())), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); text.setText(spannable, TextView.BufferType.SPANNABLE); text.setSelection(Math.min(pos,spannable.length())); Snack.barError(getActivity(), pex.getLocalizedMessage()); } } /** * Remove all highlighting * @param text */ private synchronized void removeHighlight(EditText text) { int pos = text.getSelectionStart(); int prevLen = text.length(); String t = Util.restrictionsToString(restrictions); text.setText(t); // text.setText(text.getText().toString()); text.setSelection(prevLen < text.length() ? text.length() : Math.min(pos,text.length())); } private synchronized void buildForm(ScrollView sv, ArrayList<Restriction> restrictions) { sv.removeAllViews(); Activity a = getActivity(); if (a == null) { return; } LinearLayout ll = new LinearLayout(a); ll.setId(LINEARLAYOUT_ID); ll.setPadding(0, 0, 0, dpToPixels(64)); ll.setOrientation(LinearLayout.VERTICAL); sv.addView(ll); int n = 1; for (Restriction r : restrictions) { addRestriction(ll, r); } } private void addRestriction(LinearLayout ll, final Restriction r) { LinearLayout groupHeader = (LinearLayout) inflater.inflate(R.layout.restriction_header, null); TextView header = (TextView) groupHeader.findViewById(R.id.header); header.setText(R.string.tag_restriction_header); final AutoCompleteTextView value = (AutoCompleteTextView) groupHeader.findViewById(R.id.editValue); String v = r.getValue().trim(); value.setText(v); if (restrictionValues != null) { ArrayAdapter<String>adapter = new ArrayAdapter<String>(getActivity(),R.layout.autocomplete_row,restrictionValues); if (!restrictionValues.contains(v)) { adapter.insert(v, 0); } setAdapterAndListeners(value, adapter); } TextWatcher valueWatcher = new TextWatcher() { @Override public void afterTextChanged(Editable s) { r.setValue(value.getText().toString().trim()); Runnable rebuild = new Runnable() { @Override public void run() { updateRestrictionStringFromView(value, r); } }; value.removeCallbacks(rebuild); value.postDelayed(rebuild, 500); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } }; value.addTextChangedListener(valueWatcher); addMenuItems(groupHeader, r, null); ll.addView(groupHeader); boolean first = true; for (int i=0;i<r.getConditions().size();i++) { Condition c = r.getConditions().get(i); if (c != null) { final int index = i; if (c.isExpression()) { LinearLayout expression = (LinearLayout) inflater.inflate(R.layout.expression, null); final TextView expressionText = (TextView) expression.findViewById(R.id.text); if (first) { expressionText.setText(R.string.tag_restriction_when); first = false; } else { expressionText.setText(R.string.tag_restriction_and); } final AutoCompleteTextView term1 = (AutoCompleteTextView) expression.findViewById(R.id.editTerm1); term1.setText(c.term1()); final AutoCompleteTextView term2 = (AutoCompleteTextView) expression.findViewById(R.id.editTerm2); term2.setText(c.term2()); AutoCompleteTextView term = null; if (expressionConditionValues != null) { ArrayAdapter<String>adapter = new ArrayAdapter<String>(getActivity(),R.layout.autocomplete_row,expressionConditionValues); try { //noinspection ResultOfMethodCallIgnored Integer.parseInt(c.term1()); if (!expressionConditionValues.contains(c.term2())) { adapter.insert(c.term2(), 0); } term = term2; } catch (NumberFormatException nfex) { if (!expressionConditionValues.contains(c.term1())) { adapter.insert(c.term1(), 0); } term = term1; } setAdapterAndListeners(term, adapter); } addMenuItems(expression, r, c); ArrayAdapter<String> adapter = new ArrayAdapter<String>( getActivity(), R.layout.support_simple_spinner_dropdown_item, Condition.compOpStrings); // adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); final Spinner operator = (Spinner) expression.findViewById(R.id.operator); operator.setAdapter(adapter); operator.setSelection(Condition.compOpStrings.indexOf(Condition.opToString(c.operator()))); final Runnable rebuild = new Runnable() { @Override public void run() { updateRestrictionStringFromView(null, r); } }; operator.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { List<Condition> list = r.getConditions(); final String c = term1.getText().toString().trim()+operator.getSelectedItem()+term2.getText().toString().trim(); list.set(index, new Condition(c, false)); term1.removeCallbacks(rebuild); term1.post(rebuild); } @Override public void onNothingSelected(AdapterView<?> parent) { }}); TextWatcher expressionWatcher = new TextWatcher() { @Override public void afterTextChanged(Editable s) { List<Condition> list = r.getConditions(); final String c = term1.getText().toString().trim()+operator.getSelectedItem()+term2.getText().toString().trim(); list.set(index, new Condition(c, false)); term1.removeCallbacks(rebuild); term1.postDelayed(rebuild, 500); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } }; term1.addTextChangedListener(expressionWatcher); term2.addTextChangedListener(expressionWatcher); ll.addView(expression); } else { // for now simply fill the ATV differently for OH LinearLayout condition = (LinearLayout) inflater.inflate(R.layout.condition, null); final TextView conditionText = (TextView) condition.findViewById(R.id.text); if (first) { conditionText.setText(R.string.tag_restriction_when); first = false; } else { conditionText.setText(R.string.tag_restriction_and); } final AutoCompleteTextView term = (AutoCompleteTextView) condition.findViewById(R.id.editCondition); term.setText(c.term1()); ArrayAdapter<String>adapter = null; if (c.isOpeningHours()) { if (ohTemplates != null) { adapter = new ArrayAdapter<String>(getActivity(),R.layout.autocomplete_row,ohTemplates); if (!ohTemplates.contains(c.term1())) { adapter.insert(c.term1(), 0); } } } else { if (simpleConditionValues != null) { adapter = new ArrayAdapter<String>(getActivity(),R.layout.autocomplete_row,simpleConditionValues); if (!simpleConditionValues.contains(c.term1())) { adapter.insert(c.term1(), 0); } } } if (adapter != null) { setAdapterAndListeners(term, adapter); } TextWatcher conditionWatcher = new TextWatcher() { @Override public void afterTextChanged(Editable s) { List<Condition> list = r.getConditions(); String c = term.getText().toString().trim(); boolean needsParentheses = c.indexOf(';') >= 0; if (needsParentheses) { r.setInParen(); } list.set(index, new Condition(c, false)); Runnable rebuild = new Runnable() { @Override public void run() { updateRestrictionStringFromView(term, r); } }; term.removeCallbacks(rebuild); term.postDelayed(rebuild, 500); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } }; term.addTextChangedListener(conditionWatcher); addMenuItems(condition, r, c); ll.addView(condition); } } } } private void setAdapterAndListeners(AutoCompleteTextView atv, ArrayAdapter adapter) { atv.setAdapter(adapter); atv.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { ((AutoCompleteTextView)v).showDropDown(); } } }); atv.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (v.hasFocus()) { ((AutoCompleteTextView)v).showDropDown(); } } }); } private synchronized void updateRestrictionStringFromView(EditText view, Restriction r) { text.removeTextChangedListener(watcher); int pos = 0; if (view != null) { pos = view.getSelectionStart(); } conditionalRestrictionValue = Util.restrictionsToString(restrictions); text.setText(conditionalRestrictionValue); String myText = r.toString(); int textPos = conditionalRestrictionValue.lastIndexOf(myText); // make what we are currently editing visible, this is a bit of a hack if (textPos < conditionalRestrictionValue.length()/2) { text.setSelection(textPos); } else { text.setSelection(textPos + myText.length()); } if (view != null) { view.setSelection(Math.min(pos,text.length())); } text.addTextChangedListener(watcher); } /** * Add menu items to the restriction menu to add various types of conditions * @param menu * @param item * @param r * @param c */ private void addConditionItem(Menu menu,int item,final Restriction r, final Condition c) { MenuItem mi = menu.add(item); mi.setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem arg0) { Log.d(DEBUG_TAG, "onMenuItemClick"); r.getConditions().add(c); conditionalRestrictionValue = Util.restrictionsToString(restrictions); text.setText(conditionalRestrictionValue); return true; }}); MenuItemCompat.setShowAsAction(mi,MenuItemCompat.SHOW_AS_ACTION_NEVER); } private Menu addMenuItems(LinearLayout row, final Restriction r, final Condition c) { ActionMenuView amv = (ActionMenuView) row.findViewById(R.id.menu); Menu menu = amv.getMenu(); if (c == null) { addConditionItem(menu,R.string.tag_restriction_add_simple_condition,r,new Condition("",false)); addConditionItem(menu,R.string.tag_restriction_add_expression,r,new Condition("",CompOp.EQ,"")); addConditionItem(menu,R.string.tag_restriction_add_opening_hours,r,new Condition("",true)); } MenuItem mi = menu.add(R.string.delete); mi.setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem arg0) { Log.d(DEBUG_TAG, "onMenuItemClick"); if (c == null) { restrictions.remove(r); } else { r.getConditions().remove(c); } conditionalRestrictionValue = Util.restrictionsToString(restrictions); text.setText(conditionalRestrictionValue); return true; }}); MenuItemCompat.setShowAsAction(mi,MenuItemCompat.SHOW_AS_ACTION_NEVER); return menu; } private Runnable updateStringRunnable = new Runnable() { @Override public void run() { int pos = text.getSelectionStart(); int prevLen = text.length(); text.removeTextChangedListener(watcher); String oh = Util.restrictionsToString(restrictions); text.setText(oh); text.setSelection(prevLen < text.length() ? text.length() : Math.min(pos,text.length())); text.addTextChangedListener(watcher); } }; /** * Update the actual CR string */ private void updateString() { text.removeCallbacks(updateStringRunnable); text.postDelayed(updateStringRunnable,100); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "onSaveInstanceState"); outState.putSerializable(KEY_KEY, key); outState.putSerializable(VALUE_KEY, text.getText().toString()); outState.putSerializable(TEMPLATES_KEY, templates); outState.putSerializable(OH_TEMPLATES_KEY, ohTemplates); } @Override public void onPause() { super.onPause(); Log.d(DEBUG_TAG, "onPause"); // savedParents = getParentRelationMap(); } @Override public void onStop() { super.onStop(); Log.d(DEBUG_TAG, "onStop"); } @Override public void onDestroy() { super.onDestroy(); Log.d(DEBUG_TAG, "onDestroy"); } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); // disable address tagging for stuff that won't have an address // menu.findItem(R.id.tag_menu_address).setVisible(!type.equals(Way.NAME) // || element.hasTagKey(Tags.KEY_BUILDING)); } /** * Return the view we have our rows in and work around some android * craziness * * @return */ public View getOurView() { // android.support.v4.app.NoSaveStateFrameLayout View v = getView(); if (v != null) { if (v.getId() == R.id.conditional_restriction_layout) { Log.d(DEBUG_TAG, "got correct view in getView"); return v; } else { v = v.findViewById(R.id.conditional_restriction_layout); if (v == null) { Log.d(DEBUG_TAG, "didn't find R.id.openinghours_layout"); } else { Log.d(DEBUG_TAG, "Found R.id.openinghours_layoutt"); } return v; } } else { Log.d(DEBUG_TAG, "got null view in getView"); } return null; } private int dpToPixels(int dp) { Resources r = getResources(); return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()); } }