001/*
002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games.
003 * Copyright (C) 2000-2011 The Gridarta Developers.
004 *
005 * This program is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 2 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License along
016 * with this program; if not, write to the Free Software Foundation, Inc.,
017 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
018 */
019
020package net.sf.gridarta.gui.map.maptilepane;
021
022import java.awt.Component;
023import java.awt.GridBagConstraints;
024import java.awt.GridBagLayout;
025import java.awt.Insets;
026import java.awt.event.ActionEvent;
027import java.awt.event.ActionListener;
028import java.io.File;
029import java.io.IOException;
030import javax.swing.AbstractButton;
031import javax.swing.Action;
032import javax.swing.JButton;
033import javax.swing.JFileChooser;
034import javax.swing.JPanel;
035import javax.swing.JTextField;
036import javax.swing.filechooser.FileFilter;
037import net.sf.gridarta.model.io.PathManager;
038import net.sf.gridarta.utils.ActionBuilderUtils;
039import net.sf.gridarta.utils.FileChooserUtils;
040import net.sf.japi.swing.action.ActionBuilder;
041import net.sf.japi.swing.action.ActionBuilderFactory;
042import net.sf.japi.swing.action.ActionMethod;
043import org.jetbrains.annotations.NotNull;
044import org.jetbrains.annotations.Nullable;
045
046/**
047 * A tile panel displays exactly one direction for map tiling. It's basically an
048 * extended text field with some additional buttons to make usage easier for
049 * users.
050 * @author unknown
051 */
052public class TilePanel extends JPanel {
053
054    /**
055     * The serial version UID.
056     */
057    private static final long serialVersionUID = 1L;
058
059    /**
060     * The {@link ActionBuilder}.
061     */
062    @NotNull
063    private static final ActionBuilder ACTION_BUILDER = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta");
064
065    /**
066     * The {@link FileFilter} to use.
067     */
068    @NotNull
069    private final FileFilter fileFilter;
070
071    /**
072     * The original value.
073     */
074    @NotNull
075    private final String original;
076
077    /**
078     * The context reference for relative paths.
079     */
080    @Nullable
081    private final File relativeReference;
082
083    /**
084     * The context reference for absolute paths.
085     */
086    @NotNull
087    private final File absoluteReference;
088
089    /**
090     * The {@link JTextField} with the text.
091     */
092    @NotNull
093    private final JTextField textField;
094
095    /**
096     * The {@link TilePanel.RASwitch}.
097     */
098    @NotNull
099    private final RASwitch raSwitch;
100
101    /**
102     * Creates a new instance.
103     * @param fileFilter the file filter to use
104     * @param original the original value, used as initial value and for
105     * restoring it
106     * @param relativeReference file to reference for relative paths (context
107     * file or directory, not parent directory)
108     * @param absoluteReference file to reference for absolute paths (context
109     * file or directory, not parent directory)
110     */
111    public TilePanel(@NotNull final FileFilter fileFilter, @NotNull final String original, @Nullable final File relativeReference, @NotNull final File absoluteReference) {
112        super(new GridBagLayout());
113        this.fileFilter = fileFilter;
114        this.original = original;
115        this.relativeReference = relativeReference == null ? null : getAbsolutePath(relativeReference);
116        this.absoluteReference = getAbsolutePath(absoluteReference);
117        final GridBagConstraints gbc = new GridBagConstraints();
118        textField = new JTextField();
119        textField.setColumns(16);
120
121        gbc.fill = GridBagConstraints.NONE;
122        add(iconButton(ACTION_BUILDER.createAction(false, "mapTileRevert", this)), gbc);
123        add(iconButton(ACTION_BUILDER.createAction(false, "mapTileClear", this)), gbc);
124        gbc.fill = GridBagConstraints.HORIZONTAL;
125        gbc.weightx = 1.0;
126        add(textField, gbc);
127        gbc.weightx = 0.0;
128        gbc.fill = GridBagConstraints.NONE;
129        raSwitch = new RASwitch();
130        add(raSwitch, gbc);
131        add(iconButton(ACTION_BUILDER.createAction(false, "mapTileChoose", this)), gbc);
132
133        textField.setText(original);
134        raSwitch.updateRAState();
135    }
136
137    /**
138     * Returns the absolute path of a {@link File}.
139     * @param file the file
140     * @return the absolute path
141     */
142    @NotNull
143    private static File getAbsolutePath(@NotNull final File file) {
144        try {
145            return file.getCanonicalFile();
146        } catch (final IOException ignored) {
147            return file.getAbsoluteFile();
148        }
149    }
150
151    /**
152     * Creates a new button for reverting a path entry.
153     * @param action the action for the button
154     * @return the button
155     */
156    @NotNull
157    private static Component iconButton(@NotNull final Action action) {
158        final AbstractButton button = new JButton(action);
159        button.setMargin(new Insets(0, 0, 0, 0));
160        return button;
161    }
162
163    /**
164     * Action method for reverting to stored path.
165     */
166    @ActionMethod
167    public void mapTileRevert() {
168        setText(original, false);
169    }
170
171    /**
172     * Action method for deleting the path.
173     */
174    @ActionMethod
175    public void mapTileClear() {
176        setText("", true);
177    }
178
179    /**
180     * Action method for choosing the path.
181     */
182    @ActionMethod
183    public void mapTileChoose() {
184        final File tmpRelativeReference = relativeReference;
185        final JFileChooser chooser = tmpRelativeReference == null ? new JFileChooser() : new JFileChooser(tmpRelativeReference.getParentFile());
186        final String oldFilename = getText();
187        if (!oldFilename.isEmpty()) {
188            // Point the chooser on the current path tile file
189            final File oldFile;
190            if (tmpRelativeReference == null || oldFilename.startsWith("/")) {
191                oldFile = new File(absoluteReference, oldFilename.substring(1));
192            } else {
193                oldFile = new File(tmpRelativeReference.getParentFile(), oldFilename);
194            }
195            FileChooserUtils.setCurrentDirectory(chooser, oldFile.getParentFile());
196            chooser.setSelectedFile(oldFile);
197        }
198        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
199        chooser.setMultiSelectionEnabled(false);
200        chooser.setFileFilter(fileFilter);
201        FileChooserUtils.sanitizeCurrentDirectory(chooser);
202        final int returnVal = chooser.showOpenDialog(this);
203        if (returnVal == JFileChooser.APPROVE_OPTION) {
204            try {
205                final File selected = chooser.getSelectedFile();
206                final String relPath = selected.getCanonicalPath().substring(absoluteReference.getCanonicalPath().length()).replace('\\', '/');
207                setText(relPath, true);
208            } catch (final IOException ex) {
209                setText("Error: " + ex, true);
210            }
211        }
212    }
213
214    /**
215     * Sets the text.
216     * @param text the ext to set
217     * @param keepRA if set, modify <code>text</code> to match the current RA
218     * state
219     */
220    public void setText(@NotNull final String text, final boolean keepRA) {
221        textField.setText(text);
222        if (keepRA) {
223            raSwitch.actionPerformed(false);
224        } else {
225            raSwitch.updateRAState();
226        }
227    }
228
229    /**
230     * Returns the text.
231     * @return the text
232     */
233    @NotNull
234    public String getText() {
235        final String text = textField.getText();
236        assert text != null;
237        return text;
238    }
239
240    /**
241     * Activates the text input field.
242     */
243    public void activateTextField() {
244        textField.requestFocusInWindow();
245    }
246
247    /**
248     * Adds an {@link ActionListener} to the text input field.
249     * @param actionListener the action listener
250     */
251    public void addTextFieldActionListener(@NotNull final ActionListener actionListener) {
252        textField.addActionListener(actionListener);
253    }
254
255    /**
256     * Updates the state of the {@link #raSwitch}.
257     */
258    public void updateRAState() {
259        raSwitch.updateRAState();
260    }
261
262    /**
263     * A JButton that converts relative to absolute paths and vice versa.
264     */
265    private class RASwitch extends JButton implements ActionListener {
266
267        /**
268         * The serial version UID.
269         */
270        private static final long serialVersionUID = 1L;
271
272        /**
273         * Whether the current mode is relative.
274         */
275        private boolean isRelative;
276
277        /**
278         * Creates a new instance.
279         */
280        private RASwitch() {
281            setMargin(new Insets(0, 0, 0, 0));
282            setToolTipText(ActionBuilderUtils.getString(ACTION_BUILDER, "mapTilePathMode.shortdescription"));
283            updateText();
284            addActionListener(this);
285            setPreferredSize(getMinimumSize());
286        }
287
288        @Override
289        public void actionPerformed(@NotNull final ActionEvent e) {
290            actionPerformed(true);
291        }
292
293        public void actionPerformed(final boolean toggleRelative) {
294            final String path = TilePanel.this.getText();
295            final File tmpRelativeReference = relativeReference;
296            if (path.isEmpty() || tmpRelativeReference == null) {
297                return;
298            }
299            if (toggleRelative) {
300                isRelative = !isRelative;
301                updateText();
302            }
303            final String relRef = new StringBuilder().append('/').append(PathManager.absoluteToRelative(absoluteReference.getPath() + '/', tmpRelativeReference.getPath())).toString();
304            if (isRelative) {
305                textField.setText(PathManager.absoluteToRelative(relRef, path));
306            } else {
307                textField.setText(PathManager.relativeToAbsolute(relRef, path));
308            }
309        }
310
311        /**
312         * Updates the state of the button.
313         */
314        public void updateRAState() {
315            final String path = getText();
316            isRelative = PathManager.isRelative(path);
317            updateText();
318        }
319
320        /**
321         * Updates the button text to reflect the current state.
322         */
323        private void updateText() {
324            setText(isRelative ? "R" : "A");
325        }
326
327    }
328
329}