Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ec1c136
Add card info popup with card image, spellbook, and size controls
MostCromulent Feb 15, 2026
51cff1d
Improve card info popup: single-instance enforcement, focus handling,…
MostCromulent Feb 15, 2026
4aa4a40
Constrain CardInfoPopup to Forge window bounds instead of screen bounds
MostCromulent Feb 16, 2026
8573d9a
Improve card info popup: layout, sizing, and related card categories
MostCromulent Feb 19, 2026
c2527b0
Restyle card info popup with dark pill panels and proper text rendering
MostCromulent Feb 19, 2026
41e8057
Add keyword actions to card info popup and improve layout
MostCromulent Feb 19, 2026
5ac627c
Merge branch 'Card-Forge:master' into hoveroptions
MostCromulent Feb 19, 2026
f9f51c1
Clean up card info popup: remove dead code and duplicate state
MostCromulent Feb 19, 2026
24a7fab
Fix popup during zoom and related card rendering without art
MostCromulent Feb 19, 2026
80af802
Improve keyword popup text and ordering
MostCromulent Feb 19, 2026
27bf7f1
Add keyword and related card info to card zoom view
MostCromulent Feb 19, 2026
6f5e495
Fix granted keyword detection and clean up popup labels
MostCromulent Feb 19, 2026
f47e697
Improve zoom view layout and fix related card image loading
MostCromulent Feb 19, 2026
5db88c6
Add translation keys for keyword action names and reminder text
MostCromulent Feb 19, 2026
59b0115
Remove duplicate English strings from KeywordAction enum
MostCromulent Feb 19, 2026
6709ad0
Move keyword detection logic to shared forge-gui module
MostCromulent Feb 19, 2026
a54d3dc
Fix Assemble/Set In Motion descriptions and mark scheme actions as basic
MostCromulent Feb 20, 2026
be48e91
Handle scry conjugation in keyword matching and fix zoom scroll layout
MostCromulent Feb 20, 2026
c79e6f6
Address PR reviewer feedback on keyword display and zoom view
MostCromulent Feb 21, 2026
8a2ce7d
Fix tooltip flickering on tapped cards by using component bounds for …
MostCromulent Feb 21, 2026
bc46a36
Improve hover tooltip and zoom view layout and fix keyword display is…
MostCromulent Feb 21, 2026
b1e6ca3
Fix specialize face rendering and Scryfall download for hover tooltip
MostCromulent Feb 21, 2026
c9da360
Add dynamic keyword count annotations to hover tooltips and zoom view
MostCromulent Feb 21, 2026
4f15496
Improve Affinity keyword matching and add stack hover tooltips
MostCromulent Feb 21, 2026
adfee29
Add card overlay painting to hover tooltip image
MostCromulent Feb 21, 2026
b2b0498
Remove dead code from CardInfoPopup
MostCromulent Feb 21, 2026
efc1247
Merge branch 'Card-Forge:master' into hoveroptions
MostCromulent Feb 21, 2026
9f1f468
Add menu options to hide card picture and detail panels
MostCromulent Feb 22, 2026
6600fe0
Fix card panel re-enable to create right column instead of stacking i…
MostCromulent Feb 22, 2026
f1b0fd9
Fix face-down info leak, enable keyword popup by default, and cleanup…
MostCromulent Feb 22, 2026
7e52afd
Adjust tooltip overlay positions and hide popup on game end
MostCromulent Feb 22, 2026
150c7d7
Merge upstream/master into hoveroptions
MostCromulent Feb 24, 2026
cfcd562
Show hover tooltip on game log inline card images
MostCromulent Feb 24, 2026
6820fbf
Add Card Overlay Settings dialog with independent hover overlay prefs
MostCromulent Feb 24, 2026
2843e96
Improve hover tooltip keywords, related cards, and preference handling
MostCromulent Feb 25, 2026
5af05ac
Show Amass Army tokens as related cards in hover tooltip
MostCromulent Feb 25, 2026
cbdfa8e
Merge upstream/master into hoveroptions
MostCromulent Feb 25, 2026
2ce6e02
Minor comment cleanup
MostCromulent Feb 25, 2026
e415e74
Rework tooltip preferences: master toggles, hotkeys, and updated defa…
MostCromulent Feb 25, 2026
3b990c0
Fix devotion tooltip: support dual-color devotion and immediate hotke…
MostCromulent Feb 25, 2026
2d8d828
Remove dead code: unused JSeparator and unreachable overload
MostCromulent Feb 25, 2026
985075c
Address PR review feedback: keyword display and layout fixes
MostCromulent Feb 26, 2026
00f5230
Merge upstream/master into hoveroptions
MostCromulent Mar 1, 2026
7c6552f
Fix Escape/Craft keyword headers: wrapping and full cost display
MostCromulent Mar 1, 2026
da9e502
Hover/zoom tooltip: 1-card-wide keywords + compact multi-group relate…
MostCromulent Mar 2, 2026
9fbd234
Fix Support false positives, suppress Manifest when Manifest Dread pr…
MostCromulent Mar 2, 2026
e18a9f0
Add graveyard count tooltips for Tarmogoyf/Lhurgoyf-family cards
MostCromulent Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions forge-game/src/main/java/forge/game/keyword/KeywordAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package forge.game.keyword;

