/*
* 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.CompletionDocumentation;
import com.tvl.spi.editor.completion.CompletionItem;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JToolTip;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.LineBorder;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.editor.GuardedDocument;
import org.openide.text.CloneableEditorSupport;
import org.openide.util.NbBundle;
/**
* Layout of the completion, documentation and tooltip popup windows.
*
* @author Dusan Balek, Miloslav Metelka
*/
@NbBundle.Messages({
"# {0} - Additional items text",
"# {1} - Shortcut hint",
"TXT_completion_shortcut_tips={0}Press {1} Again for All Items"
})
public final class CompletionLayout {
public static final int COMPLETION_ITEM_HEIGHT = 16;
/**
* Visual shift of the completion window to the left
* so that the text in the rendered completion items.aligns horizontally
* with the text in the document.
*/
private static final int COMPLETION_ANCHOR_HORIZONTAL_SHIFT = 22;
/**
* Gap between caret and the displayed popup.
*/
static final int POPUP_VERTICAL_GAP = 1;
private Reference<JTextComponent> editorComponentRef;
private final CompletionPopup completionPopup;
private final DocPopup docPopup;
private final TipPopup tipPopup;
private final List<CompletionLayoutPopup> visiblePopups;
@SuppressWarnings("LeakingThisInConstructor")
CompletionLayout() {
completionPopup = new CompletionPopup();
completionPopup.setLayout(this);
completionPopup.setPreferDisplayAboveCaret(false);
docPopup = new DocPopup();
docPopup.setLayout(this);
docPopup.setPreferDisplayAboveCaret(false);
tipPopup = new TipPopup();
tipPopup.setLayout(this);
tipPopup.setPreferDisplayAboveCaret(true);
visiblePopups = new ArrayList<>();
}
public JTextComponent getEditorComponent() {
return (editorComponentRef != null)
? editorComponentRef.get()
: null;
}
public void setEditorComponent(JTextComponent editorComponent) {
hideAll();
this.editorComponentRef = new WeakReference<>(editorComponent);
}
private void hideAll() {
completionPopup.hide();
docPopup.hide();
tipPopup.hide();
visiblePopups.clear();
}
public void showCompletion(List<?> data, List<? extends CompletionItem> declarationData, String title, int anchorOffset,
ListSelectionListener listSelectionListener, String additionalItemsText,
String shortcutHint, CompletionController controller,
CompletionController.Selection selection) {
completionPopup.show(data, declarationData, title, anchorOffset, listSelectionListener, additionalItemsText, shortcutHint, controller, selection);
if (!visiblePopups.contains(completionPopup))
visiblePopups.add(completionPopup);
}
public boolean hideCompletion() {
if (completionPopup.isVisible()) {
completionPopup.hide();
completionPopup.completionScrollPane = null;
visiblePopups.remove(completionPopup);
return true;
} else { // not visible
return false;
}
}
public boolean isCompletionVisible() {
return completionPopup.isVisible();
}
public @CheckForNull SelectedCompletionItem getSelectedCompletionItem() {
return completionPopup.getSelectedCompletionItem();
}
public int getSelectedIndex() {
return completionPopup.getSelectedIndex();
}
public void processKeyEvent(KeyEvent evt) {
for (int i = visiblePopups.size() - 1; i >= 0; i--) {
CompletionLayoutPopup popup = visiblePopups.get(i);
popup.processKeyEvent(evt);
if (evt.isConsumed())
return;
}
}
public void showDocumentation(CompletionDocumentation doc, int anchorOffset) {
docPopup.show(doc, anchorOffset);
if (!visiblePopups.contains(docPopup))
visiblePopups.add(docPopup);
}
public boolean hideDocumentation() {
if (docPopup.isVisible()) {
docPopup.getDocumentationScrollPane().setData(null);
docPopup.clearHistory();
docPopup.hide();
visiblePopups.remove(docPopup);
return true;
} else { // not visible
return false;
}
}
public boolean isDocumentationVisible() {
return docPopup.isVisible();
}
public void clearDocumentationHistory() {
docPopup.clearHistory();
}
public void showToolTip(JToolTip toolTip, int anchorOffset) {
tipPopup.show(toolTip, anchorOffset);
if (!visiblePopups.contains(tipPopup))
visiblePopups.add(tipPopup);
}
public boolean hideToolTip() {
if (tipPopup.isVisible()) {
tipPopup.hide();
visiblePopups.remove(tipPopup);
return true;
} else { // not visible
return false;
}
}
public boolean isToolTipVisible() {
return tipPopup.isVisible();
}
/**
* Layout either of the copmletion, documentation or tooltip popup.
* <br>
* This method can be called recursively to update other popups
* once certain popup was updated.
*
* <p>
* The rules for the displayment are the following:
* <ul>
* <li> The tooltip popup should be above caret if there is enough space.
* <li> The completion popup should be above caret if there is enough space
* and the tooltip window is not displayed.
* <li> If both tooltip and completion popups are visible then vertically
* each should be on opposite side of the anchor bounds (caret).
* <li> Documentation should be preferrably shrinked if there is not enough
* vertical space.
* <li> Documentation anchoring should be aligned with completion.
* </ul>
*/
void updateLayout(CompletionLayoutPopup popup) {
// Make sure the popup returns its natural preferred size
popup.resetPreferredSize();
if (popup == completionPopup) { // completion popup
if (isToolTipVisible()) {
// Display on opposite side than tooltip
boolean wantAboveCaret = !tipPopup.isDisplayAboveCaret();
if (completionPopup.isEnoughSpace(wantAboveCaret)) {
completionPopup.showAlongAnchorBounds(wantAboveCaret);
} else { // not enough space -> show on same side
Rectangle occupiedBounds = popup.getAnchorOffsetBounds();
occupiedBounds = tipPopup.unionBounds(occupiedBounds);
completionPopup.showAlongOccupiedBounds(occupiedBounds,
tipPopup.isDisplayAboveCaret());
}
} else { // tooltip not visible
popup.showAlongAnchorBounds();
}
// Update docPopup layout if necessary
if (docPopup.isVisible()
&& (docPopup.isOverlapped(popup) || docPopup.isOverlapped(tipPopup)
|| docPopup.getAnchorOffset() != completionPopup.getAnchorOffset()
|| !docPopup.isShowRetainedPreferredSize())
) {
updateLayout(docPopup);
}
} else if (popup == docPopup) { // documentation popup
if (isCompletionVisible()) {
// Documentation must sync anchoring with completion
popup.setAnchorOffset(completionPopup.getAnchorOffset());
}
Rectangle occupiedBounds = popup.getAnchorOffsetBounds();
occupiedBounds = tipPopup.unionBounds(completionPopup.unionBounds(occupiedBounds));
if(CompletionSettings.getInstance(getEditorComponent()).documentationPopupNextToCC()) {
docPopup.showAlongOrNextOccupiedBounds(completionPopup.getPopupBounds(), occupiedBounds);
} else {
docPopup.showAlongOccupiedBounds(occupiedBounds);
}
} else if (popup == tipPopup) { // tooltip popup
popup.showAlongAnchorBounds(); // show possibly above the caret
if (completionPopup.isOverlapped(popup) || docPopup.isOverlapped(popup)) {
// docPopup layout will be handled as part of completion popup layout
updateLayout(completionPopup);
}
}
}
CompletionPopup testGetCompletionPopup() {
return completionPopup;
}
void repaintCompletionView() {
assert EventQueue.isDispatchThread();
JComponent completionView = completionPopup.completionScrollPane;
if(completionView != null && completionView.isVisible()) {
completionView.repaint();
}
}
private static final class CompletionPopup extends CompletionLayoutPopup {
private JPanel stickyItemsPanel;
private CompletionJList stickyItemsList;
private CompletionScrollPane completionScrollPane;
public void show(List<?> data, List<? extends CompletionItem> declarationData, String title, int anchorOffset,
ListSelectionListener listSelectionListener, String additionalItemsText,
String shortcutHint, final CompletionController controller,
CompletionController.Selection selection) {
JTextComponent editorComponent = getEditorComponent();
if (editorComponent == null) {
return;
}
Dimension lastSize;
int lastAnchorOffset = getAnchorOffset();
if (isVisible() && ((getContentComponent() == completionScrollPane)^(shortcutHint != null))) {
lastSize = getContentComponent().getSize();
resetPreferredSize();
} else { // not yet visible => create completion scrollpane
lastSize = new Dimension(0, 0); // no last size => use (0,0)
stickyItemsPanel = new JPanel();
stickyItemsPanel.setLayout(new BorderLayout(0, 0));
stickyItemsPanel.setBorder(new LineBorder(Color.black, 1));
stickyItemsList = new CompletionJList(1, new MouseAdapter() {}, editorComponent);
stickyItemsList.setPreventSelection(true);
stickyItemsPanel.add(stickyItemsList, BorderLayout.CENTER);
completionScrollPane = new CompletionScrollPane(
editorComponent, listSelectionListener,
new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
JTextComponent c = getEditorComponent();
if (SwingUtilities.isLeftMouseButton(evt)) {
if (c != null && evt.getClickCount() == 2 ) {
SelectedCompletionItem selectedItem
= completionScrollPane.getSelectedCompletionItem();
if (selectedItem != null) {
Document doc = c.getDocument();
if (doc instanceof GuardedDocument && ((GuardedDocument)doc).isPosGuarded(c.getSelectionEnd())) {
Toolkit.getDefaultToolkit().beep();
} else {
LogRecord r = new LogRecord(Level.FINE, "COMPL_MOUSE_SELECT"); // NOI18N
r.setParameters(new Object[] { null, completionScrollPane.getSelectedIndex(), selectedItem.getClass().getSimpleName()});
CompletionImpl.uilog(r);
CompletionImpl.sendUndoableEdit(doc, CloneableEditorSupport.BEGIN_COMMIT_GROUP);
try {
controller.defaultAction(selectedItem.getItem(), selectedItem.isSelected());
} finally {
CompletionImpl.sendUndoableEdit(doc, CloneableEditorSupport.END_COMMIT_GROUP);
}
}
}
}
}
}
}
);
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(stickyItemsPanel, BorderLayout.NORTH);
panel.add(completionScrollPane, BorderLayout.CENTER);
if (shortcutHint != null) {
JLabel label = new JLabel();
label.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, Color.white),
BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(0, 1, 1, 1, Color.gray), BorderFactory.createEmptyBorder(2, 2, 2, 2))));
label.setFont(label.getFont().deriveFont((float)label.getFont().getSize() - 2));
label.setHorizontalAlignment(SwingConstants.RIGHT);
label.setText(Bundle.TXT_completion_shortcut_tips(additionalItemsText, shortcutHint)); //NOI18N
panel.add(label, BorderLayout.SOUTH);
}
setContentComponent(panel);
}
// Set the new data
getPreferredSize();
stickyItemsList.setData(declarationData, controller);
stickyItemsPanel.setVisible(!declarationData.isEmpty());
completionScrollPane.setData(data, title, controller, selection);
setAnchorOffset(anchorOffset);
Dimension prefSize = getPreferredSize();
boolean changePopupSize;
if (isVisible()) {
changePopupSize = (prefSize.height != lastSize.height)
|| (prefSize.width != lastSize.width)
|| anchorOffset != lastAnchorOffset;
} else { // not visible yet
changePopupSize = true;
}
if (changePopupSize) {
// Do not change the popup's above/below caret positioning
// when the popup is already displayed
getLayout().updateLayout(this);
} // otherwise present popup size will be retained
}
public @CheckForNull SelectedCompletionItem getSelectedCompletionItem() {
return isVisible() ? completionScrollPane.getSelectedCompletionItem() : null;
}
public int getSelectedIndex() {
return isVisible() ? completionScrollPane.getSelectedIndex() : -1;
}
@Override
public void processKeyEvent(KeyEvent evt) {
if (isVisible()) {
Object actionMapKey = completionScrollPane.getInputMap().get(
KeyStroke.getKeyStrokeForEvent(evt));
if (actionMapKey != null) {
Action action = completionScrollPane.getActionMap().get(actionMapKey);
if (action != null) {
action.actionPerformed(new ActionEvent(completionScrollPane, 0, null));
evt.consume();
}
}
}
}
@Override
protected int getAnchorHorizontalShift() {
return COMPLETION_ANCHOR_HORIZONTAL_SHIFT;
}
}
private static final class DocPopup extends CompletionLayoutPopup {
private DocumentationScrollPane getDocumentationScrollPane() {
return (DocumentationScrollPane)getContentComponent();
}
protected void show(CompletionDocumentation doc, int anchorOffset) {
JTextComponent editorComponent = getEditorComponent();
if (editorComponent == null) {
return;
}
if (!isVisible()) { // documentation already visible
setContentComponent(new DocumentationScrollPane(editorComponent));
}
getDocumentationScrollPane().setData(doc);
if (!isVisible()) { // do not check for size as it should remain the same
// Set anchoring only if not displayed yet because completion
// may have overriden the anchoring
setAnchorOffset(anchorOffset);
getLayout().updateLayout(this);
} // otherwise leave present doc displayed
}
@Override
public void processKeyEvent(KeyEvent evt) {
if (isVisible()) {
Object actionMapKey = getDocumentationScrollPane().getInputMap().get(
KeyStroke.getKeyStrokeForEvent(evt));
if (actionMapKey != null) {
Action action = getDocumentationScrollPane().getActionMap().get(actionMapKey);
if (action != null) {
action.actionPerformed(new ActionEvent(getDocumentationScrollPane(), 0, null));
evt.consume();
}
}
}
}
public void clearHistory() {
if (isVisible()) {
getDocumentationScrollPane().clearHistory();
}
}
@Override
protected int getAnchorHorizontalShift() {
return COMPLETION_ANCHOR_HORIZONTAL_SHIFT;
}
}
private static final class TipPopup extends CompletionLayoutPopup {
protected void show(JToolTip toolTip, int anchorOffset) {
JComponent lastComponent = null;
if (isVisible()) { // tooltip already visible
lastComponent = getContentComponent();
}
setContentComponent(toolTip);
setAnchorOffset(anchorOffset);
// Check whether doc is visible and if so then display
// on the opposite side
if (lastComponent != toolTip) {
getLayout().updateLayout(this);
}
}
@Override
public void processKeyEvent(KeyEvent evt) {
if (isVisible()) {
if (KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0).equals(
KeyStroke.getKeyStrokeForEvent(evt))
) {
evt.consume();
CompletionImpl.get().hideToolTip();
}
}
}
}
}