Skip to content
Merged
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
18 changes: 16 additions & 2 deletions CodenameOne/src/com/codename1/ui/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -6382,7 +6382,15 @@ private boolean handleInteractiveScrollPress(int x, int y) {
int trackTop = absY + scrollTrackY;
int trackLeft = absX + scrollTrackX;
if (x >= trackLeft && x < trackLeft + scrollTrackW && y >= trackTop && y < trackTop + scrollTrackH) {
pageScroll(true, y < thumbTop);
if (getUIManager().getLookAndFeel().isInteractiveScrollTrackJump()) {
// jump the thumb's center to the click and keep it grabbed, so the
// same press can go on to drag it
draggingScrollThumbY = true;
scrollThumbGrabOffset = scrollThumbH / 2;
dragScrollThumb(x, y);
} else {
pageScroll(true, y < thumbTop);
}
return true;
}
}
Expand All @@ -6399,7 +6407,13 @@ private boolean handleInteractiveScrollPress(int x, int y) {
int trackLeft = absX + hScrollTrackX;
int trackTop = absY + hScrollTrackY;
if (x >= trackLeft && x < trackLeft + hScrollTrackW && y >= trackTop && y < trackTop + hScrollTrackH) {
pageScroll(false, x < thumbLeft);
if (getUIManager().getLookAndFeel().isInteractiveScrollTrackJump()) {
draggingScrollThumbX = true;
scrollThumbGrabOffset = hScrollThumbW / 2;
dragScrollThumb(x, y);
} else {
pageScroll(false, x < thumbLeft);
}
return true;
}
}
Expand Down
16 changes: 16 additions & 0 deletions CodenameOne/src/com/codename1/ui/plaf/LookAndFeel.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ public abstract class LookAndFeel {
/// when the content is much taller/wider than the viewport. Resolved from the
/// {@code scrollThumbMinSizeInt} theme constant, defaulting to roughly 4mm.
private int interactiveScrollThumbMin;
/// When true a click on an interactive scrollbar's track moves the thumb to the click and
/// keeps it grabbed, instead of paging by one viewport. Resolved from the
/// {@code interactiveScrollTrackJumpBool} theme constant, off by default.
private boolean interactiveScrollTrackJump;
private int fadeScrollBarSpeed = 5;
private int fadeScrollEdgeLength = 15;
private Image fadeScrollTop;
Expand Down Expand Up @@ -1099,6 +1103,7 @@ public void refreshTheme(boolean completeClear) {
interactiveScroll = manager.isThemeConstant("interactiveScrollBool", false);
interactiveScrollThumbMin = manager.getThemeConstant("scrollThumbMinSizeInt",
Display.getInstance().convertToPixels(4f));
interactiveScrollTrackJump = manager.isThemeConstant("interactiveScrollTrackJumpBool", false);
initScroll();
if (menuRenderer != null) {
if (menuRenderer instanceof Component) {
Expand Down Expand Up @@ -1644,6 +1649,17 @@ public boolean isInteractiveScroll() {
return interactiveScroll;
}

/// Indicates whether a click on an interactive scrollbar's track jumps the thumb to the
/// click position (and keeps it grabbed for dragging) rather than paging one viewport
/// toward it. Controlled by the {@code interactiveScrollTrackJumpBool} theme constant.
///
/// #### Returns
///
/// true if a track click jumps to the clicked position
public boolean isInteractiveScrollTrackJump() {
return interactiveScrollTrackJump;
}

/// Scrollbar thumb used for interactive desktop scrollbars. It renders its selected style
/// when the pointer hovers the thumb and its pressed style while the thumb is being dragged,
/// giving the desktop-conventional highlight feedback. The visual state is pushed in by the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5378,6 +5378,18 @@ public void installNativeTheme(){
// undefined StatusBar UIID would also paint an opaque strip there).
tp.put("@paintsTitleBarBool", "false");

// A browser driven by a mouse gets desktop scrollbars: a thumb that can be grabbed
// and dragged, and a track that pages on click. Without this constant the core
// treats a press on the scrollbar as a press on the content, so dragging the thumb
// became a touch swipe and scrolled the opposite way. The JavaSE port injects the
// same constant for desktop builds. ANY fine pointer qualifies, not only the
// primary one: a touch laptop still has its mouse, and the scrollbar only claims
// presses that land on it, so a finger swipe over the content still scrolls. A
// theme that sets the constant itself keeps its own value.
if (!tp.containsKey("@interactiveScrollBool") && matchesMediaQuery("(any-pointer: fine)")) {
tp.put("@interactiveScrollBool", "true");
}

// Before the theme applies: a desktop theme sets commandBehavior Native, and
// setCommandBehavior normalises that to the default for good when the port reports no
// native menu bar at that moment -- which it does until the HTML chrome exists. Forms
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.ui;

import com.codename1.junit.FormTest;
Expand Down Expand Up @@ -125,6 +147,40 @@ void trackClickBelowThumbPagesDown() {
sc.pointerReleased(px, py);
}

@FormTest
void trackClickJumpsToPositionWhenEnabled() {
setInteractiveScroll(true);
Hashtable props = new Hashtable();
props.put("@interactiveScrollTrackJumpBool", "true");
UIManager.getInstance().addThemeProps(props);
assertTrue(UIManager.getInstance().getLookAndFeel().isInteractiveScrollTrackJump());
try {
Container sc = scrollableContainer(8000);
int thumbH = sc.getHeight() / 5;
injectVerticalThumb(sc, 0, thumbH);

// click the middle of the track: the thumb centers there, i.e. half the range
int px = sc.getAbsoluteX() + sc.getWidth() - GUTTER / 2;
int py = sc.getAbsoluteY() + sc.getHeight() / 2;
sc.pointerPressed(px, py);
assertTrue(sc.isInteractiveScrollThumbGrabbed(), "a jump keeps the thumb grabbed for dragging");
int max = sc.getScrollDimension().getHeight() - sc.getHeight();
int expected = max / 2;
assertTrue(Math.abs(sc.getScrollY() - expected) <= max / 50,
"a track click should jump to the clicked position, got " + sc.getScrollY()
+ " expected about " + expected);

int mid = sc.getScrollY();
sc.pointerDragged(px, py + 50);
assertTrue(sc.getScrollY() > mid, "the same press goes on to drag the thumb");
sc.pointerReleased(px, py + 50);
assertFalse(sc.isInteractiveScrollThumbGrabbed());
} finally {
props.put("@interactiveScrollTrackJumpBool", "false");
UIManager.getInstance().addThemeProps(props);
}
}

@FormTest
void disabledIsInert() {
setInteractiveScroll(false);
Expand Down
Loading