mirror of
https://github.com/correl/mage.git
synced 2024-11-24 19:19:56 +00:00
* GUI: added card popup info in choose dialog (example: choose dungeon, #8012);
* GUI: added texts popup info in choose dialog (example: choose from any list);
This commit is contained in:
parent
b73f10a0ab
commit
d587cc9151
12 changed files with 322 additions and 36 deletions
134
Mage.Client/src/main/java/mage/client/cards/VirtualCardInfo.java
Normal file
134
Mage.Client/src/main/java/mage/client/cards/VirtualCardInfo.java
Normal file
|
@ -0,0 +1,134 @@
|
|||
package mage.client.cards;
|
||||
|
||||
import mage.abilities.icon.CardIconRenderSettings;
|
||||
import mage.cards.MageCard;
|
||||
import mage.cards.action.TransferData;
|
||||
import mage.cards.repository.CardInfo;
|
||||
import mage.cards.repository.CardRepository;
|
||||
import mage.client.dialog.PreferencesDialog;
|
||||
import mage.client.plugins.adapters.MageActionCallback;
|
||||
import mage.client.plugins.impl.Plugins;
|
||||
import mage.client.util.ClientDefaultSettings;
|
||||
import mage.view.CardView;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* GUI: virtual card component for popup hint (move mouse over card to show a hint)
|
||||
* <p>
|
||||
* Use case: you don't have a real card but want to show a popup card hint.
|
||||
* Howto use:
|
||||
* - call "init" on new card;
|
||||
* - call "onMouseXXX" on start, update and close
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class VirtualCardInfo {
|
||||
|
||||
CardView cardView;
|
||||
MageCard cardComponent;
|
||||
BigCard bigCard;
|
||||
MageActionCallback actionCallback;
|
||||
TransferData data = new TransferData();
|
||||
Dimension cardDimension = null;
|
||||
|
||||
public VirtualCardInfo() {
|
||||
super();
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
this.cardView = null;
|
||||
this.cardComponent = null;
|
||||
}
|
||||
|
||||
public void init(String cardName, BigCard bigCard, UUID gameId) {
|
||||
CardInfo cardInfo = CardRepository.instance.findCards(cardName).stream().findFirst().orElse(null);
|
||||
if (cardInfo == null) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.init(new CardView(cardInfo.getCard()), bigCard, gameId);
|
||||
}
|
||||
|
||||
public void init(CardView cardView, BigCard bigCard, UUID gameId) {
|
||||
clear();
|
||||
|
||||
this.bigCard = bigCard != null ? bigCard : new BigCard();
|
||||
this.cardDimension = new Dimension(ClientDefaultSettings.dimensions.getFrameWidth(), ClientDefaultSettings.dimensions.getFrameHeight());
|
||||
this.actionCallback = (MageActionCallback) Plugins.instance.getActionCallback();
|
||||
|
||||
this.cardView = cardView;
|
||||
this.cardComponent = Plugins.instance.getMageCard(
|
||||
this.cardView,
|
||||
this.bigCard,
|
||||
new CardIconRenderSettings(),
|
||||
this.cardDimension,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
PreferencesDialog.getRenderMode(),
|
||||
true
|
||||
);
|
||||
this.cardComponent.update(cardView);
|
||||
|
||||
data.setComponent(this.cardComponent);
|
||||
data.setCard(this.cardView);
|
||||
data.setGameId(gameId);
|
||||
}
|
||||
|
||||
public boolean prepared() {
|
||||
return this.cardView != null
|
||||
&& this.cardComponent != null
|
||||
&& this.actionCallback != null;
|
||||
}
|
||||
|
||||
private void updateLocation(Point point) {
|
||||
Point newPoint = new Point(point);
|
||||
if (this.cardComponent != null) {
|
||||
// offset popup
|
||||
newPoint.translate(50, 50);
|
||||
}
|
||||
data.setLocationOnScreen(newPoint);
|
||||
}
|
||||
|
||||
public void onMouseEntered() {
|
||||
onMouseMoved(null);
|
||||
}
|
||||
|
||||
public void onMouseEntered(Point newLocation) {
|
||||
if (!prepared()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newLocation != null) {
|
||||
updateLocation(newLocation);
|
||||
}
|
||||
|
||||
this.actionCallback.mouseEntered(null, this.data);
|
||||
}
|
||||
|
||||
public void onMouseMoved() {
|
||||
onMouseMoved(null);
|
||||
}
|
||||
|
||||
public void onMouseMoved(Point newLocation) {
|
||||
if (!prepared()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newLocation != null) {
|
||||
updateLocation(newLocation);
|
||||
}
|
||||
|
||||
this.actionCallback.mouseMoved(null, this.data);
|
||||
}
|
||||
|
||||
public void onMouseExited() {
|
||||
if (!prepared()) {
|
||||
return;
|
||||
}
|
||||
this.actionCallback.mouseExited(null, this.data);
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ import java.util.Set;
|
|||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Class is used to save parameters and to send them to dialog.
|
||||
* GUI: parameters for dialogs, uses to store useful data
|
||||
*
|
||||
* @author mw, noxx
|
||||
*/
|
||||
|
|
|
@ -6,12 +6,19 @@ import java.util.*;
|
|||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
|
||||
import mage.choices.Choice;
|
||||
import mage.choices.ChoiceHintType;
|
||||
import mage.client.MageFrame;
|
||||
import mage.client.cards.BigCard;
|
||||
import mage.client.cards.VirtualCardInfo;
|
||||
import mage.client.util.gui.MageDialogState;
|
||||
import mage.game.command.Dungeon;
|
||||
import mage.view.CardView;
|
||||
import mage.view.DungeonView;
|
||||
|
||||
/**
|
||||
* Game GUI: choosing one of the list's item
|
||||
* GUI: choosing one of the list's item. Uses in game's and non game's GUI like fast search
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
|
@ -19,25 +26,25 @@ public class PickChoiceDialog extends MageDialog {
|
|||
|
||||
Choice choice;
|
||||
|
||||
// popup card info
|
||||
int lastModelIndex = -1;
|
||||
VirtualCardInfo cardInfo = new VirtualCardInfo();
|
||||
BigCard bigCard;
|
||||
UUID gameId;
|
||||
|
||||
java.util.List<KeyValueItem> allItems = new ArrayList<>();
|
||||
DefaultListModel<KeyValueItem> dataModel = new DefaultListModel<>();
|
||||
|
||||
final private static String HTML_HEADERS_TEMPLATE = "<html><div style='text-align: center;'>%s</div></html>";
|
||||
|
||||
public void showDialog(Choice choice) {
|
||||
showDialog(choice, null, null, null);
|
||||
}
|
||||
|
||||
public void showDialog(Choice choice, String startSelectionValue) {
|
||||
showDialog(choice, null, null, startSelectionValue);
|
||||
showDialog(choice, startSelectionValue, null, null, null);
|
||||
}
|
||||
|
||||
public void showDialog(Choice choice, UUID objectId, MageDialogState mageDialogState) {
|
||||
showDialog(choice, objectId, mageDialogState, null);
|
||||
}
|
||||
|
||||
public void showDialog(Choice choice, UUID objectId, MageDialogState mageDialogState, String startSelectionValue) {
|
||||
public void showDialog(Choice choice, String startSelectionValue, UUID objectId, MageDialogState mageDialogState, BigCard bigCard) {
|
||||
this.choice = choice;
|
||||
this.bigCard = bigCard;
|
||||
this.gameId = objectId;
|
||||
|
||||
setLabelText(this.labelMessage, choice.getMessage());
|
||||
setLabelText(this.labelSubMessage, choice.getSubMessage());
|
||||
|
@ -54,20 +61,20 @@ public class PickChoiceDialog extends MageDialog {
|
|||
this.allItems.clear();
|
||||
if (choice.isKeyChoice()) {
|
||||
for (Map.Entry<String, String> entry : choice.getKeyChoices().entrySet()) {
|
||||
this.allItems.add(new KeyValueItem(entry.getKey(), entry.getValue()));
|
||||
this.allItems.add(new KeyValueItem(entry.getKey(), entry.getValue(), choice.getHintType()));
|
||||
}
|
||||
} else {
|
||||
for (String value : choice.getChoices()) {
|
||||
this.allItems.add(new KeyValueItem(value, value));
|
||||
this.allItems.add(new KeyValueItem(value, value, choice.getHintType()));
|
||||
}
|
||||
}
|
||||
|
||||
// sorting
|
||||
if (choice.isSortEnabled()) {
|
||||
this.allItems.sort((o1, o2) -> {
|
||||
Integer n1 = choice.getSortData().get(o1.Key);
|
||||
Integer n2 = choice.getSortData().get(o2.Key);
|
||||
return n1.compareTo(n2);
|
||||
Integer n1 = choice.getSortData().get(o1.getKey());
|
||||
Integer n2 = choice.getSortData().get(o2.getKey());
|
||||
return Integer.compare(n1, n2);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -123,14 +130,36 @@ public class PickChoiceDialog extends MageDialog {
|
|||
}
|
||||
});
|
||||
|
||||
// listeners double click choose
|
||||
// listeners double click
|
||||
// you can't use mouse wheel to switch hint type, cause wheel move a scrollbar
|
||||
listChoices.addMouseListener(new MouseAdapter() {
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
doChoose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
choiceHintHide();
|
||||
}
|
||||
});
|
||||
|
||||
listChoices.addMouseMotionListener(new MouseMotionAdapter() {
|
||||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
// hint show
|
||||
JList listSource = (JList) e.getSource();
|
||||
int index = listSource.locationToIndex(e.getPoint());
|
||||
if (index > -1) {
|
||||
choiceHintShow(index);
|
||||
} else {
|
||||
choiceHintHide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// listeners for ESC close
|
||||
|
@ -163,11 +192,11 @@ public class PickChoiceDialog extends MageDialog {
|
|||
loadData();
|
||||
|
||||
// start selection
|
||||
if ((startSelectionValue != null)) {
|
||||
if (startSelectionValue != null) {
|
||||
int selectIndex = -1;
|
||||
for (int i = 0; i < this.listChoices.getModel().getSize(); i++) {
|
||||
KeyValueItem listItem = (KeyValueItem) this.listChoices.getModel().getElementAt(i);
|
||||
if (listItem.Key.equals(startSelectionValue)) {
|
||||
if (listItem.getKey().equals(startSelectionValue)) {
|
||||
selectIndex = i;
|
||||
break;
|
||||
}
|
||||
|
@ -182,10 +211,77 @@ public class PickChoiceDialog extends MageDialog {
|
|||
this.setVisible(true);
|
||||
}
|
||||
|
||||
private void choiceHintShow(int modelIndex) {
|
||||
|
||||
switch (choice.getHintType()) {
|
||||
case CARD:
|
||||
case CARD_DUNGEON: {
|
||||
// as popup card
|
||||
if (lastModelIndex != modelIndex) {
|
||||
// new card
|
||||
KeyValueItem item = (KeyValueItem) listChoices.getModel().getElementAt(modelIndex);
|
||||
String cardName = item.getValue();
|
||||
|
||||
if (choice.getHintType() == ChoiceHintType.CARD) {
|
||||
cardInfo.init(cardName, this.bigCard, this.gameId);
|
||||
} else if (choice.getHintType() == ChoiceHintType.CARD_DUNGEON) {
|
||||
CardView cardView = new CardView(new DungeonView(Dungeon.createDungeon(cardName)));
|
||||
cardInfo.init(cardView, this.bigCard, this.gameId);
|
||||
}
|
||||
|
||||
cardInfo.onMouseEntered(MouseInfo.getPointerInfo().getLocation());
|
||||
} else {
|
||||
// old card
|
||||
cardInfo.onMouseMoved(MouseInfo.getPointerInfo().getLocation());
|
||||
}
|
||||
lastModelIndex = modelIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
case TEXT: {
|
||||
// as popup text
|
||||
if (lastModelIndex != modelIndex) {
|
||||
// new hint
|
||||
listChoices.setToolTipText(null);
|
||||
KeyValueItem item = (KeyValueItem) listChoices.getModel().getElementAt(modelIndex);
|
||||
listChoices.setToolTipText(item.getValue());
|
||||
}
|
||||
lastModelIndex = modelIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void choiceHintHide() {
|
||||
switch (choice.getHintType()) {
|
||||
case CARD: {
|
||||
// as popup card
|
||||
cardInfo.onMouseExited();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
case TEXT: {
|
||||
// as popup text
|
||||
listChoices.setToolTipText(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lastModelIndex = -1;
|
||||
}
|
||||
|
||||
public void setWindowSize(int width, int height) {
|
||||
this.setSize(new Dimension(width, height));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideDialog() {
|
||||
choiceHintHide();
|
||||
super.hideDialog();
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
// load data to datamodel after filter or on startup
|
||||
String filter = choice.getSearchText();
|
||||
|
@ -196,7 +292,7 @@ public class PickChoiceDialog extends MageDialog {
|
|||
|
||||
this.dataModel.clear();
|
||||
for (KeyValueItem item : this.allItems) {
|
||||
if (!choice.isSearchEnabled() || item.Value.toLowerCase(Locale.ENGLISH).contains(filter)) {
|
||||
if (!choice.isSearchEnabled() || item.getValue().toLowerCase(Locale.ENGLISH).contains(filter)) {
|
||||
this.dataModel.addElement(item);
|
||||
}
|
||||
}
|
||||
|
@ -284,27 +380,33 @@ public class PickChoiceDialog extends MageDialog {
|
|||
}
|
||||
}
|
||||
|
||||
class KeyValueItem {
|
||||
static class KeyValueItem {
|
||||
|
||||
private final String Key;
|
||||
private final String Value;
|
||||
protected final String key;
|
||||
protected final String value;
|
||||
protected final ChoiceHintType hint;
|
||||
|
||||
public KeyValueItem(String value, String label) {
|
||||
this.Key = value;
|
||||
this.Value = label;
|
||||
public KeyValueItem(String key, String value, ChoiceHintType hint) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.hint = hint;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return this.Key;
|
||||
return this.key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.Value;
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public ChoiceHintType getHint() {
|
||||
return this.hint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.Value;
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1746,7 +1746,7 @@ public final class GamePanel extends javax.swing.JPanel {
|
|||
hideAll();
|
||||
// TODO: remember last choices and search incremental for same events?
|
||||
PickChoiceDialog pickChoice = new PickChoiceDialog();
|
||||
pickChoice.showDialog(choice, objectId, choiceWindowState);
|
||||
pickChoice.showDialog(choice, null, objectId, choiceWindowState, bigCard);
|
||||
|
||||
// special mode adds # to the answer (server side code must process that prefix, see replacementEffectChoice)
|
||||
String specialPrefix = choice.isChosenSpecial() ? "#" : "";
|
||||
|
|
|
@ -132,6 +132,7 @@ public class MageActionCallback implements ActionCallback {
|
|||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e, final TransferData data) {
|
||||
// MouseEvent can be null for custom hints calls, e.g. from choose dialog
|
||||
this.popupData = data;
|
||||
handleMouseMoveOverNewCard(data);
|
||||
}
|
||||
|
@ -288,10 +289,13 @@ public class MageActionCallback implements ActionCallback {
|
|||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e, TransferData data) {
|
||||
// MouseEvent can be null for custom hints calls, e.g. from choose dialog
|
||||
|
||||
if (!Plugins.instance.isCardPluginLoaded()) {
|
||||
return;
|
||||
}
|
||||
if (!popupData.getCard().equals(data.getCard())) {
|
||||
if (this.popupData == null
|
||||
|| !popupData.getCard().equals(data.getCard())) {
|
||||
this.popupData = data;
|
||||
handleMouseMoveOverNewCard(data);
|
||||
}
|
||||
|
@ -336,6 +340,7 @@ public class MageActionCallback implements ActionCallback {
|
|||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e, final TransferData data) {
|
||||
// MouseEvent can be null for custom hints calls, e.g. from choose dialog
|
||||
if (data != null) {
|
||||
hideAll(data.getGameId());
|
||||
} else {
|
||||
|
@ -452,6 +457,10 @@ public class MageActionCallback implements ActionCallback {
|
|||
hideTooltipPopup();
|
||||
cancelTimeout();
|
||||
Component parentComponent = SwingUtilities.getRoot(cardPanel);
|
||||
if (parentComponent == null) {
|
||||
// virtual card (example: show card popup in non cards panel like PickChoiceDialog)
|
||||
parentComponent = MageFrame.getDesktop();
|
||||
}
|
||||
Point parentPoint = parentComponent.getLocationOnScreen();
|
||||
|
||||
if (data.getLocationOnScreen() == null) {
|
||||
|
|
|
@ -162,6 +162,14 @@ public final class GUISizeHelper {
|
|||
enlargedImageHeight = 25 * PreferencesDialog.getCachedValue(PreferencesDialog.KEY_GUI_ENLARGED_IMAGE_SIZE, 20);
|
||||
}
|
||||
|
||||
public static int getTooltipCardWidth() {
|
||||
return 20 * GUISizeHelper.cardTooltipFontSize - 50;
|
||||
}
|
||||
|
||||
public static int getTooltipCardHeight() {
|
||||
return 12 * GUISizeHelper.cardTooltipFontSize - 20;
|
||||
}
|
||||
|
||||
public static void changePopupMenuFont(JPopupMenu popupMenu) {
|
||||
for (Component comp : popupMenu.getComponents()) {
|
||||
if (comp instanceof JMenuItem) {
|
||||
|
|
|
@ -44,8 +44,8 @@ public class CardInfoPaneImpl extends JEditorPane implements CardInfoPane {
|
|||
}
|
||||
|
||||
private void setGUISize() {
|
||||
addWidth = 20 * GUISizeHelper.cardTooltipFontSize - 50;
|
||||
addHeight = 12 * GUISizeHelper.cardTooltipFontSize - 20;
|
||||
addWidth = GUISizeHelper.getTooltipCardWidth();
|
||||
addHeight = GUISizeHelper.getTooltipCardHeight();
|
||||
setSize = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import java.util.UUID;
|
|||
|
||||
/**
|
||||
* Data for main card panel events like mouse moves or clicks
|
||||
*
|
||||
*/
|
||||
public class TransferData {
|
||||
|
||||
|
|
|
@ -39,6 +39,8 @@ public interface Choice extends Serializable, Copyable<Choice> {
|
|||
|
||||
String getSpecialHint();
|
||||
|
||||
ChoiceHintType getHintType();
|
||||
|
||||
// string choice
|
||||
void setChoices(Set<String> choices);
|
||||
|
||||
|
|
13
Mage/src/main/java/mage/choices/ChoiceHintType.java
Normal file
13
Mage/src/main/java/mage/choices/ChoiceHintType.java
Normal file
|
@ -0,0 +1,13 @@
|
|||
package mage.choices;
|
||||
|
||||
/**
|
||||
* For GUI: popup hint in choose dialog
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public enum ChoiceHintType {
|
||||
|
||||
TEXT,
|
||||
CARD,
|
||||
CARD_DUNGEON
|
||||
}
|
|
@ -21,6 +21,7 @@ public class ChoiceImpl implements Choice {
|
|||
protected String subMessage;
|
||||
protected boolean searchEnabled = true; // enable for all windows by default
|
||||
protected String searchText;
|
||||
protected ChoiceHintType hintType;
|
||||
|
||||
// special button with #-answer
|
||||
// warning, only for human's GUI, not AI
|
||||
|
@ -34,7 +35,12 @@ public class ChoiceImpl implements Choice {
|
|||
}
|
||||
|
||||
public ChoiceImpl(boolean required) {
|
||||
this(required, ChoiceHintType.TEXT);
|
||||
}
|
||||
|
||||
public ChoiceImpl(boolean required, ChoiceHintType hintType) {
|
||||
this.required = required;
|
||||
this.hintType = hintType;
|
||||
}
|
||||
|
||||
public ChoiceImpl(final ChoiceImpl choice) {
|
||||
|
@ -46,6 +52,7 @@ public class ChoiceImpl implements Choice {
|
|||
this.subMessage = choice.subMessage;
|
||||
this.searchEnabled = choice.searchEnabled;
|
||||
this.searchText = choice.searchText;
|
||||
this.hintType = choice.hintType;
|
||||
this.choices.addAll(choice.choices);
|
||||
this.choiceKey = choice.choiceKey;
|
||||
this.keyChoices = choice.keyChoices; // list should never change for the same object so copy by reference TODO: check errors with that, it that ok? Color list is static
|
||||
|
@ -302,4 +309,9 @@ public class ChoiceImpl implements Choice {
|
|||
public String getSpecialHint() {
|
||||
return this.specialHint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChoiceHintType getHintType() {
|
||||
return this.hintType;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import mage.abilities.effects.Effect;
|
|||
import mage.abilities.text.TextPart;
|
||||
import mage.cards.FrameStyle;
|
||||
import mage.choices.Choice;
|
||||
import mage.choices.ChoiceHintType;
|
||||
import mage.choices.ChoiceImpl;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Outcome;
|
||||
|
@ -130,11 +131,15 @@ public class Dungeon implements CommandObject {
|
|||
|
||||
public static Dungeon selectDungeon(UUID playerId, Game game) {
|
||||
Player player = game.getPlayer(playerId);
|
||||
Choice choice = new ChoiceImpl(true);
|
||||
Choice choice = new ChoiceImpl(true, ChoiceHintType.CARD_DUNGEON);
|
||||
choice.setMessage("Choose a dungeon to venture into");
|
||||
choice.setChoices(dungeonNames);
|
||||
player.choose(Outcome.Neutral, choice, game);
|
||||
switch (choice.getChoice()) {
|
||||
return createDungeon(choice.getChoice());
|
||||
}
|
||||
|
||||
public static Dungeon createDungeon(String name) {
|
||||
switch (name) {
|
||||
case "Tomb of Annihilation":
|
||||
return new TombOfAnnihilation();
|
||||
case "Lost Mine of Phandelver":
|
||||
|
|
Loading…
Reference in a new issue