/******************************************************************************* * sdrtrunk * Copyright (C) 2014-2017 Dennis Sheirer * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * ******************************************************************************/ package icon; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import properties.SystemProperties; import util.ThreadPool; import javax.swing.*; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import java.awt.*; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class IconManager { private final static Logger mLog = LoggerFactory.getLogger(IconManager.class); public static final int DEFAULT_ICON_SIZE = 12; private Path mIconFolderPath; private Path mIconFilePath; private Path mIconBackupFilePath; private Path mIconLockFilePath; private IconEditor mIconEditor; private IconTableModel mIconTableModel; private AtomicBoolean mSavingIcons = new AtomicBoolean(); private Map<String,ImageIcon> mResizedIcons = new HashMap<>(); public IconManager() { } /** * Display the icon editor centered over the specified component. * * @param centerOnComponent to center the editor */ public void showEditor(Component centerOnComponent) { if(mIconEditor == null) { mIconEditor = new IconEditor(this); } mIconEditor.setLocationRelativeTo(centerOnComponent); if(mIconEditor.isVisible()) { mIconEditor.requestFocus(); } else { EventQueue.invokeLater(new Runnable() { @Override public void run() { mIconEditor.setVisible(true); } }); } } public IconTableModel getModel() { if(mIconTableModel == null) { IconSet loadedIcons = load(); boolean saveRequired = false; if(loadedIcons == null) { loadedIcons = getDefaultIconSet(); saveRequired = true; } mIconTableModel = new IconTableModel(loadedIcons); mIconTableModel.addTableModelListener(new TableModelListener() { @Override public void tableChanged(TableModelEvent e) { scheduleSave(); } }); if(saveRequired) { scheduleSave(); } } return mIconTableModel; } /** * All icons in a sorted array */ public Icon[] getIcons() { return getModel().getIconsAsArray(); } /** * Returns named icon scaled to the specified height. Utilizes an internal map to retain scaled icons so that they * are only scaled/generated once. * * @param name - name of icon * @param height - height of icon in pixels * @return - scaled named icon (if it exists) or a scaled version of the default icon */ public ImageIcon getIcon(String name, int height) { if(name == null) { name = getModel().getDefaultIcon().getName(); } String scaledIconName = name + height; if(mResizedIcons.containsKey(scaledIconName)) { return mResizedIcons.get(scaledIconName); } Icon icon = getModel().getIcon(name); ImageIcon scaledIcon = getScaledIcon(icon.getIcon(), height); mResizedIcons.put(scaledIconName, scaledIcon); return scaledIcon; } /** * Scales the icon to the new pixel height value * * @param original image icon * @param height new height to scale the image (width will be scaled accordingly) * @return */ public static ImageIcon getScaledIcon(ImageIcon original, int height) { double scale = (double) original.getIconHeight() / (double) height; int scaledWidth = (int) ((double) original.getIconWidth() / scale); Image scaledImage = original.getImage().getScaledInstance(scaledWidth, height, java.awt.Image.SCALE_SMOOTH); return new ImageIcon(scaledImage); } /** * Folder where icon, backup and lock files are stored */ private Path getIconFolderPath() { if(mIconFolderPath == null) { SystemProperties props = SystemProperties.getInstance(); mIconFolderPath = props.getApplicationFolder("settings"); } return mIconFolderPath; } /** * Path to current icon file */ private Path getIconFilePath() { if(mIconFilePath == null) { mIconFilePath = getIconFolderPath().resolve("icons.xml"); } return mIconFilePath; } /** * Path to most recent playlist backup */ private Path getIconBackupFilePath() { if(mIconBackupFilePath == null) { mIconBackupFilePath = getIconFolderPath().resolve("icons.backup"); } return mIconBackupFilePath; } /** * Path to playlist lock file that is created prior to saving a playlist and removed immediately thereafter. * Presence of a lock file indicates an incomplete or corrupt playlist file on startup. */ private Path getIconLockFilePath() { if(mIconLockFilePath == null) { mIconLockFilePath = getIconFolderPath().resolve("icons.lock"); } return mIconLockFilePath; } /** * Saves the current playlist */ private void save() { IconSet iconSet = getModel().getIconSet(); JAXBContext context = null; //Create a backup copy of the current playlist if(Files.exists(getIconFilePath())) { try { Files.copy(getIconFilePath(), getIconBackupFilePath(), StandardCopyOption.REPLACE_EXISTING); } catch(Exception e) { mLog.error("Error creating backup copy of current icons prior to saving updates [" + getIconFilePath().toString() + "]", e); } } //Create a temporary lock file to signify that we're in the process of updating the playlist if(!Files.exists(getIconLockFilePath())) { try { Files.createFile(getIconLockFilePath()); } catch(IOException e) { mLog.error("Error creating temporary lock file prior to saving icons [" + getIconLockFilePath().toString() + "]", e); } } try(OutputStream out = Files.newOutputStream(getIconFilePath())) { context = JAXBContext.newInstance(IconSet.class); Marshaller m = context.createMarshaller(); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); m.marshal(iconSet, out); out.flush(); //Remove the playlist lock file to indicate that we successfully saved the file if(Files.exists(getIconLockFilePath())) { Files.delete(getIconLockFilePath()); } } catch(JAXBException je) { mLog.error("JAXB exception while serializing icons to a file [" + getIconFilePath().toString() + "]", je); } catch(IOException ioe) { mLog.error("IO error while writing icons to a file [" + getIconFilePath().toString() + "]", ioe); } catch(Exception e) { mLog.error("Error while saving icons [" + getIconFilePath().toString() + "]", e); } } /** * Loads icons from file or creates a default set of icons */ public IconSet load() { mLog.info("loading icons file [" + getIconFilePath().toString() + "]"); IconSet iconSet = null; //Check for a lock file that indicates the previous save attempt was incomplete or had an error if(Files.exists(getIconLockFilePath())) { try { //Remove the previous icons file Files.delete(getIconFilePath()); //Copy the backup file to restore the previous icons file if(Files.exists(getIconBackupFilePath())) { Files.copy(getIconBackupFilePath(), getIconFilePath()); } //Remove the lock file Files.delete(getIconLockFilePath()); } catch(IOException ioe) { mLog.error("Previous icons save attempt was incomplete and there was an error restoring the " + "icons backup file", ioe); } } if(Files.exists(getIconFilePath())) { JAXBContext context = null; try(InputStream in = Files.newInputStream(getIconFilePath())) { context = JAXBContext.newInstance(IconSet.class); Unmarshaller m = context.createUnmarshaller(); iconSet = (IconSet) m.unmarshal(in); } catch(JAXBException je) { mLog.error("JAXB exception while loading/unmarshalling icons", je); } catch(IOException ioe) { mLog.error("IO error while reading icons file", ioe); } } else { mLog.info("Icons file not found at [" + getIconFilePath().toString() + "]"); } return iconSet; } /** * Creates a default icon set */ private IconSet getDefaultIconSet() { IconSet iconSet = new IconSet(); Icon defaultIcon = new Icon(IconTableModel.DEFAULT_ICON, "images/no_icon.png"); iconSet.add(defaultIcon); iconSet.setDefaultIcon(defaultIcon.getName()); iconSet.add(new Icon("Ambulance", "images/ambulance.png")); iconSet.add(new Icon("Block Truck", "images/concrete_block_truck.png")); iconSet.add(new Icon("CWID", "images/cwid.png")); iconSet.add(new Icon("Dispatcher", "images/dispatcher.png")); iconSet.add(new Icon("Dump Truck", "images/dump_truck_red.png")); iconSet.add(new Icon("Fire Truck", "images/fire_truck.png")); iconSet.add(new Icon("Garbage Truck", "images/garbage_truck.png")); iconSet.add(new Icon("Loader", "images/loader.png")); iconSet.add(new Icon("Police", "images/police.png")); iconSet.add(new Icon("Propane Truck", "images/propane_truck.png")); iconSet.add(new Icon("Rescue Truck", "images/rescue_truck.png")); iconSet.add(new Icon("School Bus", "images/school_bus.png")); iconSet.add(new Icon("Taxi", "images/taxi.png")); iconSet.add(new Icon("Train", "images/train.png")); iconSet.add(new Icon("Transport Bus", "images/opt_bus.png")); iconSet.add(new Icon("Van", "images/van.png")); return iconSet; } /** * Schedules an icon file save task. Subsequent calls to this method will be ignored until the save event occurs, * thus limiting repetitive saving to a minimum. */ private void scheduleSave() { if(mSavingIcons.compareAndSet(false, true)) { ThreadPool.SCHEDULED.schedule(new IconSaveTask(), 2, TimeUnit.SECONDS); } } /** * Resets the playlist save pending flag to false and proceeds to save the playlist. */ public class IconSaveTask implements Runnable { @Override public void run() { save(); mSavingIcons.set(false); } } }