diff --git a/ChromiumBasedEditors/lib/CMakeLists.txt b/ChromiumBasedEditors/lib/CMakeLists.txt index 6d4624768..3e55e3326 100644 --- a/ChromiumBasedEditors/lib/CMakeLists.txt +++ b/ChromiumBasedEditors/lib/CMakeLists.txt @@ -279,7 +279,6 @@ list(APPEND ASCDOCUMENTSCORE_HEADERS "${CEF_SRC_PATH}/include/base/cef_atomic_flag.h" "${CEF_SRC_PATH}/include/base/cef_atomic_ref_count.h" "${CEF_SRC_PATH}/include/base/cef_auto_reset.h" - "${CEF_SRC_PATH}/include/base/cef_basictypes.h" "${CEF_SRC_PATH}/include/base/cef_bind.h" "${CEF_SRC_PATH}/include/base/cef_build.h" "${CEF_SRC_PATH}/include/base/cef_callback.h" @@ -295,7 +294,6 @@ list(APPEND ASCDOCUMENTSCORE_HEADERS "${CEF_SRC_PATH}/include/base/cef_ptr_util.h" "${CEF_SRC_PATH}/include/base/cef_ref_counted.h" "${CEF_SRC_PATH}/include/base/cef_scoped_refptr.h" - "${CEF_SRC_PATH}/include/base/cef_template_util.h" "${CEF_SRC_PATH}/include/base/cef_thread_checker.h" "${CEF_SRC_PATH}/include/base/cef_trace_event.h" "${CEF_SRC_PATH}/include/base/cef_tuple.h" @@ -496,6 +494,7 @@ list(APPEND ASCDOCUMENTSCORE_HEADERS "${CEF_SRC_PATH}/include/cef_xml_reader.h" "${CEF_SRC_PATH}/include/cef_zip_reader.h" "${CEF_SRC_PATH}/include/internal/cef_export.h" + "${CEF_SRC_PATH}/include/internal/cef_linux.h" "${CEF_SRC_PATH}/include/internal/cef_logging_internal.h" "${CEF_SRC_PATH}/include/internal/cef_ptr.h" "${CEF_SRC_PATH}/include/internal/cef_string.h" @@ -510,6 +509,7 @@ list(APPEND ASCDOCUMENTSCORE_HEADERS "${CEF_SRC_PATH}/include/internal/cef_trace_event_internal.h" "${CEF_SRC_PATH}/include/internal/cef_types.h" "${CEF_SRC_PATH}/include/internal/cef_types_geometry.h" + "${CEF_SRC_PATH}/include/internal/cef_types_linux.h" "${CEF_SRC_PATH}/include/internal/cef_types_wrappers.h" "${CEF_SRC_PATH}/include/test/cef_test_helpers.h" "${CEF_SRC_PATH}/include/test/cef_test_server.h" diff --git a/ChromiumBasedEditors/lib/include/cefview.h b/ChromiumBasedEditors/lib/include/cefview.h index 5351cca81..6a4d6fc28 100644 --- a/ChromiumBasedEditors/lib/include/cefview.h +++ b/ChromiumBasedEditors/lib/include/cefview.h @@ -91,6 +91,15 @@ class CCefViewWidgetImpl virtual void OnLoaded() {} virtual void OnRelease() {} + virtual double GetDeviceScaleFactor() { return 1.0; } + virtual bool IsWayland() { return false; } + + // Returns the widget's top-left position in screen device (pixel) coordinates. + // Used by CEF's GetScreenPoint to map view DIPs to screen pixels. + virtual void GetWidgetScreenPosition(int& screenX, int& screenY) { screenX = 0; screenY = 0; } + + virtual void OnPaint(const void* buffer, int width, int height) {} + static void SetParentNull(WindowHandleId handle); }; @@ -148,6 +157,11 @@ class DESKTOP_DECL CCefView void ExecuteInAllFrames(const std::string& sCode, const bool& isMain = true); + void SendMouseClickEvent(int x, int y, int button, bool mouseUp, int modifiers, int clickCount); + void SendMouseMoveEvent(int x, int y, bool mouseLeave, int modifiers); + void SendMouseWheelEvent(int x, int y, int deltaX, int deltaY, int modifiers); + void SendKeyEvent(int type, int key, int modifiers, const std::wstring& character); + protected: int m_nId; CefViewWrapperType m_eWrapperType; diff --git a/ChromiumBasedEditors/lib/qt_wrapper/CMakeLists.txt b/ChromiumBasedEditors/lib/qt_wrapper/CMakeLists.txt index 158dba13a..dc4c3f4fd 100644 --- a/ChromiumBasedEditors/lib/qt_wrapper/CMakeLists.txt +++ b/ChromiumBasedEditors/lib/qt_wrapper/CMakeLists.txt @@ -36,6 +36,11 @@ if(NOT TARGET videoplayerlib) add_subdirectory(${PWD_ROOT_DIR}/../../videoplayerlib videoplayerlib) endif() +# QOpenGLWidget (Wayland OSR presenter) lives in the OpenGLWidgets module, which +# common.cmake's find_package does not request. Pull it in here so this stays +# self-contained within desktop-sdk (the only submodule the build resets). +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS OpenGLWidgets) + # --- Main Target --- add_library(qtascdocumentscore SHARED) @@ -72,6 +77,7 @@ target_link_libraries(qtascdocumentscore PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Multimedia Qt${QT_VERSION_MAJOR}::MultimediaWidgets + Qt${QT_VERSION_MAJOR}::OpenGLWidgets # Internal Dependencies kernel graphics @@ -89,7 +95,7 @@ if(UNIX AND NOT APPLE) target_compile_definitions(qtascdocumentscore PRIVATE _LINUX LINUX OS_LINUX _X11) # Qt6 Linux handling - target_link_libraries(qtascdocumentscore PRIVATE Qt${QT_VERSION_MAJOR}::GuiPrivate) + target_link_libraries(qtascdocumentscore PRIVATE Qt${QT_VERSION_MAJOR}::Gui) #target_link_options(qtascdocumentscore PRIVATE "-Wl,-unresolved-symbols=ignore-in-shared-libs") diff --git a/ChromiumBasedEditors/lib/qt_wrapper/include/qcefview.h b/ChromiumBasedEditors/lib/qt_wrapper/include/qcefview.h index 76a7467e2..829a6d9de 100644 --- a/ChromiumBasedEditors/lib/qt_wrapper/include/qcefview.h +++ b/ChromiumBasedEditors/lib/qt_wrapper/include/qcefview.h @@ -33,10 +33,42 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include + + #include "./../../include/cefview.h" #include "./../../include/applicationmanager.h" class QCefViewProps; + +// Wayland-only presenter for the CEF off-screen buffer. Rendering through a GL +// surface makes the swap itself the wl_surface_commit, which participates in +// the compositor's frame-callback / vsync loop natively -- so the raster +// backing-store "commit never completes until input" deadlock cannot occur. +// It is a mouse-transparent, non-focusable child that fully overlays its +// QCefView parent; all input continues to be handled by QCefView. +class QCefGLWidget : public QOpenGLWidget +{ + Q_OBJECT +public: + explicit QCefGLWidget(QWidget* parent); + // Stage a new frame (deep-copied by the caller) and schedule a GL repaint. + void SetFrame(const QImage& image); + +protected: + virtual void paintGL() override; + +private: + QImage m_frame; +}; + class DESKTOP_DECL QCefView : public QWidget, public CCefViewWidgetImpl { Q_OBJECT @@ -56,6 +88,16 @@ class DESKTOP_DECL QCefView : public QWidget, public CCefViewWidgetImpl virtual void resizeEvent(QResizeEvent* e); virtual void moveEvent(QMoveEvent* e); + // input events for OSR + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + virtual void keyPressEvent(QKeyEvent *event) override; + virtual void keyReleaseEvent(QKeyEvent *event) override; + virtual void inputMethodEvent(QInputMethodEvent *event) override; + virtual QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; + // drag'n'drop #if defined (_LINUX) && !defined(_MAC) virtual void dragEnterEvent(QDragEnterEvent *e); @@ -93,6 +135,18 @@ class DESKTOP_DECL QCefView : public QWidget, public CCefViewWidgetImpl void SetBackgroundCefColor(unsigned char r, unsigned char g, unsigned char b); void paintEvent(QPaintEvent *event); + virtual double GetDeviceScaleFactor() override; + virtual bool IsWayland() override; + virtual void OnPaint(const void* buffer, int width, int height) override; + virtual void GetWidgetScreenPosition(int& screenX, int& screenY) override; + + // Wayland: called from the external message loop poller (top of loop, + // non-reentrant) after CEF is pumped. Repaints any view whose OnPaint + // staged a new frame and drives the frame-callback handshake so the + // commit is synchronized to the compositor. Must never be called from + // inside a CEF callback (e.g. OnPaint). + static void FlushDirtyWaylandViews(); + // check support z-index static bool IsSupportLayers(); void SetCaptionMaskSize(int); @@ -105,6 +159,21 @@ class DESKTOP_DECL QCefView : public QWidget, public CCefViewWidgetImpl CCefView* m_pCefView; QPointer m_pOverride; QCefViewProps* m_pProperties; + + QImage m_imageBuffer; + bool m_isWayland; + + // Wayland: GL presenter overlaying this view (see QCefGLWidget). null on + // other platforms and until Init() runs. + QCefGLWidget* m_pGLView = nullptr; + + // Set by OnPaint when a new frame is staged; cleared by the poller. + QAtomicInt m_dirty; + + // Registry of live Wayland views, so the message-loop poller can find + // dirty views to drive the frame-callback handshake. Populated only on + // Wayland; single-threaded access (main/UI thread). + static QList s_waylandViews; void Init(); @@ -113,9 +182,11 @@ class DESKTOP_DECL QCefView : public QWidget, public CCefViewWidgetImpl void _loaded(); void _closed(); + protected Q_SLOTS: void _loadedSlot(); void _closedSlot(); + }; #if defined (_LINUX) && !defined(_MAC) diff --git a/ChromiumBasedEditors/lib/qt_wrapper/include/qexternalmessageloop.h b/ChromiumBasedEditors/lib/qt_wrapper/include/qexternalmessageloop.h index 2fc640292..b9ab8a788 100644 --- a/ChromiumBasedEditors/lib/qt_wrapper/include/qexternalmessageloop.h +++ b/ChromiumBasedEditors/lib/qt_wrapper/include/qexternalmessageloop.h @@ -54,6 +54,7 @@ public Q_SLOTS: private: QTimer m_timer; + QTimer* m_wayland_poller = nullptr; CAscApplicationManager* m_manager; }; diff --git a/ChromiumBasedEditors/lib/qt_wrapper/qtascdocumentscore.pro b/ChromiumBasedEditors/lib/qt_wrapper/qtascdocumentscore.pro index 8f157bbcd..97e604d12 100644 --- a/ChromiumBasedEditors/lib/qt_wrapper/qtascdocumentscore.pro +++ b/ChromiumBasedEditors/lib/qt_wrapper/qtascdocumentscore.pro @@ -1,4 +1,4 @@ -QT += core gui printsupport widgets +QT += core gui printsupport widgets openglwidgets TARGET = qtascdocumentscore TEMPLATE = lib diff --git a/ChromiumBasedEditors/lib/qt_wrapper/src/qcefview.cpp b/ChromiumBasedEditors/lib/qt_wrapper/src/qcefview.cpp index 3e214fdbd..265e9d7fe 100644 --- a/ChromiumBasedEditors/lib/qt_wrapper/src/qcefview.cpp +++ b/ChromiumBasedEditors/lib/qt_wrapper/src/qcefview.cpp @@ -26,6 +26,14 @@ #include "./../include/qcefview.h" #include #include +#include +#include +#include +#include +#include +#include +#include +#include class QCefViewProps { @@ -38,16 +46,22 @@ class QCefViewProps } }; +QList QCefView::s_waylandViews; + QCefView::QCefView(QWidget* parent, const QSize& initial_size) : QWidget(parent) { m_pCefView = NULL; m_pProperties = NULL; + m_isWayland = (QGuiApplication::platformName() == "wayland"); if (!initial_size.isEmpty()) resize(initial_size); QObject::connect(this, SIGNAL( _loaded() ) , this, SLOT( _loadedSlot() ), Qt::QueuedConnection ); QObject::connect(this, SIGNAL( _closed() ) , this, SLOT( _closedSlot() ), Qt::QueuedConnection ); + if (m_isWayland) { + s_waylandViews.append(this); + } if (IsSupportLayers()) this->installEventFilter(this); @@ -55,6 +69,9 @@ QCefView::QCefView(QWidget* parent, const QSize& initial_size) : QWidget(parent) QCefView::~QCefView() { + if (m_isWayland) + s_waylandViews.removeAll(this); + // release from CApplicationManager if (m_pProperties) { @@ -99,6 +116,241 @@ bool QCefView::setFocusToCef() return isActivate; } +static int GetCefModifiers(Qt::KeyboardModifiers qt_mod, Qt::MouseButtons qt_btn) { + int modifiers = 0; + if (qt_mod & Qt::ShiftModifier) modifiers |= 1 << 1; // EVENTFLAG_SHIFT_DOWN + if (qt_mod & Qt::ControlModifier) modifiers |= 1 << 2; // EVENTFLAG_CONTROL_DOWN + if (qt_mod & Qt::AltModifier) modifiers |= 1 << 3; // EVENTFLAG_ALT_DOWN + if (qt_btn & Qt::LeftButton) modifiers |= 1 << 4; // EVENTFLAG_LEFT_MOUSE_BUTTON + if (qt_btn & Qt::MiddleButton) modifiers |= 1 << 5; // EVENTFLAG_MIDDLE_MOUSE_BUTTON + if (qt_btn & Qt::RightButton) modifiers |= 1 << 6; // EVENTFLAG_RIGHT_MOUSE_BUTTON + return modifiers; +} + +static int QtKeyToWindowsKeyCode(int key) { + if (key >= Qt::Key_0 && key <= Qt::Key_9) + return key; // 0x30 - 0x39 + if (key >= Qt::Key_A && key <= Qt::Key_Z) + return key; // 0x41 - 0x5a + + switch (key) { + case Qt::Key_Backspace: return 0x08; + case Qt::Key_Tab: return 0x09; + case Qt::Key_Clear: return 0x0C; + case Qt::Key_Return: return 0x0D; + case Qt::Key_Enter: return 0x0D; + case Qt::Key_Shift: return 0x10; + case Qt::Key_Control: return 0x11; + case Qt::Key_Alt: return 0x12; + case Qt::Key_Pause: return 0x13; + case Qt::Key_CapsLock: return 0x14; + case Qt::Key_Escape: return 0x1B; + case Qt::Key_Space: return 0x20; + case Qt::Key_PageUp: return 0x21; + case Qt::Key_PageDown: return 0x22; + case Qt::Key_End: return 0x23; + case Qt::Key_Home: return 0x24; + case Qt::Key_Left: return 0x25; + case Qt::Key_Up: return 0x26; + case Qt::Key_Right: return 0x27; + case Qt::Key_Down: return 0x28; + case Qt::Key_Select: return 0x29; + case Qt::Key_Print: return 0x2A; + case Qt::Key_Execute: return 0x2B; + case Qt::Key_SysReq: return 0x2C; + case Qt::Key_Insert: return 0x2D; + case Qt::Key_Delete: return 0x2E; + case Qt::Key_Help: return 0x2F; + case Qt::Key_NumLock: return 0x90; + case Qt::Key_ScrollLock: return 0x91; + case Qt::Key_Semicolon: return 0xBA; + case Qt::Key_Equal: return 0xBB; + case Qt::Key_Plus: return 0xBB; + case Qt::Key_Comma: return 0xBC; + case Qt::Key_Minus: return 0xBD; + case Qt::Key_Period: return 0xBE; + case Qt::Key_Slash: return 0xBF; + case Qt::Key_QuoteLeft: return 0xC0; + case Qt::Key_BracketLeft: return 0xDB; + case Qt::Key_Backslash: return 0xDC; + case Qt::Key_BracketRight: return 0xDD; + case Qt::Key_Apostrophe: return 0xDE; + default: + if (key >= Qt::Key_F1 && key <= Qt::Key_F24) + return 0x70 + (key - Qt::Key_F1); + break; + } + return 0; +} + +void QCefView::mousePressEvent(QMouseEvent *event) { + if (m_isWayland && m_pCefView) { + int button = 1; + if (event->button() == Qt::RightButton) button = 2; + else if (event->button() == Qt::MiddleButton) button = 3; + + // CEF coordinate space is DIPs (same as GetViewRect). Qt delivers DIPs. + m_pCefView->SendMouseClickEvent(event->x(), event->y(), button, false, GetCefModifiers(event->modifiers(), event->buttons()), 1); + } + QWidget::mousePressEvent(event); +} + +void QCefView::mouseReleaseEvent(QMouseEvent *event) { + if (m_isWayland && m_pCefView) { + int button = 1; + if (event->button() == Qt::RightButton) button = 2; + else if (event->button() == Qt::MiddleButton) button = 3; + m_pCefView->SendMouseClickEvent(event->x(), event->y(), button, true, GetCefModifiers(event->modifiers(), event->buttons()), 1); + } + QWidget::mouseReleaseEvent(event); +} + +void QCefView::mouseMoveEvent(QMouseEvent *event) { + if (m_isWayland && m_pCefView) { + m_pCefView->SendMouseMoveEvent(event->x(), event->y(), false, GetCefModifiers(event->modifiers(), event->buttons())); + } + QWidget::mouseMoveEvent(event); +} + +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) +void QCefView::wheelEvent(QWheelEvent *event) { + if (m_isWayland && m_pCefView) { + m_pCefView->SendMouseWheelEvent(event->position().x(), event->position().y(), event->angleDelta().x(), event->angleDelta().y(), GetCefModifiers(event->modifiers(), event->buttons())); + } + QWidget::wheelEvent(event); +} +#else +void QCefView::wheelEvent(QWheelEvent *event) { + if (m_isWayland && m_pCefView) { + m_pCefView->SendMouseWheelEvent(event->pos().x(), event->pos().y(), event->angleDelta().x(), event->angleDelta().y(), GetCefModifiers(event->modifiers(), event->buttons())); + } + QWidget::wheelEvent(event); +} +#endif + +void QCefView::keyPressEvent(QKeyEvent *event) { + if (m_isWayland && m_pCefView) { + int key = event->key(); + + // Suppress raw dead key events — let Qt's input method compose them. + // The composed character will arrive via inputMethodEvent. + if (key >= Qt::Key_Dead_Grave && key <= Qt::Key_Dead_Greek) { + event->accept(); + return; + } + + int windows_key_code = QtKeyToWindowsKeyCode(key); + + wchar_t unmodified_char = 0; + if (key >= Qt::Key_A && key <= Qt::Key_Z) { + unmodified_char = (event->modifiers() & Qt::ShiftModifier) ? key : (key - Qt::Key_A + 'a'); + } else if (key >= Qt::Key_0 && key <= Qt::Key_9) { + unmodified_char = key; + } else if (key == Qt::Key_Space) { + unmodified_char = ' '; + } else if (key == Qt::Key_Return || key == Qt::Key_Enter) { + unmodified_char = '\r'; + } else { + if (!event->text().isEmpty()) { + unmodified_char = event->text()[0].unicode(); + } + } + + wchar_t character = unmodified_char; + if (event->modifiers() & Qt::ControlModifier) { + if (key >= Qt::Key_A && key <= Qt::Key_Z) { + character = key - Qt::Key_A + 1; + } + } + + std::wstring character_str; + character_str.push_back(character); + character_str.push_back(unmodified_char); + character_str.push_back(static_cast(event->nativeScanCode() + 8)); + + m_pCefView->SendKeyEvent(0, windows_key_code, GetCefModifiers(event->modifiers(), Qt::NoButton), character_str); + m_pCefView->SendKeyEvent(3, windows_key_code, GetCefModifiers(event->modifiers(), Qt::NoButton), character_str); + event->accept(); + return; + } + QWidget::keyPressEvent(event); +} + +void QCefView::keyReleaseEvent(QKeyEvent *event) { + if (m_isWayland && m_pCefView) { + int key = event->key(); + + // Suppress raw dead key release events — matching keyPressEvent suppression. + if (key >= Qt::Key_Dead_Grave && key <= Qt::Key_Dead_Greek) { + event->accept(); + return; + } + + int windows_key_code = QtKeyToWindowsKeyCode(key); + + wchar_t unmodified_char = 0; + if (key >= Qt::Key_A && key <= Qt::Key_Z) { + unmodified_char = (event->modifiers() & Qt::ShiftModifier) ? key : (key - Qt::Key_A + 'a'); + } else if (key >= Qt::Key_0 && key <= Qt::Key_9) { + unmodified_char = key; + } else if (key == Qt::Key_Space) { + unmodified_char = ' '; + } else if (key == Qt::Key_Return || key == Qt::Key_Enter) { + unmodified_char = '\r'; + } else { + if (!event->text().isEmpty()) { + unmodified_char = event->text()[0].unicode(); + } + } + + std::wstring character_str; + character_str.push_back(unmodified_char); + character_str.push_back(unmodified_char); + character_str.push_back(static_cast(event->nativeScanCode() + 8)); + + m_pCefView->SendKeyEvent(2, windows_key_code, GetCefModifiers(event->modifiers(), Qt::NoButton), character_str); + event->accept(); + return; + } + QWidget::keyReleaseEvent(event); +} + +// An alternative approach would be to use CEF's ImeCommitText API directly, +// which is the canonical IME path. However, this would require: +// +// 1. Adding a new ImeCommitText wrapper to CCefView (similar to existing SendKeyEvent) +// 2. Including additional CEF headers for CefRange +// +// The SendKeyEvent(KEYEVENT_CHAR) approach is simpler, already proven in the +// codebase, and sufficient for dead key compose sequences (which produce final +// committed characters, not preedit/intermediate compositions like CJK input). +void QCefView::inputMethodEvent(QInputMethodEvent *event) { + if (m_isWayland && m_pCefView) { + QString commitStr = event->commitString(); + if (!commitStr.isEmpty()) { + for (int i = 0; i < commitStr.length(); i++) { + wchar_t ch = commitStr[i].unicode(); + std::wstring character_str; + character_str.push_back(ch); + character_str.push_back(ch); + character_str.push_back(0); + // Send the full key event sequence: KEYDOWN(0) -> CHAR(3) -> KEYUP(2) + // CEF requires the complete sequence to properly inject characters. + m_pCefView->SendKeyEvent(0, ch, 0, character_str); + m_pCefView->SendKeyEvent(3, ch, 0, character_str); + m_pCefView->SendKeyEvent(2, ch, 0, character_str); + } + } + } + event->accept(); +} + +QVariant QCefView::inputMethodQuery(Qt::InputMethodQuery query) const { + if (query == Qt::ImEnabled) + return true; + return QWidget::inputMethodQuery(query); +} + // focus void QCefView::focusInEvent(QFocusEvent* e) { @@ -120,6 +372,8 @@ void QCefView::resizeEvent(QResizeEvent* e) if (m_pOverride) m_pOverride->setGeometry(0, 0, cef_width, cef_height); + if (m_pGLView) + m_pGLView->setGeometry(0, 0, cef_width, cef_height); if (m_pCefView) m_pCefView->resizeEvent(); } @@ -225,8 +479,118 @@ void QCefView::SetBackgroundCefColor(unsigned char r, unsigned char g, unsigned this->setStyleSheet(sStyle); } -void QCefView::paintEvent(QPaintEvent *) +double QCefView::GetDeviceScaleFactor() +{ + return this->devicePixelRatio(); +} + +bool QCefView::IsWayland() +{ + return m_isWayland; +} + +void QCefView::GetWidgetScreenPosition(int& screenX, int& screenY) +{ + // Map widget's top-left to global screen coordinates. + // On Wayland, mapToGlobal may return (0,0) since global coords + // aren't available, but CEF primarily needs the widget offset + // for internal coordinate calculations. + QPoint globalPos = mapToGlobal(QPoint(0, 0)); + double dpr = devicePixelRatio(); + // CEF expects screen device (pixel) coordinates on Linux + screenX = (int)(globalPos.x() * dpr); + screenY = (int)(globalPos.y() * dpr); +} + +QCefGLWidget::QCefGLWidget(QWidget* parent) + : QOpenGLWidget(parent) +{ + // Input stays with the QCefView parent; this overlay is display-only. + setAttribute(Qt::WA_TransparentForMouseEvents, true); + setFocusPolicy(Qt::NoFocus); + setAutoFillBackground(false); +} + +void QCefGLWidget::SetFrame(const QImage& image) +{ + // image is already a deep copy owned by the caller (QImage is implicitly + // shared, so this is a cheap ref, not a second pixel copy). + m_frame = image; + // Schedules paintGL() + a GL swap. On Wayland the swap is the surface + // commit and is throttled by the compositor's frame callback natively, so + // the frame is presented without needing a physical input event. + update(); +} + +void QCefGLWidget::paintGL() +{ + if (m_frame.isNull()) + return; + + // Draw the full physical-pixel CEF buffer into the full widget area, same + // mapping as QCefView::paintEvent used for the raster path. + QPainter painter(this); + painter.drawImage( + QRectF(0, 0, width(), height()), + m_frame, + QRectF(0, 0, m_frame.width(), m_frame.height()) + ); +} + +void QCefView::OnPaint(const void* buffer, int width, int height) +{ + if (!m_isWayland) return; + + // Runs inside CEF's OnPaint callback: only stage the frame (no event-loop + // re-entrancy). Push it to the GL overlay, which presents it via a GL swap + // (= wl_surface_commit) that participates in the compositor frame-callback + // loop, so no top-of-loop flush is needed to unstick the commit. + QImage img((const uchar*)buffer, width, height, QImage::Format_ARGB32_Premultiplied); + QImage frame = img.copy(); + + // Keep a raster copy too: QCefView::paintEvent uses it as a backdrop behind + // the GL overlay (e.g. during resize), and it costs nothing extra (COW). + m_imageBuffer = frame; + + if (m_pGLView) + m_pGLView->SetFrame(frame); + + m_dirty.storeRelaxed(1); +} + +void QCefView::FlushDirtyWaylandViews() +{ + // Runs from the message-loop poller (top of loop, non-reentrant) after CEF + // has been pumped. Nudge a repaint of any view that produced a frame this + // tick so the GL swap is scheduled at the top of the loop. No event-loop + // spin here: nothing that could starve input or re-enter CEF. + for (QCefView* view : s_waylandViews) + { + if (view->m_dirty.fetchAndStoreRelaxed(0) && view->m_pGLView) + view->m_pGLView->update(); + } +} + + + +void QCefView::paintEvent(QPaintEvent* event) { + if (m_isWayland && !m_imageBuffer.isNull()) { + QPainter painter(this); + // Draw the full physical-pixel CEF buffer into the full widget area. + // Source: all physical pixels from the CEF OnPaint buffer. + // Target: full widget rect in logical (DIP) coordinates. + // Qt maps source onto target, stretching to fill. Since the buffer + // is DIP*DPR pixels and the widget is DIP logical (= DIP*DPR physical), + // this results in a 1:1 pixel mapping with no clipping. + painter.drawImage( + QRectF(0, 0, width(), height()), + m_imageBuffer, + QRectF(0, 0, m_imageBuffer.width(), m_imageBuffer.height()) + ); + return; + } + QStyleOption opt; opt.initFrom(this); QPainter p(this); @@ -416,7 +780,23 @@ bool QCefEmbedWindow::eventFilter(QObject *watched, QEvent *event) void QCefView::Init() { - if (IsSupportLayers()) + if (m_isWayland) + { + cef_handle = 0; + setAcceptDrops(true); + setMouseTracking(true); + setFocusPolicy(Qt::StrongFocus); + setAttribute(Qt::WA_InputMethodEnabled, true); + + // GL overlay that presents the CEF OSR buffer (see QCefGLWidget). It + // fully covers this view and is mouse-transparent, so QCefView keeps + // handling all input. + m_pGLView = new QCefGLWidget(this); + m_pGLView->setGeometry(0, 0, width(), height()); + m_pGLView->show(); + m_pGLView->raise(); + } + else if (IsSupportLayers()) { Display* display = (Display*)CefGetXDisplay(); Window x11root = XDefaultRootWindow(display); @@ -501,6 +881,8 @@ void SetWindowSize(Window window, QWidget* parent) void QCefView::UpdateSize() { + if (m_isWayland) return; + if (IsSupportLayers()) SetWindowSize(cef_handle, this); diff --git a/ChromiumBasedEditors/lib/qt_wrapper/src/qdpichecker.cpp b/ChromiumBasedEditors/lib/qt_wrapper/src/qdpichecker.cpp index 85f09d28f..e63d4ce2c 100644 --- a/ChromiumBasedEditors/lib/qt_wrapper/src/qdpichecker.cpp +++ b/ChromiumBasedEditors/lib/qt_wrapper/src/qdpichecker.cpp @@ -29,13 +29,7 @@ #include #include "./../include/qcefview.h" -#ifdef _LINUX -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include -#else -#include -#endif -#endif + @@ -72,16 +66,7 @@ int QDpiChecker::GetMonitorDpi(int nScreenNumber, unsigned int* dx, unsigned int int nDpiX = _screen->physicalDotsPerInchX(); int nDpiY = _screen->physicalDotsPerInchY(); -#ifdef _LINUX - if (QX11Info::isPlatformX11()) - { - int _x11_dpix = QX11Info::appDpiX(nScreenNumber), - _x11_dpiy = QX11Info::appDpiY(nScreenNumber); - if (nDpiX < _x11_dpix) nDpiX = _x11_dpix; - if (nDpiY < _x11_dpiy) nDpiY = _x11_dpiy; - } -#endif QSize size = _screen->size(); diff --git a/ChromiumBasedEditors/lib/qt_wrapper/src/qexternalmessageloop.cpp b/ChromiumBasedEditors/lib/qt_wrapper/src/qexternalmessageloop.cpp index ef24899b7..9d340f755 100644 --- a/ChromiumBasedEditors/lib/qt_wrapper/src/qexternalmessageloop.cpp +++ b/ChromiumBasedEditors/lib/qt_wrapper/src/qexternalmessageloop.cpp @@ -24,6 +24,8 @@ */ #include "./../include/qexternalmessageloop.h" +#include "./../include/qcefview.h" +#include QExternalMessageLoop::QExternalMessageLoop(CAscApplicationManager* manager) { @@ -31,10 +33,29 @@ QExternalMessageLoop::QExternalMessageLoop(CAscApplicationManager* manager) m_timer.setSingleShot(true); QObject::connect(&m_timer, SIGNAL(timeout()), this, SLOT(slot_onTimeout()), Qt::QueuedConnection); QObject::connect(this, SIGNAL(onExecuteCommand(void*)), this, SLOT(slot_onExecuteCommand(void*)), Qt::QueuedConnection); + + if (QGuiApplication::platformName() == "wayland" || qgetenv("XDG_SESSION_TYPE").toLower().contains("wayland")) { + // On Wayland, the Qt event dispatcher can go into deep sleep when there is no user input, + // stalling QueuedConnection events and starving the CEF message pump. + // A 16ms heartbeat timer forces the event loop to wake up and process CEF queues at 60Hz. + m_wayland_poller = new QTimer(this); + m_wayland_poller->setInterval(16); + QObject::connect(m_wayland_poller, &QTimer::timeout, this, [manager](){ + manager->ExternalMessageLoop_OnTimeout(); // pump CEF (may stage frames) + // Repaint any view whose OnPaint staged a frame and drive the + // Wayland frame-callback handshake. Runs at the top of the loop + // (non-reentrant), never from inside a CEF callback. + QCefView::FlushDirtyWaylandViews(); + }); + m_wayland_poller->start(); + } } QExternalMessageLoop::~QExternalMessageLoop() { m_timer.stop(); + if (m_wayland_poller) { + m_wayland_poller->stop(); + } } void QExternalMessageLoop::Run() diff --git a/ChromiumBasedEditors/lib/src/applicationmanager_p.h b/ChromiumBasedEditors/lib/src/applicationmanager_p.h index df59c9d16..c7ec84549 100644 --- a/ChromiumBasedEditors/lib/src/applicationmanager_p.h +++ b/ChromiumBasedEditors/lib/src/applicationmanager_p.h @@ -266,12 +266,12 @@ namespace NSCommon namespace NSArgumentList { - static int64 GetInt64(CefRefPtr args, const int& index) + static int64_t GetInt64(CefRefPtr args, const int& index) { std::string tmp = args->GetString(index).ToString(); - return (int64)std::stoll(tmp); + return (int64_t)std::stoll(tmp); } - static bool SetInt64(CefRefPtr args, const int& index, const int64& value) + static bool SetInt64(CefRefPtr args, const int& index, const int64_t& value) { std::string tmp = std::to_string(value); return args->SetString(index, tmp); @@ -482,15 +482,15 @@ namespace NSRequest } void OnUploadProgress(CefRefPtr request, - int64 current, - int64 total) override + int64_t current, + int64_t total) override { m_upload_total = total; } void OnDownloadProgress(CefRefPtr request, - int64 current, - int64 total) override + int64_t current, + int64_t total) override { m_download_total = total; } @@ -535,8 +535,8 @@ namespace NSRequest } private: - int64 m_upload_total; - int64 m_download_total; + int64_t m_upload_total; + int64_t m_download_total; std::string m_download_data; private: diff --git a/ChromiumBasedEditors/lib/src/cefapplication.cpp b/ChromiumBasedEditors/lib/src/cefapplication.cpp index f68c80919..d370a744d 100644 --- a/ChromiumBasedEditors/lib/src/cefapplication.cpp +++ b/ChromiumBasedEditors/lib/src/cefapplication.cpp @@ -400,6 +400,15 @@ int CApplicationCEF::Init_CEF(CAscApplicationManager* pManager, int argc, char* settings.no_sandbox = true; #endif +#if defined(_LINUX) && !defined(_MAC) + const char* qpaPlatform = getenv("QT_QPA_PLATFORM"); + const char* xdgSession = getenv("XDG_SESSION_TYPE"); + if ((qpaPlatform && std::string(qpaPlatform) == "wayland") || + (xdgSession && std::string(xdgSession) == "wayland")) { + settings.windowless_rendering_enabled = true; + } +#endif + // Populate the settings based on command line arguments. #if defined(CEF_VERSION_ABOVE_102) m_pInternal->context = std::make_unique(command_line, false); @@ -471,6 +480,7 @@ int CApplicationCEF::Init_CEF(CAscApplicationManager* pManager, int argc, char* settings.persist_session_cookies = true; + // Initialize CEF. m_pInternal->context->Initialize(main_args, settings, m_pInternal->m_app.get(), NULL); asc_scheme::InitScheme(pManager); @@ -488,7 +498,10 @@ int CApplicationCEF::Init_CEF(CAscApplicationManager* pManager, int argc, char* XSetIOErrorHandler(XIOErrorHandlerImpl); Display* pDisplay = (Display*)CefGetXDisplay(); - XSynchronize(pDisplay, True); + if (pDisplay) + { + XSynchronize(pDisplay, True); + } #endif CPluginsManager oPlugins; @@ -644,7 +657,16 @@ bool CApplicationCEF::IsChromiumSubprocess() void CApplicationCEF::Prepare(int argc, char* argv[]) { #if defined(_LINUX) && !defined(_MAC) - NSSystem::SetEnvValueA("GDK_BACKEND", "x11"); + const char* qpaPlatform = getenv("QT_QPA_PLATFORM"); + const char* xdgSession = getenv("XDG_SESSION_TYPE"); + bool isWayland = false; + if ((qpaPlatform && std::string(qpaPlatform) == "wayland") || + (xdgSession && std::string(xdgSession) == "wayland")) { + isWayland = true; + } + if (!isWayland) { + NSSystem::SetEnvValueA("GDK_BACKEND", "x11"); + } #endif } diff --git a/ChromiumBasedEditors/lib/src/cefview.cpp b/ChromiumBasedEditors/lib/src/cefview.cpp index aaf3afd59..043b1c7af 100644 --- a/ChromiumBasedEditors/lib/src/cefview.cpp +++ b/ChromiumBasedEditors/lib/src/cefview.cpp @@ -149,6 +149,7 @@ class CDownloadFilesAborted : public NSTimers::CTimer virtual bool IsInProgress() { return false; } virtual bool IsComplete() { return false; } virtual bool IsCanceled() { return true; } + virtual bool IsInterrupted() { return false; } virtual int64 GetCurrentSpeed() { return 0; } virtual int GetPercentComplete() { return 0; } virtual int64 GetTotalBytes() { return 0; } @@ -1780,7 +1781,7 @@ class CCefResizeTask : public CefTask IMPLEMENT_REFCOUNTING(CCefResizeTask); }; -class CAscClientHandler : public client::ClientHandler, public CCookieFoundCallback, public client::ClientHandler::Delegate, public CefDialogHandler +class CAscClientHandler : public client::ClientHandler, public CCookieFoundCallback, public client::ClientHandler::Delegate, public CefDialogHandler, public CefRenderHandler { public: class CAscCefJSDialogHandler : public CefJSDialogHandler @@ -1829,6 +1830,75 @@ class CAscClientHandler : public client::ClientHandler, public CCookieFoundCallb IMPLEMENT_REFCOUNTING(CAscCefJSDialogHandler); }; +public: + virtual CefRefPtr GetRenderHandler() OVERRIDE + { + return this; + } + + virtual void GetViewRect(CefRefPtr browser, CefRect& rect) OVERRIDE + { + if (m_pParent && m_pParent->GetWidgetImpl()) { + // In OSR mode, --force-device-scale-factor does NOT override + // GetScreenInfo.device_scale_factor. CEF computes the buffer as + // GetViewRect * device_scale_factor. So we must return DIPs here + // (logical pixels from QWidget::width/height), NOT physical pixels. + rect.x = 0; + rect.y = 0; + rect.width = m_pParent->GetWidgetImpl()->cef_width; + rect.height = m_pParent->GetWidgetImpl()->cef_height; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + } else { + rect.x = rect.y = 0; + rect.width = rect.height = 1; + } + } + + virtual bool GetScreenInfo(CefRefPtr browser, CefScreenInfo& screen_info) OVERRIDE + { + if (m_pParent && m_pParent->GetWidgetImpl()) { + double scale = m_pParent->GetWidgetImpl()->GetDeviceScaleFactor(); + screen_info.device_scale_factor = (float)scale; + // Rect must be in DIPs (same as GetViewRect). CEF computes the + // physical buffer as rect * device_scale_factor internally. + screen_info.rect.x = 0; + screen_info.rect.y = 0; + screen_info.rect.width = m_pParent->GetWidgetImpl()->cef_width; + screen_info.rect.height = m_pParent->GetWidgetImpl()->cef_height; + if (screen_info.rect.width == 0) screen_info.rect.width = 1; + if (screen_info.rect.height == 0) screen_info.rect.height = 1; + screen_info.available_rect = screen_info.rect; + return true; + } + return false; + } + + virtual bool GetScreenPoint(CefRefPtr browser, + int viewX, int viewY, + int& screenX, int& screenY) OVERRIDE + { + if (m_pParent && m_pParent->GetWidgetImpl()) { + double scale = m_pParent->GetWidgetImpl()->GetDeviceScaleFactor(); + int widgetScreenX = 0, widgetScreenY = 0; + m_pParent->GetWidgetImpl()->GetWidgetScreenPosition(widgetScreenX, widgetScreenY); + // Convert view DIP coordinates to screen device (pixel) coordinates. + // Per CEF docs: "Windows/Linux should provide screen device (pixel) coordinates" + screenX = widgetScreenX + (int)(viewX * scale); + screenY = widgetScreenY + (int)(viewY * scale); + + return true; + } + return false; + } + + virtual void OnPaint(CefRefPtr browser, PaintElementType type, const RectList& dirtyRects, const void* buffer, int width, int height) OVERRIDE + { + if (m_pParent && m_pParent->GetWidgetImpl()) { + m_pParent->GetWidgetImpl()->OnPaint(buffer, width, height); + } + } + public: CCefView* m_pParent; bool m_bIsLoaded; @@ -4869,6 +4939,16 @@ virtual void OnLoadEnd(CefRefPtr browser, { m_pParent->m_pInternal->m_hideChecker.Show(browser); +#if defined(_LINUX) && !defined(_MAC) + if (frame && frame->IsMain()) { + const char* session = getenv("XDG_SESSION_TYPE"); + if (session && std::string(session) == "wayland") { + std::string sCode = "if (window.Common && window.Common.Utils) { window.Common.Utils.zoom = function() { return 1; }; }"; + frame->ExecuteJavaScript(sCode, frame->GetURL(), 0); + } + } +#endif + bool bIsCryptoSupport = true; if (m_pParent->m_pInternal->m_bIsExternalCloud) { @@ -4930,10 +5010,6 @@ virtual void OnLoadError(CefRefPtr browser, const CefString& errorText, const CefString& failedUrl) OVERRIDE { - std::string s1 = frame->GetURL().ToString(); - std::string s2 = failedUrl.ToString(); - std::string s3 = errorText.ToString(); - if (m_pParent && errorCode != ERR_ABORTED) { if (frame->IsMain()) @@ -5616,73 +5692,6 @@ virtual void OnDownloadUpdated(CefRefPtr browser, if (NULL == m_pParent) return; -#if 0 - FILE* f = fopen("...", "a+"); - - fprintf(f, "-------------------------------\n"); - fprintf(f, "IsValid: %d\n", download_item->IsValid() ? 1 : 0); - fprintf(f, "IsInProgress: %d\n", download_item->IsInProgress() ? 1 : 0); - fprintf(f, "IsComplete: %d\n", download_item->IsComplete() ? 1 : 0); - fprintf(f, "IsCanceled: %d\n", download_item->IsCanceled() ? 1 : 0); - - fprintf(f, "GetCurrentSpeed: %d\n", (int)download_item->GetCurrentSpeed()); - fprintf(f, "GetPercentComplete: %d\n", (int)download_item->GetPercentComplete()); - fprintf(f, "GetTotalBytes: %d\n", (int)download_item->GetTotalBytes()); - fprintf(f, "GetReceivedBytes: %d\n", (int)download_item->GetReceivedBytes()); - fprintf(f, "GetId: %d\n", (int)download_item->GetId()); - - if (!download_item->GetFullPath().empty()) - { - std::string s = download_item->GetFullPath().ToString(); - NSStringUtils::string_replaceA(s, "%", "%%"); - fprintf(f, "GetFullPath: "); - fprintf(f, s.c_str()); - fprintf(f, "\n"); - } - if (!download_item->GetOriginalUrl().empty()) - { - std::string s = download_item->GetOriginalUrl().ToString(); - NSStringUtils::string_replaceA(s, "%", "%%"); - fprintf(f, "GetOriginalUrl: "); - fprintf(f, s.c_str()); - fprintf(f, "\n"); - } - if (!download_item->GetURL().empty()) - { - std::string s = download_item->GetURL().ToString(); - NSStringUtils::string_replaceA(s, "%", "%%"); - fprintf(f, "GetURL: "); - fprintf(f, s.c_str()); - fprintf(f, "\n"); - } - if (!download_item->GetSuggestedFileName().empty()) - { - std::string s = download_item->GetSuggestedFileName().ToString(); - NSStringUtils::string_replaceA(s, "%", "%%"); - fprintf(f, "GetSuggestedFileName: "); - fprintf(f, s.c_str()); - fprintf(f, "\n"); - } - if (!download_item->GetContentDisposition().empty()) - { - std::string s = download_item->GetContentDisposition().ToString(); - NSStringUtils::string_replaceA(s, "%", "%%"); - fprintf(f, "GetContentDisposition: "); - fprintf(f, s.c_str()); - fprintf(f, "\n"); - } - if (!download_item->GetMimeType().empty()) - { - std::string s = download_item->GetMimeType().ToString(); - NSStringUtils::string_replaceA(s, "%", "%%"); - fprintf(f, "GetMimeType: "); - fprintf(f, s.c_str()); - fprintf(f, "\n"); - } - - fclose(f); -#endif - if (!download_item->IsValid()) return; @@ -5708,10 +5717,6 @@ virtual void OnDownloadUpdated(CefRefPtr browser, pEvent->m_pData = pData; m_pParent->GetAppManager()->GetEventListener()->OnEvent(pEvent); - - // OpenLocalFile нужно запускать в главном потоке - //m_pParent->m_pInternal->m_pDownloadViewCallback->m_pInternal->OnViewDownloadFile(); - //m_pParent->m_pInternal->m_pDownloadViewCallback = NULL; } return; } @@ -6368,8 +6373,17 @@ void CCefView_Private::CheckZoom() if (CAscApplicationManager::IsUseSystemScaling()) return; - if (NULL == CAscApplicationManager::GetDpiChecker()) + // On Wayland OSR, GetScreenInfo already reports the correct device_scale_factor + // to CEF, so the custom zoom compensation must be skipped. Otherwise the DPR + // from GetScreenInfo and the CSS zoom from SetZoomLevel compound, causing + // double scaling (e.g., 1.15 × 1.25 = 1.4375 effective DPR). + if (m_pWidgetImpl && m_pWidgetImpl->IsWayland()) { + return; + } + + if (NULL == CAscApplicationManager::GetDpiChecker()) { return; + } if (!m_bIsWindowsCheckZoom) return; @@ -6434,6 +6448,8 @@ void CCefView_Private::UpdateSize() if (m_handler && m_handler->GetBrowser() && m_handler->GetBrowser()->GetHost()) { m_handler->GetBrowser()->GetHost()->NotifyMoveOrResizeStarted(); + if (m_handler->GetBrowser()->GetHost()->IsWindowRenderingDisabled()) + m_handler->GetBrowser()->GetHost()->WasResized(); // Fix bug #62086 CefRefPtr pFrame = m_handler->GetBrowser()->GetMainFrame(); @@ -6947,9 +6963,18 @@ void CCefView::load(const std::wstring& urlInputSrc) int _h = m_pInternal->m_pWidgetImpl->cef_height; #ifdef CEF_VERSION_ABOVE_102 +#if defined(_LINUX) && !defined(_MAC) + if (m_pInternal && m_pInternal->m_pWidgetImpl && m_pInternal->m_pWidgetImpl->IsWayland()) { + info.SetAsWindowless(0); + } else { + info.SetAsChild(_handle, CefRect(0, 0, _w, _h)); + m_pInternal->m_hideChecker.Hide(info, _handle); + } +#else info.SetAsChild(_handle, CefRect(0, 0, _w, _h)); m_pInternal->m_hideChecker.Hide(info, _handle); +#endif #else #ifdef WIN32 @@ -8877,6 +8902,71 @@ namespace NSRequest } #endif +void CCefView::SendMouseClickEvent(int x, int y, int button, bool mouseUp, int modifiers, int clickCount) +{ + if (!m_pInternal || !m_pInternal->m_handler) return; + CefRefPtr browser = m_pInternal->m_handler->GetBrowser(); + if (!browser) return; + + CefMouseEvent mouse_event; + mouse_event.x = x; + mouse_event.y = y; + mouse_event.modifiers = modifiers; + + cef_mouse_button_type_t btn_type = MBT_LEFT; + if (button == 2) btn_type = MBT_RIGHT; + else if (button == 3) btn_type = MBT_MIDDLE; + + browser->GetHost()->SendMouseClickEvent(mouse_event, btn_type, mouseUp, clickCount); +} + +void CCefView::SendMouseMoveEvent(int x, int y, bool mouseLeave, int modifiers) +{ + if (!m_pInternal || !m_pInternal->m_handler) return; + CefRefPtr browser = m_pInternal->m_handler->GetBrowser(); + if (!browser) return; + + CefMouseEvent mouse_event; + mouse_event.x = x; + mouse_event.y = y; + mouse_event.modifiers = modifiers; + + browser->GetHost()->SendMouseMoveEvent(mouse_event, mouseLeave); +} + +void CCefView::SendMouseWheelEvent(int x, int y, int deltaX, int deltaY, int modifiers) +{ + if (!m_pInternal || !m_pInternal->m_handler) return; + CefRefPtr browser = m_pInternal->m_handler->GetBrowser(); + if (!browser) return; + + CefMouseEvent mouse_event; + mouse_event.x = x; + mouse_event.y = y; + mouse_event.modifiers = modifiers; + + browser->GetHost()->SendMouseWheelEvent(mouse_event, deltaX, deltaY); +} + +void CCefView::SendKeyEvent(int type, int key, int modifiers, const std::wstring& character) +{ + if (!m_pInternal || !m_pInternal->m_handler) return; + CefRefPtr browser = m_pInternal->m_handler->GetBrowser(); + if (!browser) return; + + CefKeyEvent key_event; + key_event.type = (cef_key_event_type_t)type; + key_event.modifiers = modifiers; + key_event.windows_key_code = key; + key_event.native_key_code = (character.length() >= 3) ? static_cast(character[2]) : key; + key_event.is_system_key = (modifiers & (1 << 3)) != 0; // EVENTFLAG_ALT_DOWN is 1 << 3 + key_event.character = character.empty() ? 0 : character[0]; + key_event.unmodified_character = (character.length() >= 2) ? character[1] : key_event.character; + key_event.focus_on_editable_field = true; + + browser->GetHost()->SendKeyEvent(key_event); +} + #if defined(_LINUX) && !defined(_MAC) void* CefGetXDisplay(void) {