From cea846e6b899fb5903011acc67004d73f59c3ba1 Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Thu, 12 Sep 2024 16:52:01 +0100 Subject: [PATCH] JAL-4461 Added a dynamic tooltip to give more information when 'By extension' is the selected File Type. Utilising MIT licensed SwingUtils.getDescendantsByClass() to delve into the JFileChooser. --- resources/lang/Messages.properties | 5 +- resources/lang/Messages_es.properties | 5 +- src/jalview/io/JalviewFileChooser.java | 66 +++- src/jalview/util/SwingUtils.java | 529 ++++++++++++++++++++++++++++++++ 4 files changed, 592 insertions(+), 13 deletions(-) create mode 100644 src/jalview/util/SwingUtils.java diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index 4576252..72148f2 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -401,7 +401,7 @@ label.selected_database_to_fetch_from = Selected {0} database {1} to fetch from label.database_param = Database: {0} label.example = Example label.example_param = Example: {0} -label.select_file_format_before_saving = You must select a file format before saving! +label.select_file_format_before_saving = You must select a file format or use a recognised file extension before saving! label.file_format_not_specified = File format not specified label.couldnt_save_file = Couldn''t save file: {0} label.error_saving_file = Error Saving File @@ -1479,7 +1479,8 @@ action.cluster_matrix = Cluster matrix action.clustering_matrix_for = Calculating tree for matrix {0} and clustering at {1} action.cluster_matrix_tooltip = Computes an average distance tree for the matrix and displays it label.all_known_alignment_files = All known alignment files -label.select_format_from_extension = Select format from filename extension +label.by_extension = By extension +label.by_extension_tooltip = File will be saved in a format corresponding to the given file extension (if the extension is recognised) label.command_line_arguments = Command Line Arguments warning.using_old_command_line_arguments = It looks like you are using old command line arguments. These are now deprecated and will be removed in a future release of Jalview.\nFind out about the new command line arguments at\n warning.using_mixed_command_line_arguments = Jalview cannot use both old (-arg) and new (--arg) command line arguments. Please check your command line arguments.\ne.g. {0} and {1} diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index f87b195..23c6671 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -359,7 +359,7 @@ label.selected_database_to_fetch_from = Seleccionada {0} Base de datos {1} para label.database_param = Base de datos: {0} label.example = Ejemplo label.example_param = Ejemplo: {0} -label.select_file_format_before_saving = Debe seleccionar un formato de fichero antes de guardar! +label.select_file_format_before_saving = ¡Debe seleccionar un formato de fichero o utilizar una extensión de archivo reconocida antes de guardar! label.file_format_not_specified = Formato de fichero no especificado label.couldnt_save_file = No se pudo guardar el fichero: {0} label.error_saving_file = Error guardando el fichero @@ -1444,7 +1444,8 @@ label.nothing_selected = Nada seleccionado prompt.analytics_title = Jalview Estadísticas de Uso prompt.analytics = ¿Quiere ayudar a mejorar Jalview habilitando la recopilación de estadísticas de uso con análisis Plausible?\nPuede habilitar o deshabilitar el seguimiento de uso en las preferencias. label.all_known_alignment_files = Todos los archivos de alineación conocidos -label.select_format_from_extension = Seleccione el formato de la extensión del archivo +label.by_extension = Por extensión +label.by_extension_tooltip = El archivo se guardará en un formato correspondiente a la extensión de archivo indicada (si se reconoce la extensión) label.command_line_arguments = Argumentos de línea de comando warning.using_old_command_line_arguments = Parece que estás utilizando argumentos antiguos de línea de comando. Estos ahora están en desuso y se eliminarán en una versión futura de Jalview.\nObtenga más información sobre los nuevos argumentos de la línea de comando en\n warning.using_mixed_command_line_arguments = Jalview no puede utilizar argumentos de línea de comando antiguos (-arg) y nuevos (--arg). Verifique los argumentos de su línea de comando.\ne.g. {0} y {1} diff --git a/src/jalview/io/JalviewFileChooser.java b/src/jalview/io/JalviewFileChooser.java index 0113717..63c9a61 100755 --- a/src/jalview/io/JalviewFileChooser.java +++ b/src/jalview/io/JalviewFileChooser.java @@ -41,6 +41,7 @@ import java.util.Vector; import javax.swing.BoxLayout; import javax.swing.JCheckBox; +import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; @@ -61,6 +62,7 @@ import jalview.gui.JvOptionPane; import jalview.util.ChannelProperties; import jalview.util.MessageManager; import jalview.util.Platform; +import jalview.util.SwingUtils; import jalview.util.dialogrunner.DialogRunnerI; /** @@ -306,9 +308,9 @@ public class JalviewFileChooser extends JFileChooser // only add "All known alignment files" if acceptAny is allowed if (acceptAny || addSelectFormatFromExtension) { - String label = MessageManager.getString(addSelectFormatFromExtension - ? "label.select_format_from_extension" - : "label.all_known_alignment_files"); + String label = MessageManager + .getString(addSelectFormatFromExtension ? "label.by_extension" + : "label.all_known_alignment_files"); JalviewFileFilter alljvf = new JalviewFileFilter( allExtensions.toArray(new String[] {}), label); alljvf.setMultiFormat(true); @@ -319,6 +321,31 @@ public class JalviewFileChooser extends JFileChooser { chosen = alljvf; } + + // Add the tooltip that appears for the multiformat file type in the + // dropdown, but not others + if (addSelectFormatFromExtension) + { + List dropdowns = SwingUtils + .getDescendantsOfType(JComboBox.class, this); + for (JComboBox dd : dropdowns) + { + if (dd.getItemCount() > 0 + && dd.getItemAt(0) instanceof JalviewFileFilter) + { + setByExtensionTooltip(dd); + dd.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + setByExtensionTooltip(dd); + } + }); + break; + } + } + } } for (String[] format : formats) @@ -400,6 +427,23 @@ public class JalviewFileChooser extends JFileChooser setAccessory(multi); } + private static void setByExtensionTooltip(JComboBox dd) + { + if (dd.getItemCount() > 0 + && dd.getItemAt(0) instanceof JalviewFileFilter) + { + if (((JalviewFileFilter) dd.getSelectedItem()).isMultiFormat()) + { + dd.setToolTipText( + MessageManager.getString("label.by_extension_tooltip")); + } + else + { + dd.setToolTipText(null); + } + } + } + @Override public void setFileFilter(javax.swing.filechooser.FileFilter filter) { @@ -471,29 +515,33 @@ public class JalviewFileChooser extends JFileChooser } return null; } + /** * Unused - could delete ? - * @param format - matches and configures the filefilter according to given format + * + * @param format + * - matches and configures the filefilter according to given format * @return true if the format given matched an available filter */ public boolean setSelectedFormat(FileFormatI format) { - if (format==null) + if (format == null) { return false; } String toSelect = format.getName(); - for (FileFilter available:getChoosableFileFilters()) + for (FileFilter available : getChoosableFileFilters()) { if (available instanceof JalviewFileFilter) { - if (((JalviewFileFilter)available).getDescription().equals(toSelect)) + if (((JalviewFileFilter) available).getDescription() + .equals(toSelect)) { setFileFilter(available); return true; } } - } + } return false; } @@ -576,7 +624,7 @@ public class JalviewFileChooser extends JFileChooser // extension, see if we can set the format from the file extension for (FileFilter jff : this.getChoosableFileFilters()) { - if (jvf!=jff && jff.accept(selectedFile)) + if (jvf != jff && jff.accept(selectedFile)) { setFileFilter(jff); return; diff --git a/src/jalview/util/SwingUtils.java b/src/jalview/util/SwingUtils.java new file mode 100644 index 0000000..cd9c52d --- /dev/null +++ b/src/jalview/util/SwingUtils.java @@ -0,0 +1,529 @@ +/* + * @(#)SwingUtils.java 1.02 11/15/08 + * + */ +/* from https://github.com/tips4java/tips4java/blob/main/source/SwingUtils.java MIT License */ +//package darrylbu.util; +package jalview.util; + +import java.awt.Component; +import java.awt.Container; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.swing.JComponent; +import javax.swing.UIDefaults; +import javax.swing.UIManager; + +/** + * A collection of utility methods for Swing. + * + * @author Darryl Burke + */ +public final class SwingUtils +{ + + private SwingUtils() + { + throw new Error("SwingUtils is just a container for static methods"); + } + + /** + * Convenience method for searching below container in the + * component hierarchy and return nested components that are instances of + * class clazz it finds. Returns an empty list if no such + * components exist in the container. + *

+ * Invoking this method with a class parameter of JComponent.class will return + * all nested components. + *

+ * This method invokes getDescendantsOfType(clazz, container, true) + * + * @param clazz + * the class of components whose instances are to be found. + * @param container + * the container at which to begin the search + * @return the List of components + */ + public static List getDescendantsOfType( + Class clazz, Container container) + { + return getDescendantsOfType(clazz, container, true); + } + + /** + * Convenience method for searching below container in the + * component hierarchy and return nested components that are instances of + * class clazz it finds. Returns an empty list if no such + * components exist in the container. + *

+ * Invoking this method with a class parameter of JComponent.class will return + * all nested components. + * + * @param clazz + * the class of components whose instances are to be found. + * @param container + * the container at which to begin the search + * @param nested + * true to list components nested within another listed component, + * false otherwise + * @return the List of components + */ + public static List getDescendantsOfType( + Class clazz, Container container, boolean nested) + { + List tList = new ArrayList(); + for (Component component : container.getComponents()) + { + if (clazz.isAssignableFrom(component.getClass())) + { + tList.add(clazz.cast(component)); + } + if (nested || !clazz.isAssignableFrom(component.getClass())) + { + tList.addAll(SwingUtils. getDescendantsOfType(clazz, + (Container) component, nested)); + } + } + return tList; + } + + /** + * Convenience method that searches below container in the + * component hierarchy and returns the first found component that is an + * instance of class clazz having the bound property value. + * Returns {@code null} if such component cannot be found. + *

+ * This method invokes getDescendantOfType(clazz, container, property, value, + * true) + * + * @param clazz + * the class of component whose instance is to be found. + * @param container + * the container at which to begin the search + * @param property + * the className of the bound property, exactly as expressed in the + * accessor e.g. "Text" for getText(), "Value" for getValue(). + * @param value + * the value of the bound property + * @return the component, or null if no such component exists in the container + * @throws java.lang.IllegalArgumentException + * if the bound property does not exist for the class or cannot be + * accessed + */ + public static T getDescendantOfType(Class clazz, + Container container, String property, Object value) + throws IllegalArgumentException + { + return getDescendantOfType(clazz, container, property, value, true); + } + + /** + * Convenience method that searches below container in the + * component hierarchy and returns the first found component that is an + * instance of class clazz and has the bound property value. + * Returns {@code null} if such component cannot be found. + * + * @param clazz + * the class of component whose instance to be found. + * @param container + * the container at which to begin the search + * @param property + * the className of the bound property, exactly as expressed in the + * accessor e.g. "Text" for getText(), "Value" for getValue(). + * @param value + * the value of the bound property + * @param nested + * true to list components nested within another component which is + * also an instance of clazz, false otherwise + * @return the component, or null if no such component exists in the container + * @throws java.lang.IllegalArgumentException + * if the bound property does not exist for the class or cannot be + * accessed + */ + public static T getDescendantOfType(Class clazz, + Container container, String property, Object value, + boolean nested) throws IllegalArgumentException + { + List list = getDescendantsOfType(clazz, container, nested); + return getComponentFromList(clazz, list, property, value); + } + + /** + * Convenience method for searching below container in the + * component hierarchy and return nested components of class + * clazz it finds. Returns an empty list if no such components + * exist in the container. + *

+ * This method invokes getDescendantsOfClass(clazz, container, true) + * + * @param clazz + * the class of components to be found. + * @param container + * the container at which to begin the search + * @return the List of components + */ + public static List getDescendantsOfClass( + Class clazz, Container container) + { + return getDescendantsOfClass(clazz, container, true); + } + + /** + * Convenience method for searching below container in the + * component hierarchy and return nested components of class + * clazz it finds. Returns an empty list if no such components + * exist in the container. + * + * @param clazz + * the class of components to be found. + * @param container + * the container at which to begin the search + * @param nested + * true to list components nested within another listed component, + * false otherwise + * @return the List of components + */ + public static List getDescendantsOfClass( + Class clazz, Container container, boolean nested) + { + List tList = new ArrayList(); + for (Component component : container.getComponents()) + { + if (clazz.equals(component.getClass())) + { + tList.add(clazz.cast(component)); + } + if (nested || !clazz.equals(component.getClass())) + { + tList.addAll(SwingUtils. getDescendantsOfClass(clazz, + (Container) component, nested)); + } + } + return tList; + } + + /** + * Convenience method that searches below container in the + * component hierarchy in a depth first manner and returns the first found + * component of class clazz having the bound property value. + *

+ * Returns {@code null} if such component cannot be found. + *

+ * This method invokes getDescendantOfClass(clazz, container, property, value, + * true) + * + * @param clazz + * the class of component to be found. + * @param container + * the container at which to begin the search + * @param property + * the className of the bound property, exactly as expressed in the + * accessor e.g. "Text" for getText(), "Value" for getValue(). This + * parameter is case sensitive. + * @param value + * the value of the bound property + * @return the component, or null if no such component exists in the + * container's hierarchy. + * @throws java.lang.IllegalArgumentException + * if the bound property does not exist for the class or cannot be + * accessed + */ + public static T getDescendantOfClass( + Class clazz, Container container, String property, + Object value) throws IllegalArgumentException + { + return getDescendantOfClass(clazz, container, property, value, true); + } + + /** + * Convenience method that searches below container in the + * component hierarchy in a depth first manner and returns the first found + * component of class clazz having the bound property value. + *

+ * Returns {@code null} if such component cannot be found. + * + * @param clazz + * the class of component to be found. + * @param container + * the container at which to begin the search + * @param property + * the className of the bound property, exactly as expressed in the + * accessor e.g. "Text" for getText(), "Value" for getValue(). This + * parameter is case sensitive. + * @param value + * the value of the bound property + * @param nested + * true to include components nested within another listed component, + * false otherwise + * @return the component, or null if no such component exists in the + * container's hierarchy + * @throws java.lang.IllegalArgumentException + * if the bound property does not exist for the class or cannot be + * accessed + */ + public static T getDescendantOfClass( + Class clazz, Container container, String property, + Object value, boolean nested) throws IllegalArgumentException + { + List list = getDescendantsOfClass(clazz, container, nested); + return getComponentFromList(clazz, list, property, value); + } + + private static T getComponentFromList( + Class clazz, List list, String property, Object value) + throws IllegalArgumentException + { + T retVal = null; + Method method = null; + try + { + method = clazz.getMethod("get" + property); + } catch (NoSuchMethodException ex) + { + try + { + method = clazz.getMethod("is" + property); + } catch (NoSuchMethodException ex1) + { + throw new IllegalArgumentException("Property " + property + + " not found in class " + clazz.getName()); + } + } + try + { + for (T t : list) + { + Object testVal = method.invoke(t); + if (equals(value, testVal)) + { + return t; + } + } + } catch (InvocationTargetException ex) + { + throw new IllegalArgumentException("Error accessing property " + + property + " in class " + clazz.getName()); + } catch (IllegalAccessException ex) + { + throw new IllegalArgumentException("Property " + property + + " cannot be accessed in class " + clazz.getName()); + } catch (SecurityException ex) + { + throw new IllegalArgumentException("Property " + property + + " cannot be accessed in class " + clazz.getName()); + } + return retVal; + } + + /** + * Convenience method for determining whether two objects are either equal or + * both null. + * + * @param obj1 + * the first reference object to compare. + * @param obj2 + * the second reference object to compare. + * @return true if obj1 and obj2 are equal or if both are null, false + * otherwise + */ + public static boolean equals(Object obj1, Object obj2) + { + return obj1 == null ? obj2 == null : obj1.equals(obj2); + } + + /** + * Convenience method for mapping a container in the hierarchy to its + * contained components. The keys are the containers, and the values are lists + * of contained components. + *

+ * Implementation note: The returned value is a HashMap and the values are of + * type ArrayList. This is subject to change, so callers should code against + * the interfaces Map and List. + * + * @param container + * The JComponent to be mapped + * @param nested + * true to drill down to nested containers, false otherwise + * @return the Map of the UI + */ + public static Map> getComponentMap( + JComponent container, boolean nested) + { + HashMap> retVal = new HashMap>(); + for (JComponent component : getDescendantsOfType(JComponent.class, + container, false)) + { + if (!retVal.containsKey(container)) + { + retVal.put(container, new ArrayList()); + } + retVal.get(container).add(component); + if (nested) + { + retVal.putAll(getComponentMap(component, nested)); + } + } + return retVal; + } + + /** + * Convenience method for retrieving a subset of the UIDefaults pertaining to + * a particular class. + * + * @param clazz + * the class of interest + * @return the UIDefaults of the class + */ + public static UIDefaults getUIDefaultsOfClass(Class clazz) + { + String name = clazz.getName(); + name = name.substring(name.lastIndexOf(".") + 2); + return getUIDefaultsOfClass(name); + } + + /** + * Convenience method for retrieving a subset of the UIDefaults pertaining to + * a particular class. + * + * @param className + * fully qualified name of the class of interest + * @return the UIDefaults of the class named + */ + public static UIDefaults getUIDefaultsOfClass(String className) + { + UIDefaults retVal = new UIDefaults(); + UIDefaults defaults = UIManager.getLookAndFeelDefaults(); + List listKeys = Collections.list(defaults.keys()); + for (Object key : listKeys) + { + if (key instanceof String && ((String) key).startsWith(className)) + { + String stringKey = (String) key; + String property = stringKey; + if (stringKey.contains(".")) + { + property = stringKey.substring(stringKey.indexOf(".") + 1); + } + retVal.put(property, defaults.get(key)); + } + } + return retVal; + } + + /** + * Convenience method for retrieving the UIDefault for a single property of a + * particular class. + * + * @param clazz + * the class of interest + * @param property + * the property to query + * @return the UIDefault property, or null if not found + */ + public static Object getUIDefaultOfClass(Class clazz, String property) + { + Object retVal = null; + UIDefaults defaults = getUIDefaultsOfClass(clazz); + List listKeys = Collections.list(defaults.keys()); + for (Object key : listKeys) + { + if (key.equals(property)) + { + return defaults.get(key); + } + if (key.toString().equalsIgnoreCase(property)) + { + retVal = defaults.get(key); + } + } + return retVal; + } + + /** + * Exclude methods that return values that are meaningless to the user + */ + static Set setExclude = new HashSet(); + static + { + setExclude.add("getFocusCycleRootAncestor"); + setExclude.add("getAccessibleContext"); + setExclude.add("getColorModel"); + setExclude.add("getGraphics"); + setExclude.add("getGraphicsConfiguration"); + } + + /** + * Convenience method for obtaining most non-null human readable properties of + * a JComponent. Array properties are not included. + *

+ * Implementation note: The returned value is a HashMap. This is subject to + * change, so callers should code against the interface Map. + * + * @param component + * the component whose proerties are to be determined + * @return the class and value of the properties + */ + public static Map getProperties(JComponent component) + { + Map retVal = new HashMap(); + Class clazz = component.getClass(); + Method[] methods = clazz.getMethods(); + Object value = null; + for (Method method : methods) + { + if (method.getName().matches("^(is|get).*") + && method.getParameterTypes().length == 0) + { + try + { + Class returnType = method.getReturnType(); + if (returnType != void.class + && !returnType.getName().startsWith("[") + && !setExclude.contains(method.getName())) + { + String key = method.getName(); + value = method.invoke(component); + if (value != null && !(value instanceof Component)) + { + retVal.put(key, value); + } + } + // ignore exceptions that arise if the property could not be accessed + } catch (IllegalAccessException ex) + { + } catch (IllegalArgumentException ex) + { + } catch (InvocationTargetException ex) + { + } + } + } + return retVal; + } + + /** + * Convenience method to obtain the Swing class from which this component was + * directly or indirectly derived. + * + * @param component + * The component whose Swing superclass is to be determined + * @return The nearest Swing class in the inheritance tree + */ + public static Class getJClass(T component) + { + Class clazz = component.getClass(); + while (!clazz.getName().matches("javax.swing.J[^.]*$")) + { + clazz = clazz.getSuperclass(); + } + return clazz; + } +} -- 1.7.10.2