/* * Deskflow -- mouse and keyboard sharing utility * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers * SPDX-FileCopyrightText: (C) 2024 Symless Ltd. * SPDX-FileCopyrightText: (C) 2022 Red Hat, Inc. * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ #include "platform/EiScreen.h" #include "arch/Arch.h" #include "arch/ArchException.h" #include "base/IEventQueue.h" #include "base/Log.h" #include "base/Stopwatch.h" #include "common/Constants.h" #include "deskflow/Clipboard.h" #include "deskflow/KeyMap.h" #include "deskflow/ScreenException.h" #include "platform/EiEventQueueBuffer.h" #include "platform/EiKeyState.h" #include "platform/PortalInputCapture.h" #include "platform/PortalRemoteDesktop.h" #include #include #include #include #include #include // Values are in pixels struct ScrollRemainder { double x; double y; }; namespace deskflow { EiScreen::EiScreen(bool isPrimary, IEventQueue *events, bool usePortal, deskflow::ClientScrollDirection scrollDirection) : PlatformScreen{events, scrollDirection}, m_isPrimary{isPrimary}, m_events{events}, m_w{1}, m_h{1}, m_isOnScreen{isPrimary} { initEi(); m_keyState = new EiKeyState(this, events); // install event handlers m_events->addHandler(EventTypes::System, m_events->getSystemTarget(), [this](const auto &e) { handleSystemEvent(e); }); if (usePortal) { m_events->addHandler(EventTypes::EIConnected, getEventTarget(), [this](const auto &e) { handleConnectedToEisEvent(e); }); if (isPrimary) { m_portalInputCapture = new PortalInputCapture(this, m_events); } else { m_events->addHandler(EventTypes::EISessionClosed, getEventTarget(), [this](const auto &) { handlePortalSessionClosed(); }); m_portalRemoteDesktop = new PortalRemoteDesktop(this, m_events); } } else { // Note: socket backend does not support reconnections auto rc = ei_setup_backend_socket(m_ei, nullptr); if (rc != 0) { LOG_ERR("ei init error: %s", strerror(-rc)); throw std::runtime_error("failed to init ei context"); } } } EiScreen::~EiScreen() { m_events->adoptBuffer(nullptr); m_events->removeHandler(EventTypes::System, m_events->getSystemTarget()); cleanupEi(); delete m_keyState; delete m_portalRemoteDesktop; } void EiScreen::eiLogEvent(ei_log_priority priority, const char *message) const { switch (priority) { case EI_LOG_PRIORITY_DEBUG: LOG_DEBUG1("ei: %s", message); break; case EI_LOG_PRIORITY_INFO: LOG_INFO("ei: %s", message); break; case EI_LOG_PRIORITY_WARNING: LOG_WARN("ei: %s", message); break; case EI_LOG_PRIORITY_ERROR: LOG_ERR("ei: %s", message); break; default: LOG_PRINT("ei: %s", message); break; } } void EiScreen::initEi() { if (m_isPrimary) { m_ei = ei_new_receiver(nullptr); // we receive from the display server } else { m_ei = ei_new_sender(nullptr); // we send to the display server } ei_set_user_data(m_ei, this); ei_log_set_priority(m_ei, EI_LOG_PRIORITY_DEBUG); ei_log_set_handler(m_ei, handleEiLogEvent); std::string configName = kAppId; ei_configure_name(m_ei, configName.append(" client").c_str()); // install the platform event queue m_events->adoptBuffer(nullptr); m_events->adoptBuffer(new EiEventQueueBuffer(m_ei, m_events)); } void EiScreen::cleanupEi() { if (m_eiPointer) { free(ei_device_get_user_data(m_eiPointer)); ei_device_set_user_data(m_eiPointer, nullptr); m_eiPointer = ei_device_unref(m_eiPointer); } if (m_eiKeyboard) { free(ei_device_get_user_data(m_eiKeyboard)); ei_device_set_user_data(m_eiKeyboard, nullptr); m_eiKeyboard = ei_device_unref(m_eiKeyboard); } if (m_eiAbs) { free(ei_device_get_user_data(m_eiAbs)); ei_device_set_user_data(m_eiAbs, nullptr); m_eiAbs = ei_device_unref(m_eiAbs); } m_eiSeat = ei_seat_unref(m_eiSeat); for (auto it = m_eiDevices.begin(); it != m_eiDevices.end(); it++) { free(ei_device_get_user_data(*it)); ei_device_set_user_data(*it, nullptr); ei_device_unref(*it); } m_eiDevices.clear(); m_ei = ei_unref(m_ei); } void *EiScreen::getEventTarget() const { return const_cast(static_cast(this)); } bool EiScreen::getClipboard(ClipboardID id, IClipboard *clipboard) const { return false; } void EiScreen::getShape(int32_t &x, int32_t &y, int32_t &w, int32_t &h) const { x = m_x; y = m_y; w = m_w; h = m_h; } void EiScreen::getCursorPos(int32_t &x, int32_t &y) const { x = m_cursorX; y = m_cursorY; } void EiScreen::reconfigure(uint32_t activeSides) { const static auto sidesText = sidesMaskToString(activeSides); LOG_DEBUG("active sides: %s (0x%02x)", sidesText.c_str(), activeSides); m_activeSides = activeSides; } std::uint32_t EiScreen::activeSides() { return m_activeSides; } void EiScreen::warpCursor(int32_t x, int32_t y) { m_cursorX = x; m_cursorY = y; } std::uint32_t EiScreen::registerHotKey(KeyID key, KeyModifierMask mask) { static std::uint32_t next_id; std::uint32_t id = std::min(++next_id, 1u); // Bug: id rollover means duplicate hotkey ids. Oh well. auto set = m_hotkeys.find(key); if (set == m_hotkeys.end()) { m_hotkeys.try_emplace(key, HotKeySet{key}); set = m_hotkeys.find(key); } set->second.addItem(HotKeyItem(mask, id)); return id; } void EiScreen::unregisterHotKey(uint32_t id) { for (auto &[key, set] : m_hotkeys) { (void)key; if (set.removeById(id)) { break; } } } void EiScreen::fakeInputBegin() { // FIXME -- not implemented } void EiScreen::fakeInputEnd() { // FIXME -- not implemented } std::int32_t EiScreen::getJumpZoneSize() const { return 1; } bool EiScreen::isAnyMouseButtonDown(uint32_t &buttonID) const { return false; } void EiScreen::getCursorCenter(int32_t &x, int32_t &y) const { x = m_x + m_w / 2; y = m_y + m_h / 2; } void EiScreen::fakeMouseButton(ButtonID button, bool press) { uint32_t code; if (!m_eiPointer) return; switch (button) { case kButtonLeft: code = 0x110; // BTN_LEFT break; case kButtonMiddle: code = 0x112; // BTN_MIDDLE break; case kButtonRight: code = 0x111; // BTN_RIGHT break; default: code = 0x110 + (button - 1); break; } ei_device_button_button(m_eiPointer, code, press); ei_device_frame(m_eiPointer, ei_now(m_ei)); } void EiScreen::fakeMouseMove(int32_t x, int32_t y) { // We get one motion event before enter() with the target position if (!m_isOnScreen) { m_cursorX = x; m_cursorY = y; return; } if (!m_eiAbs) return; ei_device_pointer_motion_absolute(m_eiAbs, x, y); ei_device_frame(m_eiAbs, ei_now(m_ei)); } void EiScreen::fakeMouseRelativeMove(int32_t dx, int32_t dy) const { if (!m_eiPointer) return; ei_device_pointer_motion(m_eiPointer, dx, dy); ei_device_frame(m_eiPointer, ei_now(m_ei)); } void EiScreen::fakeMouseWheel(int32_t xDelta, int32_t yDelta) const { if (!m_eiPointer) return; xDelta = mapClientScrollDirection(xDelta); yDelta = mapClientScrollDirection(yDelta); // libei and deskflow seem to use opposite directions, so we have // to send EI the opposite of the value received if we want to remain // compatible with other platforms (including X11). ei_device_scroll_discrete(m_eiPointer, -xDelta, -yDelta); ei_device_frame(m_eiPointer, ei_now(m_ei)); } void EiScreen::fakeKey(uint32_t keycode, bool isDown) const { if (!m_eiKeyboard) return; auto xkbKeycode = keycode + 8; m_keyState->updateXkbState(xkbKeycode, isDown); ei_device_keyboard_key(m_eiKeyboard, keycode, isDown); ei_device_frame(m_eiKeyboard, ei_now(m_ei)); } void EiScreen::enable() { // Nothing really to be done here } void EiScreen::disable() { // Nothing really to be done here, maybe cleanup in the future but ideally // that's handled elsewhere } void EiScreen::enter() { m_isOnScreen = true; if (!m_isPrimary) { ++m_sequenceNumber; if (m_eiPointer) { ei_device_start_emulating(m_eiPointer, m_sequenceNumber); } if (m_eiKeyboard) { ei_device_start_emulating(m_eiKeyboard, m_sequenceNumber); } if (m_eiAbs) { ei_device_start_emulating(m_eiAbs, m_sequenceNumber); fakeMouseMove(m_cursorX, m_cursorY); } } else { LOG_DEBUG("releasing input capture at x=%i y=%i", m_cursorX, m_cursorY); m_portalInputCapture->release(m_cursorX, m_cursorY); } } bool EiScreen::canLeave() { return true; } void EiScreen::leave() { if (!m_isPrimary) { if (m_eiPointer) { ei_device_stop_emulating(m_eiPointer); } if (m_eiKeyboard) { ei_device_stop_emulating(m_eiKeyboard); } if (m_eiAbs) { ei_device_stop_emulating(m_eiAbs); } } m_isOnScreen = false; } bool EiScreen::setClipboard(ClipboardID id, const IClipboard *clipboard) { return false; } void EiScreen::checkClipboards() { // do nothing, we're always up to date } void EiScreen::openScreensaver(bool notify) { // FIXME } void EiScreen::closeScreensaver() { // FIXME } void EiScreen::screensaver(bool activate) { // FIXME } void EiScreen::resetOptions() { // Should reset options to neutral, see setOptions(). // We don't have ei-specific options, nothing to do here } void EiScreen::setOptions(const OptionsList &options) { // We don't have ei-specific options, nothing to do here } void EiScreen::setSequenceNumber(uint32_t seqNum) { // FIXME: what is this used for? } bool EiScreen::isPrimary() const { return m_isPrimary; } void EiScreen::updateShape() { m_w = 1; m_h = 1; m_x = std::numeric_limits::max(); m_y = std::numeric_limits::max(); for (auto it = m_eiDevices.begin(); it != m_eiDevices.end(); it++) { auto idx = 0; struct ei_region *r; while ((r = ei_device_get_region(*it, idx++)) != nullptr) { m_x = std::min(ei_region_get_x(r), m_x); m_y = std::min(ei_region_get_y(r), m_y); m_w = std::max(ei_region_get_x(r) + ei_region_get_width(r), m_w); m_h = std::max(ei_region_get_y(r) + ei_region_get_height(r), m_h); } } LOG_DEBUG("logical output size: %dx%d@%d.%d", m_w, m_h, m_x, m_y); m_cursorX = m_x + m_w / 2; m_cursorY = m_y + m_h / 2; sendEvent(EventTypes::ScreenShapeChanged, nullptr); } void EiScreen::addDevice(struct ei_device *device) { LOG_DEBUG("adding device %s", ei_device_get_name(device)); // Noteworthy: EI in principle supports multiple devices with multiple // capabilities, so there may be more than one logical pointer (or even // multiple seats). Supporting this is ... tricky so for now we go the easy // route: one device for each capability. Note this may be the same device // if the first device comes with multiple capabilities. if (!m_eiPointer && ei_device_has_capability(device, EI_DEVICE_CAP_POINTER) && ei_device_has_capability(device, EI_DEVICE_CAP_BUTTON) && ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL)) { m_eiPointer = ei_device_ref(device); } if (!m_eiKeyboard && ei_device_has_capability(device, EI_DEVICE_CAP_KEYBOARD)) { m_eiKeyboard = ei_device_ref(device); if (auto keymap = ei_device_keyboard_get_keymap(device); keymap && ei_keymap_get_type(keymap) == EI_KEYMAP_TYPE_XKB) { int fd = ei_keymap_get_fd(keymap); size_t len = ei_keymap_get_size(keymap); m_keyState->init(fd, len); } else { // We rely on the EIS implementation to give us a keymap, otherwise we // really have no idea what a keycode means (other than it's linux/input.h // code) Where the EIS implementation does not tell us, we just default to // whatever libxkbcommon thinks is default. At least this way we can // influence with env vars what we get LOG_WARN("keyboard device %s does not have a keymap, we are guessing", ei_device_get_name(device)); m_keyState->initDefaultKeymap(); } m_keyState->updateKeyMap(); } if (!m_eiAbs && ei_device_has_capability(device, EI_DEVICE_CAP_POINTER_ABSOLUTE) && ei_device_has_capability(device, EI_DEVICE_CAP_BUTTON) && ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL)) { m_eiAbs = ei_device_ref(device); } m_eiDevices.emplace_back(ei_device_ref(device)); updateShape(); } void EiScreen::removeDevice(struct ei_device *device) { LOG_DEBUG("removing device %s", ei_device_get_name(device)); if (device == m_eiPointer) m_eiPointer = ei_device_unref(m_eiPointer); if (device == m_eiKeyboard) m_eiKeyboard = ei_device_unref(m_eiKeyboard); if (device == m_eiAbs) m_eiAbs = ei_device_unref(m_eiAbs); for (auto it = m_eiDevices.begin(); it != m_eiDevices.end(); it++) { if (*it == device) { m_eiDevices.erase(it); ei_device_unref(device); break; } } updateShape(); } void EiScreen::sendEvent(EventTypes type, void *data) { m_events->addEvent(Event(type, getEventTarget(), data)); } ButtonID EiScreen::mapButtonFromEvdev(ei_event *event) const { switch (ei_event_button_get_button(event)) { case 0x110: return kButtonLeft; case 0x111: return kButtonRight; case 0x112: return kButtonMiddle; case 0x113: return kButtonExtra0; case 0x114: return kButtonExtra1; default: return kButtonNone; } return kButtonNone; } bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask) { auto it = m_hotkeys.find(keyid); if (it == m_hotkeys.end()) { return false; } // Note: our mask (see on_key_event) only contains some modifiers // but we don't put a limitation on modifiers in the hotkeys. So some // key combinations may not work correctly, more effort is needed here. if (auto id = it->second.findByMask(mask); id != 0) { EventTypes type = isPressed ? EventTypes::PrimaryScreenHotkeyDown : EventTypes::PrimaryScreenHotkeyUp; sendEvent(type, HotKeyInfo::alloc(id)); return true; } return false; } void EiScreen::onKeyEvent(ei_event *event) { auto keycode = ei_event_keyboard_get_key(event); uint32_t keyval = keycode + 8; bool pressed = ei_event_keyboard_get_key_is_press(event); KeyID keyid = m_keyState->mapKeyFromKeyval(keyval); auto keybutton = static_cast(keyval); m_keyState->updateXkbState(keyval, pressed); KeyModifierMask mask = m_keyState->pollActiveModifiers(); LOG_DEBUG1("event: key %s keycode=%d keyid=%d mask=0x%x", pressed ? "press" : "release", keycode, keyid, mask); if (m_isPrimary && onHotkey(keyid, pressed, mask)) { return; } if (keyid != kKeyNone) { m_keyState->sendKeyEvent(getEventTarget(), pressed, false, keyid, mask, 1, keybutton); } } void EiScreen::onButtonEvent(ei_event *event) { assert(m_isPrimary); auto buttonID = mapButtonFromEvdev(event); bool pressed = ei_event_button_get_is_press(event); KeyModifierMask mask = m_keyState->pollActiveModifiers(); LOG_DEBUG1("event: button %s button=%d mask=0x%x", pressed ? "press" : "release", buttonID, mask); if (buttonID == kButtonNone) { LOG_DEBUG("event: button not recognized"); return; } auto eventType = pressed ? EventTypes::PrimaryScreenButtonDown : EventTypes::PrimaryScreenButtonUp; sendEvent(eventType, ButtonInfo::alloc(buttonID, mask)); } void EiScreen::onPointerScrollEvent(ei_event *event) { // Ratio of 10 pixels == one wheel click because that's what mutter/gtk // use (for historical reasons). static const int s_pixelsPerWheelClick = 10; // Our logical wheel clicks are multiples 120, so we // convert between the two and keep the remainders because // we will very likely get subpixel scroll events. // This means a single pixel is 120/s_pixelToWheelRation in wheel values. const int s_pixelToWheelRatio = 120 / s_pixelsPerWheelClick; assert(m_isPrimary); auto dx = ei_event_scroll_get_dx(event); auto dy = ei_event_scroll_get_dy(event); struct ei_device *device = ei_event_get_device(event); LOG_DEBUG1("event: scroll (%.2f, %.2f)", dx, dy); auto *remainder = static_cast(ei_device_get_user_data(device)); if (!remainder) { remainder = new ScrollRemainder(); ei_device_set_user_data(device, remainder); } dx += remainder->x; dy += remainder->y; double x; double y; double rx = modf(dx, &x); double ry = modf(dy, &y); assert(!std::isnan(x) && !std::isinf(x)); assert(!std::isnan(y) && !std::isinf(y)); // libei and deskflow seem to use opposite directions, so we have // to send the opposite of the value reported by EI if we want to // remain compatible with other platforms (including X11). if (x != 0 || y != 0) sendEvent( EventTypes::PrimaryScreenWheel, WheelInfo::alloc((int32_t)-x * s_pixelToWheelRatio, (int32_t)-y * s_pixelToWheelRatio) ); remainder->x = rx; remainder->y = ry; } void EiScreen::onPointerScrollDiscreteEvent(ei_event *event) { // both libei and deskflow use multiples of 120 to represent // one scroll wheel click event so we can just forward things // as-is. assert(m_isPrimary); auto dx = ei_event_scroll_get_discrete_dx(event); auto dy = ei_event_scroll_get_discrete_dy(event); LOG_DEBUG1("event: scroll discrete (%d, %d)", dx, dy); // libei and deskflow seem to use opposite directions, so we have // to send the opposite of the value reported by EI if we want to // remain compatible with other platforms (including X11). sendEvent(EventTypes::PrimaryScreenWheel, WheelInfo::alloc(-dx, -dy)); } void EiScreen::onMotionEvent(ei_event *event) { assert(m_isPrimary); auto dx = ei_event_pointer_get_dx(event); auto dy = ei_event_pointer_get_dy(event); if (m_isOnScreen) { LOG_DEBUG("event: motion on primary x=%i y=%i)", m_cursorX, m_cursorY); sendEvent(EventTypes::PrimaryScreenMotionOnPrimary, MotionInfo::alloc(m_cursorX, m_cursorY)); if (m_portalInputCapture->isActive()) { m_portalInputCapture->release(); } } else { m_bufferDX += dx; m_bufferDY += dy; auto pixelDx = static_cast(m_bufferDX); auto pixelDy = static_cast(m_bufferDY); if (pixelDx || pixelDy) { LOG_DEBUG1("event: motion on secondary x=%d y=%d", pixelDx, pixelDy); sendEvent(EventTypes::PrimaryScreenMotionOnSecondary, MotionInfo::alloc(pixelDx, pixelDy)); m_bufferDX -= pixelDx; m_bufferDY -= pixelDy; } } } void EiScreen::onAbsMotionEvent(const ei_event *) const { assert(m_isPrimary); } void EiScreen::handleConnectedToEisEvent(const Event &event) { int fd = static_cast(event.getData())->m_fd; LOG_DEBUG("eis connection established, fd=%d", fd); auto rc = ei_setup_backend_fd(m_ei, fd); if (rc != 0) { LOG_WARN("failed to set up ei: %s", strerror(-rc)); } } void EiScreen::handlePortalSessionClosed() { // Portal may or may not EI_EVENT_DISCONNECT us before sending the DBus Closed // signal. Let's clean up either way. LOG_DEBUG("eis screen handling portal session closed"); cleanupEi(); initEi(); } void EiScreen::handleSystemEvent(const Event &sysevent) { std::scoped_lock lock{m_mutex}; // Only one ei_dispatch per system event, see the comment in // EiEventQueueBuffer::addEvent ei_dispatch(m_ei); struct ei_event *event; while ((event = ei_get_event(m_ei)) != nullptr) { auto type = ei_event_get_type(event); auto seat = ei_event_get_seat(event); auto device = ei_event_get_device(event); switch (type) { case EI_EVENT_CONNECT: LOG_DEBUG("connected to eis"); break; case EI_EVENT_SEAT_ADDED: if (!m_eiSeat) { m_eiSeat = ei_seat_ref(seat); ei_seat_bind_capabilities( m_eiSeat, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_POINTER_ABSOLUTE, EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_BUTTON, EI_DEVICE_CAP_SCROLL, nullptr ); LOG_DEBUG("ei: using seat %s", ei_seat_get_name(m_eiSeat)); // we don't care about touch } break; case EI_EVENT_DEVICE_ADDED: if (seat == m_eiSeat) { addDevice(device); } else { LOG_INFO("seat %s is ignored", ei_seat_get_name(m_eiSeat)); } break; case EI_EVENT_DEVICE_REMOVED: removeDevice(device); break; case EI_EVENT_SEAT_REMOVED: if (seat == m_eiSeat) { m_eiSeat = ei_seat_unref(m_eiSeat); } break; case EI_EVENT_DISCONNECT: // We're using libei which emulates the various seat/device remove events // so by the time we get here our EiScreen should be in a neutral state. // // We must release the xdg-portal InputCapture in case it is still active // so that the cursor is usable and not stuck on the deskflow server. LOG_WARN("disconnected from eis, will afterwards commence attempt to reconnect"); if (m_isPrimary) { LOG_DEBUG("re-allocating portal input capture connection and releasing active captures"); if (m_portalInputCapture) { if (m_portalInputCapture->isActive()) { m_portalInputCapture->release(); } delete m_portalInputCapture; m_portalInputCapture = new PortalInputCapture(this, this->m_events); } } this->handlePortalSessionClosed(); break; case EI_EVENT_DEVICE_PAUSED: LOG_DEBUG("device %s is paused", ei_device_get_name(device)); break; case EI_EVENT_DEVICE_RESUMED: LOG_DEBUG("device %s is resumed", ei_device_get_name(device)); if (!m_isPrimary && m_isOnScreen) { ei_device_start_emulating(device, ++m_sequenceNumber); } break; case EI_EVENT_KEYBOARD_MODIFIERS: // FIXME break; // events below are for a receiver context (barriers) case EI_EVENT_FRAME: break; case EI_EVENT_DEVICE_START_EMULATING: LOG_DEBUG("device %s started emulating", ei_device_get_name(device)); break; case EI_EVENT_DEVICE_STOP_EMULATING: LOG_DEBUG("device %s stopped emulating", ei_device_get_name(device)); break; case EI_EVENT_KEYBOARD_KEY: onKeyEvent(event); break; case EI_EVENT_BUTTON_BUTTON: onButtonEvent(event); break; case EI_EVENT_POINTER_MOTION: onMotionEvent(event); break; case EI_EVENT_POINTER_MOTION_ABSOLUTE: onAbsMotionEvent(event); break; case EI_EVENT_TOUCH_UP: break; case EI_EVENT_TOUCH_MOTION: break; case EI_EVENT_TOUCH_DOWN: break; case EI_EVENT_SCROLL_DELTA: onPointerScrollEvent(event); break; case EI_EVENT_SCROLL_DISCRETE: onPointerScrollDiscreteEvent(event); break; case EI_EVENT_SCROLL_STOP: case EI_EVENT_SCROLL_CANCEL: break; default: break; } ei_event_unref(event); } } void EiScreen::updateButtons() { // libei relies on the EIS implementation to keep our button count correct, // so there's not much we need to/can do here. } IKeyState *EiScreen::getKeyState() const { return m_keyState; } std::string EiScreen::getSecureInputApp() const { throw std::runtime_error("get security input app not implemented"); } EiScreen::HotKeyItem::HotKeyItem(std::uint32_t mask, std::uint32_t id) : mask(mask), id(id) { // Todo: Implement } EiScreen::HotKeySet::HotKeySet(KeyID key) : m_id(key) { // Todo: Implement } bool EiScreen::HotKeySet::removeById(std::uint32_t id) { for (auto it = m_set.begin(); it != m_set.end(); ++it) { if (it->id == id) { m_set.erase(it); return true; } } return false; } void EiScreen::HotKeySet::addItem(HotKeyItem item) { m_set.push_back(item); } std::uint32_t EiScreen::HotKeySet::findByMask(std::uint32_t mask) const { for (const auto &item : m_set) { if (item.mask == mask) { return item.id; } } return 0; } } // namespace deskflow