diff --git a/include/eepp/window/inputevent.hpp b/include/eepp/window/inputevent.hpp index 6c80cb194..8dc5c5db7 100644 --- a/include/eepp/window/inputevent.hpp +++ b/include/eepp/window/inputevent.hpp @@ -169,6 +169,19 @@ class InputEvent { Int32 length; }; + /** Clipboard ownership when the backend generated the notification. */ + enum class ClipboardOwner : Uint8 { + Unknown, /**< Clipboard ownership is unknown or unavailable. */ + Self, /**< This application owns the clipboard. */ + External /**< This application does not own the clipboard. */ + }; + + /** Clipboard change metadata. Query Clipboard for the current contents and MIME types. + * Initialize owner explicitly when injecting a ClipboardChanged event. */ + struct ClipboardEvent { + ClipboardOwner owner; + }; + /** The "quit requested" event */ struct QuitEvent { Uint8 type; @@ -214,6 +227,10 @@ class InputEvent { TextDropped, TextEditing, MouseWheel, + /** Clipboard notification, including self-writes when reported by the backend. + * WinID is 0; query Clipboard for the current contents. SDL2 may also report PRIMARY + * changes. */ + ClipboardChanged, EventUser, EventCount = EventUser - 1 }; @@ -245,6 +262,7 @@ class InputEvent { UserEvent user; WheelEvent wheel; SysWMEvent syswm; + ClipboardEvent clipboard; }; }; diff --git a/src/eepp/window/backend/SDL2/inputsdl2.cpp b/src/eepp/window/backend/SDL2/inputsdl2.cpp index f643dc470..f277deec5 100644 --- a/src/eepp/window/backend/SDL2/inputsdl2.cpp +++ b/src/eepp/window/backend/SDL2/inputsdl2.cpp @@ -499,6 +499,12 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { event.syswm.msg = (InputEvent::SysWMmsg*)SDLEvent.syswm.msg; break; } + case SDL_CLIPBOARDUPDATE: { + event.Type = InputEvent::ClipboardChanged; + event.clipboard.owner = InputEvent::ClipboardOwner::Unknown; + event.WinID = 0; + break; + } case SDL_DROPFILE: { event.Type = InputEvent::FileDropped; event.file.file = SDLEvent.drop.file; diff --git a/src/eepp/window/backend/SDL3/inputsdl3.cpp b/src/eepp/window/backend/SDL3/inputsdl3.cpp index 775035d1d..7a8491f1f 100644 --- a/src/eepp/window/backend/SDL3/inputsdl3.cpp +++ b/src/eepp/window/backend/SDL3/inputsdl3.cpp @@ -437,6 +437,13 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { event.quit.type = event.Type; break; } + case SDL_EVENT_CLIPBOARD_UPDATE: { + event.Type = InputEvent::ClipboardChanged; + event.clipboard.owner = SDLEvent.clipboard.owner ? InputEvent::ClipboardOwner::Self + : InputEvent::ClipboardOwner::External; + event.WinID = 0; + break; + } case SDL_EVENT_DROP_FILE: { event.Type = InputEvent::FileDropped; event.file.file = SDLEvent.drop.data; diff --git a/src/tests/unit_tests/inputevent_tests.cpp b/src/tests/unit_tests/inputevent_tests.cpp new file mode 100644 index 000000000..bd8f03226 --- /dev/null +++ b/src/tests/unit_tests/inputevent_tests.cpp @@ -0,0 +1,44 @@ +#include "utest.h" +#include +#include + +using namespace EE; +using namespace EE::UI; +using namespace EE::Window; + +UTEST( InputEvent, clipboardChangedReachesCallbacksWithoutWindowID ) { + UIApplication app( WindowSettings{ 320, 240, "Clipboard Event Test" } ); + auto* input = app.getWindow()->getInput(); + unsigned int received = 0; + Uint32 windowID = 1; + InputEvent::ClipboardOwner owner = InputEvent::ClipboardOwner::Self; + const auto callbackID = input->pushCallback( [&]( InputEvent* event ) { + if ( event->Type == InputEvent::ClipboardChanged ) { + ++received; + windowID = event->WinID; + owner = event->clipboard.owner; + } + } ); + + InputEvent event{}; + event.Type = InputEvent::ClipboardChanged; + event.clipboard.owner = InputEvent::ClipboardOwner::Unknown; + EXPECT_TRUE( input->pushEvent( event ) ); + EXPECT_EQ( received, 1u ); + EXPECT_EQ( windowID, 0u ); + EXPECT_EQ( owner, InputEvent::ClipboardOwner::Unknown ); + + event.clipboard.owner = InputEvent::ClipboardOwner::Self; + EXPECT_TRUE( input->pushEvent( event ) ); + EXPECT_EQ( received, 2u ); + EXPECT_EQ( owner, InputEvent::ClipboardOwner::Self ); + + event.clipboard.owner = InputEvent::ClipboardOwner::External; + EXPECT_TRUE( input->pushEvent( event ) ); + EXPECT_EQ( received, 3u ); + EXPECT_EQ( owner, InputEvent::ClipboardOwner::External ); + + input->popCallback( callbackID ); + EXPECT_TRUE( input->pushEvent( event ) ); + EXPECT_EQ( received, 3u ); +} diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index c96202d3d..7fc8cd245 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -7236,6 +7236,9 @@ UTEST( UIHTML, MarkdownSpaceScrollingUsesParentViewport ) { const Float range = markdown->getPixelsSize().getHeight() - viewport; ASSERT_TRUE( range > 2.f * viewport ); scene->getEventDispatcher()->setFocusNode( markdown ); + // Establish the key-only path explicitly; SDL2 enables text input on desktop by default. + scene->getWindow()->stopTextInput(); + ASSERT_FALSE( scene->getWindow()->isTextInputActive() ); scene->getEventDispatcher()->sendKeyDown( KEY_SPACE, SCANCODE_SPACE, 0, 0 ); EXPECT_NEAR( viewport, bar->getValue() * range, 1.f ); scene->getEventDispatcher()->sendKeyDown( KEY_SPACE, SCANCODE_SPACE, 0, KEYMOD_LSHIFT ); diff --git a/src/tests/unit_tests/uiwebview_tests.cpp b/src/tests/unit_tests/uiwebview_tests.cpp index bd93a6fc4..acbad8442 100644 --- a/src/tests/unit_tests/uiwebview_tests.cpp +++ b/src/tests/unit_tests/uiwebview_tests.cpp @@ -3621,6 +3621,10 @@ UTEST( UIWebView, DefaultShortcutsScrollByViewportAndRespectFocusedControls ) { scene->getEventDispatcher()->sendKeyDown( key, SCANCODE_UNKNOWN, 0, mod ); }; scene->getEventDispatcher()->setFocusNode( view ); + // SDL2 starts text input automatically on desktop. These assertions send keys only; + // the committed-text path is exercised separately below. + window->stopTextInput(); + ASSERT_FALSE( window->isTextInputActive() ); press( KEY_SPACE ); EXPECT_NEAR( viewport, scroll->getValue() * range, 1.f ); press( KEY_PAGEDOWN ); diff --git a/src/tools/eterm/appconfig.cpp b/src/tools/eterm/appconfig.cpp index b2186c2b9..73b5af80d 100644 --- a/src/tools/eterm/appconfig.cpp +++ b/src/tools/eterm/appconfig.cpp @@ -66,6 +66,8 @@ void AppConfig::load() { mIni.getValueB( "window", "warn_before_closing", window.warnBeforeClose ); window.alwaysShowTabBar = mIni.getValueB( "window", "always_show_tab_bar", window.alwaysShowTabBar ); + window.showTabBarWhenSplit = + mIni.getValueB( "window", "show_tab_bar_when_split", window.showTabBarWhenSplit ); window.rendererVersion = Renderer::glVersionFromString( mIni.getValue( "window", "renderer_version", Renderer::graphicsLibraryVersionToString( window.rendererVersion ) ) ); @@ -132,6 +134,7 @@ bool AppConfig::savePreferences() { mIni.setValueB( "window", "benchmark_mode", window.benchmarkMode ); mIni.setValueB( "window", "warn_before_closing", window.warnBeforeClose ); mIni.setValueB( "window", "always_show_tab_bar", window.alwaysShowTabBar ); + mIni.setValueB( "window", "show_tab_bar_when_split", window.showTabBarWhenSplit ); mIni.setValue( "window", "renderer_version", Renderer::graphicsLibraryVersionToString( window.rendererVersion ) ); mIni.setValueU( "window", "multisamples", window.multisamples ); diff --git a/src/tools/eterm/appconfig.hpp b/src/tools/eterm/appconfig.hpp index 8fdc5d6a6..6a336c1ce 100644 --- a/src/tools/eterm/appconfig.hpp +++ b/src/tools/eterm/appconfig.hpp @@ -59,6 +59,7 @@ struct WindowConfig { bool benchmarkMode{ false }; bool warnBeforeClose{ false }; bool alwaysShowTabBar{ false }; + bool showTabBarWhenSplit{ true }; GraphicsLibraryVersion rendererVersion{ GLv_default }; Uint32 multisamples{ 0 }; }; diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 367e2bb70..10cce3b43 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -966,6 +966,7 @@ int App::run( int argc, char* argv[] ) { UITabWidgetSplitter::New( &splitterClient, scene ) ); tabSplitter = tabSplitterOwner.get(); tabSplitter->setHideTabBarOnSingleTab( !config->window.alwaysShowTabBar ); + tabSplitter->setShowTabBarWhenSplit( config->window.showTabBarWhenSplit ); tabSplitter->setCanCreateSplitFn( [this]( SplitDirection, UIWidget* ) { restoreMaximizedTabWidget(); return true; diff --git a/src/tools/eterm/settingspanel.cpp b/src/tools/eterm/settingspanel.cpp index 80fc48073..b262acd9b 100644 --- a/src/tools/eterm/settingspanel.cpp +++ b/src/tools/eterm/settingspanel.cpp @@ -447,6 +447,16 @@ UIWindow* SettingsPanel::create( App& app ) { app.savePreferences(); } ); + panel->addBool( + { "showTabBarWhenSplit", "terminal.appearance", + app.i18n( "show_tab_bar_when_split", "Show Tab Bar When Split" ), + app.i18n( "show_tab_bar_when_split_desc", + "Show single-tab bars when the terminal is split into multiple panes." ) }, + &app.config->window.showTabBarWhenSplit, [&app]( bool value ) { + app.tabSplitter->setShowTabBarWhenSplit( value ); + app.savePreferences(); + } ); + panel->build(); settingsWindow->on( Event::OnWindowReady, [panel]( const Event* ) { panel->runOnMainThread( [panel] { panel->focusSearch(); } );