All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fr.vergne.translation.editor.MapListPanel Maven / Gradle / Ivy

There is a newer version: 2.0
Show newest version
package fr.vergne.translation.editor;

import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.border.EtchedBorder;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import fr.vergne.translation.TranslationEntry;
import fr.vergne.translation.TranslationMap;
import fr.vergne.translation.TranslationProject;
import fr.vergne.translation.editor.ListModel.MapsChangedListener;
import fr.vergne.translation.editor.content.MapCellRenderer;
import fr.vergne.translation.editor.content.MapTreeNode;
import fr.vergne.translation.impl.EmptyProject;
import fr.vergne.translation.impl.TranslationUtil;
import fr.vergne.translation.util.Feature;
import fr.vergne.translation.util.MapInformer;
import fr.vergne.translation.util.MapInformer.MapSummaryListener;
import fr.vergne.translation.util.MapInformer.NoDataException;
import fr.vergne.translation.util.MapNamer;
import fr.vergne.translation.util.ProjectLoader;

@SuppressWarnings("serial")
public class MapListPanel, TMap extends TranslationMap, MapID, TProject extends TranslationProject>
		extends JPanel {

	private static final String CONFIG_CLEARED_DISPLAYED = "clearedDisplayed";
	private static final String CONFIG_LABELS_DISPLAYED = "labelsDisplayed";
	private static final String CONFIG_LIST_ORDER = "listOrder";
	private static final String CONFIG_MAP_DIR = "mapDir";
	public static final Logger logger = Logger.getLogger(MapListPanel.class
			.getName());
	private final JTextField folderPathField = new JTextField();
	private final JTree tree;
	private final Map> namers = new HashMap>();
	private final Map mapSummaries = Collections
			.synchronizedMap(new HashMap());
	private final Collection> mapSummaryListeners = new HashSet>();
	private final TreeSet currentIDs = new TreeSet(
			new Comparator() {

				@Override
				public int compare(MapID id1, MapID id2) {
					return id1.toString().compareToIgnoreCase(id2.toString());
				}
			});
	private final ProjectLoader projectLoader;
	private TranslationProject currentProject = new EmptyProject<>();
	private final Collection listeners = new HashSet();
	private final JPanel featureRow = new JPanel(new FlowLayout());

	public MapListPanel(ProjectLoader projectLoader) {
		this.projectLoader = projectLoader;

		final MapInformer mapInformer = new MapInformer() {

			@Override
			public int getEntriesCount(MapID id) throws NoDataException {
				MapSummary mapSummary = mapSummaries.get(id);
				if (mapSummary == null) {
					throw new NoDataException();
				} else {
					return mapSummary.total;
				}
			}

			@Override
			public int getEntriesRemaining(MapID id) throws NoDataException {
				MapSummary mapSummary = mapSummaries.get(id);
				if (mapSummary == null) {
					throw new NoDataException();
				} else {
					return mapSummary.remaining;
				}
			}

			@Override
			public boolean isModified(MapID id) throws NoDataException {
				MapSummary mapSummary = mapSummaries.get(id);
				if (mapSummary == null) {
					throw new NoDataException();
				} else {
					return mapSummary.isModified;
				}
			}

			@Override
			public void addMapSummaryListener(MapSummaryListener listener) {
				mapSummaryListeners.add(listener);
			}

			@Override
			public void removeMapSummaryListener(
					MapSummaryListener listener) {
				mapSummaryListeners.remove(listener);
			}
		};

		final MapNamer labelNamer = new MapNamer() {

			@Override
			public String getNameFor(MapID id) {
				String label = currentProject.getMapName(id);
				if (label == null) {
					return "[" + id + "]";
				} else {
					return label;
				}
			}
		};
		namers.put(Order.LABEL, labelNamer);
		final MapNamer idNamer = new MapNamer() {

			@Override
			public String getNameFor(MapID id) {
				return id.toString();
			}
		};
		namers.put(Order.ID, idNamer);

		setBorder(new EtchedBorder());

		setLayout(new GridBagLayout());
		GridBagConstraints constraints = new GridBagConstraints();

		constraints.gridx = 0;
		constraints.gridy = 0;
		constraints.fill = GridBagConstraints.HORIZONTAL;
		constraints.weightx = 1;
		add(buildFileChooserPanel(), constraints);

		constraints.gridy++;
		constraints.fill = GridBagConstraints.BOTH;
		constraints.weighty = 1;
		tree = buildTreeComponent(mapInformer, idNamer, labelNamer);
		add(new JScrollPane(tree), constraints);

		constraints.gridy++;
		constraints.fill = GridBagConstraints.NONE;
		constraints.weighty = 0;
		JPanel options = buildQuickOptions(labelNamer, idNamer);
		add(options, constraints);

		configureBackgroundSummarizing();
		String projectPath = Editor.config.getProperty(CONFIG_MAP_DIR, null);
		if (projectPath == null) {
			// nothing to load
		} else {
			loadProjectFrom(new File(projectPath));
		}
	}

	@SuppressWarnings("unchecked")
	private JPanel buildQuickOptions(final MapNamer labelNamer,
			final MapNamer idNamer) {
		JPanel buttons = new JPanel(new GridLayout(0, 1));
		JPanel options = new JPanel(new FlowLayout());
		buttons.add(options);
		buttons.add(featureRow);

		final JCheckBox displayCleared = new JCheckBox();
		displayCleared.setAction(new AbstractAction("Cleared") {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				boolean selected = displayCleared.isSelected();
				Editor.config.setProperty(CONFIG_CLEARED_DISPLAYED, ""
						+ selected);
				((ListModel) tree.getModel())
						.setClearedDisplayed(selected);
			}
		});
		displayCleared.setSelected(((ListModel) tree.getModel())
				.isClearedDisplayed());
		displayCleared.setToolTipText("Display cleared maps.");
		options.add(displayCleared);

		final JCheckBox displayLabels = new JCheckBox();
		displayLabels.setAction(new AbstractAction("Labels") {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				boolean selected = displayLabels.isSelected();
				Editor.config.setProperty(CONFIG_LABELS_DISPLAYED, ""
						+ selected);
				((MapCellRenderer) tree.getCellRenderer())
						.setMapNamer(selected ? labelNamer : idNamer);

				((ListModel) tree.getModel()).requestUpdate();
			}
		});
		displayLabels.setSelected(Boolean.parseBoolean(Editor.config
				.getProperty(CONFIG_LABELS_DISPLAYED, "false")));
		displayLabels.setToolTipText("Display maps' English labels.");
		options.add(displayLabels);

		final JComboBox sortingChoice = new JComboBox<>(Order.values());
		sortingChoice.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				Order order = (Order) sortingChoice.getSelectedItem();
				Editor.config.setProperty(CONFIG_LIST_ORDER, "" + order);
				((ListModel) tree.getModel()).setOrderNamer(namers
						.get(order));
			}
		});
		try {
			sortingChoice.setSelectedItem(Order.valueOf(Editor.config
					.getProperty(CONFIG_LIST_ORDER, Order.ID.toString())));
		} catch (IllegalArgumentException e) {
			sortingChoice.setSelectedItem(Order.ID);
		}
		sortingChoice.setToolTipText("Choose map sorting order.");
		options.add(new JLabel("Sort: "));
		options.add(sortingChoice);

		return buttons;
	}

	@SuppressWarnings("unchecked")
	private void loadProjectFrom(File directory) {
		synchronized (mapSummaries) {
			currentProject = projectLoader.load(directory);

			featureRow.removeAll();
			for (final Feature feature : currentProject.getFeatures()) {
				JButton featureButton = new JButton();
				featureButton.setAction(new AbstractAction(feature.getName()) {

					@Override
					public void actionPerformed(ActionEvent arg0) {
						feature.run();
					}
				});
				featureButton.setToolTipText(feature.getDescription());
				featureRow.add(featureButton);
			}

			Collection newIDs = new LinkedList<>();
			for (MapID id : currentProject) {
				newIDs.add(id);
			}

			if (currentIDs.containsAll(newIDs)
					&& newIDs.containsAll(currentIDs)) {
				// same IDs, don't change
			} else {
				Collection removed = new LinkedList(currentIDs);
				removed.removeAll(newIDs);
				for (MapID id : removed) {
					mapSummaries.remove(id);
				}
				currentIDs.clear();
				currentIDs.addAll(newIDs);

				Editor.config.setProperty(CONFIG_MAP_DIR, directory.toString());
				folderPathField.setText(directory.toString());
				((ListModel) tree.getModel()).setMaps(currentIDs);
			}
		}
	}

	private JPanel buildFileChooserPanel() {
		folderPathField.setEditable(false);
		folderPathField.setText("Map folder...");

		JButton openButton = new JButton(new AbstractAction("Browse") {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				String path = folderPathField.getText();
				JFileChooser fileChooser = new JFileChooser(new File(path
						.isEmpty() ? "." : path));
				fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
				fileChooser.setFileHidingEnabled(true);
				fileChooser.setMultiSelectionEnabled(false);
				int answer = fileChooser.showDialog(MapListPanel.this, "Open");
				if (answer == JFileChooser.APPROVE_OPTION) {
					loadProjectFrom(fileChooser.getSelectedFile());
				} else {
					// do not consider it
				}
			}
		});
		openButton
				.setToolTipText("Select the folder of the translation project.");

		JPanel panel = new JPanel();
		panel.setLayout(new GridBagLayout());
		GridBagConstraints constraints = new GridBagConstraints();
		constraints.gridx = 1;
		panel.add(openButton, constraints);
		constraints.gridx = 0;
		constraints.fill = GridBagConstraints.HORIZONTAL;
		constraints.weightx = 1;
		panel.add(folderPathField, constraints);
		return panel;
	}

	private JTree buildTreeComponent(MapInformer mapInformer,
			final MapNamer idNamer, final MapNamer labelNamer) {
		final ListModel listModel = new ListModel(mapInformer,
				namers.values());
		listModel.setClearedDisplayed(Boolean.parseBoolean(Editor.config
				.getProperty(CONFIG_CLEARED_DISPLAYED, "true")));
		Order order;
		try {
			order = Order.valueOf(Editor.config.getProperty(CONFIG_LIST_ORDER,
					Order.ID.toString()));
		} catch (IllegalArgumentException e) {
			order = Order.ID;
		}
		listModel.setOrderNamer(namers.get(order));
		final JTree tree = new JTree(listModel);

		MapCellRenderer cellRenderer = new MapCellRenderer(
				tree.getCellRenderer(), mapInformer);
		boolean isLabelDisplayed = Boolean.parseBoolean(Editor.config
				.getProperty(CONFIG_LABELS_DISPLAYED, "false"));
		cellRenderer.setMapNamer(isLabelDisplayed ? labelNamer : idNamer);
		tree.setCellRenderer(cellRenderer);

		tree.setRootVisible(false);
		tree.getSelectionModel().setSelectionMode(
				TreeSelectionModel.SINGLE_TREE_SELECTION);
		final TreePath[] selection = new TreePath[1];
		tree.addTreeSelectionListener(new TreeSelectionListener() {

			@Override
			public void valueChanged(TreeSelectionEvent event) {
				TreePath[] paths = event.getPaths();
				for (TreePath path : paths) {
					if (event.isAddedPath(path)) {
						selection[0] = path;
					} else {
						// do not change
					}
				}
			}
		});
		listModel.addTreeModelListener(new TreeModelListener() {

			private void recoverSelection() {
				/*
				 * Invoke the recovery later in order to ensure it is done after
				 * any other action on the tree, otherwise the selection could
				 * be lost due to another action made done the recovery.
				 */
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						if (selection[0] != null) {
							TreePath path = selection[0];
							@SuppressWarnings("unchecked")
							MapID id = ((MapTreeNode) path
									.getLastPathComponent()).getMapID();
							Collection ids = listModel
									.getCurrentMapIDs();
							if (ids.contains(id)) {
								tree.clearSelection();
								tree.setSelectionPath(selection[0]);
							} else {
								// still present
							}
						} else {
							// no selection to recover
						}
					}
				});
			}

			@Override
			public void treeStructureChanged(TreeModelEvent e) {
				recoverSelection();
			}

			@Override
			public void treeNodesRemoved(TreeModelEvent e) {
				recoverSelection();
			}

			@Override
			public void treeNodesInserted(TreeModelEvent e) {
				recoverSelection();
			}

			@Override
			public void treeNodesChanged(TreeModelEvent e) {
				recoverSelection();
			}
		});
		tree.addMouseListener(new MouseListener() {

			@Override
			public void mouseReleased(MouseEvent event) {
				// nothing to do
			}

			@Override
			public void mousePressed(MouseEvent event) {
				// nothing to do
			}

			@Override
			public void mouseExited(MouseEvent event) {
				// nothing to do
			}

			@Override
			public void mouseEntered(MouseEvent event) {
				// nothing to do
			}

			@SuppressWarnings("unchecked")
			@Override
			public void mouseClicked(MouseEvent event) {
				synchronized (mapSummaries) {
					if (event.getButton() == MouseEvent.BUTTON1
							&& event.getClickCount() == 2) {
						MapID file = getSelectedID(tree);
						updateMapSummary(file, false);
						for (MapListListener listener : listeners) {
							if (listener instanceof MapSelectedListener) {
								((MapSelectedListener) listener)
										.mapSelected(file);
							} else {
								// not the right listener
							}
						}
					} else {
						// nothing to do for single click
					}
				}
			}

		});
		tree.addKeyListener(new KeyListener() {

			@Override
			public void keyTyped(KeyEvent arg0) {
				// nothing to do
			}

			@Override
			public void keyReleased(KeyEvent arg0) {
				int keyCode = arg0.getKeyCode();
				if (keyCode == KeyEvent.VK_F5) {
					updateMapSummary(getSelectedID(tree), true);
				} else {
					// no action for other keys
				}
			}

			@Override
			public void keyPressed(KeyEvent arg0) {
				// nothing to do
			}
		});
		return tree;
	}

	private MapID getSelectedID(final JTree tree) {
		@SuppressWarnings("unchecked")
		MapTreeNode node = (MapTreeNode) tree.getSelectionPath()
				.getLastPathComponent();
		MapID id = node.getMapID();
		return id;
	}

	private void configureBackgroundSummarizing() {
		// sense the app closing
		final boolean[] isClosed = { false };
		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {
				JFrame frame = (JFrame) SwingUtilities
						.getWindowAncestor(MapListPanel.this);
				if (frame == null) {
					SwingUtilities.invokeLater(this);
				} else {
					frame.addWindowListener(new WindowListener() {

						@Override
						public void windowOpened(WindowEvent arg0) {
							// do nothing
						}

						@Override
						public void windowIconified(WindowEvent arg0) {
							// do nothing
						}

						@Override
						public void windowDeiconified(WindowEvent arg0) {
							// do nothing
						}

						@Override
						public void windowDeactivated(WindowEvent arg0) {
							// do nothing
						}

						@Override
						public void windowClosing(WindowEvent arg0) {
							// do nothing
						}

						@Override
						public void windowClosed(WindowEvent arg0) {
							isClosed[0] = true;
						}

						@Override
						public void windowActivated(WindowEvent arg0) {
							// do nothing
						}

					});
				}
			}
		});

		// create the background task
		final boolean[] isRunning = { false };
		final Runnable backgroundSummary = new Runnable() {

			@Override
			public void run() {
				try {
					isRunning[0] = true;
					MapID id;
					if (isClosed[0] || (id = getWaitingMap()) == null) {
						isRunning[0] = false;
					} else {
						updateMapSummary(id, false);
						SwingUtilities.invokeLater(this);
					}
				} catch (Exception e) {
					isRunning[0] = false;
					throw new RuntimeException(e);
				}
			}

			private MapID getWaitingMap() {
				@SuppressWarnings("unchecked")
				ListModel model = (ListModel) tree.getModel();
				MapInformer mapInformer = model.getMapInformer();

				Iterator iterator = model.getCurrentMapIDs().iterator();
				while (iterator.hasNext()) {
					MapID id = iterator.next();
					try {
						mapInformer.getEntriesCount(id);
					} catch (NoDataException e) {
						return id;
					}
				}

				Iterator iterator2 = model.getAllMapsIDs().iterator();
				while (iterator2.hasNext()) {
					MapID id = iterator2.next();
					try {
						mapInformer.getEntriesCount(id);
					} catch (NoDataException e) {
						return id;
					}
				}

				return null;
			}
		};

		// sense the necessity to launch the task
		@SuppressWarnings("unchecked")
		ListModel model = (ListModel) tree.getModel();
		model.addMapsChangedListener(new MapsChangedListener() {

			@Override
			public void mapsChanged() {
				if (isRunning[0]) {
					// already running
				} else {
					SwingUtilities.invokeLater(backgroundSummary);
				}
			}
		});
	}

	public void updateMapSummary(final MapID id, boolean force) {
		synchronized (mapSummaries) {
			if (!force && mapSummaries.get(id) != null) {
				// nothing to load
			} else {
				logger.finest("Summarizing " + id + "...");
				MapSummary summary = new MapSummary();
				TMap map = currentProject.getMap(id);
				summary.total = map.size();
				summary.remaining = 0;
				Iterator iterator = map.iterator();
				while (iterator.hasNext()) {
					TEntry entry = iterator.next();
					summary.remaining += TranslationUtil
							.isActuallyTranslated(entry) ? 0 : 1;
				}
				mapSummaries.put(id, summary);
				logger.finest("Map summarized: " + summary);

				for (MapSummaryListener listener : mapSummaryListeners) {
					listener.mapSummarized(id);
				}
			}
		}
	}

	private static class MapSummary {
		int total;
		int remaining;
		boolean isModified;

		@Override
		public String toString() {
			String status = isModified ? "modified" : "saved";
			return (total - remaining) + "/" + total + "(" + status + ")";
		}
	}

	public void addListener(MapListListener listener) {
		listeners.add(listener);
	}

	public void removeListener(MapListListener listener) {
		listeners.remove(listener);
	}

	public static interface MapListListener {
	}

	public static interface MapSelectedListener extends MapListListener {
		public void mapSelected(MapID id);
	}

	public TranslationProject> getProject() {
		return currentProject;
	}

	public void setModifiedStatus(MapID id, boolean isModified) {
		mapSummaries.get(id).isModified = isModified;
		for (MapSummaryListener listener : mapSummaryListeners) {
			listener.mapSummarized(id);
		}
	}

	/**
	 * Sorting used to display the list of maps.
	 * 
	 * @author Matthieu VERGNE 
	 * 
	 */
	private static enum Order {
		/**
		 * Order the maps based on their IDs.
		 */
		ID,
		/**
		 * Order the maps based on their label.
		 */
		LABEL
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy