JAL-4461 Added a dynamic tooltip to give more information when 'By extension' is...
authorBen Soares <b.soares@dundee.ac.uk>
Thu, 12 Sep 2024 15:52:01 +0000 (16:52 +0100)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 12 Sep 2024 15:52:01 +0000 (16:52 +0100)
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/io/JalviewFileChooser.java
src/jalview/util/SwingUtils.java [new file with mode: 0644]

index 4576252..72148f2 100644 (file)
@@ -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}
index f87b195..23c6671 100644 (file)
@@ -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}
index 0113717..63c9a61 100755 (executable)
@@ -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<JComboBox> 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 (file)
index 0000000..cd9c52d
--- /dev/null
@@ -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 <code>container</code> in the
+   * component hierarchy and return nested components that are instances of
+   * class <code>clazz</code> it finds. Returns an empty list if no such
+   * components exist in the container.
+   * <P>
+   * Invoking this method with a class parameter of JComponent.class will return
+   * all nested components.
+   * <P>
+   * 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 <T extends JComponent> List<T> getDescendantsOfType(
+          Class<T> clazz, Container container)
+  {
+    return getDescendantsOfType(clazz, container, true);
+  }
+
+  /**
+   * Convenience method for searching below <code>container</code> in the
+   * component hierarchy and return nested components that are instances of
+   * class <code>clazz</code> it finds. Returns an empty list if no such
+   * components exist in the container.
+   * <P>
+   * 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 <T extends JComponent> List<T> getDescendantsOfType(
+          Class<T> clazz, Container container, boolean nested)
+  {
+    List<T> tList = new ArrayList<T>();
+    for (Component component : container.getComponents())
+    {
+      if (clazz.isAssignableFrom(component.getClass()))
+      {
+        tList.add(clazz.cast(component));
+      }
+      if (nested || !clazz.isAssignableFrom(component.getClass()))
+      {
+        tList.addAll(SwingUtils.<T> getDescendantsOfType(clazz,
+                (Container) component, nested));
+      }
+    }
+    return tList;
+  }
+
+  /**
+   * Convenience method that searches below <code>container</code> in the
+   * component hierarchy and returns the first found component that is an
+   * instance of class <code>clazz</code> having the bound property value.
+   * Returns {@code null} if such component cannot be found.
+   * <P>
+   * 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 extends JComponent> T getDescendantOfType(Class<T> clazz,
+          Container container, String property, Object value)
+          throws IllegalArgumentException
+  {
+    return getDescendantOfType(clazz, container, property, value, true);
+  }
+
+  /**
+   * Convenience method that searches below <code>container</code> in the
+   * component hierarchy and returns the first found component that is an
+   * instance of class <code>clazz</code> 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 <code>clazz</code>, 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 extends JComponent> T getDescendantOfType(Class<T> clazz,
+          Container container, String property, Object value,
+          boolean nested) throws IllegalArgumentException
+  {
+    List<T> list = getDescendantsOfType(clazz, container, nested);
+    return getComponentFromList(clazz, list, property, value);
+  }
+
+  /**
+   * Convenience method for searching below <code>container</code> in the
+   * component hierarchy and return nested components of class
+   * <code>clazz</code> it finds. Returns an empty list if no such components
+   * exist in the container.
+   * <P>
+   * 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 <T extends JComponent> List<T> getDescendantsOfClass(
+          Class<T> clazz, Container container)
+  {
+    return getDescendantsOfClass(clazz, container, true);
+  }
+
+  /**
+   * Convenience method for searching below <code>container</code> in the
+   * component hierarchy and return nested components of class
+   * <code>clazz</code> 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 <T extends JComponent> List<T> getDescendantsOfClass(
+          Class<T> clazz, Container container, boolean nested)
+  {
+    List<T> tList = new ArrayList<T>();
+    for (Component component : container.getComponents())
+    {
+      if (clazz.equals(component.getClass()))
+      {
+        tList.add(clazz.cast(component));
+      }
+      if (nested || !clazz.equals(component.getClass()))
+      {
+        tList.addAll(SwingUtils.<T> getDescendantsOfClass(clazz,
+                (Container) component, nested));
+      }
+    }
+    return tList;
+  }
+
+  /**
+   * Convenience method that searches below <code>container</code> in the
+   * component hierarchy in a depth first manner and returns the first found
+   * component of class <code>clazz</code> having the bound property value.
+   * <P>
+   * Returns {@code null} if such component cannot be found.
+   * <P>
+   * 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 extends JComponent> T getDescendantOfClass(
+          Class<T> clazz, Container container, String property,
+          Object value) throws IllegalArgumentException
+  {
+    return getDescendantOfClass(clazz, container, property, value, true);
+  }
+
+  /**
+   * Convenience method that searches below <code>container</code> in the
+   * component hierarchy in a depth first manner and returns the first found
+   * component of class <code>clazz</code> having the bound property value.
+   * <P>
+   * 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 extends JComponent> T getDescendantOfClass(
+          Class<T> clazz, Container container, String property,
+          Object value, boolean nested) throws IllegalArgumentException
+  {
+    List<T> list = getDescendantsOfClass(clazz, container, nested);
+    return getComponentFromList(clazz, list, property, value);
+  }
+
+  private static <T extends JComponent> T getComponentFromList(
+          Class<T> clazz, List<T> 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.
+   * <P>
+   * 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<JComponent, List<JComponent>> getComponentMap(
+          JComponent container, boolean nested)
+  {
+    HashMap<JComponent, List<JComponent>> retVal = new HashMap<JComponent, List<JComponent>>();
+    for (JComponent component : getDescendantsOfType(JComponent.class,
+            container, false))
+    {
+      if (!retVal.containsKey(container))
+      {
+        retVal.put(container, new ArrayList<JComponent>());
+      }
+      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<Object> 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<String> setExclude = new HashSet<String>();
+  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.
+   * <P>
+   * 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<Object, Object> getProperties(JComponent component)
+  {
+    Map<Object, Object> retVal = new HashMap<Object, Object>();
+    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 <T extends JComponent> Class getJClass(T component)
+  {
+    Class<?> clazz = component.getClass();
+    while (!clazz.getName().matches("javax.swing.J[^.]*$"))
+    {
+      clazz = clazz.getSuperclass();
+    }
+    return clazz;
+  }
+}