diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java index a638e877356..3eaf47d4954 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java @@ -24,6 +24,7 @@ */ package com.sun.glass.ui.gtk; +import com.sun.glass.events.ViewEvent; import com.sun.glass.ui.Pixels; import com.sun.glass.ui.View; @@ -98,8 +99,18 @@ protected void _uploadPixels(long ptr, Pixels pixels) { private native void _uploadPixelsByteArray(long viewPtr, byte[] pixels, int offset, int width, int height); private native void _uploadPixelsIntArray(long viewPtr, int[] pixels, int offset, int width, int height); + protected native void enterFullscreenImpl(long ptr, boolean animate, boolean keepRatio, boolean hideCursor); + @Override - protected native boolean _enterFullscreen(long ptr, boolean animate, boolean keepRatio, boolean hideCursor); + protected boolean _enterFullscreen(long ptr, boolean animate, boolean keepRatio, boolean hideCursor) { + enterFullscreenImpl(ptr, animate, keepRatio, hideCursor); + + if (getWindow() != null && !getWindow().isVisible()) { + notifyView(ViewEvent.FULLSCREEN_ENTER); + } + + return true; + } @Override protected native void _exitFullscreen(long ptr, boolean animate); 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 002d698398f..eb7965d01c5 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 @@ -83,10 +83,15 @@ protected boolean _setMenubar(long ptr, long menubarPtr) { protected native void _setLevel(long ptr, int level); @Override - protected native void _setAlpha(long ptr, float alpha); + protected void _setAlpha(long ptr, float alpha) { + // Not supported + } @Override - protected native boolean _setBackground(long ptr, float r, float g, float b); + protected boolean _setBackground(long ptr, float r, float g, float b) { + //Not supported + return false; + } @Override protected native void _setEnabled(long ptr, boolean enabled); @@ -128,16 +133,24 @@ 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; + + if (!isVisible()) { + notifyStateChanged(WindowEvent.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 b621bc7d882..4d921419447 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp @@ -482,13 +482,12 @@ 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); gtk_main_do_event(event); + ctx->process_configure(&event->configure); break; case GDK_FOCUS_CHANGE: ctx->process_focus(&event->focus_change); @@ -503,11 +502,12 @@ static void process_events(GdkEvent* event, gpointer data) break; case GDK_EXPOSE: case GDK_DAMAGE: - ctx->process_expose(&event->expose); + ctx->notify_repaint(&event->expose.area); break; case GDK_WINDOW_STATE: - ctx->process_state(&event->window_state); + // Let gtk handle it first, so state functions are updated gtk_main_do_event(event); + ctx->process_state(&event->window_state); break; case GDK_BUTTON_PRESS: case GDK_BUTTON_RELEASE: @@ -535,7 +535,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: @@ -550,6 +551,7 @@ static void process_events(GdkEvent* event, gpointer data) } } else { + //FIXME: Those do not work anymore if (window == gdk_screen_get_root_window(gdk_screen_get_default())) { if (event->any.type == GDK_PROPERTY_NOTIFY) { if (event->property.atom == gdk_atom_intern_static_string("_NET_WORKAREA") 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..ef6c0fcacf2 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassView.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassView.cpp @@ -271,12 +271,11 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkView__1uploadPixelsByteArray /* * Class: com_sun_glass_ui_gtk_GtkView - * Method: _enterFullscreen - * Signature: (JZZZ)Z + * Method: enterFullscreenImpl + * Signature: (JZZZ)V */ -JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkView__1enterFullscreen - (JNIEnv * env, jobject obj, jlong ptr, jboolean animate, jboolean keepRation, jboolean hideCursor) -{ +JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkView_enterFullscreenImpl + (JNIEnv * env, jobject obj, jlong ptr, jboolean animate, jboolean keepRation, jboolean hideCursor) { (void)animate; (void)keepRation; (void)hideCursor; @@ -284,10 +283,7 @@ 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 +303,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 3bd5bd38c1c..7aca1c09a20 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp @@ -84,7 +84,7 @@ JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1createWindow WindowContext* parent = JLONG_TO_WINDOW_CTX(owner); - WindowContext* ctx = new WindowContextTop(obj, + WindowContext* ctx = new WindowContext(obj, parent, screen, glass_mask_to_window_frame_type(mask), @@ -312,37 +312,6 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1setLevel ctx->set_level(level); } -/* - * Class: com_sun_glass_ui_gtk_GtkWindow - * Method: _setAlpha - * Signature: (JF)V - */ -JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1setAlpha - (JNIEnv * env, jobject obj, jlong ptr, jfloat alpha) -{ - (void)env; - (void)obj; - - WindowContext* ctx = JLONG_TO_WINDOW_CTX(ptr); - ctx->set_alpha(alpha); -} - -/* - * Class: com_sun_glass_ui_gtk_GtkWindow - * Method: _setBackground - * Signature: (JFFF)Z - */ -JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1setBackground - (JNIEnv * env, jobject obj, jlong ptr, jfloat r, jfloat g, jfloat b) -{ - (void)env; - (void)obj; - - WindowContext* ctx = JLONG_TO_WINDOW_CTX(ptr); - ctx->set_background(r, g, b); - return JNI_TRUE; -} - /* * Class: com_sun_glass_ui_gtk_GtkWindow * Method: _setEnabled @@ -388,8 +357,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; 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 28e07eaa6a9..fed00c9b6fd 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 @@ -80,8 +80,6 @@ jmethodID jWindowNotifyDelegatePtr; jfieldID jWindowPtr; jfieldID jCursorPtr; -jmethodID jGtkWindowNotifyStateChanged; - jmethodID jClipboardContentChanged; jmethodID jSizeInit; @@ -269,9 +267,6 @@ JNI_OnLoad(JavaVM *jvm, void *reserved) clazz = env->FindClass("com/sun/glass/ui/gtk/GtkWindow"); if (env->ExceptionCheck()) return JNI_ERR; - jGtkWindowNotifyStateChanged = - env->GetMethodID(clazz, "notifyStateChanged", "(I)V"); - if (env->ExceptionCheck()) return JNI_ERR; clazz = env->FindClass("com/sun/glass/ui/Clipboard"); if (env->ExceptionCheck()) return JNI_ERR; 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 9e0289fe207..030bd80bdcc 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 @@ -194,8 +194,6 @@ struct jni_exception: public std::exception { extern jfieldID jWindowPtr; // com.sun.glass.ui.Window#ptr extern jfieldID jCursorPtr; // com.sun.glass.ui.Cursor#ptr - extern jmethodID jGtkWindowNotifyStateChanged; // com.sun.glass.ui.GtkWindow#notifyStateChanged (I)V - extern jmethodID jClipboardContentChanged; // com.sun.glass.ui.Clipboard#contentChanged ()V extern jmethodID jSizeInit; // com.sun.class.ui.Size# ()V @@ -249,6 +247,8 @@ struct jni_exception: public std::exception { #define LOG3(msg, param1, param2, param3) {printf(msg, param1, param2, param3);fflush(stdout);} #define LOG4(msg, param1, param2, param3, param4) {printf(msg, param1, param2, param3, param4);fflush(stdout);} #define LOG5(msg, param1, param2, param3, param4, param5) {printf(msg, param1, param2, param3, param4, param5);fflush(stdout);} +#define LOG10(msg, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10) \ + {printf(msg, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10);fflush(stdout);} #define LOG_STRING_ARRAY(env, array) dump_jstring_array(env, array); @@ -264,6 +264,7 @@ struct jni_exception: public std::exception { #define LOG3(msg, param1, param2, param3) #define LOG4(msg, param1, param2, param3, param4) #define LOG5(msg, param1, param2, param3, param4, param5) +#define LOG10(msg, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10) #define LOG_STRING_ARRAY(env, array) 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 6417f52c2e8..14eaf420461 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,46 +27,170 @@ #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 #define MOUSE_BACK_BTN 8 #define MOUSE_FORWARD_BTN 9 -WindowContext * WindowContextBase::sm_grab_window = NULL; -WindowContext * WindowContextBase::sm_mouse_drag_window = NULL; +#define NONNEGATIVE_OR(val, fallback) (((val) < 0) ? (fallback) : (val)) + +#define DEFAULT_WIDTH 320 +#define DEFAULT_HEIGHT 200 + +static gboolean event_realize(GtkWidget *widget, gpointer user_data) { + WindowContext *ctx = USER_PTR_TO_CTX(user_data); + ctx->process_realize(); + + return FALSE; +} + +gboolean enter_fullscreen_later(gpointer data) { + GtkWindow *window = GTK_WINDOW(data); + + // might have been destroyed + if (GTK_IS_WINDOW(window)) { + gtk_window_fullscreen(window); + } + + return G_SOURCE_REMOVE; +} + +static void process_pending_events() { + while (gtk_events_pending()) { + gtk_main_iteration_do(FALSE); + } +} + +void destroy_and_delete_ctx(WindowContext* ctx) { + LOG0("destroy_and_delete_ctx\n"); + if (ctx) { + ctx->process_destroy(); + + if (!ctx->get_events_count()) { + LOG0("delete ctx\n"); + delete ctx; + } + // else: ctx will be deleted in EventsCounterHelper after completing + // an event processing + } +} + +static gboolean is_window_floating(GdkWindowState state) { + return !(state & GDK_WINDOW_STATE_MAXIMIZED) + && !(state & GDK_WINDOW_STATE_FULLSCREEN); +} + +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; + } +} + +WindowContext * WindowContext::sm_grab_window = NULL; +WindowContext * WindowContext::sm_mouse_drag_window = NULL; + +// 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) : + screen(_screen), + frame_type(_frame_type), + window_type(type), + owner(_owner), + geometry(), + resizable(), + im_ctx() { + jwindow = mainEnv->NewGlobalRef(_jwindow); + initial_wmf = wmf; + current_wmf = wmf; + is_mouse_entered = false; + is_disabled = false; + on_top = false; + can_be_deleted = false; + was_mapped = false; + initial_state_mask = 0; + + 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); + } + + if (owner) { + owner->add_child(this); + if (on_top_inherited()) { + gtk_window_set_keep_above(GTK_WINDOW(gtk_widget), TRUE); + } + } -GdkWindow* WindowContextBase::get_gdk_window(){ - return gdk_window; + if (type == UTILITY) { + gtk_window_set_type_hint(GTK_WINDOW(gtk_widget), GDK_WINDOW_TYPE_HINT_UTILITY); + } + + glong xvisualID = (glong)mainEnv->GetStaticLongField(jApplicationCls, jApplicationVisualID); + + if (xvisualID != 0) { + GdkVisual *visual = gdk_x11_screen_lookup_visual(gdk_screen_get_default(), xvisualID); + glass_gtk_window_configure_from_visual(gtk_widget, visual); + } + + gtk_widget_set_app_paintable(gtk_widget, TRUE); + + glass_configure_window_transparency(gtk_widget, frame_type == TRANSPARENT); + gtk_window_set_title(GTK_WINDOW(gtk_widget), ""); + + gtk_window_set_decorated(GTK_WINDOW(gtk_widget), frame_type == TITLED); + load_cached_extents(); +} + +GdkWindow* WindowContext::get_gdk_window() { + if (GDK_IS_WINDOW(gdk_window)) { + return gdk_window; + } + + return NULL; } -jobject WindowContextBase::get_jview() { +jobject WindowContext::get_jview() { return jview; } -jobject WindowContextBase::get_jwindow() { +jobject WindowContext::get_jwindow() { return jwindow; } -bool WindowContextBase::isEnabled() { +bool WindowContext::isEnabled() { if (jwindow) { bool result = (JNI_TRUE == mainEnv->CallBooleanMethod(jwindow, jWindowIsEnabled)); LOG_EXCEPTION(mainEnv) @@ -76,65 +200,37 @@ bool WindowContextBase::isEnabled() { } } -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; - } +void WindowContext::process_map() { + // We need only first map + if (was_mapped || window_type == POPUP) return; - 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); - } - } + was_mapped = true; + LOG0("--------------------------------------------------------> mapped\n"); - if (jwindow) { - mainEnv->CallVoidMethod(jwindow, - jGtkWindowNotifyStateChanged, - glass_state); - CHECK_JNI_EXCEPTION(mainEnv); + // Work around JDK-8337400 (Initial window position is not centered on Xorg) + if (geometry.x > 0 || geometry.y > 0) { + move(geometry.x, geometry.y); } -} - -void WindowContextBase::process_state(GdkEventWindowState* event) { - if (event->changed_mask & (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_MAXIMIZED)) { - if (event->changed_mask & GDK_WINDOW_STATE_ICONIFIED) { - is_iconified = event->new_window_state & GDK_WINDOW_STATE_ICONIFIED; - } - - if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED) { - is_maximized = event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED; - } + if (geometry.width <= 0) { + geometry.width = DEFAULT_WIDTH - geometry.extents.width; + } - jint stateChangeEvent; + if (geometry.height <= 0) { + geometry.height = DEFAULT_HEIGHT - geometry.extents.height; + } - if (is_iconified) { - stateChangeEvent = com_sun_glass_events_WindowEvent_MINIMIZE; - } else if (is_maximized) { - stateChangeEvent = com_sun_glass_events_WindowEvent_MAXIMIZE; - } 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); - } - } + resize(geometry.width, geometry.height); - notify_state(stateChangeEvent); - } else if (event->changed_mask & GDK_WINDOW_STATE_ABOVE) { - notify_on_top(event->new_window_state & GDK_WINDOW_STATE_ABOVE); + // Work-around for Xorg initial state before show to work + if (initial_state_mask != 0) { + process_pending_events(); + update_initial_state(); } } -void WindowContextBase::process_focus(GdkEventFocus* event) { - if (!event->in && WindowContextBase::sm_grab_window == this) { +void WindowContext::process_focus(GdkEventFocus *event) { + if (!event->in && WindowContext::sm_grab_window == this) { ungrab_focus(); } @@ -160,47 +256,41 @@ void WindowContextBase::process_focus(GdkEventFocus* event) { } } -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(); +void WindowContext::process_destroy() { + LOG0("process_destroy\n"); - if (!ctx->get_events_count()) { - delete ctx; - } - // else: ctx will be deleted in EventsCounterHelper after completing - // an event processing + 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 + // WindowContext::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); @@ -227,39 +317,23 @@ void WindowContextBase::process_destroy() { can_be_deleted = true; } -void WindowContextBase::process_delete() { +void WindowContext::process_delete() { + LOG0("process_delete\n"); if (jwindow && isEnabled()) { + LOG0("jWindowNotifyClose\n"); mainEnv->CallVoidMethod(jwindow, jWindowNotifyClose); CHECK_JNI_EXCEPTION(mainEnv) } } -void WindowContextBase::process_expose(GdkEventExpose* event) { +void WindowContext::notify_repaint(GdkRectangle *rect) { if (jview) { - mainEnv->CallVoidMethod(jview, jViewNotifyRepaint, event->area.x, event->area.y, event->area.width, event->area.height); + mainEnv->CallVoidMethod(jview, jViewNotifyRepaint, rect->x, rect->y, rect->width, rect->height); CHECK_JNI_EXCEPTION(mainEnv) } } -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 WindowContextBase::process_mouse_button(GdkEventButton* event) { +void WindowContext::process_mouse_button(GdkEventButton *event) { bool press = event->type == GDK_BUTTON_PRESS; guint state = event->state; guint mask = 0; @@ -337,7 +411,7 @@ void WindowContextBase::process_mouse_button(GdkEventButton* event) { } } -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 | @@ -347,7 +421,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 == NULL) { // 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. @@ -380,7 +454,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; @@ -420,10 +494,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; @@ -446,7 +519,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); @@ -483,8 +556,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, @@ -494,12 +568,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 = @@ -514,50 +587,28 @@ void WindowContextBase::paint(void* data, jint width, jint height) { 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()); } -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() { +bool WindowContext::is_visible() { return gtk_widget_get_visible(gtk_widget); } -bool WindowContextBase::set_view(jobject view) { +bool WindowContext::set_view(jobject view) { if (jview) { mainEnv->CallVoidMethod(jview, jViewNotifyMouse, com_sun_glass_events_MouseEvent_EXIT, @@ -578,39 +629,39 @@ bool WindowContextBase::set_view(jobject view) { return TRUE; } -bool WindowContextBase::grab_mouse_drag_focus() { +bool WindowContext::grab_mouse_drag_focus() { if (glass_gdk_mouse_devices_grab_with_cursor( gdk_window, gdk_window_get_cursor(gdk_window), FALSE)) { - WindowContextBase::sm_mouse_drag_window = this; + 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() { + WindowContext::sm_mouse_drag_window = NULL; 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() { + 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() { + if (!WindowContext::sm_mouse_drag_window) { glass_gdk_mouse_devices_ungrab(); } - WindowContextBase::sm_grab_window = NULL; + WindowContext::sm_grab_window = NULL; if (jwindow) { mainEnv->CallVoidMethod(jwindow, jWindowNotifyFocusUngrab); @@ -618,226 +669,170 @@ void WindowContextBase::ungrab_focus() { } } -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_window_set_cursor(gdk_window, 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); +GdkAtom WindowContext::get_net_frame_extents_atom() { + static GdkAtom atom = NULL; + if (atom == NULL) { + atom = gdk_atom_intern_static_string("_NET_FRAME_EXTENTS"); + } + return atom; } -WindowContextBase::~WindowContextBase() { - disableIME(); - gtk_widget_destroy(gtk_widget); -} +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); -////////////////////////////// WindowContextTop ///////////////////////////////// + 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; -// 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}; + XSendEvent(display, XDefaultRootWindow(display), False, + SubstructureRedirectMask | SubstructureNotifyMask, + (XEvent *) &clientMessage); + XFlush(display); + } +} +void WindowContext::update_window_size_location() { + if (!geometry.needs_to_restore_geometry + || (gdk_window_get_state(gdk_window) & (GDK_WINDOW_STATE_FULLSCREEN | GDK_WINDOW_STATE_MAXIMIZED))) { + return; + } -static void event_realize(GtkWidget* self, gpointer user_data) { - WindowContextTop *ctx = ((WindowContextTop *) user_data); - ctx->process_realize(); + process_pending_events(); + geometry.needs_to_restore_geometry = false; + move(geometry.x, geometry.y); + LOG2("update_window_size_location: %d, %d\n", geometry.width, geometry.height); + resize(geometry.width, geometry.height); } -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) { + LOG0("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) { + LOG0("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) { + LOG0("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); +void WindowContext::update_frame_extents() { + if (frame_type != TITLED) return; - if (gchar* app_name = get_application_name()) { - gtk_window_set_wmclass(GTK_WINDOW(gtk_widget), app_name, app_name); - g_free(app_name); - } + int top, left, bottom, right; - if (owner) { - owner->add_child(this); - if (on_top_inherited()) { - gtk_window_set_keep_above(GTK_WINDOW(gtk_widget), TRUE); - } - } + if (get_frame_extents_property(&top, &left, &bottom, &right)) { + if (top > 0 || right > 0 || bottom > 0 || left > 0) { + bool changed = geometry.extents.x != left + || geometry.extents.y != top + || geometry.extents.width != (left + right) + || geometry.extents.height != (top + bottom); - if (type == UTILITY) { - gtk_window_set_type_hint(GTK_WINDOW(gtk_widget), GDK_WINDOW_TYPE_HINT_UTILITY); - } + LOG1(" ------------------------------------------- frame extents - changed: %d\n", changed); - const char* wm_name = gdk_x11_screen_get_window_manager_name(gdk_screen_get_default()); - wmanager = (g_strcmp0("Compiz", wm_name) == 0) ? COMPIZ : UNKNOWN; + if (!changed) return; -// glong xdisplay = (glong)mainEnv->GetStaticLongField(jApplicationCls, jApplicationDisplay); -// gint xscreenID = (gint)mainEnv->GetStaticIntField(jApplicationCls, jApplicationScreen); - glong xvisualID = (glong)mainEnv->GetStaticLongField(jApplicationCls, jApplicationVisualID); + GdkRectangle rect = { left, top, (left + right), (top + bottom) }; + set_cached_extents(rect); - if (xvisualID != 0) { - GdkVisual *visual = gdk_x11_screen_lookup_visual(gdk_screen_get_default(), xvisualID); - glass_gtk_window_configure_from_visual(gtk_widget, visual); - } + if (geometry.width <= 0 && geometry.height <= 0) { + return; + } - gtk_widget_set_events(gtk_widget, GDK_FILTERED_EVENTS_MASK); - gtk_widget_set_app_paintable(gtk_widget, TRUE); + int newW = gdk_window_get_width(gdk_window); + int newH = gdk_window_get_height(gdk_window); - glass_configure_window_transparency(gtk_widget, frame_type == TRANSPARENT); - gtk_window_set_title(GTK_WINDOW(gtk_widget), ""); + // Here the user might change the desktop theme and in consequence + // change decoration sizes. - if (frame_type != TITLED) { - gtk_window_set_decorated(GTK_WINDOW(gtk_widget), FALSE); - } else { - geometry.extents = get_cached_extents(); - } -} + // Re-add the extents and then subtract the new + newW = newW + + ((geometry.frame_extents_received) ? geometry.extents.width : 0) + - rect.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; - } -} + // Re-add the extents and then subtract the new + newH = newH + + ((geometry.frame_extents_received) ? geometry.extents.height : 0) + - rect.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 = NONNEGATIVE_OR(newW, 1); + newH = NONNEGATIVE_OR(newH, 1); - if (rfeAtom != None) { - XClientMessageEvent clientMessage; - memset(&clientMessage, 0, sizeof(clientMessage)); + LOG2("extents received -> new view size: %d, %d\n", newW, newH); + int x = geometry.x; + int y = geometry.y; - clientMessage.type = ClientMessage; - clientMessage.window = GDK_WINDOW_XID(gdk_window); - clientMessage.message_type = rfeAtom; - clientMessage.format = 32; + // Gravity x, y are used in centerOnScreen(). Here it's used to adjust the position + // accounting decorations + if (geometry.gravity_x > 0 && x > 0) { + x -= geometry.gravity_x * (float) (geometry.extents.width); + x = NONNEGATIVE_OR(x, 0); + } - XSendEvent(display, XDefaultRootWindow(display), False, - SubstructureRedirectMask | SubstructureNotifyMask, - (XEvent *) &clientMessage); - XFlush(display); - } -} + if (geometry.gravity_y > 0 && y > 0) { + y -= geometry.gravity_y * (float) (geometry.extents.height); + y = NONNEGATIVE_OR(y, 0); + } -void WindowContextTop::update_frame_extents() { - int top, left, bottom, right; + geometry.extents = rect; + geometry.frame_extents_received = true; + geometry.width = newW; + geometry.height = newH; + geometry.x = x; + geometry.y = y; - 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); - } - } - } -} + LOG4("Geometry after frame extents: %d, %d - %d, %d\n", geometry.x, + geometry.y, geometry.width, geometry.height); -void WindowContextTop::set_cached_extents(WindowFrameExtents ex) { - if (window_type == NORMAL) { - normal_extents = ex; - } else { - utility_extents = ex; + update_window_constraints(newW, newH); + + if ((gdk_window_get_state(gdk_window) + & (GDK_WINDOW_STATE_FULLSCREEN | GDK_WINDOW_STATE_MAXIMIZED)) == 0) { + resize(newW, newH); + move(x, y); + } else { + geometry.needs_to_restore_geometry = true; + } + } } } -WindowFrameExtents WindowContextTop::get_cached_extents() { - return window_type == NORMAL ? normal_extents : utility_extents; +void WindowContext::save_geometry() { + geometry.width = gdk_window_get_width(gdk_window); + geometry.height = gdk_window_get_height(gdk_window); + gdk_window_get_root_origin(gdk_window, &geometry.x, &geometry.y); } -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; @@ -863,76 +858,127 @@ 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(GdkRectangle 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()) { + geometry.extents = normal_extents.value(); + LOG4("Loaded Normal Extents: x = %d, y = %d, width = %d, height = %d\n", + geometry.extents.x, geometry.extents.y, geometry.extents.width, geometry.extents.height); + geometry.frame_extents_received = true; 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()) { + geometry.extents = utility_extents.value(); + LOG4("Loaded Utility Extents: x = %d, y = %d, width = %d, height = %d\n", + geometry.extents.x, geometry.extents.y, geometry.extents.width, geometry.extents.height); + geometry.frame_extents_received = true; + } +} - gint length; +void WindowContext::process_property_notify(GdkEventProperty *event) { +// LOG1("process_property_notify: %s\n", gdk_atom_name(event->atom)); + 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))) { + return; + } - if (gdk_property_get(gdk_window, atom_net_wm_state, atom_atom, - 0, G_MAXLONG, FALSE, NULL, NULL, &length, (guchar**) &atoms)) { + if (event->changed_mask & GDK_WINDOW_STATE_ABOVE) { + notify_on_top(event->new_window_state & GDK_WINDOW_STATE_ABOVE); - 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; - } - } + // Only state mask + if (event->new_window_state == GDK_WINDOW_STATE_ABOVE) return; + } - g_free(atoms); + // Those represent the real current size in the state + int cw = gdk_window_get_width(gdk_window); + int ch = gdk_window_get_height(gdk_window); - if (is_iconified != is_hidden) { - is_iconified = is_hidden; + int ww, wh; + get_window_size(&ww, &wh); - notify_state((is_hidden) - ? com_sun_glass_events_WindowEvent_MINIMIZE - : com_sun_glass_events_WindowEvent_RESTORE); - } + LOG4("process_state: cw = %d, ch = %d, ww = %d, wh = %d\n", cw, ch, ww, wh); - 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)) { + LOG0("com_sun_glass_events_WindowEvent_RESTORE\n"); + notify_window_resize(com_sun_glass_events_WindowEvent_RESTORE, ww, wh); + } else if (event->new_window_state & (GDK_WINDOW_STATE_ICONIFIED)) { + LOG0("com_sun_glass_events_WindowEvent_MINIMIZE\n"); + notify_window_resize(com_sun_glass_events_WindowEvent_MINIMIZE, ww, wh); + } else if (event->new_window_state & (GDK_WINDOW_STATE_MAXIMIZED)) { + LOG0("com_sun_glass_events_WindowEvent_MAXIMIZE\n"); + notify_window_resize(com_sun_glass_events_WindowEvent_MAXIMIZE, ww, wh); } -} -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(); - } + //FIXME: remove when 8351867 is fixed + GdkRectangle rect = { 0, 0, cw, ch }; + notify_repaint(&rect); } -} -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 only iconified, no further processing + if (event->new_window_state == GDK_WINDOW_STATE_ICONIFIED) return; 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)); + && (event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) == 0) { + remove_wmf(GDK_FUNC_MAXIMIZE); } - WindowContextBase::process_state(event); + if (jview && event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) { + if (event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) { + LOG0("com_sun_glass_events_ViewEvent_FULLSCREEN_ENTER\n"); + mainEnv->CallVoidMethod(jview, jViewNotifyView, com_sun_glass_events_ViewEvent_FULLSCREEN_ENTER); + CHECK_JNI_EXCEPTION(mainEnv) + } else { + LOG0("com_sun_glass_events_ViewEvent_FULLSCREEN_EXIT\n"); + mainEnv->CallVoidMethod(jview, jViewNotifyView, com_sun_glass_events_ViewEvent_FULLSCREEN_EXIT); + CHECK_JNI_EXCEPTION(mainEnv) + } + } + + notify_view_resize(cw, ch); + // Since FullScreen (or custom modes of maximized) can undecorate the + // window, request view position change + notify_view_move(); + + // This only accounts MAXIMIZED and FULLSCREEN + 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); + + // In case the size or location changed while maximized of fullscreened + if (restored && geometry.needs_to_restore_geometry) { + LOG0("restored, call update_window_size_location\n"); + update_window_size_location(); + } } -void WindowContextTop::process_realize() { +void WindowContext::process_realize() { + LOG0("realized\n"); gdk_window = gtk_widget_get_window(gtk_widget); + if (frame_type == TITLED) { request_frame_extents(); } @@ -941,127 +987,216 @@ void WindowContextTop::process_realize() { g_object_set_data_full(G_OBJECT(gdk_window), GDK_WINDOW_DATA_CONTEXT, this, NULL); gdk_window_register_dnd(gdk_window); - if (gdk_windowManagerFunctions) { - gdk_window_set_functions(gdk_window, gdk_windowManagerFunctions); + if (frame_type != TITLED) { + initial_wmf = GDK_FUNC_ALL; + } + + if (initial_wmf) { + gdk_window_set_functions(gdk_window, initial_wmf); } } -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_window_resize(int state, int width, int height) { + if (jwindow) { + LOG3("jWindowNotifyResize: %d -> %d, %d\n", state, width, height); + mainEnv->CallVoidMethod(jwindow, jWindowNotifyResize, state, width, height); + CHECK_JNI_EXCEPTION(mainEnv) + } +} - // 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); +void WindowContext::notify_window_move(int x, int y) { + if (jwindow) { + LOG2("jWindowNotifyMove: %d, %d\n", x, y); + mainEnv->CallVoidMethod(jwindow, jWindowNotifyMove, x, y); CHECK_JNI_EXCEPTION(mainEnv) + } +} - if (jview) { - mainEnv->CallVoidMethod(jview, jViewNotifyResize, event->width, event->height); - CHECK_JNI_EXCEPTION(mainEnv) - } +void WindowContext::notify_view_resize(int width, int height) { + if (jview) { + LOG2("jViewNotifyResize: %d, %d\n", width, height); + mainEnv->CallVoidMethod(jview, jViewNotifyResize, width, height); + CHECK_JNI_EXCEPTION(mainEnv) } +} + +void WindowContext::notify_current_sizes() { + int ww, wh, cw, ch; + + get_window_size(&ww, &wh); + get_view_size(&cw, &ch); - if (!is_iconified && !is_fullscreen && !is_maximized) { - geometry.final_width.value = (geometry.final_width.type == BOUNDSTYPE_CONTENT) - ? event->width : ww; + GdkWindowState state = (gtk_widget_get_realized(gtk_widget)) + ? gdk_window_get_state(gdk_window) + : (GdkWindowState) 0; - geometry.final_height.value = (geometry.final_height.type == BOUNDSTYPE_CONTENT) - ? event->height : wh; + notify_window_resize((state & GDK_WINDOW_STATE_MAXIMIZED) + ? com_sun_glass_events_WindowEvent_MAXIMIZE + : com_sun_glass_events_WindowEvent_RESIZE, + ww, wh); + + notify_view_resize(cw, ch); +} + +void WindowContext::notify_view_move() { + if (jview) { + LOG0("com_sun_glass_events_ViewEvent_MOVE\n"); + mainEnv->CallVoidMethod(jview, jViewNotifyView, + com_sun_glass_events_ViewEvent_MOVE); + CHECK_JNI_EXCEPTION(mainEnv) + } +} + +void WindowContext::process_configure(GdkEventConfigure *event) { + LOG5("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); + + GdkWindowState state = gdk_window_get_state(gdk_window); + + if (state & GDK_WINDOW_STATE_ICONIFIED) { + return; } 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); - // 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; - - // 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. + // 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. geometry.view_x = origin_x - root_x; geometry.view_y = origin_y - root_y; - notify_window_move(); - - 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; + LOG2("view x, y: %d, %d\n", geometry.view_x, geometry.view_y); + + int cw = event->width; + int ch = event->height; + + notify_view_resize(cw, ch); + notify_view_move(); + + int ww = cw; + int wh = ch; + + // Fullscreen usually have no decorations + if (geometry.view_x > 0) { + ww += geometry.extents.width; + } + + if (geometry.view_y > 0) { + wh += geometry.extents.height; + } + + notify_window_resize((state & GDK_WINDOW_STATE_MAXIMIZED) + ? com_sun_glass_events_WindowEvent_MAXIMIZE + : com_sun_glass_events_WindowEvent_RESIZE, + ww, wh); + + notify_window_move(root_x, root_y); + + glong to_screen = getScreenPtrForLocation(event->x, event->y); + if (to_screen != -1 && to_screen != screen) { + if (jwindow) { + LOG0("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; +void WindowContext::remove_window_constraints() { + LOG0("remove_window_constraints\n"); + GdkGeometry reset; + reset.min_width = 1; + reset.min_height = 1; + reset.max_width = G_MAXINT; + reset.max_height = G_MAXINT; + + gtk_window_set_geometry_hints(GTK_WINDOW(gtk_widget), NULL, &reset, + (GdkWindowHints)(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE)); +} + +void WindowContext::update_window_constraints() { + int cw, ch; + get_view_size(&cw, &ch); + update_window_constraints(cw, ch); +} - if (!is_floating) { - // window is not floating on the screen +void WindowContext::update_window_constraints(int width, int height) { + // Not ready to re-apply the constraints + if ((gtk_widget_get_realized(gtk_widget) && !is_window_floating(gdk_window_get_state(gdk_window))) + || !is_window_floating((GdkWindowState) initial_state_mask)) { + LOG0("not floating: update_window_constraints ignored\n"); return; } GdkGeometry hints; if (resizable.value && !is_disabled) { - int min_w = (resizable.minw == -1) ? 1 - : resizable.minw - geometry.extents.left - geometry.extents.right; - int min_h = (resizable.minh == -1) ? 1 - : resizable.minh - geometry.extents.top - geometry.extents.bottom; - - hints.min_width = (min_w < 1) ? 1 : min_w; - hints.min_height = (min_h < 1) ? 1 : min_h; - - hints.max_width = (resizable.maxw == -1) ? G_MAXINT - : resizable.maxw - geometry.extents.left - geometry.extents.right; - - hints.max_height = (resizable.maxh == -1) ? G_MAXINT - : resizable.maxh - geometry.extents.top - geometry.extents.bottom; + hints.min_width = (resizable.minw == -1) + ? 1 + : NONNEGATIVE_OR(resizable.minw - geometry.extents.width, 1); + hints.min_height = (resizable.minh == -1) + ? 1 + : NONNEGATIVE_OR(resizable.minh - geometry.extents.height, 1); + hints.max_width = (resizable.maxw == -1) + ? G_MAXINT + : NONNEGATIVE_OR(resizable.maxw - geometry.extents.width, 1); + hints.max_height = (resizable.maxh == -1) + ? G_MAXINT + : NONNEGATIVE_OR(resizable.maxh - geometry.extents.height, 1); } else { - int w = geometry_get_content_width(&geometry); - int h = geometry_get_content_height(&geometry); - - hints.min_width = w; - hints.min_height = h; - hints.max_width = w; - hints.max_height = h; + hints.min_width = width; + hints.min_height = height; + hints.max_width = width; + hints.max_height = height; } + LOG4("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); + gtk_window_set_geometry_hints(GTK_WINDOW(gtk_widget), NULL, &hints, - (GdkWindowHints)(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE)); + (GdkWindowHints) (GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE)); } -void WindowContextTop::set_resizable(bool res) { +void WindowContext::set_resizable(bool res) { resizable.value = res; update_window_constraints(); } -void WindowContextTop::set_visible(bool visible) { - WindowContextBase::set_visible(visible); - - if (visible && !geometry.size_assigned) { - set_bounds(0, 0, false, false, 320, 200, -1, -1, 0, 0); - } +void WindowContext::set_visible(bool visible) { + LOG1("set_visible: %d\n", visible); + if (visible) { + gtk_widget_show(gtk_widget); - //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); + // JDK-8220272 - fire event first because GDK_FOCUS_CHANGE is not always in order + if (jwindow && isEnabled()) { + mainEnv->CallVoidMethod(jwindow, jWindowNotifyFocus, com_sun_glass_events_WindowEvent_FOCUS_GAINED); + CHECK_JNI_EXCEPTION(mainEnv); + } + } 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) + } } } -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) { + LOG10("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; @@ -1070,58 +1205,62 @@ void WindowContextTop::set_bounds(int x, int y, bool xSet, bool ySet, int w, int geometry.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); + newW = NONNEGATIVE_OR(w - geometry.extents.width, 1); } else if (cw > 0) { - geometry.final_width.type = BOUNDSTYPE_CONTENT; - geometry.final_width.value = cw; 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); + newH = NONNEGATIVE_OR(h - geometry.extents.height, 1); } 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 (xSet) geometry.x = x; + if (ySet) geometry.y = y; - 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 (newW > 0) geometry.width = newW; + if (newH > 0) geometry.height = newH; - 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); - } - geometry.size_assigned = true; - notify_window_resize(); - } + LOG2("set_bounds: geometry.width = %d, geometry.height = %d\n", geometry.width, geometry.height); - if (xSet || ySet) { - if (xSet) { - geometry.x = x; + if (gtk_widget_get_realized(gtk_widget)) { + GdkWindowState state = gdk_window_get_state(gdk_window); + + // If it is in fullscreen mode, it will be applied later on restore + if (!geometry.needs_to_restore_geometry && + (state & GDK_WINDOW_STATE_FULLSCREEN)) { + LOG0("set_bounds: needs_to_restore_geometry = true\n"); + geometry.needs_to_restore_geometry = true; } - if (ySet) { - geometry.y = y; + if (geometry.needs_to_restore_geometry || (state & GDK_WINDOW_STATE_MAXIMIZED)) { + LOG0("need to restore geometry of maximized\n"); + // Report back to java with current sizes + if (newW > 0 || newH > 0) { + notify_current_sizes(); + } + + if (xSet || xSet) { + int x, y; + gdk_window_get_root_origin(gdk_window, &x, &y); + notify_window_move(x, y); + } + + return; } + } - gtk_window_move(GTK_WINDOW(gtk_widget), geometry.x, geometry.y); - notify_window_move(); + // Re-apply the constraints removed for fullscreen / maximize + if (!resizable.value) { + update_window_constraints(newW, newH); } + + resize(newW, newH); + move(x, y, xSet, ySet); } -void WindowContextTop::applyShapeMask(void* data, uint width, uint height) { +void WindowContext::applyShapeMask(void* data, uint width, uint height) { if (frame_type != TRANSPARENT) { return; } @@ -1129,20 +1268,9 @@ void WindowContextTop::applyShapeMask(void* data, uint width, uint height) { glass_window_apply_shape_mask(gtk_widget_get_window(gtk_widget), data, width, height); } -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); - } +void WindowContext::iconify(bool state) { + if (state) { + add_wmf(GDK_FUNC_MINIMIZE); gtk_window_iconify(GTK_WINDOW(gtk_widget)); } else { gtk_window_deiconify(GTK_WINDOW(gtk_widget)); @@ -1150,13 +1278,14 @@ void WindowContextTop::set_minimized(bool minimize) { } } -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); + + if (!resizable.value) { + remove_window_constraints(); + process_pending_events(); + } gtk_window_maximize(GTK_WINDOW(gtk_widget)); } else { @@ -1164,63 +1293,107 @@ void WindowContextTop::set_maximized(bool maximize) { } } -void WindowContextTop::enter_fullscreen() { - gtk_window_fullscreen(GTK_WINDOW(gtk_widget)); - is_fullscreen = true; +void WindowContext::set_minimized(bool state) { + LOG1("set_minimized = %d\n", state); + if (was_mapped) { + iconify(state); + } else { + initial_state_mask = state + ? (initial_state_mask | GDK_WINDOW_STATE_ICONIFIED) + : (initial_state_mask & ~GDK_WINDOW_STATE_ICONIFIED); + } } -void WindowContextTop::exit_fullscreen() { - gtk_window_unfullscreen(GTK_WINDOW(gtk_widget)); +void WindowContext::set_maximized(bool state) { + LOG1("set_maximized = %d\n", state); + if (was_mapped) { + maximize(state); + } else { + initial_state_mask = state + ? (initial_state_mask | GDK_WINDOW_STATE_MAXIMIZED) + : (initial_state_mask & ~GDK_WINDOW_STATE_MAXIMIZED); + } } -void WindowContextTop::request_focus() { - if (is_visible()) { - gtk_window_present(GTK_WINDOW(gtk_widget)); +void WindowContext::enter_fullscreen() { + LOG0("enter_fullscreen\n"); + if (was_mapped) { + // save state before fullscreen to work-around an issue were + // it would restore to max-size + save_geometry(); + geometry.needs_to_restore_geometry = true; + + if (!resizable.value) { + remove_window_constraints(); + process_pending_events(); + // Needs to happen "in the future" because constraints removal are not applied immediately + gdk_threads_add_idle((GSourceFunc) enter_fullscreen_later, GTK_WINDOW(gtk_widget)); + } else { + gtk_window_fullscreen(GTK_WINDOW(gtk_widget)); + } + } 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::exit_fullscreen() { + LOG0("exit_fullscreen\n"); + if (was_mapped) { + gtk_window_unfullscreen(GTK_WINDOW(gtk_widget)); + } else { + initial_state_mask &= ~GDK_WINDOW_STATE_FULLSCREEN; + } } -void WindowContextTop::set_title(const char* title) { - gtk_window_set_title(GTK_WINDOW(gtk_widget), title); +void WindowContext::request_focus() { + LOG0("request_focus\n"); + if (!is_visible()) return; + + gtk_window_present(GTK_WINDOW(gtk_widget)); } -void WindowContextTop::set_alpha(double alpha) { - gtk_window_set_opacity(GTK_WINDOW(gtk_widget), (gdouble)alpha); +void WindowContext::set_focusable(bool focusable) { + gtk_window_set_accept_focus(GTK_WINDOW(gtk_widget), focusable ? TRUE : FALSE); +} + +void WindowContext::set_title(const char* title) { + gtk_window_set_title(GTK_WINDOW(gtk_widget), title); } -void WindowContextTop::set_enabled(bool enabled) { +void WindowContext::set_enabled(bool enabled) { is_disabled = !enabled; update_window_constraints(); } -void WindowContextTop::set_minimum_size(int w, int h) { - resizable.minw = (w <= 0) ? 1 : w; - resizable.minh = (h <= 0) ? 1 : h; +void WindowContext::set_minimum_size(int w, int h) { + LOG2("set_minimum_size: %d, %d\n", w, h); + resizable.minw = w; + resizable.minh = h; update_window_constraints(); } -void WindowContextTop::set_maximum_size(int w, int h) { - resizable.maxw = w; - resizable.maxh = h; +void WindowContext::set_maximum_size(int w, int h) { + LOG2("set_maximum_size: %d, %d\n", w, h); + resizable.maxw = (w == -1) ? -1 : w; + resizable.maxh = (h == -1) ? -1 : h; update_window_constraints(); } -void WindowContextTop::set_icon(GdkPixbuf* pixbuf) { +void WindowContext::set_icon(GdkPixbuf* pixbuf) { gtk_window_set_icon(GTK_WINDOW(gtk_widget), pixbuf); } -void WindowContextTop::to_front() { +void WindowContext::to_front() { + LOG0("to_front\n"); gdk_window_raise(gdk_window); } -void WindowContextTop::to_back() { +void WindowContext::to_back() { + LOG0("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) { @@ -1230,26 +1403,26 @@ void WindowContextTop::set_modal(bool modal, WindowContext* parent) { gtk_window_set_modal(GTK_WINDOW(gtk_widget), modal ? TRUE : FALSE); } -GtkWindow *WindowContextTop::get_gtk_window() { +GtkWindow *WindowContext::get_gtk_window() { return GTK_WINDOW(gtk_widget); } -WindowGeometry WindowContextTop::get_geometry() { +WindowGeometry WindowContext::get_geometry() { return geometry; } -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) { + 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; @@ -1259,15 +1432,134 @@ 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::get_view_size(int *width, int *height) { + if (gtk_widget_get_realized(gtk_widget)) { + *width = gdk_window_get_width(gdk_window); + *height = gdk_window_get_height(gdk_window); + } else { + *width = geometry.width; + *height = geometry.height; + } + + LOG2("get_view_size: %d, %d\n", *width, *height); +} + +void WindowContext::get_window_size(int *width, int *height) { + int ww, wh; + get_view_size(&ww, &wh); + + if (gtk_widget_get_realized(gtk_widget)) { + 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); + + // Here is detected if there are any decorations as it might vary, for example + // if the window is fullscreen + if ((origin_x - root_x) > 0) { + ww += geometry.extents.width; + } + + if ((origin_y - root_y) > 0) { + wh += geometry.extents.height; + } + } + + LOG2("get_window_size: %d, %d\n", ww, wh); + *width = ww; + *height = wh; +} + +// Values are view size +void WindowContext::resize(int width, int height) { + LOG2("resize (requested): %d, %d\n", width, height); + int current_width, current_height; + get_view_size(¤t_width, ¤t_height); + + int newW = (width <= 0) ? current_width : width; + int newH = (height <= 0) ? current_height : height; + + // Windows that are undecorated or transparent will not respect + // minimum or maximum size constraints + if (resizable.minw > 0 && newW < resizable.minw) { + newW = NONNEGATIVE_OR(resizable.minw - geometry.extents.width, 1); + } + + if (resizable.minh > 0 && newH < resizable.minh) { + newH = NONNEGATIVE_OR(resizable.minh - geometry.extents.height, 1); + } + + if (resizable.maxw > 0 && newW > resizable.maxw) { + newW = NONNEGATIVE_OR(resizable.maxw - geometry.extents.width, 1); + } + + if (resizable.maxh > 0 && newH > resizable.maxh) { + newH = NONNEGATIVE_OR(resizable.maxh - geometry.extents.height, 1); + } + + LOG2("resize (real): %d, %d\n", newW, newH); + + if (gtk_widget_get_realized(gtk_widget)) { + gtk_window_resize(GTK_WINDOW(gtk_widget), newW, newH); + // If not changed, configure event will not happen, so we need to notify here + if (current_width == newW && current_height == newH) notify_current_sizes(); + } else { + gtk_window_set_default_size(GTK_WINDOW(gtk_widget), newW, newH); + // If the GdkWindow is not yet created, report back to Java, because the configure event + // won't happen + notify_current_sizes(); + } +} + +void WindowContext::move(int x, int y) { + move(x, y, true, true); +} + +void WindowContext::move(int x, int y, bool xSet, bool ySet) { + LOG2("move %d, %d\n", x, y); + int to_x = x; + int to_y = y; + + if (!xSet || !ySet) { + int cur_x, cur_y; + + if (gtk_widget_get_realized(gtk_widget)) { + gdk_window_get_root_origin(gdk_window, &cur_x, &cur_y); + } else { + cur_x = geometry.x; + cur_y = geometry.y; + } + + if (!xSet) to_x = cur_x; + if (!ySet) to_y = cur_y; + } + + gtk_window_move(GTK_WINDOW(gtk_widget), to_x, to_y); +} + + +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) { @@ -1284,7 +1576,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 @@ -1298,58 +1590,24 @@ 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() { - // 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) { - - notify_view_resize(); - } -} - -void WindowContextTop::notify_view_resize() { +void WindowContext::update_view_size() { 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); + int cw, ch; + get_view_size(&cw, &ch); - 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) + if (cw > 0 && ch > 0) { + notify_view_resize(cw, ch); } } } -void WindowContextTop::process_destroy() { - if (owner) { - owner->remove_child(this); - } - - WindowContextBase::process_destroy(); +WindowContext::~WindowContext() { + LOG0("~WindowContext\n"); + disableIME(); + gtk_widget_destroy(gtk_widget); } + 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 5d58c9d2e77..784d4f208f3 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,21 +25,18 @@ #ifndef GLASS_WINDOW_H #define GLASS_WINDOW_H +#define USER_PTR_TO_CTX(value) ((WindowContext *) value) + #include -#include #include #include #include +#include #include "DeletedMemDebug.h" - #include "glass_view.h" - -enum WindowManager { - COMPIZ, - UNKNOWN -}; +#include "glass_general.h" enum WindowFrameType { TITLED, @@ -53,127 +50,55 @@ 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_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; + WindowGeometry(): + needs_to_restore_geometry(false), + width(-1), height(-1), x(), y(), view_x(), view_y(), + gravity_x(), gravity_y(), + extents(), frame_extents_received(false) {} + + bool needs_to_restore_geometry; + + // width, height, x, w are not update by user interactions and + // may not reflect current geometry. + // width / height are content size + int width; + int height; int x; int y; + int view_x; int view_y; float gravity_x; float gravity_y; - WindowFrameExtents extents; + GdkRectangle extents; + + bool frame_extents_received; }; -class WindowContextTop; +class WindowContext; -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 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 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_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*) = 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 is_dead() = 0; - virtual ~WindowContext() {} -}; +class WindowContext: public DeletedMemDebug<0xCC> { +private: + static std::optional normal_extents; + static std::optional utility_extents; -class WindowContextBase: public WindowContext { + 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) {} + bool value; //actual value of resizable for a window + int minw, minh, maxw, maxh; //minimum and maximum window width/height; + } resizable; struct ImContext { GtkIMContext *ctx; @@ -183,21 +108,26 @@ 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; - GdkWMFunction gdk_windowManagerFunctions; + jobject jview{}; + + GtkWidget *gtk_widget; + GdkWindow *gdk_window{}; + GdkWMFunction initial_wmf; + GdkWMFunction current_wmf; - bool is_iconified; - bool is_maximized; bool is_mouse_entered; bool is_disabled; + bool on_top; + bool can_be_deleted; + bool was_mapped; + gint initial_state_mask; +protected: +protected: /* * sm_grab_window points to WindowContext holding a mouse grab. * It is mostly used for popup windows. @@ -216,22 +146,26 @@ class WindowContextBase: public WindowContext { * should be reported during this drag. */ static WindowContext* sm_mouse_drag_window; + public: + WindowContext(jobject, WindowContext*, long, WindowFrameType, WindowType, GdkWMFunction); + 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(); - void add_child(WindowContextTop*); - void remove_child(WindowContextTop*); + void add_child(WindowContext*); + void remove_child(WindowContext*); void set_visible(bool); bool is_visible(); bool set_view(jobject); @@ -240,62 +174,27 @@ class WindowContextBase: public WindowContext { void ungrab_focus(); void ungrab_mouse_drag_focus(); void set_cursor(GdkCursor*); - void set_level(int) {} - void set_background(float, float, float); + void process_map(); void process_focus(GdkEventFocus*); - void process_destroy(); - void process_delete(); - void process_expose(GdkEventExpose*); + void notify_repaint(GdkRectangle*); void process_mouse_button(GdkEventButton*); 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_realize(); + 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 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) {} - bool value; //actual value of resizable for a window - int minw, minh, maxw, maxh; //minimum and maximum window width/height; - } 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 work_around_compiz_state(); - WindowGeometry get_geometry(); void set_minimized(bool); @@ -305,7 +204,6 @@ class WindowContextTop: public WindowContextBase { void request_focus(); void set_focusable(bool); void set_title(const char*); - void set_alpha(double); void set_enabled(bool); void set_minimum_size(int, int); void set_maximum_size(int, int); @@ -314,35 +212,49 @@ class WindowContextTop: public WindowContextBase { void to_back(); void set_modal(bool, WindowContext* parent = NULL); 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 enter_fullscreen(); void exit_fullscreen(); - void set_owner(WindowContext*); - GtkWindow *get_gtk_window(); - void detach_from_java(); + void update_window_size_location(); + void update_initial_state(); + + ~WindowContext(); protected: void applyShapeMask(void*, uint width, uint height); private: + void maximize(bool); + void iconify(bool); + void get_view_size(int *, int *); + void get_window_size(int *, int *); + void resize(int, int); + void move(int, int, bool, bool); + void move(int, int); + void add_wmf(GdkWMFunction); + void remove_wmf(GdkWMFunction); + void save_geometry(); + void notify_on_top(bool); + void notify_window_resize(int, int, int); + void notify_window_move(int, int); + void notify_view_resize(int, int); + void notify_view_move(); + void notify_current_sizes(); + 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(GdkRectangle); + void load_cached_extents(); bool get_frame_extents_property(int *, int *, int *, int *); + void remove_window_constraints(); void update_window_constraints(); + void update_window_constraints(int, int); void update_ontop_tree(bool); bool on_top_inherited(); bool effective_on_top(); - void notify_window_move(); - void notify_window_resize(); - WindowContextTop(WindowContextTop&); - WindowContextTop& operator= (const WindowContextTop&); }; void destroy_and_delete_ctx(WindowContext* ctx); @@ -361,6 +273,7 @@ class EventsCounterHelper { if (ctx != nullptr) { ctx->decrement_events_counter(); if (ctx->is_dead() && ctx->get_events_count() == 0) { + LOG0("EventsCounterHelper: delete ctx\n"); delete ctx; } ctx = NULL; 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..e761402073c 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,11 +136,11 @@ 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(), @@ -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,7 +185,7 @@ void WindowContextBase::enableOrResetIME() { im_ctx.enabled = true; } -void WindowContextBase::disableIME() { +void WindowContext::disableIME() { if (im_ctx.ctx != NULL) { g_object_unref(im_ctx.ctx); im_ctx.ctx = NULL; diff --git a/tests/manual/stage/TestStage.java b/tests/manual/stage/TestStage.java new file mode 100644 index 00000000000..cc5da136866 --- /dev/null +++ b/tests/manual/stage/TestStage.java @@ -0,0 +1,196 @@ +/* + * 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.DoubleProperty; +import javafx.collections.FXCollections; +import javafx.geometry.Orientation; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.Background; +import javafx.scene.layout.VBox; +import javafx.stage.StageStyle; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.FlowPane; +import javafx.scene.paint.Color; +import javafx.stage.Stage; + +public class TestStage extends Application { + private Stage testStage = new Stage(); + private Label lblWidth = new Label(); + private Label lblHeight = new Label(); + private Label lblMinWidth = new Label(); + private Label lblMinHeight = new Label(); + private Label lblMaxWidth = new Label(); + private Label lblMaxHeight = new Label(); + private Label lblX = new Label(); + private Label lblY = new Label(); + private Label lblSceneWidth = new Label(); + private Label lblSceneHeight = new Label(); + private Label lblSceneX = new Label(); + private Label lblSceneY = new Label(); + private ComboBox cbStageStyle = new ComboBox<>(FXCollections.observableArrayList(StageStyle.values())); + private CheckBox cbIsFullScreen = new CheckBox("Is FullScreen"); + private CheckBox cbIsMaximized = new CheckBox("Is Maximized"); + private CheckBox cbIsIconified = new CheckBox("Is Iconified"); + private CheckBox cbIsResizable = new CheckBox("Is Resizable"); + + @Override + public void start(Stage stage) { + cbStageStyle.valueProperty().addListener((observable, oldValue, newValue) -> { + testStage.initStyle(StageStyle.valueOf(newValue.name())); + }); + + cbStageStyle.getSelectionModel().select(StageStyle.DECORATED); + + Button btnMaxminize = new Button("Toggle Maximize"); + btnMaxminize.setOnAction(e -> testStage.setMaximized(!testStage.isMaximized())); + + Button btnFullScreen = new Button("Toggle FullScreen"); + btnFullScreen.setOnAction(e -> testStage.setFullScreen(!testStage.isFullScreen())); + + Button btnIconify = new Button("Toggle Iconified"); + btnIconify.setOnAction(e -> testStage.setIconified(!testStage.isIconified())); + + Button btnResizable = new Button("Toggle Resizable"); + btnResizable.setOnAction(e -> testStage.setResizable(!testStage.isResizable())); + + Button btnShow = new Button("Show"); + btnShow.setOnAction(e -> { + testStage.show(); + }); + + Button btnClose = new Button("Close"); + btnClose.setOnAction(e -> { + testStage.close(); + createTestStage(); + }); + + Button btnSizeToScene = new Button("Size to scene"); + btnSizeToScene.setOnAction(e -> { + testStage.sizeToScene(); + }); + + Button btnCenterOnScreen = new Button("Center on screen"); + btnCenterOnScreen.setOnAction(e -> { + testStage.centerOnScreen(); + }); + + Button btnResize = new Button("Resize"); + btnResize.setOnAction(e -> { + testStage.setWidth(300); + testStage.setHeight(300); + }); + + Button btnMaxSize = new Button("Set Max Size"); + btnMaxSize.setOnAction(e -> { + testStage.setMaxWidth(250); + testStage.setMaxHeight(250); + }); + + Button btnUnsetMaxSize = new Button("Unset Max Size"); + btnUnsetMaxSize.setOnAction(e -> { + testStage.setMaxWidth(Double.MAX_VALUE); + testStage.setMaxHeight(Double.MAX_VALUE); + }); + + Button btnMove = new Button("Move"); + btnMove.setOnAction(e -> { + testStage.setX(100); + testStage.setY(100); + }); + + cbIsMaximized.setDisable(true); + cbIsFullScreen.setDisable(true); + cbIsIconified.setDisable(true); + cbIsResizable.setDisable(true); + + FlowPane commandPane = new FlowPane(cbStageStyle, btnShow, btnClose, btnSizeToScene, btnCenterOnScreen, + btnResize, btnMaxSize, btnUnsetMaxSize, btnMove, btnIconify, btnMaxminize, btnFullScreen, btnResizable); + commandPane.setHgap(5); + commandPane.setVgap(5); + + + VBox root = new VBox(commandPane, + new Separator(Orientation.HORIZONTAL), + new Label("Stage Properties:"), + cbIsIconified, cbIsMaximized, + cbIsFullScreen, cbIsResizable, + lblMinWidth, lblMinHeight, lblMaxWidth, lblMaxHeight, + lblWidth, lblHeight, lblX, lblY, + new Separator(Orientation.HORIZONTAL), + new Label("Scene Properties:"), + lblSceneWidth, lblSceneHeight, lblSceneX, lblSceneY); + root.setSpacing(5); + root.setFillWidth(true); + + createTestStage(); + + Scene scene = new Scene(root, 500, 600); + stage.setTitle("Command Stage"); + stage.setScene(scene); + stage.show(); + } + + private void createTestStage() { + testStage = new Stage(); + + StackPane stackPane = new StackPane(); + stackPane.setBackground(Background.fill(Color.TRANSPARENT)); + testStage = new Stage(); + Scene testScene = new Scene(stackPane, 300, 300, Color.HOTPINK); + testStage.setScene(testScene); + testStage.initStyle(cbStageStyle.getValue()); + testStage.setTitle("Test Stage"); + testStage.setWidth(800); + testStage.setHeight(600); + + cbIsMaximized.selectedProperty().bind(testStage.maximizedProperty()); + cbIsFullScreen.selectedProperty().bind(testStage.fullScreenProperty()); + cbIsIconified.selectedProperty().bind(testStage.iconifiedProperty()); + cbIsResizable.selectedProperty().bind(testStage.resizableProperty()); + lblWidth.textProperty().bind(Bindings.format("Width: %.2f", testStage.widthProperty())); + lblHeight.textProperty().bind(Bindings.format("Height: %.2f", testStage.heightProperty())); + lblMinWidth.textProperty().bind(Bindings.format("Min Width: %.2f", testStage.minWidthProperty())); + lblMinHeight.textProperty().bind(Bindings.format("Min Height: %.2f", testStage.minHeightProperty())); + lblMaxWidth.textProperty().bind(Bindings.format("Max Width: %.2f", testStage.maxWidthProperty())); + lblMaxHeight.textProperty().bind(Bindings.format("Max Height: %.2f", testStage.maxHeightProperty())); + lblX.textProperty().bind(Bindings.format("X: %.2f", testStage.xProperty())); + lblY.textProperty().bind(Bindings.format("Y: %.2f", testStage.yProperty())); + lblSceneWidth.textProperty().bind(Bindings.format("Width: %.2f", testScene.widthProperty())); + lblSceneHeight.textProperty().bind(Bindings.format("Height: %.2f", testScene.heightProperty())); + lblSceneX.textProperty().bind(Bindings.format("X: %.2f", testScene.xProperty())); + lblSceneY.textProperty().bind(Bindings.format("Y: %.2f", testScene.yProperty())); + } + + public static void main(String[] args) { + launch(TestStage.class, args); + } +} 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 fe172c75422..c3232c3b4e4 100644 --- a/tests/system/src/test/java/test/javafx/scene/RestoreSceneSizeTest.java +++ b/tests/system/src/test/java/test/javafx/scene/RestoreSceneSizeTest.java @@ -91,8 +91,7 @@ public static void teardown() { @Test public void testUnfullscreenSize() throws Exception { // Disable on Mac until JDK-8176813 is fixed - // Disable on Linux until JDK-8353556 is fixed - assumeTrue(!(PlatformUtil.isMac() || PlatformUtil.isLinux())); + assumeTrue(!PlatformUtil.isMac()); Thread.sleep(200); final double w = (Math.ceil(WIDTH * scaleX)) / scaleX; 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..e2073dadcc2 --- /dev/null +++ b/tests/system/src/test/java/test/javafx/stage/CenterOnScreenTest.java @@ -0,0 +1,125 @@ +/* + * 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", "UNDECORATED", "TRANSPARENT"}) + void testStateCenterOnScreenWhenShown(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", "UNDECORATED", "TRANSPARENT"}) + void testStateCenterOnScreenWhenShownWithSceneSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, null); + Util.sleep(MEDIUM_WAIT); + assertStageCentered(stageStyle, true); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "TRANSPARENT"}) + void testStateCenterOnScreenAfterShown(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", "UNDECORATED", "TRANSPARENT"}) + void testStateCenterOnScreenAfterShownWithSceneSize(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..77ad599a274 --- /dev/null +++ b/tests/system/src/test/java/test/javafx/stage/FullScreenTest.java @@ -0,0 +1,141 @@ +/* + * 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 = 100; + private static final int HEIGHT = 150; + + private static final int SHOW_WIDTH = 500; + private static final int SHOW_HEIGHT = 500; + private static final int SHOW_X = 500; + private static final int SHOW_Y = 500; + + private static final Consumer TEST_SETTINGS = s -> { + s.setWidth(WIDTH); + s.setHeight(HEIGHT); + s.setX(POS_X); + s.setY(POS_Y); + }; + + private static final Consumer CHANGE_GEOMETRY_TESTS_SETTINGS = s -> { + s.setWidth(SHOW_WIDTH); + s.setHeight(SHOW_HEIGHT); + s.setX(SHOW_X); + s.setY(SHOW_Y); + }; + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "TRANSPARENT"}) + void testFullScreenShouldKeepGeometryOnRestore(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", "TRANSPARENT"}) + void testFullScreenBeforeShowShouldKeepGeometryOnRestore(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(); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "TRANSPARENT"}) + void testUnFullScreenChangedPosition(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, CHANGE_GEOMETRY_TESTS_SETTINGS); + + Util.doTimeLine(LONG_WAIT, + () -> getStage().setFullScreen(true), + () -> assertTrue(getStage().isFullScreen()), + () -> { + getStage().setX(POS_X); + getStage().setY(POS_Y); + }, + () -> getStage().setFullScreen(false)); + + Util.sleep(LONG_WAIT); + + assertEquals(POS_X, getStage().getX(), POSITION_DELTA, + "Window failed to restore position set while fullscreened"); + assertEquals(POS_Y, getStage().getY(), POSITION_DELTA, + "Window failed to restore position set while fullscreened"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "TRANSPARENT"}) + void testUnFullScreenChangedSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, CHANGE_GEOMETRY_TESTS_SETTINGS); + + Util.doTimeLine(LONG_WAIT, + () -> getStage().setFullScreen(true), + () -> assertTrue(getStage().isFullScreen()), + () -> { + getStage().setWidth(WIDTH); + getStage().setHeight(HEIGHT); + }, + () -> getStage().setFullScreen(false)); + + Util.sleep(LONG_WAIT); + + assertEquals(WIDTH, getStage().getWidth(), SIZING_DELTA, "Window failed to restore size set while fullscreened"); + assertEquals(HEIGHT, getStage().getHeight(), SIZING_DELTA, "Window failed to restore size set while fullscreened"); + } + + 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..04f495fe134 --- /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", "TRANSPARENT"}) + void testMaximizeUndecorated(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, TEST_SETTINGS); + + Util.doTimeLine(SHORT_WAIT, + () -> getStage().setMaximized(true), + () -> { + assertTrue(getStage().isMaximized()); + assertNotEquals(POS_X, getStage().getX()); + assertNotEquals(POS_Y, getStage().getY()); + }, + () -> getStage().setMaximized(false)); + + Util.sleep(SHORT_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", "TRANSPARENT"}) + void testMaximizeKeepGeometryOnRestore(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, TEST_SETTINGS); + + Util.doTimeLine(SHORT_WAIT, + () -> getStage().setMaximized(true), + () -> assertTrue(getStage().isMaximized()), + () -> getStage().setMaximized(false)); + + Util.sleep(SHORT_WAIT); + assertSizePosition(); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "TRANSPARENT"}) + void testMaximizeBeforeShowShouldKeepGeometryOnRestore(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, TEST_SETTINGS.andThen(s -> s.setMaximized(true))); + Util.sleep(SHORT_WAIT); + + Util.runAndWait(() -> { + assertTrue(getStage().isMaximized()); + getStage().setMaximized(false); + }); + Util.sleep(SHORT_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..0725a4f9d06 --- /dev/null +++ b/tests/system/src/test/java/test/javafx/stage/SizingTest.java @@ -0,0 +1,319 @@ +/* + * 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.control.Label; +import javafx.scene.layout.Background; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +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 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", "TRANSPARENT"}) + void testMaximizeUnresizable(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + s.setWidth(WIDTH); + s.setHeight(HEIGHT); + s.setResizable(false); + }); + Util.runAndWait(() -> getStage().setMaximized(true)); + Util.sleep(MEDIUM_WAIT); + + assertTrue(getStage().isMaximized(), "Unresizable stage should be maximized"); + assertTrue(getStage().getWidth() > WIDTH, "Stage width should be maximized"); + assertTrue(getStage().getHeight() > HEIGHT, "Stage height should be maximized"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "TRANSPARENT"}) + void testFullScreenUnresizable(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + s.setWidth(WIDTH); + s.setHeight(HEIGHT); + s.setResizable(false); + }); + + Util.runAndWait(() -> getStage().setFullScreen(true)); + Util.sleep(LONG_WAIT); + assertTrue(getStage().isFullScreen(), "Unresizable stage should be fullscreen"); + assertTrue(getStage().getWidth() > WIDTH, "Stage width should be fullscreen"); + assertTrue(getStage().getHeight() > HEIGHT, "Stage height should be fullscreen"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "TRANSPARENT", "UTILITY"}) + void testResizeUnresizable(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + s.setWidth(WIDTH); + s.setHeight(HEIGHT); + s.setResizable(false); + }); + + Util.runAndWait(() -> { + getStage().setWidth(NEW_WIDTH); + getStage().setHeight(NEW_HEIGHT); + }); + Util.sleep(MEDIUM_WAIT); + + assertEquals(NEW_WIDTH, getStage().getWidth(), SIZING_DELTA, "Stage should have resized"); + assertEquals(NEW_HEIGHT, getStage().getHeight(), SIZING_DELTA, "Stage should have resized"); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED", "TRANSPARENT", "UTILITY"}) + void testMaxSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + 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", "TRANSPARENT", "UTILITY"}) + void testMaxWidth(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", "TRANSPARENT", "UTILITY"}) + void testMaxHeight(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + 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", "TRANSPARENT", "UTILITY"}) + void testMinSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + 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", "TRANSPARENT", "UTILITY"}) + void testMinWidth(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + 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", "TRANSPARENT", "UTILITY"}) + void testMinHeight(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + 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", "TRANSPARENT", "UTILITY"}) + void testNoSize(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> s.initStyle(stageStyle)); + + 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", "TRANSPARENT", "UTILITY"}) + void testNoHeight(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + 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", "TRANSPARENT", "UTILITY"}) + void testNoWidth(StageStyle stageStyle) { + setupStageWithStyle(stageStyle, s -> { + s.initStyle(stageStyle); + 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"); + } +} 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..93adb2823a7 --- /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); + if (pc != null) { + pc.accept(stage); + } + stage.initStyle(stageStyle); + stage.setScene(createScene(stageStyle)); + 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..e9fd99840d7 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,20 +25,24 @@ 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 test.util.Util; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +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; public class StageAttributesTest extends VisualTestBase { @@ -50,11 +54,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 +84,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 +105,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(value = StageStyle.class, mode = EnumSource.Mode.INCLUDE, names = {"DECORATED", "UNDECORATED"}) + public void testIconifiedStage(StageStyle stageStyle) throws InterruptedException { + setupStages(true, true, stageStyle); runAndWait(() -> { Color color = getColor(200, 200); @@ -116,7 +121,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 +130,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 +143,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 +153,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 +170,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 +183,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 +195,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 +204,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 +219,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 +230,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 +244,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 +254,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 +272,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 +286,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 +298,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 +306,130 @@ public void testFullScreenStageBeforeShow() throws InterruptedException { assertColorEquals(TOP_COLOR, color, TOLERANCE); }); } -} + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + void testStageStatePrecedenceOrderIconifiedMaximizedBeforeShow(StageStyle stageStyle) throws InterruptedException { + setupStages(false, false, stageStyle); + + Util.doTimeLine(WAIT, + () -> { + Color color = getColor(200, 200); + assertColorEquals(BOTTOM_COLOR, color, TOLERANCE); + + topStage.setIconified(true); + topStage.setMaximized(true); + topStage.show(); + }, + () -> { + assertTrue(topStage.isIconified()); + assertTrue(topStage.isMaximized()); + + Color color = getColor(200, 200); + // Should remain iconified + assertColorEquals(BOTTOM_COLOR, color, TOLERANCE); + }, + () -> topStage.setIconified(false), + () -> { + assertTrue(topStage.isMaximized()); + assertFalse(topStage.isIconified()); + + Color color = getColor(200, 200); + assertColorEquals(TOP_COLOR, color, TOLERANCE); + }); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + void testStageStatePrecedenceOrderIconifiedFullScreenBeforeShow(StageStyle stageStyle) throws InterruptedException { + setupStages(false, false, stageStyle); + + Util.doTimeLine(WAIT, + () -> { + Color color = getColor(200, 200); + assertColorEquals(BOTTOM_COLOR, color, TOLERANCE); + + topStage.setIconified(true); + topStage.setFullScreen(true); + topStage.show(); + }, + () -> { + assertTrue(topStage.isIconified()); + assertTrue(topStage.isFullScreen()); + + Color color = getColor(200, 200); + // Should remain iconified + assertColorEquals(BOTTOM_COLOR, color, TOLERANCE); + }, + () -> topStage.setIconified(false), + () -> { + assertTrue(topStage.isFullScreen()); + assertFalse(topStage.isIconified()); + + Color color = getColor(200, 200); + assertColorEquals(TOP_COLOR, color, TOLERANCE); + }); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + void testStageStatePrecedenceOrderIconifiedMaximizedAfterShow(StageStyle stageStyle) throws InterruptedException { + setupStages(true, true, stageStyle); + + Util.doTimeLine(WAIT, + () -> { + Color color = getColor(200, 200); + assertColorEquals(TOP_COLOR, color, TOLERANCE); + + topStage.setIconified(true); + topStage.setMaximized(true); + }, + () -> { + assertTrue(topStage.isMaximized()); + assertTrue(topStage.isIconified()); + + Color color = getColor(200, 200); + // Should remain iconified + assertColorEquals(BOTTOM_COLOR, color, TOLERANCE); + }, + () -> topStage.setIconified(false), + () -> { + assertTrue(topStage.isMaximized()); + assertFalse(topStage.isIconified()); + + Color color = getColor(200, 200); + assertColorEquals(TOP_COLOR, color, TOLERANCE); + }); + } + + @ParameterizedTest(name = PARAMETERIZED_TEST_DISPLAY) + @EnumSource(names = {"DECORATED", "UNDECORATED"}) + void testStageStatePrecedenceOrderIconifiedFullScreenAfterShow(StageStyle stageStyle) throws InterruptedException { + setupStages(true, true, stageStyle); + + Util.doTimeLine(WAIT, + () -> { + Color color = getColor(200, 200); + assertColorEquals(TOP_COLOR, color, TOLERANCE); + + topStage.setIconified(true); + topStage.setFullScreen(true); + }, + () -> { + assertTrue(topStage.isFullScreen()); + assertTrue(topStage.isIconified()); + + Color color = getColor(200, 200); + // Should remain iconified + assertColorEquals(BOTTOM_COLOR, color, TOLERANCE); + }, + () -> topStage.setIconified(false), + () -> { + assertTrue(topStage.isFullScreen()); + assertFalse(topStage.isIconified()); + + Color color = getColor(200, 200); + 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..36a35b6923c --- /dev/null +++ b/tests/system/src/test/java/test/robot/javafx/stage/StageLocationTest.java @@ -0,0 +1,145 @@ +/* + * 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.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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import test.robot.testharness.VisualTestBase; +import test.util.Util; + +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; + +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", "UTILITY"}) + void testMove(StageStyle stageStyle) { + Util.runAndWait(() -> stage = createStage(stageStyle)); + + Util.doTimeLine(WAIT, + () -> { + stage.setX(X); + stage.setY(Y); + }, + stage::show, + () -> 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", "UTILITY"}) + void testMoveXAxis(StageStyle stageStyle) { + Util.runAndWait(() -> stage = createStage(stageStyle)); + + Util.doTimeLine(WAIT, + () -> { + stage.setX(X); + stage.setY(Y); + }, + stage::show, + () -> 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", "UTILITY"}) + void testMoveYAxis(StageStyle stageStyle) { + Util.runAndWait(() -> stage = createStage(stageStyle)); + + Util.doTimeLine(WAIT, + () -> { + stage.setX(X); + stage.setY(Y); + }, + stage::show, + () -> 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", "UTILITY"}) + void testMoveAfterShow(StageStyle stageStyle) { + Util.runAndWait(() -> stage = createStage(stageStyle)); + + Util.doTimeLine(WAIT, + stage::show, + () -> { + 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..a3aca966b07 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) 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,60 @@ package test.robot.javafx.stage; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -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.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.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.assertEquals; +import static test.util.Util.PARAMETERIZED_TEST_DISPLAY; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static test.util.Util.TIMEOUT; - -public class StageMixedSizeTest extends VisualTestBase { +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))); - - 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, + () -> setupContentSizeTestStage(stageStyle, initialContentSize, initialContentSize), + () -> 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"); - - runAndWait(() -> assertColorDoesNotEqual(BACKGROUND_COLOR, - getColor(initialContentSize / 2, initialContentSize - 10), TOLERANCE)); - Assertions.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(); - }); + Util.doTimeLine(WAIT, + () -> setupContentSizeTestStage(stageStyle, initialContentSize, initialContentSize), + () -> testStage.setHeight(finalHeight), + () -> assertColorDoesNotEqual(BACKGROUND_COLOR, + getColor(initialContentSize / 2, initialContentSize - 10), TOLERANCE), + () -> assertEquals(finalHeight, testStage.getHeight(), "Window height should be " + finalHeight)); } - 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(); + private void setupContentSizeTestStage(StageStyle stageStyle, int width, int height) { + 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.show(); } } 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(); + } }