diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java index 4bc3e1b1f39..41255639169 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java @@ -139,16 +139,19 @@ protected boolean _setVisible(long ptr, boolean visible) { @Override protected boolean _minimize(long ptr, boolean minimize) { minimizeImpl(ptr, minimize); - notifyStateChanged(WindowEvent.MINIMIZE); - return minimize; + + if (!isVisible()) { + notifyStateChanged(WindowEvent.MINIMIZE); + } + + return isMinimized(); } @Override protected boolean _maximize(long ptr, boolean maximize, boolean wasMaximized) { maximizeImpl(ptr, maximize, wasMaximized); - notifyStateChanged(WindowEvent.MAXIMIZE); - return maximize; + return isMaximized(); } protected void notifyStateChanged(final int state) { diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp index a9e84cd1559..b393715cb59 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp @@ -488,13 +488,13 @@ static void process_events(GdkEvent* event, gpointer data) try { switch (event->type) { case GDK_PROPERTY_NOTIFY: - // let gtk handle it first to prevent a glitch - gtk_main_do_event(event); ctx->process_property_notify(&event->property); + gtk_main_do_event(event); break; case GDK_CONFIGURE: - ctx->process_configure(&event->configure); + // Let gtk handle it first, so state values are updated gtk_main_do_event(event); + ctx->process_configure(&event->configure); break; case GDK_FOCUS_CHANGE: ctx->process_focus(&event->focus_change); @@ -512,8 +512,9 @@ static void process_events(GdkEvent* event, gpointer data) ctx->process_expose(&event->expose); break; case GDK_WINDOW_STATE: - ctx->process_state(&event->window_state); + // Let gtk handle it first, so state values are updated gtk_main_do_event(event); + ctx->process_state(&event->window_state); break; case GDK_BUTTON_PRESS: case GDK_2BUTTON_PRESS: @@ -542,7 +543,8 @@ static void process_events(GdkEvent* event, gpointer data) process_dnd_target(ctx, &event->dnd); break; case GDK_MAP: - // fall-through + ctx->process_map(); + break; case GDK_UNMAP: case GDK_CLIENT_EVENT: case GDK_VISIBILITY_NOTIFY: diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassCommonDialogs.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassCommonDialogs.cpp index 99de6170c82..f68e18e1e4e 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassCommonDialogs.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassCommonDialogs.cpp @@ -65,12 +65,21 @@ static void jstring_to_utf_release(JNIEnv *env, jstring jstr, } } -static GtkWindow *gdk_window_handle_to_gtk(jlong handle) { +static GdkWindow *get_gdk_window(jlong handle) { return (handle != 0) - ? ((WindowContext*)JLONG_TO_PTR(handle))->get_gtk_window() + ? ((WindowContext*)JLONG_TO_PTR(handle))->get_gdk_window() : NULL; } +static void on_dialog_realize_set_parent(GtkWidget *dialog, gpointer user_data) { + GdkWindow *parent_gdk_window = (GdkWindow *) user_data; + GdkWindow *dialog_gdk_window = gtk_widget_get_window(dialog); + + if (dialog_gdk_window && parent_gdk_window) { + gdk_window_set_transient_for(dialog_gdk_window, parent_gdk_window); + } +} + static jobject create_empty_result() { jclass jFileChooserResult = (jclass) mainEnv->FindClass("com/sun/glass/ui/CommonDialogs$FileChooserResult"); if (EXCEPTION_OCCURED(mainEnv)) return NULL; @@ -112,7 +121,7 @@ JNIEXPORT jobject JNICALL Java_com_sun_glass_ui_gtk_GtkCommonDialogs__1showFileC return create_empty_result(); } - GtkWidget* chooser = gtk_file_chooser_dialog_new(chooser_title, gdk_window_handle_to_gtk(parent), + GtkWidget* chooser = gtk_file_chooser_dialog_new(chooser_title, NULL, static_cast(chooser_type), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, @@ -120,6 +129,8 @@ JNIEXPORT jobject JNICALL Java_com_sun_glass_ui_gtk_GtkCommonDialogs__1showFileC GTK_RESPONSE_ACCEPT, NULL); + g_signal_connect(chooser, "realize", G_CALLBACK(on_dialog_realize_set_parent), get_gdk_window(parent)); + if (chooser_type == GTK_FILE_CHOOSER_ACTION_SAVE) { gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(chooser), chooser_filename); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER (chooser), TRUE); @@ -208,7 +219,7 @@ JNIEXPORT jstring JNICALL Java_com_sun_glass_ui_gtk_GtkCommonDialogs__1showFolde GtkWidget* chooser = gtk_file_chooser_dialog_new( chooser_title, - gdk_window_handle_to_gtk(parent), + NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, @@ -216,6 +227,8 @@ JNIEXPORT jstring JNICALL Java_com_sun_glass_ui_gtk_GtkCommonDialogs__1showFolde GTK_RESPONSE_ACCEPT, NULL); + g_signal_connect(chooser, "realize", G_CALLBACK(on_dialog_realize_set_parent), get_gdk_window(parent)); + if (chooser_folder != NULL) { gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(chooser), chooser_folder); diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassRobot.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassRobot.cpp index 625f6a2263c..5395c984bac 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassRobot.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassRobot.cpp @@ -273,7 +273,7 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkRobot__1getScreenCapture GdkPixbuf *screenshot, *tmp; GdkWindow *root_window = gdk_get_default_root_window(); - tmp = glass_pixbuf_from_window(root_window, x, y, width, height); + tmp = gdk_pixbuf_get_from_window(root_window, x, y, width, height); if (!tmp) { return; } diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassView.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassView.cpp index a9538aaec5e..9cf3f88b8df 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassView.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassView.cpp @@ -102,7 +102,7 @@ JNIEXPORT jint JNICALL Java_com_sun_glass_ui_gtk_GtkView__1getX GlassView* view = JLONG_TO_GLASSVIEW(ptr); if (view && view->current_window) { - return view->current_window->get_geometry().view_x; + return view->current_window->get_view_position().x; } return 0; } @@ -120,7 +120,7 @@ JNIEXPORT jint JNICALL Java_com_sun_glass_ui_gtk_GtkView__1getY GlassView* view = JLONG_TO_GLASSVIEW(ptr); if (view && view->current_window) { - return view->current_window->get_geometry().view_y; + return view->current_window->get_view_position().y; } return 0; } @@ -269,14 +269,15 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkView__1uploadPixelsByteArray } } + /* * Class: com_sun_glass_ui_gtk_GtkView * Method: _enterFullscreen * Signature: (JZZZ)Z */ JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkView__1enterFullscreen - (JNIEnv * env, jobject obj, jlong ptr, jboolean animate, jboolean keepRation, jboolean hideCursor) -{ + (JNIEnv * env, jobject obj, jlong ptr, jboolean animate, jboolean keepRation, jboolean hideCursor) { + (void)animate; (void)keepRation; (void)hideCursor; @@ -284,9 +285,8 @@ JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkView__1enterFullscreen GlassView* view = JLONG_TO_GLASSVIEW(ptr); if (view->current_window) { view->current_window->enter_fullscreen(); - env->CallVoidMethod(obj, jViewNotifyView, com_sun_glass_events_ViewEvent_FULLSCREEN_ENTER); - CHECK_JNI_EXCEPTION_RET(env, JNI_FALSE) } + return JNI_TRUE; } @@ -307,10 +307,7 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkView__1exitFullscreen } else { view->current_window->exit_fullscreen(); } - env->CallVoidMethod(obj, jViewNotifyView, com_sun_glass_events_ViewEvent_FULLSCREEN_EXIT); - CHECK_JNI_EXCEPTION(env) } - } } // extern "C" diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp index 16f033357ed..e29e564baa7 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp @@ -87,13 +87,13 @@ JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1createWindow WindowContext* parent = JLONG_TO_WINDOW_CTX(owner); - WindowContext* ctx = new WindowContextTop(obj, - parent, - screen, - glass_mask_to_window_frame_type(mask), - glass_mask_to_window_type(mask), - glass_mask_to_wm_function(mask) - ); + WindowType type = glass_mask_to_window_type(mask); + WindowFrameType frameType = glass_mask_to_window_frame_type(mask); + GdkWMFunction wmFunctions = glass_mask_to_wm_function(mask); + + WindowContext* ctx = (frameType == EXTENDED) + ? new WindowContextExtended(obj, parent, screen, wmFunctions) + : new WindowContext(obj, parent, screen, frameType, type, wmFunctions); return PTR_TO_JLONG(ctx); } @@ -408,8 +408,6 @@ JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1setMaximumSize WindowContext* ctx = JLONG_TO_WINDOW_CTX(ptr); if (w == 0 || h == 0) return JNI_FALSE; - if (w == -1) w = G_MAXSHORT; - if (h == -1) h = G_MAXSHORT; ctx->set_maximum_size(w, h); return JNI_TRUE; @@ -581,13 +579,7 @@ JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1getNativeWindowImp (void)obj; WindowContext* ctx = JLONG_TO_WINDOW_CTX(ptr); - GdkWindow *win = ctx->get_gdk_window(); - - if (win == NULL) { - return 0; - } - - return GDK_WINDOW_XID(win); + return ctx->get_native_window(); } } // extern "C" diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp index ea08f79bdb6..aba8a796e0e 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp @@ -26,6 +26,7 @@ #include #include +#include char const * const GDK_WINDOW_DATA_CONTEXT = "glass_window_context"; @@ -573,7 +574,6 @@ typedef struct _DeviceGrabContext { gboolean disableGrab = FALSE; static gboolean configure_transparent_window(GtkWidget *window); -static void configure_opaque_window(GtkWidget *window); gint glass_gdk_visual_get_depth (GdkVisual * visual) { @@ -581,16 +581,6 @@ gint glass_gdk_visual_get_depth (GdkVisual * visual) return gdk_visual_get_depth(visual); } -GdkScreen * glass_gdk_window_get_screen(GdkWindow * gdkWindow) -{ -#ifdef GLASS_GTK3 - GdkVisual * gdkVisual = gdk_window_get_visual(gdkWindow); - return gdk_visual_get_screen(gdkVisual); -#else - return gdk_window_get_screen(gdkWindow); -#endif -} - gboolean glass_gdk_mouse_devices_grab(GdkWindow *gdkWindow) { return glass_gdk_mouse_devices_grab_with_cursor(gdkWindow, NULL, TRUE); @@ -624,56 +614,25 @@ glass_gdk_mouse_devices_ungrab() { void glass_gdk_master_pointer_get_position(gint *x, gint *y) { -#ifdef GLASS_GTK3 gdk_device_get_position(gdk_device_manager_get_client_pointer( gdk_display_get_device_manager( gdk_display_get_default())), NULL, x, y); -#else - gdk_display_get_pointer(gdk_display_get_default(), NULL, x, y, NULL); -#endif } gboolean glass_gdk_device_is_grabbed(GdkDevice *device) { -#ifdef GLASS_GTK3 - return gdk_display_device_is_grabbed(gdk_display_get_default(), device); -#else - (void) device; - return gdk_display_pointer_is_grabbed(gdk_display_get_default()); -#endif + return gdk_display_device_is_grabbed(gdk_display_get_default(), device); } void glass_gdk_device_ungrab(GdkDevice *device) { -#ifdef GLASS_GTK3 - gdk_device_ungrab(device, GDK_CURRENT_TIME); -#else - (void) device; - gdk_pointer_ungrab(GDK_CURRENT_TIME); -#endif + gdk_device_ungrab(device, GDK_CURRENT_TIME); } GdkWindow * glass_gdk_device_get_window_at_position(GdkDevice *device, gint *x, gint *y) { -#ifdef GLASS_GTK3 - return gdk_device_get_window_at_position(device, x, y); -#else - (void) device; - return gdk_display_get_window_at_pointer(gdk_display_get_default(), x, y); -#endif -} - -void -glass_gtk_configure_transparency_and_realize(GtkWidget *window, - gboolean transparent) { - gboolean isTransparent = glass_configure_window_transparency(window, transparent); - gtk_widget_realize(window); -} - -void -glass_gtk_window_configure_from_visual(GtkWidget *widget, GdkVisual *visual) { - glass_widget_set_visual(widget, visual); + return gdk_device_get_window_at_position(device, x, y); } static gboolean @@ -681,42 +640,22 @@ configure_transparent_window(GtkWidget *window) { GdkScreen *default_screen = gdk_screen_get_default(); GdkDisplay *default_display = gdk_display_get_default(); -#ifdef GLASS_GTK3 - GdkVisual *visual = gdk_screen_get_rgba_visual(default_screen); - if (visual - && gdk_display_supports_composite(default_display) - && gdk_screen_is_composited(default_screen)) { - glass_widget_set_visual(window, visual); - return TRUE; - } -#else - GdkColormap *colormap = gdk_screen_get_rgba_colormap(default_screen); - if (colormap - && gdk_display_supports_composite(default_display) - && gdk_screen_is_composited(default_screen)) { - gtk_widget_set_colormap(window, colormap); - return TRUE; - } -#endif + GdkVisual *visual = gdk_screen_get_rgba_visual(default_screen); + if (visual + && gdk_display_supports_composite(default_display) + && gdk_screen_is_composited(default_screen)) { + glass_widget_set_visual(window, visual); + return TRUE; + } return FALSE; } -void -glass_gdk_window_get_size(GdkWindow *window, gint *w, gint *h) { - *w = gdk_window_get_width(window); - *h = gdk_window_get_height(window); -} - void glass_gdk_display_get_pointer(GdkDisplay* display, gint* x, gint *y) { -#ifdef GLASS_GTK3 - gdk_device_get_position( - gdk_device_manager_get_client_pointer( - gdk_display_get_device_manager(display)), NULL , x, y); -#else - gdk_display_get_pointer(display, NULL, x, y, NULL); -#endif + gdk_device_get_position( + gdk_device_manager_get_client_pointer( + gdk_display_get_device_manager(display)), NULL , x, y); } @@ -732,19 +671,6 @@ glass_gtk_selection_data_get_data_with_length( return gtk_selection_data_get_data(selectionData); } -static void -configure_opaque_window(GtkWidget *window) { - (void) window; -/* We need to pick a visual that really is glx compatible - * instead of using the default visual - */ - /* see: JDK-8087516 for why this is commented out - glass_widget_set_visual(window, - gdk_screen_get_system_visual( - gdk_screen_get_default())); - */ -} - gboolean glass_configure_window_transparency(GtkWidget *window, gboolean transparent) { if (transparent) { @@ -752,13 +678,10 @@ glass_configure_window_transparency(GtkWidget *window, gboolean transparent) { return TRUE; } - fprintf(stderr,"Can't create transparent stage, because your screen doesn't" - " support alpha channel." - " You need to enable XComposite extension.\n"); + fprintf(stderr, ALPHA_CHANNEL_ERROR_MSG); fflush(stderr); } - configure_opaque_window(window); return FALSE; } @@ -769,56 +692,10 @@ glass_pixbuf_from_window(GdkWindow *window, { GdkPixbuf * ret = NULL; -#ifdef GLASS_GTK3 - ret = gdk_pixbuf_get_from_window (window, srcx, srcy, width, height); -#else - ret = gdk_pixbuf_get_from_drawable (NULL, - window, - NULL, - srcx, srcy, - 0, 0, - width, height); -#endif + gdk_pixbuf_get_from_window (window, srcx, srcy, width, height); return ret; } -void -glass_window_apply_shape_mask(GdkWindow *window, - void* data, uint width, uint height) -{ -#ifdef GLASS_GTK3 - (void) window; - (void) data; - (void) width; - (void) height; -#else - GdkPixbuf* pixbuf = gdk_pixbuf_new_from_data((guchar *) data, - GDK_COLORSPACE_RGB, TRUE, 8, width, height, width * 4, NULL, NULL); - - if (GDK_IS_PIXBUF(pixbuf)) { - GdkBitmap* mask = NULL; - gdk_pixbuf_render_pixmap_and_mask(pixbuf, NULL, &mask, 128); - - gdk_window_input_shape_combine_mask(window, mask, 0, 0); - - g_object_unref(pixbuf); - if (mask) { - g_object_unref(mask); - } - } -#endif -} - -void -glass_window_reset_input_shape_mask(GdkWindow *window) -{ -#ifdef GLASS_GTK3 - gdk_window_input_shape_combine_region(window, NULL, 0, 0); -#else - gdk_window_input_shape_combine_mask(window, NULL, 0, 0); -#endif -} - GdkWindow * glass_gdk_drag_context_get_dest_window (GdkDragContext * context) { @@ -829,15 +706,10 @@ glass_gdk_drag_context_get_dest_window (GdkDragContext * context) void glass_gdk_x11_display_set_window_scale (GdkDisplay *display, gint scale) { -#ifdef GLASS_GTK3 // Optional call, if it does not exist then GTK3 is not yet // doing automatic scaling of coordinates so we do not need // to override it. wrapped_gdk_x11_display_set_window_scale(display, scale); -#else - (void) display; - (void) scale; -#endif } //-------- Glass utility ---------------------------------------- @@ -845,12 +717,7 @@ void glass_gdk_x11_display_set_window_scale (GdkDisplay *display, void glass_widget_set_visual(GtkWidget *widget, GdkVisual *visual) { -#ifdef GLASS_GTK3 gtk_widget_set_visual (widget, visual); -#else - GdkColormap *colormap = gdk_colormap_new(visual, TRUE); - gtk_widget_set_colormap (widget, colormap); -#endif } guint glass_settings_get_guint_opt (const gchar *schema_name, diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h index 3c4e3cb57f0..c30d5caac86 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h @@ -28,11 +28,8 @@ #include #include -#include #include -#include #include - #include "wrapped.h" #define GLASS_GTK3 @@ -44,6 +41,10 @@ #define GDK_FILTERED_EVENTS_MASK static_cast(GDK_ALL_EVENTS_MASK \ & ~GDK_TOUCH_MASK) +#define ALPHA_CHANNEL_ERROR_MSG \ + "Can't create transparent stage, because your screen doesn't support alpha channel. " \ + "You need to enable XComposite extension.\n" + #define JLONG_TO_PTR(value) ((void*)(intptr_t)(value)) #define PTR_TO_JLONG(value) ((jlong)(intptr_t)(value)) @@ -236,6 +237,7 @@ struct jni_exception: public std::exception { extern jmethodID jColorRgb; // javafx.scene.paint.Color#rgb(IIID)Ljavafx/scene/paint/Color; #ifdef VERBOSE +#define LOG(msg, ...) { printf(msg, ##__VA_ARGS__); fflush(stdout); } #define LOG0(msg) {printf(msg);fflush(stdout);} #define LOG1(msg, param) {printf(msg, param);fflush(stdout);} #define LOG2(msg, param1, param2) {printf(msg, param1, param2);fflush(stdout);} @@ -251,6 +253,7 @@ struct jni_exception: public std::exception { #define ERROR3(msg, param1, param2, param3) {fprintf(stderr, msg, param1, param2, param3);fflush(stderr);} #define ERROR4(msg, param1, param2, param3, param4) {fprintf(stderr, msg, param1, param2, param3, param4);fflush(stderr);} #else +#define LOG(msg, ...) #define LOG0(msg) #define LOG1(msg, param) #define LOG2(msg, param1, param2) @@ -269,22 +272,22 @@ struct jni_exception: public std::exception { #define LOG_EXCEPTION(env) check_and_clear_exception(env); - gchar* get_application_name(); - void glass_throw_exception(JNIEnv * env, - const char * exceptionClass, - const char * exceptionMessage); - int glass_throw_oom(JNIEnv * env, const char * exceptionMessage); - void dump_jstring_array(JNIEnv*, jobjectArray); +gchar* get_application_name(); +void glass_throw_exception(JNIEnv * env, + const char * exceptionClass, + const char * exceptionMessage); +int glass_throw_oom(JNIEnv * env, const char * exceptionMessage); +void dump_jstring_array(JNIEnv*, jobjectArray); - guint8* convert_BGRA_to_RGBA(const int* pixels, int stride, int height); +guint8* convert_BGRA_to_RGBA(const int* pixels, int stride, int height); - gboolean check_and_clear_exception(JNIEnv *env); +gboolean check_and_clear_exception(JNIEnv *env); - jboolean is_display_valid(); +jboolean is_display_valid(); - gsize get_files_count(gchar **uris); +gsize get_files_count(gchar **uris); - jobject uris_to_java(JNIEnv *env, gchar **uris, gboolean files); +jobject uris_to_java(JNIEnv *env, gchar **uris, gboolean files); #ifdef __cplusplus @@ -299,9 +302,6 @@ glass_widget_set_visual (GtkWidget *widget, GdkVisual *visual); gint glass_gdk_visual_get_depth (GdkVisual * visual); -GdkScreen * -glass_gdk_window_get_screen(GdkWindow * gdkWindow); - gboolean glass_gdk_mouse_devices_grab(GdkWindow * gdkWindow); @@ -311,12 +311,6 @@ glass_gdk_mouse_devices_grab_with_cursor(GdkWindow * gdkWindow, GdkCursor *curso void glass_gdk_mouse_devices_ungrab(); -void -glass_gdk_master_pointer_grab(GdkEvent *event, GdkWindow *window, GdkCursor *cursor); - -void -glass_gdk_master_pointer_ungrab(GdkEvent *event); - void glass_gdk_master_pointer_get_position(gint *x, gint *y); @@ -330,21 +324,12 @@ GdkWindow * glass_gdk_device_get_window_at_position( GdkDevice *device, gint *x, gint *y); -void -glass_gtk_configure_transparency_and_realize(GtkWidget *window, - gboolean transparent); const guchar * glass_gtk_selection_data_get_data_with_length( GtkSelectionData * selectionData, gint * length); -void -glass_gtk_window_configure_from_visual(GtkWidget *widget, GdkVisual *visual); - -void -glass_gdk_window_get_size(GdkWindow *window, gint *w, gint *h); - void glass_gdk_display_get_pointer(GdkDisplay* display, gint* x, gint *y); @@ -363,9 +348,6 @@ void glass_window_apply_shape_mask(GdkWindow *window, void* data, uint width, uint height); -void -glass_window_reset_input_shape_mask(GdkWindow *window); - GdkWindow * glass_gdk_drag_context_get_dest_window (GdkDragContext * context); diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_key.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_key.cpp index a6f1e54b904..3cfaf538d2a 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_key.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_key.cpp @@ -29,7 +29,9 @@ #include #include "glass_general.h" #include +#include #include +#include #include diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp index 8416bb5cd14..bb110876e75 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp @@ -27,26 +27,21 @@ #include "glass_key.h" #include "glass_screen.h" #include "glass_dnd.h" +#include "glass_evloop.h" #include #include #include #include - #include -#include #include -#include -#include #include -#ifdef GLASS_GTK3 -#include -#endif +#include #include - #include +#include #define MOUSE_BACK_BTN 8 #define MOUSE_FORWARD_BTN 9 @@ -54,90 +49,254 @@ // Resize border width of EXTENDED windows #define RESIZE_BORDER_WIDTH 5 -WindowContext * WindowContextBase::sm_grab_window = NULL; -WindowContext * WindowContextBase::sm_mouse_drag_window = NULL; -GdkWindow* WindowContextBase::get_gdk_window(){ - return gdk_window; +void destroy_and_delete_ctx(WindowContext* ctx) { + LOG("destroy_and_delete_ctx\n"); + if (ctx) { + ctx->process_destroy(); + + if (!ctx->get_events_count()) { + LOG("delete ctx\n"); + delete ctx; + } + // else: ctx will be deleted in EventsCounterHelper after completing + // an event processing + } } -jobject WindowContextBase::get_jview() { - return jview; +static bool gdk_visual_is_rgba(GdkVisual *visual) { + if (!visual) return false; + + int depth = gdk_visual_get_depth(visual); + guint32 red_mask, green_mask, blue_mask; + gdk_visual_get_red_pixel_details(visual, &red_mask, nullptr, nullptr); + gdk_visual_get_green_pixel_details(visual, &green_mask, nullptr, nullptr); + gdk_visual_get_blue_pixel_details(visual, &blue_mask, nullptr, nullptr); + + return (depth == 32 + && red_mask == 0xff0000 + && green_mask == 0x00ff00 + && blue_mask == 0x0000ff); } -jobject WindowContextBase::get_jwindow() { - return jwindow; +// Iconified not considered here +static bool is_state_floating(GdkWindowState state) { + return (state & (GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_FULLSCREEN)) == 0; } -bool WindowContextBase::isEnabled() { - if (jwindow) { - bool result = (JNI_TRUE == mainEnv->CallBooleanMethod(jwindow, jWindowIsEnabled)); - LOG_EXCEPTION(mainEnv) - return result; - } else { - return false; +static inline jint gdk_button_number_to_mouse_button(guint button) { + switch (button) { + case 1: + return com_sun_glass_events_MouseEvent_BUTTON_LEFT; + case 2: + return com_sun_glass_events_MouseEvent_BUTTON_OTHER; + case 3: + return com_sun_glass_events_MouseEvent_BUTTON_RIGHT; + case MOUSE_BACK_BTN: + return com_sun_glass_events_MouseEvent_BUTTON_BACK; + case MOUSE_FORWARD_BTN: + return com_sun_glass_events_MouseEvent_BUTTON_FORWARD; + default: + // Other buttons are not supported by quantum and are not reported by other platforms + return com_sun_glass_events_MouseEvent_BUTTON_NONE; } } -void WindowContextBase::notify_state(jint glass_state) { - if (glass_state == com_sun_glass_events_WindowEvent_RESTORE) { - if (is_maximized) { - glass_state = com_sun_glass_events_WindowEvent_MAXIMIZE; - } +WindowContext * WindowContext::sm_grab_window = nullptr; +WindowContext * WindowContext::sm_mouse_drag_window = nullptr; - int w, h; - glass_gdk_window_get_size(gdk_window, &w, &h); - if (jview) { - mainEnv->CallVoidMethod(jview, - jViewNotifyRepaint, - 0, 0, w, h); - CHECK_JNI_EXCEPTION(mainEnv); - } +// Work-around because frame extents are only obtained after window is shown. +// This is used to know the total window size (content + decoration) +// The first window will have a duplicated resize event, subsequent windows will use the cached value. +std::optional WindowContext::normal_extents; +std::optional WindowContext::utility_extents; + +WindowContext::WindowContext(jobject _jwindow, WindowContext* _owner, long _screen, + WindowFrameType _frame_type, WindowType type, GdkWMFunction wmf) : + owner(_owner), + screen(_screen), + frame_type(_frame_type), + window_type(type), + initial_wmf(wmf), + current_wmf(wmf) { + jwindow = mainEnv->NewGlobalRef(_jwindow); + + if (frame_type != TITLED) { + initial_wmf = GDK_FUNC_ALL; } - if (jwindow) { - mainEnv->CallVoidMethod(jwindow, - jGtkWindowNotifyStateChanged, - glass_state); - CHECK_JNI_EXCEPTION(mainEnv); + int attr_mask = GDK_WA_VISUAL; + GdkWindowAttr attributes; + attributes.visual = find_best_visual(); + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.event_mask = GDK_FILTERED_EVENTS_MASK; + attributes.width = DEFAULT_WIDTH; + attributes.height = DEFAULT_HEIGHT; + attributes.window_type = (window_type == POPUP) ? GDK_WINDOW_TEMP : GDK_WINDOW_TOPLEVEL; + + if (gchar* app_name = get_application_name()) { + attributes.wmclass_name = app_name; + attributes.wmclass_class = app_name; + attr_mask |= GDK_WA_WMCLASS; + } + + if (window_type == UTILITY && frame_type != EXTENDED) { + attributes.type_hint = GDK_WINDOW_TYPE_HINT_UTILITY; + attr_mask |= GDK_WA_TYPE_HINT; } -} -void WindowContextBase::process_state(GdkEventWindowState* event) { - if (event->changed_mask & (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_MAXIMIZED)) { + gdk_window = gdk_window_new(gdk_get_default_root_window(), &attributes, attr_mask); - if (event->changed_mask & GDK_WINDOW_STATE_ICONIFIED) { - is_iconified = event->new_window_state & GDK_WINDOW_STATE_ICONIFIED; - } + if (frame_type == TITLED) { + request_frame_extents(); + } - if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED) { - is_maximized = event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED; + if (frame_type != TRANSPARENT) { + GdkRGBA white = { 1.0, 1.0, 1.0, 1.0 }; + gdk_window_set_background_rgba(gdk_window, &white); + } + + g_object_set_data_full(G_OBJECT(gdk_window), GDK_WINDOW_DATA_CONTEXT, this, nullptr); + gdk_window_register_dnd(gdk_window); + + if (initial_wmf) { + gdk_window_set_functions(gdk_window, initial_wmf); + } + + if (frame_type != TITLED) { + gdk_window_set_decorations(gdk_window, (GdkWMDecoration) 0); + } + + if (owner) { + owner->add_child(this); + if (on_top_inherited()) { + gdk_window_set_keep_above(gdk_window, true); } + } - jint stateChangeEvent; + set_title(""); + update_window_constraints(); - if (is_iconified) { - stateChangeEvent = com_sun_glass_events_WindowEvent_MINIMIZE; - } else if (is_maximized) { - stateChangeEvent = com_sun_glass_events_WindowEvent_MAXIMIZE; + window_location.setOnChange([this](const Point& point) { + notify_window_move(); + }); + + view_position.setOnChange([this](const Point& point) { + notify_view_move(); + }); + + window_size.setOnChange([this](const Size& size) { + notify_window_resize(is_maximized() + ? com_sun_glass_events_WindowEvent_MAXIMIZE + : com_sun_glass_events_WindowEvent_RESIZE); + }); + + view_size.setOnChange([this](const Size& size) { + notify_view_resize(); + update_window_constraints(); + }); + + window_extents.setOnChange([this](const Rectangle& rect) { + update_window_constraints(); + update_window_size(); + }); + + resizable.setOnChange([this](const bool& resizable) { + update_window_constraints(); + }); + + minimum_size.setOnChange([this](const Size& size) { + update_window_constraints(); + }); + + sys_min_size.setOnChange([this](const Size& size) { + update_window_constraints(); + }); + + maximum_size.setOnChange([this](const Size& size) { + update_window_constraints(); + }); + + load_cached_extents(); +} + +GdkVisual* WindowContext::find_best_visual() { + // This comes from prism-es2 + static glong xvisualID = (glong)mainEnv->GetStaticLongField(jApplicationCls, jApplicationVisualID); + static GdkVisual *prismVisual = (xvisualID != 0) + ? gdk_x11_screen_lookup_visual(gdk_screen_get_default(), xvisualID) + : nullptr; + + if (frame_type == TRANSPARENT && !gdk_visual_is_rgba(prismVisual)) { + GdkVisual *rgbaVisual = gdk_screen_get_rgba_visual(gdk_screen_get_default()); + if (rgbaVisual) { + return rgbaVisual; } else { - stateChangeEvent = com_sun_glass_events_WindowEvent_RESTORE; - if ((gdk_windowManagerFunctions & GDK_FUNC_MINIMIZE) == 0 - || (gdk_windowManagerFunctions & GDK_FUNC_MAXIMIZE) == 0) { - // in this case - the window manager will not support the programatic - // request to iconify / maximize - so we need to restore it now. - gdk_window_set_functions(gdk_window, gdk_windowManagerFunctions); - } + fprintf(stderr, ALPHA_CHANNEL_ERROR_MSG); + fflush(stderr); } + } - notify_state(stateChangeEvent); - } else if (event->changed_mask & GDK_WINDOW_STATE_ABOVE) { - notify_on_top(event->new_window_state & GDK_WINDOW_STATE_ABOVE); + if (prismVisual != nullptr) { + LOG("Using prism visual\n"); + return prismVisual; + } + + LOG("Using GDK system visual\n"); + return gdk_screen_get_system_visual(gdk_screen_get_default()); +} + +GdkWindow* WindowContext::get_gdk_window() { + if (GDK_IS_WINDOW(gdk_window)) { + return gdk_window; + } + + return nullptr; +} + +// Returns de XWindow ID to be used in rendering +XID WindowContext::get_native_window() { + // This is used to delay the window map (it's only really mapped when there's + // something rendered) + if (!is_visible()) return 0; + + return GDK_WINDOW_XID(gdk_window); +} + +bool WindowContext::isEnabled() { + if (jwindow) { + bool result = (JNI_TRUE == mainEnv->CallBooleanMethod(jwindow, jWindowIsEnabled)); + LOG_EXCEPTION(mainEnv) + return result; + } else { + return false; } } -void WindowContextBase::process_focus(GdkEventFocus* event) { - if (!event->in && WindowContextBase::sm_grab_window == this) { +void WindowContext::process_expose(GdkEventExpose* event) { + GdkRectangle r = event->area; + notify_repaint({ r.x, r.y, r.width, r.height }); +} + +void WindowContext::process_map() { + // We need only first map + if (mapped || window_type == POPUP) return; + + LOG("--------------------------------------------------------> mapped\n"); + Point loc = window_location.get(); + Size size = view_size.get(); + + move_resize(loc.x, loc.y, true, true, size.width, size.height); + mapped = true; + + if (initial_state_mask != 0) { + update_initial_state(); + } +} + +void WindowContext::process_focus(GdkEventFocus *event) { + LOG("process_focus (keyboard): %d\n", event->in); + if (!event->in && WindowContext::sm_grab_window == this) { ungrab_focus(); } @@ -148,66 +307,61 @@ void WindowContextBase::process_focus(GdkEventFocus* event) { gtk_im_context_focus_out(im_ctx.ctx); } } +} +void WindowContext::process_focus(bool focus_in) { + LOG("process_focus (state): %d\n", focus_in); if (jwindow) { - if (!event->in || isEnabled()) { - mainEnv->CallVoidMethod(jwindow, jWindowNotifyFocus, - event->in ? com_sun_glass_events_WindowEvent_FOCUS_GAINED - : com_sun_glass_events_WindowEvent_FOCUS_LOST); - CHECK_JNI_EXCEPTION(mainEnv) - } else { + if (focus_in && !isEnabled()) { // when the user tries to activate a disabled window, send FOCUS_DISABLED + LOG("jWindowNotifyFocusDisabled"); mainEnv->CallVoidMethod(jwindow, jWindowNotifyFocusDisabled); CHECK_JNI_EXCEPTION(mainEnv) + } else { + LOG("%s\n", (focus_in) ? "com_sun_glass_events_WindowEvent_FOCUS_GAINED" + : "com_sun_glass_events_WindowEvent_FOCUS_LOST"); + + mainEnv->CallVoidMethod(jwindow, jWindowNotifyFocus, + focus_in ? com_sun_glass_events_WindowEvent_FOCUS_GAINED + : com_sun_glass_events_WindowEvent_FOCUS_LOST); + CHECK_JNI_EXCEPTION(mainEnv) } } } -void WindowContextBase::increment_events_counter() { +void WindowContext::increment_events_counter() { ++events_processing_cnt; } -void WindowContextBase::decrement_events_counter() { +void WindowContext::decrement_events_counter() { --events_processing_cnt; } -size_t WindowContextBase::get_events_count() { +size_t WindowContext::get_events_count() { return events_processing_cnt; } -bool WindowContextBase::is_dead() { +bool WindowContext::is_dead() { return can_be_deleted; } -void destroy_and_delete_ctx(WindowContext* ctx) { - if (ctx) { - ctx->process_destroy(); - - if (!ctx->get_events_count()) { - delete ctx; - } - // else: ctx will be deleted in EventsCounterHelper after completing - // an event processing +void WindowContext::process_destroy() { + LOG("process_destroy\n"); + if (owner) { + owner->remove_child(this); } -} -void WindowContextBase::process_destroy() { - if (WindowContextBase::sm_mouse_drag_window == this) { + if (WindowContext::sm_mouse_drag_window == this) { ungrab_mouse_drag_focus(); } - if (WindowContextBase::sm_grab_window == this) { + if (WindowContext::sm_grab_window == this) { ungrab_focus(); } - std::set::iterator it; + std::set::iterator it; for (it = children.begin(); it != children.end(); ++it) { - // FIX JDK-8226537: this method calls set_owner(NULL) which prevents - // WindowContextTop::process_destroy() to call remove_child() (because children - // is being iterated here) but also prevents gtk_window_set_transient_for from - // being called - this causes the crash on gnome. - gtk_window_set_transient_for((*it)->get_gtk_window(), NULL); - (*it)->set_owner(NULL); + (*it)->set_owner(nullptr); destroy_and_delete_ctx(*it); } children.clear(); @@ -219,50 +373,40 @@ void WindowContextBase::process_destroy() { if (jview) { mainEnv->DeleteGlobalRef(jview); - jview = NULL; + jview = nullptr; } if (jwindow) { mainEnv->DeleteGlobalRef(jwindow); - jwindow = NULL; + jwindow = nullptr; } can_be_deleted = true; } -void WindowContextBase::process_delete() { +void WindowContext::process_delete() { + LOG("process_delete\n"); if (jwindow && isEnabled()) { + LOG("jWindowNotifyClose\n"); mainEnv->CallVoidMethod(jwindow, jWindowNotifyClose); CHECK_JNI_EXCEPTION(mainEnv) } } -void WindowContextBase::process_expose(GdkEventExpose* event) { - if (jview) { - mainEnv->CallVoidMethod(jview, jViewNotifyRepaint, event->area.x, event->area.y, event->area.width, event->area.height); - CHECK_JNI_EXCEPTION(mainEnv) - } +void WindowContext::notify_repaint() { + Size size = view_size.get(); + notify_repaint({ 0, 0, size.width, size.height }); } -static inline jint gtk_button_number_to_mouse_button(guint button) { - switch (button) { - case 1: - return com_sun_glass_events_MouseEvent_BUTTON_LEFT; - case 2: - return com_sun_glass_events_MouseEvent_BUTTON_OTHER; - case 3: - return com_sun_glass_events_MouseEvent_BUTTON_RIGHT; - case MOUSE_BACK_BTN: - return com_sun_glass_events_MouseEvent_BUTTON_BACK; - case MOUSE_FORWARD_BTN: - return com_sun_glass_events_MouseEvent_BUTTON_FORWARD; - default: - // Other buttons are not supported by quantum and are not reported by other platforms - return com_sun_glass_events_MouseEvent_BUTTON_NONE; +void WindowContext::notify_repaint(Rectangle rect) { + if (jview) { + mainEnv->CallVoidMethod(jview, jViewNotifyRepaint, rect.x, rect.y, rect.width, rect.height); + CHECK_JNI_EXCEPTION(mainEnv) } } -void WindowContextBase::process_mouse_button(GdkEventButton* event, bool synthesized) { +void WindowContext::process_mouse_button(GdkEventButton* event, bool synthesized) { + LOG("process_mouse_button\n"); // We only handle single press/release events here. if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) { return; @@ -303,8 +447,8 @@ void WindowContextBase::process_mouse_button(GdkEventButton* event, bool synthes GdkDevice* device = event->device; if (glass_gdk_device_is_grabbed(device) - && (glass_gdk_device_get_window_at_position(device, NULL, NULL) - == NULL)) { + && (glass_gdk_device_get_window_at_position(device, nullptr, nullptr) + == nullptr)) { ungrab_focus(); return; } @@ -322,7 +466,7 @@ void WindowContextBase::process_mouse_button(GdkEventButton* event, bool synthes } } - jint button = gtk_button_number_to_mouse_button(event->button); + jint button = gdk_button_number_to_mouse_button(event->button); if (jview && button != com_sun_glass_events_MouseEvent_BUTTON_NONE) { mainEnv->CallVoidMethod(jview, jViewNotifyMouse, @@ -345,7 +489,7 @@ void WindowContextBase::process_mouse_button(GdkEventButton* event, bool synthes } } -void WindowContextBase::process_mouse_motion(GdkEventMotion* event) { +void WindowContext::process_mouse_motion(GdkEventMotion *event) { jint glass_modifier = gdk_modifier_mask_to_glass(event->state); jint isDrag = glass_modifier & ( com_sun_glass_events_KeyEvent_MODIFIER_BUTTON_PRIMARY | @@ -355,7 +499,7 @@ void WindowContextBase::process_mouse_motion(GdkEventMotion* event) { com_sun_glass_events_KeyEvent_MODIFIER_BUTTON_FORWARD); jint button = com_sun_glass_events_MouseEvent_BUTTON_NONE; - if (isDrag && WindowContextBase::sm_mouse_drag_window == NULL) { + if (isDrag && WindowContext::sm_mouse_drag_window == nullptr) { // Upper layers expects from us Windows behavior: // all mouse events should be delivered to window where drag begins // and no exit/enter event should be reported during this drag. @@ -388,7 +532,7 @@ void WindowContextBase::process_mouse_motion(GdkEventMotion* event) { } } -void WindowContextBase::process_mouse_scroll(GdkEventScroll* event) { +void WindowContext::process_mouse_scroll(GdkEventScroll *event) { jdouble dx = 0; jdouble dy = 0; @@ -428,10 +572,9 @@ void WindowContextBase::process_mouse_scroll(GdkEventScroll* event) { (jdouble) 40.0, (jdouble) 40.0); CHECK_JNI_EXCEPTION(mainEnv) } - } -void WindowContextBase::process_mouse_cross(GdkEventCrossing* event) { +void WindowContext::process_mouse_cross(GdkEventCrossing *event) { bool enter = event->type == GDK_ENTER_NOTIFY; if (jview) { guint state = event->state; @@ -454,7 +597,7 @@ void WindowContextBase::process_mouse_cross(GdkEventCrossing* event) { } } -void WindowContextBase::process_key(GdkEventKey* event) { +void WindowContext::process_key(GdkEventKey *event) { bool press = event->type == GDK_KEY_PRESS; jint glassKey = get_glass_key(event); jint glassModifier = gdk_modifier_mask_to_glass(event->state); @@ -463,7 +606,7 @@ void WindowContextBase::process_key(GdkEventKey* event) { } else { glassModifier &= ~glass_key_to_modifier(glassKey); } - jcharArray jChars = NULL; + jcharArray jChars = nullptr; jchar key = gdk_keyval_to_unicode(event->keyval); if (key >= 'a' && key <= 'z' && (event->state & GDK_CONTROL_MASK)) { key = key - 'a' + 1; // map 'a' to ctrl-a, and so on. @@ -491,8 +634,9 @@ void WindowContextBase::process_key(GdkEventKey* event) { glassModifier); CHECK_JNI_EXCEPTION(mainEnv) + // TYPED events should only be sent for printable characters. // jview is checked again because previous call might be an exit key - if (press && key > 0 && jview) { // TYPED events should only be sent for printable characters. + if (press && key > 0 && jview) { mainEnv->CallVoidMethod(jview, jViewNotifyKey, com_sun_glass_events_KeyEvent_TYPED, com_sun_glass_events_KeyEvent_VK_UNDEFINED, @@ -502,12 +646,11 @@ void WindowContextBase::process_key(GdkEventKey* event) { } } -void WindowContextBase::paint(void* data, jint width, jint height) { -#ifdef GLASS_GTK3 +void WindowContext::paint(void* data, jint width, jint height) { cairo_rectangle_int_t rect = {0, 0, width, height}; cairo_region_t *region = cairo_region_create_rectangle(&rect); gdk_window_begin_paint_region(gdk_window, region); -#endif + cairo_t* context = gdk_cairo_create(gdk_window); cairo_surface_t* cairo_surface = @@ -516,60 +659,32 @@ void WindowContextBase::paint(void* data, jint width, jint height) { CAIRO_FORMAT_ARGB32, width, height, width * 4); - applyShapeMask(data, width, height); - cairo_set_source_surface(context, cairo_surface, 0, 0); cairo_set_operator(context, CAIRO_OPERATOR_SOURCE); cairo_paint(context); -#ifdef GLASS_GTK3 gdk_window_end_paint(gdk_window); cairo_region_destroy(region); -#endif cairo_destroy(context); cairo_surface_destroy(cairo_surface); } -void WindowContextBase::add_child(WindowContextTop* child) { +void WindowContext::add_child(WindowContext* child) { children.insert(child); - gtk_window_set_transient_for(child->get_gtk_window(), this->get_gtk_window()); + gdk_window_set_transient_for(child->get_gdk_window(), gdk_window); } -void WindowContextBase::remove_child(WindowContextTop* child) { +void WindowContext::remove_child(WindowContext* child) { children.erase(child); - gtk_window_set_transient_for(child->get_gtk_window(), NULL); -} - -void WindowContextBase::set_visible(bool visible) { - if (visible) { - gtk_widget_show(gtk_widget); - } else { - gtk_widget_hide(gtk_widget); - if (jview && is_mouse_entered) { - is_mouse_entered = false; - mainEnv->CallVoidMethod(jview, jViewNotifyMouse, - com_sun_glass_events_MouseEvent_EXIT, - com_sun_glass_events_MouseEvent_BUTTON_NONE, - 0, 0, - 0, 0, - 0, - JNI_FALSE, - JNI_FALSE); - CHECK_JNI_EXCEPTION(mainEnv) - } - } -} - -bool WindowContextBase::is_visible() { - return gtk_widget_get_visible(gtk_widget); } -bool WindowContextBase::is_resizable() { - return false; +bool WindowContext::is_visible() { + return gdk_window_is_visible(gdk_window); } -bool WindowContextBase::set_view(jobject view) { +bool WindowContext::set_view(jobject view) { + LOG("set_view\n"); if (jview) { mainEnv->CallVoidMethod(jview, jViewNotifyMouse, com_sun_glass_events_MouseEvent_EXIT, @@ -584,307 +699,230 @@ bool WindowContextBase::set_view(jobject view) { if (view) { jview = mainEnv->NewGlobalRef(view); + view_size.reset({-1, -1}); + view_position.reset({-1, -1}); } else { - jview = NULL; + jview = nullptr; } - return TRUE; + return true; } -bool WindowContextBase::grab_mouse_drag_focus() { +bool WindowContext::grab_mouse_drag_focus() { + LOG("grab_mouse_drag_focus\n"); if (glass_gdk_mouse_devices_grab_with_cursor( - gdk_window, gdk_window_get_cursor(gdk_window), FALSE)) { - WindowContextBase::sm_mouse_drag_window = this; + gdk_window, gdk_window_get_cursor(gdk_window), false)) { + WindowContext::sm_mouse_drag_window = this; return true; } else { return false; } } -void WindowContextBase::ungrab_mouse_drag_focus() { - WindowContextBase::sm_mouse_drag_window = NULL; +void WindowContext::ungrab_mouse_drag_focus() { + if (!WindowContext::sm_mouse_drag_window) { + return; + } + + LOG("ungrab_mouse_drag_focus\n"); + WindowContext::sm_mouse_drag_window = nullptr; glass_gdk_mouse_devices_ungrab(); - if (WindowContextBase::sm_grab_window) { - WindowContextBase::sm_grab_window->grab_focus(); + if (WindowContext::sm_grab_window) { + WindowContext::sm_grab_window->grab_focus(); } } -bool WindowContextBase::grab_focus() { - if (WindowContextBase::sm_mouse_drag_window +bool WindowContext::grab_focus() { + LOG("grab_focus\n"); + if (WindowContext::sm_mouse_drag_window || glass_gdk_mouse_devices_grab(gdk_window)) { - WindowContextBase::sm_grab_window = this; + WindowContext::sm_grab_window = this; return true; } else { return false; } } -void WindowContextBase::ungrab_focus() { - if (!WindowContextBase::sm_mouse_drag_window) { +void WindowContext::ungrab_focus() { + LOG("ungrab_focus\n"); + if (!WindowContext::sm_mouse_drag_window) { glass_gdk_mouse_devices_ungrab(); } - WindowContextBase::sm_grab_window = NULL; + + WindowContext::sm_grab_window = nullptr; if (jwindow) { + LOG("jWindowNotifyFocusUngrab\n"); mainEnv->CallVoidMethod(jwindow, jWindowNotifyFocusUngrab); CHECK_JNI_EXCEPTION(mainEnv) } } -void WindowContextBase::set_cursor(GdkCursor* cursor) { +void WindowContext::set_cursor(GdkCursor* cursor) { if (!is_in_drag()) { - if (WindowContextBase::sm_mouse_drag_window) { + if (WindowContext::sm_mouse_drag_window) { glass_gdk_mouse_devices_grab_with_cursor( - WindowContextBase::sm_mouse_drag_window->get_gdk_window(), cursor, FALSE); - } else if (WindowContextBase::sm_grab_window) { + WindowContext::sm_mouse_drag_window->get_gdk_window(), cursor, false); + } else if (WindowContext::sm_grab_window) { glass_gdk_mouse_devices_grab_with_cursor( - WindowContextBase::sm_grab_window->get_gdk_window(), cursor, TRUE); + WindowContext::sm_grab_window->get_gdk_window(), cursor, true); } } gdk_cursor = cursor; - if (gdk_cursor_override == NULL) { + if (gdk_cursor_override == nullptr) { gdk_window_set_cursor(gdk_window, cursor); } } -void WindowContextBase::set_cursor_override(GdkCursor* cursor) { +void WindowContext::set_cursor_override(GdkCursor* cursor) { if (gdk_cursor_override == cursor) { return; } gdk_cursor_override = cursor; - if (cursor != NULL) { + if (cursor != nullptr) { gdk_window_set_cursor(gdk_window, cursor); } else { gdk_window_set_cursor(gdk_window, gdk_cursor); } } -void WindowContextBase::set_background(float r, float g, float b) { - GdkRGBA rgba = {r, g, b, 1.}; - gtk_widget_override_background_color(gtk_widget, GTK_STATE_FLAG_NORMAL, &rgba); -} - -bool WindowContextBase::get_window_edge(int x, int y, GdkWindowEdge* window_edge) { - return false; +void WindowContext::set_background(float r, float g, float b) { + GdkRGBA rgba = {r, g, b, 1.0}; + gdk_window_set_background_rgba(gdk_window, &rgba); } -WindowContextBase::~WindowContextBase() { - disableIME(); - gtk_widget_destroy(gtk_widget); +GdkAtom WindowContext::get_net_frame_extents_atom() { + static GdkAtom atom = nullptr; + if (atom == nullptr) { + atom = gdk_atom_intern_static_string("_NET_FRAME_EXTENTS"); + } + return atom; } -////////////////////////////// WindowContextTop ///////////////////////////////// - +void WindowContext::request_frame_extents() { + Display *display = GDK_DISPLAY_XDISPLAY(gdk_window_get_display(gdk_window)); + static Atom rfeAtom = XInternAtom(display, "_NET_REQUEST_FRAME_EXTENTS", False); -// Work-around because frame extents are only obtained after window is shown. -// This is used to know the total window size (content + decoration) -// The first window will have a duplicated resize event, subsequent windows will use the cached value. -WindowFrameExtents WindowContextTop::normal_extents = {0, 0, 0, 0}; -WindowFrameExtents WindowContextTop::utility_extents = {0, 0, 0, 0}; + if (rfeAtom != None) { + XClientMessageEvent clientMessage; + memset(&clientMessage, 0, sizeof(clientMessage)); + clientMessage.type = ClientMessage; + clientMessage.window = GDK_WINDOW_XID(gdk_window); + clientMessage.message_type = rfeAtom; + clientMessage.format = 32; -static void event_realize(GtkWidget* self, gpointer user_data) { - WindowContextTop *ctx = ((WindowContextTop *) user_data); - ctx->process_realize(); + XSendEvent(display, XDefaultRootWindow(display), False, + SubstructureRedirectMask | SubstructureNotifyMask, + (XEvent *) &clientMessage); + XFlush(display); + } } -static int geometry_get_window_width(const WindowGeometry *windowGeometry) { - return (windowGeometry->final_width.type == BOUNDSTYPE_WINDOW) - ? windowGeometry->final_width.value - : windowGeometry->final_width.value - + windowGeometry->extents.left - + windowGeometry->extents.right; -} +void WindowContext::update_initial_state() { + GdkWindowState state = gdk_window_get_state(gdk_window); -static int geometry_get_window_height(const WindowGeometry *windowGeometry) { - return (windowGeometry->final_height.type == BOUNDSTYPE_WINDOW) - ? windowGeometry->final_height.value - : windowGeometry->final_height.value - + windowGeometry->extents.top - + windowGeometry->extents.bottom; -} + if (initial_state_mask & GDK_WINDOW_STATE_MAXIMIZED) { + LOG("update_initial_state: maximized\n"); + maximize(true); + } -static int geometry_get_content_width(WindowGeometry *windowGeometry) { - return (windowGeometry->final_width.type == BOUNDSTYPE_CONTENT) - ? windowGeometry->final_width.value - : windowGeometry->final_width.value - - windowGeometry->extents.left - - windowGeometry->extents.right; -} + if (initial_state_mask & GDK_WINDOW_STATE_FULLSCREEN) { + LOG("update_initial_state: fullscreen\n"); + enter_fullscreen(); + } -static int geometry_get_content_height(WindowGeometry *windowGeometry) { - return (windowGeometry->final_height.type == BOUNDSTYPE_CONTENT) - ? windowGeometry->final_height.value - : windowGeometry->final_height.value - - windowGeometry->extents.top - - windowGeometry->extents.bottom; -} + if (initial_state_mask & GDK_WINDOW_STATE_ICONIFIED) { + LOG("update_initial_state: iconify\n"); + iconify(true); + } -static GdkAtom get_net_frame_extents_atom() { - static const char * extents_str = "_NET_FRAME_EXTENTS"; - return gdk_atom_intern(extents_str, FALSE); + initial_state_mask = 0; } -WindowContextTop::WindowContextTop(jobject _jwindow, WindowContext* _owner, long _screen, - WindowFrameType _frame_type, WindowType type, GdkWMFunction wmf) : - WindowContextBase(), - screen(_screen), - frame_type(_frame_type), - window_type(type), - owner(_owner), - geometry(), - resizable(), - on_top(false), - is_fullscreen(false) { - jwindow = mainEnv->NewGlobalRef(_jwindow); - gdk_windowManagerFunctions = wmf; - - gtk_widget = gtk_window_new(type == POPUP ? GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL); - g_signal_connect(G_OBJECT(gtk_widget), "realize", G_CALLBACK(event_realize), this); - - if (gchar* app_name = get_application_name()) { - gtk_window_set_wmclass(GTK_WINDOW(gtk_widget), app_name, app_name); - g_free(app_name); - } +void WindowContext::update_frame_extents() { + if (frame_type != TITLED) return; - if (owner) { - owner->add_child(this); - if (on_top_inherited()) { - gtk_window_set_keep_above(GTK_WINDOW(gtk_widget), TRUE); - } - } + int top, left, bottom, right; - if (type == UTILITY && frame_type != EXTENDED) { - gtk_window_set_type_hint(GTK_WINDOW(gtk_widget), GDK_WINDOW_TYPE_HINT_UTILITY); - } + if (get_frame_extents_property(&top, &left, &bottom, &right)) { + if (top > 0 || right > 0 || bottom > 0 || left > 0) { + Rectangle old_extents = window_extents.get(); + Rectangle new_extents = { left, top, (left + right), (top + bottom) }; + bool changed = old_extents != new_extents; - const char* wm_name = gdk_x11_screen_get_window_manager_name(gdk_screen_get_default()); - wmanager = (g_strcmp0("Compiz", wm_name) == 0) ? COMPIZ : UNKNOWN; + LOG("------------------------------------------- frame extents - changed: %d\n", changed); -// glong xdisplay = (glong)mainEnv->GetStaticLongField(jApplicationCls, jApplicationDisplay); -// gint xscreenID = (gint)mainEnv->GetStaticIntField(jApplicationCls, jApplicationScreen); - glong xvisualID = (glong)mainEnv->GetStaticLongField(jApplicationCls, jApplicationVisualID); + if (!changed) return; - if (xvisualID != 0) { - GdkVisual *visual = gdk_x11_screen_lookup_visual(gdk_screen_get_default(), xvisualID); - glass_gtk_window_configure_from_visual(gtk_widget, visual); - } + set_cached_extents(new_extents); - gtk_widget_set_events(gtk_widget, GDK_FILTERED_EVENTS_MASK); - gtk_widget_set_app_paintable(gtk_widget, TRUE); + if (!is_floating()) { + // Delay for then window is restored + needs_to_update_frame_extents = true; + LOG("Frame extents will be updated on restore"); + return; + } - glass_configure_window_transparency(gtk_widget, frame_type == TRANSPARENT); - gtk_window_set_title(GTK_WINDOW(gtk_widget), ""); + Size size = view_size.get(); + int newW = size.width; + int newH = size.height; - if (frame_type != TITLED) { - gtk_window_set_decorated(GTK_WINDOW(gtk_widget), FALSE); - } else { - geometry.extents = get_cached_extents(); - } -} + // Here the user might change the desktop theme and in consequence + // change decoration sizes. + if (width_type == BOUNDSTYPE_WINDOW) { + // Re-add the extents and then subtract the new + newW = newW + old_extents.width - new_extents.width; + } -// Applied to a temporary full screen window to prevent sending events to Java -void WindowContextTop::detach_from_java() { - if (jview) { - mainEnv->DeleteGlobalRef(jview); - jview = NULL; - } - if (jwindow) { - mainEnv->DeleteGlobalRef(jwindow); - jwindow = NULL; - } -} + if (height_type == BOUNDSTYPE_WINDOW) { + // Re-add the extents and then subtract the new + newH = newH + old_extents.height - new_extents.height; + } -void WindowContextTop::request_frame_extents() { - Display *display = GDK_DISPLAY_XDISPLAY(gdk_window_get_display(gdk_window)); - static Atom rfeAtom = XInternAtom(display, "_NET_REQUEST_FRAME_EXTENTS", False); + newW = std::clamp(newW, 1, MAX_WINDOW_SIZE); + newH = std::clamp(newH, 1, MAX_WINDOW_SIZE); - if (rfeAtom != None) { - XClientMessageEvent clientMessage; - memset(&clientMessage, 0, sizeof(clientMessage)); + LOG("extents received -> new view size: %d, %d\n", newW, newH); - clientMessage.type = ClientMessage; - clientMessage.window = GDK_WINDOW_XID(gdk_window); - clientMessage.message_type = rfeAtom; - clientMessage.format = 32; + Point loc = window_location.get(); + int x = loc.x; + int y = loc.y; - XSendEvent(display, XDefaultRootWindow(display), False, - SubstructureRedirectMask | SubstructureNotifyMask, - (XEvent *) &clientMessage); - XFlush(display); - } -} + // Gravity x, y are used in centerOnScreen(). Here it's used to adjust the position + // accounting decorations + if (gravity_x > 0 && x > 0) { + x -= gravity_x * (float) (new_extents.width); + } -void WindowContextTop::update_frame_extents() { - int top, left, bottom, right; + if (gravity_y > 0 && y > 0) { + y -= gravity_y * (float) (new_extents.height); + } - if (get_frame_extents_property(&top, &left, &bottom, &right)) { - if (top > 0 || right > 0 || bottom > 0 || left > 0) { - bool changed = geometry.extents.top != top - || geometry.extents.left != left - || geometry.extents.bottom != bottom - || geometry.extents.right != right; - - if (changed) { - geometry.extents.top = top; - geometry.extents.left = left; - geometry.extents.bottom = bottom; - geometry.extents.right = right; - - set_cached_extents(geometry.extents); - - // set bounds again to correct window size - // accounting decorations - int w = geometry_get_window_width(&geometry); - int h = geometry_get_window_height(&geometry); - int cw = geometry_get_content_width(&geometry); - int ch = geometry_get_content_height(&geometry); - - int x = geometry.x; - int y = geometry.y; - - if (geometry.gravity_x != 0) { - x -= geometry.gravity_x * (float) (left + right); - } - - if (geometry.gravity_y != 0) { - y -= geometry.gravity_y * (float) (top + bottom); - } - - set_bounds(x, y, true, true, w, h, cw, ch, 0, 0); - } + window_extents.set(new_extents); + view_size.set({newW, newH}); + window_location.set({x, y}); + move_resize(x, y, true, true, newW, newH); } } } -void WindowContextTop::set_cached_extents(WindowFrameExtents ex) { - if (window_type == NORMAL) { - normal_extents = ex; - } else { - utility_extents = ex; - } -} - -WindowFrameExtents WindowContextTop::get_cached_extents() { - return window_type == NORMAL ? normal_extents : utility_extents; -} - -bool WindowContextTop::get_frame_extents_property(int *top, int *left, +bool WindowContext::get_frame_extents_property(int *top, int *left, int *bottom, int *right) { unsigned long *extents; if (gdk_property_get(gdk_window, get_net_frame_extents_atom(), - gdk_atom_intern("CARDINAL", FALSE), + gdk_atom_intern("CARDINAL", false), 0, sizeof (unsigned long) * 4, - FALSE, - NULL, - NULL, - NULL, + false, + nullptr, + nullptr, + nullptr, (guchar**) & extents)) { *left = extents [0]; *right = extents [1]; @@ -898,172 +936,247 @@ bool WindowContextTop::get_frame_extents_property(int *top, int *left, return false; } -void WindowContextTop::work_around_compiz_state() { - // Workaround for https://bugs.launchpad.net/unity/+bug/998073 - if (wmanager != COMPIZ) { +void WindowContext::set_cached_extents(Rectangle ex) { + if (window_type == UTILITY) { + utility_extents = ex; + } else { + normal_extents = ex; + } +} + +void WindowContext::load_cached_extents() { + if (frame_type != TITLED) return; + + if (window_type == NORMAL && normal_extents.has_value()) { + window_extents.set(normal_extents.value()); return; } - static GdkAtom atom_atom = gdk_atom_intern_static_string("ATOM"); - static GdkAtom atom_net_wm_state = gdk_atom_intern_static_string("_NET_WM_STATE"); - static GdkAtom atom_net_wm_state_hidden = gdk_atom_intern_static_string("_NET_WM_STATE_HIDDEN"); - static GdkAtom atom_net_wm_state_above = gdk_atom_intern_static_string("_NET_WM_STATE_ABOVE"); + if (window_type == UTILITY && utility_extents.has_value()) { + window_extents.set(utility_extents.value()); + } +} - gint length; +void WindowContext::process_property_notify(GdkEventProperty *event) { + if (event->atom == get_net_frame_extents_atom()) { + update_frame_extents(); + } +} - glong* atoms = NULL; +void WindowContext::process_state(GdkEventWindowState *event) { + if (!(event->changed_mask & (GDK_WINDOW_STATE_ICONIFIED + | GDK_WINDOW_STATE_MAXIMIZED + | GDK_WINDOW_STATE_FULLSCREEN + | GDK_WINDOW_STATE_ABOVE + | GDK_WINDOW_STATE_FOCUSED))) { + return; + } - if (gdk_property_get(gdk_window, atom_net_wm_state, atom_atom, - 0, G_MAXLONG, FALSE, NULL, NULL, &length, (guchar**) &atoms)) { + LOG("process_state\n"); - bool is_hidden = false; - bool is_above = false; - for (gint i = 0; i < (gint)(length / sizeof(glong)); i++) { - if (atom_net_wm_state_hidden == (GdkAtom)atoms[i]) { - is_hidden = true; - } else if (atom_net_wm_state_above == (GdkAtom)atoms[i]) { - is_above = true; - } - } + if (event->changed_mask & GDK_WINDOW_STATE_FOCUSED) { + process_focus(event->new_window_state & GDK_WINDOW_STATE_FOCUSED); - g_free(atoms); + if (event->changed_mask == GDK_WINDOW_STATE_FOCUSED) return; + } - if (is_iconified != is_hidden) { - is_iconified = is_hidden; + if (event->changed_mask & GDK_WINDOW_STATE_ABOVE) { + notify_on_top(event->new_window_state & GDK_WINDOW_STATE_ABOVE); - notify_state((is_hidden) - ? com_sun_glass_events_WindowEvent_MINIMIZE - : com_sun_glass_events_WindowEvent_RESTORE); - } + if (event->changed_mask == GDK_WINDOW_STATE_ABOVE) return; + } - notify_on_top(is_above); + if ((event->changed_mask & (GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_ICONIFIED)) + && ((event->new_window_state & (GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_ICONIFIED)) == 0)) { + LOG("com_sun_glass_events_WindowEvent_RESTORE\n"); + notify_window_resize(com_sun_glass_events_WindowEvent_RESTORE); + } else if (event->new_window_state & (GDK_WINDOW_STATE_ICONIFIED)) { + LOG("com_sun_glass_events_WindowEvent_MINIMIZE\n"); + notify_window_resize(com_sun_glass_events_WindowEvent_MINIMIZE); + } else if (event->new_window_state & (GDK_WINDOW_STATE_MAXIMIZED)) { + LOG("com_sun_glass_events_WindowEvent_MAXIMIZE\n"); + notify_window_resize(com_sun_glass_events_WindowEvent_MAXIMIZE); } -} -void WindowContextTop::process_property_notify(GdkEventProperty* event) { - static GdkAtom atom_net_wm_state = gdk_atom_intern_static_string("_NET_WM_STATE"); + if (event->changed_mask & GDK_WINDOW_STATE_ICONIFIED + && (event->new_window_state & GDK_WINDOW_STATE_ICONIFIED) == 0) { + remove_wmf(GDK_FUNC_MINIMIZE); + } - if (event->window == gdk_window) { - if (event->atom == get_net_frame_extents_atom()) { - update_frame_extents(); - } else if (event->atom == atom_net_wm_state) { - work_around_compiz_state(); - } + // If only iconified, no further processing + if (event->changed_mask == GDK_WINDOW_STATE_ICONIFIED) return; + + if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED + && (event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) == 0) { + remove_wmf(GDK_FUNC_MAXIMIZE); } -} -void WindowContextTop::process_state(GdkEventWindowState* event) { - if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) { - is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; + if (jview && event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) { + notify_fullscreen(event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN); } - if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED - && !(event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED)) { - gtk_window_resize(GTK_WINDOW(gtk_widget), geometry_get_content_width(&geometry), - geometry_get_content_height(&geometry)); + // Since FullScreen (or custom modes of maximized) can undecorate the + // window, request view position change + if (frame_type == TITLED) { + notify_view_move(); } - WindowContextBase::process_state(event); + bool restored = (event->changed_mask & (GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_FULLSCREEN)) + && ((event->new_window_state & (GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_FULLSCREEN)) == 0); + + if (restored && needs_to_update_frame_extents) { + LOG("State restored"); + needs_to_update_frame_extents = false; + load_cached_extents(); + } } -void WindowContextTop::process_realize() { - gdk_window = gtk_widget_get_window(gtk_widget); - if (frame_type == TITLED) { - request_frame_extents(); +void WindowContext::notify_fullscreen(bool enter) { + if (enter) { + LOG("com_sun_glass_events_ViewEvent_FULLSCREEN_ENTER\n"); + mainEnv->CallVoidMethod(jview, jViewNotifyView, com_sun_glass_events_ViewEvent_FULLSCREEN_ENTER); + CHECK_JNI_EXCEPTION(mainEnv) + } else { + LOG("com_sun_glass_events_ViewEvent_FULLSCREEN_EXIT\n"); + mainEnv->CallVoidMethod(jview, jViewNotifyView, com_sun_glass_events_ViewEvent_FULLSCREEN_EXIT); + CHECK_JNI_EXCEPTION(mainEnv) } +} - gdk_window_set_events(gdk_window, GDK_FILTERED_EVENTS_MASK); - g_object_set_data_full(G_OBJECT(gdk_window), GDK_WINDOW_DATA_CONTEXT, this, NULL); - gdk_window_register_dnd(gdk_window); +void WindowContext::notify_window_resize(int state) { + if (jwindow) { + Size size = window_size.get(); + LOG("jWindowNotifyResize: %d -> %d, %d\n", state, size.width, size.height); + mainEnv->CallVoidMethod(jwindow, jWindowNotifyResize, state, size.width, size.height); + CHECK_JNI_EXCEPTION(mainEnv) + } +} - if (gdk_windowManagerFunctions) { - gdk_window_set_functions(gdk_window, gdk_windowManagerFunctions); +void WindowContext::notify_window_move() { + if (jwindow) { + Point point = window_location.get(); + LOG("jWindowNotifyMove: %d, %d\n", point.x, point.y); + mainEnv->CallVoidMethod(jwindow, jWindowNotifyMove, point.x, point.y); + CHECK_JNI_EXCEPTION(mainEnv) } } -void WindowContextTop::process_configure(GdkEventConfigure* event) { - int ww = event->width + geometry.extents.left + geometry.extents.right; - int wh = event->height + geometry.extents.top + geometry.extents.bottom; +void WindowContext::notify_view_resize() { + if (jview) { + Size size = view_size.get(); + LOG("jViewNotifyResize: %d, %d\n", size.width, size.height); + mainEnv->CallVoidMethod(jview, jViewNotifyResize, size.width, size.height); + CHECK_JNI_EXCEPTION(mainEnv) + } +} + +void WindowContext::notify_current_sizes() { + notify_window_resize(is_maximized() + ? com_sun_glass_events_WindowEvent_MAXIMIZE + : com_sun_glass_events_WindowEvent_RESIZE); - // Do not report if iconified, because Java side would set the state to NORMAL - if (jwindow && !is_iconified) { - mainEnv->CallVoidMethod(jwindow, jWindowNotifyResize, - (is_maximized) - ? com_sun_glass_events_WindowEvent_MAXIMIZE - : com_sun_glass_events_WindowEvent_RESIZE, - ww, wh); + notify_view_resize(); +} + +void WindowContext::notify_view_move() { + if (jview) { + LOG("com_sun_glass_events_ViewEvent_MOVE\n"); + mainEnv->CallVoidMethod(jview, jViewNotifyView, + com_sun_glass_events_ViewEvent_MOVE); CHECK_JNI_EXCEPTION(mainEnv) + } +} - if (jview) { - mainEnv->CallVoidMethod(jview, jViewNotifyResize, event->width, event->height); - CHECK_JNI_EXCEPTION(mainEnv) - } +void WindowContext::process_configure(GdkEventConfigure *event) { + LOG("Configure Event - send_event: %d, x: %d, y: %d, width: %d, height: %d\n", + event->send_event, event->x, event->y, event->width, event->height); + + if (mapped && !event->send_event) { + // This is used to let the compositor detect the resize + gdk_window_invalidate_rect(gdk_window, nullptr, false); } - if (!is_iconified && !is_fullscreen && !is_maximized) { - geometry.final_width.value = (geometry.final_width.type == BOUNDSTYPE_CONTENT) - ? event->width : ww; + int x, y; + int view_x = 0, view_y = 0; + + if (frame_type == TITLED) { + // view_x and view_y represent the position of the content relative to the left corner of the window, + // taking into account window decorations (such as title bars and borders) applied by the window manager + // and might vary by window state. + int root_x, root_y; + gdk_window_get_root_origin(gdk_window, &root_x, &root_y); + + view_x = event->x - root_x; + view_y = event->y - root_y; + + x = root_x; + y = root_y; - geometry.final_height.value = (geometry.final_height.type == BOUNDSTYPE_CONTENT) - ? event->height : wh; + view_position.set({view_x, view_y}); + } else { + x = event->x; + y = event->y; } - gint root_x, root_y, origin_x, origin_y; - gdk_window_get_root_origin(gdk_window, &root_x, &root_y); - gdk_window_get_origin(gdk_window, &origin_x, &origin_y); + int ww = event->width; + int wh = event->height; - // x and y represent the position of the top-left corner of the window relative to the desktop area - geometry.x = root_x; - geometry.y = root_y; + Rectangle extents = window_extents.get(); - // view_x and view_y represent the position of the content relative to the top-left corner of the window, - // taking into account window decorations (such as title bars and borders) applied by the window manager. - geometry.view_x = origin_x - root_x; - geometry.view_y = origin_y - root_y; - notify_window_move(); + // Fullscreen usually have no decorations + if (view_x > 0) { + ww += extents.width; + } - glong to_screen = getScreenPtrForLocation(geometry.x, geometry.y); - if (to_screen != -1) { - if (to_screen != screen) { - if (jwindow) { - //notify screen changed - jobject jScreen = createJavaScreen(mainEnv, to_screen); - mainEnv->CallVoidMethod(jwindow, jWindowNotifyMoveToAnotherScreen, jScreen); - CHECK_JNI_EXCEPTION(mainEnv) - } - screen = to_screen; + if (view_y > 0) { + wh += extents.height; + } + + if (mapped) { + window_location.set({x, y}); + view_size.set({event->width, event->height}); + window_size.set({ww, wh}); + } + + glong to_screen = getScreenPtrForLocation(event->x, event->y); + if (to_screen != -1 && to_screen != screen) { + if (jwindow) { + LOG("jWindowNotifyMoveToAnotherScreen\n"); + //notify screen changed + jobject jScreen = createJavaScreen(mainEnv, to_screen); + mainEnv->CallVoidMethod(jwindow, jWindowNotifyMoveToAnotherScreen, jScreen); + CHECK_JNI_EXCEPTION(mainEnv) } + screen = to_screen; } } -void WindowContextTop::update_window_constraints() { - bool is_floating = !is_iconified && !is_fullscreen && !is_maximized; - - if (!is_floating) { - // window is not floating on the screen +void WindowContext::update_window_constraints() { + LOG("update_window_constraints\n") + // Not ready to re-apply the constraints + if (!is_floating() || !is_state_floating((GdkWindowState) initial_state_mask)) { + LOG("not floating: update_window_constraints ignored\n"); return; } GdkGeometry hints; if (is_resizable() && !is_disabled) { - int w = std::max(resizable.sysminw, resizable.minw); - int h = std::max(resizable.sysminh, resizable.minh); + Size min = minimum_size.get().max(sys_min_size.get()); - int min_w = (w == -1) ? 1 : w - geometry.extents.left - geometry.extents.right; - int min_h = (h == -1) ? 1 : h - geometry.extents.top - geometry.extents.bottom; + Rectangle extents = window_extents.get(); - hints.min_width = (min_w < 1) ? 1 : min_w; - hints.min_height = (min_h < 1) ? 1 : min_h; + hints.min_width = std::clamp(min.width - extents.width, 1, MAX_WINDOW_SIZE); + hints.min_height = std::clamp(min.height - extents.height, 1, MAX_WINDOW_SIZE); - hints.max_width = (resizable.maxw == -1) ? G_MAXINT - : resizable.maxw - geometry.extents.left - geometry.extents.right; + Size max = maximum_size.get(); - hints.max_height = (resizable.maxh == -1) ? G_MAXINT - : resizable.maxh - geometry.extents.top - geometry.extents.bottom; + hints.max_width = std::clamp(max.width - extents.width, 1, MAX_WINDOW_SIZE); + hints.max_height = std::clamp(max.height - extents.height, 1, MAX_WINDOW_SIZE); } else { - int w = geometry_get_content_width(&geometry); - int h = geometry_get_content_height(&geometry); + Size size = view_size.get(); + int w = std::clamp(size.width, 1, MAX_WINDOW_SIZE); + int h = std::clamp(size.height, 1, MAX_WINDOW_SIZE); hints.min_width = w; hints.min_height = h; @@ -1071,231 +1184,246 @@ void WindowContextTop::update_window_constraints() { hints.max_height = h; } - gtk_window_set_geometry_hints(GTK_WINDOW(gtk_widget), NULL, &hints, - (GdkWindowHints)(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE)); + LOG("geometry hints: min w,h: %d, %d - max w,h: %d, %d\n", hints.min_width, + hints.min_height, hints.max_width, hints.max_height); + + // GDK_HINT_USER_POS is used for the initial position to work + gdk_window_set_geometry_hints(gdk_window, &hints, + (GdkWindowHints) (GDK_HINT_USER_POS | GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE)); } -void WindowContextTop::set_resizable(bool res) { - resizable.value = res; - update_window_constraints(); +void WindowContext::set_resizable(bool res) { + LOG("set_resizable: %d\n", res); + resizable.set(res); } -bool WindowContextTop::is_resizable() { - return resizable.value; +bool WindowContext::is_resizable() { + return resizable.get(); } -void WindowContextTop::set_visible(bool visible) { - WindowContextBase::set_visible(visible); +bool WindowContext::is_maximized() { + return gdk_window_get_state(gdk_window) & GDK_WINDOW_STATE_MAXIMIZED; +} - if (visible && !geometry.size_assigned) { - set_bounds(0, 0, false, false, 320, 200, -1, -1, 0, 0); - } +bool WindowContext::is_fullscreen() { + return gdk_window_get_state(gdk_window) & GDK_WINDOW_STATE_FULLSCREEN; +} - //JDK-8220272 - fire event first because GDK_FOCUS_CHANGE is not always in order - if (visible && jwindow && isEnabled()) { - mainEnv->CallVoidMethod(jwindow, jWindowNotifyFocus, com_sun_glass_events_WindowEvent_FOCUS_GAINED); - CHECK_JNI_EXCEPTION(mainEnv); +bool WindowContext::is_iconified() { + return gdk_window_get_state(gdk_window) & GDK_WINDOW_STATE_ICONIFIED; +} + +bool WindowContext::is_floating() { + return is_state_floating(gdk_window_get_state(gdk_window)); +} + +void WindowContext::set_visible(bool visible) { + LOG("set_visible: %d\n", visible); + if (visible) { + gdk_window_show(gdk_window); + } else { + gdk_window_hide(gdk_window); + if (jview && is_mouse_entered) { + is_mouse_entered = false; + mainEnv->CallVoidMethod(jview, jViewNotifyMouse, + com_sun_glass_events_MouseEvent_EXIT, + com_sun_glass_events_MouseEvent_BUTTON_NONE, + 0, 0, + 0, 0, + 0, + JNI_FALSE, + JNI_FALSE); + CHECK_JNI_EXCEPTION(mainEnv) + } } } -void WindowContextTop::set_bounds(int x, int y, bool xSet, bool ySet, int w, int h, int cw, int ch, - float gravity_x, float gravity_y) { -// fprintf(stderr, "set_bounds -> x = %d, y = %d, xset = %d, yset = %d, w = %d, h = %d, cw = %d, ch = %d, gx = %f, gy = %f\n", -// x, y, xSet, ySet, w, h, cw, ch, gravity_x, gravity_y); +void WindowContext::set_bounds(int x, int y, bool xSet, bool ySet, int w, int h, int cw, int ch, + float gravity_x, float gravity_y) { + LOG("set_bounds -> x = %d, y = %d, xset = %d, yset = %d, w = %d, h = %d, cw = %d, ch = %d, gx = %f, gy = %f\n", + x, y, xSet, ySet, w, h, cw, ch, gravity_x, gravity_y); // newW / newH are view/content sizes int newW = 0; int newH = 0; - geometry.gravity_x = gravity_x; - geometry.gravity_y = gravity_y; + this->gravity_x = gravity_x; + this->gravity_y = gravity_y; if (w > 0) { - geometry.final_width.type = BOUNDSTYPE_WINDOW; - geometry.final_width.value = w; - newW = w - (geometry.extents.left + geometry.extents.right); + width_type = BOUNDSTYPE_WINDOW; + newW = std::clamp(w - window_extents.get().width, 1, MAX_WINDOW_SIZE); } else if (cw > 0) { - geometry.final_width.type = BOUNDSTYPE_CONTENT; - geometry.final_width.value = cw; + // once set to window, stick with it + if (width_type == BOUNDSTYPE_UNKNOWN) { + width_type = BOUNDSTYPE_VIEW; + } newW = cw; - } else { - newW = geometry_get_content_width(&geometry); } if (h > 0) { - geometry.final_height.type = BOUNDSTYPE_WINDOW; - geometry.final_height.value = h; - newH = h - (geometry.extents.top + geometry.extents.bottom); + height_type = BOUNDSTYPE_WINDOW; + newH = std::clamp(h - window_extents.get().height, 1, MAX_WINDOW_SIZE); } else if (ch > 0) { - geometry.final_height.type = BOUNDSTYPE_CONTENT; - geometry.final_height.value = ch; - newH = ch; - } else { - newH = geometry_get_content_height(&geometry); - } - - - if (newW > 0 || newH > 0) { - // call update_window_constraints() to let gtk_window_resize succeed, because it's bound to geometry constraints - update_window_constraints(); - - if (gtk_widget_get_realized(gtk_widget)) { - gtk_window_resize(GTK_WINDOW(gtk_widget), newW, newH); - } else { - gtk_window_set_default_size(GTK_WINDOW(gtk_widget), newW, newH); + // once set to window, stick with it + if (width_type == BOUNDSTYPE_UNKNOWN) { + height_type = BOUNDSTYPE_VIEW; } - geometry.size_assigned = true; - notify_window_resize(); + newH = ch; } - if (xSet || ySet) { - if (xSet) { - geometry.x = x; - } - - if (ySet) { - geometry.y = y; - } - - gtk_window_move(GTK_WINDOW(gtk_widget), geometry.x, geometry.y); + // Ignore when maximized / fullscreen (not floating) + // Report back to java to correct the values + if (mapped && !is_floating()) { + notify_current_sizes(); notify_window_move(); - } -} - -void WindowContextTop::applyShapeMask(void* data, uint width, uint height) { - if (frame_type != TRANSPARENT) { return; } - glass_window_apply_shape_mask(gtk_widget_get_window(gtk_widget), data, width, height); + move_resize(x, y, xSet, ySet, newW, newH); } -void WindowContextTop::set_minimized(bool minimize) { - is_iconified = minimize; - if (minimize) { - if (frame_type == TRANSPARENT && wmanager == COMPIZ) { - // https://bugs.launchpad.net/ubuntu/+source/unity/+bug/1245571 - glass_window_reset_input_shape_mask(gtk_widget_get_window(gtk_widget)); - } - - if ((gdk_windowManagerFunctions & GDK_FUNC_MINIMIZE) == 0) { - // in this case - the window manager will not support the programatic - // request to iconify - so we need to disable this until we are restored. - GdkWMFunction wmf = (GdkWMFunction)(gdk_windowManagerFunctions | GDK_FUNC_MINIMIZE); - gdk_window_set_functions(gdk_window, wmf); - } - gtk_window_iconify(GTK_WINDOW(gtk_widget)); +void WindowContext::iconify(bool state) { + if (state) { + add_wmf(GDK_FUNC_MINIMIZE); + gdk_window_iconify(gdk_window); } else { - gtk_window_deiconify(GTK_WINDOW(gtk_widget)); + gdk_window_deiconify(gdk_window); gdk_window_focus(gdk_window, GDK_CURRENT_TIME); } } -void WindowContextTop::set_maximized(bool maximize) { - is_maximized = maximize; - if (maximize) { - // enable the functionality on the window manager as it might ignore the maximize command, - // for example when the window is undecorated. - GdkWMFunction wmf = (GdkWMFunction)(gdk_windowManagerFunctions | GDK_FUNC_MAXIMIZE); - gdk_window_set_functions(gdk_window, wmf); +void WindowContext::maximize(bool state) { + if (state) { + add_wmf(GDK_FUNC_MAXIMIZE); + gdk_window_maximize(gdk_window); + } else { + gdk_window_unmaximize(gdk_window); + } +} - gtk_window_maximize(GTK_WINDOW(gtk_widget)); +void WindowContext::set_minimized(bool state) { + LOG("set_minimized = %d\n", state); + if (mapped) { + iconify(state); } else { - gtk_window_unmaximize(GTK_WINDOW(gtk_widget)); + initial_state_mask = state + ? (initial_state_mask | GDK_WINDOW_STATE_ICONIFIED) + : (initial_state_mask & ~GDK_WINDOW_STATE_ICONIFIED); } } -void WindowContextTop::enter_fullscreen() { - gtk_window_fullscreen(GTK_WINDOW(gtk_widget)); - is_fullscreen = true; +void WindowContext::set_maximized(bool state) { + LOG("set_maximized = %d\n", state); + if (mapped) { + maximize(state); + } else { + initial_state_mask = state + ? (initial_state_mask | GDK_WINDOW_STATE_MAXIMIZED) + : (initial_state_mask & ~GDK_WINDOW_STATE_MAXIMIZED); + } } -void WindowContextTop::exit_fullscreen() { - gtk_window_unfullscreen(GTK_WINDOW(gtk_widget)); +void WindowContext::enter_fullscreen() { + LOG("enter_fullscreen\n"); + if (mapped) { + gdk_window_fullscreen(gdk_window); + } else { + initial_state_mask |= GDK_WINDOW_STATE_FULLSCREEN; + } } -void WindowContextTop::request_focus() { - if (is_visible()) { - gtk_window_present(GTK_WINDOW(gtk_widget)); +void WindowContext::exit_fullscreen() { + LOG("exit_fullscreen\n"); + if (mapped) { + gdk_window_unfullscreen(gdk_window); + } else { + initial_state_mask &= ~GDK_WINDOW_STATE_FULLSCREEN; } } -void WindowContextTop::set_focusable(bool focusable) { - gtk_window_set_accept_focus(GTK_WINDOW(gtk_widget), focusable ? TRUE : FALSE); +void WindowContext::request_focus() { + LOG("request_focus\n"); + if (!is_visible()) return; + + gdk_window_focus(gdk_window, GDK_CURRENT_TIME); } -void WindowContextTop::set_title(const char* title) { - gtk_window_set_title(GTK_WINDOW(gtk_widget), title); +void WindowContext::set_focusable(bool focusable) { + gdk_window_set_accept_focus(gdk_window, focusable ? true : false); } -void WindowContextTop::set_alpha(double alpha) { - gtk_window_set_opacity(GTK_WINDOW(gtk_widget), (gdouble)alpha); +void WindowContext::set_title(const char* title) { + gdk_window_set_title(gdk_window, title); } -void WindowContextTop::set_enabled(bool enabled) { +// This only works o Xorg +void WindowContext::set_alpha(double alpha) { + gdk_window_set_opacity(gdk_window, (gdouble)alpha); +} + +void WindowContext::set_enabled(bool enabled) { is_disabled = !enabled; update_window_constraints(); } -void WindowContextTop::set_system_minimum_size(int w, int h) { - resizable.sysminw = w; - resizable.sysminh = h; - update_window_constraints(); +void WindowContext::set_minimum_size(int w, int h) { + LOG("set_minimum_size: %d, %d\n", w, h); + minimum_size.set({w, h}); } -void WindowContextTop::set_minimum_size(int w, int h) { - resizable.minw = (w <= 0) ? 1 : w; - resizable.minh = (h <= 0) ? 1 : h; - update_window_constraints(); +void WindowContext::set_system_minimum_size(int w, int h) { + LOG("set_system_minimum_size: %d,%d\n", w, h) + sys_min_size.set({w, h}); } -void WindowContextTop::set_maximum_size(int w, int h) { - resizable.maxw = w; - resizable.maxh = h; - update_window_constraints(); +void WindowContext::set_maximum_size(int w, int h) { + LOG("set_maximum_size: %d, %d\n", w, h); + int maxw = (w == -1) ? G_MAXINT : w; + int maxh = (h == -1) ? G_MAXINT : h; + + maximum_size.set({maxw, maxh}); } -void WindowContextTop::set_icon(GdkPixbuf* pixbuf) { - gtk_window_set_icon(GTK_WINDOW(gtk_widget), pixbuf); +void WindowContext::set_icon(GdkPixbuf* icon) { + if (icon == nullptr || !GDK_IS_PIXBUF(icon)) return; + + GList *icons = nullptr; + icons = g_list_append(icons, icon); + gdk_window_set_icon_list(gdk_window, icons); + g_list_free(icons); } -void WindowContextTop::to_front() { +void WindowContext::to_front() { + LOG("to_front\n"); gdk_window_raise(gdk_window); } -void WindowContextTop::to_back() { +void WindowContext::to_back() { + LOG("to_back\n"); gdk_window_lower(gdk_window); } -void WindowContextTop::set_modal(bool modal, WindowContext* parent) { +void WindowContext::set_modal(bool modal, WindowContext* parent) { if (modal) { - //gtk_window_set_type_hint(GTK_WINDOW(gtk_widget), GDK_WINDOW_TYPE_HINT_DIALOG); if (parent) { - gtk_window_set_transient_for(GTK_WINDOW(gtk_widget), parent->get_gtk_window()); + gdk_window_set_transient_for(gdk_window, parent->get_gdk_window()); } } - gtk_window_set_modal(GTK_WINDOW(gtk_widget), modal ? TRUE : FALSE); -} - -GtkWindow *WindowContextTop::get_gtk_window() { - return GTK_WINDOW(gtk_widget); -} - -WindowGeometry WindowContextTop::get_geometry() { - return geometry; + gdk_window_set_modal_hint(gdk_window, modal ? true : false); } -void WindowContextTop::update_ontop_tree(bool on_top) { +void WindowContext::update_ontop_tree(bool on_top) { bool effective_on_top = on_top || this->on_top; - gtk_window_set_keep_above(GTK_WINDOW(gtk_widget), effective_on_top ? TRUE : FALSE); - for (std::set::iterator it = children.begin(); it != children.end(); ++it) { + gdk_window_set_keep_above(gdk_window, effective_on_top ? true : false); + for (std::set::iterator it = children.begin(); it != children.end(); ++it) { (*it)->update_ontop_tree(effective_on_top); } } -bool WindowContextTop::on_top_inherited() { +bool WindowContext::on_top_inherited() { WindowContext* o = owner; while (o) { - WindowContextTop* topO = dynamic_cast(o); + WindowContext* topO = dynamic_cast(o); if (!topO) break; if (topO->on_top) { return true; @@ -1305,20 +1433,103 @@ bool WindowContextTop::on_top_inherited() { return false; } -bool WindowContextTop::effective_on_top() { +bool WindowContext::effective_on_top() { if (owner) { - WindowContextTop* topO = dynamic_cast(owner); + WindowContext* topO = dynamic_cast(owner); return (topO && topO->effective_on_top()) || on_top; } return on_top; } -void WindowContextTop::notify_on_top(bool top) { +void WindowContext::update_window_size() { + LOG("update_window_size\n") + Size size = view_size.get(); + + if (frame_type == TITLED) { + window_size.set({size.width + window_extents.get().width, size.height + window_extents.get().height}); + } else { + window_size.set(size); + } +} + +void WindowContext::move_resize(int x, int y, bool xSet, bool ySet, int width, int height) { + LOG("move_resize: x,y: %d,%d / cw,ch: %d,%d\n", x, y, width, height); + Size size = view_size.get(); + int newW = (width > 0) ? width : size.width; + int newH = (height > 0) ? height : size.height; + + Rectangle extents = window_extents.get(); + int boundsW = newW, boundsH = newH; + + Size max_size = maximum_size.get(); + Size min_size = minimum_size.get().max(sys_min_size.get()); + + // Windows that are undecorated or transparent will not respect + // minimum or maximum size constraints + if (min_size.width > 0 && newW < min_size.width) { + boundsW = min_size.width - extents.width; + } + + if (max_size.width > 0 && newW > max_size.width) { + boundsW = max_size.height - extents.width; + } + + if (min_size.height > 0 && newH < min_size.height) { + boundsH = min_size.height - extents.height; + } + + if (max_size.height > 0 && newH > max_size.height) { + boundsH = max_size.height - extents.height; + } + + boundsW = std::clamp(boundsW, 1, MAX_WINDOW_SIZE); + boundsH = std::clamp(boundsH, 1, MAX_WINDOW_SIZE); + + Size current_size = view_size.get(); + + // Need to force notify back to java, because it probably + // has wrong sizes + if ((newW != boundsW && current_size.width == boundsW) + || newH != boundsH && current_size.height == boundsH) { + view_size.invalidate(); + window_size.invalidate(); + } + + Point loc = window_location.get(); + int newX = (xSet) ? x : loc.x; + int newY = (ySet) ? y : loc.y; + + if (!mapped) { + view_size.set({boundsW, boundsH}); + update_window_size(); + window_location.set({newX, newY}); + } + + LOG("gdk_window_move_resize: x,y: %d,%d / cw,ch: %d,%d\n", newX, newY, boundsW, boundsH); + + gdk_window_move_resize(gdk_window, newX, newY, boundsW, boundsH); +} + +void WindowContext::add_wmf(GdkWMFunction wmf) { + if ((initial_wmf & wmf) == 0) { + current_wmf = (GdkWMFunction)((int)current_wmf | (int)wmf); + gdk_window_set_functions(gdk_window, current_wmf); + } +} + +void WindowContext::remove_wmf(GdkWMFunction wmf) { + if ((initial_wmf & wmf) == 0) { + current_wmf = (GdkWMFunction)((int)current_wmf & ~(int)wmf); + gdk_window_set_functions(gdk_window, current_wmf); + } +} + +void WindowContext::notify_on_top(bool top) { // Do not report effective (i.e. native) values to the FX, only if the user sets it manually if (top != effective_on_top() && jwindow) { if (on_top_inherited() && !top) { // Disallow user's "on top" handling on windows that inherited the property - gtk_window_set_keep_above(GTK_WINDOW(gtk_widget), TRUE); + gdk_window_set_keep_above(gdk_window, true); } else { on_top = top; update_ontop_tree(top); @@ -1330,7 +1541,7 @@ void WindowContextTop::notify_on_top(bool top) { } } -void WindowContextTop::set_level(int level) { +void WindowContext::set_level(int level) { if (level == com_sun_glass_ui_Window_Level_NORMAL) { on_top = false; } else if (level == com_sun_glass_ui_Window_Level_FLOATING @@ -1344,55 +1555,18 @@ void WindowContextTop::set_level(int level) { } } -void WindowContextTop::set_owner(WindowContext * owner_ctx) { +void WindowContext::set_owner(WindowContext * owner_ctx) { owner = owner_ctx; } -void WindowContextTop::update_view_size() { +void WindowContext::update_view_size() { // Notify the view size only if size is oriented by WINDOW, otherwise it knows its own size - if (geometry.final_width.type == BOUNDSTYPE_WINDOW - || geometry.final_height.type == BOUNDSTYPE_WINDOW) { - + if (width_type == BOUNDSTYPE_WINDOW || height_type == BOUNDSTYPE_WINDOW) { notify_view_resize(); } } -void WindowContextTop::notify_view_resize() { - if (jview) { - int cw = geometry_get_content_width(&geometry); - int ch = geometry_get_content_height(&geometry); - - mainEnv->CallVoidMethod(jview, jViewNotifyResize, cw, ch); - CHECK_JNI_EXCEPTION(mainEnv) - } -} - -void WindowContextTop::notify_window_resize() { - int w = geometry_get_window_width(&geometry); - int h = geometry_get_window_height(&geometry); - - mainEnv->CallVoidMethod(jwindow, jWindowNotifyResize, - com_sun_glass_events_WindowEvent_RESIZE, w, h); - CHECK_JNI_EXCEPTION(mainEnv) - - notify_view_resize(); -} - -void WindowContextTop::notify_window_move() { - if (jwindow) { - mainEnv->CallVoidMethod(jwindow, jWindowNotifyMove, - geometry.x, geometry.y); - CHECK_JNI_EXCEPTION(mainEnv) - - if (jview) { - mainEnv->CallVoidMethod(jview, jViewNotifyView, - com_sun_glass_events_ViewEvent_MOVE); - CHECK_JNI_EXCEPTION(mainEnv) - } - } -} - -void WindowContextTop::show_system_menu(int x, int y) { +void WindowContext::show_system_menu(int x, int y) { GdkDisplay* display = gdk_display_get_default(); if (!display) { return; @@ -1418,27 +1592,49 @@ void WindowContextTop::show_system_menu(int x, int y) { gdk_event_free(event); } +Size WindowContext::get_view_size() { + return view_size.get(); +} + +Point WindowContext::get_view_position() { + return view_position.get(); +} + +WindowContext::~WindowContext() { + LOG("~WindowContext\n"); + disableIME(); + gdk_window_destroy(gdk_window); +} + +WindowContextExtended::WindowContextExtended(jobject jwin, + WindowContext* owner, + long screen, + GdkWMFunction wmf) + : WindowContext(jwin, owner, screen, EXTENDED, NORMAL, wmf) { +} + /* * Handles mouse button events of EXTENDED windows and adds the window behaviors for non-client * regions that are usually provided by the window manager. Note that a full-screen window has * no non-client regions. */ -void WindowContextTop::process_mouse_button(GdkEventButton* event, bool synthesized) { +void WindowContextExtended::process_mouse_button(GdkEventButton* event, bool synthesized) { + LOG("WindowContextExtended::process_mouse_button\n"); // Non-EXTENDED or full-screen windows don't have additional behaviors, so we delegate // directly to the base implementation. - if (is_fullscreen || frame_type != EXTENDED || jwindow == NULL) { - WindowContextBase::process_mouse_button(event); + if (is_fullscreen() || get_jwindow() == nullptr) { + WindowContext::process_mouse_button(event); return; } // Double-clicking on the drag area maximizes the window (or restores its size). if (is_resizable() && event->type == GDK_2BUTTON_PRESS) { jboolean dragArea = mainEnv->CallBooleanMethod( - jwindow, jGtkWindowDragAreaHitTest, (jint)event->x, (jint)event->y); + get_jwindow(), jGtkWindowDragAreaHitTest, (jint)event->x, (jint)event->y); CHECK_JNI_EXCEPTION(mainEnv); if (dragArea) { - set_maximized(!is_maximized); + set_maximized(!is_maximized()); } // We don't process the GDK_2BUTTON_PRESS event in the base implementation. @@ -1447,43 +1643,43 @@ void WindowContextTop::process_mouse_button(GdkEventButton* event, bool synthesi if (event->button == 1 && event->type == GDK_BUTTON_PRESS) { GdkWindowEdge edge; - bool shouldStartResizeDrag = is_resizable() && !is_maximized && get_window_edge(event->x, event->y, &edge); + bool shouldStartResizeDrag = is_resizable() && !is_maximized() && get_window_edge(event->x, event->y, &edge); // Clicking on a window edge starts a move-resize operation. if (shouldStartResizeDrag) { // Send a synthetic PRESS + RELEASE to FX. This allows FX to do things that need to be done // prior to resizing the window, like closing a popup menu. We do this because we won't be // sending events to FX once the resize operation has started. - WindowContextBase::process_mouse_button(event, true); + WindowContext::process_mouse_button(event, true); event->type = GDK_BUTTON_RELEASE; - WindowContextBase::process_mouse_button(event, true); + WindowContext::process_mouse_button(event, true); gint rx = 0, ry = 0; gdk_window_get_root_coords(get_gdk_window(), event->x, event->y, &rx, &ry); - gtk_window_begin_resize_drag(get_gtk_window(), edge, 1, rx, ry, event->time); + gdk_window_begin_resize_drag(get_gdk_window(), edge, 1, rx, ry, event->time); return; } bool shouldStartMoveDrag = mainEnv->CallBooleanMethod( - jwindow, jGtkWindowDragAreaHitTest, (jint)event->x, (jint)event->y); + get_jwindow(), jGtkWindowDragAreaHitTest, (jint)event->x, (jint)event->y); CHECK_JNI_EXCEPTION(mainEnv); // Clicking on a draggable area starts a move-drag operation. if (shouldStartMoveDrag) { // Send a synthetic PRESS + RELEASE to FX. - WindowContextBase::process_mouse_button(event, true); + WindowContext::process_mouse_button(event, true); event->type = GDK_BUTTON_RELEASE; - WindowContextBase::process_mouse_button(event, true); + WindowContext::process_mouse_button(event, true); gint rx = 0, ry = 0; gdk_window_get_root_coords(get_gdk_window(), event->x, event->y, &rx, &ry); - gtk_window_begin_move_drag(get_gtk_window(), 1, rx, ry, event->time); + gdk_window_begin_move_drag(get_gdk_window(), 1, rx, ry, event->time); return; } } // Call the base implementation for client area events. - WindowContextBase::process_mouse_button(event); + WindowContext::process_mouse_button(event); } /* @@ -1491,17 +1687,15 @@ void WindowContextTop::process_mouse_button(GdkEventButton* event, bool synthesi * of the internal resize border. Note that a full-screen window or maximized window has no * resize border. */ -void WindowContextTop::process_mouse_motion(GdkEventMotion* event) { +void WindowContextExtended::process_mouse_motion(GdkEventMotion* event) { GdkWindowEdge edge; // Call the base implementation for client area events. - if (is_fullscreen - || is_maximized - || frame_type != EXTENDED + if (!is_floating() || !is_resizable() || !get_window_edge(event->x, event->y, &edge)) { - set_cursor_override(NULL); - WindowContextBase::process_mouse_motion(event); + set_cursor_override(nullptr); + WindowContext::process_mouse_motion(event); return; } @@ -1516,7 +1710,7 @@ void WindowContextTop::process_mouse_motion(GdkEventMotion* event) { GdkCursor* NORTH_WEST = gdk_cursor_new(GDK_TOP_LEFT_CORNER); } cursors; - GdkCursor* cursor = NULL; + GdkCursor* cursor = nullptr; switch (edge) { case GDK_WINDOW_EDGE_NORTH: cursor = cursors.NORTH; break; @@ -1532,8 +1726,8 @@ void WindowContextTop::process_mouse_motion(GdkEventMotion* event) { set_cursor_override(cursor); // If the cursor is not on a resize border, call the base handler. - if (cursor == NULL) { - WindowContextBase::process_mouse_motion(event); + if (cursor == nullptr) { + WindowContext::process_mouse_motion(event); } } @@ -1541,38 +1735,29 @@ void WindowContextTop::process_mouse_motion(GdkEventMotion* event) { * Determines the GdkWindowEdge at the specified coordinate; returns true if the coordinate * identifies a window edge, false otherwise. */ -bool WindowContextTop::get_window_edge(int x, int y, GdkWindowEdge* window_edge) { +bool WindowContextExtended::get_window_edge(int x, int y, GdkWindowEdge* window_edge) { GdkWindowEdge edge; - gint width, height; - gtk_window_get_size(get_gtk_window(), &width, &height); + Size size = get_view_size(); if (x <= RESIZE_BORDER_WIDTH) { if (y <= 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_NORTH_WEST; - else if (y >= height - 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_SOUTH_WEST; + else if (y >= size.height - 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_SOUTH_WEST; else edge = GDK_WINDOW_EDGE_WEST; - } else if (x >= width - RESIZE_BORDER_WIDTH) { + } else if (x >= size.width - RESIZE_BORDER_WIDTH) { if (y <= 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_NORTH_EAST; - else if (y >= height - 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_SOUTH_EAST; + else if (y >= size.height - 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_SOUTH_EAST; else edge = GDK_WINDOW_EDGE_EAST; } else if (y <= RESIZE_BORDER_WIDTH) { edge = GDK_WINDOW_EDGE_NORTH; - } else if (y >= height - RESIZE_BORDER_WIDTH) { + } else if (y >= size.height - RESIZE_BORDER_WIDTH) { edge = GDK_WINDOW_EDGE_SOUTH; } else { return false; } - if (window_edge != NULL) { + if (window_edge != nullptr) { *window_edge = edge; } return true; } - -void WindowContextTop::process_destroy() { - if (owner) { - owner->remove_child(this); - } - - WindowContextBase::process_destroy(); -} diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h index 82bd72169ed..9d4e0dffab2 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h @@ -25,22 +25,120 @@ #ifndef GLASS_WINDOW_H #define GLASS_WINDOW_H +#define DEFAULT_WIDTH 320 +#define DEFAULT_HEIGHT 200 + +// Native Windows wider or taller than 32767 pixels are not supported +#define MAX_WINDOW_SIZE 32767 + +#define GETTER(type, name) \ + type get_##name() const { return name; } + #include #include #include #include #include +#include #include "DeletedMemDebug.h" - #include "glass_view.h" +#include "glass_general.h" + +#include +#include + +template +class Observable { +private: + T value; + std::function onChange; + +public: + Observable(const T& initialValue = T()) : value(initialValue) {} + + void set(const T& newValue) { + if (value != newValue) { + value = newValue; + invalidate(); + } + } -enum WindowManager { - COMPIZ, - UNKNOWN + void invalidate() { + if (onChange) { + onChange(value); + } + } + + // This resets the value without notifying + void reset(const T& newValue) { + value = newValue; + } + + const T& get() const { + return value; + } + + operator T() const { + return value; + } + + Observable& operator=(const T& newValue) { + set(newValue); + return *this; + } + + void setOnChange(std::function callback) { + onChange = callback; + } }; +struct Rectangle { + int x, y, width, height; + + bool operator!=(const Rectangle& other) const { + return x != other.x || y != other.y + || width != other.width || height != other.height; + } + + bool operator==(const Rectangle& other) const { + return x == other.x && y == other.y + && width == other.width && height == other.height; + } +}; + +struct Size { + int width, height; + + bool operator!=(const Size& other) const { + return width != other.width || height != other.height; + } + + bool operator==(const Size& other) const { + return width == other.width && height == other.height; + } + + Size max(const Size& other) const { + int w = std::max(other.width, width); + int h = std::max(other.height, height); + return {w, h}; + } +}; + +struct Point { + int x, y; + + bool operator!=(const Point& other) const { + return x != other.x || y != other.y; + } + + bool operator==(const Point& other) const { + return x == other.x && y == other.y; + } +}; + + enum WindowFrameType { TITLED, UNTITLED, @@ -54,133 +152,26 @@ enum WindowType { POPUP }; -struct WindowFrameExtents { - int top; - int left; - int bottom; - int right; -}; - -static const guint MOUSE_BUTTONS_MASK = (guint) (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK); - enum BoundsType { - BOUNDSTYPE_CONTENT, + BOUNDSTYPE_UNKNOWN, + BOUNDSTYPE_VIEW, BOUNDSTYPE_WINDOW }; -struct WindowGeometry { - WindowGeometry(): final_width(), final_height(), - size_assigned(false), x(), y(), view_x(), view_y(), gravity_x(), gravity_y(), extents() {} - // estimate of the final width the window will get after all pending - // configure requests are processed by the window manager - struct { - int value; - BoundsType type; - } final_width; - - struct { - int value; - BoundsType type; - } final_height; - - bool size_assigned; - - int x; - int y; - int view_x; - int view_y; - - float gravity_x; - float gravity_y; - - WindowFrameExtents extents; -}; +static const guint MOUSE_BUTTONS_MASK = (guint) (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK); -class WindowContextTop; -class WindowContext : public DeletedMemDebug<0xCC> { -public: - virtual bool isEnabled() = 0; - virtual bool hasIME() = 0; - virtual bool filterIME(GdkEvent *) = 0; - virtual void enableOrResetIME() = 0; - virtual void updateCaretPos() = 0; - virtual void disableIME() = 0; - virtual void setOnPreEdit(bool) = 0; - virtual void commitIME(gchar *) = 0; - - virtual void paint(void* data, jint width, jint height) = 0; - virtual WindowGeometry get_geometry() = 0; - - virtual void show_system_menu(int x, int y) = 0; - virtual void enter_fullscreen() = 0; - virtual void exit_fullscreen() = 0; - virtual void set_visible(bool) = 0; - virtual bool is_visible() = 0; - virtual void set_bounds(int, int, bool, bool, int, int, int, int, float, float) = 0; - virtual void set_resizable(bool) = 0; - virtual bool is_resizable() = 0; - virtual void request_focus() = 0; - virtual void set_focusable(bool)= 0; - virtual bool grab_focus() = 0; - virtual bool grab_mouse_drag_focus() = 0; - virtual void ungrab_focus() = 0; - virtual void ungrab_mouse_drag_focus() = 0; - virtual void set_title(const char*) = 0; - virtual void set_alpha(double) = 0; - virtual void set_enabled(bool) = 0; - virtual void set_system_minimum_size(int, int) = 0; - virtual void set_minimum_size(int, int) = 0; - virtual void set_maximum_size(int, int) = 0; - virtual void set_minimized(bool) = 0; - virtual void set_maximized(bool) = 0; - virtual void set_icon(GdkPixbuf*) = 0; - virtual void to_front() = 0; - virtual void to_back() = 0; - virtual void set_cursor(GdkCursor*) = 0; - virtual void set_modal(bool, WindowContext* parent = NULL) = 0; - virtual void set_level(int) = 0; - virtual void set_background(float, float, float) = 0; - - virtual void process_realize() = 0; - virtual void process_property_notify(GdkEventProperty*) = 0; - virtual void process_configure(GdkEventConfigure*) = 0; - virtual void process_focus(GdkEventFocus*) = 0; - virtual void process_destroy() = 0; - virtual void process_delete() = 0; - virtual void process_expose(GdkEventExpose*) = 0; - virtual void process_mouse_button(GdkEventButton*, bool synthesized = false) = 0; - virtual void process_mouse_motion(GdkEventMotion*) = 0; - virtual void process_mouse_scroll(GdkEventScroll*) = 0; - virtual void process_mouse_cross(GdkEventCrossing*) = 0; - virtual void process_key(GdkEventKey*) = 0; - virtual void process_state(GdkEventWindowState*) = 0; - - virtual void notify_state(jint) = 0; - virtual void notify_on_top(bool) {} - virtual void update_view_size() = 0; - virtual void notify_view_resize() = 0; - - virtual void add_child(WindowContextTop* child) = 0; - virtual void remove_child(WindowContextTop* child) = 0; - virtual bool set_view(jobject) = 0; - - virtual GdkWindow *get_gdk_window() = 0; - virtual GtkWindow *get_gtk_window() = 0; - virtual jobject get_jview() = 0; - virtual jobject get_jwindow() = 0; - - virtual void increment_events_counter() = 0; - virtual void decrement_events_counter() = 0; - virtual size_t get_events_count() = 0; - virtual bool get_window_edge(int x, int y, GdkWindowEdge*) = 0; - virtual bool is_dead() = 0; - virtual ~WindowContext() {} -}; +class WindowContext; +class WindowContextExtended; -class WindowContextBase: public WindowContext { +class WindowContext: public DeletedMemDebug<0xCC> { +private: + static std::optional normal_extents; + static std::optional utility_extents; - struct ImContext { + struct _ImContext { + _ImContext(): ctx(nullptr), enabled(false), on_preedit(false), + send_keypress(false), on_key_event(false) {} GtkIMContext *ctx; bool enabled; bool on_preedit; @@ -188,22 +179,47 @@ class WindowContextBase: public WindowContext { bool on_key_event; } im_ctx; - size_t events_processing_cnt; - bool can_be_deleted; -protected: - std::set children; + size_t events_processing_cnt{}; + std::set children; + jobject jwindow; - jobject jview; - GtkWidget* gtk_widget; - GdkWindow* gdk_window = NULL; - GdkCursor* gdk_cursor = NULL; - GdkCursor* gdk_cursor_override = NULL; - GdkWMFunction gdk_windowManagerFunctions; - - bool is_iconified; - bool is_maximized; - bool is_mouse_entered; - bool is_disabled; + jobject jview{}; + + struct WindowContext *owner; + jlong screen; + + bool is_mouse_entered{false}; + bool is_disabled{false}; + bool on_top{false}; + bool can_be_deleted{false}; + bool mapped{false}; + gint initial_state_mask{0}; + + WindowFrameType frame_type; + WindowType window_type; + + GdkWindow *gdk_window{}; + + GdkWMFunction initial_wmf; + GdkWMFunction current_wmf; + + GdkCursor* gdk_cursor{}; + GdkCursor* gdk_cursor_override{}; + + Observable minimum_size = Size{1, 1}; + Observable maximum_size = Size{G_MAXINT, G_MAXINT}; + Observable sys_min_size = Size{1, 1}; + Observable resizable{true}; + Observable view_position = Point{-1, -1}; + Observable view_size = Size{-1, -1}; + Observable window_size = Size{-1, -1}; + Observable window_location = Point{-1, -1}; + Observable window_extents = Rectangle{0, 0, 0, 0}; + bool needs_to_update_frame_extents{false}; + float gravity_x{0}; + float gravity_y{0}; + BoundsType width_type{BOUNDSTYPE_UNKNOWN}; + BoundsType height_type{BOUNDSTYPE_UNKNOWN}; /* * sm_grab_window points to WindowContext holding a mouse grab. @@ -223,143 +239,140 @@ class WindowContextBase: public WindowContext { * should be reported during this drag. */ static WindowContext* sm_mouse_drag_window; + public: + WindowContext() = delete; + WindowContext(jobject, WindowContext* _owner, long _screen, + WindowFrameType _frame_type, WindowType type, GdkWMFunction wmf); + + GETTER(jobject, jwindow) + GETTER(jobject, jview) + GETTER(WindowFrameType, frame_type); + GETTER(WindowType, window_type); + + Size get_view_size(); + Point get_view_position(); + bool isEnabled(); bool hasIME(); - bool filterIME(GdkEvent *); + bool filterIME(GdkEvent*); void enableOrResetIME(); void setOnPreEdit(bool); void commitIME(gchar *); void updateCaretPos(); void disableIME(); + void paint(void*, jint, jint); GdkWindow *get_gdk_window(); - jobject get_jwindow(); - jobject get_jview(); + XID get_native_window(); - void add_child(WindowContextTop*); - void remove_child(WindowContextTop*); + void add_child(WindowContext*); + void remove_child(WindowContext*); void set_visible(bool); bool is_visible(); + bool is_iconified(); bool is_resizable(); + bool is_maximized(); + bool is_fullscreen(); + bool is_floating(); bool set_view(jobject); bool grab_focus(); - bool grab_mouse_drag_focus(); void ungrab_focus(); - void ungrab_mouse_drag_focus(); void set_cursor(GdkCursor*); void set_cursor_override(GdkCursor*); - void set_level(int) {} void set_background(float, float, float); - void process_focus(GdkEventFocus*); - void process_destroy(); - void process_delete(); + void process_map(); void process_expose(GdkEventExpose*); - void process_mouse_button(GdkEventButton*, bool synthesized = false); - void process_mouse_motion(GdkEventMotion*); + void process_focus(GdkEventFocus*); + void process_focus(bool); + virtual void process_mouse_button(GdkEventButton*, bool synthesized = false); + virtual void process_mouse_motion(GdkEventMotion*); void process_mouse_scroll(GdkEventScroll*); void process_mouse_cross(GdkEventCrossing*); void process_key(GdkEventKey*); void process_state(GdkEventWindowState*); - - void notify_state(jint); + void process_property_notify(GdkEventProperty*); + void process_configure(GdkEventConfigure*); + void process_delete(); + void process_destroy(); void increment_events_counter(); void decrement_events_counter(); size_t get_events_count(); - bool get_window_edge(int x, int y, GdkWindowEdge*); bool is_dead(); - ~WindowContextBase(); -protected: - virtual void applyShapeMask(void*, uint width, uint height) = 0; -}; - -class WindowContextTop: public WindowContextBase { - jlong screen; - WindowFrameType frame_type; - WindowType window_type; - struct WindowContext *owner; - WindowGeometry geometry; - struct _Resizable {// we can't use set/get gtk_window_resizable function - _Resizable(): value(true), - minw(-1), minh(-1), maxw(-1), maxh(-1), sysminw(-1), sysminh(-1) {} - bool value; //actual value of resizable for a window - int minw, minh, maxw, maxh; //minimum and maximum window width/height; - int sysminw, sysminh; // size of window button area of EXTENDED windows - } resizable; - - bool on_top; - bool is_fullscreen; - - static WindowFrameExtents normal_extents; - static WindowFrameExtents utility_extents; - - WindowManager wmanager; -public: - WindowContextTop(jobject, WindowContext*, long, WindowFrameType, WindowType, GdkWMFunction); - - void process_realize(); - void process_property_notify(GdkEventProperty*); - void process_state(GdkEventWindowState*); - void process_configure(GdkEventConfigure*); - void process_destroy(); - void process_mouse_motion(GdkEventMotion*); - void process_mouse_button(GdkEventButton*, bool synthesized = false); - void work_around_compiz_state(); - - WindowGeometry get_geometry(); - void set_minimized(bool); void set_maximized(bool); void set_bounds(int, int, bool, bool, int, int, int, int, float, float); void set_resizable(bool); - bool is_resizable(); void request_focus(); void set_focusable(bool); void set_title(const char*); void set_alpha(double); void set_enabled(bool); - void set_system_minimum_size(int, int); void set_minimum_size(int, int); void set_maximum_size(int, int); void set_icon(GdkPixbuf*); void to_front(); void to_back(); - void set_modal(bool, WindowContext* parent = NULL); + void set_modal(bool, WindowContext* parent = nullptr); void set_level(int); - void set_visible(bool); - void notify_on_top(bool); + void set_owner(WindowContext*); void update_view_size(); - void notify_view_resize(); - - void show_system_menu(int x, int y); void enter_fullscreen(); void exit_fullscreen(); + void show_system_menu(int, int); + void set_system_minimum_size(int, int); - void set_owner(WindowContext*); - - GtkWindow *get_gtk_window(); - void detach_from_java(); + virtual ~WindowContext(); -protected: - void applyShapeMask(void*, uint width, uint height); private: + GdkVisual* find_best_visual(); + void maximize(bool); + void iconify(bool); + void update_window_size(); + void move_resize(int, int, bool, bool, int, int); + void add_wmf(GdkWMFunction); + void remove_wmf(GdkWMFunction); + void notify_on_top(bool); + void notify_fullscreen(bool); + void notify_window_resize(int); + void notify_window_move(); + void notify_view_resize(); + void notify_view_move(); + void notify_current_sizes(); + void notify_repaint(); + void notify_repaint(Rectangle); + GdkAtom get_net_frame_extents_atom(); void request_frame_extents(); void update_frame_extents(); - void set_cached_extents(WindowFrameExtents ex); - WindowFrameExtents get_cached_extents(); + void set_cached_extents(Rectangle); + void load_cached_extents(); bool get_frame_extents_property(int *, int *, int *, int *); void update_window_constraints(); void update_ontop_tree(bool); bool on_top_inherited(); bool effective_on_top(); - void notify_window_move(); - void notify_window_resize(); - bool get_window_edge(int x, int y, GdkWindowEdge*); - WindowContextTop(WindowContextTop&); - WindowContextTop& operator= (const WindowContextTop&); + + void update_initial_state(); + bool grab_mouse_drag_focus(); + void ungrab_mouse_drag_focus(); +}; + +class WindowContextExtended : public WindowContext { +public: + WindowContextExtended() = delete; + WindowContextExtended(jobject jwin, + WindowContext* owner, + long screen, + GdkWMFunction wmf); + + void process_mouse_button(GdkEventButton*, bool synthesized = false) override; + void process_mouse_motion(GdkEventMotion*) override; + +private: + bool get_window_edge(int, int, GdkWindowEdge*); }; void destroy_and_delete_ctx(WindowContext* ctx); @@ -378,9 +391,10 @@ class EventsCounterHelper { if (ctx != nullptr) { ctx->decrement_events_counter(); if (ctx->is_dead() && ctx->get_events_count() == 0) { + LOG("EventsCounterHelper: delete ctx\n"); delete ctx; } - ctx = NULL; + ctx = nullptr; } } }; diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window_ime.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window_ime.cpp index 0f1bcdccae5..a5f73d89e4c 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window_ime.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window_ime.cpp @@ -97,7 +97,7 @@ static gboolean on_retrieve_surrounding(GtkIMContext* self, gpointer user_data) return TRUE; } -void WindowContextBase::commitIME(gchar *str) { +void WindowContext::commitIME(gchar *str) { if (im_ctx.on_preedit || !im_ctx.on_key_event) { jstring jstr = mainEnv->NewStringUTF(str); EXCEPTION_OCCURED(mainEnv); @@ -115,11 +115,11 @@ void WindowContextBase::commitIME(gchar *str) { } } -bool WindowContextBase::hasIME() { +bool WindowContext::hasIME() { return im_ctx.enabled; } -bool WindowContextBase::filterIME(GdkEvent *event) { +bool WindowContext::filterIME(GdkEvent *event) { if (!hasIME()) { return false; } @@ -136,18 +136,18 @@ bool WindowContextBase::filterIME(GdkEvent *event) { return filtered; } -void WindowContextBase::setOnPreEdit(bool preedit) { +void WindowContext::setOnPreEdit(bool preedit) { im_ctx.on_preedit = preedit; } -void WindowContextBase::updateCaretPos() { +void WindowContext::updateCaretPos() { double *nativePos; jdoubleArray pos = (jdoubleArray)mainEnv->CallObjectMethod(get_jview(), jViewNotifyInputMethodCandidateRelativePosRequest, 0); - nativePos = mainEnv->GetDoubleArrayElements(pos, NULL); + nativePos = mainEnv->GetDoubleArrayElements(pos, nullptr); GdkRectangle rect; if (nativePos) { @@ -161,7 +161,7 @@ void WindowContextBase::updateCaretPos() { } } -void WindowContextBase::enableOrResetIME() { +void WindowContext::enableOrResetIME() { if (im_ctx.on_preedit) { gtk_im_context_focus_out(im_ctx.ctx); } @@ -185,10 +185,10 @@ void WindowContextBase::enableOrResetIME() { im_ctx.enabled = true; } -void WindowContextBase::disableIME() { - if (im_ctx.ctx != NULL) { +void WindowContext::disableIME() { + if (im_ctx.ctx != nullptr) { g_object_unref(im_ctx.ctx); - im_ctx.ctx = NULL; + im_ctx.ctx = nullptr; } im_ctx.enabled = false; diff --git a/tests/manual/stage/TestStage.java b/tests/manual/stage/TestStage.java new file mode 100644 index 00000000000..48d0a176331 --- /dev/null +++ b/tests/manual/stage/TestStage.java @@ -0,0 +1,729 @@ +/* + * 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. + */ +/* + * 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. + */ + +import javafx.application.Application; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.Event; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Rectangle2D; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TextField; +import javafx.scene.control.TitledPane; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.layout.HeaderBar; +import javafx.scene.layout.HeaderDragType; +import javafx.scene.paint.Color; +import javafx.scene.image.Image; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.StringConverter; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.DoubleConsumer; + +public class TestStage extends Application { + private final ObservableList stages = FXCollections.observableArrayList(); + private int stageCounter = 0; + + private final ComboBox cbStageStyle = + new ComboBox<>(FXCollections.observableArrayList(StageStyle.values())); + private final ComboBox cbModality = new ComboBox<>(FXCollections.observableArrayList(Modality.values())); + private final ComboBox cbOwner = new ComboBox<>(); + + private final Button btnToFront = new Button("To Front"); + private final Button btnToBack = new Button("To Back"); + private final Button btnCreate = new Button("Create"); + private final Button btnCreateShow = new Button("Create/Show"); + private final Button btnSelectLast = new Button("Select Last"); + private final Button btnSelectPrevious = new Button("â—€"); + private final Button btnSelectNone = new Button("None"); + private final Button btnSelectNext = new Button("â–¶"); + private final Button btnHide = new Button("Hide/Close"); + private final Button btnShow = new Button("Show"); + private final Button btnSizeToScene = new Button("Size to Scene"); + private final Button btnCenterOnScreen = new Button("Center on Screen"); + private final Button btnFocus = new Button("Focus"); + private final PropertyEditor propertyEditor = new PropertyEditor(); + + private final ObjectProperty initStyle = new SimpleObjectProperty<>(StageStyle.DECORATED); + private final ObjectProperty initModality = new SimpleObjectProperty<>(Modality.NONE); + private final ObjectProperty initOwner = new SimpleObjectProperty<>(null); + private Stage currentStage = null; + + private static final double MAX_WIDTH = 7680; + private static final double MAX_HEIGHT = 4320; + + private void updateCommandButtonsState() { + boolean disabled = stages.isEmpty() || currentStage == null; + + btnShow.setDisable(disabled); + btnHide.setDisable(disabled); + btnSizeToScene.setDisable(disabled); + btnCenterOnScreen.setDisable(disabled); + btnToFront.setDisable(disabled); + btnToBack.setDisable(disabled); + btnFocus.setDisable(disabled); + btnSelectNone.setDisable(disabled); + + btnSelectLast.setDisable(stages.isEmpty() || currentStage == stages.getLast()); + btnSelectNext.setDisable(stages.isEmpty() || currentStage == null || currentStage == stages.getLast()); + btnSelectPrevious.setDisable(stages.isEmpty() || currentStage == null || currentStage == stages.getFirst()); + } + + private void updateBindings() { + if (currentStage == null) { + propertyEditor.unbind(); + } else { + propertyEditor.bindToStage(currentStage); + } + } + + private final CheckBox cbCommandAlwaysOnTop = new CheckBox("Command Always On Top"); + + @Override + public void start(Stage stage) { + cbStageStyle.getSelectionModel().select(StageStyle.DECORATED); + cbModality.getSelectionModel().select(Modality.NONE); + cbOwner.itemsProperty().bind(Bindings.createObjectBinding(() -> { + ObservableList listWithNull = FXCollections.observableArrayList(); + listWithNull.add(null); + listWithNull.addAll(stages); + return listWithNull; + }, stages)); + + cbOwner.setConverter(new StringConverter<>() { + @Override + public String toString(Stage stage) { + if (stage == null) { + return "None"; + } + + return stage.getTitle(); + } + + @Override + public Stage fromString(String string) { + return null; + } + }); + + initStyle.bind(cbStageStyle.valueProperty()); + initModality.bind(cbModality.valueProperty()); + initOwner.bind(cbOwner.valueProperty()); + + btnToFront.setOnAction(e -> { + if (currentStage != null) { + currentStage.toFront(); + } + }); + + btnToBack.setOnAction(e -> { + if (currentStage != null) { + currentStage.toBack(); + } + }); + + btnCreate.setOnAction(e -> createTestStage()); + + btnCreateShow.setOnAction(e -> { + createTestStage(); + currentStage.show(); + }); + + btnSelectNone.setOnAction(e -> { + currentStage = null; + updateBindings(); + updateCommandButtonsState(); + }); + + btnSelectLast.setOnAction(e -> { + currentStage = stages.get(stages.size() - 1); + updateBindings(); + updateCommandButtonsState(); + }); + + btnSelectNext.setOnAction(e -> { + if (!stages.isEmpty()) { + int index = stages.indexOf(currentStage); + if (index < stages.size() - 1) { + currentStage = stages.get(index + 1); + updateBindings(); + updateCommandButtonsState(); + } + } + }); + + btnSelectPrevious.setOnAction(e -> { + if (!stages.isEmpty()) { + int index = stages.indexOf(currentStage); + if (index > 0) { + currentStage = stages.get(index - 1); + updateBindings(); + updateCommandButtonsState(); + } + } + }); + + btnHide.setOnAction(e -> { + if (currentStage != null) { + boolean isShowing = currentStage.isShowing(); + currentStage.hide(); + + if (!isShowing) { + stages.remove(currentStage); + currentStage = stages.isEmpty() ? null : stages.getLast(); + updateCommandButtonsState(); + updateBindings(); + } + } + }); + + btnShow.setOnAction(e -> { + if (currentStage != null) { + currentStage.show(); + } + }); + + btnSizeToScene.setOnAction(e -> { + if (currentStage != null) { + currentStage.sizeToScene(); + } + }); + + btnCenterOnScreen.setOnAction(e -> { + if (currentStage != null) { + currentStage.centerOnScreen(); + } + }); + + btnFocus.setOnAction(e -> { + if (currentStage != null) { + currentStage.requestFocus(); + } + }); + + updateCommandButtonsState(); + + FlowPane flow0 = new FlowPane(label("Style: ", cbStageStyle), label("Modality: ", cbModality), + label("Owner: ", cbOwner)); + FlowPane flow1 = new FlowPane(btnCreate, btnShow, btnCreateShow, btnHide); + FlowPane flow2 = new FlowPane(btnCenterOnScreen, btnSizeToScene); + FlowPane flow3 = new FlowPane(btnToFront, btnToBack, btnSelectNone, btnSelectLast, btnSelectPrevious, + btnSelectNext, btnFocus); + + List.of(flow0, flow1, flow2, flow3).forEach(flow -> { + flow.setHgap(5); + flow.setVgap(5); + }); + + VBox commandPane = new VBox(cbCommandAlwaysOnTop, flow0, flow1, flow2, flow3); + commandPane.setSpacing(5); + commandPane.setFillWidth(true); + + TitledPane commandPaneTitledPane = new TitledPane("Commands", commandPane); + commandPaneTitledPane.setCollapsible(false); + + TitledPane editorTitledPane = new TitledPane("Properties", propertyEditor); + editorTitledPane.setCollapsible(false); + + VBox root = new VBox( + commandPaneTitledPane, + editorTitledPane + ); + root.setSpacing(5); + root.setFillWidth(true); + + + Scene scene = new Scene(root); + stage.setTitle("Command Stage"); + stage.setScene(scene); + stage.setOnShown(e -> { + Rectangle2D stageBounds = new Rectangle2D( + stage.getX(), + stage.getY(), + stage.getWidth(), + stage.getHeight() + ); + + Screen currentScreen = Screen.getScreens() + .stream() + .filter(screen -> screen.getVisualBounds().intersects(stageBounds)) + .findFirst() + .orElse(Screen.getPrimary()); + + Rectangle2D visualBounds = currentScreen.getVisualBounds(); + stage.setHeight(visualBounds.getHeight()); + + double x = visualBounds.getMaxX() - stage.getWidth(); + double y = visualBounds.getMaxY() - stage.getHeight(); + + stage.setX(x); + stage.setY(y); + }); + stage.setWidth(500); + stage.show(); + } + + private HBox label(String label, Control control) { + HBox hbox = new HBox(new Label(label), control); + hbox.setSpacing(5); + hbox.setAlignment(Pos.CENTER_LEFT); + return hbox; + } + + private void createTestStage() { + Stage newStage = new Stage(); + + stageCounter++; + + newStage.initStyle(initStyle.getValue()); + newStage.initModality(initModality.getValue()); + newStage.initOwner(initOwner.getValue()); + newStage.setTitle("Test Stage " + stageCounter); + + newStage.focusedProperty().addListener((obs, oldVal, newVal) -> { + if (newVal) { + currentStage = newStage; + updateBindings(); + updateCommandButtonsState(); + } + }); + + stages.add(newStage); + currentStage = newStage; + createDefaultScene(); + + newStage.setOnHidden(e -> { + stages.remove(newStage); + if (currentStage == newStage) { + currentStage = stages.isEmpty() ? null : stages.getLast(); + updateBindings(); + } + updateCommandButtonsState(); + }); + + updateBindings(); + updateCommandButtonsState(); + } + + public static void main(String[] args) { + launch(TestStage.class, args); + } + + private Label createLabel(String prefix, ReadOnlyProperty property) { + Label label = new Label(); + label.textProperty().bind(Bindings.concat(prefix, Bindings.convert(property))); + return label; + } + + private void createDefaultScene() { + Scene scene; + + StringProperty lastEvent = new SimpleStringProperty(); + + Label ownerLabel = new Label("Owner: NONE"); + if (currentStage.getOwner() instanceof Stage owner) { + ownerLabel.setText("Owner: " + owner.getTitle()); + } + + VBox root = new VBox(); + if (currentStage.getStyle() == StageStyle.EXTENDED) { + HeaderBar headerbar = new HeaderBar(); + Label headerLabel = new Label(); + var headerPane = new StackPane(headerLabel); + headerLabel.textProperty().bind(currentStage.titleProperty()); + headerbar.setCenter(headerPane); + headerbar.setDragType(headerPane, HeaderDragType.DRAGGABLE_SUBTREE); + root.getChildren().add(headerbar); + } + + root.getChildren().addAll(createLabel("Focused: ", currentStage.focusedProperty()), + new Label("Modality: " + currentStage.getModality()), + ownerLabel, + createLabel("Last Event: ", lastEvent)); + root.setBackground(Background.EMPTY); + + if (currentStage.getStyle() == StageStyle.TRANSPARENT) { + BackgroundFill fill = new BackgroundFill( + Color.HOTPINK.deriveColor(0, 1, 1, 0.5), + CornerRadii.EMPTY, + Insets.EMPTY + ); + root.setBackground(new Background(fill)); + + scene = new Scene(root, 300, 300); + scene.setFill(Color.TRANSPARENT); + } else { + scene = new Scene(root, 300, 300, Color.HOTPINK); + } + + currentStage.addEventHandler(Event.ANY, e -> lastEvent.set(e.getEventType().getName())); + setupContextMenu(root); + currentStage.setScene(scene); + } + + private void createSceneWithTextField() { + StackPane root = new StackPane(); + + TextField textField = new TextField(); + textField.setPromptText("Enter text here"); + + root.getChildren().add(textField); + setupContextMenu(root); + Scene scene = new Scene(root, 300, 200); + + currentStage.setScene(scene); + } + + private void createSceneWithTooltipBox() { + StackPane root = new StackPane(); + + StackPane coloredBox = new StackPane(); + coloredBox.setBackground(Background.fill(Color.CORNFLOWERBLUE)); + + Tooltip tooltip = new Tooltip("The quick brown fox jumps over the lazy dog."); + Tooltip.install(coloredBox, tooltip); + root.getChildren().add(coloredBox); + setupContextMenu(root); + Scene scene = new Scene(root, 300, 200); + currentStage.setScene(scene); + } + + private void createAlert(boolean windowModal) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Alert"); + alert.setHeaderText("The quick brown fox jumps over the lazy dog."); + + if (windowModal) { + alert.initModality(Modality.WINDOW_MODAL); + alert.initOwner(currentStage); + } + + alert.showAndWait(); + } + + private void createFileOpen() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open Resource File"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("All Files", "*.*") + ); + File file = fileChooser.showOpenDialog(currentStage); + + if (file != null) { + new Alert(Alert.AlertType.INFORMATION, "File selected: " + file.getAbsolutePath()).showAndWait(); + } else { + new Alert(Alert.AlertType.WARNING, "No file selected").showAndWait(); + } + } + + private void setStageIcon() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Choose Icon Image"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg", "*.gif") + ); + + File file = fileChooser.showOpenDialog(currentStage); + if (file != null) { + Image icon = new Image(file.toURI().toString()); + currentStage.getIcons().clear(); + currentStage.getIcons().add(icon); + } + } + + private void setupContextMenu(Node root) { + ContextMenu contextMenu = new ContextMenu(); + + MenuItem defaultSceneMenuItem = new MenuItem("Default Scene"); + defaultSceneMenuItem.setOnAction(e -> createDefaultScene()); + MenuItem textFieldMenuItem = new MenuItem("Scene with TextField"); + textFieldMenuItem.setOnAction(e -> createSceneWithTextField()); + MenuItem tooltipBoxMenuItem = new MenuItem("Scene with Tooltip Box"); + tooltipBoxMenuItem.setOnAction(e -> createSceneWithTooltipBox()); + MenuItem alertMenuItem = new MenuItem("Alert - Application Modal"); + alertMenuItem.setOnAction(e -> createAlert(false)); + MenuItem alertWindowModalMenuItem = new MenuItem("Alert - Window Modal"); + alertWindowModalMenuItem.setOnAction(e -> createAlert(true)); + MenuItem fileOpenMenuItem = new MenuItem("File Open"); + fileOpenMenuItem.setOnAction(e -> createFileOpen()); + MenuItem iconMenuItem = new MenuItem("Set Stage Icon"); + iconMenuItem.setOnAction(e -> setStageIcon()); + + contextMenu.getItems().addAll(defaultSceneMenuItem, textFieldMenuItem, tooltipBoxMenuItem, + alertMenuItem, alertWindowModalMenuItem, fileOpenMenuItem, iconMenuItem); + root.setOnContextMenuRequested(e -> contextMenu.show(root, e.getScreenX(), e.getScreenY())); + + root.setOnMousePressed(e -> { + if (contextMenu.isShowing()) { + contextMenu.hide(); + } + }); + } + + class PropertyEditor extends VBox { + private final PropertyEditorPane stagePane = new PropertyEditorPane("Stage"); + private final PropertyEditorPane scenePane = new PropertyEditorPane("Scene"); + + private final ObjectProperty sceneProperty = new SimpleObjectProperty<>(); + + PropertyEditor() { + getChildren().addAll(stagePane, scenePane); + stagePane.setMaxHeight(550); + setFillWidth(true); + } + + public void bindToStage(Stage stage) { + unbind(); + + stagePane.addStringProperty("Title", stage.titleProperty(), stage::setTitle); + stagePane.addBooleanProperty("Always OnTop", stage.alwaysOnTopProperty(), stage::setAlwaysOnTop); + stagePane.addBooleanProperty("FullScreen", stage.fullScreenProperty(), stage::setFullScreen); + stagePane.addBooleanProperty("Maximized", stage.maximizedProperty(), stage::setMaximized); + stagePane.addBooleanProperty("Iconified", stage.iconifiedProperty(), stage::setIconified); + stagePane.addBooleanProperty("Resizeable", stage.resizableProperty(), stage::setResizable); + stagePane.addBooleanProperty("Focused", stage.focusedProperty(), null); + stagePane.addDoublePropery("X", stage.xProperty(), stage::setX, 0, MAX_WIDTH * 2, 1.0); + stagePane.addDoublePropery("Y", stage.yProperty(), stage::setY, 0, MAX_HEIGHT * 2, 1.0); + stagePane.addDoublePropery("Width", stage.widthProperty(), stage::setWidth, 1, MAX_WIDTH, 1.0); + stagePane.addDoublePropery("Height", stage.heightProperty(), stage::setHeight, 1, MAX_HEIGHT, 1.0); + stagePane.addDoublePropery("Min Width", stage.minWidthProperty(), stage::setMinWidth, 1, MAX_WIDTH, 1.0); + stagePane.addDoublePropery("Min Height", stage.minHeightProperty(), stage::setMinHeight, 1, MAX_HEIGHT, + 1.0); + stagePane.addDoublePropery("Max Width", stage.maxWidthProperty(), stage::setMaxWidth, 1, Double.MAX_VALUE, + 1.0); + stagePane.addDoublePropery("Max Height", stage.maxHeightProperty(), stage::setMaxHeight, 1, + Double.MAX_VALUE, 1.0); + stagePane.addDoublePropery("RenderScale X", stage.renderScaleXProperty(), stage::setRenderScaleX, 0, 2, + 0.25); + stagePane.addDoublePropery("RenderScale Y", stage.renderScaleYProperty(), stage::setRenderScaleY, 0, 2, + 0.25); + stagePane.addDoublePropery("Opacity", stage.opacityProperty(), stage::setOpacity, 0, 1, 0.1); + + sceneProperty.bind(stage.sceneProperty()); + bindScene(stage.getScene()); + + sceneProperty.addListener((obs, oldScene, newScene) -> { + if (newScene != null) { + bindScene(newScene); + } + }); + } + + private void bindScene(Scene scene) { + scenePane.unbind(); + scenePane.addDoubleLabelProperty("X", scene.xProperty()); + scenePane.addDoubleLabelProperty("Y", scene.yProperty()); + scenePane.addDoubleLabelProperty("Width", scene.widthProperty()); + scenePane.addDoubleLabelProperty("Height", scene.heightProperty()); + } + + public void unbind() { + scenePane.unbind(); + stagePane.unbind(); + } + } + + class PropertyEditorPane extends TitledPane { + private int currentRow = 0; + private final List clearChangeListeners = new ArrayList<>(); + private final GridPane gridPane = new GridPane(); + + PropertyEditorPane(String title) { + setText(title); + ScrollPane propertiesScrollPane = new ScrollPane(propertyEditor); + propertiesScrollPane.setFitToWidth(true); + propertiesScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + propertiesScrollPane.setContent(gridPane); + + gridPane.setHgap(5); + gridPane.setVgap(5); + gridPane.setPadding(new Insets(10)); + + setContent(propertiesScrollPane); + } + + private void addListener(ReadOnlyProperty property, ChangeListener changeListener) { + property.addListener(changeListener); + clearChangeListeners.add(() -> property.removeListener(changeListener)); + } + + private void addLabel(String label) { + Label lbl = new Label(label); + gridPane.add(lbl, 0, currentRow); + GridPane.setHgrow(lbl, Priority.SOMETIMES); + GridPane.setHalignment(lbl, HPos.RIGHT); + } + + public void addDoubleLabelProperty(String label, ReadOnlyDoubleProperty property) { + addLabel(label); + Label lbl = new Label(); + lbl.textProperty().bind(property.asString("%.2f")); + gridPane.add(lbl, 1, currentRow); + GridPane.setHgrow(lbl, Priority.ALWAYS); + currentRow++; + } + + public void addDoublePropery(String label, ReadOnlyDoubleProperty property, DoubleConsumer setConsumer, + double min, double max, + double amountToStepBy) { + addLabel(label); + Spinner spinner = new Spinner<>(); + spinner.setEditable(true); + SpinnerValueFactory.DoubleSpinnerValueFactory spinnerValueFactory = + new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, property.get(), amountToStepBy); + spinner.setValueFactory(spinnerValueFactory); + gridPane.add(spinner, 1, currentRow); + GridPane.setHgrow(spinner, Priority.ALWAYS); + + AtomicBoolean suppressListener = new AtomicBoolean(false); + addListener(property, (obs, oldValue, newValue) -> { + if (!newValue.equals(spinner.getValue())) { + try { + suppressListener.set(true); + spinnerValueFactory.setValue((Double) newValue); + } finally { + suppressListener.set(false); + } + } + }); + + if (setConsumer != null) { + spinner.valueProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.equals(oldValue) && !suppressListener.get()) { + setConsumer.accept(newValue); + } + }); + } else { + spinner.setDisable(true); + } + + currentRow++; + } + + public void addStringProperty(String label, ReadOnlyStringProperty property, Consumer setConsumer) { + addLabel(label); + TextField textField = new TextField(property.get()); + gridPane.add(textField, 1, currentRow); + GridPane.setHgrow(textField, Priority.ALWAYS); + + addListener(property, (obs, oldValue, newValue) -> textField.setText(newValue)); + + if (setConsumer != null) { + textField.setOnAction(e -> setConsumer.accept(textField.getText())); + } else { + textField.setDisable(true); + } + currentRow++; + } + + public void addBooleanProperty(String label, ReadOnlyBooleanProperty property, Consumer setConsumer) { + addLabel(label); + CheckBox checkBox = new CheckBox(); + checkBox.setSelected(property.get()); + gridPane.add(checkBox, 1, currentRow); + + addListener(property, (obs, oldValue, newValue) -> checkBox.setSelected(newValue)); + + if (setConsumer != null) { + checkBox.setOnAction(e -> setConsumer.accept(checkBox.isSelected())); + } else { + checkBox.setDisable(true); + } + currentRow++; + } + + public void unbind() { + clearChangeListeners.forEach(Runnable::run); + gridPane.getChildren().clear(); + currentRow = 0; + } + } +} diff --git a/tests/system/src/test/java/test/javafx/scene/RestoreSceneSizeTest.java b/tests/system/src/test/java/test/javafx/scene/RestoreSceneSizeTest.java index 00855cc21f8..1fffad3b406 100644 --- a/tests/system/src/test/java/test/javafx/scene/RestoreSceneSizeTest.java +++ b/tests/system/src/test/java/test/javafx/scene/RestoreSceneSizeTest.java @@ -24,9 +24,6 @@ */ package test.javafx.scene; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.value.ChangeListener; @@ -38,9 +35,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import com.sun.javafx.PlatformUtil; import test.util.Util; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + public class RestoreSceneSizeTest { static CountDownLatch startupLatch = new CountDownLatch(1); static Stage stage; @@ -90,9 +89,6 @@ public static void teardown() { @Test public void testUnfullscreenSize() throws Exception { - // Disable on Linux until JDK-8353556 is fixed - assumeTrue(!PlatformUtil.isLinux()); - Thread.sleep(200); final double w = (Math.ceil(WIDTH * scaleX)) / scaleX; final double h = (Math.ceil(HEIGHT * scaleY)) / scaleY; diff --git a/tests/system/src/test/java/test/javafx/stage/CenterOnScreenTest.java b/tests/system/src/test/java/test/javafx/stage/CenterOnScreenTest.java new file mode 100644 index 00000000000..7b6d0600fa4 --- /dev/null +++ b/tests/system/src/test/java/test/javafx/stage/CenterOnScreenTest.java @@ -0,0 +1,124 @@ +/* + * 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.javafx.stage; + +import javafx.geometry.Rectangle2D; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.stage.Screen; +import javafx.stage.StageStyle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import test.util.Util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; + +class CenterOnScreenTest extends StageTestBase { + private static final float CENTER_ON_SCREEN_X_FRACTION = 1.0f / 2; + private static final float CENTER_ON_SCREEN_Y_FRACTION = 1.0f / 3; + + private static final double STAGE_WIDTH = 400; + private static final double STAGE_HEIGHT = 200; + + // Must be cointained in Stage dimensions + private static final double SCENE_WIDTH = 300; + private static final double SCENE_HEIGHT = 100; + + private static final double DECORATED_DELTA = 50.0; + + @Override + protected Region createRoot() { + StackPane stackPane = new StackPane(); + stackPane.setPrefSize(SCENE_WIDTH, SCENE_HEIGHT); + return stackPane; + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "EXTENDED", "UNDECORATED", "TRANSPARENT"}) + void centerOnScreenWhenShown(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, stage -> { + stage.setWidth(STAGE_WIDTH); + stage.setHeight(STAGE_HEIGHT); + }); + Util.sleep(MEDIUM_WAIT); + assertStageCentered(stageStyle, false); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "EXTENDED", "UNDECORATED", "TRANSPARENT"}) + void centerOnScreenWhenShownWithSceneSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, null); + Util.sleep(MEDIUM_WAIT); + assertStageCentered(stageStyle, true); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "EXTENDED", "UNDECORATED", "TRANSPARENT"}) + void centerOnScreenAfterShown(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, stage -> { + stage.setWidth(STAGE_WIDTH); + stage.setHeight(STAGE_HEIGHT); + stage.setX(0); + stage.setY(0); + }); + + Util.sleep(MEDIUM_WAIT); + Util.runAndWait(() -> getStage().centerOnScreen()); + Util.sleep(MEDIUM_WAIT); + assertStageCentered(stageStyle, false); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "EXTENDED", "UNDECORATED", "TRANSPARENT"}) + void centerOnScreenAfterShownWithSceneSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, stage -> { + stage.setX(0); + stage.setY(0); + }); + + Util.sleep(MEDIUM_WAIT); + Util.runAndWait(() -> getStage().centerOnScreen()); + Util.sleep(MEDIUM_WAIT); + assertStageCentered(stageStyle, true); + } + + private void assertStageCentered(StageStyle stageStyle, boolean useSceneSize) { + Screen screen = Util.getScreen(getStage()); + + double delta = (stageStyle == StageStyle.DECORATED) ? DECORATED_DELTA : SIZING_DELTA; + + Rectangle2D bounds = screen.getVisualBounds(); + double centerX = + bounds.getMinX() + (bounds.getWidth() - ((useSceneSize) ? SCENE_WIDTH : STAGE_WIDTH)) + * CENTER_ON_SCREEN_X_FRACTION; + double centerY = + bounds.getMinY() + (bounds.getHeight() - ((useSceneSize) ? SCENE_HEIGHT : STAGE_HEIGHT)) + * CENTER_ON_SCREEN_Y_FRACTION; + + assertEquals(centerX, getStage().getX(), delta, "Stage is not centered in X axis"); + assertEquals(centerY, getStage().getY(), delta, "Stage is not centered in Y axis"); + } +} diff --git a/tests/system/src/test/java/test/javafx/stage/FullScreenTest.java b/tests/system/src/test/java/test/javafx/stage/FullScreenTest.java new file mode 100644 index 00000000000..c8219a69e44 --- /dev/null +++ b/tests/system/src/test/java/test/javafx/stage/FullScreenTest.java @@ -0,0 +1,87 @@ +/* + * 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.javafx.stage; + +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import test.util.Util; + +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; + +class FullScreenTest extends StageTestBase { + private static final int POS_X = 100; + private static final int POS_Y = 150; + private static final int WIDTH = 200; + private static final int HEIGHT = 250; + + private static final Consumer TEST_SETTINGS = s -> { + s.setWidth(WIDTH); + s.setHeight(HEIGHT); + s.setX(POS_X); + s.setY(POS_Y); + }; + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT"}) + void fullScreenShouldKeepGeometryOnRestore(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, TEST_SETTINGS); + + Util.doTimeLine(LONG_WAIT, + () -> getStage().setFullScreen(true), + () -> assertTrue(getStage().isFullScreen()), + () -> getStage().setFullScreen(false)); + + Util.sleep(LONG_WAIT); + assertSizePosition(); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT"}) + void fullScreenBeforeShowShouldKeepGeometryOnRestore(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, TEST_SETTINGS.andThen(s -> s.setFullScreen(true))); + + Util.sleep(LONG_WAIT); + Util.runAndWait(() -> { + assertTrue(getStage().isFullScreen()); + getStage().setFullScreen(false); + }); + + Util.sleep(LONG_WAIT); + assertSizePosition(); + } + + private void assertSizePosition() { + assertEquals(WIDTH, getStage().getWidth(), SIZING_DELTA, "Stage's width should have remained"); + assertEquals(HEIGHT, getStage().getHeight(), SIZING_DELTA, "Stage's height should have remained"); + assertEquals(POS_X, getStage().getX(), POSITION_DELTA, "Stage's X position should have remained"); + assertEquals(POS_Y, getStage().getY(), POSITION_DELTA, "Stage's Y position should have remained"); + } +} diff --git a/tests/system/src/test/java/test/javafx/stage/MaximizeTest.java b/tests/system/src/test/java/test/javafx/stage/MaximizeTest.java new file mode 100644 index 00000000000..75a15b1efb6 --- /dev/null +++ b/tests/system/src/test/java/test/javafx/stage/MaximizeTest.java @@ -0,0 +1,107 @@ +/* + * 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.javafx.stage; + +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import test.util.Util; + +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; + +class MaximizeTest extends StageTestBase { + private static final int WIDTH = 300; + private static final int HEIGHT = 300; + private static final int POS_X = 100; + private static final int POS_Y = 150; + + private static final Consumer TEST_SETTINGS = s -> { + s.setWidth(WIDTH); + s.setHeight(HEIGHT); + s.setX(POS_X); + s.setY(POS_Y); + }; + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"UNDECORATED", "EXTENDED", "TRANSPARENT"}) + void maximizeUndecorated(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, TEST_SETTINGS); + + Util.doTimeLine(MEDIUM_WAIT, + () -> getStage().setMaximized(true), + () -> { + assertTrue(getStage().isMaximized()); + assertNotEquals(POS_X, getStage().getX()); + assertNotEquals(POS_Y, getStage().getY()); + }, + () -> getStage().setMaximized(false)); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(POS_X, getStage().getX(), POSITION_DELTA, "Stage maximized position changed"); + assertEquals(POS_Y, getStage().getY(), POSITION_DELTA, "Stage maximized position changed"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT"}) + void maximizeShouldKeepGeometryOnRestore(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, TEST_SETTINGS); + + Util.doTimeLine(MEDIUM_WAIT, + () -> getStage().setMaximized(true), + () -> assertTrue(getStage().isMaximized()), + () -> getStage().setMaximized(false)); + + Util.sleep(MEDIUM_WAIT); + assertSizePosition(); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT"}) + void maximizeBeforeShowShouldKeepGeometryOnRestore(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, TEST_SETTINGS.andThen(s -> s.setMaximized(true))); + Util.sleep(MEDIUM_WAIT); + + Util.runAndWait(() -> { + assertTrue(getStage().isMaximized()); + getStage().setMaximized(false); + }); + Util.sleep(MEDIUM_WAIT); + assertSizePosition(); + } + + private void assertSizePosition() { + assertEquals(WIDTH, getStage().getWidth(), SIZING_DELTA, "Stage's width should have remained"); + assertEquals(HEIGHT, getStage().getHeight(), SIZING_DELTA, "Stage's height should have remained"); + assertEquals(POS_X, getStage().getX(), POSITION_DELTA, "Stage's X position should have remained"); + assertEquals(POS_Y, getStage().getY(), POSITION_DELTA, "Stage's Y position should have remained"); + } +} diff --git a/tests/system/src/test/java/test/javafx/stage/MaximizeUndecorated.java b/tests/system/src/test/java/test/javafx/stage/MaximizeUndecorated.java deleted file mode 100644 index dc389361153..00000000000 --- a/tests/system/src/test/java/test/javafx/stage/MaximizeUndecorated.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2020, 2024, 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.javafx.stage; - -import java.util.concurrent.CountDownLatch; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.scene.Group; -import javafx.scene.Scene; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.stage.WindowEvent; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import test.util.Util; - -public class MaximizeUndecorated { - static CountDownLatch startupLatch = new CountDownLatch(1); - static Stage stage; - static final int POS = 500; - - public static class TestApp extends Application { - @Override - public void start(Stage primaryStage) throws Exception { - primaryStage.setScene(new Scene(new Group())); - stage = primaryStage; - stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e -> - Platform.runLater(startupLatch::countDown)); - stage.initStyle(StageStyle.UNDECORATED); - stage.setX(POS); - stage.setY(POS); - stage.setOnShown(e -> stage.setMaximized(true)); - stage.show(); - } - } - - @BeforeAll - public static void initFX() { - Util.launch(startupLatch, TestApp.class); - } - - @AfterAll - public static void teardown() { - Util.shutdown(); - } - - @Test - public void testMaximize() throws Exception { - Util.sleep(200); - - boolean movedToTopCorner = stage.getY() != POS && stage.getX() != POS; - Assertions.assertTrue(movedToTopCorner, "Stage has moved to desktop top corner"); - } -} diff --git a/tests/system/src/test/java/test/javafx/stage/SizeToSceneTest.java b/tests/system/src/test/java/test/javafx/stage/SizeToSceneTest.java index 042a2eee238..69c6ab4fb8f 100644 --- a/tests/system/src/test/java/test/javafx/stage/SizeToSceneTest.java +++ b/tests/system/src/test/java/test/javafx/stage/SizeToSceneTest.java @@ -120,7 +120,6 @@ private void createAndShowStage(Consumer stageConsumer) { @Test void testInitialSizeOnMaximizedThenSizeToScene() { - assumeTrue(!PlatformUtil.isLinux()); // JDK-8353612 createAndShowStage(stage -> { stage.setMaximized(true); stage.sizeToScene(); @@ -132,7 +131,6 @@ void testInitialSizeOnMaximizedThenSizeToScene() { @Test void testInitialSizeOnFullscreenThenSizeToScene() { - assumeTrue(!PlatformUtil.isLinux()); // JDK-8353612 createAndShowStage(stage -> { stage.setFullScreen(true); stage.sizeToScene(); @@ -144,7 +142,6 @@ void testInitialSizeOnFullscreenThenSizeToScene() { @Test void testInitialSizeOnSizeToSceneThenMaximized() { - assumeTrue(!PlatformUtil.isLinux()); // JDK-8353612 createAndShowStage(stage -> { stage.sizeToScene(); stage.setMaximized(true); @@ -156,7 +153,6 @@ void testInitialSizeOnSizeToSceneThenMaximized() { @Test void testInitialSizeOnSizeToSceneThenFullscreen() { - assumeTrue(!PlatformUtil.isLinux()); // JDK-8353612 createAndShowStage(stage -> { stage.sizeToScene(); stage.setFullScreen(true); @@ -168,7 +164,6 @@ void testInitialSizeOnSizeToSceneThenFullscreen() { @Test void testInitialSizeAfterShowSizeToSceneThenFullscreen() { - assumeTrue(!PlatformUtil.isLinux()); // JDK-8353612 createAndShowStage(stage -> { stage.show(); @@ -181,7 +176,6 @@ void testInitialSizeAfterShowSizeToSceneThenFullscreen() { @Test void testInitialSizeAfterShowSizeToSceneThenMaximized() { - assumeTrue(!PlatformUtil.isLinux()); // JDK-8353612 createAndShowStage(stage -> { stage.show(); @@ -194,7 +188,6 @@ void testInitialSizeAfterShowSizeToSceneThenMaximized() { @Test void testInitialSizeAfterShowFullscreenThenSizeToScene() { - assumeTrue(!PlatformUtil.isLinux()); // JDK-8353612 createAndShowStage(stage -> { stage.show(); @@ -207,7 +200,6 @@ void testInitialSizeAfterShowFullscreenThenSizeToScene() { @Test void testInitialSizeAfterShowMaximizedThenSizeToScene() { - assumeTrue(!PlatformUtil.isLinux()); // JDK-8353612 createAndShowStage(stage -> { stage.show(); diff --git a/tests/system/src/test/java/test/javafx/stage/SizingTest.java b/tests/system/src/test/java/test/javafx/stage/SizingTest.java new file mode 100644 index 00000000000..5ea499e57f2 --- /dev/null +++ b/tests/system/src/test/java/test/javafx/stage/SizingTest.java @@ -0,0 +1,314 @@ +/* + * 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.javafx.stage; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.layout.Background; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.StageStyle; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import test.util.Util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; + +class SizingTest extends StageTestBase { + private static final int WIDTH = 300; + private static final int HEIGHT = 300; + private static final int MAX_WIDTH = 350; + private static final int MAX_HEIGHT = 350; + private static final int MIN_WIDTH = 500; + private static final int MIN_HEIGHT = 500; + private static final int NEW_WIDTH = 450; + private static final int NEW_HEIGHT = 450; + + protected Label createLabel(String prefix, ReadOnlyDoubleProperty property) { + Label label = new Label(); + label.textProperty().bind(Bindings.concat(prefix, Bindings.convert(property))); + return label; + } + + @Override + protected Region createRoot() { + VBox vBox = new VBox(createLabel("Width: ", getStage().widthProperty()), + createLabel("Height: ", getStage().heightProperty()), + createLabel("Max Width: ", getStage().maxWidthProperty()), + createLabel("Max Height: ", getStage().maxHeightProperty()), + createLabel("Min Width: ", getStage().minWidthProperty()), + createLabel("Min Height: ", getStage().minHeightProperty())); + vBox.setBackground(Background.EMPTY); + + return vBox; + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void maxSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.setMaxWidth(MAX_WIDTH); + s.setMaxHeight(MAX_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + Util.runAndWait(() -> { + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(MAX_WIDTH, getStage().getWidth(), SIZING_DELTA, + "Stage width should have been limited to max width"); + assertEquals(MAX_HEIGHT, getStage().getHeight(), SIZING_DELTA, + "Stage height should have been limited to max height"); + + // Reset it + Util.runAndWait(() -> { + getStage().setMaxWidth(Double.MAX_VALUE); + getStage().setMaxHeight(Double.MAX_VALUE); + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(NEW_WIDTH, getStage().getWidth(), SIZING_DELTA, + "Stage width should have been accepted after removing min width"); + assertEquals(NEW_HEIGHT, getStage().getHeight(), SIZING_DELTA, + "Stage height should have been accepted after removing min height"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void maxWidth(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + s.setMaxWidth(MAX_WIDTH); + }); + + Util.sleep(MEDIUM_WAIT); + + Util.runAndWait(() -> { + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(MAX_WIDTH, getStage().getWidth(), SIZING_DELTA, + "Stage width should have been limited to max width"); + assertEquals(NEW_HEIGHT, getStage().getHeight(), SIZING_DELTA, "Only max width should be limited"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void maxHeight(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> s.setMaxHeight(MAX_HEIGHT)); + + Util.sleep(MEDIUM_WAIT); + + Util.runAndWait(() -> { + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(NEW_WIDTH, getStage().getWidth(), SIZING_DELTA, "Only max height should be limited"); + assertEquals(MAX_HEIGHT, getStage().getHeight(), SIZING_DELTA, + "Stage height should have been limited to max height"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void minSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.setMinWidth(MIN_WIDTH); + s.setMinHeight(MIN_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + Util.runAndWait(() -> { + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(MIN_WIDTH, getStage().getWidth(), SIZING_DELTA, + "Stage width should have been limited to min width"); + assertEquals(MIN_HEIGHT, getStage().getHeight(), SIZING_DELTA, + "Stage height should have been limited to min height"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void minWidth(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> s.setMinWidth(MIN_WIDTH)); + + Util.sleep(MEDIUM_WAIT); + + Util.runAndWait(() -> { + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(MIN_WIDTH, getStage().getWidth(), SIZING_DELTA, + "Stage width should have been limited to min width"); + assertEquals(NEW_HEIGHT, getStage().getHeight(), SIZING_DELTA, "Only min width should be limited"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void minHeight(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> s.setMinHeight(MIN_HEIGHT)); + + Util.sleep(MEDIUM_WAIT); + + Util.runAndWait(() -> { + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(NEW_WIDTH, getStage().getWidth(), SIZING_DELTA, "Only min height should be limited"); + assertEquals(MIN_HEIGHT, getStage().getHeight(), SIZING_DELTA, + "Stage height should have been limited to min height"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void noSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, null); + + Util.sleep(MEDIUM_WAIT); + + assertTrue(getStage().getWidth() > 1, "Stage width should be greater than 1"); + assertTrue(getStage().getHeight() > 1, "Stage height should be greater than 1"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void noHeight(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> s.setWidth(WIDTH)); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(WIDTH, getStage().getWidth(), SIZING_DELTA, "Stage do not match the set width"); + assertTrue(getStage().getHeight() > 1, "Stage height should be greater than 1"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "TRANSPARENT", "UTILITY"}) + void noWidth(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> s.setHeight(HEIGHT)); + + Util.sleep(MEDIUM_WAIT); + + assertTrue(getStage().getWidth() > 1, "Stage width should be greater than 1"); + assertEquals(HEIGHT, getStage().getHeight(), SIZING_DELTA, "Stage do not match the set height"); + } + + @Test + void sceneSizeOnly() { + setupStageWithStyle(StageStyle.DECORATED, s -> s.setScene(new Scene(new StackPane(), WIDTH, HEIGHT))); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(WIDTH, getScene().getWidth(), SIZING_DELTA, + "Scene width should not be affected by decoration if stage width not set"); + assertEquals(HEIGHT, getScene().getHeight(), SIZING_DELTA, + "Scene height should not be affected by decoration if stage height not set"); + } + + @Test + void sceneWidthWithWindowHeight() { + setupStageWithStyle(StageStyle.DECORATED, s -> { + s.setScene(new Scene(new StackPane(), WIDTH, HEIGHT)); + s.setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(WIDTH, getScene().getWidth(), + "Scene width should not be affected by decoration if stage width not set"); + assertEquals(NEW_HEIGHT, getStage().getHeight(), SIZING_DELTA, "Stage height should match the new height"); + } + + @Test + void sceneHeightWithWindowWidth() { + setupStageWithStyle(StageStyle.DECORATED, s -> { + s.setScene(new Scene(new StackPane(), WIDTH, HEIGHT)); + s.setWidth(NEW_WIDTH); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(NEW_WIDTH, getStage().getWidth(), SIZING_DELTA, "Stage with should match the set new width"); + assertEquals(HEIGHT, getScene().getHeight(), + "Scene height should not be affected by decoration if stage height not set"); + } + + @Test + void sceneSizeThenStageSize() { + setupStageWithStyle(StageStyle.DECORATED, s -> s.setScene(new Scene(new StackPane(), WIDTH, HEIGHT))); + + Util.sleep(MEDIUM_WAIT); + + Util.runAndWait(() -> { + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(NEW_WIDTH, getStage().getWidth(), SIZING_DELTA, + "Scene width should match the new stage width"); + assertEquals(NEW_HEIGHT, getStage().getHeight(), SIZING_DELTA, + "Scene height should match the new stage height"); + + Util.runAndWait(() -> getStage().setScene(new Scene(new StackPane(), WIDTH, HEIGHT))); + + Util.sleep(MEDIUM_WAIT); + + assertEquals(NEW_WIDTH, getStage().getWidth(), SIZING_DELTA, + "Scene width should remain unchanged after setting a new scene"); + assertEquals(NEW_HEIGHT, getStage().getHeight(), SIZING_DELTA, + "Scene height should remain unchanged after setting a new scene"); + } +} diff --git a/tests/system/src/test/java/test/javafx/stage/StageTestBase.java b/tests/system/src/test/java/test/javafx/stage/StageTestBase.java new file mode 100644 index 00000000000..2e390c5e106 --- /dev/null +++ b/tests/system/src/test/java/test/javafx/stage/StageTestBase.java @@ -0,0 +1,151 @@ +/* + * 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.javafx.stage; + +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import test.util.Util; + +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertNull; + +abstract class StageTestBase { + private static final CountDownLatch startupLatch = new CountDownLatch(1); + private Stage stage = null; + + protected static final int SHORT_WAIT = 300; + protected static final int MEDIUM_WAIT = 500; + protected static final int LONG_WAIT = 1000; + protected static final double SIZING_DELTA = 1.0; + protected static final double POSITION_DELTA = 1.0; + + /** + * Creates a Scene for the test stage acoording to the {@link StageStyle} + * @param stageStyle {@link StageStyle} of the Stage + * @return a {@link Scene} + */ + protected Scene createScene(StageStyle stageStyle) { + if (stageStyle == StageStyle.TRANSPARENT) { + Region root = createRoot(); + BackgroundFill fill = new BackgroundFill( + Color.HOTPINK.deriveColor(0, 1, 1, 0.5), + CornerRadii.EMPTY, + Insets.EMPTY + ); + root.setBackground(new Background(fill)); + + Scene scene = new Scene(root); + scene.setFill(Color.TRANSPARENT); + + return scene; + } + + return new Scene(createRoot(), Color.HOTPINK); + } + + /** + * Gets the Scene root + */ + protected Region createRoot() { + return new StackPane(); + } + + /** + * Utility method to setup test Stages according to {@link StageStyle} + * @param stageStyle The Stage Style. + * @param pc A consumer to set state properties + */ + protected void setupStageWithStyle(StageStyle stageStyle, Consumer pc) { + CountDownLatch shownLatch = new CountDownLatch(1); + Util.runAndWait(() -> { + assertNull(stage, "Stage is not null"); + stage = new Stage(); + stage.setAlwaysOnTop(true); + stage.initStyle(stageStyle); + stage.setScene(createScene(stageStyle)); + if (pc != null) { + pc.accept(stage); + } + stage.setOnShown(e -> shownLatch.countDown()); + stage.show(); + }); + + Util.waitForLatch(shownLatch, 5, "Stage failed to show"); + } + + @BeforeAll + public static void initFX() { + Platform.setImplicitExit(false); + Util.startup(startupLatch, startupLatch::countDown); + } + + @AfterAll + public static void teardown() { + Util.shutdown(); + } + + /** + * Hides the test stage after each test + */ + @AfterEach + public void cleanup() { + if (stage != null) { + CountDownLatch hideLatch = new CountDownLatch(1); + stage.setOnHidden(e -> hideLatch.countDown()); + Util.runAndWait(stage::hide); + Util.waitForLatch(hideLatch, 5, "Stage failed to hide"); + stage = null; + } + } + + /** + * @return The stage that is created for each test + */ + protected Stage getStage() { + return stage; + } + + /** + * Gets the Scene of the test stage. + * @return The Scene of the test stage. + */ + protected Scene getScene() { + return stage.getScene(); + } +} diff --git a/tests/system/src/test/java/test/robot/javafx/stage/DualWindowTest.java b/tests/system/src/test/java/test/robot/javafx/stage/DualWindowTest.java index ee3806aa4cc..4fc2d51e0ed 100644 --- a/tests/system/src/test/java/test/robot/javafx/stage/DualWindowTest.java +++ b/tests/system/src/test/java/test/robot/javafx/stage/DualWindowTest.java @@ -147,10 +147,6 @@ void clickButton(TestButton button) throws Exception { @Test public void testTwoStages() throws Exception { - if (PlatformUtil.isLinux()) { - Assumptions.assumeTrue(Boolean.getBoolean("unstable.test")); // JDK-8321624 - } - Util.sleep(1000); Util.runAndWait(() -> { Assertions.assertEquals(STAGE1_X, stage1.getX(), 1.0); diff --git a/tests/system/src/test/java/test/robot/javafx/stage/IconifyTest.java b/tests/system/src/test/java/test/robot/javafx/stage/IconifyTest.java index 5e0c8932b80..608a850c44b 100644 --- a/tests/system/src/test/java/test/robot/javafx/stage/IconifyTest.java +++ b/tests/system/src/test/java/test/robot/javafx/stage/IconifyTest.java @@ -126,7 +126,6 @@ public void canIconifyStage(StageStyle stageStyle, boolean resizable) throws Exc @Test public void canIconifyDecoratedStage() throws Exception { - assumeTrue(!PlatformUtil.isLinux()); // Skip due to JDK-8316891 canIconifyStage(StageStyle.DECORATED, true); } diff --git a/tests/system/src/test/java/test/robot/javafx/stage/StageAttributesTest.java b/tests/system/src/test/java/test/robot/javafx/stage/StageAttributesTest.java index e7c77f2b12d..8c22f0b0de9 100644 --- a/tests/system/src/test/java/test/robot/javafx/stage/StageAttributesTest.java +++ b/tests/system/src/test/java/test/robot/javafx/stage/StageAttributesTest.java @@ -25,21 +25,23 @@ package test.robot.javafx.stage; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static test.util.Util.TIMEOUT; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.stage.Stage; import javafx.stage.StageStyle; -import org.junit.jupiter.api.Test; -import com.sun.javafx.PlatformUtil; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import test.robot.testharness.VisualTestBase; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; +import static test.util.Util.TIMEOUT; + public class StageAttributesTest extends VisualTestBase { private static final int WIDTH = 400; @@ -50,11 +52,15 @@ public class StageAttributesTest extends VisualTestBase { private static final double TOLERANCE = 0.07; + private static final int WAIT = 1000; + private static final int SHORT_WAIT = 100; + private Stage bottomStage; private Scene topScene; private Stage topStage; - private void setupStages(boolean overlayed, boolean topShown) throws InterruptedException { + private void setupStages(boolean overlayed, boolean topShown, StageStyle topStageStyle) + throws InterruptedException { final CountDownLatch bottomShownLatch = new CountDownLatch(1); final CountDownLatch topShownLatch = new CountDownLatch(1); @@ -76,7 +82,7 @@ private void setupStages(boolean overlayed, boolean topShown) throws Interrupted runAndWait(() -> { // Top stage, will be inconified topStage = getStage(true); - topStage.initStyle(StageStyle.DECORATED); + topStage.initStyle(topStageStyle); topScene = new Scene(new Pane(), WIDTH, HEIGHT); topScene.setFill(TOP_COLOR); topStage.setScene(topScene); @@ -97,16 +103,13 @@ private void setupStages(boolean overlayed, boolean topShown) throws Interrupted assertTrue(topShownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS), "Timeout waiting for top stage to be shown"); } - sleep(1000); + sleep(WAIT); } - @Test - public void testIconifiedStage() throws InterruptedException { - // Skip on Linux due to: - // - JDK-8316423 - assumeTrue(!PlatformUtil.isLinux()); - - setupStages(true, true); + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + public void testIconifiedStage(StageStyle stageStyle) throws InterruptedException { + setupStages(true, true, stageStyle); runAndWait(() -> { Color color = getColor(200, 200); @@ -116,7 +119,7 @@ public void testIconifiedStage() throws InterruptedException { }); // wait a bit to let window system animate the change - sleep(1000); + sleep(WAIT); runAndWait(() -> { assertTrue(topStage.isIconified()); @@ -125,13 +128,10 @@ public void testIconifiedStage() throws InterruptedException { }); } - @Test - public void testMaximizedStage() throws InterruptedException { - // Skip on Linux due to: - // - JDK-8316423 - assumeTrue(!PlatformUtil.isLinux()); - - setupStages(false, true); + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + public void testMaximizedStage(StageStyle stageStyle) throws InterruptedException { + setupStages(false, true, stageStyle); runAndWait(() -> { Color color = getColor(200, 200); @@ -141,7 +141,7 @@ public void testMaximizedStage() throws InterruptedException { }); // wait a bit to let window system animate the change - sleep(1000); + sleep(WAIT); runAndWait(() -> { assertTrue(topStage.isMaximized()); @@ -151,9 +151,14 @@ public void testMaximizedStage() throws InterruptedException { assertColorEquals(TOP_COLOR, color, TOLERANCE); }); + // Do not test decorations for UNDECORATED + if (stageStyle.equals(StageStyle.UNDECORATED)) { + return; + } + // wait a little bit between getColor() calls - on macOS the below one // would fail without this wait - sleep(100); + sleep(SHORT_WAIT); runAndWait(() -> { // top left corner (plus some tolerance) should show decorations (so not TOP_COLOR) @@ -163,13 +168,10 @@ public void testMaximizedStage() throws InterruptedException { }); } - @Test - public void testFullScreenStage() throws InterruptedException { - // Skip on Linux due to: - // - JDK-8316423 - assumeTrue(!PlatformUtil.isLinux()); - - setupStages(false, true); + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + public void testFullScreenStage(StageStyle stageStyle) throws InterruptedException { + setupStages(false, true, stageStyle); runAndWait(() -> { Color color = getColor(200, 200); @@ -179,7 +181,7 @@ public void testFullScreenStage() throws InterruptedException { }); // wait a bit to let window system animate the change - sleep(1000); + sleep(WAIT); runAndWait(() -> { assertTrue(topStage.isFullScreen()); @@ -191,7 +193,7 @@ public void testFullScreenStage() throws InterruptedException { // wait a little bit between getColor() calls - on macOS the below one // would fail without this wait - sleep(100); + sleep(SHORT_WAIT); runAndWait(() -> { // top left corner (plus some tolerance) should NOT show decorations @@ -200,13 +202,10 @@ public void testFullScreenStage() throws InterruptedException { }); } - @Test - public void testIconifiedStageBeforeShow() throws InterruptedException { - // Skip on Linux due to: - // - JDK-8316423 - assumeTrue(!PlatformUtil.isLinux()); - - setupStages(true, false); + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + public void testIconifiedStageBeforeShow(StageStyle stageStyle) throws InterruptedException { + setupStages(true, false, stageStyle); runAndWait(() -> { Color color = getColor(200, 200); @@ -218,7 +217,7 @@ public void testIconifiedStageBeforeShow() throws InterruptedException { }); // wait a bit to let window system animate the change - sleep(1000); + sleep(WAIT); runAndWait(() -> { assertTrue(topStage.isIconified()); @@ -229,14 +228,10 @@ public void testIconifiedStageBeforeShow() throws InterruptedException { }); } - @Test - public void testMaximizedStageBeforeShow() throws InterruptedException { - // Skip on Linux due to: - // - JDK-8316423 - // - JDK-8316425 - assumeTrue(!PlatformUtil.isLinux()); - - setupStages(false, false); + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + public void testMaximizedStageBeforeShow(StageStyle stageStyle) throws InterruptedException { + setupStages(false, false, stageStyle); runAndWait(() -> { Color color = getColor(200, 200); @@ -247,7 +242,7 @@ public void testMaximizedStageBeforeShow() throws InterruptedException { }); // wait a bit to let window system animate the change - sleep(1000); + sleep(WAIT); runAndWait(() -> { assertTrue(topStage.isMaximized()); @@ -257,9 +252,15 @@ public void testMaximizedStageBeforeShow() throws InterruptedException { assertColorEquals(TOP_COLOR, color, TOLERANCE); }); + + // Do not test decorations for UNDECORATED + if (stageStyle.equals(StageStyle.UNDECORATED)) { + return; + } + // wait a little bit between getColor() calls - on macOS the below one // would fail without this wait - sleep(100); + sleep(SHORT_WAIT); runAndWait(() -> { // top left corner (plus some tolerance) should show decorations (so not TOP_COLOR) @@ -269,14 +270,10 @@ public void testMaximizedStageBeforeShow() throws InterruptedException { }); } - @Test - public void testFullScreenStageBeforeShow() throws InterruptedException { - // Skip on Linux due to: - // - JDK-8316423 - // - JDK-8316425 - assumeTrue(!PlatformUtil.isLinux()); - - setupStages(false, false); + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + public void testFullScreenStageBeforeShow(StageStyle stageStyle) throws InterruptedException { + setupStages(false, false, stageStyle); runAndWait(() -> { Color color = getColor(200, 200); @@ -287,7 +284,7 @@ public void testFullScreenStageBeforeShow() throws InterruptedException { }); // wait a bit to let window system animate the change - sleep(1000); + sleep(WAIT); runAndWait(() -> { assertTrue(topStage.isFullScreen()); @@ -299,7 +296,7 @@ public void testFullScreenStageBeforeShow() throws InterruptedException { // wait a little bit between getColor() calls - on macOS the below one // would fail without this wait - sleep(100); + sleep(SHORT_WAIT); runAndWait(() -> { // top left corner (plus some tolerance) should NOT show decorations @@ -307,4 +304,4 @@ public void testFullScreenStageBeforeShow() throws InterruptedException { assertColorEquals(TOP_COLOR, color, TOLERANCE); }); } -} +} \ No newline at end of file diff --git a/tests/system/src/test/java/test/robot/javafx/stage/StageLocationTest.java b/tests/system/src/test/java/test/robot/javafx/stage/StageLocationTest.java new file mode 100644 index 00000000000..11b4d11adf0 --- /dev/null +++ b/tests/system/src/test/java/test/robot/javafx/stage/StageLocationTest.java @@ -0,0 +1,177 @@ +/* + * 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.stage; + +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.layout.Background; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import test.robot.testharness.VisualTestBase; +import test.util.Util; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; +import static test.util.Util.TIMEOUT; + +@Timeout(value = TIMEOUT, unit= TimeUnit.MILLISECONDS) +class StageLocationTest extends VisualTestBase { + private static final int WIDTH = 300; + private static final int HEIGHT = 300; + private static final int X = 100; + private static final int Y = 100; + private static final int TO_X = 500; + private static final int TO_Y = 500; + private static final Color COLOR = Color.RED; + private static final double TOLERANCE = 0.07; + private static final int WAIT = 300; + + private Stage createStage(StageStyle stageStyle) { + Stage s = getStage(true); + s.initStyle(stageStyle); + VBox vBox = new VBox(createLabel("X: ", s.xProperty()), + createLabel("Y: ", s.yProperty())); + vBox.setBackground(Background.EMPTY); + Scene scene = new Scene(vBox, WIDTH, HEIGHT); + scene.setFill(COLOR); + s.setScene(scene); + s.setWidth(WIDTH); + s.setHeight(HEIGHT); + return s; + } + + protected Label createLabel(String prefix, ReadOnlyDoubleProperty property) { + Label label = new Label(); + label.textProperty().bind(Bindings.concat(prefix, Bindings.convert(property))); + return label; + } + + private void assertColorEquals(Color expected, int x, int y) { + Color color = getColor(x, y); + assertColorEquals(expected, color, TOLERANCE); + } + + private Stage stage; + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "UTILITY"}) + void moveXY(StageStyle stageStyle) { + CountDownLatch shownLatch = new CountDownLatch(1); + Util.runAndWait(() -> { + stage = createStage(stageStyle); + stage.setX(X); + stage.setY(Y); + stage.setOnShown(e -> Platform.runLater(shownLatch::countDown)); + stage.show(); + }); + + Util.await(shownLatch); + Util.sleep(WAIT); + + Util.doTimeLine(WAIT, + () -> assertColorEquals(COLOR, X + 100, Y + 100), + () -> { + stage.setX(TO_X); + stage.setY(TO_Y); + }, + () -> assertColorEquals(COLOR, TO_X + 100, TO_Y + 100)); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "UTILITY"}) + void moveX(StageStyle stageStyle) { + CountDownLatch shownLatch = new CountDownLatch(1); + Util.runAndWait(() -> { + stage = createStage(stageStyle); + stage.setX(X); + stage.setY(Y); + stage.setOnShown(e -> Platform.runLater(shownLatch::countDown)); + stage.show(); + }); + + Util.await(shownLatch); + Util.sleep(WAIT); + + Util.doTimeLine(WAIT, + () -> assertColorEquals(COLOR, X + 100, Y + 100), + () -> stage.setX(TO_X), + () -> assertColorEquals(COLOR, TO_X + 100, Y + 100)); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "UTILITY"}) + void moveY(StageStyle stageStyle) { + CountDownLatch shownLatch = new CountDownLatch(1); + Util.runAndWait(() -> { + stage = createStage(stageStyle); + stage.setX(X); + stage.setY(Y); + stage.setOnShown(e -> Platform.runLater(shownLatch::countDown)); + stage.show(); + }); + + + Util.await(shownLatch); + Util.sleep(WAIT); + + Util.doTimeLine(WAIT, + () -> assertColorEquals(COLOR, X + 100, Y + 100), + () -> stage.setY(TO_Y), + () -> assertColorEquals(COLOR, X + 100, TO_Y + 100)); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED", "UTILITY"}) + void moveAfterShow(StageStyle stageStyle) { + CountDownLatch shownLatch = new CountDownLatch(1); + Util.runAndWait(() -> { + stage = createStage(stageStyle); + stage.setX(X); + stage.setY(Y); + stage.setOnShown(e -> Platform.runLater(shownLatch::countDown)); + stage.show(); + }); + + Util.await(shownLatch); + Util.sleep(WAIT); + + Util.doTimeLine(WAIT, + () -> { + stage.setX(TO_X); + stage.setY(TO_Y); + }, + () -> assertColorEquals(COLOR, TO_X + 100, TO_Y + 100)); + } +} diff --git a/tests/system/src/test/java/test/robot/javafx/stage/StageMixedSizeTest.java b/tests/system/src/test/java/test/robot/javafx/stage/StageMixedSizeTest.java index 0607b84c3d2..6c970d21c56 100644 --- a/tests/system/src/test/java/test/robot/javafx/stage/StageMixedSizeTest.java +++ b/tests/system/src/test/java/test/robot/javafx/stage/StageMixedSizeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 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 @@ -25,89 +25,77 @@ package test.robot.javafx.stage; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.geometry.Insets; +import javafx.application.Platform; import javafx.scene.Scene; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.stage.Stage; import javafx.stage.StageStyle; -import javafx.util.Duration; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import test.robot.testharness.VisualTestBase; +import test.util.Util; -import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; import static test.util.Util.TIMEOUT; -public class StageMixedSizeTest extends VisualTestBase { +@Timeout(value = TIMEOUT, unit= TimeUnit.MILLISECONDS) +class StageMixedSizeTest extends VisualTestBase { private static final Color BACKGROUND_COLOR = Color.YELLOW; private static final double TOLERANCE = 0.07; + private static final int WAIT = 300; private Stage testStage; - @Test - public void testSetWidthOnlyAfterShownOnContentSizeWindow() throws Exception { - CountDownLatch latch = new CountDownLatch(1); + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + void testSetWidthOnlyAfterShownOnContentSizeWindow(StageStyle stageStyle) { final int finalWidth = 200; final int initialContentSize = 300; - setupContentSizeTestStage(initialContentSize, initialContentSize, - () -> doTimeLine(Map.of(500L, () -> testStage.setWidth(finalWidth), - 1000L, latch::countDown))); + setupContentSizeTestStage(stageStyle, initialContentSize, initialContentSize); - assertTrue(latch.await(TIMEOUT, TimeUnit.MILLISECONDS), "Timeout waiting for test stage to be shown"); - - runAndWait(() -> assertColorDoesNotEqual(BACKGROUND_COLOR, - getColor(initialContentSize - 10, initialContentSize / 2), TOLERANCE)); - Assertions.assertEquals(finalWidth, testStage.getWidth(), "Window width should be " + finalWidth); + Util.doTimeLine(WAIT, + () -> testStage.setWidth(finalWidth), + () -> assertColorDoesNotEqual(BACKGROUND_COLOR, + getColor(initialContentSize - 10, initialContentSize / 2), TOLERANCE), + () -> assertEquals(finalWidth, testStage.getWidth(), "Window width should be " + finalWidth)); } - @Test - public void testSetHeightOnlyAfterShownOnContentSizeWindow() throws Exception { - CountDownLatch latch = new CountDownLatch(1); + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + void testSetHeightOnlyAfterShownOnContentSizeWindow(StageStyle stageStyle) { final int finalHeight = 200; final int initialContentSize = 300; - setupContentSizeTestStage(initialContentSize, initialContentSize, - () -> doTimeLine(Map.of(500L, () -> testStage.setHeight(finalHeight), - 1000L, latch::countDown))); - - assertTrue(latch.await(TIMEOUT, TimeUnit.MILLISECONDS), "Timeout waiting for test stage to be shown"); + setupContentSizeTestStage(stageStyle, initialContentSize, initialContentSize); - runAndWait(() -> assertColorDoesNotEqual(BACKGROUND_COLOR, - getColor(initialContentSize / 2, initialContentSize - 10), TOLERANCE)); - Assertions.assertEquals(finalHeight, testStage.getHeight(), "Window height should be " + finalHeight); + Util.doTimeLine(WAIT, + () -> testStage.setHeight(finalHeight), + () -> assertColorDoesNotEqual(BACKGROUND_COLOR, + getColor(initialContentSize / 2, initialContentSize - 10), TOLERANCE), + () -> assertEquals(finalHeight, testStage.getHeight(), "Window height should be " + finalHeight)); } - private void setupContentSizeTestStage(int width, int height, Runnable onShown) { - runAndWait(() -> { - testStage = getStage(true); - testStage.initStyle(StageStyle.TRANSPARENT); - Pane pane = new Pane(); - pane.setPrefSize(width, height); - pane.setBackground(new Background(new BackgroundFill(BACKGROUND_COLOR, CornerRadii.EMPTY, Insets.EMPTY))); - Scene scene = new Scene(pane); - testStage.setScene(scene); - testStage.setX(0); - testStage.setY(0); - testStage.setOnShown(e -> onShown.run()); - testStage.show(); - }); - } + private void setupContentSizeTestStage(StageStyle stageStyle, int width, int height) { + CountDownLatch shownLatch = new CountDownLatch(1); + + Util.runAndWait(() -> { + testStage = getStage(true); + testStage.initStyle(stageStyle); + Scene scene = new Scene(new StackPane(), width, height, BACKGROUND_COLOR); + testStage.setScene(scene); + testStage.setX(0); + testStage.setY(0); + testStage.setOnShown(e -> Platform.runLater(shownLatch::countDown)); + testStage.show(); + }); - private void doTimeLine(Map keyFrames) { - Timeline timeline = new Timeline(); - timeline.setCycleCount(1); - keyFrames.forEach((duration, runnable) -> - timeline.getKeyFrames().add(new KeyFrame(Duration.millis(duration), e -> runnable.run()))); - timeline.play(); + Util.await(shownLatch); + Util.sleep(WAIT); } } diff --git a/tests/system/src/test/java/test/robot/javafx/stage/StageOwnershipTest.java b/tests/system/src/test/java/test/robot/javafx/stage/StageOwnershipTest.java new file mode 100644 index 00000000000..504a5affaf8 --- /dev/null +++ b/tests/system/src/test/java/test/robot/javafx/stage/StageOwnershipTest.java @@ -0,0 +1,299 @@ +/* + * 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.stage; + +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.layout.Background; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import test.robot.testharness.VisualTestBase; +import test.util.Util; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; +import static test.util.Util.TIMEOUT; + +@Timeout(value = TIMEOUT, unit = TimeUnit.MILLISECONDS) +class StageOwnershipTest extends VisualTestBase { + private static final int WIDTH = 200; + private static final int HEIGHT = 200; + private static final double BOUNDS_EDGE_DELTA = 75; + private Stage topStage; + private Stage bottomStage; + private static final Color TOP_COLOR = Color.RED; + private static final Color BOTTOM_COLOR = Color.LIME; + private static final Color COLOR0 = Color.RED; + private static final Color COLOR1 = Color.ORANGE; + private static final Color COLOR2 = Color.YELLOW; + private static final int X_DELTA = 15; // shadows + private static final int Y_DELTA = 75; // shadows + decoration + + private static final double TOLERANCE = 0.07; + private static final int WAIT_TIME = 500; + private static final int LONG_WAIT_TIME = 1000; + + private void setupBottomStage() throws InterruptedException { + final CountDownLatch shownLatch = new CountDownLatch(1); + + runAndWait(() -> { + bottomStage = getStage(false); + bottomStage.initStyle(StageStyle.DECORATED); + Scene bottomScene = new Scene(getFocusedLabel(BOTTOM_COLOR, bottomStage), WIDTH, HEIGHT); + bottomScene.setFill(BOTTOM_COLOR); + bottomStage.setScene(bottomScene); + bottomStage.setX(0); + bottomStage.setY(0); + bottomStage.setOnShown(e -> Platform.runLater(shownLatch::countDown)); + bottomStage.show(); + }); + assertTrue(shownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS), + "Timeout waiting for bottom stage to be shown"); + + sleep(WAIT_TIME); + } + + private void setupTopStage(Stage owner, StageStyle stageStyle, Modality modality) { + runAndWait(() -> { + topStage = getStage(true); + if (stageStyle != null) { + topStage.initStyle(stageStyle); + } + Scene topScene = new Scene(getFocusedLabel(TOP_COLOR, topStage), WIDTH, HEIGHT); + topScene.setFill(TOP_COLOR); + topStage.setScene(topScene); + if (owner != null) { + topStage.initOwner(owner); + } + if (modality != null) { + topStage.initModality(modality); + } + topStage.setWidth(WIDTH); + topStage.setHeight(HEIGHT); + topStage.setX(0); + topStage.setY(0); + }); + } + + private void assertColorEqualsVisualBounds(Color expected) { + Rectangle2D visualBounds = Screen.getPrimary().getVisualBounds(); + int x = (int) (visualBounds.getWidth() - BOUNDS_EDGE_DELTA); + int y = (int) (visualBounds.getHeight() - BOUNDS_EDGE_DELTA); + + Color color = getColor(x, y); + assertColorEquals(expected, color, TOLERANCE); + } + + private Stage createStage(StageStyle stageStyle, Color color, Stage owner, Modality modality, int x, int y) { + Stage stage = getStage(true); + stage.initStyle(stageStyle); + StackPane pane = getFocusedLabel(color, stage); + Scene scene = new Scene(pane, WIDTH, HEIGHT); + scene.setFill(color); + stage.setScene(scene); + stage.setWidth(WIDTH); + stage.setHeight(HEIGHT); + if (x != -1) { + stage.setX(x); + } + if (y != -1) { + stage.setY(y); + } + if (owner != null) { + stage.initOwner(owner); + } + stage.initModality(modality); + return stage; + } + + private static StackPane getFocusedLabel(Color color, Stage stage) { + Label label = new Label(); + label.textProperty().bind(Bindings.when(stage.focusedProperty()) + .then("Focused").otherwise("Unfocused")); + StackPane pane = new StackPane(label); + pane.setBackground(Background.EMPTY); + + double luminance = 0.2126 * color.getRed() + + 0.7152 * color.getGreen() + + 0.0722 * color.getBlue(); + + Color textColor = luminance < 0.5 ? Color.WHITE : Color.BLACK; + + label.setTextFill(textColor); + return pane; + } + + private void assertColorEquals(Color expected, Stage stage) { + Color color = getColor((int) stage.getX() + X_DELTA, (int) stage.getY() + Y_DELTA); + assertColorEquals(expected, color, TOLERANCE); + } + + private void assertColorDoesNotEqual(Color notExpected, Stage stage) { + Color color = getColor((int) stage.getX() + X_DELTA, (int) stage.getY() + Y_DELTA); + assertColorDoesNotEqual(notExpected, color, TOLERANCE); + } + + private static Stream getTestsParams() { + return Stream.of(StageStyle.DECORATED, StageStyle.UNDECORATED, StageStyle.EXTENDED) + .flatMap(stageStyle -> Stream.of(Modality.APPLICATION_MODAL, Modality.WINDOW_MODAL) + .map(modality -> Arguments.of(stageStyle, modality))); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @MethodSource("getTestsParams") + void openingModalChildStageWhileMaximizedShouldHaveFocus(StageStyle stageStyle, Modality modality) + throws InterruptedException { + setupBottomStage(); + setupTopStage(bottomStage, stageStyle, modality); + + Util.doTimeLine(WAIT_TIME, + () -> bottomStage.setMaximized(true), + topStage::show, + () -> { + assertTrue(bottomStage.isMaximized()); + // Make sure state is still maximized + assertColorEqualsVisualBounds(BOTTOM_COLOR); + + Color color = getColor(100, 100); + assertColorEquals(TOP_COLOR, color, TOLERANCE); + }); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @MethodSource("getTestsParams") + void openingModalChildStageWhileFullScreenShouldHaveFocus(StageStyle stageStyle, Modality modality) + throws InterruptedException { + setupBottomStage(); + setupTopStage(bottomStage, stageStyle, modality); + + Util.doTimeLine(LONG_WAIT_TIME, + () -> bottomStage.setFullScreen(true), + topStage::show, + () -> { + assertTrue(bottomStage.isFullScreen()); + + // Make sure state is still fullscreen + assertColorEqualsVisualBounds(BOTTOM_COLOR); + + Color color = getColor(100, 100); + assertColorEquals(TOP_COLOR, color, TOLERANCE); + }); + } + + private Stage stage0; + private Stage stage1; + private Stage stage2; + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @MethodSource("getTestsParams") + void closingModalWindowShouldFocusParent(StageStyle style, Modality modality) { + CountDownLatch shownLatch = new CountDownLatch(1); + Util.runAndWait(() -> { + stage0 = createStage(style, COLOR0, null, null, 100, 100); + stage1 = createStage(style, COLOR1, stage0, null, 150, 150); + stage2 = createStage(style, COLOR2, stage1, modality, 200, 200); + + stage0.setOnShown(e -> Platform.runLater(shownLatch::countDown)); + stage0.show(); + }); + + Util.await(shownLatch); + Util.sleep(WAIT_TIME); + + Util.doTimeLine(WAIT_TIME, + stage1::show, + stage2::show, + () -> { + assertTrue(stage2.isFocused()); + assertColorEquals(COLOR2, stage2); + assertFalse(stage1.isFocused()); + assertFalse(stage0.isFocused()); + }, + stage2::close, + () -> { + assertTrue(stage1.isFocused()); + assertColorEquals(COLOR1, stage1); + assertFalse(stage0.isFocused()); + }, + stage1::close, + () -> { + assertTrue(stage0.isFocused()); + assertColorEquals(COLOR0, stage0); + }); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "EXTENDED"}) + void iconifyParentShouldHideChildren(StageStyle style) { + CountDownLatch shownLatch = new CountDownLatch(3); + Util.runAndWait(() -> { + stage0 = createStage(style, COLOR0, null, null, 100, 100); + stage1 = createStage(style, COLOR1, stage0, null, 200, 150); + stage2 = createStage(style, COLOR2, stage1, null, 300, 200); + + List.of(stage0, stage1, stage2).forEach(stage -> { + stage.setOnShown(e -> Platform.runLater(shownLatch::countDown)); + stage.show(); + }); + }); + + Util.await(shownLatch); + Util.sleep(WAIT_TIME); + + Util.doTimeLine(WAIT_TIME, + () -> stage0.setIconified(true), + () -> { + assertTrue(stage0.isIconified()); + assertColorDoesNotEqual(COLOR0, stage0); + assertColorDoesNotEqual(COLOR1, stage1); + assertColorDoesNotEqual(COLOR2, stage2); + }, + () -> stage0.setIconified(false), + () -> { + assertFalse(stage0.isIconified()); + assertColorEquals(COLOR0, stage0); + assertColorEquals(COLOR1, stage1); + assertColorEquals(COLOR2, stage2); + }); + } +} diff --git a/tests/system/src/test/java/test/util/Util.java b/tests/system/src/test/java/test/util/Util.java index 0a32641a5f7..73c097e1839 100644 --- a/tests/system/src/test/java/test/util/Util.java +++ b/tests/system/src/test/java/test/util/Util.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 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 @@ -36,8 +36,15 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.application.Application; import javafx.application.Platform; import javafx.geometry.Rectangle2D; @@ -47,7 +54,9 @@ import javafx.scene.layout.Region; import javafx.scene.robot.Robot; import javafx.stage.Screen; +import javafx.stage.Stage; import javafx.stage.Window; +import javafx.util.Duration; import org.junit.jupiter.api.Assertions; import com.sun.javafx.PlatformUtil; @@ -55,6 +64,8 @@ * Utility methods for life-cycle testing */ public class Util { + public static final String PARAMETERIZED_TEST_DISPLAY = "{displayName} [{index}] {arguments}"; + /** Default startup timeout value in seconds */ public static final int STARTUP_TIMEOUT = 15; /** Test timeout value in milliseconds */ @@ -453,4 +464,94 @@ public static boolean isOnWayland() { String waylandDisplay = System.getenv("WAYLAND_DISPLAY"); return waylandDisplay != null && !waylandDisplay.isEmpty(); } + + /** + * Creates a {@link Timeline} where each {@link KeyFrame} runs a {@link Runnable}. + * Each {@link Runnable} will be scheduled at an increment of {@code msToIncrement} milliseconds. + * + * @param msToIncrement the number of milliseconds to increment between each {@link KeyFrame} + * @param runnables the list of {@link Runnable} instances to execute sequentially + */ + public static void doTimeLine(int msToIncrement, Runnable... runnables) { + long millis = msToIncrement; + + CompletableFuture future = new CompletableFuture<>(); + + Timeline timeline = new Timeline(); + timeline.setCycleCount(1); + for (Runnable runnable : runnables) { + timeline.getKeyFrames().add(new KeyFrame(Duration.millis(millis), e -> { + try { + runnable.run(); + } catch (Throwable ex) { + future.completeExceptionally(ex); + } + })); + millis += msToIncrement; + } + timeline.setOnFinished(e -> future.complete(null)); + timeline.play(); + + final long waitms = millis + 5000; + + try { + future.get(waitms, TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException ex) { + throwError(ex); + } + } + + /** + * Creates a {@link Timeline} where each {@link KeyFrame} executes a {@link Runnable}. + * + * @param runnables a {@link Map} where the key is the {@link Duration} at which to trigger the action, + * and the value is the {@link Runnable} to execute at that time + */ + public static void doTimeLine(Map runnables) { + CompletableFuture future = new CompletableFuture<>(); + + Timeline timeline = new Timeline(); + timeline.setCycleCount(1); + Duration totalDuration = Duration.seconds(5); + + for (Map.Entry entry : runnables.entrySet()) { + Duration duration = entry.getKey(); + Runnable runnable = entry.getValue(); + totalDuration = totalDuration.add(duration); + timeline.getKeyFrames().add(new KeyFrame(duration, e -> { + try { + runnable.run(); + } catch (Throwable ex) { + future.completeExceptionally(ex); + } + })); + } + + timeline.setOnFinished(e -> future.complete(null)); + timeline.play(); + + long waitms = (long) totalDuration.toMillis(); + try { + future.get(waitms, TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException ex) { + throwError(ex); + } + } + + /** + * Finds the {@link Screen} where the top-left corner of the given {@link Stage} is located. + * + * @param stage the {@link Stage} to check + * @return the {@link Screen} containing the stage's top-left corner or {@link Screen#getPrimary()} + */ + public static Screen getScreen(Stage stage) { + for (Screen screen : Screen.getScreens()) { + Rectangle2D bounds = screen.getVisualBounds(); + if (bounds.contains(stage.getX(), stage.getY())) { + return screen; + } + } + + return Screen.getPrimary(); + } }