[tiled-user] [Patch] Tile Instance Properties

Christian Henz chrhenz at gmx.de
Wed Jun 21 12:45:37 PDT 2006


On Wed, Jun 21, 2006 at 11:44:52AM +0200, Bjørn Lindeijer wrote:
> Hello Christian,
> 
> Yeah, sorry about not replying sooner. I planned to reply after
> releasing 0.6.0 final, but that release is still taking longer than
> expected.
> 
> Tile instance properties are indeed something that'd be nice to add.
> Unfortunately the way of storing these properties proposed by you
> depends on storing the layer data as pure XML, while many prefer the
> binary method because of its size.
> 
> A fork of Tiled made for the Stendhal project already supports tile
> instance properties. It stores the properties in a propertieslayer
> (which is present implicitly, and there is only one). As follows:
> 
> <propertieslayer>
>   <tile x="5" y="6">
>     <properties>
>       key=value
>     </properties>
>   </tile>
> </propertieslayer>
> 
> The way the keys and values are stored is not compatible with how
> Tiled stores properties, and also doesn't allow for multiline
> properties. But other than that I like this way of storing the
> properties. The only problem would be that it doesn't allow for a
> different set of properties on each layer. Do you think this is
> important?
> 

Yes I do. I think I'm not the only one currently implementing objects
with a tile layer, and I would like to have properties for the object
layer as well as the background layer.

> If so we could also simply add a tileproperties child to the layer
> element, so that we can store these properties per-layer.
> 

I have changed my patch to do that (attached). The output looks like this now:

<layer>
  <data>
    [...]
  </data>
  <tileproperties>
    <tile x="5" y="6">
      <properties>
        <property name="..." value="..."</property>
        [...]
      </properties>
    <tile>
    [...]
  </tileproperties>
</layer>


Regarding the implementation, one issue is proper notification
when marqueeSelection has changed. There should be some kind of 
callback mechanism whenever the selection has changed. The properties editor 
could then subscribe to the notifications.

This would also mean that marqueeSelection should be permament (ie doing something like 
"marqueeSelection.clear();" instead of "marqueeSelection = null;"  
and test for "!marqueeSelection.isEmpty()" instead of "marqueeSelection != null").

Something like "Observable" might to do the job, but I'm not that familar with Java,
there might be better ways to do it :-)

As for the GUI, my approach is making the TIPDialog behave exactly like TilePalette, ie
with a floating window and a button to invoke it.