import forge.util.Localizer;

/**
* Keyword actions — verbs that appear in rules text but aren't keyword abilities.
* Descriptions are based on the MTG comprehensive rules (section 701).
*
* <p>Unlike {@link Keyword} (keyword abilities that grant continuous effects or
* triggered/static abilities), keyword actions are one-shot game actions performed
* when instructed by a spell or ability.</p>
*
* <p>Actions marked {@code basic=true} are fundamental game actions (destroy, exile,
* sacrifice, etc.) that every player knows — UI code may choose to omit these from
* tooltips to avoid clutter.</p>
*
* <p>Display names and reminder text are stored in {@code en-US.properties} under
* keys derived from the enum name (e.g. {@code lblKwActionActivate},
* {@code lblKwActionActivateReminder}).</p>
*/
public enum KeywordAction {
// 701.2 – 701.13: Basic game actions
ACTIVATE(true),
ATTACH(true),
BEHOLD(false),
CAST(true),
COUNTER(true),
CREATE(true),
DESTROY(true),
DISCARD(true),
DOUBLE(true),
TRIPLE(true),
EXCHANGE(true),
EXILE(true),

// 701.14 – 701.17: Combat & graveyard actions
FIGHT(false),
GOAD(false),
INVESTIGATE(false),
MILL(false),

// 701.18 – 701.21: More basic actions
PLAY(true),
REGENERATE(false),
REVEAL(true),
SACRIFICE(true),

// 701.22 – 701.28: Library & transform actions
SCRY(false),
SEARCH(true),
SHUFFLE(true),
SURVEIL(false),
TAP_UNTAP(true),
TRANSFORM(false),
CONVERT(false),

// 701.29 – 701.30: Opponent manipulation
FATESEAL(false),
CLASH(false),

// 701.31 – 701.33: Supplemental format actions
PLANESWALK(false),
SET_IN_MOTION(false),
ABANDON(false),

// 701.34 – 701.36: Counter & token actions
PROLIFERATE(false),
DETAIN(false),
POPULATE(false),

// 701.37 – 701.39: Creature enhancement
MONSTROSITY(false),
VOTE(false),
BOLSTER(false),

// 701.40 – 701.44: Face-down & explore
MANIFEST(false),
SUPPORT(false),
MELD(false),
EXERT(false),
EXPLORE(false),

// 701.45 – 701.48: Un-set & learning
ASSEMBLE(false),
ADAPT(false),
AMASS(false),
LEARN(false),

// 701.49 – 701.50: Dungeon & connive
VENTURE(false),
CONNIVE(false),

// 701.51 – 701.52: Attraction actions
OPEN_AN_ATTRACTION(false),
ROLL_TO_VISIT(false),

// 701.53 – 701.54: Incubate & ring
INCUBATE(false),
THE_RING_TEMPTS_YOU(false),

// 701.55 – 701.56: Villainous choice & time travel
FACE_A_VILLAINOUS_CHOICE(false),
TIME_TRAVEL(false),

// 701.57 – 701.60: Discover, cloak, evidence, suspect
DISCOVER(false),
CLOAK(false),
COLLECT_EVIDENCE(false),
SUSPECT(false),

// 701.61 – 701.64: Bloomburrow & beyond
FORAGE(false),
MANIFEST_DREAD(false),
ENDURE(false),
HARNESS(false),

// 701.65 – 701.68: Avatar & Lorwyn Eclipsed
AIRBEND(false),
EARTHBEND(false),
WATERBEND(false),
BLIGHT(false);

/** True for fundamental game actions (destroy, exile, sacrifice, etc.) that don't need tooltip explanations. */
public final boolean basic;
/** Translation key prefix derived from enum name, e.g. "lblKwActionActivate". */
private final String translationKey;

KeywordAction(boolean basic) {
this.basic = basic;
this.translationKey = "lblKwAction" + toCamelCase(name());
}

private static String toCamelCase(String enumName) {
StringBuilder sb = new StringBuilder();
boolean capitalize = true;
for (char c : enumName.toCharArray()) {
if (c == '_') {
capitalize = true;
} else {
sb.append(capitalize ? c : Character.toLowerCase(c));
capitalize = false;
}
}
return sb.toString();
}

/** Returns the localized display name. */
public String getDisplayName() {
return Localizer.getInstance().getMessage(translationKey);
}

/** Returns the localized reminder text. */
public String getReminderText() {
return Localizer.getInstance().getMessage(translationKey + "Reminder");
}

@Override
public String toString() {
return getDisplayName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public String getTitle() {

@Override
protected void parse(String details) {
fromWhat = details.startsWith("from ") ? details.substring(5) : details;
}

@Override
Expand Down
2 changes: 1 addition & 1 deletion forge-gui-desktop/src/main/java/forge/CachedCardImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public abstract class CachedCardImage implements ImageFetcher.Callback {
final int width;
final int height;

static final SwingImageFetcher fetcher = new SwingImageFetcher();
public static final SwingImageFetcher fetcher = new SwingImageFetcher();

public CachedCardImage(final CardView card, final Iterable<PlayerView> viewers, final int width, final int height) {
this.card = card;
Expand Down
4 changes: 4 additions & 0 deletions forge-gui-desktop/src/main/java/forge/ImageCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ public static Pair<BufferedImage, Boolean> getCardOriginalImageInfo(String image
return getOriginalImageInternal(imageKey, useDefaultIfNotFound, null);
}

public static Pair<BufferedImage, Boolean> getCardOriginalImageInfo(String imageKey, boolean useDefaultIfNotFound, CardView cardView) {
return getOriginalImageInternal(imageKey, useDefaultIfNotFound, cardView);
}

// return the pair of image and a flag to indicate if it is a placeholder image.
private static Pair<BufferedImage, Boolean> getOriginalImageInternal(String imageKey, boolean useDefaultIfNotFound,
CardView cardView) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ public void initialize() {
lstControls.add(Pair.of(view.getCbOpenPacksIndiv(), FPref.UI_OPEN_PACKS_INDIV));
lstControls.add(Pair.of(view.getCbTokensInSeparateRow(), FPref.UI_TOKENS_IN_SEPARATE_ROW));
lstControls.add(Pair.of(view.getCbStackCreatures(), FPref.UI_STACK_CREATURES));
lstControls.add(Pair.of(view.getCbPopupKeywordInfo(), FPref.UI_POPUP_KEYWORD_INFO));
lstControls.add(Pair.of(view.getCbPopupRelatedCards(), FPref.UI_POPUP_RELATED_CARDS));
lstControls.add(Pair.of(view.getCbManaLostPrompt(), FPref.UI_MANA_LOST_PROMPT));
lstControls.add(Pair.of(view.getCbEscapeEndsTurn(), FPref.UI_ALLOW_ESC_TO_END_TURN));
lstControls.add(Pair.of(view.getCbDetailedPaymentDesc(), FPref.UI_DETAILED_SPELLDESC_IN_PROMPT));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
private final JCheckBox cbOpenPacksIndiv = new OptionsCheckBox(localizer.getMessage("cbOpenPacksIndiv"));
private final JCheckBox cbTokensInSeparateRow = new OptionsCheckBox(localizer.getMessage("cbTokensInSeparateRow"));
private final JCheckBox cbStackCreatures = new OptionsCheckBox(localizer.getMessage("cbStackCreatures"));
private final JCheckBox cbPopupKeywordInfo = new OptionsCheckBox(localizer.getMessage("cbPopupKeywordInfo"));
private final JCheckBox cbPopupRelatedCards = new OptionsCheckBox(localizer.getMessage("cbPopupRelatedCards"));
private final JCheckBox cbFilterLandsByColorId = new OptionsCheckBox(localizer.getMessage("cbFilterLandsByColorId"));
private final JCheckBox cbShowStormCount = new OptionsCheckBox(localizer.getMessage("cbShowStormCount"));
private final JCheckBox cbRemindOnPriority = new OptionsCheckBox(localizer.getMessage("cbRemindOnPriority"));
Expand Down Expand Up @@ -439,6 +441,12 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
pnlPrefs.add(cbStackCreatures, titleConstraints);
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlStackCreatures")), descriptionConstraints);

pnlPrefs.add(cbPopupKeywordInfo, titleConstraints);
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlPopupKeywordInfo")), descriptionConstraints);

pnlPrefs.add(cbPopupRelatedCards, titleConstraints);
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlPopupRelatedCards")), descriptionConstraints);

pnlPrefs.add(cbTimedTargOverlay, titleConstraints);
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlTimedTargOverlay")), descriptionConstraints);

Expand Down Expand Up @@ -993,6 +1001,14 @@ public final JCheckBox getCbStackCreatures() {
return cbStackCreatures;
}

public final JCheckBox getCbPopupKeywordInfo() {
return cbPopupKeywordInfo;
}

public final JCheckBox getCbPopupRelatedCards() {
return cbPopupRelatedCards;
}

public final JCheckBox getCbManaLostPrompt() {
return cbManaLostPrompt;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package forge.screens.match.menus;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.MouseEvent;

import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SwingConstants;

import forge.localinstance.properties.ForgePreferences;
import forge.localinstance.properties.ForgePreferences.FPref;
import forge.model.FModel;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinnedLabel;
import forge.toolbox.FSkin.SkinnedSlider;
import forge.util.Localizer;

/**
* Submenu under the Game menu for toggling card info popup sections
* (keyword explanations and related cards) during a match.
*/
public final class CardInfoPopupMenu {
private static final ForgePreferences prefs = FModel.getPreferences();

public CardInfoPopupMenu() {
}

public JMenu getMenu() {
final Localizer localizer = Localizer.getInstance();
final JMenu menu = new JMenu(localizer.getMessage("lblCardInfoPopups"));

// --- Hover Tooltip section ---
menu.add(createSectionHeader(localizer.getMessage("lblHoverTooltip")));
menu.add(getCheckboxItem(localizer.getMessage("lblCardImage"),
FPref.UI_POPUP_CARD_IMAGE));
menu.add(getCheckboxItem(localizer.getMessage("lblRelatedCards"),
FPref.UI_POPUP_RELATED_CARDS));
menu.add(getCheckboxItem(localizer.getMessage("lblKeywordExplanations"),
FPref.UI_POPUP_KEYWORD_INFO));

menu.add(new JSeparator());

// --- Card Zoom View section ---
menu.add(createSectionHeader(localizer.getMessage("lblCardZoomView")));
menu.add(getCheckboxItem(localizer.getMessage("lblRelatedCards"),
FPref.UI_ZOOM_RELATED_CARDS));
menu.add(getCheckboxItem(localizer.getMessage("lblKeywordExplanations"),
FPref.UI_ZOOM_KEYWORD_INFO));

menu.add(new JSeparator());
menu.add(buildImageSizePanel(localizer));
return menu;
}

private static JMenuItem createSectionHeader(final String text) {
final JMenuItem header = new JMenuItem(text);
header.setEnabled(false);
header.setFont(header.getFont().deriveFont(Font.BOLD));
return header;
}

private static JCheckBoxMenuItem getCheckboxItem(final String label, final FPref pref) {
final JCheckBoxMenuItem item = new JCheckBoxMenuItem(label) {
@Override
protected void processMouseEvent(final MouseEvent e) {
if (e.getID() == MouseEvent.MOUSE_RELEASED && contains(e.getPoint())) {
doClick(0);
setArmed(true);
} else {
super.processMouseEvent(e);
}
}
};
item.setState(prefs.getPrefBoolean(pref));
item.addActionListener(e -> {
final boolean newState = !prefs.getPrefBoolean(pref);
prefs.setPref(pref, newState);
prefs.save();
});
return item;
}

private static JPanel buildImageSizePanel(final Localizer localizer) {
final Color bg = FSkin.getColor(FSkin.Colors.CLR_THEME2).getColor();
final Color fg = FSkin.getColor(FSkin.Colors.CLR_TEXT).getColor();
final int rawValue = prefs.getPrefInt(FPref.UI_POPUP_IMAGE_SIZE);
final int currentValue = Math.max(100, Math.min(500, rawValue));

final JPanel panel = new JPanel(new BorderLayout());
panel.setBackground(bg);

final SkinnedLabel label = new SkinnedLabel();
label.setText(localizer.getMessage("lblImageSize"));
label.setForeground(fg);
label.setFont(FSkin.getFont());
label.setOpaque(true);
label.setBackground(bg);

final SkinnedSlider slider = new SkinnedSlider(SwingConstants.HORIZONTAL, 100, 500, currentValue);
slider.setMajorTickSpacing(100);
slider.setMinorTickSpacing(20);
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setBackground(bg);
slider.setForeground(fg);
slider.setFont(FSkin.getFont());

slider.addChangeListener(e -> {
slider.repaint();
prefs.setPref(FPref.UI_POPUP_IMAGE_SIZE, String.valueOf(slider.getValue()));
prefs.save();
});

panel.add(label, BorderLayout.NORTH);
panel.add(slider, BorderLayout.CENTER);
return panel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public JMenu getMenu() {
menu.addSeparator();
menu.add(getMenuItem_TargetingArcs());
menu.add(new CardOverlaysMenu(matchUI).getMenu());
menu.add(new CardInfoPopupMenu().getMenu());
menu.add(getMenuItem_AutoYields());
menu.addSeparator();
menu.add(getMenuItem_ViewDeckList());
Expand Down
Loading