Skip to content

Commit efea005

Browse files
Merge pull request #439 from JordanMartinez/addContextMenu
Add API for setting/getting context menu; implement default behavior
2 parents 5109237 + f28fc3d commit efea005

4 files changed

Lines changed: 138 additions & 1 deletion

File tree

richtextfx/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies {
1818
testCompile ("org.testfx:testfx-junit:4.0.5-alpha") {
1919
exclude(group: "junit", module: "junit")
2020
}
21+
testCompile group: 'com.nitorcreations', name: 'junit-runners', version: '1.2'
2122
}
2223

2324
javadoc {

richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import javafx.geometry.Insets;
4242
import javafx.geometry.Point2D;
4343
import javafx.scene.Node;
44+
import javafx.scene.control.ContextMenu;
4445
import javafx.scene.control.IndexRange;
4546
import javafx.scene.layout.Background;
4647
import javafx.scene.layout.BackgroundFill;
@@ -275,6 +276,29 @@ public static enum CaretVisibility {
275276
public IntFunction<? extends Node> getParagraphGraphicFactory() { return paragraphGraphicFactory.get(); }
276277
public ObjectProperty<IntFunction<? extends Node>> paragraphGraphicFactoryProperty() { return paragraphGraphicFactory; }
277278

279+
/** The {@link ContextMenu} for the area, which is by default null. */
280+
private ObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>(null);
281+
public final ContextMenu getContextMenu() { return contextMenu.get(); }
282+
public final void setContextMenu(ContextMenu menu) { contextMenu.setValue(menu); }
283+
public final ObjectProperty<ContextMenu> contextMenuObjectProperty() { return contextMenu; }
284+
protected final boolean isContextMenuPresent() { return contextMenu.get() != null; }
285+
286+
/**
287+
* The horizontal amount in pixels by which to offset the {@link #contextMenu} when it is shown, which has
288+
* a default value of 2.
289+
*/
290+
private double contextMenuXOffset = 2;
291+
public final double getContextMenuXOffset() { return contextMenuXOffset; }
292+
public final void setContextMenuXOffset(double offset) { contextMenuXOffset = offset; }
293+
294+
/**
295+
* The vertical amount in pixels by which to offset the {@link #contextMenu} when it is shown, which has
296+
* a default value of 2.
297+
*/
298+
private double contextMenuYOffset = 2;
299+
public final double getContextMenuYOffset() { return contextMenuYOffset; }
300+
public final void setContextMenuYOffset(double offset) { contextMenuYOffset = offset; }
301+
278302
/**
279303
* Indicates whether the initial style should also be used for plain text
280304
* inserted into this text area. When {@code false}, the style immediately

richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
import javafx.event.Event;
1414
import javafx.geometry.Bounds;
1515
import javafx.geometry.Point2D;
16+
import javafx.scene.control.ContextMenu;
1617
import javafx.scene.control.IndexRange;
18+
import javafx.scene.input.ContextMenuEvent;
1719
import javafx.scene.input.KeyEvent;
1820
import javafx.scene.input.MouseButton;
1921
import javafx.scene.input.MouseEvent;
@@ -183,7 +185,13 @@ class StyledTextAreaBehavior {
183185
consume(eventType(MouseEvent.MOUSE_RELEASED), StyledTextAreaBehavior::mouseReleased)
184186
);
185187

186-
EVENT_TEMPLATE = sequence(mouseEventTemplate, keyPressedTemplate, keyTypedTemplate);
188+
InputMapTemplate<StyledTextAreaBehavior, ? super ContextMenuEvent> contextMenuEventTemplate = consumeWhen(
189+
EventPattern.eventType(ContextMenuEvent.CONTEXT_MENU_REQUESTED),
190+
b -> !b.view.isDisabled() && b.view.isContextMenuPresent(),
191+
StyledTextAreaBehavior::showContextMenu
192+
);
193+
194+
EVENT_TEMPLATE = sequence(mouseEventTemplate, keyPressedTemplate, keyTypedTemplate, contextMenuEventTemplate);
187195
}
188196

189197
/**
@@ -385,12 +393,24 @@ private void skipToNextWord(SelectionPolicy selectionPolicy) {
385393
* Mouse handling implementation *
386394
* ********************************************************************** */
387395

396+
private void showContextMenu(ContextMenuEvent e) {
397+
ContextMenu menu = view.getContextMenu();
398+
double xOffset = view.getContextMenuXOffset();
399+
double yOffset = view.getContextMenuYOffset();
400+
401+
menu.show(view, e.getScreenX() + xOffset, e.getScreenY() + yOffset);
402+
}
403+
388404
private void mousePressed(MouseEvent e) {
389405
// don't respond if disabled
390406
if(view.isDisabled()) {
391407
return;
392408
}
393409

410+
if (view.isContextMenuPresent() && view.getContextMenu().isShowing()) {
411+
view.getContextMenu().hide();
412+
}
413+
394414
if(e.getButton() == MouseButton.PRIMARY) {
395415
// ensure focus
396416
view.requestFocus();
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.fxmisc.richtext.model;
2+
3+
import com.nitorcreations.junit.runners.NestedRunner;
4+
import javafx.scene.Scene;
5+
import javafx.scene.control.ContextMenu;
6+
import javafx.scene.control.MenuItem;
7+
import javafx.scene.input.KeyCode;
8+
import javafx.scene.input.MouseButton;
9+
import javafx.stage.Stage;
10+
import org.fxmisc.flowless.VirtualizedScrollPane;
11+
import org.fxmisc.richtext.InlineCssTextArea;
12+
import org.junit.Test;
13+
import org.junit.runner.RunWith;
14+
import org.testfx.framework.junit.ApplicationTest;
15+
16+
@RunWith(NestedRunner.class)
17+
public class StyledTextAreaBehaviorTest {
18+
19+
static {
20+
String osName = System.getProperty("os.name").toLowerCase();
21+
22+
WINDOWS_OS = osName.contains("win");
23+
}
24+
25+
private static final boolean WINDOWS_OS;
26+
27+
public class ContextMenuTests extends ApplicationTest {
28+
29+
public InlineCssTextArea area = new InlineCssTextArea();
30+
31+
@Override
32+
public void start(Stage stage) throws Exception {
33+
area.setContextMenu(new ContextMenu(new MenuItem("A Menu Item")));
34+
// offset needs to be 5 to prevent test failures
35+
area.setContextMenuXOffset(5);
36+
area.setContextMenuYOffset(5);
37+
38+
VirtualizedScrollPane<InlineCssTextArea> pane = new VirtualizedScrollPane<>(area);
39+
stage.setScene(new Scene(pane, 500, 500));
40+
stage.show();
41+
}
42+
43+
@Test
44+
public void clickingSecondaryShowsContextMenu() {
45+
// when
46+
rightClickOn(area);
47+
48+
// then
49+
area.getContextMenu().isShowing();
50+
}
51+
52+
@Test
53+
public void pressingSecondaryShowsContextMenu() {
54+
// when
55+
moveTo(area);
56+
press(MouseButton.SECONDARY);
57+
58+
// then
59+
area.getContextMenu().isShowing();
60+
}
61+
62+
@Test
63+
public void pressingPrimaryMouseButtonHidesContextMenu() {
64+
// given menu is showing
65+
rightClickOn(area);
66+
67+
press(MouseButton.PRIMARY);
68+
assert !area.getContextMenu().isShowing();
69+
}
70+
71+
@Test
72+
public void pressingMiddleMouseButtonHidesContextMenu() {
73+
// given menu is showing
74+
rightClickOn(area);
75+
76+
press(MouseButton.MIDDLE);
77+
assert !area.getContextMenu().isShowing();
78+
}
79+
80+
@Test
81+
public void requestingContextMenuViaKeyboardWorksOnWindows() {
82+
if (WINDOWS_OS) {
83+
clickOn(area);
84+
press(KeyCode.CONTEXT_MENU);
85+
86+
assert area.getContextMenu().isShowing();
87+
}
88+
}
89+
90+
}
91+
92+
}

0 commit comments

Comments
 (0)