Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/cinder/app/AppBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ class CI_API AppBase {
virtual void keyDown( KeyEvent event ) {}
//! Override to receive key-up events.
virtual void keyUp( KeyEvent event ) {}
//! Override to receive character input events. Preferred for text entry as it properly handles IME, dead keys, and composed characters.
virtual void keyChar( KeyEvent event ) {}
//! Override to receive window resize events.
virtual void resize() {}
//! Override to receive file-drop events.
Expand Down
29 changes: 24 additions & 5 deletions include/cinder/app/FileDropEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ namespace cinder { namespace app {
//! Represents a file-drop event, typically received from Windows Explorer or Mac OS X Finder
class CI_API FileDropEvent : public Event {
public:
FileDropEvent( WindowRef win, int aX, int aY, const std::vector<fs::path> &aFiles )
: Event( win ), mX( aX ), mY( aY ), mFiles( aFiles )
FileDropEvent( WindowRef win, int aX, int aY, const std::vector<fs::path> &aFiles, unsigned int modifiers = 0 )
: Event( win ), mX( aX ), mY( aY ), mFiles( aFiles ), mModifiers( modifiers )
{}

//! Returns the X coordinate measured in points of the mouse during the event
int getX() const { return mX; }
//! Returns the Y coordinate measured in points of the mouse during the event
//! Returns the Y coordinate measured in points of the mouse during the event
int getY() const { return mY; }
//! Returns the coordinates measured in points of the mouse during the event
glm::ivec2 getPos() const { return ivec2( mX, mY ); }
Expand All @@ -52,12 +52,31 @@ class CI_API FileDropEvent : public Event {
const std::vector<fs::path>& getFiles() const { return mFiles; }
//! Returns the number of files dropped during the event
size_t getNumFiles() const { return mFiles.size(); }
//! Returns the path for file number \a index.
//! Returns the path for file number \a index.
const fs::path& getFile( size_t index ) const { return mFiles.at(index); }

//! Returns a bitwise-or of modifier key flags active during the drop. Unimplemented on Linux.
unsigned int getModifiers() const { return mModifiers; }
//! Returns whether the Shift key was held during the drop. Unimplemented on Linux.
bool isShiftDown() const { return ( mModifiers & SHIFT_DOWN ) != 0; }
//! Returns whether the Alt (Option on macOS) key was held during the drop. Unimplemented on Linux.
bool isAltDown() const { return ( mModifiers & ALT_DOWN ) != 0; }
//! Returns whether the Control key was held during the drop. Unimplemented on Linux.
bool isControlDown() const { return ( mModifiers & CTRL_DOWN ) != 0; }
//! Returns whether the Meta key (Command on macOS, Windows key on Windows) was held during the drop. Unimplemented on Linux.
bool isMetaDown() const { return ( mModifiers & META_DOWN ) != 0; }

enum {
SHIFT_DOWN = 0x0008,
ALT_DOWN = 0x0010,
CTRL_DOWN = 0x0020,
META_DOWN = 0x0040
};

private:
int mX, mY;
std::vector<fs::path> mFiles;
unsigned int mModifiers;
};

CI_API inline std::ostream& operator<<( std::ostream &os, const FileDropEvent &event )
Expand Down
6 changes: 5 additions & 1 deletion include/cinder/app/Window.h
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,10 @@ class CI_API Window : public std::enable_shared_from_this<Window> {
EventSignalKey& getSignalKeyUp() { return mSignalKeyUp; }
void emitKeyUp( KeyEvent *event );

//! Returns the signal emitted when character input is received. Preferred for text entry.
EventSignalKey& getSignalKeyChar() { return mSignalKeyChar; }
void emitKeyChar( KeyEvent *event );

EventSignalWindow& getSignalDraw() { return mSignalDraw; }
//! Fires the 'draw' signal. Note in general this should not be called directly as it doesn't perform all necessary setup.
void emitDraw();
Expand Down Expand Up @@ -493,7 +497,7 @@ class CI_API Window : public std::enable_shared_from_this<Window> {

EventSignalMouse mSignalMouseDown, mSignalMouseDrag, mSignalMouseUp, mSignalMouseWheel, mSignalMouseMove;
EventSignalTouch mSignalTouchesBegan, mSignalTouchesMoved, mSignalTouchesEnded;
EventSignalKey mSignalKeyDown, mSignalKeyUp;
EventSignalKey mSignalKeyDown, mSignalKeyUp, mSignalKeyChar;
EventSignalWindow mSignalDraw, mSignalPostDraw, mSignalMove, mSignalResize, mSignalPostResize, mSignalDisplayChange, mSignalClose;
EventSignalFileDrop mSignalFileDrop;

Expand Down
4 changes: 4 additions & 0 deletions include/cinder/app/glfw/AppImplGlfwMac.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ void enableResizeTrackingForWindow( void *nativeWindow, WindowImplGlfw *windowIm
// This removes notification observers and cleans up - call before window destruction
void disableResizeTrackingForWindow( void *nativeWindow );

// Get current global modifier key state (works even when window doesn't have focus)
// This is useful for file drop events where the drag originates from an external app
unsigned int getGlobalModifierState();

}} // namespace cinder::app
35 changes: 25 additions & 10 deletions src/cinder/CinderImGui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -584,25 +584,17 @@ static void ImGui_ImplCinder_KeyDown( ci::app::KeyEvent& event )
{
ImGuiIO& io = ImGui::GetIO();

#if defined CINDER_LINUX
auto character = event.getChar();
#else
uint32_t character = event.getCharUtf32();
#endif

ImGuiKey key = CinderKeyToImGuiKey( event.getCode() );
if( key != ImGuiKey_None )
io.AddKeyEvent( key, true );

if( ! event.isAccelDown() && character > 0 && character <= 255 ) {
io.AddInputCharacter( (char)character );
}

io.AddKeyEvent( ImGuiMod_Ctrl, event.isControlDown() );
io.AddKeyEvent( ImGuiMod_Shift, event.isShiftDown() );
io.AddKeyEvent( ImGuiMod_Alt, event.isAltDown() );
io.AddKeyEvent( ImGuiMod_Super, event.isMetaDown() );

// Only mark handled if ImGui wants full keyboard capture (not just text input)
// This allows global hotkeys to work even when ImGui text field has focus
event.setHandled( io.WantCaptureKeyboard );
}

Expand All @@ -622,6 +614,28 @@ static void ImGui_ImplCinder_KeyUp( ci::app::KeyEvent& event )
event.setHandled( io.WantCaptureKeyboard );
}

static void ImGui_ImplCinder_KeyChar( ci::app::KeyEvent& event )
{
ImGuiIO& io = ImGui::GetIO();

uint32_t character = event.getCharUtf32();
if( character > 0 ) {
// Handle Unicode characters beyond the Basic Multilingual Plane (BMP)
// by converting to UTF-16 surrogate pairs
if( character > 0xFFFF ) {
character -= 0x10000;
io.AddInputCharacterUTF16( static_cast<ImWchar16>( 0xD800 + ( character >> 10 ) ) );
io.AddInputCharacterUTF16( static_cast<ImWchar16>( 0xDC00 + ( character & 0x3FF ) ) );
}
else {
io.AddInputCharacterUTF16( static_cast<ImWchar16>( character ) );
}
}

// Mark handled if ImGui wants text input or full keyboard capture
event.setHandled( io.WantTextInput || io.WantCaptureKeyboard );
}

static void ImGui_ImplCinder_NewFrameGuard( const ci::app::WindowRef& window );

static void ImGui_ImplCinder_Resize( const ci::app::WindowRef& window )
Expand Down Expand Up @@ -704,6 +718,7 @@ static bool ImGui_ImplCinder_Init( const ci::app::WindowRef& window, const ImGui
sWindowConnections[window] += window->getSignalMouseWheel().connect( signalPriority, ImGui_ImplCinder_MouseWheel );
sWindowConnections[window] += window->getSignalKeyDown().connect( signalPriority, ImGui_ImplCinder_KeyDown );
sWindowConnections[window] += window->getSignalKeyUp().connect( signalPriority, ImGui_ImplCinder_KeyUp );
sWindowConnections[window] += window->getSignalKeyChar().connect( signalPriority, ImGui_ImplCinder_KeyChar );
sWindowConnections[window] += window->getSignalResize().connect( signalPriority, std::bind( ImGui_ImplCinder_Resize, window ) );
if( options.isAutoRenderEnabled() ) {
sWindowConnections[window] += ci::app::App::get()->getSignalUpdate().connect( std::bind( ImGui_ImplCinder_NewFrameGuard, window ) );
Expand Down
10 changes: 10 additions & 0 deletions src/cinder/app/Window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,16 @@ void Window::emitKeyUp( KeyEvent *event )
getApp()->keyUp( *event );
}

void Window::emitKeyChar( KeyEvent *event )
{
applyCurrentContext();

CollectorEvent<KeyEvent> collector( event );
mSignalKeyChar.emit( collector, *event );
if( ! event->isHandled() )
getApp()->keyChar( *event );
}

void Window::emitDraw()
{
applyCurrentContext();
Expand Down
52 changes: 49 additions & 3 deletions src/cinder/app/glfw/AppImplGlfw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,21 @@ class GlfwCallbacks {
static std::map<GLFWwindow*, std::pair<AppImplGlfw*,WindowRef>> sWindowMapping;
static bool sCapsLockDown, sNumLockDown, sScrollLockDown;

//! Stores key data from the most recent keyDown for correlation with subsequent char events
struct LastKeyData {
int code = 0;
int scancode = 0;
unsigned int modifiers = 0;
};
static std::map<GLFWwindow*, LastKeyData> sLastKeyData;

static void registerWindowEvents( GLFWwindow *glfwWindow, AppImplGlfw* cinderAppImpl, const WindowRef& cinderWindow ) {
sWindowMapping[glfwWindow] = std::make_pair( cinderAppImpl, cinderWindow );

::glfwSetWindowSizeCallback( glfwWindow, GlfwCallbacks::onWindowSize );
::glfwSetWindowPosCallback( glfwWindow, GlfwCallbacks::onWindowMove );
::glfwSetKeyCallback( glfwWindow, GlfwCallbacks::onKeyboard );
// Note: NOT using glfwSetCharCallback - we compute characters directly in onKeyboard
::glfwSetCharCallback( glfwWindow, GlfwCallbacks::onCharInput );
::glfwSetCursorPosCallback( glfwWindow, GlfwCallbacks::onMousePos );
::glfwSetMouseButtonCallback( glfwWindow, GlfwCallbacks::onMouseButton );
::glfwSetScrollCallback( glfwWindow, GlfwCallbacks::onMouseWheel );
Expand All @@ -60,6 +68,7 @@ class GlfwCallbacks {

static void unregisterWindowEvents( GLFWwindow *glfwWindow ) {
sWindowMapping.erase( glfwWindow );
sLastKeyData.erase( glfwWindow );
}

static void onError( int error, const char* description ) {
Expand Down Expand Up @@ -224,6 +233,8 @@ class GlfwCallbacks {
KeyEvent keyEvent( cinderWindow, nativeKeyCode, charUtf32, convertedChar, modifiers, scancode );

if( GLFW_PRESS == action || GLFW_REPEAT == action ) {
// Store key data for correlation with subsequent char callback
sLastKeyData[glfwWindow] = { nativeKeyCode, scancode, static_cast<unsigned int>( modifiers ) };
cinderWindow->emitKeyDown( &keyEvent );
}
else if( GLFW_RELEASE == action ) {
Expand All @@ -232,7 +243,32 @@ class GlfwCallbacks {
}
}

// onCharInput removed - we now compute characters directly in onKeyboard
//! Character input callback - emits keyChar events for text input
static void onCharInput( GLFWwindow *glfwWindow, unsigned int codepoint ) {
auto iter = sWindowMapping.find( glfwWindow );
if( sWindowMapping.end() != iter ) {
auto& cinderAppImpl = iter->second.first;
auto& cinderWindow = iter->second.second;
cinderAppImpl->setWindow( cinderWindow );

// Retrieve stored key data from preceding keyDown (if any)
int keyCode = 0;
int scancode = 0;
unsigned int modifiers = 0;
auto keyDataIter = sLastKeyData.find( glfwWindow );
if( keyDataIter != sLastKeyData.end() ) {
keyCode = keyDataIter->second.code;
scancode = keyDataIter->second.scancode;
modifiers = keyDataIter->second.modifiers;
}

// Convert UTF-32 codepoint to char (ASCII only, 0 otherwise)
char charValue = ( codepoint < 128 ) ? static_cast<char>( codepoint ) : 0;

KeyEvent charEvent( cinderWindow, keyCode, codepoint, charValue, modifiers, scancode );
cinderWindow->emitKeyChar( &charEvent );
}
}

static void onMousePos( GLFWwindow* glfwWindow, double mouseX, double mouseY ) {
auto iter = sWindowMapping.find( glfwWindow );
Expand Down Expand Up @@ -318,7 +354,16 @@ class GlfwCallbacks {
double xpos, ypos;
::glfwGetCursorPos( glfwWindow, &xpos, &ypos );
vec2 dropPoint = { static_cast<float>(xpos), static_cast<float>(ypos) };
FileDropEvent dropEvent( cinderWindow, dropPoint.x, dropPoint.y, files );

// Get current modifier key state
// On macOS, use global modifier state since the drag may originate from an external app
#if defined( CINDER_MAC )
unsigned int modifiers = getGlobalModifierState();
#else
unsigned int modifiers = getGlfwKeyModifiersMouse( glfwWindow );
#endif

FileDropEvent dropEvent( cinderWindow, static_cast<int>( dropPoint.x ), static_cast<int>( dropPoint.y ), files, modifiers );
cinderWindow->emitFileDrop( &dropEvent );
}
}
Expand All @@ -337,6 +382,7 @@ class GlfwCallbacks {
};

std::map<GLFWwindow*, std::pair<AppImplGlfw*,WindowRef>> GlfwCallbacks::sWindowMapping;
std::map<GLFWwindow*, GlfwCallbacks::LastKeyData> GlfwCallbacks::sLastKeyData;
bool GlfwCallbacks::sCapsLockDown = false;
bool GlfwCallbacks::sNumLockDown = false;
bool GlfwCallbacks::sScrollLockDown = false;
Expand Down
18 changes: 18 additions & 0 deletions src/cinder/app/glfw/AppImplGlfwMac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,24 @@ void enableResizeTrackingForWindow( void *nativeWindow, WindowImplGlfw *windowIm
objc_setAssociatedObject( nsWindow, &kResizeObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC );
}

// Get current global modifier key state (works even when window doesn't have focus)
unsigned int getGlobalModifierState()
{
NSUInteger cocoaMods = [NSEvent modifierFlags];
unsigned int modifiers = 0;

if( cocoaMods & NSEventModifierFlagShift )
modifiers |= 0x0008; // MouseEvent::SHIFT_DOWN
if( cocoaMods & NSEventModifierFlagOption )
modifiers |= 0x0010; // MouseEvent::ALT_DOWN
if( cocoaMods & NSEventModifierFlagControl )
modifiers |= 0x0020; // MouseEvent::CTRL_DOWN
if( cocoaMods & NSEventModifierFlagCommand )
modifiers |= 0x0040; // MouseEvent::META_DOWN

return modifiers;
}

// Disable resize tracking on a GLFW window
void disableResizeTrackingForWindow( void *nativeWindow )
{
Expand Down
38 changes: 35 additions & 3 deletions src/cinder/app/msw/AppImplMsw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ LRESULT CALLBACK BlankingWndProc( HWND mWnd, UINT uMsg, WPARAM wParam, LPARAM lP
static const wchar_t *WINDOWED_WIN_CLASS_NAME = TEXT("CinderWinClass");
static const wchar_t *BLANKING_WINDOW_CLASS_NAME = TEXT("CinderBlankingWindow");

//! Stores key data from the most recent WM_KEYDOWN for correlation with subsequent WM_CHAR
struct LastKeyData {
int code = 0;
unsigned int nativeKeyCode = 0;
unsigned int modifiers = 0;
};
static LastKeyData sLastKeyDown;

AppImplMsw::AppImplMsw( AppBase *aApp )
: mApp( aApp ), mSetupHasBeenCalled( false ), mActive( true ), mNeedsToRefreshDisplays( false )
{
Expand Down Expand Up @@ -650,8 +658,12 @@ LRESULT CALLBACK WndProc( HWND mWnd, // Handle For This Window
case WM_SYSKEYDOWN:
case WM_KEYDOWN: {
WCHAR c = mapVirtualKey( wParam );
KeyEvent event( impl->getWindow(), KeyEvent::translateNativeKeyCode( prepNativeKeyCode( wParam ) ),
c, static_cast<char>( c ), prepKeyEventModifiers(), static_cast<unsigned int>(wParam) );
int keyCode = KeyEvent::translateNativeKeyCode( prepNativeKeyCode( wParam ) );
unsigned int modifiers = prepKeyEventModifiers();
// Store key data for correlation with subsequent WM_CHAR
sLastKeyDown = { keyCode, static_cast<unsigned int>( wParam ), modifiers };
KeyEvent event( impl->getWindow(), keyCode,
c, static_cast<char>( c ), modifiers, static_cast<unsigned int>(wParam) );
impl->getWindow()->emitKeyDown( &event );
if ( event.isHandled() )
return 0;
Expand All @@ -667,6 +679,23 @@ LRESULT CALLBACK WndProc( HWND mWnd, // Handle For This Window
return 0;
}
break;
case WM_DEADCHAR:
case WM_SYSDEADCHAR:
// Dead keys are intermediate characters in a compose sequence (e.g., ' before e to make é)
// Ignore them - the final composed character will arrive via WM_CHAR
return 0;
case WM_SYSCHAR:
case WM_CHAR: {
// WM_CHAR provides the actual character input (handles dead keys, Alt codes, etc.)
uint32_t charUtf32 = static_cast<uint32_t>( wParam );
char charValue = ( charUtf32 < 128 ) ? static_cast<char>( charUtf32 ) : 0;
KeyEvent event( impl->getWindow(), sLastKeyDown.code, charUtf32, charValue,
sLastKeyDown.modifiers, sLastKeyDown.nativeKeyCode );
impl->getWindow()->emitKeyChar( &event );
if( event.isHandled() )
return 0;
}
break;
// mouse events
case WM_LBUTTONDOWN: {
::SetCapture( mWnd );
Expand Down Expand Up @@ -804,7 +833,10 @@ LRESULT CALLBACK WndProc( HWND mWnd, // Handle For This Window
::DragQueryPoint( dropH, &dropPoint );
::DragFinish( dropH );

FileDropEvent dropEvent( impl->getWindow(), impl->toPoints( (int)dropPoint.x ), impl->toPoints( (int)dropPoint.y ), files );
// Capture modifier key state during drop
unsigned int modifiers = prepKeyEventModifiers();

FileDropEvent dropEvent( impl->getWindow(), impl->toPoints( (int)dropPoint.x ), impl->toPoints( (int)dropPoint.y ), files, modifiers );
impl->getWindow()->emitFileDrop( &dropEvent );
return 0;
}
Expand Down
Loading
Loading