cheers,
Christian
-------------- next part --------------
Index: src/tiled/mapeditor/dialogs/TileInstancePropertiesDialog.java
===================================================================
--- src/tiled/mapeditor/dialogs/TileInstancePropertiesDialog.java	(Revision 0)
+++ src/tiled/mapeditor/dialogs/TileInstancePropertiesDialog.java	(Revision 0)
@@ -0,0 +1,254 @@
+/*
+ *  Tiled Map Editor, (c) 2004-2006
+ *
+ *  This program 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 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Adam Turk <aturk at biggeruniverse.com>
+ *  Bjorn Lindeijer <b.lindeijer at xs4all.nl>
+ */
+
+package tiled.mapeditor.dialogs;
+
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.Rectangle;
+
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.List;
+import java.util.LinkedList;
+import javax.swing.*;
+
+import tiled.mapeditor.Resources;
+import tiled.mapeditor.util.PropertiesTableModel;
+import tiled.mapeditor.widget.VerticalStaticJPanel;
+import tiled.mapeditor.selection.SelectionLayer;
+import tiled.core.MapLayer;
+import tiled.core.TileLayer;
+import tiled.core.Tile;
+import tiled.mapeditor.MapEditor;
+
+/**
+ * @version $Id:$
+ */
+public class TileInstancePropertiesDialog extends JDialog
+{
+    private JTable tProperties;
+    private Properties properties = new Properties();
+    private PropertiesTableModel tableModel = new PropertiesTableModel();
+
+    private static final String DIALOG_TITLE = "Tile Properties"; // Resource this
+    private static final String APPLY_BUTTON = "Apply"; // Resource this
+    private static final String APPLY_TOOLTIP = "Apply properties to selected tiles"; // Resource this
+    private static final String DELETE_BUTTON = Resources.getString("general.button.delete");
+
+    private MapEditor editor = null;
+    private LinkedList propertiesList = new LinkedList(); // Holds all currently selected Properties
+
+    public TileInstancePropertiesDialog(MapEditor editor) {
+
+        super((JFrame)null, DIALOG_TITLE, false);
+        this.editor = editor;
+        init();
+        pack();
+        setLocationRelativeTo(getOwner());
+    }
+
+    private void init() {
+        tProperties = new JTable(tableModel);
+        JScrollPane propScrollPane = new JScrollPane(tProperties);
+        propScrollPane.setPreferredSize(new Dimension(200, 150));
+
+        JButton applyButton = new JButton(APPLY_BUTTON);
+        applyButton.setToolTipText(APPLY_TOOLTIP);
+        JButton deleteButton = new JButton(Resources.getIcon("gnome-delete.png"));
+        deleteButton.setToolTipText(DELETE_BUTTON);
+
+        JPanel user = new VerticalStaticJPanel();
+        user.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
+        user.setLayout(new BoxLayout(user, BoxLayout.X_AXIS));
+        user.add(Box.createGlue());
+        user.add(Box.createRigidArea(new Dimension(5, 0)));
+        user.add(deleteButton);
+
+        JPanel buttons = new VerticalStaticJPanel();
+        buttons.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
+        buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
+        buttons.add(Box.createGlue());
+        buttons.add(applyButton);
+        buttons.add(Box.createRigidArea(new Dimension(5, 0)));
+
+        JPanel mainPanel = new JPanel();
+        mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+        mainPanel.add(propScrollPane);
+        mainPanel.add(user);
+        mainPanel.add(buttons);
+
+        getContentPane().add(mainPanel);
+        getRootPane().setDefaultButton(applyButton);
+
+        //create actionlisteners
+        applyButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent actionEvent) {
+            	buildPropertiesAndApply();
+            }
+        });
+
+        deleteButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                deleteSelected();
+            }
+        });
+    }
+
+    public void setSelection( SelectionLayer selection ) {
+
+	// Start off fresh...
+	properties.clear();
+	propertiesList.clear();
+
+	// Get all properties of all selected tiles...
+	MapLayer ml = editor.getCurrentLayer();
+	if( ml instanceof TileLayer ) { 
+
+	    TileLayer tl = (TileLayer)ml;
+	    Rectangle r = selection.getSelectedAreaBounds();   
+	    int maxJ = (int)(r.getY() + r.getHeight());
+	    int maxI = (int)(r.getX() + r.getWidth());
+	    
+	    for( int j = (int)r.getY(); j < maxJ; j++ ) {
+		for( int i = (int)r.getX(); i < maxI; i++ ) {
+		    
+		    Tile t = selection.getTileAt( i, j );
+		    if ( t != null ) {
+
+			Properties p = tl.getTileInstancePropertiesAt( i, j );
+			if ( p != null ) propertiesList.add( p );
+		    }
+		}
+	    }
+	}
+	
+	if( propertiesList.size() > 0 ) {
+
+	    // Start with properties of first tile instance
+	    Properties p = (Properties)propertiesList.get( 0 );
+			    
+	    for( Enumeration e = p.keys(); e.hasMoreElements(); ) {
+
+		String key = (String)e.nextElement();
+		properties.put( key, p.getProperty( key ) );
+	    }
+
+	    for( int i = 1; i < propertiesList.size(); i++ ) {
+
+		// Merge the other properties...
+		p = (Properties)propertiesList.get( i );
+
+		for( Enumeration e = properties.keys(); e.hasMoreElements(); ) {
+
+		    // We only care for properties that are already "known"...
+
+		    String key = (String)e.nextElement();
+		    String val = properties.getProperty( key );
+		    String mval = p.getProperty( key );
+
+		    if( mval == null) {
+
+			properties.remove( key ); // Drop non-common properties
+
+		    } else if ( !mval.equals( val ) ) {
+
+			properties.setProperty( key, "?" ); // Hide non-common values
+		    }
+		}
+	    }
+	}
+
+	updateInfo(); // Refresh display
+    }
+
+    private void updateInfo() {
+        // Make a copy of the properties that will be changed by the
+        // properties table model.
+        Properties props = new Properties();
+        Enumeration keys = properties.keys();
+        while (keys.hasMoreElements()) {
+            String key = (String)keys.nextElement();
+            props.put(key, properties.getProperty(key));
+        }
+        tableModel.update(props);
+    }
+
+    public void getProps() {
+        updateInfo();
+        setVisible(true);
+    }
+
+    private void buildPropertiesAndApply() {
+        // Copy over the new set of properties from the properties table
+        // model.
+
+        properties.clear();
+
+        Properties newProps = tableModel.getProperties();
+        Enumeration keys = newProps.keys();
+        while (keys.hasMoreElements()) {
+            String key = (String)keys.nextElement();
+            properties.put(key, newProps.getProperty(key));
+        }
+
+	applyPropertiesToTiles();
+    }
+
+
+    private void deleteFromSelectedTiles( String key ) {
+	
+	for( int i = 0; i < propertiesList.size(); i++ ) {
+
+	    Properties p = ( Properties )propertiesList.get( i );
+	    p.remove( key );
+	}
+    }
+
+    private void deleteSelected() {
+        int total = tProperties.getSelectedRowCount();
+        Object[] keys = new Object[total];
+        int[] selRows = tProperties.getSelectedRows();
+
+        for(int i = 0; i < total; i++) {
+            keys[i] = tProperties.getValueAt(selRows[i], 0);
+        }
+
+        for (int i = 0; i < total; i++) {
+            if (keys[i] != null) {
+
+                tableModel.remove(keys[i]);
+                deleteFromSelectedTiles( ( String )keys[ i ] );
+            }  
+        }
+    }
+
+    private void applyPropertiesToTiles() {
+
+	for( int i = 0; i < propertiesList.size(); i++) {
+
+	    Properties tp = (Properties)propertiesList.get( i );
+
+	    for( Enumeration e = properties.keys();
+		 e.hasMoreElements(); ) {
+
+		String key = (String)e.nextElement();
+		String val = properties.getProperty( key );
+		if ( !val.equals( "?" ) ) 
+		    tp.setProperty( key, val );
+	    }
+	}
+    }
+
+}
Index: src/tiled/mapeditor/MapEditor.java
===================================================================
--- src/tiled/mapeditor/MapEditor.java	(Revision 668)
+++ src/tiled/mapeditor/MapEditor.java	(Arbeitskopie)
@@ -124,6 +124,8 @@
     private AboutDialog aboutDialog;
     private MapLayerEdit paintEdit;
 
