Skip to content

8207333: [Linux, macOS] Column sorting is triggered always after context menu request on table header #1754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -233,6 +233,8 @@ public TableColumnHeader(final TableColumnBase tc) {
private final WeakListChangeListener<String> weakStyleClassListener =
new WeakListChangeListener<>(styleClassListener);

private static boolean popupTriggeredOnMousePressed;

private static final EventHandler<MouseEvent> mousePressedHandler = me -> {
TableColumnHeader header = (TableColumnHeader) me.getSource();
TableColumnBase tableColumn = header.getTableColumn();
Expand All @@ -243,6 +245,7 @@ public TableColumnHeader(final TableColumnBase tc) {
}

if (me.isConsumed()) return;
popupTriggeredOnMousePressed = me.isPopupTrigger();
me.consume();

header.getTableHeaderRow().columnDragLock = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Mac a context menu can be invoked using the right button or the left button in conjunction with the Control key. In the second case the pop trigger flag will be set and the primary button will be down. If I'm reading the code correctly this could get the header stuck in column re-ordering mode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing on macOS 15.3.2, it does not get stuck when using control+left-click, works as expected.

There is one (existing and unrelated) problem - if the user tries to dismiss the popup menu by clicking on the header, it dismisses the popup (correctly), but then proceeds to re-sort the table (unexpected).

One can say it works as designed, since the click falls on a header cell and the expected operation is to toggle the sorting, with the side effect of dismissing the popup.

On the other hand, Excel (both mac and windows) behaves differently - a click on the spreadsheet header dismisses the popup and does not select the column, as it normally does when no popup is present.

What do you guys think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

control+left-click works as expected for me too.

When the context menu is showing, clicking anywhere outside of it, closes the popup (because of the autohide property), but doesn't consume the event. If you click on a button, for instance, it will be fired. In any case, this is the same before and after this PR, so maybe an issue for a later discussion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely an unrelated issue, I just wanted to ask whether you guys think it crosses the threshold for a bug.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

control+left-click works as expected for me too.

For control+left-click it looks like the code will call columnReordingStarted but on the released event it won't call columnReorderingComplete. I have no idea if this will lead to problems or not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beldenfox Do you think we should address that issue (ctrl+left click + columnReordering) in this PR? Else, do we need to do anything else to move it forward?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be good with a few tweaks. If the press event is a popup trigger don't set the columnDragLock or call columnReorderingStarted (since you might not get a release event). In the drag handler check popupTriggeredOnMousePressed and do nothing if it's set. Your released handler looks fine as-is. I think that should cover it.

By the way, in a perfect world you would have more predictable behavior. When the popup menu is shown it would grab the mouse pointer and you would never see the release event. That would allow the user to click to bring up the popup and start dragging across the menu items without having to release the mouse.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @beldenfox! I've included your suggestions, and also added the ctrl+left click macOS case to the test.

Expand All @@ -251,13 +254,13 @@ public TableColumnHeader(final TableColumnBase tc) {
// the focus rectangle around the table control.
header.getTableSkin().getSkinnable().requestFocus();

if (me.isPrimaryButtonDown() && header.isColumnReorderingEnabled()) {
if (me.isPrimaryButtonDown() && header.isColumnReorderingEnabled() && !popupTriggeredOnMousePressed) {
header.columnReorderingStarted(me.getX());
}
};

private static final EventHandler<MouseEvent> mouseDraggedHandler = me -> {
if (me.isConsumed()) return;
if (me.isConsumed() || popupTriggeredOnMousePressed) return;
me.consume();

TableColumnHeader header = (TableColumnHeader) me.getSource();
Expand All @@ -271,7 +274,10 @@ public TableColumnHeader(final TableColumnBase tc) {
TableColumnHeader header = (TableColumnHeader) me.getSource();
header.getTableHeaderRow().columnDragLock = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Ubuntu 22.04 the table column header is not receiving a mouse released event if a context menu is shown. Not sure where the released event is going but it's not to any node on the primary stage. The docs are vague on this but this looks like a separate bug that's making it hard to test this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens in the master branch (without this PR's changes)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was having trouble reproducing the original bug on the master branch; the sorting is done when the mouse is released and most of the time that event never arrives (I think it does every now and then).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is strange - the bug is immediately apparent. which macOS version do you have? although I don't think the version matters.

I am testing with an external mouse, with control-left-click, and with the double-finger-click gesture on the trackpad.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was having problems reproducing on Linux. No problem reproducing on macOS.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Linux I have some issues too: the test that I attached to the JBS issue doesn't fail for me on Linux, but the test that I added to this PR does fail without the fix.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try the monkey tester? it allows setting properties on the table view / table column at runtime...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just verified that on Linux the released event usually goes to the popup window. Basically it's whatever window is under the cursor at the time; if I move the mouse fast enough it may go to a node on the primary stage. I don't think this is our doing, it's how GTK is delivering the event.

On the JavaFX side I'm not sure this counts as a bug. The docs state that entered and released events should arrive in pairs during drag and drop operations but doesn't address this case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Monkey tester app works for me on Linux (Ubuntu 22.04, either mouse or pad), before and after this PR (so I can't reproduce the issue there, as I mentioned, though the test added to the PR fails without the patch).


if (me.isPopupTrigger()) return;
if (popupTriggeredOnMousePressed || me.isPopupTrigger()) {
popupTriggeredOnMousePressed = false;
return;
}
if (me.isConsumed()) return;
me.consume();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package test.robot.javafx.scene.tableview;

import com.sun.javafx.PlatformUtil;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.robot.Robot;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.WindowEvent;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import test.util.Util;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class TableViewContextMenuSortTest {

static Robot robot;
static volatile Stage stage;
static volatile Scene scene;
static final int SCENE_WIDTH = 800;
static final int SCENE_HEIGHT = 250;
static final CountDownLatch startupLatch = new CountDownLatch(1);
private static final List<TableEntry> unsortedList = List.of(new TableEntry("One"), new TableEntry("Two"), new TableEntry("Three"), new TableEntry("Four"));

private static TableView<TableEntry> table;

public static void main(String[] args) {
TableViewContextMenuSortTest test = new TableViewContextMenuSortTest();
test.testContextMenuRequestDoesNotSort();
}

@Test
public void testContextMenuRequestDoesNotSort() {
Node header = table.lookupAll(".column-header").stream()
.filter(Objects::nonNull)
.filter(n -> n.getStyleClass().contains("table-column"))
.findFirst()
.orElseThrow();
Bounds bounds = header.localToScreen(header.getLayoutBounds());
double posX = bounds.getMinX() + 10;
double posY = bounds.getMinY() + 5;

AtomicInteger counter = new AtomicInteger();
header.addEventFilter(MouseEvent.ANY, e -> {
if (e.isPopupTrigger()) {
counter.incrementAndGet();
}
});
Util.runAndWait(() -> {
robot.mouseMove((int) posX, (int) posY);
robot.mousePress(MouseButton.SECONDARY);
robot.mouseRelease(MouseButton.SECONDARY);
});
Util.sleep(1000);

assertEquals(counter.get(), 1);
for (int i = 0; i < 4; i++) {
assertEquals(unsortedList.get(i).getName(), table.getItems().get(i).getName());
}

// macOS only: Ctrl + Left click also triggers the context menu
if (PlatformUtil.isMac()) {
Util.runAndWait(() -> {
robot.keyPress(KeyCode.ESCAPE);
robot.keyRelease(KeyCode.ESCAPE);
});
Util.sleep(100);

Util.runAndWait(() -> {
robot.keyPress(KeyCode.CONTROL);
robot.mousePress(MouseButton.PRIMARY);
robot.mouseRelease(MouseButton.PRIMARY);
robot.keyRelease(KeyCode.CONTROL);
});
Util.sleep(1000);

assertEquals(counter.get(), 2);
for (int i = 0; i < 4; i++) {
assertEquals(unsortedList.get(i).getName(), table.getItems().get(i).getName());
}
}
}

@BeforeAll
public static void initFX() {
Util.launch(startupLatch, TestApp.class);
}

@AfterAll
public static void exit() {
Util.shutdown();
}

public static class TestApp extends Application {

@Override
public void start(Stage primaryStage) {
robot = new Robot();
stage = primaryStage;

primaryStage.setTitle("TableView Test");

TableColumn<TableEntry, String> col = new TableColumn<>("First Name");
col.setSortable(true);
col.setContextMenu(new ContextMenu(new MenuItem("Item")));

table = new TableView<>();
table.getColumns().addAll(col);
table.getItems().addAll(unsortedList);
col.setCellValueFactory(new PropertyValueFactory<>("name"));

StackPane root = new StackPane();
root.getChildren().add(table);
scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT);
stage.setScene(scene);
stage.initStyle(StageStyle.UNDECORATED);
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e -> Platform.runLater(startupLatch::countDown));
stage.setAlwaysOnTop(true);
stage.show();
}
}

public static class TableEntry {
StringProperty name = new SimpleStringProperty();

public TableEntry(String name) {
this.name.set(name);
}

public String getName() {
return name.get();
}

public void setName(String name) {
this.name.set(name);
}

@Override
public String toString() {
return "TableEntry [name=" + name + "]";
}
}
}