/* * ********************************************************************************************************************* * 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 * ********************************************************************************************************************* */ package io.github.dsheirer.icon; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.ImageIcon; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import java.awt.Component; import java.awt.EventQueue; import java.awt.Image; 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 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; ImageIcon mapValue = mResizedIcons.get(scaledIconName); if (mapValue != null) { return mapValue; } Icon icon = getModel().getIcon(name); ImageIcon scaledIcon = getScaledIcon(icon.getIcon(), height); if(scaledIcon != null) { 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) { if(original != null) { 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); } return null; } /** * Constructs an icon and scales it to the specified height * @param path * @param height * @return */ public static ImageIcon getScaledIcon(String path, int height) { if(path != null) { Icon icon = new Icon("", path); return getScaledIcon(icon.getIcon(), height); } return null; } /** * 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(); //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())) { JacksonXmlModule xmlModule = new JacksonXmlModule(); xmlModule.setDefaultUseWrapper(false); ObjectMapper objectMapper = new XmlMapper(xmlModule); objectMapper.enable(SerializationFeature.INDENT_OUTPUT); objectMapper.writeValue(out, iconSet); out.flush(); //Remove the playlist lock file to indicate that we successfully saved the file if(Files.exists(getIconLockFilePath())) { Files.delete(getIconLockFilePath()); } } 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())) { JacksonXmlModule xmlModule = new JacksonXmlModule(); xmlModule.setDefaultUseWrapper(false); ObjectMapper objectMapper = new XmlMapper(xmlModule); try(InputStream in = Files.newInputStream(getIconFilePath())) { iconSet = objectMapper.readValue(in, IconSet.class); } 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); } } }