/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package com.tvl.modules.editor.completion; import com.tvl.spi.editor.completion.CompletionController; import com.tvl.spi.editor.completion.CompletionItem; import com.tvl.spi.editor.completion.LazyCompletionItem; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.event.MouseListener; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.List; import java.util.Map; import javax.accessibility.Accessible; import javax.accessibility.AccessibleContext; import javax.swing.AbstractListModel; import javax.swing.DefaultListCellRenderer; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.ListCellRenderer; import javax.swing.ListModel; import javax.swing.SwingUtilities; import javax.swing.text.JTextComponent; import org.netbeans.api.annotations.common.NonNull; import org.openide.util.NbBundle; import org.openide.util.Utilities; /** * @author Miloslav Metelka, Dusan Balek * @version 1.00 */ @NbBundle.Messages({ "completion-please-wait=Please wait...", "ACSN_CompletionView=Code Completion", "ACSD_CompletionView=Code Completion Window", "# {0} - selected completion item", "ACSN_CompletionView_SelectedItem=Selected code completion item {0}", "ACSN_CompletionView_NoSelectedItem=No selection", }) public class CompletionJList extends JList<Object> { private static final Object COMPLETION_CONTROLLER_PROPERTY = CompletionJList.class.getName() + ".controller"; private static final Object COMPLETION_DATA_PROPERTY = CompletionJList.class.getName() + ".data"; private static final int DARKER_COLOR_COMPONENT = 5; private final RenderComponent renderComponent; private Graphics cellPreferredSizeGraphics; private int fixedItemHeight; private int maxVisibleRowCount; private WeakReference<JTextComponent> editorComponent; private int smartIndex; /** The current completion controller. */ private WeakReference<CompletionController> controller; /** <code>true</code> if the best match is selected, otherwise <code>false</code>. */ private boolean isSelected; private boolean preventSelection; public CompletionJList(int maxVisibleRowCount, MouseListener mouseListener, JTextComponent editorComponent) { this.maxVisibleRowCount = maxVisibleRowCount; this.editorComponent = new WeakReference<>(editorComponent); addMouseListener(mouseListener); setFont(editorComponent.getFont()); setLayoutOrientation(JList.VERTICAL); setFixedCellHeight(fixedItemHeight = Math.max(CompletionLayout.COMPLETION_ITEM_HEIGHT, getFontMetrics(getFont()).getHeight())); setModel(new Model(Collections.EMPTY_LIST)); setFocusable(false); renderComponent = new RenderComponent(); setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); setCellRenderer(new ListCellRenderer<Object>() { private final ListCellRenderer<Object> defaultRenderer = new DefaultListCellRenderer(); @Override public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isBestMatch, boolean cellHasFocus) { if( value instanceof CompletionItem ) { CompletionItem item = (CompletionItem)value; renderComponent.setItem(item); renderComponent.setSelected(isBestMatch, isBestMatch && CompletionJList.this.isSelected); renderComponent.setSeparator(smartIndex > 0 && smartIndex == index); Color bgColor = list.getBackground(); Color bgSelectedColor = list.getSelectionBackground(); Color fgColor = list.getForeground(); Color fgSelectedColor = list.getSelectionForeground(); if ((index % 2) == 0) { // every second item slightly different bgColor = new Color( Math.abs(bgColor.getRed() - DARKER_COLOR_COMPONENT), Math.abs(bgColor.getGreen() - DARKER_COLOR_COMPONENT), Math.abs(bgColor.getBlue() - DARKER_COLOR_COMPONENT) ); } renderComponent.setColors(fgColor, bgColor, fgSelectedColor, bgSelectedColor); // quick check Component.setBackground() always fires change if (renderComponent.getBackground() != bgColor) { renderComponent.setBackground(bgColor); } if (renderComponent.getForeground() != fgColor) { renderComponent.setForeground(fgColor); } return renderComponent; } else { return defaultRenderer.getListCellRendererComponent( list, value, index, isBestMatch, cellHasFocus); } } }); getAccessibleContext().setAccessibleName(Bundle.ACSN_CompletionView()); getAccessibleContext().setAccessibleDescription(Bundle.ACSD_CompletionView()); } public boolean isPreventSelection() { return preventSelection; } public void setPreventSelection(boolean preventSelection) { this.preventSelection = preventSelection; } public @Override void paint(Graphics g) { Object value = (Map)(Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints")); //NOI18N Map<?, ?> renderingHints = (value instanceof Map) ? (Map<?, ?>)value : null; if (renderingHints != null && g instanceof Graphics2D) { Graphics2D g2d = (Graphics2D) g; RenderingHints oldHints = g2d.getRenderingHints(); g2d.addRenderingHints(renderingHints); try { super.paint(g2d); } finally { g2d.setRenderingHints(oldHints); } } else { super.paint(g); } } void setData(List<?> data, @NonNull CompletionController controller) { smartIndex = -1; this.controller = new WeakReference<>(controller); // since this.controller is a weak reference, add a strong reference to // the editor component editorComponent.get().putClientProperty(COMPLETION_CONTROLLER_PROPERTY, controller); editorComponent.get().putClientProperty(COMPLETION_DATA_PROPERTY, data); if (data != null) { int itemCount = data.size(); ListCellRenderer<? super Object> renderer = getCellRenderer(); int width = 0; int maxWidth = getParent().getParent().getMaximumSize().width; boolean stop = false; for(int index = 0; index < itemCount; index++) { Object value = data.get(index); if (value instanceof LazyCompletionItem) { maxWidth = (int)(Utilities.getUsableScreenBounds().width * CompletionLayoutPopup.COMPL_COVERAGE); } Component c = renderer.getListCellRendererComponent(this, value, index, false, false); if (c != null) { Dimension cellSize = c.getPreferredSize(); if (cellSize.width > width) { width = cellSize.width; if (width >= maxWidth) stop = true; } } if (smartIndex < 0 && value instanceof CompletionItem && ((CompletionItem)value).getSortPriority() >= 0) smartIndex = index; if (stop && smartIndex >= 0) break; } setFixedCellWidth(width); ListModel<Object> lm = LazyListModel.<Object>create( new Model(data), CompletionImpl.filter, 1.0d, Bundle.completion_please_wait() ); //NOI18N setModel(lm); if (itemCount > 0) { setSelection(0, false); } int visibleRowCount = Math.min(itemCount, maxVisibleRowCount); setVisibleRowCount(visibleRowCount); } } @Override public void setVisible(boolean aFlag) { super.setVisible(aFlag); if (isVisible()) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateAccessible(); } }); } else { AccessibleContext editorAC = editorComponent.get().getAccessibleContext(); if (accessibleLabel != null) { editorAC.firePropertyChange(AccessibleContext.ACCESSIBLE_ACTIVE_DESCENDANT_PROPERTY, accessibleLabel, null); editorAC.firePropertyChange(AccessibleContext.ACCESSIBLE_CHILD_PROPERTY, accessibleLabel, null); } if (accessibleFakeLabel != null) { editorAC.firePropertyChange(AccessibleContext.ACCESSIBLE_CHILD_PROPERTY, accessibleFakeLabel, null); } } } @Override public void setSelectedIndex(int index) { super.setSelectedIndex(index); if (isVisible()) { updateAccessible(); } } @Override public void addSelectionInterval(int anchor, int lead) { // make sure Ctrl+Click sets isSelected to true this.isSelected = true; super.addSelectionInterval(anchor, lead); } @Override public void setSelectionInterval(int anchor, int lead) { // make sure Click and Shift+Click sets isSelected to true this.isSelected = true; super.setSelectionInterval(anchor, lead); } public @NonNull CompletionController.Selection getSelection() { return new CompletionController.Selection(getSelectedIndex(), isSelected); } public void setSelection(@NonNull CompletionController.Selection selection) { setSelection(selection.getIndex(), selection.isSelected()); } public void setSelection(int index, boolean isSelected) { this.isSelected = isSelected; setSelectedIndex(index); } private JLabel accessibleLabel; private JLabel accessibleFakeLabel; private void updateAccessible() { AccessibleContext editorAC = editorComponent.get().getAccessibleContext(); if (accessibleFakeLabel == null) { accessibleFakeLabel = new JLabel(""); //NOI18N editorAC.firePropertyChange(AccessibleContext.ACCESSIBLE_CHILD_PROPERTY, null, accessibleFakeLabel); } JLabel orig = accessibleLabel; editorAC.firePropertyChange(AccessibleContext.ACCESSIBLE_ACTIVE_DESCENDANT_PROPERTY, accessibleLabel, accessibleFakeLabel); Object selectedValue = getSelectedValue(); if (selectedValue == null) { selectedValue = Bundle.ACSN_CompletionView_NoSelectedItem(); //NOI18N } String accName = selectedValue instanceof Accessible ? ((Accessible) selectedValue).getAccessibleContext().getAccessibleName() : selectedValue.toString(); accessibleLabel = new JLabel(Bundle.ACSN_CompletionView_SelectedItem(accName)); //NOI18N editorAC.firePropertyChange(AccessibleContext.ACCESSIBLE_CHILD_PROPERTY, null, accessibleLabel); editorAC.firePropertyChange(AccessibleContext.ACCESSIBLE_ACTIVE_DESCENDANT_PROPERTY, accessibleFakeLabel, accessibleLabel); if (orig != null) { editorAC.firePropertyChange(AccessibleContext.ACCESSIBLE_CHILD_PROPERTY, orig, null); } } public void up() { int size = getModel().getSize(); if (size > 0) { int idx = (getSelectedIndex() - 1 + size) % size; while(idx > 0 && getModel().getElementAt(idx) == null) idx--; setSelection(idx, true); ensureIndexIsVisible(idx); } } public void down() { int size = getModel().getSize(); if (size > 0) { int idx = (getSelectedIndex() + 1) % size; while(idx < size && getModel().getElementAt(idx) == null) idx++; if (idx == size) idx = 0; setSelection(idx, true); ensureIndexIsVisible(idx); } } public void pageUp() { if (getModel().getSize() > 0) { int pageSize = Math.max(getLastVisibleIndex() - getFirstVisibleIndex(), 0); int idx = Math.max(getSelectedIndex() - pageSize, 0); while(idx > 0 && getModel().getElementAt(idx) == null) idx--; setSelection(idx, true); ensureIndexIsVisible(idx); } } public void pageDown() { int size = getModel().getSize(); if (size > 0) { int pageSize = Math.max(getLastVisibleIndex() - getFirstVisibleIndex(), 0); int idx = Math.min(getSelectedIndex() + pageSize, size - 1); while(idx < size && getModel().getElementAt(idx) == null) idx++; if (idx == size) { idx = Math.min(getSelectedIndex() + pageSize, size - 1); while(idx > 0 && getModel().getElementAt(idx) == null) idx--; } setSelection(idx, true); ensureIndexIsVisible(idx); } } public void begin() { if (getModel().getSize() > 0) { setSelection(0, true); ensureIndexIsVisible(0); } } public void end() { int size = getModel().getSize(); if (size > 0) { int idx = size - 1; while(idx > 0 && getModel().getElementAt(idx) == null) idx--; setSelection(idx, true); ensureIndexIsVisible(idx); } } private static final class Model extends AbstractListModel<Object> { WeakReference<List<?>> _data; public Model(List<?> data) { this._data = new WeakReference<List<?>>(data); } @Override public int getSize() { List<?> data = this._data.get(); return data != null ? data.size() : 0; } @Override public Object getElementAt(int index) { List<?> data = this._data.get(); if (data == null) { return null; } return (index >= 0 && index < data.size()) ? data.get(index) : null; } } private final class RenderComponent extends JComponent { private WeakReference<CompletionItem> item; private boolean isBestMatch; private boolean isSelected; private boolean separator; private Color fgColor; private Color bgColor; private Color fgSelectedColor; private Color bgSelectedColor; void setItem(CompletionItem item) { this.item = new WeakReference<>(item); } void setSelected(boolean isBestMatch, boolean isSelected) { this.isBestMatch = isBestMatch; this.isSelected = isSelected; } void setSeparator(boolean separator) { this.separator = separator; } public @Override void paintComponent(Graphics g) { // Although the JScrollPane without horizontal scrollbar // is explicitly set with a preferred size // it does not force its items with the only width into which // they can render (and still leaves them with the preferred width // of the widest item). // Therefore the item's render width is taken from the viewport's width. int itemRenderWidth = CompletionJList.this.getParent().getWidth(); int height = getHeight(); // Render the item controller.get().render(g, CompletionJList.this.getFont(), fgColor, bgColor, fgSelectedColor, bgSelectedColor, itemRenderWidth, getHeight(), item.get(), isBestMatch && !preventSelection, isSelected && !preventSelection); if (separator) { g.setColor(Color.gray); g.drawLine(0, 0, itemRenderWidth, 0); g.setColor(fgColor); } } public @Override Dimension getPreferredSize() { if (cellPreferredSizeGraphics == null) { // CompletionJList.this.getGraphics() is null cellPreferredSizeGraphics = java.awt.GraphicsEnvironment. getLocalGraphicsEnvironment().getDefaultScreenDevice(). getDefaultConfiguration().createCompatibleImage(1, 1).getGraphics(); assert (cellPreferredSizeGraphics != null); } return new Dimension(item.get().getPreferredWidth(cellPreferredSizeGraphics, CompletionJList.this.getFont()), fixedItemHeight); } private void setColors(Color fgColor, Color bgColor, Color fgSelectedColor, Color bgSelectedColor) { this.fgColor = fgColor; this.bgColor = bgColor; this.fgSelectedColor = fgSelectedColor; this.bgSelectedColor = bgSelectedColor; } } }