Index: trunk/src/org/openstreetmap/josm/gui/QuadStateCheckBox.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/QuadStateCheckBox.java	(revision 591)
+++ trunk/src/org/openstreetmap/josm/gui/QuadStateCheckBox.java	(revision 591)
@@ -0,0 +1,189 @@
+// License: GPL. Copyright 2008 by Frederik Ramm and others
+package org.openstreetmap.josm.gui;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+
+import javax.swing.AbstractAction;
+import javax.swing.ActionMap;
+import javax.swing.ButtonGroup;
+import javax.swing.ButtonModel;
+import javax.swing.Icon;
+import javax.swing.JCheckBox;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeListener;
+import javax.swing.plaf.ActionMapUIResource;
+
+public class QuadStateCheckBox extends JCheckBox {
+
+	public enum State { NOT_SELECTED, SELECTED, UNSET, PARTIAL }
+
+	private final QuadStateDecorator model;
+	private State[] allowed;
+
+	public QuadStateCheckBox(String text, Icon icon, State initial, State[] allowed) {
+		super(text, icon);
+		this.allowed = allowed;
+		// Add a listener for when the mouse is pressed
+		super.addMouseListener(new MouseAdapter() {
+			@Override public void mousePressed(MouseEvent e) {
+				grabFocus();
+				model.nextState();
+			}
+		});
+		// Reset the keyboard action map
+		ActionMap map = new ActionMapUIResource();
+		map.put("pressed", new AbstractAction() {
+			public void actionPerformed(ActionEvent e) {
+				grabFocus();
+				model.nextState();
+			}
+		});
+		map.put("released", null);
+		SwingUtilities.replaceUIActionMap(this, map);
+		// set the model to the adapted model
+		model = new QuadStateDecorator(getModel());
+		setModel(model);
+		setState(initial);
+	}
+	public QuadStateCheckBox(String text, State initial, State[] allowed) {
+		this(text, null, initial, allowed);
+	}
+
+	/** Do not let anyone add mouse listeners */
+	@Override public void addMouseListener(MouseListener l) { }
+	/**
+	 * Set the new state.
+	 */
+	public void setState(State state) { model.setState(state); }
+	/** Return the current state, which is determined by the
+	 * selection status of the model. */
+	public State getState() { return model.getState(); }
+	@Override public void setSelected(boolean b) {
+		if (b) {
+			setState(State.SELECTED);
+		} else {
+			setState(State.NOT_SELECTED);
+		}
+	}
+
+	private class QuadStateDecorator implements ButtonModel {
+		private final ButtonModel other;
+		private QuadStateDecorator(ButtonModel other) {
+			this.other = other;
+		}
+		private void setState(State state) {
+			if (state == State.NOT_SELECTED) {
+				other.setArmed(false);
+				setPressed(false);
+				setSelected(false);
+				setToolTipText(tr("false: the property is explicitly switched off"));
+			} else if (state == State.SELECTED) {
+				other.setArmed(false);
+				setPressed(false);
+				setSelected(true);
+				setToolTipText(tr("true: the property is explicitly switched off"));
+			} else if (state == State.PARTIAL) {
+				other.setArmed(true);
+				setPressed(true);
+				setSelected(true);
+				setToolTipText(tr("partial: different selected objects have different values, do not change"));
+			} else {
+				other.setArmed(true);
+				setPressed(true);
+				setSelected(false);
+				setToolTipText(tr("unset: do not set this property on the selected objects"));
+			}
+		}
+		/**
+		 * The current state is embedded in the selection / armed
+		 * state of the model.
+		 * 
+		 * We return the SELECTED state when the checkbox is selected
+		 * but not armed, PARTIAL state when the checkbox is
+		 * selected and armed (grey) and NOT_SELECTED when the
+		 * checkbox is deselected.
+		 */
+		private State getState() {
+			if (isSelected() && !isArmed()) {
+				// normal black tick
+				return State.SELECTED;
+			} else if (isSelected() && isArmed()) {
+				// don't care grey tick
+				return State.PARTIAL;
+			} else if (!isSelected() && !isArmed()) {
+				return State.NOT_SELECTED;
+			} else {
+				return State.UNSET;
+			}
+		}
+		/** Rotate to the next allowed state.*/
+		private void nextState() {
+			State current = getState();
+			for (int i = 0; i < allowed.length; i++) {
+				if (allowed[i] == current) {
+					setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]);
+					break;
+				}
+			}
+		}
+		/** Filter: No one may change the armed status except us. */
+		public void setArmed(boolean b) {
+		}
+		/** We disable focusing on the component when it is not
+		 * enabled. */
+		public void setEnabled(boolean b) {
+			setFocusable(b);
+			other.setEnabled(b);
+		}
+		/** All these methods simply delegate to the "other" model
+		 * that is being decorated. */
+		public boolean isArmed() { return other.isArmed(); }
+		public boolean isSelected() { return other.isSelected(); }
+		public boolean isEnabled() { return other.isEnabled(); }
+		public boolean isPressed() { return other.isPressed(); }
+		public boolean isRollover() { return other.isRollover(); }
+		public void setSelected(boolean b) { other.setSelected(b); }
+		public void setPressed(boolean b) { other.setPressed(b); }
+		public void setRollover(boolean b) { other.setRollover(b); }
+		public void setMnemonic(int key) { other.setMnemonic(key); }
+		public int getMnemonic() { return other.getMnemonic(); }
+		public void setActionCommand(String s) {
+			other.setActionCommand(s);
+		}
+		public String getActionCommand() {
+			return other.getActionCommand();
+		}
+		public void setGroup(ButtonGroup group) {
+			other.setGroup(group);
+		}
+		public void addActionListener(ActionListener l) {
+			other.addActionListener(l);
+		}
+		public void removeActionListener(ActionListener l) {
+			other.removeActionListener(l);
+		}
+		public void addItemListener(ItemListener l) {
+			other.addItemListener(l);
+		}
+		public void removeItemListener(ItemListener l) {
+			other.removeItemListener(l);
+		}
+		public void addChangeListener(ChangeListener l) {
+			other.addChangeListener(l);
+		}
+		public void removeChangeListener(ChangeListener l) {
+			other.removeChangeListener(l);
+		}
+		public Object[] getSelectedObjects() {
+			return other.getSelectedObjects();
+		}
+	}
+}
+
Index: trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPreset.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPreset.java	(revision 590)
+++ trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPreset.java	(revision 591)
@@ -16,6 +16,9 @@
 import java.net.URL;
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 import java.util.StringTokenizer;
 
