public enum TimeArea {
  TimePickerTextValidTime(Color.black),
  TimePickerTextInvalidTime(Color.red),
  TimePickerTextVetoedTime(Color.black),
  TimePickerTextDisabled(new Color(109, 109, 109)),
  TextFieldBackgroundValidTime(Color.white),
  TextFieldBackgroundInvalidTime(Color.white),
  TextFieldBackgroundVetoedTime(Color.white),
  TextFieldBackgroundDisallowedEmptyTime(Color.pink),
  TextFieldBackgroundDisabled(new Color(240, 240, 240));

  TimeArea(Color defaultColor) {
    this.defaultColor = defaultColor;
  }

  public Color defaultColor;
} public class PickerUtilities {

  /**
   * createFormatterFromPatternString, This creates a DateTimeFormatter from the supplied pattern
   * string and supplied locale. The pattern will be created to be "lenient" and "case insensitive",
   * so it can be used for display or for user-friendly parsing. Information about creating a
   * pattern string can be found in the DateTimeFormatter class Javadocs. @see <a
   * href="https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html">The
   * DateTimeFormatter Javadocs </a>
   *
   * <p>Note: It is important to use the letter "u" (astronomical year) instead of "y" (year of era)
   * when creating pattern strings for BCE dates. This is because the DatePicker uses ISO 8601,
   * which specifies "Astronomical year numbering". (Additional details: The astronomical year "-1"
   * and "1 BC" are not the same thing. Astronomical years are zero-based, and BC dates are
   * one-based. Astronomical year "0", is the same year as "1 BC", and astronomical year "-1" is the
   * same year as "2 BC", and so forth.)
   */
  public static DateTimeFormatter createFormatterFromPatternString(
      String formatPattern, Locale locale) {
    DateTimeFormatter formatter =
        new DateTimeFormatterBuilder()
            .parseLenient()
            .parseCaseInsensitive()
            .appendPattern(formatPattern)
            .toFormatter(locale);
    return formatter;
  }

  /**
   * isLocalTimeInRange, This returns true if the specified value is inside of the specified range.
   * This returns false if the specified value is outside of the specified range. If the specified
   * value is null, then this will return false.
   *
   * <p>If optionalMinimum is null, then it will be set to LocalTime.MIN. If optionalMaximum is
   * null, then it will be set to LocalTime.MAX.
   *
   * <p>If inclusiveOfEndpoints is true, then values that equal the minimum or maximum will return
   * true. Otherwise, values that equal the minimum or maximum will return false.
   */
  public static boolean isLocalTimeInRange(
      LocalTime value,
      LocalTime optionalMinimum,
      LocalTime optionalMaximum,
      boolean inclusiveOfEndpoints) {
    // If either bounding time does does not already exist, then set it to the maximum range.
    LocalTime minimum = (optionalMinimum == null) ? LocalTime.MIN : optionalMinimum;
    LocalTime maximum = (optionalMaximum == null) ? LocalTime.MAX : optionalMaximum;
    // Null is never considered to be inside of a range.
    if (value == null) {
      return false;
    }
    // Return false if the range does not contain any times.
    if (maximum.isBefore(minimum) || maximum.equals(minimum)) {
      return false;
    }
    if (inclusiveOfEndpoints) {
      return ((value.isAfter(minimum) || value.equals(minimum))
          && (value.isBefore(maximum) || value.equals(maximum)));
    } else {
      return (value.isAfter(minimum) && value.isBefore(maximum));
    }
  }

  /**
   * isSameLocalDate, This compares two date variables to see if their values are equal. Returns
   * true if the values are equal, otherwise returns false.
   *
   * <p>More specifically: This returns true if both values are null (an empty date). Or, this
   * returns true if both of the supplied dates contain a date and represent the same date.
   * Otherwise this returns false.
   */
  public static boolean isSameLocalDate(LocalDate first, LocalDate second) {
    // If both values are null, return true.
    if (first == null && second == null) {
      return true;
    }
    // At least one value contains a date. If the other value is null, then return false.
    if (first == null || second == null) {
      return false;
    }
    // Both values contain dates. Return true if the dates are equal, otherwise return false.
    return first.isEqual(second);
  }

  /**
   * isSameYearMonth, This compares two YearMonth variables to see if their values are equal.
   * Returns true if the values are equal, otherwise returns false.
   *
   * <p>More specifically: This returns true if both values are null (an empty YearMonth). Or, this
   * returns true if both of the supplied YearMonths contain a YearMonth and represent the same year
   * and month. Otherwise this returns false.
   */
  public static boolean isSameYearMonth(YearMonth first, YearMonth second) {
    // If both values are null, return true.
    if (first == null && second == null) {
      return true;
    }
    // At least one value contains a YearMonth. If the other value is null, then return false.
    if (first == null || second == null) {
      return false;
    }
    // Both values contain a YearMonth.
    // Return true if the YearMonth are equal, otherwise return false.
    return first.equals(second);
  }

  public static boolean isSameLocalTime(LocalTime first, LocalTime second) {
    // If both values are null, return true.
    if (first == null && second == null) {
      return true;
    }
    // At least one value contains a time. If the other value is null, then return false.
    if (first == null || second == null) {
      return false;
    }
    // Both values contain times. Return true if the times are equal, otherwise return false.
    return first.equals(second);
  }

  /**
   * localDateTimeToString, This will return the supplied LocalDateTime as a string. If the value is
   * null, this will return the value of emptyTimeString. Time values will be output in the same
   * format as LocalDateTime.toString().
   *
   * <p>Javadocs from LocalDateTime.toString():
   *
   * <p>Outputs this date-time as a {@code String}, such as {@code 2007-12-03T10:15:30}.
   *
   * <p>The output will be one of the following ISO-8601 formats:
   *
   * <ul>
   *   <li>{@code uuuu-MM-dd'T'HH:mm}
   *   <li>{@code uuuu-MM-dd'T'HH:mm:ss}
   *   <li>{@code uuuu-MM-dd'T'HH:mm:ss.SSS}
   *   <li>{@code uuuu-MM-dd'T'HH:mm:ss.SSSSSS}
   *   <li>{@code uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS}
   * </ul>
   *
   * The format used will be the shortest that outputs the full value of the time where the omitted
   * parts are implied to be zero.
   */
  public static String localDateTimeToString(LocalDateTime value, String emptyTimeString) {
    return (value == null) ? emptyTimeString : value.toString();
  }

  /**
   * localDateTimeToString, This will return the supplied LocalDateTime as a string. If the value is
   * null, this will return an empty string (""). Time values will be output in the same format as
   * LocalDateTime.toString().
   *
   * <p>Javadocs from LocalDateTime.toString():
   *
   * <p>Outputs this date-time as a {@code String}, such as {@code 2007-12-03T10:15:30}.
   *
   * <p>The output will be one of the following ISO-8601 formats:
   *
   * <ul>
   *   <li>{@code uuuu-MM-dd'T'HH:mm}
   *   <li>{@code uuuu-MM-dd'T'HH:mm:ss}
   *   <li>{@code uuuu-MM-dd'T'HH:mm:ss.SSS}
   *   <li>{@code uuuu-MM-dd'T'HH:mm:ss.SSSSSS}
   *   <li>{@code uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS}
   * </ul>
   *
   * The format used will be the shortest that outputs the full value of the time where the omitted
   * parts are implied to be zero.
   */
  public static String localDateTimeToString(LocalDateTime value) {
    return (value == null) ? "" : value.toString();
  }

  /**
   * localDateToString, This returns the supplied date in the ISO-8601 format (uuuu-MM-dd). For any
   * CE years that are between 0 and 9999 inclusive, the output will have a fixed length of 10
   * characters. Years before or after that range will output longer strings. If the date is null,
   * this will return an empty string ("").
   */
  public static String localDateToString(LocalDate date) {
    return localDateToString(date, "");
  }

  /**
   * localDateToString, This returns the supplied date in the ISO-8601 format (uuuu-MM-dd). For any
   * CE years that are between 0 and 9999 inclusive, the output will have a fixed length of 10
   * characters. Years before or after that range will output longer strings. If the date is null,
   * this will return the value of emptyDateString.
   */
  public static String localDateToString(LocalDate date, String emptyDateString) {
    return (date == null) ? emptyDateString : date.toString();
  }

  /**
   * localTimeToString, This will return the supplied time as a string. If the time is null, this
   * will return an empty string ("").
   *
   * <p>Time values will be output in one of the following ISO-8601 formats: "HH:mm", "HH:mm:ss",
   * "HH:mm:ss.SSS", "HH:mm:ss.SSSSSS", "HH:mm:ss.SSSSSSSSS".
   *
   * <p>The format used will be the shortest that outputs the full value of the time where the
   * omitted parts are implied to be zero.
   */
  public static String localTimeToString(LocalTime time) {
    return (time == null) ? "" : time.toString();
  }

  /**
   * localTimeToString, This will return the supplied time as a string. If the time is null, this
   * will return the value of emptyTimeString.
   *
   * <p>Time values will be output in one of the following ISO-8601 formats: "HH:mm", "HH:mm:ss",
   * "HH:mm:ss.SSS", "HH:mm:ss.SSSSSS", "HH:mm:ss.SSSSSSSSS".
   *
   * <p>The format used will be the shortest that outputs the full value of the time where the
   * omitted parts are implied to be zero.
   */
  public static String localTimeToString(LocalTime time, String emptyTimeString) {
    return (time == null) ? emptyTimeString : time.toString();
  }
} public interface TimeChangeListener {

  /**
   * timeChanged, This function will be called whenever the time in the applicable time picker has
   * changed. Both the old time, and the new time, are supplied in the event object. Note that
   * either parameter may contain null, which represents a cleared or empty time.
   */
  public void timeChanged(TimeChangeEvent event);
} public interface TimeVetoPolicy {

  /**
   * isTimeAllowed, Implement this function to indicate which times are allowed, and which ones are
   * vetoed. A vetoed time cannot be selected by typing in the time manually, and cannot be selected
   * by using the mouse. Also, vetoed times will not be added to the time drop down menu. Return
   * true to indicate that a time is allowed, or return false to indicate that a time is vetoed.
   *
   * <p>To disallow empty times, set "TimePickerSettings.allowEmptyTimes" to false.
   *
   * <p>The value of null will never be passed to this function, under any case.
   */
  public boolean isTimeAllowed(LocalTime time);
} public class CalculateMinimumTimeFieldSize {

  /**
   * getFormattedDateWidthInPixels, This returns the width (in pixels) of the longest formatted
   * time, using the supplied DateTimeFormatter instance and font. Note that the locale information
   * is built into the display format.
   *
   * <p>You may optionally add extra characters to the longestTimeString that is used in the
   * calculation, by supplying a nonzero value for the parameter numberOfExtraCharacters. This
   * parameter can also be used with a negative value to reduce the size that is returned.
   */
  public static int getFormattedTimeWidthInPixels(
      DateTimeFormatter formatForDisplayTime, Font fontValidTime, int numberOfExtraCharacters) {
    // Create the font metrics that will be used in the calculation.
    JTextField textField = new JTextField();
    FontMetrics fontMetrics = textField.getFontMetrics(fontValidTime);
    // Create the longest possible time.
    LocalTime longestTime = LocalTime.of(22, 59, 59, 999999999);
    // Generate the longest time string. Note, The locale is built into the formatter instance.
    String longestTimeString = longestTime.format(formatForDisplayTime);
    // Get the width of the longest time string (in pixels), using the supplied font metrics.
    int longestTimeWidth = fontMetrics.stringWidth(longestTimeString);
    // Add space for two characters.
    int singleNumericCharacterWidth = fontMetrics.stringWidth("8");
    longestTimeWidth += (2 * singleNumericCharacterWidth);
    // Don't return return anything less than 50 pixels by default.
    longestTimeWidth = (longestTimeWidth < 50) ? 50 : longestTimeWidth;
    // If requested, pad the result with space for any (programmer specified) extra characters.
    longestTimeWidth += (numberOfExtraCharacters * singleNumericCharacterWidth);
    // Return the width of the longest formatted time, in pixels.
    return longestTimeWidth;
  }
} public class CustomPopup extends Popup implements WindowFocusListener, ComponentListener {

  /**
   * displayWindow, This is the visible window that is used with this popup. The "Popup" class does
   * not provide its own visible component. A visible component must be supplied by the
   * implementation. This is set to null in the hide() function.
   */
  private JWindow displayWindow;

  /**
   * CustomPopupCloseListener, If this has been set to something besides null, then this listener
   * will be notified whenever this popup is closed, regardless of whether that close was initiated
   * internally or externally.
   */
  private CustomPopupCloseListener optionalCustomPopupCloseListener;

  /**
   * topWindow, This is a reference to the top window of the component which initiated the popup.
   * This is used for registering and deregistering the window movement listener with the topWindow.
   * If the top window moves, the popup is closed. This is set to null in the hide() function
   */
  private Window topWindow;

  /**
   * enableHideWhenFocusIsLost, This is used to prevent a previous intermittent bug where blank
   * popup windows could occur in linux. This is set to true during the "WindowOpen" event. This is
   * never set to false after initialization. This is checked whenever the window loses focus. The
   * window can only be hidden when the focus is lost, if this has been set to true.
   */
  private boolean enableHideWhenFocusIsLost = false;

  /**
   * Constructor, This creates and initializes instances of this class.
   *
   * @param contentsComponent This is the component that you wish to display inside this popup.
   * @param topWindow When the window that surrounds a popup is moved, the popup will be
   *     automatically closed. The topWindow is the window that should be watched for movement. The
   *     window that is needed is usually the top window of the component hierarchy, of the
   *     component that initiated the popup. The function SwingUtilities.getWindowAncestor() can be
   *     useful for getting the topWindow.
   * @param optionalCustomPopupCloseListener If this is supplied, it will be notified when the
   *     hide() function is called on this popup. This will occur regardless of whether the hide()
   *     function was called internally or externally.
   * @param optionalBorder If this is supplied, it will be used as the border for the popup window.
   *     If no border is supplied, then a default border will be used.
   */
  public CustomPopup(
      Component contentsComponent,
      Window topWindow,
      CustomPopupCloseListener optionalCustomPopupCloseListener,
      Border optionalBorder) {
    // Call the constructor of the ancestor Popup class.
    super();
    // Save the initialization variables for future use.
    this.topWindow = topWindow;
    this.optionalCustomPopupCloseListener = optionalCustomPopupCloseListener;
    // Create the panel that will be added to the display window.
    JPanel mainPanel = new JPanel();
    mainPanel.setLayout(new BorderLayout());
    mainPanel.add(contentsComponent, BorderLayout.CENTER);
    // Add the appropriate border to the main panel.
    if (optionalBorder == null) {
      // This creates and uses a default popup border.
      // The design of this border was based on the JPopupMenu border, but this border
      // behaves better. It does not show any of the main panel through transparent areas.
      Border outsideBorder = new LineBorder(new Color(99, 130, 191));
      Border insideBorder = BorderFactory.createMatteBorder(1, 0, 0, 0, Color.white);
      Border compoundBorder = BorderFactory.createCompoundBorder(outsideBorder, insideBorder);
      mainPanel.setBorder(compoundBorder);
    } else {
      // This uses the supplied border.
      mainPanel.setBorder(optionalBorder);
    }
    // Create the display window.
    displayWindow = new JWindow(topWindow);
    // This is part of the bug fix for blank popup windows in linux.
    displayWindow.addWindowListener(
        new WindowAdapter() {
          @Override
          public void windowOpened(WindowEvent e) {
            enableHideWhenFocusIsLost = true;
          }
        });

    displayWindow.getContentPane().add(mainPanel);
    displayWindow.setFocusable(true);

    // Bug Fix for Github Issue #49
    // "Date selection and navigation not working on Mac OS X 10.12.4 under Java 8 update 121."
    displayWindow.setAlwaysOnTop(true);

    displayWindow.pack();
    displayWindow.validate();
    // Add the action that is needed to close the popup when the escape key is pressed.
    String cancelName = "cancel";
    InputMap inputMap = mainPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelName);
    ActionMap actionMap = mainPanel.getActionMap();
    actionMap.put(
        cancelName,
        new AbstractAction() {
          @Override
          public void actionPerformed(ActionEvent e) {
            hide();
          }
        });
    // Register this class as a listener with the appropriate components.
    // De-registration is handled in the hide() function.
    registerListeners();
  }

  /**
   * componentHidden, Part of ComponentListener. Whenever the topWindow is hidden, the popup will be
   * hidden.
   */
  @Override
  public void componentHidden(ComponentEvent e) {
    hide();
  }

  /**
   * componentMoved, Part of ComponentListener. Whenever the topWindow is moved, the popup will be
   * hidden.
   */
  @Override
  public void componentMoved(ComponentEvent e) {
    hide();
  }

  /**
   * componentResized, Part of ComponentListener. Whenever the topWindow is resized, the popup will
   * be hidden.
   */
  @Override
  public void componentResized(ComponentEvent e) {
    hide();
  }

  /**
   * componentShown, Part of ComponentListener. This does nothing, it is included because all
   * listener functions must be implemented.
   */
  @Override
  public void componentShown(ComponentEvent e) {
    // Do nothing here.
  }

  /**
   * getBounds, This returns the bounds of the CustomPopup displayWindow in the form of a Rectangle
   * object. The bounds specify this component's width, height, and location relative to its parent.
   */
  public Rectangle getBounds() {
    return displayWindow.getBounds();
  }

  /**
   * hide, This hides the popup window. This removes this class from the list of window focus
   * listeners for the popup window, and removes this class from the list of window movement
   * listeners for the top window. This can be called internally or externally. If this is called
   * multiple times, then only the first call will have an effect.
   */
  @Override
  public void hide() {
    if (displayWindow != null) {
      displayWindow.setVisible(false);
      displayWindow.removeWindowFocusListener(this);
      displayWindow = null;
    }
    if (topWindow != null) {
      topWindow.removeComponentListener(this);
      topWindow = null;
    }
    if (optionalCustomPopupCloseListener != null) {
      optionalCustomPopupCloseListener.zEventCustomPopupWasClosed(this);
      optionalCustomPopupCloseListener = null;
    }
  }

  /**
   * registerListeners, This function registers this class as a listener with the appropriate
   * components. De-registration is handled in the hide() function.
   */
  private void registerListeners() {
    // Register this class as a focus listener with the display window.
    displayWindow.addWindowFocusListener(this);
    // Register this class as a window movement listener with the top window.
    topWindow.addComponentListener(this);
  }

  /** setLocation, This changes the location of the popup window. */
  public void setLocation(int popupX, int popupY) {
    displayWindow.setLocation(popupX, popupY);
  }

  /** show, This shows the visible component of the popup window. */
  @Override
  public void show() {
    displayWindow.setVisible(true);
  }

  /**
   * windowGainedFocus, Part of WindowFocusListener. This does nothing, it is included because all
   * listener functions must be implemented.
   */
  @Override
  public void windowGainedFocus(WindowEvent e) {
    // Do nothing here.
  }

  /**
   * windowLostFocus, Part of WindowFocusListener. Whenever the popup window loses focus, it will be
   * hidden.
   */
  @Override
  public void windowLostFocus(WindowEvent e) {
    // This section is part of the bug fix for blank popup windows in linux.
    if (!enableHideWhenFocusIsLost) {
      e.getWindow().requestFocus();
      return;
    }
    // This fixes a linux-specific behavior where the focus can be "lost" by clicking a child
    // component (inside the same panel!).
    if (InternalUtilities.isMouseWithinComponent(displayWindow)) {
      return;
    }
    hide();
  }

  public void setMinimumSize(Dimension minimumSize) {
    displayWindow.setMinimumSize(minimumSize);
  }

  /**
   * CustomPopupCloseListener, Any class that uses a CustomPopup (or any other class), may implement
   * this interface to be notified when the CustomPopup is closed. The implementing class should
   * pass itself into the CustomPopup constructor.
   */
  public static interface CustomPopupCloseListener {

    /**
     * zEventCustomPopupWasClosed, This will be called whenever the CustomPopup is closed, either
     * internally or externally. More specifically, this is called by the CustomPopup.hide()
     * function.
     */
    public void zEventCustomPopupWasClosed(CustomPopup popup);
  }
} public class InternalUtilities {

  /**
   * areObjectsEqual, This function exists as a workaround for the fact that Objects.equals() did
   * not exist in Java 1.6.
   *
   * <p>Returns {@code true} if the arguments are equal to each other and {@code false} otherwise.
   * Consequently, if both arguments are {@code null}, {@code true} is returned and if exactly one
   * argument is {@code null}, {@code false} is returned. Otherwise, equality is determined by using
   * the {@link Object#equals equals} method of the first argument.
   *
   * @param a an object
   * @param b an object to be compared with {@code a} for equality
   * @return {@code true} if the arguments are equal to each other and {@code false} otherwise
   * @see Object#equals(Object)
   */
  public static boolean areObjectsEqual(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
  }

  /**
   * doesParsedDateMatchText, This compares the numbers in a parsed date, to the original text from
   * which the date was parsed. Specifically this compares the day of the month and the year of the
   * parsed date to the text.
   *
   * <p>Testing note: Invalid dates this function should detect are any of the following: The 31st
   * day of February, April, June, September, or November. The 30th day of February. Or the 29th day
   * of February on any year that is not a leap year.
   */
  public static boolean doesParsedDateMatchText(
      LocalDate parsedDate, String text, DateTimeFormatter usedFormatter) {
    if (parsedDate == null || text == null) {
      return false;
    }

    final TemporalAccessor parseResult = usedFormatter.parseUnresolved(text, new ParsePosition(0));

    if (parseResult.isSupported(ChronoField.YEAR)) {
      if (parseResult.get(ChronoField.YEAR) != parsedDate.get(ChronoField.YEAR)) {
        return false;
      }
    }
    if (parseResult.isSupported(ChronoField.YEAR_OF_ERA)) {
      if (parseResult.get(ChronoField.YEAR_OF_ERA) != parsedDate.get(ChronoField.YEAR_OF_ERA)) {
        return false;
      }
    }
    if (parseResult.isSupported(ChronoField.DAY_OF_MONTH)) {
      if (parseResult.get(ChronoField.DAY_OF_MONTH) != parsedDate.get(ChronoField.DAY_OF_MONTH)) {
        return false;
      }
    }
    if (parseResult.isSupported(ChronoField.ERA)) {
      if (parseResult.get(ChronoField.ERA) != parsedDate.get(ChronoField.ERA)) {
        return false;
      }
    }

    return true;
  }

  /** getJavaRunningVersionAsDouble, Returns a double with the currently running java version. */
  public static double getJavaRunningVersionAsDouble() {
    String version = System.getProperty("java.version");
    int pos = version.indexOf('.');
    pos = version.indexOf('.', pos + 1);
    return Double.parseDouble(version.substring(0, pos));
  }

  /** getJavaRunningVersionAsString, Returns a string with the currently running java version. */
  public static String getJavaRunningVersionAsString() {
    String version = System.getProperty("java.version");
    return version;
  }

  /**
   * getJavaTargetVersionFromPom, Returns a string with the java "target" version, as it was
   * specified in the pom file at compile time.
   */
  public static String getJavaTargetVersionFromPom() {
    try {
      Properties properties = new Properties();
      ClassLoader classLoader = ClassLoader.getSystemClassLoader();
      properties.load(classLoader.getResourceAsStream("project.properties"));
      return properties.getProperty("targetJavaVersion");
    } catch (Exception ex) {
      return "";
    }
  }

  /**
   * getMostCommonElementInList, This returns the most common element in the supplied list. In the
   * event of a tie, any element that is tied as the "largest element" may be returned. If the list
   * has no elements, or if the list is null, then this will return null. This can also return null
   * if null happens to be the most common element in the source list.
   */
  public static <T> T getMostCommonElementInList(List<T> sourceList) {
    if (sourceList == null || sourceList.isEmpty()) {
      return null;
    }
    Map<T, Integer> hashMap = new HashMap<>();
    for (T element : sourceList) {
      Integer countOrNull = hashMap.get(element);
      int newCount = (countOrNull == null) ? 1 : (countOrNull + 1);
      hashMap.put(element, newCount);
    }
    // Find the largest entry.
    // In the event of a tie, the first entry (the first entry in the hash map, not in the list)
    // with the maximum count will be returned.
    Entry<T, Integer> largestEntry = null;
    for (Entry<T, Integer> currentEntry : hashMap.entrySet()) {
      if (largestEntry == null || currentEntry.getValue() > largestEntry.getValue()) {
        largestEntry = currentEntry;
      }
    }
    T result = (largestEntry == null) ? null : largestEntry.getKey();
    return result;
  }

  /** getProjectVersionString, Returns a string with the project version number. */
  public static String getProjectVersionString() {
    try {
      Properties properties = new Properties();
      ClassLoader classLoader = ClassLoader.getSystemClassLoader();
      properties.load(classLoader.getResourceAsStream("project.properties"));
      return "v" + properties.getProperty("projectVersion");
    } catch (Exception ex) {
      return "";
    }
  }

  /**
   * getScreenInsets, This returns the insets of the screen, which are defined by any task bars that
   * have been set up by the user. This function accounts for multi-monitor setups. If a window is
   * supplied, then the the monitor that contains the window will be used. If a window is not
   * supplied, then the primary monitor will be used.
   */
  public static Insets getScreenInsets(Window windowOrNull) {
    Insets insets;
    if (windowOrNull == null) {
      insets =
          Toolkit.getDefaultToolkit()
              .getScreenInsets(
                  GraphicsEnvironment.getLocalGraphicsEnvironment()
                      .getDefaultScreenDevice()
                      .getDefaultConfiguration());
    } else {
      insets = windowOrNull.getToolkit().getScreenInsets(windowOrNull.getGraphicsConfiguration());
    }
    return insets;
  }

  /**
   * getScreenTotalArea, This returns the total area of the screen. (The total area includes any
   * task bars.) This function accounts for multi-monitor setups. If a window is supplied, then the
   * the monitor that contains the window will be used. If a window is not supplied, then the
   * primary monitor will be used.
   */
  public static Rectangle getScreenTotalArea(Window windowOrNull) {
    Rectangle bounds;
    if (windowOrNull == null) {
      GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
      bounds = ge.getDefaultScreenDevice().getDefaultConfiguration().getBounds();
    } else {
      GraphicsConfiguration gc = windowOrNull.getGraphicsConfiguration();
      bounds = gc.getBounds();
    }
    return bounds;
  }

  /**
   * getScreenWorkingArea, This returns the working area of the screen. (The working area excludes
   * any task bars.) This function accounts for multi-monitor setups. If a window is supplied, then
   * the the monitor that contains the window will be used. If a window is not supplied, then the
   * primary monitor will be used.
   */
  public static Rectangle getScreenWorkingArea(Window windowOrNull) {
    Insets insets;
    Rectangle bounds;
    if (windowOrNull == null) {
      GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
      insets =
          Toolkit.getDefaultToolkit()
              .getScreenInsets(ge.getDefaultScreenDevice().getDefaultConfiguration());
      bounds = ge.getDefaultScreenDevice().getDefaultConfiguration().getBounds();
    } else {
      GraphicsConfiguration gc = windowOrNull.getGraphicsConfiguration();
      insets = windowOrNull.getToolkit().getScreenInsets(gc);
      bounds = gc.getBounds();
    }
    bounds.x += insets.left;
    bounds.y += insets.top;
    bounds.width -= (insets.left + insets.right);
    bounds.height -= (insets.top + insets.bottom);
    return bounds;
  }

  /**
   * generateDefaultFormatterCE, This returns a default formatter for the specified locale, that can
   * be used for displaying or parsing AD dates. The formatter is generated from the default
   * FormatStyle.LONG formatter in the specified locale.
   */
  public static DateTimeFormatter generateDefaultFormatterCE(Locale pickerLocale) {
    DateTimeFormatter formatCE =
        new DateTimeFormatterBuilder()
            .parseLenient()
            .parseCaseInsensitive()
            .appendLocalized(FormatStyle.LONG, null)
            .toFormatter(pickerLocale);
    // Get the local language as a string.
    String language = pickerLocale.getLanguage();
    // Override the format for the turkish locale to remove the name of the weekday.
    if ("tr".equals(language)) {
      formatCE = PickerUtilities.createFormatterFromPatternString("dd MMMM yyyy", pickerLocale);
    }
    // Return the generated format.
    return formatCE;
  }

  /**
   * generateDefaultFormatterBCE, This returns a default formatter for the specified locale, that
   * can be used for displaying or parsing BC dates. The formatter is generated from the default
   * FormatStyle.LONG formatter in the specified locale. The resulting format is intended to be
   * nearly identical to the default formatter used for AD dates.
   */
  public static DateTimeFormatter generateDefaultFormatterBCE(Locale pickerLocale) {
    // This is verified to work for the following locale languages:
    // en, de, fr, pt, ru, it, nl, es, pl, da, ro, sv, zh.
    String displayFormatterBCPattern =
        DateTimeFormatterBuilder.getLocalizedDateTimePattern(
            FormatStyle.LONG, null, IsoChronology.INSTANCE, pickerLocale);
    displayFormatterBCPattern = displayFormatterBCPattern.replace("y", "u");
    // Note: We could have used DateUtilities.createFormatterFromPatternString(), which should
    // have the same formatter options as this line. We kept this code independent in case
    // anyone ever mistakenly changes that utility function.
    DateTimeFormatter displayFormatterBC =
        new DateTimeFormatterBuilder()
            .parseLenient()
            .parseCaseInsensitive()
            .appendPattern(displayFormatterBCPattern)
            .toFormatter(pickerLocale);
    // Get the local language as a string.
    String language = pickerLocale.getLanguage();
    // Override the format for the turkish locale to remove the name of the weekday.
    if ("tr".equals(language)) {
      displayFormatterBC =
          PickerUtilities.createFormatterFromPatternString("dd MMMM uuuu", pickerLocale);
    }
    return displayFormatterBC;
  }

  /**
   * getParsedDateOrNull, This takes text from the date picker text field, and tries to parse it
   * into a java.time.LocalDate instance. If the text cannot be parsed, this will return null.
   *
   * <p>Implementation note: The DateTimeFormatter parsing class was accepting invalid dates like
   * February 31st, and returning the last valid date of the month, like Feb 28. This could be seen
   * as an attempt to be lenient, but in the context of the date picker class it is considered a
   * mistake or a bug. There was no setting to disable that functionality. So, this function calls
   * another function called doesParsedDateMatchText(), to analyze and reject those kinds of
   * mistakes. If the parsed text does not match the day of the month (and year) of the parsed date,
   * then this function will return null.
   */
  public static LocalDate getParsedDateOrNull(
      String text,
      DateTimeFormatter displayFormatterAD,
      DateTimeFormatter displayFormatterBC,
      ArrayList<DateTimeFormatter> parsingFormatters) {
    if (text == null || text.trim().isEmpty()) {
      return null;
    }
    text = text.trim();
    DateTimeFormatter usedFormatter = null;
    LocalDate parsedDate = null;
    if (parsedDate == null) {
      try {
        parsedDate = LocalDate.parse(text, displayFormatterAD);
        usedFormatter = displayFormatterAD;
      } catch (Exception ex) {
      }
    }
    if (parsedDate == null) {
      try {
        // Note: each parse attempt must have its own try/catch block.
        parsedDate = LocalDate.parse(text, displayFormatterBC);
        usedFormatter = displayFormatterBC;
      } catch (Exception ex) {
      }
    }
    for (int i = 0; ((parsedDate == null) && (i < parsingFormatters.size())); ++i) {
      try {
        parsedDate = LocalDate.parse(text, parsingFormatters.get(i));
        usedFormatter = parsingFormatters.get(i);
      } catch (Exception ex) {
      }
    }
    // Check for any "successfully" parsed but nonexistent dates like Feb 31.
    // Note, this function has been thoroughly tested. See the function docs for details.
    if ((parsedDate != null)
        && (!InternalUtilities.doesParsedDateMatchText(parsedDate, text, usedFormatter))) {
      return null;
    }
    return parsedDate;
  }

  public static LocalTime getParsedTimeOrNull(
      String timeText,
      DateTimeFormatter formatForDisplayTime,
      DateTimeFormatter formatForMenuTimes,
      ArrayList<DateTimeFormatter> formatsForParsing,
      Locale timePickerLocale) {
    if (timeText == null || timeText.trim().isEmpty()) {
      return null;
    }
    timeText = timeText.trim().toLowerCase();
    LocalTime parsedTime = null;
    if (parsedTime == null) {
      try {
        parsedTime = LocalTime.parse(timeText, formatForDisplayTime);
      } catch (Exception ex) {
      }
    }
    if (parsedTime == null) {
      try {
        // Note: each parse attempt must have its own try/catch block.
        parsedTime = LocalTime.parse(timeText, formatForMenuTimes);
      } catch (Exception ex) {
      }
    }
    for (int i = 0; ((parsedTime == null) && (i < formatsForParsing.size())); ++i) {
      try {
        parsedTime = LocalTime.parse(timeText, formatsForParsing.get(i));
      } catch (Exception ex) {
      }
    }
    return parsedTime;
  }

  /**
   * capitalizeFirstLetterOfString, This capitalizes the first letter of the supplied string, in a
   * way that is sensitive to the specified locale.
   */
  static String capitalizeFirstLetterOfString(String text, Locale locale) {
    if (text == null || text.length() < 1) {
      return text;
    }
    String textCapitalized = text.substring(0, 1).toUpperCase(locale) + text.substring(1);
    return textCapitalized;
  }

  /**
   * getConstraints, This returns a grid bag constraints object that can be used for placing a
   * component appropriately into a grid bag layout.
   */
  public static GridBagConstraints getConstraints(int gridx, int gridy) {
    GridBagConstraints gc = new GridBagConstraints();
    gc.fill = GridBagConstraints.BOTH;
    gc.gridx = gridx;
    gc.gridy = gridy;
    return gc;
  }

  /**
   * isDateVetoed, This is a convenience function for checking whether or not a particular date is
   * vetoed. Note that veto policies do not have any say about null dates, so this function always
   * returns false for null dates.
   */
  public static boolean isDateVetoed(DateVetoPolicy policy, LocalDate date) {
    if (policy == null || date == null) {
      return false;
    }
    return (!policy.isDateAllowed(date));
  }

  /**
   * isMouseWithinComponent, This returns true if the mouse is inside of the specified component,
   * otherwise returns false.
   */
  public static boolean isMouseWithinComponent(Component component) {
    if (!component.isVisible()) {
      // component.getLocationOnScreen() will throw an
      // IllegalComponentStateException if the component
      // is currently not visible
      return false;
    }
    Point mousePos = MouseInfo.getPointerInfo().getLocation();
    Rectangle bounds = component.getBounds();
    bounds.setLocation(component.getLocationOnScreen());
    return bounds.contains(mousePos);
  }

  public static boolean isTimeVetoed(TimeVetoPolicy policy, LocalTime time) {
    if (policy == null) {
      return false;
    }
    return (!policy.isTimeAllowed(time));
  }

  /**
   * safeSubstring, This is a version of the substring function which is guaranteed to never throw
   * an exception. If the supplied string is null then this will return null. If the beginIndex or
   * endIndexExclusive are out of range for the string, then the indexes will be compressed to fit
   * within the bounds of the supplied string. If the beginIndex is greater than or equal to
   * endIndexExclusive, then this will return an empty string.
   */
  public static String safeSubstring(String text, int beginIndex, int endIndexExclusive) {
    if (text == null) {
      return null;
    }
    int textLength = text.length();
    if (beginIndex < 0) {
      beginIndex = 0;
    }
    if (endIndexExclusive < 0) {
      endIndexExclusive = 0;
    }
    if (endIndexExclusive > textLength) {
      endIndexExclusive = textLength;
    }
    if (beginIndex > endIndexExclusive) {
      beginIndex = endIndexExclusive;
    }
    if (beginIndex == endIndexExclusive) {
      return "";
    }
    return text.substring(beginIndex, endIndexExclusive);
  }

  /**
   * getCompiledJavaVersionFromJavaClassFile, Given an input stream to a Java class file, this will
   * return the major or minor version of Java that was used to compile the file. In a Maven POM
   * file, this is known as the "target" version of Java that was used to compile the file.
   *
   * <p>Getting an input stream for a class file inside a jar file:
   *
   * <p>InputStream input = getClass().getResourceAsStream("/classpath/to/my/file.class");
   */
  public static int getCompiledJavaVersionFromJavaClassFile(
      InputStream classByteStream, boolean majorVersionRequested) throws Exception {
    DataInputStream dataInputStream = new DataInputStream(classByteStream);
    // Skip the "magic number".
    dataInputStream.readInt();
    int minorVersion = dataInputStream.readUnsignedShort();
    int majorVersion = dataInputStream.readUnsignedShort();
    return (majorVersionRequested) ? majorVersion : minorVersion;
  }

  /**
   * getCompiledJavaMajorVersionFromJavaClassFileAsString, Given an input stream to a Java class
   * file, this will return the major version of Java that was used to compile the file (as a
   * string). In a Maven POM file, this is known as the "target" version of Java that was used to
   * compile the file.
   *
   * <p>Getting an input stream for a class file inside a jar file:
   *
   * <p>InputStream input = getClass().getResourceAsStream("/classpath/to/my/file.class");
   */
  public static String getCompiledJavaMajorVersionFromJavaClassFileAsString(
      InputStream classByteStream) throws Exception {
    int majorVersion = getCompiledJavaVersionFromJavaClassFile(classByteStream, true);
    switch (majorVersion) {
      case 50:
        return "Java 6";
      case 51:
        return "Java 7";
      case 52:
        return "Java 8";
      case 53:
        return "Java 9";
      case 54:
        return "Java 10";
      case 55:
        return "Java 11";
      default:
        return "Could not find version string for major version: " + majorVersion;
    }
  }

  /**
   * setDefaultTableEditorsClicks, This sets the number of clicks required to start the default
   * table editors in the supplied table. Typically you would set the table editors to start after 1
   * click or 2 clicks, as desired.
   *
   * <p>The default table editors of the table editors that are supplied by the JTable class, for
   * Objects, Numbers, and Booleans. Note, the editor which is used to edit Objects, is the same
   * editor used for editing Strings.
   */
  public static void setDefaultTableEditorsClicks(JTable table, int clickCountToStart) {
    TableCellEditor editor;
    editor = table.getDefaultEditor(Object.class);
    if (editor instanceof DefaultCellEditor) {
      ((DefaultCellEditor) editor).setClickCountToStart(clickCountToStart);
    }
    editor = table.getDefaultEditor(Number.class);
    if (editor instanceof DefaultCellEditor) {
      ((DefaultCellEditor) editor).setClickCountToStart(clickCountToStart);
    }
    editor = table.getDefaultEditor(Boolean.class);
    if (editor instanceof DefaultCellEditor) {
      ((DefaultCellEditor) editor).setClickCountToStart(clickCountToStart);
    }
  }
} public class TimeChangeEvent {

  /** Constructor. */
  public TimeChangeEvent(TimePicker source, LocalTime oldTime, LocalTime newTime) {
    this.source = source;
    this.oldTime = oldTime;
    this.newTime = newTime;
  }

  /** source, This is the time picker that generated the event. */
  private TimePicker source;

  /** oldTime, This holds the value of the TimePicker time, before the time changed. */
  private LocalTime oldTime;

  /** newTime, This holds the value of the TimePicker time, after the time changed. */
  private LocalTime newTime;

  /** getSource, Returns the time picker that generated the event. */
  public TimePicker getSource() {
    return source;
  }

  /**
   * getOldTime, Returns the previous value of the TimePicker time. This is the value that existed
   * before the time was changed.
   */
  public LocalTime getOldTime() {
    return oldTime;
  }

  /**
   * getNewTime, Returns the new value of the TimePicker time. This is the value that currently
   * exists. (Put in another way, this is the value that exists after the time was changed.)
   */
  public LocalTime getNewTime() {
    return newTime;
  }
} public class TimeMenuPanel extends JPanel {

  /**
   * minimumMouseReleaseFromToggleButtonMilliseconds, This is how long we should wait, before
   * allowing the time menu panel to be closed during a mouse release event that originates from the
   * toggle button. This is used to prevent accidental premature list closing when pressing the
   * toggle button and moving the mouse downwards. The mouse release event from the toggle button
   * will only be enabled after this amount of time has passed since construction.
   */
  private int minimumMouseReleaseFromToggleButtonMilliseconds = 400;

  /**
   * minimumTimeToEnableMouseReleaseFromToggleButton, This is the "currentTimeMillis()" value that
   * must be passed, to enable the mouse release event from the toggle button. This equals
   * ((System.currentTimeMillis() at construction) + minimumMouseReleaseCloseMilliSeconds). This is
   * calculated at the end of the constructor.
   */
  private long minimumTimeToEnableMouseReleaseFromToggleButton;

  /**
   * parentTimePicker, This holds our parent time picker instance. This is supplied at construction.
   */
  private TimePicker parentTimePicker;

  /** settings, This holds our time picker settings instance. This is supplied at construction. */
  private TimePickerSettings settings;

  /** timeListModel, The holds the list model that is used for populating the time list. */
  private final DefaultListModel<String> timeListModel;

  public TimeMenuPanel(TimePicker parentTimePicker, TimePickerSettings settings) {
    this.parentTimePicker = parentTimePicker;
    this.settings = settings;
    initComponents();
    timeListModel = new DefaultListModel<>();
    timeList.setModel(timeListModel);

    timeList.addMouseMotionListener(
        new MouseAdapter() {
          @Override
          public void mouseMoved(MouseEvent event) {
            Point mousePositionRelativeToComponent = new Point(event.getX(), event.getY());
            int index = timeList.locationToIndex(mousePositionRelativeToComponent);
            if ((index != -1) && (index != timeList.getSelectedIndex())) {
              timeList.setSelectedIndex(index);
            }
          }
        });
    timeList.addMouseListener(
        new MouseAdapter() {
          @Override
          public void mouseReleased(MouseEvent event) {
            mouseReleasedWhileTimeListIsOpen();
          }
        });
    timeList.addKeyListener(
        new KeyAdapter() {
          @Override
          public void keyReleased(KeyEvent event) {
            if (event.getKeyCode() == KeyEvent.VK_ENTER) {
              event.consume();
              String selectedTimeString = timeList.getSelectedValue();
              if (selectedTimeString != null) {
                userSelectedATime(selectedTimeString);
              }
            }
          }
        });
    // Generate the time entries for the drop down menu.
    generateTimeEntriesFromSettings();
    // Set the maximum number of visible menu rows to the appropriate value.
    int maximumMenuRows = Math.min(settings.maximumVisibleMenuRows, timeListModel.getSize());
    timeList.setVisibleRowCount(maximumMenuRows);

    // Calculate and save the minimum system time, at which to enable the mouse release from
    // toggle button event.
    minimumTimeToEnableMouseReleaseFromToggleButton =
        System.currentTimeMillis() + minimumMouseReleaseFromToggleButtonMilliseconds;
  }

  public void mouseDraggedFromToggleButton() {
    Point mousePositionRelativeToScreen = MouseInfo.getPointerInfo().getLocation();
    Rectangle timeListBounds = timeList.getBounds();
    timeListBounds.setLocation(timeList.getLocationOnScreen());
    if (timeListBounds.contains(mousePositionRelativeToScreen)) {
      SwingUtilities.convertPointFromScreen(mousePositionRelativeToScreen, timeList);
      Point mousePositionRelativeToComponent = (Point) mousePositionRelativeToScreen.clone();
      int index = timeList.locationToIndex(mousePositionRelativeToComponent);
      if ((index != -1) && (index != timeList.getSelectedIndex())) {
        timeList.setSelectedIndex(index);
      }
    }
  }

  public void mouseReleasedFromToggleButtonOutsideButton() {
    // Make sure that the window has been open long enough for the user to read (or see) the
    // time menu, before allowing the popup to be closed from a mouse release event that
    // originates from the toggle button.
    boolean enableMouseReleaseEventsFromToggleButton =
        (System.currentTimeMillis() > minimumTimeToEnableMouseReleaseFromToggleButton);

    // Check to see if mouse release events from the toggle button are enabled.
    if (!enableMouseReleaseEventsFromToggleButton) {
      return;
    }
    // Call the mouse release event funtion.
    mouseReleasedWhileTimeListIsOpen();
  }

  private void mouseReleasedWhileTimeListIsOpen() {
    // Get the mouse position and time list bounds.
    Point mousePositionRelativeToScreen = MouseInfo.getPointerInfo().getLocation();
    Rectangle timeListBounds = timeList.getBounds();
    timeListBounds.setLocation(timeList.getLocationOnScreen());
    // If the release happened outside of the list, close the popup and do nothing.
    if (!timeListBounds.contains(mousePositionRelativeToScreen)) {
      tryClosePopup();
      return;
    }
    Point mousePositionRelativeToComponent = timeList.getMousePosition();
    int index = timeList.locationToIndex(mousePositionRelativeToComponent);
    Rectangle cellBoundsOfIndex = timeList.getCellBounds(index, index);
    if (cellBoundsOfIndex == null
        || (!cellBoundsOfIndex.contains(mousePositionRelativeToComponent))) {
      tryClosePopup();
      return;
    }
    try {
      String selectedTimeString = timeList.getModel().getElementAt(index);
      if (selectedTimeString != null && !selectedTimeString.isEmpty()) {
        userSelectedATime(selectedTimeString);
      }
    } catch (Exception e) {
      throw new RuntimeException(
          "TimeMenuPanel.mouseReleasedWhileTimeListIsOpen() "
              + "Time menu list index is out of bounds. This should not happen. "
              + e.getMessage());
    }
  }

  public void clearParent() {
    parentTimePicker = null;
    settings = null;
  }

  final void generateTimeEntriesFromSettings() {
    timeListModel.clear();
    DateTimeFormatter formatForMenuTimes = settings.getFormatForMenuTimes();
    ArrayList<LocalTime> menuTimes = settings.getPotentialMenuTimes();
    for (LocalTime localTime : menuTimes) {
      if (!InternalUtilities.isTimeVetoed(settings.getVetoPolicy(), localTime)) {
        String localizedTime = formatForMenuTimes.format(localTime);
        if (settings.useLowercaseForMenuTimes) {
          localizedTime = localizedTime.toLowerCase(settings.getLocale());
        }
        timeListModel.addElement(localizedTime);
      }
    }
  }

  private void userSelectedATime(String selectedTimeString) {
    // Try to parse the selected time string.
    LocalTime selectedTime =
        InternalUtilities.getParsedTimeOrNull(
            selectedTimeString,
            settings.getFormatForDisplayTime(),
            settings.getFormatForMenuTimes(),
            settings.formatsForParsing,
            settings.getLocale());
    // Check to see if the time was parsed. The time should always parse successfully.
    if (selectedTime == null) {
      throw new RuntimeException(
          "TimeMenuPanel, " + "Could not parse menu time. This should not happen.");
    }

    // If the selected time is vetoed, do nothing.
    TimeVetoPolicy vetoPolicy = settings.getVetoPolicy();
    if (InternalUtilities.isTimeVetoed(vetoPolicy, selectedTime)) {
      return;
    }
    // We close the popup after the user selects a time.
    if (parentTimePicker != null) {
      parentTimePicker.setTime(selectedTime);
      parentTimePicker.closePopup();
    }
  }

  private void initComponents() {
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
    timeScrollPane = new JScrollPane();
    timeList = new JList<>();

    // ======== this ========
    setBorder(null);
    setLayout(new FormLayout("default:grow", "fill:default:grow"));

    // ======== timeScrollPane ========
    {
      timeScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

      // ---- timeList ----
      timeList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
      timeList.setVisibleRowCount(10);
      timeScrollPane.setViewportView(timeList);
    }
    add(timeScrollPane, CC.xy(1, 1));
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
  }

  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
  private JScrollPane timeScrollPane;
  private JList<String> timeList;
  // JFormDesigner - End of variables declaration  //GEN-END:variables

  private void tryClosePopup() {
    if (parentTimePicker != null) {
      parentTimePicker.closePopup();
    }
  }

  public void requestListFocus() {
    timeList.requestFocusInWindow();
  }

  public void selectFirstEntry() {
    if (timeList.getModel().getSize() > 0) {
      timeList.setSelectedIndex(0);
      timeList.requestFocusInWindow();
    }
  }
} public class TimeSpinnerTimer {

  /** timePicker, This holds the time picker that is associated with this class. */
  private final TimePicker timePicker;

  /**
   * timePicker, This holds the number of minutes that should be added or subtracted with each
   * increment of the time picker value. This will typically be -1 or 1.
   */
  private final int changeAmountMinutes;

  /** timer, This holds the timer associated with this class. */
  private final Timer timer;

  /**
   * startDelayMillis, This indicates how long the timer should wait before firing its first tick.
   * This value is used to make sure that the user can easily increase or decrease the date picker
   * value by only 1 minute.
   */
  private final int startDelayMillis = 700;
  /**
   * timerRate, This indicates how often the timer should call the tick function, in milliseconds.
   */
  private final int timerRate = 20;
  /**
   * millisForDivisorList, This indicates how long each value in the divisorList should be used,
   * before moving onto the next value in the divisorList.
   */
  private final int[] millisForDivisorList = {1800, 900, 400, 400, 400, 400, 400, 0};
  /**
   * divisorList, For as long as any particular index in this array remains in effect, the currently
   * used number indicates how many tick calls should pass before the time picker value should be
   * changed. For example, the number 3 indicates that the time picker value should be changed only
   * once for every 3 calls to the tick function. Higher numbers make the spinner change slower,
   * lower numbers make the spinner change faster.
   */
  private final int[] divisorList = {12, 10, 8, 6, 4, 3, 2, 1};
  /**
   * startedIndexTimeStamp, This indicates the time that the currently used index in the divisorList
   * started to be used.
   */
  private long startedIndexTimeStamp = 0;
  /**
   * currentIndex, This indicates the index that is currently in effect for the divisorList and the
   * millisForIndexList.
   */
  private int currentIndex = 0;
  /**
   * ticksSinceIndexChange, This keeps track of the number of ticks that has passed since the last
   * time that the current index was changed. This helps us calculate when we need to change the
   * index to the next value.
   */
  private int ticksSinceIndexChange;

  /** Constructor. */
  public TimeSpinnerTimer(TimePicker timePicker, int changeAmountMinutes) {
    this.timePicker = timePicker;
    this.changeAmountMinutes = changeAmountMinutes;
    timer = new Timer(timerRate, event -> tick());
    timer.setInitialDelay(startDelayMillis);
  }

  /**
   * tick, This is called once each time that the timer fires. (Every 20 milliseconds).
   *
   * <p>However, the value in the time picker will only be changed once for every certain number of
   * calls to the tick() function. The number of calls which is required is controlled by the
   * "divisorList" array values.
   *
   * <p>The amount of time that is spent using each divisorList array value is controlled by the
   * "millisForIndexList".
   */
  private void tick() {
    if (startedIndexTimeStamp == 0) {
      startedIndexTimeStamp = System.currentTimeMillis();
    }
    long timeElapsedSinceIndexStartMilliseconds =
        System.currentTimeMillis() - startedIndexTimeStamp;
    int maximumIndex = divisorList.length - 1;
    int currentDivisor = divisorList[currentIndex];
    if (ticksSinceIndexChange % currentDivisor == 0) {
      timePicker.zInternalTryChangeTimeByIncrement(changeAmountMinutes);
      // System.out.println("\nMillis: " + System.currentTimeMillis());
      // System.out.println("Divisor: " + divisor);
      // System.out.println("Value: " + timePicker.getTime());
      if ((currentIndex < maximumIndex)
          && (timeElapsedSinceIndexStartMilliseconds > millisForDivisorList[currentIndex])) {
        ticksSinceIndexChange = 0;
        ++currentIndex;
        startedIndexTimeStamp = System.currentTimeMillis();
      }
    }
    ++ticksSinceIndexChange;
  }

  /** stop, This is called to stop the timer. */
  public void stop() {
    timer.stop();
  }

  /** start, This is called to start the timer, and to initialize the needed variables. */
  public void start() {
    startedIndexTimeStamp = 0;
    currentIndex = 0;
    ticksSinceIndexChange = 0;
    timer.start();
  }
} public final class CC implements Cloneable, Serializable {

  // Constants ************************************************************
  public static final Alignment DEFAULT = CellConstraints.DEFAULT;
  public static final Alignment FILL = CellConstraints.FILL;
  public static final Alignment LEFT = CellConstraints.LEFT;
  public static final Alignment RIGHT = CellConstraints.RIGHT;
  public static final Alignment CENTER = CellConstraints.CENTER;
  public static final Alignment TOP = CellConstraints.TOP;
  public static final Alignment BOTTOM = CellConstraints.BOTTOM;

  // Setters with Column-Row Order ******************************************
  /**
   * Sets column and row origins; sets width and height to 1; uses the default alignments.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xy(1, 1);
   * CC.xy(1, 3);
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @return this
   */
  public static CellConstraints xy(int col, int row) {
    return xywh(col, row, 1, 1);
  }

  /**
   * Sets column and row origins; sets width and height to 1; decodes horizontal and vertical
   * alignments from the given string.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xy(1, 3, "left, bottom");
   * CC.xy(1, 3, "l, b");
   * CC.xy(1, 3, "center, fill");
   * CC.xy(1, 3, "c, f");
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @param encodedAlignments describes the horizontal and vertical alignments
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints xy(int col, int row, String encodedAlignments) {
    return xywh(col, row, 1, 1, encodedAlignments);
  }

  /**
   * Sets the column and row origins; sets width and height to 1; set horizontal and vertical
   * alignment using the specified objects.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xy(1, 3, CellConstraints.LEFT,   CellConstraints.BOTTOM);
   * CC.xy(1, 3, CellConstraints.CENTER, CellConstraints.FILL);
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @param colAlign horizontal component alignment
   * @param rowAlign vertical component alignment
   * @return this
   */
  public static CellConstraints xy(int col, int row, Alignment colAlign, Alignment rowAlign) {
    return xywh(col, row, 1, 1, colAlign, rowAlign);
  }

  /**
   * Sets the column, row, width, and height; uses a height (row span) of 1 and the horizontal and
   * vertical default alignments.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xyw(1, 3, 7);
   * CC.xyw(1, 3, 2);
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @param colSpan the column span or grid width
   * @return this
   */
  public static CellConstraints xyw(int col, int row, int colSpan) {
    return xywh(col, row, colSpan, 1, CellConstraints.DEFAULT, CellConstraints.DEFAULT);
  }

  /**
   * Sets the column, row, width, and height; decodes the horizontal and vertical alignments from
   * the given string. The row span (height) is set to 1 .
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xyw(1, 3, 7, "left, bottom");
   * CC.xyw(1, 3, 7, "l, b");
   * CC.xyw(1, 3, 2, "center, fill");
   * CC.xyw(1, 3, 2, "c, f");
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @param colSpan the column span or grid width
   * @param encodedAlignments describes the horizontal and vertical alignments
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints xyw(int col, int row, int colSpan, String encodedAlignments) {
    return xywh(col, row, colSpan, 1, encodedAlignments);
  }

  /**
   * Sets the column, row, width, and height; sets the horizontal and vertical alignment using the
   * specified alignment objects. The row span (height) is set to 1 .
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xyw(1, 3, 2, CellConstraints.LEFT,   CellConstraints.BOTTOM);
   * CC.xyw(1, 3, 7, CellConstraints.CENTER, CellConstraints.FILL);
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @param colSpan the column span or grid width
   * @param colAlign horizontal component alignment
   * @param rowAlign vertical component alignment
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints xyw(
      int col, int row, int colSpan, Alignment colAlign, Alignment rowAlign) {
    return xywh(col, row, colSpan, 1, colAlign, rowAlign);
  }

  /**
   * Sets the column, row, width, and height; uses default alignments.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xywh(1, 3, 2, 1);
   * CC.xywh(1, 3, 7, 3);
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @param colSpan the column span or grid width
   * @param rowSpan the row span or grid height
   * @return this
   */
  public static CellConstraints xywh(int col, int row, int colSpan, int rowSpan) {
    return xywh(col, row, colSpan, rowSpan, CellConstraints.DEFAULT, CellConstraints.DEFAULT);
  }

  /**
   * Sets the column, row, width, and height; decodes the horizontal and vertical alignments from
   * the given string.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xywh(1, 3, 2, 1, "left, bottom");
   * CC.xywh(1, 3, 2, 1, "l, b");
   * CC.xywh(1, 3, 7, 3, "center, fill");
   * CC.xywh(1, 3, 7, 3, "c, f");
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @param colSpan the column span or grid width
   * @param rowSpan the row span or grid height
   * @param encodedAlignments describes the horizontal and vertical alignments
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints xywh(
      int col, int row, int colSpan, int rowSpan, String encodedAlignments) {
    return new CellConstraints().xywh(col, row, colSpan, rowSpan, encodedAlignments);
  }

  /**
   * Sets the column, row, width, and height; sets the horizontal and vertical alignment using the
   * specified alignment objects.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.xywh(1, 3, 2, 1, CellConstraints.LEFT,   CellConstraints.BOTTOM);
   * CC.xywh(1, 3, 7, 3, CellConstraints.CENTER, CellConstraints.FILL);
   * </pre>
   *
   * @param col the new column index
   * @param row the new row index
   * @param colSpan the column span or grid width
   * @param rowSpan the row span or grid height
   * @param colAlign horizontal component alignment
   * @param rowAlign vertical component alignment
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints xywh(
      int col, int row, int colSpan, int rowSpan, Alignment colAlign, Alignment rowAlign) {
    return new CellConstraints(col, row, colSpan, rowSpan, colAlign, rowAlign);
  }

  // Setters with Row-Column Order ******************************************
  /**
   * Sets row and column origins; sets height and width to 1; uses the default alignments.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rc(1, 1);
   * CC.rc(3, 1);
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @return this
   */
  public static CellConstraints rc(int row, int col) {
    return rchw(row, col, 1, 1);
  }

  /**
   * Sets row and column origins; sets height and width to 1; decodes vertical and horizontal
   * alignments from the given string.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rc(3, 1, "bottom, left");
   * CC.rc(3, 1, "b, l");
   * CC.rc(3, 1, "fill, center");
   * CC.rc(3, 1, "f, c");
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @param encodedAlignments describes the vertical and horizontal alignments
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints rc(int row, int col, String encodedAlignments) {
    return rchw(row, col, 1, 1, encodedAlignments);
  }

  /**
   * Sets the row and column origins; sets width and height to 1; set horizontal and vertical
   * alignment using the specified objects.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rc(3, 1, CellConstraints.BOTTOM, CellConstraints.LEFT);
   * CC.rc(3, 1, CellConstraints.FILL,   CellConstraints.CENTER);
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @param rowAlign vertical component alignment
   * @param colAlign horizontal component alignment
   * @return this
   */
  public static CellConstraints rc(int row, int col, Alignment rowAlign, Alignment colAlign) {
    return rchw(row, col, 1, 1, rowAlign, colAlign);
  }

  /**
   * Sets the row, column, height, and width; uses a height (row span) of 1 and the vertical and
   * horizontal default alignments.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rcw(3, 1, 7);
   * CC.rcw(3, 1, 2);
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @param colSpan the column span or grid width
   * @return this
   */
  public static CellConstraints rcw(int row, int col, int colSpan) {
    return rchw(row, col, 1, colSpan, CellConstraints.DEFAULT, CellConstraints.DEFAULT);
  }

  /**
   * Sets the row, column, height, and width; decodes the vertical and horizontal alignments from
   * the given string. The row span (height) is set to 1 .
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rcw(3, 1, 7, "bottom, left");
   * CC.rcw(3, 1, 7, "b, l");
   * CC.rcw(3, 1, 2, "fill, center");
   * CC.rcw(3, 1, 2, "f, c");
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @param colSpan the column span or grid width
   * @param encodedAlignments describes the vertical and horizontal alignments
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints rcw(int row, int col, int colSpan, String encodedAlignments) {
    return rchw(row, col, 1, colSpan, encodedAlignments);
  }

  /**
   * Sets the row, column, height, and width; sets the vertical and horizontal alignment using the
   * specified alignment objects. The row span (height) is set to 1 .
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rcw(3, 1, 2, CellConstraints.BOTTOM, CellConstraints.LEFT);
   * CC.rcw(3, 1, 7, CellConstraints.FILL,   CellConstraints.CENTER);
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @param colSpan the column span or grid width
   * @param rowAlign vertical component alignment
   * @param colAlign horizontal component alignment
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints rcw(
      int row, int col, int colSpan, Alignment rowAlign, Alignment colAlign) {
    return rchw(row, col, 1, colSpan, rowAlign, colAlign);
  }

  /**
   * Sets the row, column, height, and width; uses default alignments.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rchw(1, 3, 2, 1);
   * CC.rchw(1, 3, 7, 3);
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @param rowSpan the row span or grid height
   * @param colSpan the column span or grid width
   * @return this
   */
  public static CellConstraints rchw(int row, int col, int rowSpan, int colSpan) {
    return rchw(row, col, rowSpan, colSpan, CellConstraints.DEFAULT, CellConstraints.DEFAULT);
  }

  /**
   * Sets the row, column, height, and width; decodes the vertical and horizontal alignments from
   * the given string.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rchw(3, 1, 1, 2, "bottom, left");
   * CC.rchw(3, 1, 1, 2, "b, l");
   * CC.rchw(3, 1, 3, 7, "fill, center");
   * CC.rchw(3, 1, 3, 7, "f, c");
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @param rowSpan the row span or grid height
   * @param colSpan the column span or grid width
   * @param encodedAlignments describes the vertical and horizontal alignments
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints rchw(
      int row, int col, int rowSpan, int colSpan, String encodedAlignments) {
    return new CellConstraints().rchw(row, col, rowSpan, colSpan, encodedAlignments);
  }

  /**
   * Sets the row, column, height, and width; sets the vertical and horizontal alignment using the
   * specified alignment objects.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * CC.rchw(3, 1, 1, 2, CellConstraints.BOTTOM, CellConstraints.LEFT);
   * CC.rchw(3, 1, 3, 7, CellConstraints.FILL,   CellConstraints.CENTER);
   * </pre>
   *
   * @param row the new row index
   * @param col the new column index
   * @param rowSpan the row span or grid height
   * @param colSpan the column span or grid width
   * @param rowAlign vertical component alignment
   * @param colAlign horizontal component alignment
   * @return this
   * @throws IllegalArgumentException if an alignment orientation is invalid
   */
  public static CellConstraints rchw(
      int row, int col, int rowSpan, int colSpan, Alignment rowAlign, Alignment colAlign) {
    return xywh(col, row, colSpan, rowSpan, colAlign, rowAlign);
  }
} public final class FormLayout implements LayoutManager2, Serializable {

  // Instance Fields ********************************************************
  /**
   * Holds the column specifications.
   *
   * @see ColumnSpec
   * @see #getColumnCount()
   * @see #getColumnSpec(int)
   * @see #appendColumn(ColumnSpec)
   * @see #insertColumn(int, ColumnSpec)
   * @see #removeColumn(int)
   */
  private final List<ColumnSpec> colSpecs;

  /**
   * Holds the row specifications.
   *
   * @see RowSpec
   * @see #getRowCount()
   * @see #getRowSpec(int)
   * @see #appendRow(RowSpec)
   * @see #insertRow(int, RowSpec)
   * @see #removeRow(int)
   */
  private final List<RowSpec> rowSpecs;

  /**
   * Holds the column groups as an array of arrays of column indices.
   *
   * @see #getColumnGroups()
   * @see #setColumnGroups(int[][])
   * @see #addGroupedColumn(int)
   */
  private int[][] colGroupIndices;

  /**
   * Holds the row groups as an array of arrays of row indices.
   *
   * @see #getRowGroups()
   * @see #setRowGroups(int[][])
   * @see #addGroupedRow(int)
   */
  private int[][] rowGroupIndices;

  /**
   * Maps components to their associated {@code CellConstraints}.
   *
   * @see CellConstraints
   * @see #getConstraints(Component)
   * @see #setConstraints(Component, CellConstraints)
   */
  private final Map<Component, CellConstraints> constraintMap;

  private boolean honorsVisibility = true;

  // Fields used by the Layout Algorithm ************************************
  /**
   * Holds the components that occupy exactly one column. For each column we keep a list of these
   * components.
   */
  private transient List<List<Component>> colComponents;

  /**
   * Holds the components that occupy exactly one row. For each row we keep a list of these
   * components.
   */
  private transient List<List<Component>> rowComponents;

  /**
   * Caches component minimum and preferred sizes. All requests for component sizes shall be
   * directed to the cache.
   */
  private final ComponentSizeCache componentSizeCache;

  /**
   * These functional objects are used to measure component sizes. They abstract from horizontal and
   * vertical orientation and so, allow to implement the layout algorithm for both orientations with
   * a single set of methods.
   */
  private final Measure minimumWidthMeasure;

  private final Measure minimumHeightMeasure;
  private final Measure preferredWidthMeasure;
  private final Measure preferredHeightMeasure;

  // Instance Creation ****************************************************
  /**
   * Constructs an empty FormLayout. Columns and rows must be added before components can be added
   * to the layout container.
   *
   * <p>This constructor is intended to be used in environments that add columns and rows
   * dynamically.
   */
  public FormLayout() {
    this(new ColumnSpec[0], new RowSpec[0]);
  }

  /**
   * Constructs a FormLayout using the given encoded column specifications. The constructed layout
   * has no rows; these must be added before components can be added to the layout container. The
   * string decoding uses the default LayoutMap.
   *
   * <p>This constructor is intended to be used with builder classes that add rows dynamically, such
   * as the {@code DefaultFormBuilder} .
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * // Label, gap, component
   * FormLayout layout = new FormLayout(
   *      "pref, 4dlu, pref");
   *
   * // Right-aligned label, gap, component, gap, component
   * FormLayout layout = new FormLayout(
   *      "right:pref, 4dlu, 50dlu, 4dlu, 50dlu");
   *
   * // Left-aligned labels, gap, components, gap, components
   * FormLayout layout = new FormLayout(
   *      "left:pref, 4dlu, pref, 4dlu, pref");
   * </pre>
   *
   * See the class comment for more examples.
   *
   * @param encodedColumnSpecs comma separated encoded column specifications
   * @throws NullPointerException if encodedColumnSpecs is {@code null}
   * @see LayoutMap#getRoot()
   */
  public FormLayout(String encodedColumnSpecs) {
    this(encodedColumnSpecs, LayoutMap.getRoot());
  }

  /**
   * Constructs a FormLayout using the given encoded column specifications and LayoutMap. The
   * constructed layout has no rows; these must be added before components can be added to the
   * layout container.
   *
   * <p>This constructor is intended to be used with builder classes that add rows dynamically, such
   * as the {@code DefaultFormBuilder} .
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * // Label, gap, component
   * FormLayout layout = new FormLayout(
   *      "pref, 4dlu, pref",
   *      myLayoutMap);
   *
   * // Right-aligned label, gap, component, gap, component
   * FormLayout layout = new FormLayout(
   *      "right:pref, &#x0040;lcgap, 50dlu, 4dlu, 50dlu",
   *      myLayoutMap);
   *
   * // Left-aligned labels, gap, components, gap, components
   * FormLayout layout = new FormLayout(
   *      "left:pref, &#x0040;lcgap, pref, &#x0040;myGap, pref",
   *      myLayoutMap);
   * </pre>
   *
   * See the class comment for more examples.
   *
   * @param encodedColumnSpecs comma separated encoded column specifications
   * @param layoutMap expands layout column and row variables
   * @throws NullPointerException if {@code encodedColumnSpecs} or {@code layoutMap} is {@code null}
   * @see LayoutMap#getRoot()
   * @since 1.2
   */
  public FormLayout(String encodedColumnSpecs, LayoutMap layoutMap) {
    this(ColumnSpec.decodeSpecs(encodedColumnSpecs, layoutMap), new RowSpec[0]);
  }

  /**
   * Constructs a FormLayout using the given encoded column and row specifications and the default
   * LayoutMap.
   *
   * <p>This constructor is recommended for most hand-coded layouts.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * FormLayout layout = new FormLayout(
   *      "pref, 4dlu, pref",               // columns
   *      "p, 3dlu, p");                    // rows
   *
   * FormLayout layout = new FormLayout(
   *      "right:pref, 4dlu, pref",         // columns
   *      "p, 3dlu, p, 3dlu, fill:p:grow"); // rows
   *
   * FormLayout layout = new FormLayout(
   *      "left:pref, 4dlu, 50dlu",         // columns
   *      "p, 2px, p, 3dlu, p, 9dlu, p");   // rows
   *
   * FormLayout layout = new FormLayout(
   *      "max(75dlu;pref), 4dlu, default", // columns
   *      "p, 3dlu, p, 3dlu, p, 3dlu, p");  // rows
   * </pre>
   *
   * See the class comment for more examples.
   *
   * @param encodedColumnSpecs comma separated encoded column specifications
   * @param encodedRowSpecs comma separated encoded row specifications
   * @throws NullPointerException if encodedColumnSpecs or encodedRowSpecs is {@code null}
   * @see LayoutMap#getRoot()
   */
  public FormLayout(String encodedColumnSpecs, String encodedRowSpecs) {
    this(encodedColumnSpecs, encodedRowSpecs, LayoutMap.getRoot());
  }

  /**
   * Constructs a FormLayout using the given encoded column and row specifications and the given
   * LayoutMap.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * FormLayout layout = new FormLayout(
   *      "pref, 4dlu, pref",               // columns
   *      "p, 3dlu, p",                     // rows
   *      myLayoutMap);                     // custom LayoutMap
   *
   * FormLayout layout = new FormLayout(
   *      "right:pref, 4dlu, pref",         // columns
   *      "p, &#x0040;lgap, p, &#x0040;lgap, fill:p:grow",// rows
   *      myLayoutMap);                     // custom LayoutMap
   *
   * FormLayout layout = new FormLayout(
   *      "left:pref, 4dlu, 50dlu",         // columns
   *      "p, 2px, p, 3dlu, p, 9dlu, p",    // rows
   *      myLayoutMap);                     // custom LayoutMap
   *
   * FormLayout layout = new FormLayout(
   *      "max(75dlu;pref), 4dlu, default", // columns
   *      "p, 3dlu, p, 3dlu, p, 3dlu, p",   // rows
   *      myLayoutMap);                     // custom LayoutMap
   * </pre>
   *
   * See the class comment for more examples.
   *
   * @param encodedColumnSpecs comma separated encoded column specifications
   * @param encodedRowSpecs comma separated encoded row specifications
   * @param layoutMap expands layout column and row variables
   * @throws NullPointerException if {@code encodedColumnSpecs}, {@code encodedRowSpecs}, or {@code
   *     layoutMap} is {@code null}
   * @since 1.2
   */
  public FormLayout(String encodedColumnSpecs, String encodedRowSpecs, LayoutMap layoutMap) {
    this(
        ColumnSpec.decodeSpecs(encodedColumnSpecs, layoutMap),
        RowSpec.decodeSpecs(encodedRowSpecs, layoutMap));
  }

  /**
   * Constructs a FormLayout using the given column specifications. The constructed layout has no
   * rows; these must be added before components can be added to the layout container.
   *
   * @param colSpecs an array of column specifications.
   * @throws NullPointerException if {@code colSpecs} is {@code null}
   * @since 1.1
   */
  public FormLayout(ColumnSpec[] colSpecs) {
    this(colSpecs, new RowSpec[] {});
  }

  /**
   * Constructs a FormLayout using the given column and row specifications.
   *
   * @param colSpecs an array of column specifications.
   * @param rowSpecs an array of row specifications.
   * @throws NullPointerException if {@code colSpecs} or {@code rowSpecs} is {@code null}
   */
  public FormLayout(ColumnSpec[] colSpecs, RowSpec[] rowSpecs) {
    checkNotNull(colSpecs, "The column specifications must not be null.");
    checkNotNull(rowSpecs, "The row specifications must not be null.");
    this.colSpecs = new ArrayList<>(Arrays.asList(colSpecs));
    this.rowSpecs = new ArrayList<>(Arrays.asList(rowSpecs));
    colGroupIndices = new int[][] {};
    rowGroupIndices = new int[][] {};
    int initialCapacity = colSpecs.length * rowSpecs.length / 4;
    constraintMap = new HashMap<>(initialCapacity);
    componentSizeCache = new ComponentSizeCache(initialCapacity);
    minimumWidthMeasure = new MinimumWidthMeasure(componentSizeCache);
    minimumHeightMeasure = new MinimumHeightMeasure(componentSizeCache);
    preferredWidthMeasure = new PreferredWidthMeasure(componentSizeCache);
    preferredHeightMeasure = new PreferredHeightMeasure(componentSizeCache);
  }

  // Accessing the Column and Row Specifications **************************
  /**
   * Returns the number of columns in this layout.
   *
   * @return the number of columns
   */
  public int getColumnCount() {
    return colSpecs.size();
  }

  /**
   * Returns the {@code ColumnSpec} at the specified column index.
   *
   * @param columnIndex the column index of the requested {@code ColumnSpec}
   * @return the {@code ColumnSpec} at the specified column
   * @throws IndexOutOfBoundsException if the column index is out of range
   */
  public ColumnSpec getColumnSpec(int columnIndex) {
    return colSpecs.get(columnIndex - 1);
  }

  /**
   * Sets the ColumnSpec at the specified column index.
   *
   * @param columnIndex the index of the column to be changed
   * @param columnSpec the ColumnSpec to be set
   * @throws NullPointerException if {@code columnSpec} is {@code null}
   * @throws IndexOutOfBoundsException if the column index is out of range
   */
  public void setColumnSpec(int columnIndex, ColumnSpec columnSpec) {
    checkNotNull(columnSpec, "The column spec must not be null.");
    colSpecs.set(columnIndex - 1, columnSpec);
  }

  /**
   * Appends the given column specification to the right hand side of all columns.
   *
   * @param columnSpec the column specification to be added
   * @throws NullPointerException if {@code columnSpec} is {@code null}
   */
  public void appendColumn(ColumnSpec columnSpec) {
    checkNotNull(columnSpec, "The column spec must not be null.");
    colSpecs.add(columnSpec);
  }

  /**
   * Inserts the specified column at the specified position. Shifts components that intersect the
   * new column to the right hand side and readjusts column groups.
   *
   * <p>The component shift works as follows: components that were located on the right hand side of
   * the inserted column are shifted one column to the right; component column span is increased by
   * one if it intersects the new column.
   *
   * <p>Column group indices that are greater or equal than the given column index will be increased
   * by one.
   *
   * @param columnIndex index of the column to be inserted
   * @param columnSpec specification of the column to be inserted
   * @throws IndexOutOfBoundsException if the column index is out of range
   */
  public void insertColumn(int columnIndex, ColumnSpec columnSpec) {
    if (columnIndex < 1 || columnIndex > getColumnCount()) {
      throw new IndexOutOfBoundsException(
          "The column index "
              + columnIndex
              + "must be in the range [1, "
              + getColumnCount()
              + "].");
    }
    colSpecs.add(columnIndex - 1, columnSpec);
    shiftComponentsHorizontally(columnIndex, false);
    adjustGroupIndices(colGroupIndices, columnIndex, false);
  }

  /**
   * Removes the column with the given column index from the layout. Components will be rearranged
   * and column groups will be readjusted. Therefore, the column must not contain components and
   * must not be part of a column group.
   *
   * <p>The component shift works as follows: components that were located on the right hand side of
   * the removed column are moved one column to the left; component column span is decreased by one
   * if it intersects the removed column.
   *
   * <p>Column group indices that are greater than the column index will be decreased by one.
   *
   * <p><strong>Note:</strong> If one of the constraints mentioned above is violated, this layout's
   * state becomes illegal and it is unsafe to work with this layout. A typical layout
   * implementation can ensure that these constraints are not violated. However, in some cases you
   * may need to check these conditions before you invoke this method. The Forms extras contain
   * source code for class {@code FormLayoutUtils} that provides the required test methods:<br>
   * {@code #columnContainsComponents(Container, int)} and<br>
   * {@code #isGroupedColumn(FormLayout, int)}.
   *
   * @param columnIndex index of the column to remove
   * @throws IndexOutOfBoundsException if the column index is out of range
   * @throws IllegalStateException if the column contains components or if the column is already
   *     grouped
   *     <p>see com.jgoodies.forms.extras.FormLayoutUtils#columnContainsComponent(Container, int)
   *     see com.jgoodies.forms.extras.FormLayoutUtils#isGroupedColumn(FormLayout, int)
   */
  public void removeColumn(int columnIndex) {
    if (columnIndex < 1 || columnIndex > getColumnCount()) {
      throw new IndexOutOfBoundsException(
          "The column index "
              + columnIndex
              + " must be in the range [1, "
              + getColumnCount()
              + "].");
    }
    colSpecs.remove(columnIndex - 1);
    shiftComponentsHorizontally(columnIndex, true);
    adjustGroupIndices(colGroupIndices, columnIndex, true);
  }

  /**
   * Returns the number of rows in this layout.
   *
   * @return the number of rows
   */
  public int getRowCount() {
    return rowSpecs.size();
  }

  /**
   * Returns the {@code RowSpec} at the specified row index.
   *
   * @param rowIndex the row index of the requested {@code RowSpec}
   * @return the {@code RowSpec} at the specified row
   * @throws IndexOutOfBoundsException if the row index is out of range
   */
  public RowSpec getRowSpec(int rowIndex) {
    return rowSpecs.get(rowIndex - 1);
  }

  /**
   * Sets the RowSpec at the specified row index.
   *
   * @param rowIndex the index of the row to be changed
   * @param rowSpec the RowSpec to be set
   * @throws NullPointerException if {@code rowSpec} is {@code null}
   * @throws IndexOutOfBoundsException if the row index is out of range
   */
  public void setRowSpec(int rowIndex, RowSpec rowSpec) {
    checkNotNull(rowSpec, "The row spec must not be null.");
    rowSpecs.set(rowIndex - 1, rowSpec);
  }

  /**
   * Appends the given row specification to the bottom of all rows.
   *
   * @param rowSpec the row specification to be added to the form layout
   * @throws NullPointerException if {@code rowSpec} is {@code null}
   */
  public void appendRow(RowSpec rowSpec) {
    checkNotNull(rowSpec, "The row spec must not be null.");
    rowSpecs.add(rowSpec);
  }

  /**
   * Inserts the specified column at the specified position. Shifts components that intersect the
   * new column to the right and readjusts column groups.
   *
   * <p>The component shift works as follows: components that were located on the right hand side of
   * the inserted column are shifted one column to the right; component column span is increased by
   * one if it intersects the new column.
   *
   * <p>Column group indices that are greater or equal than the given column index will be increased
   * by one.
   *
   * @param rowIndex index of the row to be inserted
   * @param rowSpec specification of the row to be inserted
   * @throws IndexOutOfBoundsException if the row index is out of range
   */
  public void insertRow(int rowIndex, RowSpec rowSpec) {
    if (rowIndex < 1 || rowIndex > getRowCount()) {
      throw new IndexOutOfBoundsException(
          "The row index " + rowIndex + " must be in the range [1, " + getRowCount() + "].");
    }
    rowSpecs.add(rowIndex - 1, rowSpec);
    shiftComponentsVertically(rowIndex, false);
    adjustGroupIndices(rowGroupIndices, rowIndex, false);
  }

  /**
   * Removes the row with the given row index from the layout. Components will be rearranged and row
   * groups will be readjusted. Therefore, the row must not contain components and must not be part
   * of a row group.
   *
   * <p>The component shift works as follows: components that were located below the removed row are
   * moved up one row; component row span is decreased by one if it intersects the removed row.
   *
   * <p>Row group indices that are greater than the row index will be decreased by one.
   *
   * <p><strong>Note:</strong> If one of the constraints mentioned above is violated, this layout's
   * state becomes illegal and it is unsafe to work with this layout. A typical layout
   * implementation can ensure that these constraints are not violated. However, in some cases you
   * may need to check these conditions before you invoke this method. The Forms extras contain
   * source code for class {@code FormLayoutUtils} that provides the required test methods:<br>
   * {@code #rowContainsComponents(Container, int)} and<br>
   * {@code #isGroupedRow(FormLayout, int)}.
   *
   * @param rowIndex index of the row to remove
   * @throws IndexOutOfBoundsException if the row index is out of range
   * @throws IllegalStateException if the row contains components or if the row is already grouped
   *     <p>see com.jgoodies.forms.extras.FormLayoutUtils#rowContainsComponent(Container, int) see
   *     com.jgoodies.forms.extras.FormLayoutUtils#isGroupedRow(FormLayout, int)
   */
  public void removeRow(int rowIndex) {
    if (rowIndex < 1 || rowIndex > getRowCount()) {
      throw new IndexOutOfBoundsException(
          "The row index " + rowIndex + "must be in the range [1, " + getRowCount() + "].");
    }
    rowSpecs.remove(rowIndex - 1);
    shiftComponentsVertically(rowIndex, true);
    adjustGroupIndices(rowGroupIndices, rowIndex, true);
  }

  /**
   * Shifts components horizontally, either to the right if a column has been inserted or to the
   * left if a column has been removed.
   *
   * @param columnIndex index of the column to remove
   * @param remove true for remove, false for insert
   * @throws IllegalStateException if a removed column contains components
   */
  private void shiftComponentsHorizontally(int columnIndex, boolean remove) {
    final int offset = remove ? -1 : 1;
    for (Object element : constraintMap.entrySet()) {
      Map.Entry entry = (Map.Entry) element;
      CellConstraints constraints = (CellConstraints) entry.getValue();
      int x1 = constraints.gridX;
      int w = constraints.gridWidth;
      int x2 = x1 + w - 1;
      if (x1 == columnIndex && remove) {
        throw new IllegalStateException(
            "The removed column "
                + columnIndex
                + " must not contain component origins.\n"
                + "Illegal component="
                + entry.getKey());
      } else if (x1 >= columnIndex) {
        constraints.gridX += offset;
      } else if (x2 >= columnIndex) {
        constraints.gridWidth += offset;
      }
    }
  }

  /**
   * Shifts components vertically, either to the bottom if a row has been inserted or to the top if
   * a row has been removed.
   *
   * @param rowIndex index of the row to remove
   * @param remove true for remove, false for insert
   * @throws IllegalStateException if a removed column contains components
   */
  private void shiftComponentsVertically(int rowIndex, boolean remove) {
    final int offset = remove ? -1 : 1;
    for (Object element : constraintMap.entrySet()) {
      Map.Entry entry = (Map.Entry) element;
      CellConstraints constraints = (CellConstraints) entry.getValue();
      int y1 = constraints.gridY;
      int h = constraints.gridHeight;
      int y2 = y1 + h - 1;
      if (y1 == rowIndex && remove) {
        throw new IllegalStateException(
            "The removed row "
                + rowIndex
                + " must not contain component origins.\n"
                + "Illegal component="
                + entry.getKey());
      } else if (y1 >= rowIndex) {
        constraints.gridY += offset;
      } else if (y2 >= rowIndex) {
        constraints.gridHeight += offset;
      }
    }
  }

  /**
   * Adjusts group indices. Shifts the given groups to left, right, up, down according to the
   * specified remove or add flag.
   *
   * @param allGroupIndices the groups to be adjusted
   * @param modifiedIndex the modified column or row index
   * @param remove true for remove, false for add
   * @throws IllegalStateException if we remove and the index is grouped
   */
  private static void adjustGroupIndices(
      int[][] allGroupIndices, int modifiedIndex, boolean remove) {
    final int offset = remove ? -1 : +1;
    for (int[] allGroupIndice : allGroupIndices) {
      int[] groupIndices = allGroupIndice;
      for (int i = 0; i < groupIndices.length; i++) {
        int index = groupIndices[i];
        if (index == modifiedIndex && remove) {
          throw new IllegalStateException(
              "The removed index " + modifiedIndex + " must not be grouped.");
        } else if (index >= modifiedIndex) {
          groupIndices[i] += offset;
        }
      }
    }
  }

  // Accessing Constraints ************************************************
  /**
   * Looks up and returns the constraints for the specified component. A copy of the
   * actualCellConstraints object is returned.
   *
   * @param component the component to be queried
   * @return the CellConstraints for the specified component
   * @throws NullPointerException if {@code component} is {@code null}
   * @throws IllegalStateException if {@code component} has not been added to the container
   */
  public CellConstraints getConstraints(Component component) {
    return (CellConstraints) getConstraints0(component).clone();
  }

  private CellConstraints getConstraints0(Component component) {
    checkNotNull(component, "The component must not be null.");
    CellConstraints constraints = constraintMap.get(component);
    checkState(constraints != null, "The component has not been added to the container.");
    return constraints;
  }

  /**
   * Sets the constraints for the specified component in this layout.
   *
   * @param component the component to be modified
   * @param constraints the constraints to be applied
   * @throws NullPointerException if {@code component} or {@code constraints} is {@code null}
   */
  public void setConstraints(Component component, CellConstraints constraints) {
    checkNotNull(component, "The component must not be null.");
    checkNotNull(constraints, "The constraints must not be null.");
    constraints.ensureValidGridBounds(getColumnCount(), getRowCount());
    constraintMap.put(component, (CellConstraints) constraints.clone());
  }

  /**
   * Removes the constraints for the specified component in this layout.
   *
   * @param component the component to be modified
   */
  private void removeConstraints(Component component) {
    constraintMap.remove(component);
    componentSizeCache.removeEntry(component);
  }

  // Accessing Column and Row Groups **************************************
  /**
   * Returns a deep copy of the column groups.
   *
   * @return the column groups as two-dimensional int array
   */
  public int[][] getColumnGroups() {
    return deepClone(colGroupIndices);
  }

  /**
   * Sets the column groups, where each column in a group gets the same group wide width. Each group
   * is described by an array of integers that are interpreted as column indices. The parameter is
   * an array of such group descriptions.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * // Group columns 1, 3 and 4.
   * setColumnGroups(new int[][]{ {1, 3, 4}});
   *
   * // Group columns 1, 3, 4, and group columns 7 and 9
   * setColumnGroups(new int[][]{ {1, 3, 4}, {7, 9}});
   * </pre>
   *
   * @param colGroupIndices a two-dimensional array of column groups indices
   * @throws IndexOutOfBoundsException if an index is outside the grid
   * @throws IllegalArgumentException if a column index is used twice
   */
  public void setColumnGroups(int[][] colGroupIndices) {
    int maxColumn = getColumnCount();
    boolean[] usedIndices = new boolean[maxColumn + 1];
    for (int group = 0; group < colGroupIndices.length; group++) {
      for (int j = 0; j < colGroupIndices[group].length; j++) {
        int colIndex = colGroupIndices[group][j];
        if (colIndex < 1 || colIndex > maxColumn) {
          throw new IndexOutOfBoundsException(
              "Invalid column group index " + colIndex + " in group " + (group + 1));
        }
        if (usedIndices[colIndex]) {
          throw new IllegalArgumentException(
              "Column index " + colIndex + " must not be used in multiple column groups.");
        }
        usedIndices[colIndex] = true;
      }
    }
    this.colGroupIndices = deepClone(colGroupIndices);
  }

  /**
   * Adds the specified column index to the last column group. In case there are no groups, a new
   * group will be created.
   *
   * @param columnIndex the column index to be set grouped
   */
  public void addGroupedColumn(int columnIndex) {
    int[][] newColGroups = getColumnGroups();
    // Create a group if none exists.
    if (newColGroups.length == 0) {
      newColGroups = new int[][] {{columnIndex}};
    } else {
      int lastGroupIndex = newColGroups.length - 1;
      int[] lastGroup = newColGroups[lastGroupIndex];
      int groupSize = lastGroup.length;
      int[] newLastGroup = new int[groupSize + 1];
      System.arraycopy(lastGroup, 0, newLastGroup, 0, groupSize);
      newLastGroup[groupSize] = columnIndex;
      newColGroups[lastGroupIndex] = newLastGroup;
    }
    setColumnGroups(newColGroups);
  }

  /**
   * Returns a deep copy of the row groups.
   *
   * @return the row groups as two-dimensional int array
   */
  public int[][] getRowGroups() {
    return deepClone(rowGroupIndices);
  }

  /**
   * Sets the row groups, where each row in such a group gets the same group wide height. Each group
   * is described by an array of integers that are interpreted as row indices. The parameter is an
   * array of such group descriptions.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * // Group rows 1 and 2.
   * setRowGroups(new int[][]{ {1, 2}});
   *
   * // Group rows 1 and 2, and group rows 5, 7, and 9.
   * setRowGroups(new int[][]{ {1, 2}, {5, 7, 9}});
   * </pre>
   *
   * @param rowGroupIndices a two-dimensional array of row group indices.
   * @throws IndexOutOfBoundsException if an index is outside the grid
   */
  public void setRowGroups(int[][] rowGroupIndices) {
    int rowCount = getRowCount();
    boolean[] usedIndices = new boolean[rowCount + 1];
    for (int i = 0; i < rowGroupIndices.length; i++) {
      for (int j = 0; j < rowGroupIndices[i].length; j++) {
        int rowIndex = rowGroupIndices[i][j];
        if (rowIndex < 1 || rowIndex > rowCount) {
          throw new IndexOutOfBoundsException(
              "Invalid row group index " + rowIndex + " in group " + (i + 1));
        }
        if (usedIndices[rowIndex]) {
          throw new IllegalArgumentException(
              "Row index " + rowIndex + " must not be used in multiple row groups.");
        }
        usedIndices[rowIndex] = true;
      }
    }
    this.rowGroupIndices = deepClone(rowGroupIndices);
  }

  /**
   * Adds the specified row index to the last row group. In case there are no groups, a new group
   * will be created.
   *
   * @param rowIndex the index of the row that should be grouped
   */
  public void addGroupedRow(int rowIndex) {
    int[][] newRowGroups = getRowGroups();
    // Create a group if none exists.
    if (newRowGroups.length == 0) {
      newRowGroups = new int[][] {{rowIndex}};
    } else {
      int lastGroupIndex = newRowGroups.length - 1;
      int[] lastGroup = newRowGroups[lastGroupIndex];
      int groupSize = lastGroup.length;
      int[] newLastGroup = new int[groupSize + 1];
      System.arraycopy(lastGroup, 0, newLastGroup, 0, groupSize);
      newLastGroup[groupSize] = rowIndex;
      newRowGroups[lastGroupIndex] = newLastGroup;
    }
    setRowGroups(newRowGroups);
  }

  // Other Accessors ********************************************************
  /**
   * Returns whether invisible components shall be taken into account by this layout. This
   * container-wide setting can be overridden per component. See {@link
   * #setHonorsVisibility(boolean)} for details.
   *
   * @return {@code true} if the component visibility is honored by this FormLayout, {@code false}
   *     if it is ignored. This setting can be overridden for individual CellConstraints using
   *     {@link #setHonorsVisibility(Component, Boolean)}.
   * @since 1.2
   */
  public boolean getHonorsVisibility() {
    return honorsVisibility;
  }

  /**
   * Specifies whether invisible components shall be taken into account by this layout for computing
   * the layout size and setting component bounds. If set to {@code true} invisible components will
   * be ignored by the layout. If set to {@code false} components will be taken into account
   * regardless of their visibility. Visible components are always used for sizing and positioning.
   *
   * <p>The default value for this setting is {@code true}. It is useful to set the value to {@code
   * false} (in other words to ignore the visibility) if you switch the component visibility
   * dynamically and want the container to retain the size and component positions.
   *
   * <p>This container-wide default setting can be overridden per component using {@link
   * #setHonorsVisibility(Component, Boolean)} .
   *
   * <p>Components are taken into account, if
   *
   * <ol>
   *   <li>they are visible, or
   *   <li>they have no individual setting and the container-wide settings ignores the visibility
   *       (honorsVisibility set to {@code false}), or
   *   <li>the individual component ignores the visibility.
   * </ol>
   *
   * @param b {@code true} to honor the visibility, i.e. to exclude invisible components from the
   *     sizing and positioning, {@code false} to ignore the visibility, in other words to layout
   *     visible and invisible components
   * @since 1.2
   */
  public void setHonorsVisibility(boolean b) {
    boolean oldHonorsVisibility = getHonorsVisibility();
    if (oldHonorsVisibility == b) {
      return;
    }
    honorsVisibility = b;
    Set componentSet = constraintMap.keySet();
    if (componentSet.isEmpty()) {
      return;
    }
    Component firstComponent = (Component) componentSet.iterator().next();
    Container container = firstComponent.getParent();
    invalidateAndRepaint(container);
  }

  /**
   * Specifies whether the given component shall be taken into account for sizing and positioning.
   * This setting overrides the container-wide default. See {@link #setHonorsVisibility(boolean)}
   * for details.
   *
   * @param component the component that shall get an individual setting
   * @param b {@code Boolean.TRUE} to override the container default and honor the visibility for
   *     the given component, {@code Boolean.FALSE} to override the container default and ignore the
   *     visibility for the given component, {@code null} to use the container default value as
   *     specified by {@link #getHonorsVisibility()}.
   * @since 1.2
   */
  public void setHonorsVisibility(Component component, Boolean b) {
    CellConstraints constraints = getConstraints0(component);
    if (Objects.equals(b, constraints.honorsVisibility)) {
      return;
    }
    constraints.honorsVisibility = b;
    invalidateAndRepaint(component.getParent());
  }

  // Implementing the LayoutManager and LayoutManager2 Interfaces *********
  /**
   * Throws an {@code UnsupportedOperationException}. Does not add the specified component with the
   * specified name to the layout.
   *
   * @param name indicates entry's position and anchor
   * @param component component to add
   * @throws UnsupportedOperationException always
   */
  @Override
  public void addLayoutComponent(String name, Component component) {
    throw new UnsupportedOperationException("Use #addLayoutComponent(Component, Object) instead.");
  }

  /**
   * Adds the specified component to the layout, using the specified {@code constraints} object.
   * Note that constraints are mutable and are, therefore, cloned when cached.
   *
   * @param comp the component to be added
   * @param constraints the component's cell constraints
   * @throws NullPointerException if {@code constraints} is {@code null}
   * @throws IllegalArgumentException if {@code constraints} is neither a String, nor a
   *     CellConstraints object, or a String that is rejected by the CellConstraints construction
   */
  @Override
  public void addLayoutComponent(Component comp, Object constraints) {
    checkNotNull(constraints, "The constraints must not be null.");
    if (constraints instanceof String) {
      setConstraints(comp, new CellConstraints((String) constraints));
    } else if (constraints instanceof CellConstraints) {
      setConstraints(comp, (CellConstraints) constraints);
    } else {
      throw new IllegalArgumentException("Illegal constraint type " + constraints.getClass());
    }
  }

  /**
   * Removes the specified component from this layout.
   *
   * <p>Most applications do not call this method directly.
   *
   * @param comp the component to be removed.
   * @see Container#remove(java.awt.Component)
   * @see Container#removeAll()
   */
  @Override
  public void removeLayoutComponent(Component comp) {
    removeConstraints(comp);
  }

  // Layout Requests ******************************************************
  /**
   * Determines the minimum size of the {@code parent} container using this form layout.
   *
   * <p>Most applications do not call this method directly.
   *
   * @param parent the container in which to do the layout
   * @return the minimum size of the {@code parent} container
   * @see Container#doLayout()
   */
  @Override
  public Dimension minimumLayoutSize(Container parent) {
    return computeLayoutSize(parent, minimumWidthMeasure, minimumHeightMeasure);
  }

  /**
   * Determines the preferred size of the {@code parent} container using this form layout.
   *
   * <p>Most applications do not call this method directly.
   *
   * @param parent the container in which to do the layout
   * @return the preferred size of the {@code parent} container
   * @see Container#getPreferredSize()
   */
  @Override
  public Dimension preferredLayoutSize(Container parent) {
    return computeLayoutSize(parent, preferredWidthMeasure, preferredHeightMeasure);
  }

  /**
   * Returns the maximum dimensions for this layout given the components in the specified target
   * container.
   *
   * @param target the container which needs to be laid out
   * @see Container
   * @see #minimumLayoutSize(Container)
   * @see #preferredLayoutSize(Container)
   * @return the maximum dimensions for this layout
   */
  @Override
  public Dimension maximumLayoutSize(Container target) {
    return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
  }

  /**
   * Returns the alignment along the x axis. This specifies how the component would like to be
   * aligned relative to other components. The value should be a number between 0 and 1 where 0
   * represents alignment along the origin, 1 is aligned the farthest away from the origin, 0.5 is
   * centered, etc.
   *
   * @param parent the parent container
   * @return the value {@code 0.5f} to indicate center alignment
   */
  @Override
  public float getLayoutAlignmentX(Container parent) {
    return 0.5f;
  }

  /**
   * Returns the alignment along the y axis. This specifies how the component would like to be
   * aligned relative to other components. The value should be a number between 0 and 1 where 0
   * represents alignment along the origin, 1 is aligned the farthest away from the origin, 0.5 is
   * centered, etc.
   *
   * @param parent the parent container
   * @return the value {@code 0.5f} to indicate center alignment
   */
  @Override
  public float getLayoutAlignmentY(Container parent) {
    return 0.5f;
  }

  /**
   * Invalidates the layout, indicating that if the layout manager has cached information it should
   * be discarded.
   *
   * @param target the container that holds the layout to be invalidated
   */
  @Override
  public void invalidateLayout(Container target) {
    invalidateCaches();
  }

  /**
   * Lays out the specified container using this form layout. This method reshapes components in the
   * specified container in order to satisfy the constraints of this {@code FormLayout} object.
   *
   * <p>Most applications do not call this method directly.
   *
   * <p>The form layout performs the following steps:
   *
   * <ol>
   *   <li>find components that occupy exactly one column or row
   *   <li>compute minimum widths and heights
   *   <li>compute preferred widths and heights
   *   <li>give cols and row equal size if they share a group
   *   <li>compress default columns and rows if total is less than pref size
   *   <li>give cols and row equal size if they share a group
   *   <li>distribute free space
   *   <li>set components bounds
   * </ol>
   *
   * @param parent the container in which to do the layout
   * @see Container
   * @see Container#doLayout()
   */
  @Override
  public void layoutContainer(Container parent) {
    synchronized (parent.getTreeLock()) {
      initializeColAndRowComponentLists();
      Dimension size = parent.getSize();

      Insets insets = parent.getInsets();
      int totalWidth = size.width - insets.left - insets.right;
      int totalHeight = size.height - insets.top - insets.bottom;

      int[] x =
          computeGridOrigins(
              parent,
              totalWidth,
              insets.left,
              colSpecs,
              colComponents,
              colGroupIndices,
              minimumWidthMeasure,
              preferredWidthMeasure);
      int[] y =
          computeGridOrigins(
              parent,
              totalHeight,
              insets.top,
              rowSpecs,
              rowComponents,
              rowGroupIndices,
              minimumHeightMeasure,
              preferredHeightMeasure);

      layoutComponents(x, y);
    }
  }

  // Layout Algorithm *****************************************************
  /**
   * Initializes two lists for columns and rows that hold a column's or row's components that span
   * only this column or row.
   *
   * <p>Iterates over all components and their associated constraints; every component that has a
   * column span or row span of 1 is put into the column's or row's component list.
   */
  private void initializeColAndRowComponentLists() {
    colComponents = new ArrayList<>(getColumnCount());
    for (int i = 0; i < getColumnCount(); i++) {
      colComponents.add(new ArrayList<>());
    }

    rowComponents = new ArrayList<>(getRowCount());
    for (int i = 0; i < getRowCount(); i++) {
      rowComponents.add(new ArrayList<>());
    }

    for (Object element : constraintMap.entrySet()) {
      Map.Entry entry = (Map.Entry) element;
      Component component = (Component) entry.getKey();
      CellConstraints constraints = (CellConstraints) entry.getValue();
      if (takeIntoAccount(component, constraints)) {
        if (constraints.gridWidth == 1) {
          colComponents.get(constraints.gridX - 1).add(component);
        }

        if (constraints.gridHeight == 1) {
          rowComponents.get(constraints.gridY - 1).add(component);
        }
      }
    }
  }

  /**
   * Computes and returns the layout size of the given {@code parent} container using the specified
   * measures.
   *
   * @param parent the container in which to do the layout
   * @param defaultWidthMeasure the measure used to compute the default width
   * @param defaultHeightMeasure the measure used to compute the default height
   * @return the layout size of the {@code parent} container
   */
  private Dimension computeLayoutSize(
      Container parent, Measure defaultWidthMeasure, Measure defaultHeightMeasure) {
    synchronized (parent.getTreeLock()) {
      initializeColAndRowComponentLists();
      int[] colWidths =
          maximumSizes(
              parent,
              colSpecs,
              colComponents,
              minimumWidthMeasure,
              preferredWidthMeasure,
              defaultWidthMeasure);
      int[] rowHeights =
          maximumSizes(
              parent,
              rowSpecs,
              rowComponents,
              minimumHeightMeasure,
              preferredHeightMeasure,
              defaultHeightMeasure);
      int[] groupedWidths = groupedSizes(colGroupIndices, colWidths);
      int[] groupedHeights = groupedSizes(rowGroupIndices, rowHeights);

      // Convert sizes to origins.
      int[] xOrigins = computeOrigins(groupedWidths, 0);
      int[] yOrigins = computeOrigins(groupedHeights, 0);

      int width1 = sum(groupedWidths);
      int height1 = sum(groupedHeights);
      int maxWidth = width1;
      int maxHeight = height1;

      /*
       * Take components that span multiple columns or rows into account.
       * This shall be done if and only if a component spans an interval
       * that can grow.
       */
      // First computes the maximum number of cols/rows a component
      // can span without spanning a growing column.
      int[] maxFixedSizeColsTable = computeMaximumFixedSpanTable(colSpecs);
      int[] maxFixedSizeRowsTable = computeMaximumFixedSpanTable(rowSpecs);

      for (Object element : constraintMap.entrySet()) {
        Map.Entry entry = (Map.Entry) element;
        Component component = (Component) entry.getKey();
        CellConstraints constraints = (CellConstraints) entry.getValue();
        if (!takeIntoAccount(component, constraints)) {
          continue;
        }

        if (constraints.gridWidth > 1
            && constraints.gridWidth > maxFixedSizeColsTable[constraints.gridX - 1]) {
          // int compWidth = minimumWidthMeasure.sizeOf(component);
          int compWidth = defaultWidthMeasure.sizeOf(component);
          // int compWidth = preferredWidthMeasure.sizeOf(component);
          int gridX1 = constraints.gridX - 1;
          int gridX2 = gridX1 + constraints.gridWidth;
          int lead = xOrigins[gridX1];
          int trail = width1 - xOrigins[gridX2];
          int myWidth = lead + compWidth + trail;
          if (myWidth > maxWidth) {
            maxWidth = myWidth;
          }
        }

        if (constraints.gridHeight > 1
            && constraints.gridHeight > maxFixedSizeRowsTable[constraints.gridY - 1]) {
          // int compHeight = minimumHeightMeasure.sizeOf(component);
          int compHeight = defaultHeightMeasure.sizeOf(component);
          // int compHeight = preferredHeightMeasure.sizeOf(component);
          int gridY1 = constraints.gridY - 1;
          int gridY2 = gridY1 + constraints.gridHeight;
          int lead = yOrigins[gridY1];
          int trail = height1 - yOrigins[gridY2];
          int myHeight = lead + compHeight + trail;
          if (myHeight > maxHeight) {
            maxHeight = myHeight;
          }
        }
      }
      Insets insets = parent.getInsets();
      int width = maxWidth + insets.left + insets.right;
      int height = maxHeight + insets.top + insets.bottom;
      return new Dimension(width, height);
    }
  }

  /**
   * Computes and returns the grid's origins.
   *
   * @param container the layout container
   * @param totalSize the total size to assign
   * @param offset the offset from left or top margin
   * @param formSpecs the column or row specs, resp.
   * @param componentLists the components list for each col/row
   * @param minMeasure the measure used to determine min sizes
   * @param prefMeasure the measure used to determine pre sizes
   * @param groupIndices the group specification
   * @return an int array with the origins
   */
  private static int[] computeGridOrigins(
      Container container,
      int totalSize,
      int offset,
      List formSpecs,
      List<List<Component>> componentLists,
      int[][] groupIndices,
      Measure minMeasure,
      Measure prefMeasure) {
    /* For each spec compute the minimum and preferred size that is
     * the maximum of all component minimum and preferred sizes resp.
     */
    int[] minSizes =
        maximumSizes(container, formSpecs, componentLists, minMeasure, prefMeasure, minMeasure);
    int[] prefSizes =
        maximumSizes(container, formSpecs, componentLists, minMeasure, prefMeasure, prefMeasure);

    int[] groupedMinSizes = groupedSizes(groupIndices, minSizes);
    int[] groupedPrefSizes = groupedSizes(groupIndices, prefSizes);
    int totalMinSize = sum(groupedMinSizes);
    int totalPrefSize = sum(groupedPrefSizes);
    int[] compressedSizes =
        compressedSizes(
            formSpecs, totalSize, totalMinSize, totalPrefSize, groupedMinSizes, prefSizes);
    int[] groupedSizes = groupedSizes(groupIndices, compressedSizes);
    int totalGroupedSize = sum(groupedSizes);
    int[] sizes = distributedSizes(formSpecs, totalSize, totalGroupedSize, groupedSizes);
    return computeOrigins(sizes, offset);
  }

  /**
   * Computes origins from sizes taking the specified offset into account.
   *
   * @param sizes the array of sizes
   * @param offset an offset for the first origin
   * @return an array of origins
   */
  private static int[] computeOrigins(int[] sizes, int offset) {
    int count = sizes.length;
    int[] origins = new int[count + 1];
    origins[0] = offset;
    for (int i = 1; i <= count; i++) {
      origins[i] = origins[i - 1] + sizes[i - 1];
    }
    return origins;
  }

  /**
   * Lays out the components using the given x and y origins, the column and row specifications, and
   * the component constraints.
   *
   * <p>The actual computation is done by each component's form constraint object. We just compute
   * the cell, the cell bounds and then hand over the component, cell bounds, and measure to the
   * form constraints. This will allow potential subclasses of {@code CellConstraints} to do special
   * micro-layout corrections. For example, such a subclass could map JComponent classes to visual
   * layout bounds that may lead to a slightly different bounds.
   *
   * @param x an int array of the horizontal origins
   * @param y an int array of the vertical origins
   */
  private void layoutComponents(int[] x, int[] y) {
    Rectangle cellBounds = new Rectangle();
    for (Object element : constraintMap.entrySet()) {
      Map.Entry entry = (Map.Entry) element;
      Component component = (Component) entry.getKey();
      CellConstraints constraints = (CellConstraints) entry.getValue();

      int gridX = constraints.gridX - 1;
      int gridY = constraints.gridY - 1;
      int gridWidth = constraints.gridWidth;
      int gridHeight = constraints.gridHeight;
      cellBounds.x = x[gridX];
      cellBounds.y = y[gridY];
      cellBounds.width = x[gridX + gridWidth] - cellBounds.x;
      cellBounds.height = y[gridY + gridHeight] - cellBounds.y;

      constraints.setBounds(
          component,
          this,
          cellBounds,
          minimumWidthMeasure,
          minimumHeightMeasure,
          preferredWidthMeasure,
          preferredHeightMeasure);
    }
  }

  /** Invalidates the component size caches. */
  private void invalidateCaches() {
    componentSizeCache.invalidate();
  }

  /**
   * Computes and returns the sizes for the given form specs, component lists and measures for
   * minimum, preferred, and default size.
   *
   * @param container the layout container
   * @param formSpecs the column or row specs, resp.
   * @param componentLists the components list for each col/row
   * @param minMeasure the measure used to determine min sizes
   * @param prefMeasure the measure used to determine pre sizes
   * @param defaultMeasure the measure used to determine default sizes
   * @return the column or row sizes
   */
  private static int[] maximumSizes(
      Container container,
      List formSpecs,
      List<List<Component>> componentLists,
      Measure minMeasure,
      Measure prefMeasure,
      Measure defaultMeasure) {
    FormSpec formSpec;
    int size = formSpecs.size();
    int[] result = new int[size];
    for (int i = 0; i < size; i++) {
      formSpec = (FormSpec) formSpecs.get(i);
      result[i] =
          formSpec.maximumSize(
              container, componentLists.get(i), minMeasure, prefMeasure, defaultMeasure);
    }
    return result;
  }

  /**
   * Computes and returns the compressed sizes. Compresses space for columns and rows iff the
   * available space is less than the total preferred size but more than the total minimum size.
   *
   * <p>Only columns and rows that are specified to be compressible will be affected. You can
   * specify a column and row as compressible by giving it the component size <tt>default</tt>.
   *
   * @param formSpecs the column or row specs to use
   * @param totalSize the total available size
   * @param totalMinSize the sum of all minimum sizes
   * @param totalPrefSize the sum of all preferred sizes
   * @param minSizes an int array of column/row minimum sizes
   * @param prefSizes an int array of column/row preferred sizes
   * @return an int array of compressed column/row sizes
   */
  private static int[] compressedSizes(
      List formSpecs,
      int totalSize,
      int totalMinSize,
      int totalPrefSize,
      int[] minSizes,
      int[] prefSizes) {

    // If we have less space than the total min size, answer the min sizes.
    if (totalSize < totalMinSize) {
      return minSizes;
    }
    // If we have more space than the total pref size, answer the pref sizes.
    if (totalSize >= totalPrefSize) {
      return prefSizes;
    }

    int count = formSpecs.size();
    int[] sizes = new int[count];

    double totalCompressionSpace = totalPrefSize - totalSize;
    double maxCompressionSpace = totalPrefSize - totalMinSize;
    double compressionFactor = totalCompressionSpace / maxCompressionSpace;

    //      System.out.println("Total compression space=" + totalCompressionSpace);
    //      System.out.println("Max compression space  =" + maxCompressionSpace);
    //      System.out.println("Compression factor     =" + compressionFactor);
    for (int i = 0; i < count; i++) {
      FormSpec formSpec = (FormSpec) formSpecs.get(i);
      sizes[i] = prefSizes[i];
      if (formSpec.getSize().compressible()) {
        sizes[i] -= (int) Math.round((prefSizes[i] - minSizes[i]) * compressionFactor);
      }
    }
    return sizes;
  }

  /**
   * Computes and returns the grouped sizes. Gives grouped columns and rows the same size.
   *
   * @param groups the group specification
   * @param rawSizes the raw sizes before the grouping
   * @return the grouped sizes
   */
  private static int[] groupedSizes(int[][] groups, int[] rawSizes) {
    // Return the compressed sizes if there are no groups.
    if (groups == null || groups.length == 0) {
      return rawSizes;
    }

    // Initialize the result with the given compressed sizes.
    int[] sizes = new int[rawSizes.length];
    for (int i = 0; i < sizes.length; i++) {
      sizes[i] = rawSizes[i];
    }

    // For each group equalize the sizes.
    for (int[] groupIndices : groups) {
      int groupMaxSize = 0;
      // Compute the group's maximum size.
      for (int groupIndice : groupIndices) {
        int index = groupIndice - 1;
        groupMaxSize = Math.max(groupMaxSize, sizes[index]);
      }
      // Set all sizes of this group to the group's maximum size.
      for (int groupIndice : groupIndices) {
        int index = groupIndice - 1;
        sizes[index] = groupMaxSize;
      }
    }
    return sizes;
  }

  /**
   * Distributes free space over columns and rows and returns the sizes after this distribution
   * process.
   *
   * @param formSpecs the column/row specifications to work with
   * @param totalSize the total available size
   * @param totalPrefSize the sum of all preferred sizes
   * @param inputSizes the input sizes
   * @return the distributed sizes
   */
  private static int[] distributedSizes(
      List formSpecs, int totalSize, int totalPrefSize, int[] inputSizes) {
    double totalFreeSpace = totalSize - totalPrefSize;
    // Do nothing if there's no free space.
    if (totalFreeSpace < 0) {
      return inputSizes;
    }

    // Compute the total weight.
    int count = formSpecs.size();
    double totalWeight = 0.0;
    for (Object spec : formSpecs) {
      FormSpec formSpec = (FormSpec) spec;
      totalWeight += formSpec.getResizeWeight();
    }

    // Do nothing if there's no resizing column.
    if (totalWeight == 0.0) {
      return inputSizes;
    }

    int[] sizes = new int[count];

    double restSpace = totalFreeSpace;
    int roundedRestSpace = (int) totalFreeSpace;
    for (int i = 0; i < count; i++) {
      FormSpec formSpec = (FormSpec) formSpecs.get(i);
      double weight = formSpec.getResizeWeight();
      if (weight == FormSpec.NO_GROW) {
        sizes[i] = inputSizes[i];
      } else {
        double roundingCorrection = restSpace - roundedRestSpace;
        double extraSpace = totalFreeSpace * weight / totalWeight;
        double correctedExtraSpace = extraSpace - roundingCorrection;
        int roundedExtraSpace = (int) Math.round(correctedExtraSpace);
        sizes[i] = inputSizes[i] + roundedExtraSpace;
        restSpace -= extraSpace;
        roundedRestSpace -= roundedExtraSpace;
      }
    }
    return sizes;
  }

  /**
   * Computes and returns a table that maps a column/row index to the maximum number of columns/rows
   * that a component can span without spanning a growing column.
   *
   * <p>Iterates over the specs from right to left/bottom to top, sets the table value to zero if a
   * spec can grow, otherwise increases the span by one.
   *
   * <p><strong>Examples:</strong>
   *
   * <pre>
   * "pref, 4dlu, pref, 2dlu, p:grow, 2dlu,      pref" ->
   * [4,    3,    2,    1,    0,      MAX_VALUE, MAX_VALUE]
   *
   * "p:grow, 4dlu, p:grow, 9dlu,      pref" ->
   * [0,      1,    0,      MAX_VALUE, MAX_VALUE]
   *
   * "p, 4dlu, p, 2dlu, 0:grow" ->
   * [4, 3,    2, 1,    0]
   * </pre>
   *
   * @param formSpecs the column specs or row specs
   * @return a table that maps a spec index to the maximum span for fixed size specs
   */
  private static int[] computeMaximumFixedSpanTable(List formSpecs) {
    int size = formSpecs.size();
    int[] table = new int[size];
    int maximumFixedSpan = Integer.MAX_VALUE; // Could be 1
    for (int i = size - 1; i >= 0; i--) {
      FormSpec spec = (FormSpec) formSpecs.get(i); // ArrayList access
      if (spec.canGrow()) {
        maximumFixedSpan = 0;
      }
      table[i] = maximumFixedSpan;
      if (maximumFixedSpan < Integer.MAX_VALUE) {
        maximumFixedSpan++;
      }
    }
    return table;
  }

  // Helper Code ************************************************************
  /**
   * Computes and returns the sum of integers in the given array of ints.
   *
   * @param sizes an array of ints to sum up
   * @return the sum of ints in the array
   */
  private static int sum(final int[] sizes) {
    int sum = 0;
    for (int i = sizes.length - 1; i >= 0; i--) {
      sum += sizes[i];
    }
    return sum;
  }

  private static void invalidateAndRepaint(Container container) {
    if (container == null) {
      return;
    }
    if (container instanceof JComponent) {
      ((JComponent) container).revalidate();
    } else {
      container.invalidate();
    }
    container.repaint();
  }

  /**
   * Checks and answers whether the given component with the specified CellConstraints shall be
   * taken into account for the layout.
   *
   * @param component the component to test
   * @param cc the component's associated CellConstraints
   * @return {@code true} if a) {@code component} is visible, or b) {@code component} has no
   *     individual setting and the container-wide settings ignores the visibility, or c) {@code cc}
   *     indicates that this individual component ignores the visibility.
   */
  private boolean takeIntoAccount(Component component, CellConstraints cc) {
    return component.isVisible()
        || cc.honorsVisibility == null && !getHonorsVisibility()
        || Boolean.FALSE.equals(cc.honorsVisibility);
  }

  // Measuring Component Sizes ********************************************
  /**
   * An interface that describes how to measure a {@code Component}. Used to abstract from
   * horizontal and vertical dimensions as well as minimum and preferred sizes.
   *
   * @since 1.1
   */
  public static interface Measure {

    /**
     * Computes and returns the size of the given {@code Component}.
     *
     * @param component the component to measure
     * @return the component's size
     */
    int sizeOf(Component component);
  }

  /** An abstract implementation of the {@code Measure} interface that caches component sizes. */
  private abstract static class CachingMeasure implements Measure, Serializable {

    /**
     * Holds previously requested component sizes. Used to minimize size requests to subcomponents.
     */
    protected final ComponentSizeCache cache;

    private CachingMeasure(ComponentSizeCache cache) {
      this.cache = cache;
    }
  }

  /** Measures a component by computing its minimum width. */
  private static final class MinimumWidthMeasure extends CachingMeasure {

    private MinimumWidthMeasure(ComponentSizeCache cache) {
      super(cache);
    }

    @Override
    public int sizeOf(Component c) {
      return cache.getMinimumSize(c).width;
    }
  }

  /** Measures a component by computing its minimum height. */
  private static final class MinimumHeightMeasure extends CachingMeasure {

    private MinimumHeightMeasure(ComponentSizeCache cache) {
      super(cache);
    }

    @Override
    public int sizeOf(Component c) {
      return cache.getMinimumSize(c).height;
    }
  }

  /** Measures a component by computing its preferred width. */
  private static final class PreferredWidthMeasure extends CachingMeasure {

    private PreferredWidthMeasure(ComponentSizeCache cache) {
      super(cache);
    }

    @Override
    public int sizeOf(Component c) {
      return cache.getPreferredSize(c).width;
    }
  }

  /** Measures a component by computing its preferred height. */
  private static final class PreferredHeightMeasure extends CachingMeasure {

    private PreferredHeightMeasure(ComponentSizeCache cache) {
      super(cache);
    }

    @Override
    public int sizeOf(Component c) {
      return cache.getPreferredSize(c).height;
    }
  }

  // Caching Component Sizes **********************************************
  /**
   * A cache for component minimum and preferred sizes. Used to reduce the requests to determine a
   * component's size.
   */
  private static final class ComponentSizeCache implements Serializable {

    /** Maps components to their minimum sizes. */
    private final Map<Component, Dimension> minimumSizes;

    /** Maps components to their preferred sizes. */
    private final Map<Component, Dimension> preferredSizes;

    /**
     * Constructs a {@code ComponentSizeCache}.
     *
     * @param initialCapacity the initial cache capacity
     */
    private ComponentSizeCache(int initialCapacity) {
      minimumSizes = new HashMap<>(initialCapacity);
      preferredSizes = new HashMap<>(initialCapacity);
    }

    /** Invalidates the cache. Clears all stored size information. */
    void invalidate() {
      minimumSizes.clear();
      preferredSizes.clear();
    }

    /**
     * Returns the minimum size for the given component. Tries to look up the value from the cache;
     * lazily creates the value if it has not been requested before.
     *
     * @param component the component to compute the minimum size
     * @return the component's minimum size
     */
    Dimension getMinimumSize(Component component) {
      Dimension size = minimumSizes.get(component);
      if (size == null) {
        size = component.getMinimumSize();
        minimumSizes.put(component, size);
      }
      return size;
    }

    /**
     * Returns the preferred size for the given component. Tries to look up the value from the
     * cache; lazily creates the value if it has not been requested before.
     *
     * @param component the component to compute the preferred size
     * @return the component's preferred size
     */
    Dimension getPreferredSize(Component component) {
      Dimension size = preferredSizes.get(component);
      if (size == null) {
        size = component.getPreferredSize();
        preferredSizes.put(component, size);
      }
      return size;
    }

    void removeEntry(Component component) {
      minimumSizes.remove(component);
      preferredSizes.remove(component);
    }
  }

  // Exposing the Layout Information **************************************
  /**
   * Computes and returns the horizontal and vertical grid origins. Performs the same layout process
   * as {@code #layoutContainer} but does not layout the components.
   *
   * <p>This method has been added only to make it easier to debug the form layout. <strong>You must
   * not call this method directly; It may be removed in a future release or the visibility may be
   * reduced.</strong>
   *
   * @param parent the {@code Container} to inspect
   * @return an object that comprises the grid x and y origins
   */
  public LayoutInfo getLayoutInfo(Container parent) {
    synchronized (parent.getTreeLock()) {
      initializeColAndRowComponentLists();
      Dimension size = parent.getSize();

      Insets insets = parent.getInsets();
      int totalWidth = size.width - insets.left - insets.right;
      int totalHeight = size.height - insets.top - insets.bottom;

      int[] x =
          computeGridOrigins(
              parent,
              totalWidth,
              insets.left,
              colSpecs,
              colComponents,
              colGroupIndices,
              minimumWidthMeasure,
              preferredWidthMeasure);
      int[] y =
          computeGridOrigins(
              parent,
              totalHeight,
              insets.top,
              rowSpecs,
              rowComponents,
              rowGroupIndices,
              minimumHeightMeasure,
              preferredHeightMeasure);
      return new LayoutInfo(x, y);
    }
  }

  /** Stores column and row origins. */
  public static final class LayoutInfo {

    /** Holds the origins of the columns. */
    public final int[] columnOrigins;

    /** Holds the origins of the rows. */
    public final int[] rowOrigins;

    private LayoutInfo(int[] xOrigins, int[] yOrigins) {
      this.columnOrigins = xOrigins;
      this.rowOrigins = yOrigins;
    }

    /**
     * Returns the layout's horizontal origin, the origin of the first column.
     *
     * @return the layout's horizontal origin, the origin of the first column.
     */
    public int getX() {
      return columnOrigins[0];
    }

    /**
     * Returns the layout's vertical origin, the origin of the first row.
     *
     * @return the layout's vertical origin, the origin of the first row.
     */
    public int getY() {
      return rowOrigins[0];
    }

    /**
     * Returns the layout's width, the size between the first and the last column origin.
     *
     * @return the layout's width.
     */
    public int getWidth() {
      return columnOrigins[columnOrigins.length - 1] - columnOrigins[0];
    }

    /**
     * Returns the layout's height, the size between the first and last row.
     *
     * @return the layout's height.
     */
    public int getHeight() {
      return rowOrigins[rowOrigins.length - 1] - rowOrigins[0];
    }
  }

  // Helper Code **********************************************************
  /**
   * Creates and returns a deep copy of the given array. Unlike {@code #clone} that performs a
   * shallow copy, this method copies both array levels.
   *
   * @param array the array to clone
   * @return a deep copy of the given array
   * @see Object#clone()
   */
  private static int[][] deepClone(int[][] array) {
    int[][] result = new int[array.length][];
    for (int i = 0; i < result.length; i++) {
      result[i] = array[i].clone();
    }
    return result;
  }

  // Serialization ********************************************************
  /**
   * In addition to the default serialization mechanism this class invalidates the component size
   * cache. The cache will be populated again after the deserialization. Also, the fields {@code
   * colComponents} and {@code rowComponents} have been marked as transient to exclude them from the
   * serialization.
   */
  private void writeObject(ObjectOutputStream out) throws IOException {
    invalidateCaches();
    out.defaultWriteObject();
  }

  // Debug Helper Code ****************************************************

  /*
  // Prints the given column widths and row heights.
  private void printSizes(String title, int[] colWidths, int[] rowHeights) {
      System.out.println();
      System.out.println(title);
      int totalWidth = 0;
      System.out.print("Column widths: ");
      for (int i=0; i < getColumnCount(); i++) {
          int width = colWidths[i];
          totalWidth += width;
          System.out.print(width + ", ");
      }
      System.out.println(" Total=" + totalWidth);

      int totalHeight = 0;
      System.out.print("Row heights:   ");
      for (int i=0; i < getRowCount(); i++) {
          int height = rowHeights[i];
          totalHeight += height;
          System.out.print(height + ", ");
      }
      System.out.println(" Total=" + totalHeight);
      System.out.println();
  }

   */
}
import com.github.lgooddatepicker.components.TimePickerSettings.TimeArea;
import com.github.lgooddatepicker.optionalusertools.PickerUtilities;
import com.github.lgooddatepicker.optionalusertools.TimeChangeListener;
import com.github.lgooddatepicker.optionalusertools.TimeVetoPolicy;
import com.github.lgooddatepicker.zinternaltools.CalculateMinimumTimeFieldSize;
import com.github.lgooddatepicker.zinternaltools.CustomPopup;
import com.github.lgooddatepicker.zinternaltools.InternalUtilities;
import com.github.lgooddatepicker.zinternaltools.TimeChangeEvent;
import com.github.lgooddatepicker.zinternaltools.TimeMenuPanel;
import com.github.lgooddatepicker.zinternaltools.TimeSpinnerTimer;
import com.privatejgoodies.forms.factories.CC;
import com.privatejgoodies.forms.layout.FormLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Insets;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.time.Instant;
import java.time.LocalTime;
import java.util.ArrayList;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
    }
  }

  /**
   * zSetAppropriateTextFieldMinimumWidth, This sets the minimum (and preferred) width of the time
   * picker text field. The width will be determined (or calculated) from the current time picker
   * settings, as described below.
   *
   * <p>If the programmer has not supplied a setting for the minimum size, then a default minimum
   * size will be calculated from the current display format (which includes the locale
   * information), and the font for valid times.
   *
   * <p>If the programmer has supplied a setting for the text field minimum size, then the
   * programmer supplied value will be used instead, unless a default override is allowed. (In this
   * case, the default value will only be used if the default setting is larger than the programmer
   * supplied setting).
   *
   * <p>For additional details, see the javadoc for this function:
   * TimePickerSettings.setSizeTextFieldMinimumWidthDefaultOverride().
   */
  public void zSetAppropriateTextFieldMinimumWidth() {
    Integer programmerSuppliedWidth = settings.getSizeTextFieldMinimumWidth();
    // Determine the appropriate minimum width for the text field.
    int minimumWidthPixels =
        CalculateMinimumTimeFieldSize.getFormattedTimeWidthInPixels(
            settings.getFormatForDisplayTime(), settings.fontValidTime, 0);
    if (programmerSuppliedWidth != null) {
      if (settings.getSizeTextFieldMinimumWidthDefaultOverride()) {
        minimumWidthPixels = Math.max(programmerSuppliedWidth, minimumWidthPixels);
      } else {
        minimumWidthPixels = programmerSuppliedWidth;
      }
    }
    // Apply the minimum and preferred width to the text field.
    // Note that we only change the width, not the height.
    Dimension newMinimumSize = timeTextField.getMinimumSize();
    newMinimumSize.width = minimumWidthPixels;
    timeTextField.setMinimumSize(newMinimumSize);
    Dimension newPreferredSize = timeTextField.getPreferredSize();
    newPreferredSize.width = minimumWidthPixels;
    timeTextField.setPreferredSize(newPreferredSize);
    this.validate();
  }

  /**
   * zEventTextFieldChanged, This is called whenever the text in the time picker text field has
   * changed, whether programmatically or by the user.
   *
   * <p>If the current text contains a valid time, it will be stored in the variable lastValidTime.
   * Otherwise, the lastValidTime will not be changed.
   *
   * <p>This will also call the function to indicate to the user if the currently text is a valid
   * time, invalid text, or a vetoed time. These indications are created by using font, color, and
   * background changes of the text field.
   */
  private void zEventTextFieldChanged() {
    // Skip this function if it should not be run.
    if (skipTextFieldChangedFunctionWhileTrue) {
      return;
    }
    // Gather some variables that we will need.
    String timeText = timeTextField.getText();
    boolean textIsEmpty = timeText.trim().isEmpty();
    TimeVetoPolicy vetoPolicy = settings.getVetoPolicy();
    boolean nullIsAllowed = settings.getAllowEmptyTimes();
    // If the text is not empty, then try to parse the time.
    LocalTime parsedTime = null;
    if (!textIsEmpty) {
      parsedTime =
          InternalUtilities.getParsedTimeOrNull(
              timeText,
              settings.getFormatForDisplayTime(),
              settings.getFormatForMenuTimes(),
              settings.formatsForParsing,
              settings.getLocale());
    }
    // If the time was parsed successfully, then check it against the veto policy.
    boolean timeIsVetoed = false;
    if (parsedTime != null) {
      timeIsVetoed = InternalUtilities.isTimeVetoed(vetoPolicy, parsedTime);
    }
    // If the time is a valid empty time, then set the last valid time to null.
    if (textIsEmpty && nullIsAllowed) {
      zInternalSetLastValidTimeAndNotifyListeners(null);
    }
    // If the time is a valid parsed time, then store the last valid time.
    if ((!textIsEmpty) && (parsedTime != null) && (timeIsVetoed == false)) {
      zInternalSetLastValidTimeAndNotifyListeners(parsedTime);
    }
    // Draw the time status indications for the user.
    zDrawTextFieldIndicators();
    // Fire the text change event for beans binding.
    firePropertyChange("text", null, timeTextField.getText());
  }

  /**
   * zEventToggleTimeMenuButtonMousePressed, This is called when the user clicks on the "toggle time
   * menu" button of the time picker.
   *
   * <p>This will create a time menu panel and a popup, and display them to the user. If a time menu
   * panel is already opened, it will be closed instead.
   */
  private void zEventToggleTimeMenuButtonMousePressed(MouseEvent e) {
    togglePopup();
  }

  /**
   * initComponents, This initializes the components of the JFormDesigner panel. This function is
   * automatically generated by JFormDesigner from the JFD form design file, and should not be
   * modified by hand. This function can be modified, if needed, by using JFormDesigner.
   */
  private void initComponents() {
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
    timeTextField = new JTextField();
    toggleTimeMenuButton = new JButton();
    spinnerPanel = new JPanel();
    increaseButton = new JButton();
    decreaseButton = new JButton();

    // ======== this ========
    setLayout(new FormLayout("pref:grow, 3*(pref)", "fill:pref:grow"));