+    private TileInstancePropertiesDialog tileInstancePropertiesDialog;
+
     /** Available brushes */
     private Vector brushes = new Vector();
     private Brush eraserBrush;
@@ -224,6 +226,10 @@
 
         appFrame.setVisible(true);
 
+        tileInstancePropertiesDialog = 
+            new TileInstancePropertiesDialog( this );
+        tileInstancePropertiesDialog.setVisible( true );
+
         // Restore the state of the main frame. This needs to happen after
         // making the frame visible, otherwise it has no effect (in Linux).
         Preferences mainDialogPrefs = prefs.node("dialog/main");
@@ -946,6 +952,8 @@
         Point limp = mouseInitialPressLocation;
 
        if (currentPointerState == PS_MARQUEE) {
+           // Uncommented to allow single tile selections
+           /*
            Point tile = mapView.screenToTileCoords(event.getX(), event.getY());
            if (tile.y - limp.y == 0 && tile.x - limp.x == 0) {
                if (marqueeSelection != null) {
@@ -953,6 +961,11 @@
                    marqueeSelection = null;
                }
            }
+           */
+
+           // There should be a proper notification mechanism for this...
+           tileInstancePropertiesDialog.setSelection( marqueeSelection );
+
         } else if (currentPointerState == PS_MOVE) {
             if (layer != null && moveDist.x != 0 || moveDist.x != 0) {
                 undoSupport.postEdit(new MoveLayerEdit(layer, moveDist));
Index: src/tiled/io/xml/XMLMapTransformer.java
===================================================================
--- src/tiled/io/xml/XMLMapTransformer.java	(Revision 668)
+++ src/tiled/io/xml/XMLMapTransformer.java	(Arbeitskopie)
@@ -593,7 +593,25 @@
                         }
                     }
                 }
-            }
+
+            } else if ( "tileproperties".equalsIgnoreCase( child.getNodeName() ) ) {
+
+	        for ( Node tpn = child.getFirstChild();
+		      tpn != null;
+		      tpn = tpn.getNextSibling() ) {
+
+		    if ( "tile".equalsIgnoreCase( tpn.getNodeName() ) ) {
+
+	      	        int x = getAttribute( tpn, "x", -1 );
+			int y = getAttribute( tpn, "y", -1 );
+			
+			Properties tip = new Properties();
+			
+			readProperties( tpn.getChildNodes(), tip );
+			ml.setTileInstancePropertiesAt( x, y, tip );
+		    }
+		}
+	    }
         }
 
         // Invisible layers are automatically locked, so it is important to
Index: src/tiled/io/xml/XMLMapWriter.java
===================================================================
--- src/tiled/io/xml/XMLMapWriter.java	(Revision 668)
+++ src/tiled/io/xml/XMLMapWriter.java	(Arbeitskopie)
@@ -421,6 +421,28 @@
                 }
             }
             w.endElement();