@@ -23,6 +26,6 @@
 import javax.swing.Action;
 import javax.swing.ImageIcon;
-import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
+import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JOptionPane;
@@ -35,4 +38,5 @@
 import org.openstreetmap.josm.command.SequenceCommand;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.QuadStateCheckBox;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
@@ -52,24 +56,80 @@
 	public static abstract class Item {
 		public boolean focus = false;
-		abstract void addToPanel(JPanel p);
+		abstract void addToPanel(JPanel p, Collection<OsmPrimitive> sel);
 		abstract void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds);
 		boolean requestFocusInWindow() {return false;}
 	}
-
+	
+	public static class Usage {
+		Set<String> values;
+	}
+	
+	public static final String DIFFERENT = tr("<different>");
+	
+	static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
+		Usage returnValue = new Usage();
+		returnValue.values = new HashSet<String>();
+		for (OsmPrimitive s : sel) {
+			returnValue.values.add(s.get(key));
+		}
+		return returnValue;
+	}
+
+	static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
+
+		Usage returnValue = new Usage();
+		returnValue.values = new HashSet<String>();
+		for (OsmPrimitive s : sel) {
+			String v = s.get(key);
+			if ("true".equalsIgnoreCase(v)) v = "true";
+			else if ("yes".equalsIgnoreCase(v)) v = "true";
+			else if ("1".equals(v)) v = "true";
+			else if ("false".equalsIgnoreCase(v)) v = "false";
+			else if ("no".equalsIgnoreCase(v)) v = "false";
+			else if ("0".equals(v)) v = "false";			
+			returnValue.values.add(v);
+		}
+		return returnValue;
+	}
+	
 	public static class Text extends Item {
 		public String key;
 		public String text;
 		public String default_;
+		public String originalValue;
 		public boolean delete_if_empty = false;
 
-		private JTextField value = new JTextField();
-
-		@Override public void addToPanel(JPanel p) {
-			value.setText(default_ == null ? "" : default_);
+		private JComponent value;
+		
+		@Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
+			
+			// find out if our key is already used in the selection.
+			Usage usage = determineTextUsage(sel, key);
+			
+			if (usage.values.size() == 1) {
+				// all objects use the same value
+				value = new JTextField();
+				for (String s : usage.values) ((JTextField) value).setText(s);
+				originalValue = ((JTextField)value).getText();
+			} else {
+				// the objects have different values
+				value = new JComboBox(usage.values.toArray());
+				((JComboBox)value).setEditable(true);
+	            ((JComboBox)value).getEditor().setItem(DIFFERENT);
+	            originalValue = DIFFERENT;
+			}
 			p.add(new JLabel(text), GBC.std().insets(0,0,10,0));
 			p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
 		}
+		
 		@Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
-			String v = value.getText();
+			
+			// return if unchanged
+			String v = (value instanceof JComboBox) ? 
+				((JComboBox)value).getEditor().getItem().toString() : 
+				((JTextField)value).getText();
+
+			if (v.equals(originalValue) || (originalValue == null && v.length() == 0)) return;
+
 			if (delete_if_empty && v.length() == 0)
 				v = null;
@@ -80,17 +140,56 @@
 
 	public static class Check extends Item {
+
 		public String key;
 		public String text;
-		public boolean default_ = false;
-
-		private JCheckBox check = new JCheckBox();
-
-		@Override public void addToPanel(JPanel p) {
-			check.setSelected(default_);
-			check.setText(text);
+		public boolean default_ = false; // not used!
+
+		private QuadStateCheckBox check;
+		private QuadStateCheckBox.State initialState;
+		
+		@Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
+			
+			// find out if our key is already used in the selection.
+			Usage usage = determineBooleanUsage(sel, key);
+
+			String oneValue = null;
+			for (String s : usage.values) oneValue = s;
+			if (usage.values.size() < 2 && (oneValue == null || "true".equals(oneValue) || "false".equals(oneValue))) {
+				// all selected objects share the same value which is either true or false or unset, 
+				// we can display a standard check box.
+				initialState = "true".equals(oneValue) ? 
+							QuadStateCheckBox.State.SELECTED :
+							"false".equals(oneValue) ? 
+							QuadStateCheckBox.State.NOT_SELECTED :
+							QuadStateCheckBox.State.UNSET;
+				check = new QuadStateCheckBox(text, initialState, 
+						new QuadStateCheckBox.State[] { 
+						QuadStateCheckBox.State.NOT_SELECTED,
+						QuadStateCheckBox.State.SELECTED,
+						QuadStateCheckBox.State.UNSET });
+			} else {
+				// the objects have different values, or one or more objects have something
+				// else than true/false. we display a quad-state check box
+				// in "partial" state.
+				initialState = QuadStateCheckBox.State.PARTIAL;
+				check = new QuadStateCheckBox(text, QuadStateCheckBox.State.PARTIAL, 
+						new QuadStateCheckBox.State[] { 
+						QuadStateCheckBox.State.NOT_SELECTED,
+						QuadStateCheckBox.State.PARTIAL,
+						QuadStateCheckBox.State.SELECTED,
+						QuadStateCheckBox.State.UNSET });
+			}
 			p.add(check, GBC.eol().fill(GBC.HORIZONTAL));
 		}
+		
 		@Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
-			cmds.add(new ChangePropertyCommand(sel, key, check.isSelected() ? "true" : null));
+			// if the user hasn't changed anything, don't create a command.
+			if (check.getState() == initialState) return;
+			
+			// otherwise change things according to the selected value.
+			cmds.add(new ChangePropertyCommand(sel, key, 
+					check.getState() == QuadStateCheckBox.State.SELECTED ? "true" :
+					check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? "false" :
+					null));
 		}
 		@Override boolean requestFocusInWindow() {return check.requestFocusInWindow();}
@@ -98,4 +197,5 @@
 
 	public static class Combo extends Item {
+		
 		public String key;
 		public String text;
@@ -107,15 +207,51 @@
 
 		private JComboBox combo;
-
-		@Override public void addToPanel(JPanel p) {
-			combo = new JComboBox((display_values != null ? display_values : values).split(","));
+		private LinkedHashMap<String,String> lhm;
+		private Usage usage;
+		private String originalValue;
+		
+		@Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
+			
+			// find out if our key is already used in the selection.
+			usage = determineTextUsage(sel, key);
+			
+			String[] value_array = values.split(",");
+			String[] display_array = (display_values == null) ? value_array : display_values.split(",");
+
+			lhm = new LinkedHashMap<String,String>();
+			if (usage.values.size() > 1) {
+				lhm.put(DIFFERENT, DIFFERENT);
+			}
+			for (int i=0; i<value_array.length; i++) {
+				lhm.put(value_array[i], display_array[i]);
+			}
+			for (String s : usage.values) {
+				if (!lhm.containsKey(s)) lhm.put(s, s);
+			}
+			if ((default_ != null) && (!lhm.containsKey(default_))) lhm.put(default_, default_);
+			
+			combo = new JComboBox(lhm.values().toArray());
 			combo.setEditable(editable);
-			combo.setSelectedItem(default_);
+			if (usage.values.size() == 1) {
+				for (String s : usage.values) { combo.setSelectedItem(lhm.get(s)); originalValue=s; }
+			} else {
+				combo.setSelectedItem(DIFFERENT); originalValue=DIFFERENT;
+			}
 			p.add(new JLabel(text), GBC.std().insets(0,0,10,0));
 			p.add(combo, GBC.eol().fill(GBC.HORIZONTAL));
 		}
 		@Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
-			String v = combo.getSelectedIndex() == -1 ? null : values.split(",")[combo.getSelectedIndex()];
-			String str = combo.isEditable()?combo.getEditor().getItem().toString() : v;
+			Object display = combo.getSelectedItem();
+			String value = null;
+			if (display != null) 
+				for (String key : lhm.keySet()) { 
+					String k = lhm.get(key);
+					if (k != null && k.equals(display)) value=key; 
+				}
+			String str = combo.isEditable() ? combo.getEditor().getItem().toString() : value;
+			
+			// no change if same as before
+			if (str.equals(originalValue) || (originalValue == null && str.length() == 0)) return;
+			
 			if (delete_if_empty && str != null && str.length() == 0)
 				str = null;
@@ -128,5 +264,5 @@
 		public String text;
 
-		@Override public void addToPanel(JPanel p) {
+		@Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
 			p.add(new JLabel(text), GBC.eol());
 		}
@@ -138,5 +274,5 @@
 		public String value;
 
-		@Override public void addToPanel(JPanel p) {}
+		@Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) { }
 		@Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
 			cmds.add(new ChangePropertyCommand(sel, key, value != null && !value.equals("") ? value : null));
@@ -181,5 +317,5 @@
 
 	/**
-	 * Called from the XML parser to set the types, this preset affects
+	 * Called from the XML parser to set the types this preset affects
 	 */
 	public void setType(String types) throws SAXException {
@@ -262,10 +398,11 @@
 	}
 
-	public JPanel createPanel() {
+	public JPanel createPanel(Collection<OsmPrimitive> selected) {
 		if (data == null)
 			return null;
 		JPanel p = new JPanel(new GridBagLayout());
+
 		for (Item i : data)
-			i.addToPanel(p);
+			i.addToPanel(p, selected);
 		return p;
 	}
@@ -273,5 +410,5 @@
 	public void actionPerformed(ActionEvent e) {
 		Collection<OsmPrimitive> sel = Main.ds.getSelected();
-		JPanel p = createPanel();
+		JPanel p = createPanel(sel);
 		if (p == null)
 			return;
