1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.apple.laf;
  27 
  28 import java.awt.*;
  29 import java.util.Enumeration;
  30 
  31 import javax.swing.*;
  32 import javax.swing.border.Border;
  33 import javax.swing.plaf.*;
  34 import javax.swing.plaf.basic.BasicTableHeaderUI;
  35 import javax.swing.table.*;
  36 import com.apple.laf.ClientPropertyApplicator;
  37 import com.apple.laf.ClientPropertyApplicator.Property;
  38 import com.apple.laf.AquaUtils.RecyclableSingleton;
  39 
  40 public class AquaTableHeaderUI extends BasicTableHeaderUI {
  41     private int originalHeaderAlignment;
  42     protected int sortColumn;
  43     protected int sortOrder;
  44 
  45     public static ComponentUI createUI(final JComponent c) {
  46         return new AquaTableHeaderUI();
  47     }
  48 
  49     public void installDefaults() {
  50         super.installDefaults();
  51 
  52         final TableCellRenderer renderer = header.getDefaultRenderer();
  53         if (renderer instanceof UIResource && renderer instanceof DefaultTableCellRenderer) {
  54             final DefaultTableCellRenderer defaultRenderer = (DefaultTableCellRenderer)renderer;
  55             originalHeaderAlignment = defaultRenderer.getHorizontalAlignment();
  56             defaultRenderer.setHorizontalAlignment(SwingConstants.LEADING);
  57         }
  58     }
  59 
  60     public void uninstallDefaults() {
  61         final TableCellRenderer renderer = header.getDefaultRenderer();
  62         if (renderer instanceof UIResource && renderer instanceof DefaultTableCellRenderer) {
  63             final DefaultTableCellRenderer defaultRenderer = (DefaultTableCellRenderer)renderer;
  64             defaultRenderer.setHorizontalAlignment(originalHeaderAlignment);
  65         }
  66 
  67         super.uninstallDefaults();
  68     }
  69 
  70     static final RecyclableSingleton<ClientPropertyApplicator<JTableHeader, JTableHeader>> TABLE_HEADER_APPLICATORS = new RecyclableSingleton<ClientPropertyApplicator<JTableHeader, JTableHeader>>() {
  71         @Override
  72         @SuppressWarnings("unchecked")
  73         protected ClientPropertyApplicator<JTableHeader, JTableHeader> getInstance() {
  74             return new ClientPropertyApplicator<JTableHeader, JTableHeader>(
  75                     new Property<JTableHeader>("JTableHeader.selectedColumn") {
  76                         public void applyProperty(final JTableHeader target, final Object value) {
  77                             tickle(target, value, target.getClientProperty("JTableHeader.sortDirection"));
  78                         }
  79                     },
  80                     new Property<JTableHeader>("JTableHeader.sortDirection") {
  81                         public void applyProperty(final JTableHeader target, final Object value) {
  82                             tickle(target, target.getClientProperty("JTableHeader.selectedColumn"), value);
  83                         }
  84                     }
  85             );
  86         }
  87     };
  88     static ClientPropertyApplicator<JTableHeader, JTableHeader> getTableHeaderApplicators() {
  89         return TABLE_HEADER_APPLICATORS.get();
  90     }
  91 
  92     static void tickle(final JTableHeader target, final Object selectedColumn, final Object direction) {
  93         final TableColumn tableColumn = getTableColumn(target, selectedColumn);
  94         if (tableColumn == null) return;
  95 
  96         int sortDirection = 0;
  97         if ("ascending".equalsIgnoreCase(direction+"")) {
  98             sortDirection = 1;
  99         } else if ("descending".equalsIgnoreCase(direction+"")) {
 100             sortDirection = -1;
 101         } else if ("decending".equalsIgnoreCase(direction+"")) {
 102             sortDirection = -1; // stupid misspelling that GM'ed in 10.5.0
 103         }
 104 
 105         final TableHeaderUI headerUI = target.getUI();
 106         if (headerUI == null || !(headerUI instanceof AquaTableHeaderUI)) return;
 107 
 108         final AquaTableHeaderUI aquaHeaderUI = (AquaTableHeaderUI)headerUI;
 109         aquaHeaderUI.sortColumn = tableColumn.getModelIndex();
 110         aquaHeaderUI.sortOrder = sortDirection;
 111         final AquaTableCellRenderer renderer = aquaHeaderUI.new AquaTableCellRenderer();
 112         tableColumn.setHeaderRenderer(renderer);
 113     }
 114 
 115     @SuppressWarnings("serial") // Superclass is not serializable across versions
 116     class AquaTableCellRenderer extends DefaultTableCellRenderer implements UIResource {
 117         public Component getTableCellRendererComponent(final JTable localTable, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) {
 118             if (localTable != null) {
 119                 if (header != null) {
 120                     setForeground(header.getForeground());
 121                     setBackground(header.getBackground());
 122                     setFont(UIManager.getFont("TableHeader.font"));
 123                 }
 124             }
 125 
 126             setText((value == null) ? "" : value.toString());
 127 
 128             // Modify the table "border" to draw smaller, and with the titles in the right position
 129             // and sort indicators, just like an NSSave/Open panel.
 130             final AquaTableHeaderBorder cellBorder = AquaTableHeaderBorder.getListHeaderBorder();
 131             final boolean thisColumnSelected = localTable.getColumnModel().getColumn(column).getModelIndex() == sortColumn;
 132 
 133             cellBorder.setSelected(thisColumnSelected);
 134             if (thisColumnSelected) {
 135                 cellBorder.setSortOrder(sortOrder);
 136             } else {
 137                 cellBorder.setSortOrder(AquaTableHeaderBorder.SORT_NONE);
 138             }
 139             setBorder(cellBorder);
 140             return this;
 141         }
 142     }
 143 
 144     protected static TableColumn getTableColumn(final JTableHeader target, final Object value) {
 145         if (value == null || !(value instanceof Integer)) return null;
 146         final int columnIndex = ((Integer)value).intValue();
 147 
 148         final TableColumnModel columnModel = target.getColumnModel();
 149         if (columnIndex < 0 || columnIndex >= columnModel.getColumnCount()) return null;
 150 
 151         return columnModel.getColumn(columnIndex);
 152     }
 153 
 154     protected static AquaTableHeaderBorder getAquaBorderFrom(final JTableHeader header, final TableColumn column) {
 155         final TableCellRenderer renderer = column.getHeaderRenderer();
 156         if (renderer == null) return null;
 157 
 158         final Component c = renderer.getTableCellRendererComponent(header.getTable(), column.getHeaderValue(), false, false, -1, column.getModelIndex());
 159         if (!(c instanceof JComponent)) return null;
 160 
 161         final Border border = ((JComponent)c).getBorder();
 162         if (!(border instanceof AquaTableHeaderBorder)) return null;
 163 
 164         return (AquaTableHeaderBorder)border;
 165     }
 166 
 167     protected void installListeners() {
 168         super.installListeners();
 169         getTableHeaderApplicators().attachAndApplyClientProperties(header);
 170     }
 171 
 172     protected void uninstallListeners() {
 173         getTableHeaderApplicators().removeFrom(header);
 174         super.uninstallListeners();
 175     }
 176 
 177     private int getHeaderHeightAqua() {
 178         int height = 0;
 179         boolean accomodatedDefault = false;
 180 
 181         final TableColumnModel columnModel = header.getColumnModel();
 182         for (int column = 0; column < columnModel.getColumnCount(); column++) {
 183             final TableColumn aColumn = columnModel.getColumn(column);
 184             // Configuring the header renderer to calculate its preferred size is expensive.
 185             // Optimise this by assuming the default renderer always has the same height.
 186             if (aColumn.getHeaderRenderer() != null || !accomodatedDefault) {
 187                 final Component comp = getHeaderRendererAqua(column);
 188                 final int rendererHeight = comp.getPreferredSize().height;
 189                 height = Math.max(height, rendererHeight);
 190                 // If the header value is empty (== "") in the
 191                 // first column (and this column is set up
 192                 // to use the default renderer) we will
 193                 // return zero from this routine and the header
 194                 // will disappear altogether. Avoiding the calculation
 195                 // of the preferred size is such a performance win for
 196                 // most applications that we will continue to
 197                 // use this cheaper calculation, handling these
 198                 // issues as `edge cases'.
 199 
 200                 // Mac OS X Change - since we have a border on our renderers
 201                 // it is possible the height of an empty header could be > 0,
 202                 // so we chose the relatively safe number of 4 to handle this case.
 203                 // Now if we get a size of 4 or less we assume it is empty and measure
 204                 // a different header.
 205                 if (rendererHeight > 4) {
 206                     accomodatedDefault = true;
 207                 }
 208             }
 209         }
 210         return height;
 211     }
 212 
 213     private Component getHeaderRendererAqua(final int columnIndex) {
 214         final TableColumn aColumn = header.getColumnModel().getColumn(columnIndex);
 215         TableCellRenderer renderer = aColumn.getHeaderRenderer();
 216         if (renderer == null) {
 217             renderer = header.getDefaultRenderer();
 218         }
 219         return renderer.getTableCellRendererComponent(header.getTable(), aColumn.getHeaderValue(), false, false, -1, columnIndex);
 220     }
 221 
 222     private Dimension createHeaderSizeAqua(long width) {
 223         // None of the callers include the intercell spacing, do it here.
 224         if (width > Integer.MAX_VALUE) {
 225             width = Integer.MAX_VALUE;
 226         }
 227         return new Dimension((int)width, getHeaderHeightAqua());
 228     }
 229 
 230     /**
 231      * Return the minimum size of the header. The minimum width is the sum of the minimum widths of each column (plus
 232      * inter-cell spacing).
 233      */
 234     public Dimension getMinimumSize(final JComponent c) {
 235         long width = 0;
 236         final Enumeration<TableColumn> enumeration = header.getColumnModel().getColumns();
 237         while (enumeration.hasMoreElements()) {
 238             final TableColumn aColumn = enumeration.nextElement();
 239             width = width + aColumn.getMinWidth();
 240         }
 241         return createHeaderSizeAqua(width);
 242     }
 243 
 244     /**
 245      * Return the preferred size of the header. The preferred height is the maximum of the preferred heights of all of
 246      * the components provided by the header renderers. The preferred width is the sum of the preferred widths of each
 247      * column (plus inter-cell spacing).
 248      */
 249     public Dimension getPreferredSize(final JComponent c) {
 250         long width = 0;
 251         final Enumeration<TableColumn> enumeration = header.getColumnModel().getColumns();
 252         while (enumeration.hasMoreElements()) {
 253             final TableColumn aColumn = enumeration.nextElement();
 254             width = width + aColumn.getPreferredWidth();
 255         }
 256         return createHeaderSizeAqua(width);
 257     }
 258 }