+
+            w.startElement( "tileproperties" );
+
+            for ( int y = 0; y < l.getHeight(); y++ ) {
+                for ( int x = 0; x < l.getWidth(); x++ ) {
+
+		    Properties tip = ((TileLayer)l).getTileInstancePropertiesAt( x, y );
+		    if ( tip != null && tip.size() > 0 ) { 
+
+		        w.startElement( "tile" );
+
+			w.writeAttribute( "x", x );
+			w.writeAttribute( "y", y );
+
+			writeProperties( tip, w );
+
+		        w.endElement();
+		    }
+	        }
+	    }
+    
+            w.endElement();
         }
         w.endElement();
     }
Index: src/tiled/core/TileLayer.java
===================================================================
--- src/tiled/core/TileLayer.java	(Revision 668)
+++ src/tiled/core/TileLayer.java	(Arbeitskopie)
@@ -15,6 +15,7 @@
 import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.geom.Area;
+import java.util.Properties;
 
 /**
  * A TileLayer is a specialized MapLayer, used for tracking two dimensional
@@ -25,7 +26,29 @@
 public class TileLayer extends MapLayer
 {
     protected Tile[][] map;
+    protected Properties[][] tileInstanceProperties;
 
+    public Properties getTileInstancePropertiesAt( int x, int y ) {
+
+        try { 
+
+            return tileInstanceProperties[ y - bounds.y ][ x - bounds.x ];
+
+        } catch (ArrayIndexOutOfBoundsException e) {
+
+            return null;
+        }
+    }
+
+    public void setTileInstancePropertiesAt( int x, int y, Properties tip ) {
+
+	try {
+	    
+	    tileInstanceProperties[ y - bounds.y ][ x - bounds.x ] = tip;
+	    
+	} catch (ArrayIndexOutOfBoundsException e) {}
+    }
+
     /**
      * Default contructor
      */
@@ -60,9 +83,18 @@
         super(ml);
 
         map = new Tile[bounds.height][];
+        tileInstanceProperties = new Properties[ bounds.height ][];
+
         for (int y = 0; y < bounds.height; y++) {
             map[y] = new Tile[bounds.width];
             System.arraycopy(ml.map[y], 0, map[y], 0, bounds.width);
+
+            tileInstanceProperties[ y ] = new Properties[ bounds.width ];
+            for( int x = 0; x < bounds.width; x++) {
+
+                if( map[y][x] != null ) tileInstanceProperties[ y ][ x ] = new Properties();
+                else tileInstanceProperties[ y ][ x ] = null;
+            }
         }
     }
 
@@ -198,6 +230,7 @@
     public void setBounds(Rectangle bounds) {
         super.setBounds(bounds);
         map = new Tile[bounds.height][bounds.width];
+        tileInstanceProperties = new Properties[bounds.height][bounds.width];
     }
 
     /**
@@ -273,6 +306,11 @@
         try {
             if (canEdit()) {
                 map[ty - bounds.y][tx - bounds.x] = ti;
+
+            if( ti != null ) 
+                tileInstanceProperties[ ty - bounds.y][tx - bounds.x] = new Properties();
+            else  
+                tileInstanceProperties[ ty - bounds.y][tx - bounds.x] = null;
             }
         } catch (ArrayIndexOutOfBoundsException e) {
             // Silently ignore out of bounds exception
@@ -427,9 +465,20 @@
 
         // Clone the layer data
         clone.map = new Tile[map.length][];
+        clone.tileInstanceProperties = new Properties[map.length][];
+
         for (int i = 0; i < map.length; i++) {
             clone.map[i] = new Tile[map[i].length];
             System.arraycopy(map[i], 0, clone.map[i], 0, map[i].length);
+
+            clone.tileInstanceProperties[ i ] = new Properties[ map[i].length ];
+            for( int j = 0; j < map[i].length; j++ ) {
+
+                if( map[i][j] != null ) 
+                    clone.tileInstanceProperties[ i ][ j ] = new Properties();
+                else 
+                    clone.tileInstanceProperties[ i ][ j ] = null; 
+            }
         }
 
         return clone;
@@ -448,6 +497,7 @@
             return;
 
         Tile[][] newMap = new Tile[height][width];
+        Properties[][] newTileInstanceProperties = new Properties[height][width];
 
         int maxX = Math.min(width, bounds.width + dx);
         int maxY = Math.min(height, bounds.height + dy);
@@ -455,6 +505,7 @@
         for (int x = Math.max(0, dx); x < maxX; x++) {
             for (int y = Math.max(0, dy); y < maxY; y++) {
                 newMap[y][x] = getTileAt(x - dx, y - dy);
+                newTileInstanceProperties[y][x] = getTileInstancePropertiesAt( x - dx, y - dy );
             }
         }
 


More information about the tiled-user mailing list