All Downloads are FREE. Search and download functionalities are using the official Maven repository.

VAqua.src.org.violetlib.aqua.AquaTableUI Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2014-2024 Alan Snyder.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the license agreement. For details see
 * accompanying license terms.
 */

/*
 * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package org.violetlib.aqua;

import java.awt.*;
import java.awt.dnd.*;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.EventObject;
import java.util.TooManyListenersException;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.MouseInputListener;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicTableUI;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import org.jetbrains.annotations.*;
import org.violetlib.jnr.aqua.AquaUIPainter;

import static org.violetlib.jnr.aqua.AquaUIPainter.State.*;

/**
 * A table UI based on AquaTableUI for Yosemite. It implements the striped style. It paints the selection background
 * behind the entire selected row, to avoid gaps between cells. It disables the grid by default. It displays using an
 * inactive style when not the focus owner. It works around a problem in JTable that interprets Meta (Command) as an
 * ordinary key instead of a modifier. It avoids installing a non-focused cell editor. Where possible, it substitutes
 * default cell renderers and editors for ones that conflict with the native appearance and behavior.
 *
 * For best results using the striped style, cell renderer components should not be opaque, and the table should use
 * auto resizing and setFillsViewportHeight(true).
 */
public class AquaTableUI extends BasicTableUI
  implements SelectionRepaintable, AquaComponentUI, AquaViewStyleContainerUI {
    public static ComponentUI createUI(JComponent c) {
        return new AquaTableUI();
    }

    public static final String TABLE_STYLE_KEY = "JTable.style";
    public static final String QUAQUA_TABLE_STYLE_KEY = "Quaqua.Table.style";
    public static final String TABLE_VIEW_STYLE_KEY = "JTable.viewStyle";
    public static final String INSET_VIEW_MARGIN_KEY = "Aqua.insetViewMargin";
    public static final String INSET_VIEW_VERTICAL_MARGIN_KEY = "Aqua.insetViewVerticalMargin";
    public static final String USE_SHORT_DROP_LINE_COLOR_KEY = "JTable.useShortDropLineColor";

    protected final PropertyChangeListener propertyChangeListener;
    protected final ListSelectionListener selectionListener;
    private @Nullable CellEditorFocusManager cellEditorFocusManager;
    private @Nullable TableCellRenderer originalBooleanRenderer;
    private @Nullable TableCellEditor originalObjectEditor;
    private @Nullable TableCellEditor originalNumberEditor;
    private @Nullable TableCellEditor originalBooleanEditor;
    protected AquaTablePainter painter;

    private boolean isStriped = false;
    private boolean isInset = false;
    private boolean isDropActive;

    private int insetMargin = 15;
    private int insetVerticalMargin = 5;
    protected @NotNull ContainerContextualColors colors;
    protected @Nullable AppearanceContext appearanceContext;
    private DropTargetListener dropTargetListener;
    private DropTarget knownDropTarget;
    protected @Nullable Color actualTableBackground;
    protected boolean useShortDropLineColor;

    public AquaTableUI() {
        propertyChangeListener = new TablePropertyChangeListener();
        selectionListener = new SelectionListener();
        cellEditorFocusManager = new CellEditorFocusManager();
        this.colors = AquaColors.CONTAINER_COLORS;
    }

    protected FocusListener createFocusListener() {
        return new AquaTableUI.FocusHandler();
    }

    protected MouseInputListener createMouseInputListener() {
        return new AquaTableUI.MouseInputHandler();
    }

    public class FocusHandler implements FocusListener {
        public void focusGained(FocusEvent e) {
            focusChanged();
        }

        public void focusLost(FocusEvent e) {
            focusChanged();
        }

        private void focusChanged() {
            configureAppearanceContext(null);
        }
    }

    /**
     * This class removes the cell editor when it permanently loses focus. It monitors the cell editor property of the
     * table to attach a focus listener to the active cell editor component. It also ensures that the component of a
     * newly installed cell editor requests focus.
     * 

* The superclass CellEditorRemover fails to remove the editor when the focus is transferred to the table itself or * the focus owner is cleared. The latter case is a consequence of implementing the remover by monitoring the * permanent focus owner property of the keyboard focus manager, which may report a null value during a focus * transfer. */ protected class CellEditorFocusManager implements FocusListener { private @Nullable Component managedCellEditorComponent; private @Nullable Component registeredFocusOwner; public void cellEditorChanged(@Nullable TableCellEditor oldEditor, @Nullable TableCellEditor currentEditor) { detach(); if (currentEditor != null) { // In some cases, such as direct calls to editCellAt, the cell editor does not request focus managedCellEditorComponent = table.getEditorComponent(); registeredFocusOwner = managedCellEditorComponent; registeredFocusOwner.requestFocus(); registeredFocusOwner.addFocusListener(this); } } public void detach() { if (registeredFocusOwner != null) { registeredFocusOwner.removeFocusListener(this); registeredFocusOwner = null; } managedCellEditorComponent = null; } @Override public void focusGained(FocusEvent e) { } @Override public void focusLost(FocusEvent e) { if (!e.isTemporary()) { TableCellEditor editor = table.getCellEditor(); if (editor != null) { Component opposite = e.getOppositeComponent(); if (opposite != null && isPartOfEditor(table.getEditorComponent(), opposite)) { // The focus transfer is to a component within the cell editor. // Transfer this focus listener to that component. assert registeredFocusOwner != null; registeredFocusOwner.removeFocusListener(this); registeredFocusOwner = opposite; registeredFocusOwner.addFocusListener(this); return; } // The cell editor is losing focus, which should stop editing. if (!editor.stopCellEditing()) { editor.cancelCellEditing(); } } else { assert registeredFocusOwner != null; registeredFocusOwner.removeFocusListener(this); managedCellEditorComponent = null; } } } private boolean isPartOfEditor(@NotNull Component editorComponent, @NotNull Component c) { while (c != null && c != table) { if (c == editorComponent) { return true; } c = c.getParent(); } return false; } } protected class TablePropertyChangeListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent ev) { String pn = ev.getPropertyName(); if (pn != null) { if (pn.equals("enabled")) { configureAppearanceContext(null); } else if (AquaFocusHandler.DISPLAY_AS_FOCUSED_KEY.equals(pn)) { configureAppearanceContext(null); } else if (pn.equals("selectionModel")) { ListSelectionModel old = (ListSelectionModel) ev.getOldValue(); updateSelectionListener(old); } else if (isStyleProperty(pn)) { updateStriped(); } else if (isViewStyleProperty(pn)) { updateInset(); } else if (pn.equals("showHorizontalLines") || pn.equals("rowMargin")) { updateInset(); } else if (pn.equals("background")) { repaintScrollPaneCorner(); } else if (pn.equals("tableCellEditor")) { if (cellEditorFocusManager != null) { TableCellEditor oldEditor = (TableCellEditor) ev.getOldValue(); TableCellEditor editor = (TableCellEditor) ev.getNewValue(); cellEditorFocusManager.cellEditorChanged(oldEditor, editor); } } else if ("transferHandler".equals(pn)) { configureDropTargetListener(); } else if (pn.equals(USE_SHORT_DROP_LINE_COLOR_KEY)) { useShortDropLineColor = Boolean.TRUE.equals(table.getClientProperty(USE_SHORT_DROP_LINE_COLOR_KEY)); } } } } @Override protected void installDefaults() { super.installDefaults(); painter = new AquaTablePainter(table, rendererPane); table.putClientProperty("terminateEditOnFocusLost", true); table.putClientProperty("JTable.autoStartsEdit", false); // do not simulate an open cell editor table.putClientProperty(AquaCellEditorPolicy.IS_CELL_CONTAINER_PROPERTY, true); table.setShowHorizontalLines(false); table.setShowVerticalLines(false); LookAndFeel.installProperty(table, "rowHeight", 19); originalBooleanRenderer = installRendererIfPossible(Boolean.class, AquaBooleanRenderer.class); originalObjectEditor = installEditorIfPossible(Object.class, AquaObjectEditor.class); originalNumberEditor = installEditorIfPossible(Number.class, AquaNumberEditor.class); originalBooleanEditor = installEditorIfPossible(Boolean.class, AquaBooleanEditor.class); isStriped = getStripedValue(); configureAppearanceContext(null); updateInset(); useShortDropLineColor = Boolean.TRUE.equals(table.getClientProperty(USE_SHORT_DROP_LINE_COLOR_KEY)); } @Override protected void uninstallDefaults() { TableCellRenderer booleanRenderer = table.getDefaultRenderer(Boolean.class); if (booleanRenderer instanceof AquaBooleanRenderer) { table.setDefaultRenderer(Boolean.class, originalBooleanRenderer); } TableCellEditor objectEditor = table.getDefaultEditor(Object.class); if (objectEditor instanceof AquaObjectEditor) { table.setDefaultEditor(Object.class, originalObjectEditor); } TableCellEditor numberEditor = table.getDefaultEditor(Number.class); if (numberEditor instanceof AquaNumberEditor) { table.setDefaultEditor(Number.class, originalNumberEditor); } TableCellEditor booleanEditor = table.getDefaultEditor(Boolean.class); if (booleanEditor instanceof AquaBooleanEditor) { table.setDefaultEditor(Boolean.class, originalBooleanEditor); } painter = null; table.putClientProperty(INSET_VIEW_MARGIN_KEY, null); super.uninstallDefaults(); } @Override protected void installListeners() { super.installListeners(); table.addPropertyChangeListener(propertyChangeListener); updateSelectionListener(null); AppearanceManager.installListeners(table); AquaUtils.installInsetViewListener(table); } @Override protected void uninstallListeners() { if (knownDropTarget != null) { knownDropTarget.removeDropTargetListener(dropTargetListener); knownDropTarget = null; } AquaUtils.uninstallInsetViewListener(table); AppearanceManager.uninstallListeners(table); table.getSelectionModel().removeListSelectionListener(selectionListener); table.removePropertyChangeListener(propertyChangeListener); cellEditorFocusManager.detach(); super.uninstallListeners(); } // TODO: Using default handler for now, need to handle cmd-key // Replace the mouse event with one that returns the cmd-key state when asked // for the control-key state, which super assumes is what everyone does to discontiguously extend selections public class MouseInputHandler extends BasicTableUI.MouseInputHandler { /*public void mousePressed(MouseEvent e) { super.mousePressed(new SelectionMouseEvent(e)); } public void mouseDragged(MouseEvent e) { super.mouseDragged(new SelectionMouseEvent(e)); }*/ } private void configureDropTargetListener() { if (knownDropTarget != null) { knownDropTarget.removeDropTargetListener(dropTargetListener); knownDropTarget = null; } if (dropTargetListener == null) { dropTargetListener = new MyDropTargetListener(); } knownDropTarget = table.getDropTarget(); if (knownDropTarget != null) { try { knownDropTarget.addDropTargetListener(dropTargetListener); } catch (TooManyListenersException ignore) { } } } protected class MyDropTargetListener implements DropTargetListener { @Override public void dragEnter(DropTargetDragEvent dtde) { isDropActive = true; repaintDropTarget(); } @Override public void dragOver(DropTargetDragEvent dtde) { } @Override public void dropActionChanged(DropTargetDragEvent dtde) { } @Override public void dragExit(DropTargetEvent dte) { isDropActive = false; repaintDropTarget(); } @Override public void drop(DropTargetDropEvent dtde) { isDropActive = false; repaintDropTarget(); } } protected void repaintDropTarget() { SwingUtilities.invokeLater(() -> table.repaint()); } protected void updateSelectionListener(ListSelectionModel old) { if (old != null) { old.removeListSelectionListener(selectionListener); } table.getSelectionModel().addListSelectionListener(selectionListener); } /** * Because JTable takes charge of repainting on a row selection change instead of deferring to the TableUI, we must * duplicate its code to ensure that the full width of the table is repainted, not just the cells. */ protected class SelectionListener implements ListSelectionListener { @Override public void valueChanged(ListSelectionEvent e) { if (table.getRowCount() <= 0 || table.getColumnCount() <= 0) { return; } int firstIndex = limit(e.getFirstIndex(), 0, table.getRowCount() - 1); int lastIndex = limit(e.getLastIndex(), 0, table.getRowCount() - 1); if (isInset() && table.getSelectionModel().getSelectionMode() != ListSelectionModel.SINGLE_SELECTION) { // When the inset selection style is used, a change to the selection can affect rows that are not // within the range of changed rows. Consider an isolated selected row in a table that supports // multiple selection. If a row adjacent to that row is selected, then the previously isolated row is // now the top or bottom row of a group, so its rendering must change. int[] selectedRows = table.getSelectedRows(); if (selectedRows.length > 0) { int firstSelectedRow = selectedRows[0]; int lastSelectedRow = selectedRows[selectedRows.length-1]; if (firstSelectedRow < firstIndex) { firstIndex = firstSelectedRow; } if (lastSelectedRow > lastIndex) { lastIndex = lastSelectedRow; } } } Rectangle firstRowRect = table.getCellRect(firstIndex, 0, true); Rectangle lastRowRect = table.getCellRect(lastIndex, 0, true); Rectangle dirtyRegion = new Rectangle(0, firstRowRect.y, table.getWidth(), lastRowRect.y + lastRowRect.height - firstRowRect.y); table.repaint(dirtyRegion); } protected int limit(int i, int a, int b) { return Math.min(b, Math.max(i, a)); } } @Override public void appearanceChanged(@NotNull JComponent c, @NotNull AquaAppearance appearance) { configureAppearanceContext(appearance); } @Override public void activeStateChanged(@NotNull JComponent c, boolean isActive) { configureAppearanceContext(null); } protected void configureAppearanceContext(@Nullable AquaAppearance appearance) { if (appearance == null) { appearance = AppearanceManager.ensureAppearance(table); } AquaUIPainter.State state = getState(); appearanceContext = new AppearanceContext(appearance, state, false, false); colors = isStriped ? AquaColors.STRIPED_CONTAINER_COLORS : AquaColors.CONTAINER_COLORS; colors.configureForContainer(); actualTableBackground = colors.getBackground(appearanceContext); AquaColors.installColors(table, appearanceContext, colors); // JTable forces opaque to be true, so LookAndFeel.installProperty cannot be used table.setOpaque(!isStriped); table.repaint(); } protected AquaUIPainter.State getState() { return table.isEnabled() ? (shouldDisplayAsFocused() ? ACTIVE_DEFAULT : ACTIVE) : DISABLED; } protected boolean shouldDisplayAsFocused() { return AquaFocusHandler.isActive(table) && (AquaFocusHandler.hasFocus(table) || table.isEditing()); } private void updateStriped() { boolean value = getStripedValue(); if (value != isStriped) { isStriped = value; configureAppearanceContext(null); } } private boolean getStripedValue() { String value = getStyleProperty(table); return "striped".equals(value) && isBackgroundClear(); } @Override public void scrollPaneAncestorChanged(@Nullable JScrollPane sp) { } private void updateInset() { boolean value = getInsetValue(); if (value != isInset) { isInset = value; int margin = isInset ? insetMargin : 0; int verticalMargin = isInset ? insetVerticalMargin : 0; table.putClientProperty(INSET_VIEW_MARGIN_KEY, margin); table.putClientProperty(INSET_VIEW_VERTICAL_MARGIN_KEY, verticalMargin); table.setRowMargin(isInset ? 0 : 1); table.revalidate(); table.repaint(); } } private boolean getInsetValue() { // The inset view style is inhibited if the row margin is greater than 1 or horizontal lines are displayed. When // the inset view style is installed, the row margin is set to zero, to allow joining of adjacent selected // rows. if (AquaUtils.isInsetViewSupported()) { String value = getViewStyleProperty(); return "inset".equals(value) && table.getRowMargin() <= 1 && !table.getShowHorizontalLines(); } return false; } public boolean isStriped() { return isStriped; } @Override public boolean isInset() { return isInset; } private boolean isBackgroundClear() { Color c = table.getBackground(); return c == null || c.getAlpha() == 0 || c instanceof ColorUIResource; } protected boolean tableHasFocus() { if (table.isEditing()) { return AquaFocusHandler.isActive(table); } else { return AquaFocusHandler.hasFocus(table); } } protected boolean isStyleProperty(String prop) { return AquaUtils.isProperty(prop, TABLE_STYLE_KEY, QUAQUA_TABLE_STYLE_KEY); } protected static String getStyleProperty(JTable table) { return AquaUtils.getProperty(table, TABLE_STYLE_KEY, QUAQUA_TABLE_STYLE_KEY); } protected boolean isViewStyleProperty(String prop) { return AquaUtils.isProperty(prop, TABLE_VIEW_STYLE_KEY); } protected String getViewStyleProperty() { return AquaUtils.getProperty(table, TABLE_VIEW_STYLE_KEY); } @Override public @NotNull Dimension getMinimumSize(JComponent c) { Dimension d = super.getMinimumSize(c); return isInset ? expand(d) : d; } @Override public @NotNull Dimension getPreferredSize(JComponent c) { Dimension d = super.getPreferredSize(c); return isInset ? expand(d) : d; } @Override public @NotNull Dimension getMaximumSize(JComponent c) { Dimension d = super.getMaximumSize(c); return isInset ? expand(d) : d; } private @NotNull Dimension expand(@NotNull Dimension d) { long expandedWidth = (long) d.width + 2 * insetMargin; if (expandedWidth > Integer.MAX_VALUE) { expandedWidth = Integer.MAX_VALUE; } long expandedHeight = (long) d.height + 2 * insetVerticalMargin; if (expandedHeight > Integer.MAX_VALUE) { expandedHeight = Integer.MAX_VALUE; } return new Dimension((int) expandedWidth, (int) expandedHeight); } /** * This method is called after a possible change to the state that affects the display of selected cells. */ @Override public void repaintSelection() { // All of the selected cells must be repainted when the focus/active/enabled state changes, because the selected // cell background depends upon these states. configureAppearanceContext(null); } @Override public void update(Graphics g, JComponent c) { AppearanceManager.registerCurrentAppearance(c); Color background = getBackgroundColor(); if (background != null) { g.setColor(background); g.fillRect(0, 0, c.getWidth(), c.getHeight()); } paint(g, c); } private @Nullable Color getBackgroundColor() { if (table.isOpaque()) { if (isStriped && actualTableBackground != null) { // The dark mode stripes presume a dark background. return actualTableBackground; } else { return table.getBackground(); } } return null; } public void repaintScrollPaneCorner() { Container p1 = table.getParent(); if (p1 instanceof JViewport) { Container p2 = p1.getParent(); if (p2 instanceof JScrollPane) { JScrollPane sp = (JScrollPane) p2; Component corner = sp.getCorner(JScrollPane.UPPER_TRAILING_CORNER); if (corner != null) { corner.repaint(); } } } } @Override public void paint(Graphics g, JComponent c) { if (painter != null && appearanceContext != null) { painter.paint(g, c); } } protected class AquaTablePainter extends BasicTableUIPainter { protected boolean tableHasFocus; protected boolean isDropTarget; protected boolean hasSelection; protected boolean isSelectionMuted; protected boolean hasDropOnTarget; public AquaTablePainter(JTable table, CellRendererPane rendererPane) { super(table, rendererPane); } public void paint(Graphics g, JComponent c) { assert appearanceContext != null; tableHasFocus = tableHasFocus(); hasSelection = table.getSelectedRowCount() > 0 && table.getRowSelectionAllowed() || table.getSelectedColumnCount() > 0 && table.getColumnSelectionAllowed(); isDropTarget = false; isSelectionMuted = false; hasDropOnTarget = false; if (isDropActive) { JTable.DropLocation loc = table.getDropLocation(); if (loc != null) { // Drop target highlighting uses the accent color. // To avoid confusion, selections should not also use the accent color. isSelectionMuted = hasSelection; if (!loc.isInsertRow() && !loc.isInsertColumn() && (loc.getRow() >= 0 || loc.getColumn() >= 0)) { hasDropOnTarget = true; } } } // Most of the following code is copied from BasicTableUI, with minor changes. Rectangle clip = g.getClipBounds(); Rectangle bounds = table.getBounds(); // account for the fact that the graphics has already been translated // into the table's bounds bounds.x = bounds.y = 0; // The following is changed because we want to paint stripes even if there are no rows or columns. if (!bounds.intersects(clip)) { return; } boolean ltr = table.getComponentOrientation().isLeftToRight(); Point upperLeft = clip.getLocation(); Point lowerRight = new Point(clip.x + clip.width - 1, clip.y + clip.height - 1); int rMin = table.rowAtPoint(upperLeft); int rMax = table.rowAtPoint(lowerRight); boolean extendVerticalGrid = false; // This should never happen (as long as our bounds intersect the clip, // which is why we bail above if that is the case). if (rMin == -1) { rMin = 0; } // If the table does not have enough rows to fill the view we'll get -1. // (We could also get -1 if our bounds don't intersect the clip, // which is why we bail above if that is the case). if (rMax == -1) { rMax = table.getRowCount() - 1; extendVerticalGrid = true; } int cMin = table.columnAtPoint(ltr ? upperLeft : lowerRight); int cMax = table.columnAtPoint(ltr ? lowerRight : upperLeft); boolean extendHorizontalGrid = false; // This should never happen. if (cMin == -1) { cMin = 0; } // If the table does not have enough columns to fill the view we'll get -1. if (cMax == -1) { cMax = table.getColumnCount() - 1; extendHorizontalGrid = true; } colors.configureForContainer(); if (isStriped || hasSelection || hasDropOnTarget) { paintBackground(g, rMin, rMax, cMin, cMax); } paintGrid(g, rMin, rMax, cMin, cMax, extendVerticalGrid, extendHorizontalGrid); paintCells(g, rMin, rMax, cMin, cMax); colors.configureForContainer(); JTable.DropLocation loc = table.getDropLocation(); if (loc != null) { paintDropLines(g); } } @Override protected @Nullable Color getDropLineColor() { assert appearanceContext != null; boolean isDark = appearanceContext.getAppearance().isDark(); Color color = appearanceContext.getAppearance().getColor("controlAccent"); if (color == null) { color = isDark ? new Color(200, 200, 200) : new Color(100, 100, 100); } return color; } @Override protected @Nullable Color getShortDropLineColor() { if (useShortDropLineColor) { assert appearanceContext != null; boolean isDark = appearanceContext.getAppearance().isDark(); Color color = appearanceContext.getAppearance().getColor("controlAccent"); if (color == null) { color = isDark ? Color.WHITE : Color.BLACK; } else if (isDark) { color = color.brighter(); } else { color = color.darker(); } return color; } return null; } @Override protected void paintDropLines(Graphics g, @Nullable Color color, @Nullable Color shortColor) { super.paintDropLines(g, color, shortColor); JTable.DropLocation loc = table.getDropLocation(); assert loc != null; if (loc.getRow() < 0 && loc.getColumn() < 0) { // drop on table Color c = getDropLineColor(); if (c != null) { g.setColor(c); Rectangle bounds = table.getVisibleRect(); g.fillRect(bounds.x - 1, bounds.y - 1, bounds.width + 2, 3); g.fillRect(bounds.x - 1, bounds.y + bounds.height - 2, bounds.width + 2, 3); g.fillRect(bounds.x - 1, bounds.y + 2, 3, bounds.height - 4); g.fillRect(bounds.x + bounds.width - 2, bounds.y + 2, 3, bounds.height - 4); } } } protected void paintBackground(Graphics g, int rMin, int rMax, int cMin, int cMax) { Graphics2D gg = (Graphics2D) g; Rectangle clip = g.getClipBounds(); boolean isRowSelection = table.getSelectedRowCount() > 0 && table.getRowSelectionAllowed() && !table.getColumnSelectionAllowed(); boolean isColumnSelection = table.getSelectedColumnCount() > 0 && table.getColumnSelectionAllowed() && !table.getRowSelectionAllowed(); assert appearanceContext != null; // Note: the table is configured with colors for the entire table, not with colors for individual rows int nextRowY = 0; boolean isEditing = table.isEditing(); int editingRow = table.getEditingRow(); int editingColumn = table.getEditingColumn(); int tableWidth = table.getWidth(); for (int row = rMin; row <= rMax; row++) { Rectangle cellRect = table.getCellRect(row, cMin, true); boolean isSelected = isRowSelection && table.isRowSelected(row); if (isStriped) { colors.configureForRow(row, isSelected); } else { colors.configureForRow(isSelected); } AppearanceContext ac = appearanceContext; if (isSelected && isSelectionMuted) { ac = ac.withState(ACTIVE); } Color rowBackground = colors.getBackground(ac); if (!isStriped && !isSelected) { Color c = table.getBackground(); if (AquaColors.isPriority(c)) { rowBackground = c; } } g.setColor(rowBackground); if (isInset) { if (isSelected) { int y = cellRect.y; int h = cellRect.height; boolean isSelectedAbove = row > 0 && table.isRowSelected(row-1); boolean isSelectedBelow = row < table.getRowCount()-1 && table.isRowSelected(row+1); AquaUtils.paintInsetCellSelection(gg, isSelectedAbove, isSelectedBelow, 0, y, tableWidth, h); } else { AquaUtils.paintInsetStripedRow(gg, 0, cellRect.y, tableWidth, cellRect.height); } } else { g.fillRect(clip.x, cellRect.y, clip.width, cellRect.height); } // Sometimes it is useful to paint a special background color under the cell being edited to improve // contrast with the selection background. if (isSelected && isEditing && editingRow == row && editingColumn >= cMin && editingColumn <= cMax && shouldPaintSpecialEditedCellBackground()) { Color b = AquaColors.getCellEditorBackground(table); if (b != null) { Rectangle editorCellRect = table.getCellRect(row, editingColumn, true); int x1 = editorCellRect.x; int x2 = x1 + editorCellRect.width; g.setColor(b); g.fillRect(x1, cellRect.y, x2 - x1, cellRect.height); } } nextRowY = cellRect.y + cellRect.height; } if (isStriped) { int clipTop = clip.y + clip.height; if (nextRowY < clipTop) { int rowHeight = table.getRowHeight(); if (rowHeight > 0) { int row = rMax + 1; while (nextRowY < clipTop) { colors.configureForRow(row, false); AppearanceContext ac = appearanceContext; if (ac.isSelected() && isSelectionMuted) { ac = appearanceContext.withState(ACTIVE); } Color bg = colors.getBackground(ac); g.setColor(bg); if (isInset) { AquaUtils.paintInsetStripedRow(gg, 0, nextRowY, tableWidth, rowHeight); } else { g.fillRect(clip.x, nextRowY, clip.width, rowHeight); } row++; nextRowY += rowHeight; } } } } // TBD: should selected column be painted here or is it OK for just the cells to paint the selection background? } protected boolean shouldPaintSpecialEditedCellBackground() { assert appearanceContext != null; AquaUIPainter.State state = appearanceContext.getState(); return state == ACTIVE_DEFAULT && appearanceContext.getAppearance().isDark(); } protected void paintGrid(Graphics g, int rMin, int rMax, int cMin, int cMax, boolean xVertical, boolean xHorizontal) { assert appearanceContext != null; Color gridColor = colors.getGrid(appearanceContext); g.setColor(gridColor); Rectangle minCell = table.getCellRect(rMin, cMin, true); Rectangle maxCell = table.getCellRect(rMax, cMax, true); Rectangle damagedArea = minCell.union(maxCell); if (table.getShowHorizontalLines()) { if (xHorizontal) { Rectangle clip = g.getClipBounds(); int y = damagedArea.y; for (int row = rMin; row <= rMax; row++) { y += table.getRowHeight(row); g.fillRect(clip.x, y - 1, clip.width, 1); } } else { int tableWidth = damagedArea.x + damagedArea.width; int y = damagedArea.y; for (int row = rMin; row <= rMax; row++) { y += table.getRowHeight(row); g.fillRect(damagedArea.x, y - 1, tableWidth, 1); } } } if (table.getShowVerticalLines()) { TableColumnModel cm = table.getColumnModel(); int tableHeight = damagedArea.y + damagedArea.height; if (xVertical) { Rectangle clip = g.getClipBounds(); tableHeight = clip.y + clip.height; } int x; if (table.getComponentOrientation().isLeftToRight()) { x = damagedArea.x; for (int column = cMin; column <= cMax; column++) { int w = cm.getColumn(column).getWidth(); x += w; g.fillRect(x - 1, 0, 1, tableHeight); } } else { x = damagedArea.x; for (int column = cMax; column >= cMin; column--) { int w = cm.getColumn(column).getWidth(); x += w; g.fillRect(x - 1, 0, 1, tableHeight); } } } } @Override protected void paintCell(Graphics g, Rectangle cellRect, int row, int column) { if (table.isEditing() && table.getEditingRow() == row && table.getEditingColumn() == column) { paintEditorCell(g, cellRect, row, column); } else { paintRenderedCell(g, cellRect, row, column); } } protected void paintEditorCell(@NotNull Graphics g, @NotNull Rectangle cellRect, int row, int column) { Component component = table.getEditorComponent(); component.setBounds(cellRect); component.validate(); } protected void paintRenderedCell(@NotNull Graphics g, @NotNull Rectangle cellRect, int row, int column) { boolean isDropTarget = false; boolean isSelected = table.isCellSelected(row, column); if (isDropActive && hasDropOnTarget) { JTable.DropLocation loc = table.getDropLocation(); if (loc != null && loc.getRow() == row && loc.getColumn() == column) { isDropTarget = true; } } TableCellRenderer renderer = table.getCellRenderer(row, column); Component rendererComponent = getRendererComponent(renderer, row, column, isSelected, isSelectionMuted, isDropTarget); rendererPane.paintComponent(g, rendererComponent, table, cellRect.x, cellRect.y, cellRect.width, cellRect.height, true); if (!AquaColors.isPriority(table.getSelectionBackground())) { table.setSelectionBackground(null); } } protected Component getRendererComponent(TableCellRenderer renderer, int row, int column, boolean isSelected, boolean isSelectionMuted, boolean isDropTarget) { assert appearanceContext != null; // Only indicate the selection and focused cell if not printing boolean hasFocus = false; if (!isDropTarget && !isSelectionMuted && !table.isPaintingForPrint()) { boolean rowIsLead = (table.getSelectionModel().getLeadSelectionIndex() == row); boolean colIsLead = (table.getColumnModel().getSelectionModel().getLeadSelectionIndex() == column); hasFocus = (rowIsLead && colIsLead) && table.isFocusOwner(); } if (!AquaColors.isPriority(table.getSelectionBackground())) { Color c = AquaColors.CLEAR; if (isDropTarget) { c = colors.getBackground(appearanceContext.withState(ACTIVE_DEFAULT).withSelected(true)); } else if (isSelected) { if (appearanceContext.getState() == ACTIVE_DEFAULT && isSelectionMuted) { c = colors.getBackground(appearanceContext.withState(ACTIVE).withSelected(true)); } else { c = colors.getBackground(appearanceContext.withSelected(true)); } } table.setSelectionBackground(c); } return renderer.getTableCellRendererComponent(table, table.getValueAt(row, column), isSelected, hasFocus, row, column); } protected void paintDraggedArea(Graphics g, int rMin, int rMax, TableColumn draggedColumn, int distance) { int draggedColumnIndex = viewIndexForColumn(draggedColumn); Rectangle minCell = table.getCellRect(rMin, draggedColumnIndex, true); Rectangle maxCell = table.getCellRect(rMax, draggedColumnIndex, true); Rectangle vacatedColumnRect = minCell.union(maxCell); // Move to the where the cell has been dragged. vacatedColumnRect.x += distance; // Fill the background. paintDraggedAreaBackground(g, vacatedColumnRect, rMin, rMax); assert appearanceContext != null; Color gridColor = colors.getGrid(appearanceContext); // Paint the vertical grid lines if necessary. if (table.getShowVerticalLines()) { g.setColor(gridColor); int x1 = vacatedColumnRect.x; int y1 = vacatedColumnRect.y; int x2 = x1 + vacatedColumnRect.width - 1; int y2 = y1 + vacatedColumnRect.height - 1; // Left g.drawLine(x1 - 1, y1, x1 - 1, y2); // Right g.drawLine(x2, y1, x2, y2); } for (int row = rMin; row <= rMax; row++) { // Render the cell value Rectangle r = table.getCellRect(row, draggedColumnIndex, false); r.x += distance; paintCell(g, r, row, draggedColumnIndex); // Paint the (lower) horizontal grid line if necessary. if (table.getShowHorizontalLines()) { g.setColor(gridColor); Rectangle rcr = table.getCellRect(row, draggedColumnIndex, true); rcr.x += distance; int x1 = rcr.x; int y1 = rcr.y; int x2 = x1 + rcr.width - 1; int y2 = y1 + rcr.height - 1; g.drawLine(x1, y2, x2, y2); } } } protected void paintDraggedAreaBackground(Graphics g, Rectangle columnRect, int rMin, int rMax) { if (isStriped) { Graphics gg = g.create(); try { gg.clipRect(columnRect.x, columnRect.y, columnRect.width, columnRect.height); g.setColor(table.getParent().getBackground()); g.fillRect(columnRect.x, columnRect.y, columnRect.width, columnRect.height); paintBackground(gg, rMin, rMax, 0, table.getColumnCount() - 1); } finally { gg.dispose(); } } else { Color background = getBackgroundColor(); if (background != null) { g.setColor(background); g.fillRect(columnRect.x, columnRect.y, columnRect.width, columnRect.height); } } } } protected TableCellRenderer installRendererIfPossible(@NotNull Class valueClass, @NotNull Class rendererClass) { TableCellRenderer currentRenderer = table.getDefaultRenderer(valueClass); if (isDefault(valueClass, currentRenderer)) { TableCellRenderer renderer = AquaUtils.instantiate(rendererClass); if (renderer != null) { table.setDefaultRenderer(valueClass, renderer); } } return currentRenderer; } protected TableCellEditor installEditorIfPossible(@NotNull Class valueClass, @NotNull Class editorClass) { TableCellEditor currentEditor = table.getDefaultEditor(valueClass); if (isDefault(valueClass, currentEditor)) { TableCellEditor editor = AquaUtils.instantiate(editorClass); if (editor != null) { table.setDefaultEditor(valueClass, editor); } } return currentEditor; } protected boolean isDefault(@NotNull Class valueClass, @Nullable Object o) { if (o instanceof UIResource) { return true; } if (o instanceof DefaultCellEditor) { DefaultCellEditor d = (DefaultCellEditor) o; String className = d.getClass().getName(); if (valueClass == Object.class) { return className.equals("javax.swing.JTable$GenericEditor"); } if (valueClass == Number.class) { return className.equals("javax.swing.JTable$NumberEditor"); } if (valueClass == Boolean.class) { return className.equals("javax.swing.JTable$BooleanEditor"); } } return false; } /** * A version of the standard default cell editor for Object values that avoids installing a line border. * The line border acts as a focus ring, conflicting with our native styled focus ring. */ protected static class AquaObjectEditor extends DefaultCellEditor implements UIResource { Class[] argTypes = new Class[]{String.class}; java.lang.reflect.Constructor constructor; Object value; public AquaObjectEditor() { super(new JTextField()); getComponent().setName("Table.editor"); } public boolean stopCellEditing() { String s = (String) super.getCellEditorValue(); // Here we are dealing with the case where a user // has deleted the string value in a cell, possibly // after a failed validation. Return null, so that // they have the option to replace the value with // null or use escape to restore the original. // For Strings, return "" for backward compatibility. try { if ("".equals(s)) { if (constructor.getDeclaringClass() == String.class) { value = s; } return super.stopCellEditing(); } value = constructor.newInstance(s); } catch (Exception e) { return false; } return super.stopCellEditing(); } public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { this.value = null; try { Class type = table.getColumnClass(column); // Since our obligation is to produce a value which is // assignable for the required type it is OK to use the // String constructor for columns which are declared // to contain Objects. A String is an Object. if (type == Object.class) { type = String.class; } constructor = type.getConstructor(argTypes); } catch (Exception e) { return null; } return super.getTableCellEditorComponent(table, value, isSelected, row, column); } public Object getCellEditorValue() { return value; } } /** * A version of the standard default cell editor for Number values that avoids installing a line border. * The line border acts as a focus ring, conflicting with our native styled focus ring. */ protected static class AquaNumberEditor extends AquaObjectEditor { public AquaNumberEditor() { ((JTextField) getComponent()).setHorizontalAlignment(JTextField.RIGHT); } } /** * A version of the standard default cell renderer for Boolean values that avoids painting a background color. */ protected static class AquaBooleanRenderer extends JCheckBox implements TableCellRenderer, UIResource { public AquaBooleanRenderer() { setHorizontalAlignment(JLabel.CENTER); setBorderPainted(false); setOpaque(false); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (isSelected) { setForeground(table.getSelectionForeground()); } else { setForeground(table.getForeground()); } // TBD: Setting the background to a transparent color is a work around for the code in // AquaButtonLabeledUI that ignores opaque for cell renderers. Not sure why it does that. setBackground(AquaColors.CLEAR); setSelected((value != null && (Boolean) value)); return this; } } /** * A version of the standard default cell editor for Boolean values that avoids painting a background when clicked * and does not try to change the row selection when clicked. */ protected static class AquaBooleanEditor extends DefaultCellEditor { public AquaBooleanEditor() { super(new JCheckBox()); JCheckBox checkBox = (JCheckBox) getComponent(); checkBox.setHorizontalAlignment(JCheckBox.CENTER); } public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { delegate.setValue(value); editorComponent.setOpaque(false); return editorComponent; } @Override public boolean shouldSelectCell(EventObject anEvent) { return false; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy