diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d404fec2..b1b8f11e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,7 @@ jobs: contents: read outputs: compile: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.compile }} + macos: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.macos }} android: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.android }} docker: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.docker }} browser: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.browser }} @@ -105,6 +106,14 @@ jobs: - ".github/workflows/reusable-build-windows.yml" - ".github/workflows/reusable-checks.yml" - ".github/workflows/reusable-tests-lua.yml" + macos: + - "src/**" + - "cmake/**" + - "vcpkg.json" + - "CMakeLists.txt" + - "CMakePresets.json" + - ".github/workflows/ci.yml" + - ".github/workflows/reusable-build-macos.yml" android: - "src/**" - "android/**" @@ -176,6 +185,16 @@ jobs: uses: ./.github/workflows/reusable-build-windows.yml secrets: inherit + build-macos: + name: Build - macOS + needs: [changes, checks, tests-lua] + if: needs.changes.outputs.macos == 'true' && needs.checks.result == 'success' && needs.tests-lua.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) + permissions: + contents: read + pull-requests: read + uses: ./.github/workflows/reusable-build-macos.yml + secrets: inherit + build-android-apk: name: Build - Android APK needs: [changes, checks, tests-lua] diff --git a/.github/workflows/reusable-build-macos.yml b/.github/workflows/reusable-build-macos.yml new file mode 100644 index 000000000..3eb968909 --- /dev/null +++ b/.github/workflows/reusable-build-macos.yml @@ -0,0 +1,137 @@ +name: Reusable macOS Build + +on: + workflow_call: + +permissions: + contents: read + pull-requests: read + +env: + LUKKA_RUN_VCPKG_SHA: "${{ vars.LUKKA_RUN_VCPKG_SHA != '' && vars.LUKKA_RUN_VCPKG_SHA || 'b3dd708d38df5c856fe1c18dc0d59ab771f93921' }}" + CMAKE_BUILD_PARALLEL_LEVEL: 2 + MAKEFLAGS: "-j 2" + +jobs: + build: + if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} + name: ${{ matrix.os }}-${{ matrix.buildtype }} + runs-on: ${{ matrix.os }} + + concurrency: + group: otclient-${{ github.workflow }}-${{ github.ref }}-${{ matrix.buildtype }} + cancel-in-progress: true + + strategy: + fail-fast: false + matrix: + os: [macos-15] + buildtype: [macos-release] + include: + - os: macos-15 + triplet: arm64-osx + cmake_build_type: Release + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Get vcpkg commit ID + id: vcpkg-step + run: | + vcpkgCommitId=$(grep '.builtin-baseline' vcpkg.json | awk -F: '{print $2}' | tr -d '," ') + echo "vcpkgGitCommitId=$vcpkgCommitId" >> $GITHUB_OUTPUT + + - name: Compute vcpkg.json hash + id: hash + run: | + hash=$(shasum -a 256 vcpkg.json | awk '{print toupper($1)}') + echo "hash=$hash" >> $GITHUB_OUTPUT + + - name: Cache vcpkg binary artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cache/vcpkg/archives + ${{ github.workspace }}/vcpkg/downloads + ${{ github.workspace }}/vcpkg/installed + ${{ github.workspace }}/vcpkg/buildtrees + key: vcpkg-${{ matrix.os }}-${{ matrix.buildtype }}-${{ steps.hash.outputs.hash }} + restore-keys: | + vcpkg-${{ matrix.buildtype }}- + + - name: Setup Python + id: python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup vcpkg + uses: lukka/run-vcpkg@a400452f634fe49e9f18d388aeb1809dcc642136 + env: + VCPKG_PYTHON3: ${{ steps.python.outputs.python-path }} + with: + vcpkgGitCommitId: ${{ steps.vcpkg-step.outputs.vcpkgGitCommitId }} + vcpkgJsonGlob: 'vcpkg.json' + + - name: Validate vcpkg baseline SHA + env: + VCPKG_GIT_COMMIT_ID: ${{ steps.vcpkg-step.outputs.vcpkgGitCommitId }} + run: | + SHAS="${VCPKG_GIT_COMMIT_ID}" + if [ -z "${SHAS}" ]; then + echo "::error title=Missing vcpkg baseline::Provide a full 40-char vcpkgGitCommitId." + exit 1 + fi + if ! echo "${SHAS}" | grep -Eq '^[0-9a-f]{40}$'; then + echo "::error title=Invalid vcpkg baseline::vcpkgGitCommitId must be a full 40-char commit SHA." + exit 1 + fi + + - name: Install CMake and Ninja + uses: lukka/get-cmake@v3.31.6 + + - name: Cache CMake build directory + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/build-${{ matrix.buildtype }} + key: cmake-dir-${{ matrix.buildtype }} + restore-keys: | + cmake-dir-${{ matrix.buildtype }} + + - name: Configure and Build + env: + BUILD_DIR: build-${{ matrix.buildtype }} + CMAKE_BUILD_TYPE: ${{ matrix.cmake_build_type }} + VCPKG_TARGET_TRIPLET: ${{ matrix.triplet }} + run: | + cmake -G Ninja -S . -B "${BUILD_DIR}" \ + -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \ + -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake \ + -DVCPKG_TARGET_TRIPLET="${VCPKG_TARGET_TRIPLET}" \ + -DTOGGLE_BIN_FOLDER=ON \ + -DOPTIONS_ENABLE_IPO=OFF \ + -DCMAKE_CXX_FLAGS_RELEASE="-O2 -DNDEBUG" \ + -DCMAKE_C_FLAGS_RELEASE="-O2 -DNDEBUG" \ + -DTOGGLE_BOT_PROTECTION=OFF + + cmake --build "${BUILD_DIR}" --target otclient + + - name: Create app bundle archive + env: + BUILD_DIR: build-${{ matrix.buildtype }} + ARTIFACT_NAME: otclient-${{ matrix.os }}-${{ matrix.buildtype }} + run: | + cd "${BUILD_DIR}/bin" + xattr -cr OTClient.app + codesign --force --deep --sign - OTClient.app + ditto -c -k --keepParent OTClient.app "${ARTIFACT_NAME}.zip" + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: otclient-${{ matrix.os }}-${{ matrix.buildtype }}-${{ github.sha }} + path: build-${{ matrix.buildtype }}/bin/otclient-${{ matrix.os }}-${{ matrix.buildtype }}.zip + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 5fff6df26..3aea3c03e 100644 --- a/.gitignore +++ b/.gitignore @@ -196,6 +196,7 @@ pip-log.txt ################# /otclient +/OTClient.app /modules/otclientrc.lua src/framework/graphics/dx/ diff --git a/CMakePresets.json b/CMakePresets.json index c6b00e7ab..c9833adcd 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -138,7 +138,9 @@ "BUILD_STATIC_LIBRARY": "ON", "CMAKE_BUILD_TYPE": "Release", "OPTIONS_ENABLE_SCCACHE": "ON", - "CMAKE_CXX_FLAGS_RELEASE": "-O1 -DNDEBUG", + "OPTIONS_ENABLE_IPO": "OFF", + "CMAKE_CXX_FLAGS_RELEASE": "-O2 -DNDEBUG", + "CMAKE_C_FLAGS_RELEASE": "-O2 -DNDEBUG", "VCPKG_TARGET_TRIPLET": "arm64-osx", "VCPKG_HOST_TRIPLET": "arm64-osx", "VCPKG_BUILD_TYPE": "release", diff --git a/cmake/Info.plist.in b/cmake/Info.plist.in new file mode 100644 index 000000000..ffa9d2f9b --- /dev/null +++ b/cmake/Info.plist.in @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + @MACOSX_BUNDLE_DISPLAY_NAME@ + CFBundleExecutable + @MACOSX_BUNDLE_EXECUTABLE_NAME@ + CFBundleIdentifier + @MACOSX_BUNDLE_IDENTIFIER@ + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + @MACOSX_BUNDLE_BUNDLE_NAME@ + CFBundlePackageType + APPL + CFBundleShortVersionString + @MACOSX_BUNDLE_SHORT_VERSION_STRING@ + CFBundleVersion + @MACOSX_BUNDLE_VERSION@ + CFBundleIconFile + @MACOSX_BUNDLE_ICON_FILE@ + LSMinimumSystemVersion + @MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION@ + NSHumanReadableCopyright + @MACOSX_BUNDLE_COPYRIGHT@ + NSPrincipalClass + NSApplication + + diff --git a/cmake/OTClient.icns b/cmake/OTClient.icns new file mode 100644 index 000000000..4a52025bb Binary files /dev/null and b/cmake/OTClient.icns differ diff --git a/data/images/clienticon.png b/data/images/clienticon.png index 5b9c6de13..80fe7d693 100644 Binary files a/data/images/clienticon.png and b/data/images/clienticon.png differ diff --git a/modules/corelib/const.lua b/modules/corelib/const.lua index 89cce17ee..f5643f833 100644 --- a/modules/corelib/const.lua +++ b/modules/corelib/const.lua @@ -43,6 +43,8 @@ KeyboardShiftModifier = 4 KeyboardCtrlShiftModifier = 5 KeyboardAltShiftModifier = 6 KeyboardCtrlAltShiftModifier = 7 +KeyboardMetaModifier = 8 +KeyboardPrimaryModifier = 16 MouseNoButton = 0 MouseLeftButton = 1 @@ -90,9 +92,10 @@ KeyRight = 17 KeyNumLock = 18 KeyScrollLock = 19 KeyCapsLock = 20 -KeyCtrl = 21 +KeyCtrlCmd = 21 KeyShift = 22 -KeyAlt = 23 +KeyAltOpt = 23 +KeyControl = 24 KeyMeta = 25 KeyMenu = 26 KeySpace = 32 -- ' ' @@ -217,9 +220,10 @@ KeyCodeDescs = { [KeyNumLock] = 'NumLock', [KeyScrollLock] = 'ScrollLock', [KeyCapsLock] = 'CapsLock', - [KeyCtrl] = 'Ctrl', + [KeyCtrlCmd] = 'Ctrl', [KeyShift] = 'Shift', - [KeyAlt] = 'Alt', + [KeyAltOpt] = 'Alt', + [KeyControl] = 'Control', [KeyMeta] = 'Meta', [KeyMenu] = 'Menu', [KeySpace] = 'Space', @@ -358,3 +362,23 @@ DisplayInherit = 21 EVENT_TEXT_NONE = 0 EVENT_TEXT_CLICK = 1 EVENT_TEXT_HOVER = 2 + +function initPlatformKeyDescs() + local platformType = g_window.getPlatformType() + local isMacOS = platformType:find("MACOS") ~= nil + local isWindows = platformType:find("WIN32") ~= nil + local isX11 = platformType:find("X11") ~= nil + + if isMacOS then + KeyCodeDescs[KeyCtrlCmd] = 'Cmd' + KeyCodeDescs[KeyAltOpt] = 'Option' + KeyCodeDescs[KeyControl] = 'Ctrl' + KeyCodeDescs[KeyMeta] = 'Cmd' + elseif isWindows then + KeyCodeDescs[KeyControl] = 'Ctrl' + KeyCodeDescs[KeyMeta] = 'Win' + elseif isX11 then + KeyCodeDescs[KeyControl] = 'Ctrl' + KeyCodeDescs[KeyMeta] = 'Super' + end +end diff --git a/modules/corelib/corelib.otmod b/modules/corelib/corelib.otmod index 14061a8de..b33e33fef 100644 --- a/modules/corelib/corelib.otmod +++ b/modules/corelib/corelib.otmod @@ -34,5 +34,6 @@ Module dofile 'objectpool' Keybind.init() - + initPlatformKeyDescs() + @onUnload: Keybind.terminate() diff --git a/modules/corelib/keybind.lua b/modules/corelib/keybind.lua index 7340f2c54..a2982b171 100644 --- a/modules/corelib/keybind.lua +++ b/modules/corelib/keybind.lua @@ -534,19 +534,27 @@ function Keybind.setPrimaryActionKey(category, action, preset, keyCombo, chatMod local index = category .. '_' .. action local keybind = Keybind.defaultKeybinds[index] + if keyCombo and type(keyCombo) == "string" and keyCombo:len() > 0 then + keyCombo = retranslateKeyComboDesc(keyCombo) + end + + local numericChatMode = chatMode + chatMode = tostring(chatMode) + local keys = Keybind.configs.keybinds[preset]:getNode(index) if not keys then - keys = table.recursivecopy(keybind.keys) - else - chatMode = tostring(chatMode) + keys = {} + for k, v in pairs(keybind.keys) do + keys[tostring(k)] = table.recursivecopy(v) + end end if keybind.callbacks then Keybind.unbind(category, action) end - + if not keys[chatMode] then - keys[chatMode] = { primary = keyCombo, secondary = keybind.keys[tonumber(chatMode)].secondary } + keys[chatMode] = { primary = keyCombo, secondary = keybind.keys[numericChatMode].secondary } end keys[chatMode].primary = keyCombo @@ -570,19 +578,27 @@ function Keybind.setSecondaryActionKey(category, action, preset, keyCombo, chatM local index = category .. '_' .. action local keybind = Keybind.defaultKeybinds[index] + if keyCombo and type(keyCombo) == "string" and keyCombo:len() > 0 then + keyCombo = retranslateKeyComboDesc(keyCombo) + end + + local numericChatMode = chatMode + chatMode = tostring(chatMode) + local keys = Keybind.configs.keybinds[preset]:getNode(index) if not keys then - keys = table.recursivecopy(keybind.keys) - else - chatMode = tostring(chatMode) + keys = {} + for k, v in pairs(keybind.keys) do + keys[tostring(k)] = table.recursivecopy(v) + end end if keybind.callbacks then Keybind.unbind(category, action) end - + if not keys[chatMode] then - keys[chatMode] = { primary = keybind.keys[tonumber(chatMode)].primary, secondary = keyCombo } + keys[chatMode] = { primary = keybind.keys[numericChatMode].primary, secondary = keyCombo } end keys[chatMode].secondary = keyCombo @@ -640,7 +656,7 @@ function Keybind.getKeybindKeys(category, action, chatMode, preset, forceDefault secondary = keybind.keys[chatMode].secondary } else - keys = keys[chatMode] or keys[tostring(chatMode)] + keys = keys[tostring(chatMode)] or keys[chatMode] end if not keys then @@ -764,6 +780,18 @@ function Keybind.editHotkeyKeys(hotkeyId, primary, secondary, chatMode) Keybind.unbindHotkey(hotkeyId, chatMode) local hotkey = Keybind.hotkeys[chatMode][Keybind.currentPreset][hotkeyId] + if primary ~= nil then + primary = tostring(primary) + if primary:len() > 0 then + primary = retranslateKeyComboDesc(primary) + end + end + if secondary ~= nil then + secondary = tostring(secondary) + if secondary:len() > 0 then + secondary = retranslateKeyComboDesc(secondary) + end + end hotkey.primary = primary or "" hotkey.secondary = secondary or "" Keybind.configs.hotkeys[Keybind.currentPreset]:setNode(chatMode, Keybind.hotkeys[chatMode][Keybind.currentPreset]) diff --git a/modules/corelib/keyboard.lua b/modules/corelib/keyboard.lua index 657c0d29c..d5079e219 100644 --- a/modules/corelib/keyboard.lua +++ b/modules/corelib/keyboard.lua @@ -1,13 +1,93 @@ -- @docclass g_keyboard = {} +local function getPlatformFlags() + local platformType = g_window.getPlatformType() or "" + local isMacOS = platformType:find("MACOS") ~= nil + return isMacOS, not isMacOS +end + +local function resolveKeyAlias(desc) + if not desc then + return nil + end + local key = desc:trim():lower() + local isMacOS = getPlatformFlags() + if key == 'cmd' or key == 'command' then + return KeyMeta + end + if key == 'primary' then + if isMacOS then + return KeyMeta + end + return KeyCtrlCmd + end + if key == 'ctrl' then + if isMacOS then + return KeyMeta + end + return KeyCtrlCmd + end + if key == 'control' then + return KeyCtrlCmd + end + if key == 'alt' or key == 'option' then + return KeyAltOpt + end + if key == 'meta' or key == 'win' or key == 'super' then + return KeyMeta + end + return nil +end + -- private functions +local function canonicalizeKeyCombo(keyCombo) + if not keyCombo or #keyCombo == 0 then + return keyCombo + end + local hasCtrl = false + local hasMeta = false + local hasAlt = false + local hasShift = false + local mainKey = nil + for _, keyCode in ipairs(keyCombo) do + if keyCode == KeyCtrlCmd or keyCode == KeyControl then + hasCtrl = true + elseif keyCode == KeyMeta then + hasMeta = true + elseif keyCode == KeyAltOpt then + hasAlt = true + elseif keyCode == KeyShift then + hasShift = true + else + mainKey = keyCode + end + end + local combo = {} + if hasCtrl then + table.insert(combo, KeyCtrlCmd) + end + if hasMeta then + table.insert(combo, KeyMeta) + end + if hasAlt then + table.insert(combo, KeyAltOpt) + end + if hasShift then + table.insert(combo, KeyShift) + end + if mainKey then + table.insert(combo, mainKey) + end + return combo +end + function translateKeyCombo(keyCombo) if not keyCombo or #keyCombo == 0 then return nil end local keyComboDesc = '' - for k, v in pairs(keyCombo) do + for _, v in ipairs(keyCombo) do local keyDesc = KeyCodeDescs[v] if keyDesc == nil then return nil @@ -19,6 +99,10 @@ function translateKeyCombo(keyCombo) end local function getKeyCode(key) + local alias = resolveKeyAlias(key) + if alias then + return alias + end for keyCode, keyDesc in pairs(KeyCodeDescs) do if keyDesc:lower() == key:trim():lower() then return keyCode @@ -37,38 +121,51 @@ function retranslateKeyComboDesc(keyComboDesc) local keyCombo = {} for i, currentKeyDesc in ipairs(keyComboDesc:split('+')) do + local alias = resolveKeyAlias(currentKeyDesc) + if alias then + table.insert(keyCombo, alias) + else for keyCode, keyDesc in pairs(KeyCodeDescs) do if keyDesc:lower() == currentKeyDesc:trim():lower() then table.insert(keyCombo, keyCode) end end + end end - return translateKeyCombo(keyCombo) + return translateKeyCombo(canonicalizeKeyCombo(keyCombo)) end function determineKeyComboDesc(keyCode, keyboardModifiers) + local isMacOS = getPlatformFlags() local keyCombo = {} - if keyCode == KeyCtrl or keyCode == KeyShift or keyCode == KeyAlt then + if keyCode == KeyShift or keyCode == KeyAltOpt or keyCode == KeyMeta or keyCode == KeyCtrlCmd then table.insert(keyCombo, keyCode) + elseif keyCode == KeyControl then + table.insert(keyCombo, KeyCtrlCmd) elseif KeyCodeDescs[keyCode] ~= nil then - if keyboardModifiers == KeyboardCtrlModifier then - table.insert(keyCombo, KeyCtrl) - elseif keyboardModifiers == KeyboardAltModifier then - table.insert(keyCombo, KeyAlt) - elseif keyboardModifiers == KeyboardCtrlAltModifier then - table.insert(keyCombo, KeyCtrl) - table.insert(keyCombo, KeyAlt) - elseif keyboardModifiers == KeyboardShiftModifier then - table.insert(keyCombo, KeyShift) - elseif keyboardModifiers == KeyboardCtrlShiftModifier then - table.insert(keyCombo, KeyCtrl) - table.insert(keyCombo, KeyShift) - elseif keyboardModifiers == KeyboardAltShiftModifier then - table.insert(keyCombo, KeyAlt) - table.insert(keyCombo, KeyShift) - elseif keyboardModifiers == KeyboardCtrlAltShiftModifier then - table.insert(keyCombo, KeyCtrl) - table.insert(keyCombo, KeyAlt) + local primaryPressed = bit.band(keyboardModifiers, KeyboardPrimaryModifier) ~= 0 + local ctrlPressed = bit.band(keyboardModifiers, KeyboardCtrlModifier) ~= 0 + local metaPressed = bit.band(keyboardModifiers, KeyboardMetaModifier) ~= 0 + + if isMacOS then + if ctrlPressed then + table.insert(keyCombo, KeyCtrlCmd) + end + if primaryPressed or metaPressed then + table.insert(keyCombo, KeyMeta) + end + else + if ctrlPressed or primaryPressed then + table.insert(keyCombo, KeyCtrlCmd) + end + if metaPressed then + table.insert(keyCombo, KeyMeta) + end + end + if bit.band(keyboardModifiers, KeyboardAltModifier) ~= 0 then + table.insert(keyCombo, KeyAltOpt) + end + if bit.band(keyboardModifiers, KeyboardShiftModifier) ~= 0 then table.insert(keyCombo, KeyShift) end table.insert(keyCombo, keyCode) @@ -80,8 +177,11 @@ local function onWidgetKeyDown(widget, keyCode, keyboardModifiers) if keyCode == KeyUnknown then return false end - local callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)] - signalcall(callback, widget, keyCode) + local callback + if keyboardModifiers == KeyboardNoModifier then + callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)] + signalcall(callback, widget, keyCode) + end callback = widget.boundKeyDownCombos[determineKeyComboDesc(keyCode, keyboardModifiers)] return signalcall(callback, widget, keyCode) end @@ -258,7 +358,15 @@ function g_keyboard.isCtrlPressed() if (g_platform.isMobile()) then return false else - return bit.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0 + return bit.band(g_window.getKeyboardModifiers(), KeyboardPrimaryModifier) ~= 0 + end +end + +function g_keyboard.isPrimaryPressed() + if (g_platform.isMobile()) then + return false + else + return bit.band(g_window.getKeyboardModifiers(), KeyboardPrimaryModifier) ~= 0 end end @@ -277,3 +385,52 @@ function g_keyboard.isShiftPressed() return bit.band(g_window.getKeyboardModifiers(), KeyboardShiftModifier) ~= 0 end end + +function g_keyboard.isControlPressed() + if (g_platform.isMobile()) then + return false + else + return bit.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0 + end +end + +function g_keyboard.isMetaPressed() + if (g_platform.isMobile()) then + return false + else + return bit.band(g_window.getKeyboardModifiers(), KeyboardMetaModifier) ~= 0 + end +end + +local function hasOnlyModifiers(modifiers, requiredMask, allowedMask) + return bit.band(modifiers, requiredMask) == requiredMask and bit.band(modifiers, allowedMask) == modifiers +end + +local function primaryAllowedMask() + local _, primaryIsCtrl = getPlatformFlags() + if primaryIsCtrl then + return bit.bor(KeyboardPrimaryModifier, KeyboardCtrlModifier) + end + return KeyboardPrimaryModifier +end + +function g_keyboard.isPrimaryModifierOnly(keyboardModifiers) + if (g_platform.isMobile()) then + return false + end + local allowedMask = primaryAllowedMask() + return hasOnlyModifiers(keyboardModifiers, KeyboardPrimaryModifier, allowedMask) +end + +function g_keyboard.isPrimaryShiftModifierOnly(keyboardModifiers) + if (g_platform.isMobile()) then + return false + end + local requiredMask = bit.bor(KeyboardPrimaryModifier, KeyboardShiftModifier) + local allowedMask = requiredMask + local _, primaryIsCtrl = getPlatformFlags() + if primaryIsCtrl then + allowedMask = bit.bor(allowedMask, KeyboardCtrlModifier) + end + return hasOnlyModifiers(keyboardModifiers, requiredMask, allowedMask) +end diff --git a/modules/corelib/net.lua b/modules/corelib/net.lua index 1c24cca04..0e99f6810 100644 --- a/modules/corelib/net.lua +++ b/modules/corelib/net.lua @@ -1,6 +1,6 @@ function translateNetworkError(errcode, connecting, errdesc) local text - if errcode == 111 then + if errcode == 111 or errcode == 61 then text = tr('Connection refused, the server might be offline or restarting.\nPlease try again later.') elseif errcode == 110 then text = tr('Connection timed out. Either your network is failing or the server is offline.') diff --git a/modules/game_analyser/classes/BossCooldown.lua b/modules/game_analyser/classes/BossCooldown.lua index f0a539f6f..481bd45be 100644 --- a/modules/game_analyser/classes/BossCooldown.lua +++ b/modules/game_analyser/classes/BossCooldown.lua @@ -1,24 +1,24 @@ -- Add capitalize function to string library if it doesn't exist if not string.capitalize then - function string.capitalize(str) - if not str or str == "" or str == nil then - return "Unknown" - end - return str:gsub("(%l)(%w*)", function(first, rest) - return first:upper() .. rest - end) - end + function string.capitalize(str) + if not str or str == "" or str == nil then + return "Unknown" + end + return str:gsub("(%l)(%w*)", function(first, rest) + return first:upper() .. rest + end) + end end -- Function to truncate text to a maximum length local function short_text(text, maxLength) - if not text or text == "" or text == nil then - return "Unknown" - end - if string.len(text) > maxLength then - return text:sub(1, maxLength - 3) .. "..." - end - return text + if not text or text == "" or text == nil then + return "Unknown" + end + if string.len(text) > maxLength then + return text:sub(1, maxLength - 3) .. "..." + end + return text end if not BossCooldown then @@ -26,7 +26,7 @@ if not BossCooldown then launchTime = 0, lastTick = 0, sort = 0, - search = '', + search = "", cooldown = {}, widgets = {}, window = nil, @@ -41,30 +41,30 @@ function BossCooldown.create() BossCooldown.sort = 0 BossCooldown.lastTick = 0 - BossCooldown.search = '' + BossCooldown.search = "" BossCooldown.cooldown = {} BossCooldown.widgets = {} - BossCooldown.window = openedWindows['bossButton'] - + BossCooldown.window = openedWindows["bossButton"] + if not BossCooldown.window then return end - local toggleFilterButton = BossCooldown.window:recursiveGetChildById('toggleFilterButton') + local toggleFilterButton = BossCooldown.window:recursiveGetChildById("toggleFilterButton") if toggleFilterButton then toggleFilterButton:setVisible(false) end - - local newWindowButton = BossCooldown.window:recursiveGetChildById('newWindowButton') + + local newWindowButton = BossCooldown.window:recursiveGetChildById("newWindowButton") if newWindowButton then newWindowButton:setVisible(false) end - local contextMenuButton = BossCooldown.window:recursiveGetChildById('contextMenuButton') - local minimizeButton = BossCooldown.window:recursiveGetChildById('minimizeButton') - + local contextMenuButton = BossCooldown.window:recursiveGetChildById("contextMenuButton") + local minimizeButton = BossCooldown.window:recursiveGetChildById("minimizeButton") + if contextMenuButton and minimizeButton then contextMenuButton:setVisible(true) contextMenuButton:breakAnchors() @@ -72,15 +72,15 @@ function BossCooldown.create() contextMenuButton:addAnchor(AnchorRight, minimizeButton:getId(), AnchorLeft) contextMenuButton:setMarginRight(7) contextMenuButton:setMarginTop(0) - + contextMenuButton.onClick = function(widget, mousePos) local pos = mousePos or g_window.getMousePosition() return onBossExtra(pos) end end - local lockButton = BossCooldown.window:recursiveGetChildById('lockButton') - + local lockButton = BossCooldown.window:recursiveGetChildById("lockButton") + if lockButton and contextMenuButton then lockButton:setVisible(true) lockButton:breakAnchors() @@ -90,66 +90,66 @@ function BossCooldown.create() lockButton:setMarginTop(0) end - local cl = openedWindows['bossButton']:recursiveGetChildById('clickablePanel') + local cl = openedWindows["bossButton"]:recursiveGetChildById("clickablePanel") if cl then cl.onMouseWheel = scrollUIPanel end - + -- Set up the event handlers for the search text local searchText = BossCooldown.window.contentsPanel.searchText if searchText then -- Store original handlers BossCooldown.originalOnKeyPress = searchText.onKeyPress BossCooldown.originalOnTextChange = searchText.onTextChange - + -- Override onKeyPress to unbind movement keys immediately when typing searchText.onKeyPress = function(widget, keyCode, keyboardModifiers) -- Unbind movement keys when user starts typing local gameWalk = modules.game_walk if gameWalk then - gameWalk.unbindWalkKey('W') - gameWalk.unbindWalkKey('D') - gameWalk.unbindWalkKey('S') - gameWalk.unbindWalkKey('A') - gameWalk.unbindWalkKey('E') - gameWalk.unbindWalkKey('Q') - gameWalk.unbindWalkKey('C') - gameWalk.unbindWalkKey('Z') - gameWalk.unbindTurnKey('Ctrl+W') - gameWalk.unbindTurnKey('Ctrl+D') - gameWalk.unbindTurnKey('Ctrl+S') - gameWalk.unbindTurnKey('Ctrl+A') + gameWalk.unbindWalkKey("W") + gameWalk.unbindWalkKey("D") + gameWalk.unbindWalkKey("S") + gameWalk.unbindWalkKey("A") + gameWalk.unbindWalkKey("E") + gameWalk.unbindWalkKey("Q") + gameWalk.unbindWalkKey("C") + gameWalk.unbindWalkKey("Z") + gameWalk.unbindTurnKey("Control+W") + gameWalk.unbindTurnKey("Control+D") + gameWalk.unbindTurnKey("Control+S") + gameWalk.unbindTurnKey("Control+A") end - + -- Handle Escape key to clear focus and restore movement if keyCode == KeyEscape then -- Re-bind movement keys local gameWalk = modules.game_walk if gameWalk then - gameWalk.bindWalkKey('W', North) - gameWalk.bindWalkKey('D', East) - gameWalk.bindWalkKey('S', South) - gameWalk.bindWalkKey('A', West) - gameWalk.bindWalkKey('E', NorthEast) - gameWalk.bindWalkKey('Q', NorthWest) - gameWalk.bindWalkKey('C', SouthEast) - gameWalk.bindWalkKey('Z', SouthWest) - gameWalk.bindTurnKey('Ctrl+W', North) - gameWalk.bindTurnKey('Ctrl+D', East) - gameWalk.bindTurnKey('Ctrl+S', South) - gameWalk.bindTurnKey('Ctrl+A', West) + gameWalk.bindWalkKey("W", North) + gameWalk.bindWalkKey("D", East) + gameWalk.bindWalkKey("S", South) + gameWalk.bindWalkKey("A", West) + gameWalk.bindWalkKey("E", NorthEast) + gameWalk.bindWalkKey("Q", NorthWest) + gameWalk.bindWalkKey("C", SouthEast) + gameWalk.bindWalkKey("Z", SouthWest) + gameWalk.bindTurnKey("Control+W", North) + gameWalk.bindTurnKey("Control+D", East) + gameWalk.bindTurnKey("Control+S", South) + gameWalk.bindTurnKey("Control+A", West) end widget:clearFocus() return false end - + -- Call original handler if it exists if BossCooldown.originalOnKeyPress then return BossCooldown.originalOnKeyPress(widget, keyCode, keyboardModifiers) end return false end - + -- Set up focus change handler searchText.onFocusChange = onBossSearchFocusChange end @@ -160,7 +160,7 @@ function BossCooldown:reset() BossCooldown.sort = 0 BossCooldown.lastTick = 0 - BossCooldown.search = '' + BossCooldown.search = "" BossCooldown.cooldown = {} BossCooldown.widgets = {} @@ -179,7 +179,7 @@ function BossCooldown:checkTicks() local needUpdate = false for _, widget in ipairs(self.widgets) do layout:enableUpdates() - if self.search == '' or string.find(widget.name:lower(), self.search:lower()) then + if self.search == "" or string.find(widget.name:lower(), self.search:lower(), 1, true) then widget:setVisible(true) else widget:setVisible(false) @@ -189,26 +189,26 @@ function BossCooldown:checkTicks() widget.tick = widget.tick - 1 widget.cooldown = os.time() + widget.tick if widget.cooldown < os.time() then - widget.cooldown = os.time() + 60*365*60*24 + widget.cooldown = os.time() + 60 * 365 * 60 * 24 end - local bossCooldownLabel = widget:recursiveGetChildById('bossNCooldown') + local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown") if bossCooldownLabel then if widget.tick <= 0 then - bossCooldownLabel:setText('No Cooldown') - bossCooldownLabel:setColor('#c0c0c0') - if widget.type ~= 'nocd' then + bossCooldownLabel:setText("No Cooldown") + bossCooldownLabel:setColor("#c0c0c0") + if widget.type ~= "nocd" then needUpdate = true - widget.cooldown = os.time() + 60*365*60*24 + widget.cooldown = os.time() + 60 * 365 * 60 * 24 end - widget.type = 'nocd' + widget.type = "nocd" elseif widget.tick <= 60 then - bossCooldownLabel:setText(widget.tick ..'s') - bossCooldownLabel:setColor('#ff9854') - if widget.type ~= 'second' then + bossCooldownLabel:setText(widget.tick .. "s") + bossCooldownLabel:setColor("#ff9854") + if widget.type ~= "second" then needUpdate = true end - widget.type = 'second' + widget.type = "second" else local duration = math.max(1, widget.tick) local days = math.floor(duration / 86400) @@ -219,11 +219,11 @@ function BossCooldown:checkTicks() else bossCooldownLabel:setText(string.format("%02dh %02dmin", hours, minutes)) end - bossCooldownLabel:setColor('#ff9854') - if widget.type ~= 'timed' then + bossCooldownLabel:setColor("#ff9854") + if widget.type ~= "timed" then needUpdate = true end - widget.type = 'timed' + widget.type = "timed" end end end @@ -252,11 +252,11 @@ function BossCooldown:updateWindow() table.sort(BossCooldown.cooldown, function(a, b) local acd = a.cooldown if acd < os.time() then - acd = os.time() + 60*365*60*24 + acd = os.time() + 60 * 365 * 60 * 24 end local bcd = b.cooldown if bcd < os.time() then - bcd = os.time() + 60*365*60*24 + bcd = os.time() + 60 * 365 * 60 * 24 end return acd < bcd end) @@ -266,16 +266,16 @@ function BossCooldown:updateWindow() end) end - contentsPanel.searchText:setText('', false) + contentsPanel.searchText:setText("", false) local c = 1 for _, info in ipairs(BossCooldown.cooldown) do - local widget = g_ui.createWidget('BossInfo', contentsPanel.bosses) - - local creatureWidget = widget:recursiveGetChildById('creature') - local bossNameLabel = widget:recursiveGetChildById('bossName') - local bossCooldownLabel = widget:recursiveGetChildById('bossNCooldown') - + local widget = g_ui.createWidget("BossInfo", contentsPanel.bosses) + + local creatureWidget = widget:recursiveGetChildById("creature") + local bossNameLabel = widget:recursiveGetChildById("bossName") + local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown") + if creatureWidget then if info.outfit then creatureWidget:setOutfit(info.outfit) @@ -286,19 +286,19 @@ function BossCooldown:updateWindow() body = 0, legs = 0, feet = 0, - addons = 0 + addons = 0, } creatureWidget:setOutfit(fallbackOutfit) end end - + if bossNameLabel then local bossName = info.name - + if not bossName or bossName == "" or bossName:trim() == "" then bossName = "Boss " .. (info.bossId or "Unknown") end - + local displayName = short_text(string.capitalize(bossName), 13) bossNameLabel:setText(displayName) widget:setTooltip(string.capitalize(bossName)) @@ -314,13 +314,13 @@ function BossCooldown:updateWindow() local resttime = math.max(0, info.cooldown - os.time()) if bossCooldownLabel then if resttime <= 0 then - bossCooldownLabel:setText('No Cooldown') - bossCooldownLabel:setColor('#c0c0c0') - widget.type = 'nocd' + bossCooldownLabel:setText("No Cooldown") + bossCooldownLabel:setColor("#c0c0c0") + widget.type = "nocd" elseif resttime <= 60 then - bossCooldownLabel:setText(resttime ..'s') - bossCooldownLabel:setColor('#ff9854') - widget.type = 'second' + bossCooldownLabel:setText(resttime .. "s") + bossCooldownLabel:setColor("#ff9854") + widget.type = "second" else local duration = math.max(1, resttime) local days = math.floor(duration / 86400) @@ -331,8 +331,8 @@ function BossCooldown:updateWindow() else bossCooldownLabel:setText(string.format("%02dh %02dmin", hours, minutes)) end - bossCooldownLabel:setColor('#ff9854') - widget.type = 'timed' + bossCooldownLabel:setColor("#ff9854") + widget.type = "timed" end end @@ -350,17 +350,17 @@ end function BossCooldown:setupCooldown(cooldown) BossCooldown.cooldown = {} - + for i, cooldownData in pairs(cooldown) do local raceData = g_things.getRaceData(cooldownData.bossRaceId) - + local bossEntry = { - bossId = cooldownData.bossRaceId, - cooldown = cooldownData.cooldownTime, - name = raceData and raceData.name or "", - outfit = raceData and raceData.outfit or nil + bossId = cooldownData.bossRaceId, + cooldown = cooldownData.cooldownTime, + name = raceData and raceData.name or "", + outfit = raceData and raceData.outfit or nil, } - + BossCooldown.cooldown[#BossCooldown.cooldown + 1] = bossEntry end @@ -369,19 +369,19 @@ function BossCooldown:setupCooldown(cooldown) end function checkBossSearch(text) - if #text <= 1 then - BossCooldown.search = '' + if not text or #text <= 1 then + BossCooldown.search = "" else BossCooldown.search = text end - + -- Immediately apply the search filter if BossCooldown.window and BossCooldown.window.contentsPanel and BossCooldown.window.contentsPanel.bosses then local layout = BossCooldown.window.contentsPanel.bosses:getLayout() if layout then layout:enableUpdates() for _, widget in ipairs(BossCooldown.widgets) do - if BossCooldown.search == '' or string.find(widget.name:lower(), BossCooldown.search:lower()) then + if BossCooldown.search == "" or string.find(widget.name:lower(), BossCooldown.search:lower(), 1, true) then widget:setVisible(true) else widget:setVisible(false) @@ -398,41 +398,41 @@ function onBossSearchFocusChange(widget, focused) -- When gaining focus, unbind movement keys local gameWalk = modules.game_walk if gameWalk then - gameWalk.unbindWalkKey('W') - gameWalk.unbindWalkKey('D') - gameWalk.unbindWalkKey('S') - gameWalk.unbindWalkKey('A') - gameWalk.unbindWalkKey('E') - gameWalk.unbindWalkKey('Q') - gameWalk.unbindWalkKey('C') - gameWalk.unbindWalkKey('Z') - gameWalk.unbindTurnKey('Ctrl+W') - gameWalk.unbindTurnKey('Ctrl+D') - gameWalk.unbindTurnKey('Ctrl+S') - gameWalk.unbindTurnKey('Ctrl+A') + gameWalk.unbindWalkKey("W") + gameWalk.unbindWalkKey("D") + gameWalk.unbindWalkKey("S") + gameWalk.unbindWalkKey("A") + gameWalk.unbindWalkKey("E") + gameWalk.unbindWalkKey("Q") + gameWalk.unbindWalkKey("C") + gameWalk.unbindWalkKey("Z") + gameWalk.unbindTurnKey("Control+W") + gameWalk.unbindTurnKey("Control+D") + gameWalk.unbindTurnKey("Control+S") + gameWalk.unbindTurnKey("Control+A") end else -- When losing focus, bind movement keys back local gameWalk = modules.game_walk if gameWalk then - gameWalk.bindWalkKey('W', North) - gameWalk.bindWalkKey('D', East) - gameWalk.bindWalkKey('S', South) - gameWalk.bindWalkKey('A', West) - gameWalk.bindWalkKey('E', NorthEast) - gameWalk.bindWalkKey('Q', NorthWest) - gameWalk.bindWalkKey('C', SouthEast) - gameWalk.bindWalkKey('Z', SouthWest) - gameWalk.bindTurnKey('Ctrl+W', North) - gameWalk.bindTurnKey('Ctrl+D', East) - gameWalk.bindTurnKey('Ctrl+S', South) - gameWalk.bindTurnKey('Ctrl+A', West) + gameWalk.bindWalkKey("W", North) + gameWalk.bindWalkKey("D", East) + gameWalk.bindWalkKey("S", South) + gameWalk.bindWalkKey("A", West) + gameWalk.bindWalkKey("E", NorthEast) + gameWalk.bindWalkKey("Q", NorthWest) + gameWalk.bindWalkKey("C", SouthEast) + gameWalk.bindWalkKey("Z", SouthWest) + gameWalk.bindTurnKey("Control+W", North) + gameWalk.bindTurnKey("Control+D", East) + gameWalk.bindTurnKey("Control+S", South) + gameWalk.bindTurnKey("Control+A", West) end end end function clearSearch() - BossCooldown.search = '' - BossCooldown.window.contentsPanel.searchText:setText('', false) + BossCooldown.search = "" + BossCooldown.window.contentsPanel.searchText:setText("", false) end function onBossExtra(mousePosition) @@ -442,18 +442,20 @@ function onBossExtra(mousePosition) end local player = g_game.getLocalPlayer() - if not player then return false end + if not player then + return false + end local sortByCooldown = BossCooldown.sort == 0 local sortByName = BossCooldown.sort == 1 - local menu = g_ui.createWidget('PopupMenu') + local menu = g_ui.createWidget("PopupMenu") menu:setGameMenu(true) - menu:addCheckBox(tr('sort by cooldown'), sortByCooldown, function() + menu:addCheckBox(tr("sort by cooldown"), sortByCooldown, function() BossCooldown.sort = 0 BossCooldown:updateWindow() end) - menu:addCheckBox(tr('sort by name'), sortByName, function() + menu:addCheckBox(tr("sort by name"), sortByName, function() BossCooldown.sort = 1 BossCooldown:updateWindow() end) @@ -463,13 +465,13 @@ function onBossExtra(mousePosition) end function toggleBossCDFocus(visible) - local widget = BossCooldown.window:recursiveGetChildById('clickablePanel') + local widget = BossCooldown.window:recursiveGetChildById("clickablePanel") if widget and visible then widget:setPhantom(true) elseif widget then widget:setPhantom(false) widget.onClick = function() - modules.game_interface.toggleInternalFocus(); + modules.game_interface.toggleInternalFocus() toggleBossCDFocus(not visible) end end @@ -482,42 +484,51 @@ function toggleBossCDFocus(visible) -- Immediately unbind movement keys when search becomes active local gameWalk = modules.game_walk if gameWalk then - gameWalk.unbindWalkKey('W') - gameWalk.unbindWalkKey('D') - gameWalk.unbindWalkKey('S') - gameWalk.unbindWalkKey('A') - gameWalk.unbindWalkKey('E') - gameWalk.unbindWalkKey('Q') - gameWalk.unbindWalkKey('C') - gameWalk.unbindWalkKey('Z') - gameWalk.unbindTurnKey('Ctrl+W') - gameWalk.unbindTurnKey('Ctrl+D') - gameWalk.unbindTurnKey('Ctrl+S') - gameWalk.unbindTurnKey('Ctrl+A') + gameWalk.unbindWalkKey("W") + gameWalk.unbindWalkKey("D") + gameWalk.unbindWalkKey("S") + gameWalk.unbindWalkKey("A") + gameWalk.unbindWalkKey("E") + gameWalk.unbindWalkKey("Q") + gameWalk.unbindWalkKey("C") + gameWalk.unbindWalkKey("Z") + gameWalk.unbindTurnKey("Control+W") + gameWalk.unbindTurnKey("Control+D") + gameWalk.unbindTurnKey("Control+S") + gameWalk.unbindTurnKey("Control+A") end else -- Re-bind movement keys when search becomes inactive local gameWalk = modules.game_walk if gameWalk then - gameWalk.bindWalkKey('W', North) - gameWalk.bindWalkKey('D', East) - gameWalk.bindWalkKey('S', South) - gameWalk.bindWalkKey('A', West) - gameWalk.bindWalkKey('E', NorthEast) - gameWalk.bindWalkKey('Q', NorthWest) - gameWalk.bindWalkKey('C', SouthEast) - gameWalk.bindWalkKey('Z', SouthWest) - gameWalk.bindTurnKey('Ctrl+W', North) - gameWalk.bindTurnKey('Ctrl+D', East) - gameWalk.bindTurnKey('Ctrl+S', South) - gameWalk.bindTurnKey('Ctrl+A', West) + gameWalk.bindWalkKey("W", North) + gameWalk.bindWalkKey("D", East) + gameWalk.bindWalkKey("S", South) + gameWalk.bindWalkKey("A", West) + gameWalk.bindWalkKey("E", NorthEast) + gameWalk.bindWalkKey("Q", NorthWest) + gameWalk.bindWalkKey("C", SouthEast) + gameWalk.bindWalkKey("Z", SouthWest) + gameWalk.bindTurnKey("Control+W", North) + gameWalk.bindTurnKey("Control+D", East) + gameWalk.bindTurnKey("Control+S", South) + gameWalk.bindTurnKey("Control+A", West) end BossCooldown.window:setBorderWidth(0) end end function updateBossFocus() - scheduleEvent(function() BossCooldown.window:recursiveGetChildById('miniwindowScrollBar'):setValue(1) end, 1) + scheduleEvent(function() + if not BossCooldown.window then + return + end + local scrollBar = BossCooldown.window:recursiveGetChildById("miniwindowScrollBar") + if not scrollBar then + return + end + scrollBar:setValue(1) + end, 1) end function BossCooldown:hasCooldown(raceId) @@ -532,7 +543,7 @@ end function BossCooldown:getCooldown(raceId) for _, widget in pairs(BossCooldown.widgets) do if widget.bossId and widget.bossId == raceId and widget.cooldown and widget.cooldown > os.time() then - local bossCooldownLabel = widget:recursiveGetChildById('bossNCooldown') + local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown") if bossCooldownLabel then return bossCooldownLabel:getText() end diff --git a/modules/game_console/console.lua b/modules/game_console/console.lua index fef417e06..899f63622 100644 --- a/modules/game_console/console.lua +++ b/modules/game_console/console.lua @@ -221,7 +221,7 @@ function consoleController:onInit() consoleTabBar.onDragLeave = onDragLeave consoleTabBar.onDragMove = onDragMove consolePanel.onKeyPress = function(self, keyCode, keyboardModifiers) - if not (keyboardModifiers == KeyboardCtrlModifier and keyCode == KeyC) then + if not (g_keyboard.isPrimaryModifierOnly(keyboardModifiers) and keyCode == KeyC) then return false end @@ -377,10 +377,10 @@ local function unbindMovingKeys() gameWalk.unbindWalkKey('C') gameWalk.unbindWalkKey('Z') - gameWalk.unbindTurnKey('Ctrl+W') - gameWalk.unbindTurnKey('Ctrl+D') - gameWalk.unbindTurnKey('Ctrl+S') - gameWalk.unbindTurnKey('Ctrl+A') + gameWalk.unbindTurnKey('Control+W') + gameWalk.unbindTurnKey('Control+D') + gameWalk.unbindTurnKey('Control+S') + gameWalk.unbindTurnKey('Control+A') end local function bindMovingKeys() @@ -395,10 +395,10 @@ local function bindMovingKeys() gameWalk.bindWalkKey('C', SouthEast) gameWalk.bindWalkKey('Z', SouthWest) - gameWalk.bindTurnKey('Ctrl+W', North) - gameWalk.bindTurnKey('Ctrl+D', East) - gameWalk.bindTurnKey('Ctrl+S', South) - gameWalk.bindTurnKey('Ctrl+A', West) + gameWalk.bindTurnKey('Control+W', North) + gameWalk.bindTurnKey('Control+D', East) + gameWalk.bindTurnKey('Control+S', South) + gameWalk.bindTurnKey('Control+A', West) end function switchChat(enabled) diff --git a/modules/game_hotkeys/hotkeys_manager.lua b/modules/game_hotkeys/hotkeys_manager.lua index 75d62a17f..f2843d9c1 100644 --- a/modules/game_hotkeys/hotkeys_manager.lua +++ b/modules/game_hotkeys/hotkeys_manager.lua @@ -978,8 +978,17 @@ function canPerformKeyCombo(keyCombo) if not modules.game_console.isChatEnabled() then return true end + local platformType = g_window.getPlatformType() or "" + local isMacOS = platformType:find("MACOS") ~= nil + if isMacOS then + return string.match(keyCombo, "Cmd%+") or + string.match(keyCombo, "Ctrl%+") or + string.match(keyCombo, "Alt%+") or + string.match(keyCombo, "Option%+") or + string.match(keyCombo, "F%d+") + end return string.match(keyCombo, "Ctrl%+") or - string.match(keyCombo, "Alt%+") or + string.match(keyCombo, "Alt%+") or string.match(keyCombo, "F%d+") end diff --git a/modules/game_interface/gameinterface.lua b/modules/game_interface/gameinterface.lua index 2013bb0f4..13a09f1e3 100644 --- a/modules/game_interface/gameinterface.lua +++ b/modules/game_interface/gameinterface.lua @@ -1164,7 +1164,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then g_game.look(lookThing) return true - elseif useThing and keyboardModifiers == KeyboardCtrlModifier and + elseif useThing and g_keyboard.isPrimaryModifierOnly(keyboardModifiers) and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then local smartLeftClick = modules.client_options.getOption('smartLeftClick') @@ -1195,7 +1195,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u end end return true - elseif useThing and useThing:isContainer() and keyboardModifiers == KeyboardCtrlShiftModifier and + elseif useThing and useThing:isContainer() and g_keyboard.isPrimaryShiftModifierOnly(keyboardModifiers) and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then g_game.open(useThing) return true @@ -1442,7 +1442,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u end -- Common key combinations for all Classic Control modes - if useThing and useThing:isContainer() and keyboardModifiers == KeyboardCtrlShiftModifier and + if useThing and useThing:isContainer() and g_keyboard.isPrimaryShiftModifierOnly(keyboardModifiers) and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then g_game.open(useThing) return true @@ -1454,7 +1454,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then g_game.look(lookThing) return true - elseif useThing and keyboardModifiers == KeyboardCtrlModifier and + elseif useThing and g_keyboard.isPrimaryModifierOnly(keyboardModifiers) and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then createThingMenu(menuPosition, lookThing, useThing, creatureThing) return true diff --git a/modules/game_walk/walk.lua b/modules/game_walk/walk.lua index db63fedab..d27f99e86 100644 --- a/modules/game_walk/walk.lua +++ b/modules/game_walk/walk.lua @@ -23,10 +23,10 @@ local keys = { } local turnKeys = { - { "Ctrl+Up", North }, - { "Ctrl+Right", East }, - { "Ctrl+Down", South }, - { "Ctrl+Left", West }, + { "Control+Up", North }, + { "Control+Right", East }, + { "Control+Down", South }, + { "Control+Left", West }, } WalkController = Controller:new() diff --git a/modules/gamelib/ui/uiminimap.lua b/modules/gamelib/ui/uiminimap.lua index a69b254dd..24a32594f 100644 --- a/modules/gamelib/ui/uiminimap.lua +++ b/modules/gamelib/ui/uiminimap.lua @@ -256,9 +256,9 @@ function UIMinimap:onMouseWheel(mousePos, direction) self:zoomIn() elseif direction == MouseWheelDown and keyboardModifiers == KeyboardNoModifier then self:zoomOut() - elseif direction == MouseWheelDown and keyboardModifiers == KeyboardCtrlModifier then + elseif direction == MouseWheelDown and g_keyboard.isPrimaryModifierOnly(keyboardModifiers) then self:floorUp(1) - elseif direction == MouseWheelUp and keyboardModifiers == KeyboardCtrlModifier then + elseif direction == MouseWheelUp and g_keyboard.isPrimaryModifierOnly(keyboardModifiers) then self:floorDown(1) end end diff --git a/overlay-ports/inih/CMakeLists.txt b/overlay-ports/inih/CMakeLists.txt new file mode 100644 index 000000000..75945834a --- /dev/null +++ b/overlay-ports/inih/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.16) +project(inih LANGUAGES C CXX) + +option(INIH_WITH_INI_READER "Build C++ INIReader library" OFF) +option(INIH_WITH_DEBUG "Enable debug features" OFF) + +add_library(inih ini.c) +set_target_properties(inih PROPERTIES OUTPUT_NAME inih) +target_include_directories(inih + PUBLIC + $ + $ +) + +if(INIH_WITH_DEBUG) + target_compile_definitions(inih PUBLIC INI_DEBUG) +endif() + +if(INIH_WITH_INI_READER) + add_library(INIReader cpp/INIReader.cpp) + set_target_properties(INIReader PROPERTIES OUTPUT_NAME INIReader) + target_compile_features(INIReader PUBLIC cxx_std_11) + target_include_directories(INIReader + PUBLIC + $ + $ + $ + ) + target_link_libraries(INIReader PUBLIC inih) +endif() + +include(GNUInstallDirs) + +install(TARGETS inih + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +if(INIH_WITH_INI_READER) + install(TARGETS INIReader + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ) +endif() + +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ini.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +if(INIH_WITH_INI_READER) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cpp/INIReader.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +endif() diff --git a/overlay-ports/inih/portfile.cmake b/overlay-ports/inih/portfile.cmake new file mode 100644 index 000000000..5afbfba7b --- /dev/null +++ b/overlay-ports/inih/portfile.cmake @@ -0,0 +1,62 @@ +vcpkg_check_linkage(ONLY_STATIC_LIBRARY) + +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO benhoyt/inih + REF "r${VERSION}" + SHA512 d69f488299c1896e87ddd3dd20cd9db5848da7afa4c6159b8a99ba9a5d33f35cadfdb9f65d6f2fe31decdbadb8b43bf610ff2699df475e1f9ff045e343ac26ae + HEAD_REF master +) + +vcpkg_check_features( + OUT_FEATURE_OPTIONS FEATURE_OPTIONS + FEATURES + cpp INIH_WITH_INI_READER +) + +set(INIH_WITH_INI_READER OFF) +if(DEFINED VCPKG_FEATURES) + if("cpp" IN_LIST VCPKG_FEATURES) + set(INIH_WITH_INI_READER ON) + endif() +else() + # Older vcpkg baselines may not populate VCPKG_FEATURES, but we always need INIReader. + set(INIH_WITH_INI_READER ON) +endif() + +set(FEATURE_OPTIONS "") +if(INIH_WITH_INI_READER) + list(APPEND FEATURE_OPTIONS "-DINIH_WITH_INI_READER=ON") +endif() + +if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + set(INIH_CONFIG_DEBUG ON) +else() + set(INIH_CONFIG_DEBUG OFF) +endif() + +configure_file( + "${CMAKE_CURRENT_LIST_DIR}/unofficial-inihConfig.cmake.in" + "${CURRENT_PACKAGES_DIR}/share/unofficial-inih/unofficial-inihConfig.cmake" + @ONLY +) + +file(COPY "${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt" DESTINATION "${SOURCE_PATH}") + +vcpkg_configure_cmake( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS_DEBUG + ${FEATURE_OPTIONS} + "-DINIH_WITH_DEBUG=ON" + OPTIONS_RELEASE + ${FEATURE_OPTIONS} + "-DINIH_WITH_DEBUG=OFF" +) + +vcpkg_install_cmake() + +vcpkg_copy_pdbs() + +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") + +configure_file("${CMAKE_CURRENT_LIST_DIR}/usage" "${CURRENT_PACKAGES_DIR}/share/${PORT}/usage" COPYONLY) diff --git a/overlay-ports/inih/unofficial-inihConfig.cmake.in b/overlay-ports/inih/unofficial-inihConfig.cmake.in new file mode 100644 index 000000000..0d39426ed --- /dev/null +++ b/overlay-ports/inih/unofficial-inihConfig.cmake.in @@ -0,0 +1,60 @@ +if(TARGET unofficial::inih::libinih) + return() +endif() + +set(INIH_WITH_INI_READER @INIH_WITH_INI_READER@) +set(INIH_WITH_DEBUG @INIH_CONFIG_DEBUG@) + +# Compute the installation prefix relative to this file. +get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) + +################### +#### libinih #### + +add_library(unofficial::inih::libinih UNKNOWN IMPORTED) + +find_library(INIH_INIHLIB_RELEASE NAMES inih PATHS "${_IMPORT_PREFIX}/lib/" REQUIRED NO_DEFAULT_PATH) +set_target_properties(unofficial::inih::libinih PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + IMPORTED_LOCATION_RELEASE "${INIH_INIHLIB_RELEASE}" + IMPORTED_CONFIGURATIONS "RELEASE" +) + +if(INIH_WITH_DEBUG) + set_property(TARGET unofficial::inih::libinih APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG) + find_library(INIH_INIHLIB_DEBUG NAMES inih PATHS "${_IMPORT_PREFIX}/debug/lib/" REQUIRED NO_DEFAULT_PATH) + set_target_properties(unofficial::inih::libinih PROPERTIES + IMPORTED_LOCATION_DEBUG "${INIH_INIHLIB_DEBUG}" + ) +endif() + +#### libinih #### +################### +#### INIReader #### + +if(INIH_WITH_INI_READER) + add_library(unofficial::inih::inireader UNKNOWN IMPORTED) + + find_library(INIH_INIREADER_RELEASE NAMES INIReader PATHS "${_IMPORT_PREFIX}/lib/" REQUIRED NO_DEFAULT_PATH) + set_target_properties(unofficial::inih::inireader PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" + IMPORTED_LINK_INTERFACE_LANGUAGES "C;CXX" + IMPORTED_LOCATION_RELEASE "${INIH_INIREADER_RELEASE}" + INTERFACE_LINK_LIBRARIES "unofficial::inih::libinih" + IMPORTED_CONFIGURATIONS "RELEASE" + ) + + if(INIH_WITH_DEBUG) + set_property(TARGET unofficial::inih::inireader APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG) + find_library(INIH_INIREADER_DEBUG NAMES INIReader PATHS "${_IMPORT_PREFIX}/debug/lib/" REQUIRED NO_DEFAULT_PATH) + set_target_properties(unofficial::inih::inireader PROPERTIES + IMPORTED_LOCATION_DEBUG "${INIH_INIREADER_DEBUG}" + ) + endif() +endif() + +#### INIReader #### +################### diff --git a/overlay-ports/inih/usage b/overlay-ports/inih/usage new file mode 100644 index 000000000..7d6fa57c2 --- /dev/null +++ b/overlay-ports/inih/usage @@ -0,0 +1,7 @@ +The package inih provides unofficial CMake targets: + find_package(unofficial-inih CONFIG REQUIRED) + # C API + target_link_libraries(main PRIVATE unofficial::inih::libinih) + # C++ API (Requires "cpp" feature) + target_link_libraries(main PRIVATE unofficial::inih::inireader) +Alternatively, if you are using pkg-config use the name "inih" for the C API and "inireader" for the C++ API diff --git a/overlay-ports/inih/vcpkg.json b/overlay-ports/inih/vcpkg.json new file mode 100644 index 000000000..a56971e1d --- /dev/null +++ b/overlay-ports/inih/vcpkg.json @@ -0,0 +1,12 @@ +{ + "name": "inih", + "version-string": "58", + "description": "Simple .INI file parser", + "homepage": "https://github.com/benhoyt/inih", + "license": "BSD-3-Clause", + "features": { + "cpp": { + "description": "C++ INIReader wrapper" + } + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d512752ea..c7dcecec9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -176,9 +176,15 @@ if(APPLE) find_library(FOUNDATION Foundation REQUIRED) find_library(IOKIT IOKit REQUIRED) endif() -if(UNIX AND NOT ANDROID AND NOT WASM) +if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT WASM) find_package(X11 REQUIRED) endif() +if(APPLE AND NOT ANDROID AND NOT WASM) + find_library(COCOA_LIBRARY Cocoa REQUIRED) + find_library(COREVIDEO_LIBRARY CoreVideo REQUIRED) + find_library(QUARTZCORE_LIBRARY QuartzCore REQUIRED) + find_library(OPENGL_FRAMEWORK OpenGL REQUIRED) +endif() if(WIN32) find_package(DbgHelp REQUIRED) endif(WIN32) @@ -430,8 +436,6 @@ if (TOGGLE_FRAMEWORK_GRAPHICS) framework/graphics/texture.cpp framework/graphics/texturemanager.cpp framework/graphics/shadermanager.cpp - framework/platform/win32window.cpp - framework/platform/x11window.cpp framework/ui/uianchorlayout.cpp framework/ui/uiboxlayout.cpp framework/ui/uigridlayout.cpp @@ -484,11 +488,30 @@ if(ANDROID) ) endif() +if(WIN32) + set(SOURCE_FILES ${SOURCE_FILES} framework/platform/win32window.cpp) +elseif(APPLE AND NOT ANDROID AND NOT WASM) + set(SOURCE_FILES ${SOURCE_FILES} + framework/platform/cocoawindow.mm + framework/platform/cocoaview.mm + ) + set_source_files_properties( + framework/net/httplogin.cpp + framework/platform/cocoawindow.mm + framework/platform/cocoaview.mm + PROPERTIES + SKIP_UNITY_BUILD_INCLUSION ON + SKIP_PRECOMPILE_HEADERS ON + ) +elseif(UNIX AND NOT ANDROID AND NOT WASM) + set(SOURCE_FILES ${SOURCE_FILES} framework/platform/x11window.cpp) +endif() + add_library(otclient_core STATIC ${SOURCE_FILES}) target_link_libraries(otclient_core PRIVATE unofficial::inih::inireader) -if(NOT MSVC) +if(NOT MSVC AND CMAKE_INTERPROCEDURAL_OPTIMIZATION MATCHES "^(ON|1|TRUE)$") target_link_options(otclient_core PUBLIC -flto=auto) endif() @@ -916,7 +939,7 @@ else() # Linux absl::log_internal_check_op Threads::Threads - X11::X11 + $<$,$>>:X11::X11> asio::asio OpenAL::OpenAL LibLZMA::LibLZMA @@ -940,6 +963,16 @@ else() # Linux ) endif() + if(APPLE AND NOT ANDROID AND NOT WASM) + target_link_libraries(otclient_core + PUBLIC + ${COCOA_LIBRARY} + ${COREVIDEO_LIBRARY} + ${QUARTZCORE_LIBRARY} + ${OPENGL_FRAMEWORK} + ) + endif() + if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_options(otclient_core PRIVATE @@ -948,6 +981,41 @@ else() # Linux endif() endif() + +if(APPLE AND NOT ANDROID AND NOT WASM) + set(MACOSX_BUNDLE_BUNDLE_NAME "OTClient") + set(MACOSX_BUNDLE_DISPLAY_NAME "OTClient") + set(MACOSX_BUNDLE_IDENTIFIER "com.otclient.otclient") + set(MACOSX_BUNDLE_VERSION "${VERSION}") + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${VERSION}") + set(MACOSX_BUNDLE_COPYRIGHT "Copyright © 2024") + set(MACOSX_BUNDLE_ICON_FILE "OTClient") + if(DEFINED CMAKE_OSX_DEPLOYMENT_TARGET) + set(MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION "${CMAKE_OSX_DEPLOYMENT_TARGET}") + else() + set(MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION "11.0") + endif() + + set(OTCLIENT_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist") + configure_file("${CMAKE_SOURCE_DIR}/cmake/Info.plist.in" "${OTCLIENT_INFO_PLIST}" @ONLY) + + set_target_properties(${PROJECT_NAME} PROPERTIES + OUTPUT_NAME "OTClient" + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST "${OTCLIENT_INFO_PLIST}" + MACOSX_BUNDLE_ICON_FILE "OTClient" + ) + + add_custom_command(TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_SOURCE_DIR}/cmake/OTClient.icns" + "$/../Resources/OTClient.icns" + COMMENT "Copying OTClient.icns to app bundle Resources" + VERBATIM + ) + endif() + if(ENABLE_DISCORD_RPC AND NOT ANDROID) target_link_libraries(otclient_core PUBLIC ${DISCORDRPC_LIBRARY}) endif() diff --git a/src/client/map.cpp b/src/client/map.cpp index aca796b40..3d6554b9e 100644 --- a/src/client/map.cpp +++ b/src/client/map.cpp @@ -1158,7 +1158,7 @@ void Map::findPathAsync(const Position& start, const Position& goal, const std:: } } - g_asyncDispatcher.detach_task([=] { + g_asyncDispatcher->detach_task([=] { const auto ret = g_map.newFindPath(start, goal, visibleNodes); g_dispatcher.addEvent(std::bind(callback, ret)); }); diff --git a/src/client/mapview.cpp b/src/client/mapview.cpp index d8129bf11..cb4d95f26 100644 --- a/src/client/mapview.cpp +++ b/src/client/mapview.cpp @@ -46,7 +46,7 @@ MapView::MapView() : m_lightView(std::make_unique(Size())), m_pool(g_drawPool.get(DrawPoolType::MAP)) { m_floors.resize(g_gameConfig.getMapMaxZ() + 1); - m_floorThreads.resize(g_asyncDispatcher.get_thread_count()); + m_floorThreads.resize(g_asyncDispatcher->get_thread_count()); for (auto& thread : m_floorThreads) thread.resize(m_floors.size()); @@ -391,7 +391,7 @@ void MapView::updateVisibleTiles() }; if (m_multithreading) { - static const int numThreads = g_asyncDispatcher.get_thread_count(); + static const int numThreads = g_asyncDispatcher->get_thread_count(); static BS::multi_future tasks(numThreads); tasks.clear(); @@ -404,7 +404,7 @@ void MapView::updateVisibleTiles() for (auto& floor : m_floorThreads[i]) floor.cachedVisibleTiles.clear(); - tasks.emplace_back(g_asyncDispatcher.submit_task([=, this] { + tasks.emplace_back(g_asyncDispatcher->submit_task([=, this] { processDiagonalRange(m_floorThreads[i], start, end); })); } diff --git a/src/client/spritemanager.cpp b/src/client/spritemanager.cpp index 5fdee13da..20fd48f96 100644 --- a/src/client/spritemanager.cpp +++ b/src/client/spritemanager.cpp @@ -54,7 +54,7 @@ void SpriteManager::reload() { } void SpriteManager::load() { - m_spritesFiles.resize(g_asyncDispatcher.get_thread_count()); + m_spritesFiles.resize(g_asyncDispatcher->get_thread_count()); if (g_app.isLoadingAsyncTexture()) { for (auto& file : m_spritesFiles) file = std::make_unique(g_resources.openFile(m_lastFileName)); diff --git a/src/client/thingtype.cpp b/src/client/thingtype.cpp index 698c7d7a3..4e4b6a5a3 100644 --- a/src/client/thingtype.cpp +++ b/src/client/thingtype.cpp @@ -809,7 +809,7 @@ const TexturePtr& ThingType::getTexture(const int animationPhase) m_loading.store(false, std::memory_order_release); }; - g_asyncDispatcher.detach_task(std::move(action)); + g_asyncDispatcher->detach_task(std::move(action)); } return m_textureNull; diff --git a/src/framework/const.h b/src/framework/const.h index 0cb6a3be7..d7cd1dd07 100644 --- a/src/framework/const.h +++ b/src/framework/const.h @@ -283,9 +283,34 @@ namespace Fw KeyboardNoModifier = 0, KeyboardCtrlModifier = 1, KeyboardAltModifier = 2, - KeyboardShiftModifier = 4 + KeyboardShiftModifier = 4, + KeyboardMetaModifier = 8, + KeyboardPrimaryModifier = 16 }; + inline bool isPrimaryModifierOnly(const int modifiers) + { +#if defined(__APPLE__) + const int primaryMask = KeyboardPrimaryModifier; +#else + const int primaryMask = KeyboardPrimaryModifier | KeyboardCtrlModifier; +#endif + return (modifiers & primaryMask) == primaryMask && + (modifiers & ~primaryMask) == 0; + } + + inline bool isPrimaryShiftModifierOnly(const int modifiers) + { +#if defined(__APPLE__) + const int primaryMask = KeyboardPrimaryModifier; +#else + const int primaryMask = KeyboardPrimaryModifier | KeyboardCtrlModifier; +#endif + const int requiredMask = primaryMask | KeyboardShiftModifier; + return (modifiers & requiredMask) == requiredMask && + (modifiers & ~requiredMask) == 0; + } + enum WidgetState : int32_t { InvalidState = -1, diff --git a/src/framework/core/application.cpp b/src/framework/core/application.cpp index 314800079..f60e8930d 100644 --- a/src/framework/core/application.cpp +++ b/src/framework/core/application.cpp @@ -22,6 +22,7 @@ #include "application.h" +#include "asyncdispatcher.h" #include #define ADD_QUOTES_HELPER(s) #s @@ -151,6 +152,8 @@ void Application::terminate() // terminate proxy g_proxy.terminate(); + g_asyncDispatcher.reset(); + m_terminated = true; signal(SIGTERM, SIG_DFL); diff --git a/src/framework/core/asyncdispatcher.cpp b/src/framework/core/asyncdispatcher.cpp index 5622fb33b..df22ea863 100644 --- a/src/framework/core/asyncdispatcher.cpp +++ b/src/framework/core/asyncdispatcher.cpp @@ -38,4 +38,4 @@ uint8_t getThreadCount() { return std::clamp(std::thread::hardware_concurrency() - 1, MIN_THREADS, MAX_THREADS); } -AsyncDispatcher g_asyncDispatcher{ getThreadCount() }; +std::unique_ptr g_asyncDispatcher = std::make_unique(getThreadCount()); diff --git a/src/framework/core/asyncdispatcher.h b/src/framework/core/asyncdispatcher.h index 62e4f03a7..10e52f5d0 100644 --- a/src/framework/core/asyncdispatcher.h +++ b/src/framework/core/asyncdispatcher.h @@ -25,7 +25,8 @@ #include #include +#include using AsyncDispatcher = decltype(BS::thread_pool{ std::size_t{} }); -extern AsyncDispatcher g_asyncDispatcher; +extern std::unique_ptr g_asyncDispatcher; diff --git a/src/framework/core/eventdispatcher.cpp b/src/framework/core/eventdispatcher.cpp index 8b79bb1d6..fbf720d75 100644 --- a/src/framework/core/eventdispatcher.cpp +++ b/src/framework/core/eventdispatcher.cpp @@ -31,7 +31,7 @@ int16_t g_mainThreadId = stdext::getThreadId(); int16_t g_eventThreadId = -1; void EventDispatcher::init() { - for (size_t i = 0; i < g_asyncDispatcher.get_thread_count(); ++i) { + for (size_t i = 0; i < g_asyncDispatcher->get_thread_count(); ++i) { m_threads.emplace_back(std::make_unique()); } } diff --git a/src/framework/core/graphicalapplication.cpp b/src/framework/core/graphicalapplication.cpp index f38897900..420bdd058 100644 --- a/src/framework/core/graphicalapplication.cpp +++ b/src/framework/core/graphicalapplication.cpp @@ -198,7 +198,7 @@ void GraphicalApplication::run() }; #endif // THREAD - POOL & MAP - const auto& mapThread = g_asyncDispatcher.submit_task([this] { + const auto& mapThread = g_asyncDispatcher->submit_task([this] { BS::multi_future tasks; g_luaThreadId = g_eventThreadId = stdext::getThreadId(); @@ -214,7 +214,7 @@ void GraphicalApplication::run() if (canDrawMap()) { if (canDrawForeground) { - tasks.emplace_back(g_asyncDispatcher.submit_task([] { + tasks.emplace_back(g_asyncDispatcher->submit_task([] { AutoStat s(STATS_RENDER, "DrawForegroundUI"); g_ui.render(DrawPoolType::FOREGROUND); })); @@ -227,7 +227,7 @@ void GraphicalApplication::run() static constexpr std::array types{ DrawPoolType::LIGHT, DrawPoolType::FOREGROUND_MAP }; for (const auto type : types) { if (m_drawEvents->canDraw(type)) { - tasks.emplace_back(g_asyncDispatcher.submit_task([this, type] { + tasks.emplace_back(g_asyncDispatcher->submit_task([this, type] { AutoStat s(STATS_RENDER, type == DrawPoolType::LIGHT ? "DrawLight" : "DrawForegroundMap"); m_drawEvents->draw(type); })); @@ -382,7 +382,7 @@ void GraphicalApplication::doScreenshot(std::string file) auto pixels = std::make_shared>(width * height * 4 * sizeof(GLubyte), 0); glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels->data()); - g_asyncDispatcher.detach_task([resolution, pixels, file] { + g_asyncDispatcher->detach_task([resolution, pixels, file] { try { Image image(resolution, 4, pixels->data()); image.flipVertically(); diff --git a/src/framework/core/modulemanager.cpp b/src/framework/core/modulemanager.cpp index 489071852..de73471ac 100644 --- a/src/framework/core/modulemanager.cpp +++ b/src/framework/core/modulemanager.cpp @@ -220,6 +220,6 @@ void ModuleManager::enableAutoReload() { return; processing.store(true); - g_asyncDispatcher.detach_task(action); + g_asyncDispatcher->detach_task(action); }, 500); } \ No newline at end of file diff --git a/src/framework/core/resourcemanager.cpp b/src/framework/core/resourcemanager.cpp index 2a080faf9..7a16a5173 100644 --- a/src/framework/core/resourcemanager.cpp +++ b/src/framework/core/resourcemanager.cpp @@ -61,7 +61,8 @@ bool ResourceManager::discoverWorkDir(const std::string& existentFile) g_resources.getBaseDir(), g_resources.getBaseDir() + "/game_data/", g_resources.getBaseDir() + "../", - g_resources.getBaseDir() + "../share/" + g_app.getCompactName() + "/" }; + g_resources.getBaseDir() + "../share/" + g_app.getCompactName() + "/", + }; bool found = false; for (const auto& dir : possiblePaths) { @@ -703,21 +704,33 @@ bool ResourceManager::launchCorrect(const std::vector& args) { // c #if (defined(ANDROID) || defined(FREE_VERSION)) return false; #else - auto fileName2 = m_binaryPath.stem().string(); - fileName2 = stdext::split(fileName2, "-")[0]; - stdext::tolower(fileName2); + const auto normalizeName = [](std::string name) { + const auto dash = name.find('-'); + if (dash != std::string::npos) { + name = name.substr(0, dash); + } + stdext::tolower(name); + return name; + }; + + auto fileName2 = normalizeName(m_binaryPath.stem().string()); const std::filesystem::path path(m_binaryPath.parent_path()); std::error_code ec; + if (path.empty() || !std::filesystem::exists(path, ec) || ec) { + return false; + } + auto lastWrite = last_write_time(m_binaryPath, ec); std::filesystem::path binary = m_binaryPath; - for (auto& entry : std::filesystem::directory_iterator(path)) { + for (auto it = std::filesystem::directory_iterator(path, ec); + !ec && it != std::filesystem::directory_iterator(); + ++it) { + const auto& entry = *it; if (is_directory(entry.path())) continue; - auto fileName1 = entry.path().stem().string(); - fileName1 = stdext::split(fileName1, "-")[0]; - stdext::tolower(fileName1); + auto fileName1 = normalizeName(entry.path().stem().string()); if (fileName1 != fileName2) continue; @@ -731,13 +744,18 @@ bool ResourceManager::launchCorrect(const std::vector& args) { // c } } - for (auto& entry : std::filesystem::directory_iterator(path)) { // remove old + if (ec) { + return false; + } + + for (auto it = std::filesystem::directory_iterator(path, ec); + !ec && it != std::filesystem::directory_iterator(); + ++it) { // remove old + const auto& entry = *it; if (is_directory(entry.path())) continue; - auto fileName1 = entry.path().stem().string(); - fileName1 = stdext::split(fileName1, "-")[0]; - stdext::tolower(fileName1); + auto fileName1 = normalizeName(entry.path().stem().string()); if (fileName1 != fileName2) continue; @@ -749,6 +767,10 @@ bool ResourceManager::launchCorrect(const std::vector& args) { // c } } + if (ec) { + return false; + } + if (binary == m_binaryPath) return false; diff --git a/src/framework/graphics/drawpool.cpp b/src/framework/graphics/drawpool.cpp index a374d2b1b..229b28ca1 100644 --- a/src/framework/graphics/drawpool.cpp +++ b/src/framework/graphics/drawpool.cpp @@ -24,6 +24,7 @@ #include "painter.h" #include "textureatlas.h" +#include "coordsbuffer.h" DrawPool* DrawPool::create(const DrawPoolType type) { diff --git a/src/framework/graphics/framebuffer.cpp b/src/framework/graphics/framebuffer.cpp index f175dbee7..d8265bacc 100644 --- a/src/framework/graphics/framebuffer.cpp +++ b/src/framework/graphics/framebuffer.cpp @@ -197,7 +197,7 @@ void FrameBuffer::doScreenshot(std::string file, const uint16_t x, const uint16_ internalRelease(); - g_asyncDispatcher.detach_task([size, pixels, file] { + g_asyncDispatcher->detach_task([size, pixels, file] { try { Image image(size, 4, pixels->data()); image.flipVertically(); diff --git a/src/framework/graphics/glutil.h b/src/framework/graphics/glutil.h index a39076983..d5665f652 100644 --- a/src/framework/graphics/glutil.h +++ b/src/framework/graphics/glutil.h @@ -27,6 +27,10 @@ #define GL_APICALL #define EGLAPI #include +#elif defined(__APPLE__) +#define GLEW_STATIC +#define GLEW_NO_GLU +#include #else #ifndef _MSC_VER #define GLEW_STATIC diff --git a/src/framework/luaengine/luavaluecasts.h b/src/framework/luaengine/luavaluecasts.h index 6f2481b10..73e3fdeda 100644 --- a/src/framework/luaengine/luavaluecasts.h +++ b/src/framework/luaengine/luavaluecasts.h @@ -29,6 +29,11 @@ #include "framework/platform/staticdata.h" +#include +#include +#include +#include + template int push_internal_luavalue(T v); @@ -44,6 +49,32 @@ bool luavalue_cast(int index, int& i); int push_luavalue(double d); bool luavalue_cast(int index, double& d); +namespace detail { + constexpr double maxExactIntegerAsDouble = 9007199254740992.0; // 2^53 + + template + inline bool luavalue_cast_integer_from_double(const int index, T& v) + { + static_assert(std::is_integral_v); + + double d; + if (!luavalue_cast(index, d)) + return false; + if (!std::isfinite(d) || std::trunc(d) != d) + return false; + + const double typeMin = static_cast(std::numeric_limits::lowest()); + const double typeMax = static_cast(std::numeric_limits::max()); + const double exactMin = std::numeric_limits::is_signed ? -maxExactIntegerAsDouble : 0.0; + const double exactMax = maxExactIntegerAsDouble; + if (d < std::max(typeMin, exactMin) || d > std::min(typeMax, exactMax)) + return false; + + v = static_cast(d); + return true; + } +} + // float inline int push_luavalue(const float f) { push_luavalue(static_cast(f)); return 1; } inline bool luavalue_cast(const int index, float& f) @@ -91,8 +122,14 @@ inline bool luavalue_cast(const int index, uint32_t& v) inline int push_luavalue(const int64_t v) { push_luavalue(static_cast(v)); return 1; } inline bool luavalue_cast(const int index, int64_t& v) { - double d; - const bool r = luavalue_cast(index, d); v = d; return r; + return detail::luavalue_cast_integer_from_double(index, v); +} + +// uint64 +inline int push_luavalue(const uint64_t v) { push_luavalue(static_cast(v)); return 1; } +inline bool luavalue_cast(const int index, uint64_t& v) +{ + return detail::luavalue_cast_integer_from_double(index, v); } using lua_u64 = std::conditional_t; @@ -100,6 +137,7 @@ using lua_unsigned_long = lua_u64; static_assert(sizeof(lua_u64) == 8, "lua_u64 must be 64-bit"); +template requires (!std::is_same_v) inline int push_luavalue(const unsigned long v) { if constexpr (sizeof(unsigned long) <= sizeof(uint32_t)) { @@ -110,6 +148,7 @@ inline int push_luavalue(const unsigned long v) return 1; } +template requires (!std::is_same_v) inline bool luavalue_cast(const int index, unsigned long& v) { if constexpr (sizeof(unsigned long) <= sizeof(uint32_t)) { @@ -119,26 +158,20 @@ inline bool luavalue_cast(const int index, unsigned long& v) return r; } - double temp; - const bool r = luavalue_cast(index, temp); - v = static_cast(temp); - return r; + return detail::luavalue_cast_integer_from_double(index, v); } -template, int> = 0> +template requires (!std::is_same_v) inline int push_luavalue(lua_u64 v) { push_luavalue(static_cast(v)); return 1; } -template, int> = 0> +template requires (!std::is_same_v && !std::is_same_v) inline bool luavalue_cast(const int idx, lua_u64& v) { - double d; - const bool r = luavalue_cast(idx, d); - v = static_cast(d); - return r; + return detail::luavalue_cast_integer_from_double(idx, v); } // string diff --git a/src/framework/net/httplogin.cpp b/src/framework/net/httplogin.cpp index 0727b8e54..36dac70f8 100644 --- a/src/framework/net/httplogin.cpp +++ b/src/framework/net/httplogin.cpp @@ -20,6 +20,15 @@ * THE SOFTWARE. */ +#ifndef CPPHTTPLIB_OPENSSL_SUPPORT +# define CPPHTTPLIB_OPENSSL_SUPPORT +#endif +#ifdef __APPLE__ +# undef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO +# define CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES +#endif +#include + #include "httplogin.h" #include #include @@ -102,17 +111,17 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path, const std::string& password, int request_id, bool httpLogin, const std::string& token) { #ifndef __EMSCRIPTEN__ this->errorMessage.clear(); - g_asyncDispatcher.detach_task( - [this, host, path, port, email, password, request_id, token, httpLogin] { + g_asyncDispatcher->detach_task( + [this, host, path, port, email, password, token, request_id, httpLogin] { if (cancelled.load()) return; - httplib::Result result = + HttpResponse result = this->loginHttpsJson(host, path, port, email, password, token); - if (httpLogin && (!result || result->status != Success)) { + if (httpLogin && (!result || result.status != Success)) { if (cancelled.load()) return; result = loginHttpJson(host, path, port, email, password, token); } - if (result && result->status == Success && parseJsonResponse(result->body)) { + if (result && result.status == Success && parseJsonResponse(result.body)) { g_dispatcher.addEvent([this, request_id] { if (cancelled.load()) return; g_lua.callGlobalField("EnterGame", "loginSuccess", request_id, @@ -123,12 +132,12 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path, int status = 0; std::string msg = ""; if (result) { - status = result->status; + status = result.status; if (!this->errorMessage.empty()) { msg = this->errorMessage; } try { - const auto body = json::parse(result->body); + const auto body = json::parse(result.body); if (msg.empty()) { msg = body.value("errorMessage", ""); } @@ -138,8 +147,8 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path, if (msg.empty()) { if (status != Success) { msg = "HTTP " + std::to_string(status); - if (!result->reason.empty()) { - msg += " - " + result->reason; + if (!result.reason.empty()) { + msg += " - " + result.reason; } else { msg += " - Unknown status"; } @@ -167,8 +176,9 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path, }); #else this->errorMessage.clear(); - g_asyncDispatcher.detach_task( - [this, host, path, port, email, password, request_id, token, httpLogin] { + g_asyncDispatcher->detach_task( + [this, host, path, port, email, password, token, request_id, httpLogin] { + if (cancelled.load()) return; emscripten_fetch_attr_t attr; emscripten_fetch_attr_init(&attr); strcpy(attr.requestMethod, "POST"); @@ -245,12 +255,12 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path, #endif } -httplib::Result LoginHttp::loginHttpsJson(const std::string& host, - const std::string& path, - const uint16_t port, - const std::string& email, - const std::string& password, - const std::string& token) { +LoginHttp::HttpResponse LoginHttp::loginHttpsJson(const std::string& host, + const std::string& path, + const uint16_t port, + const std::string& email, + const std::string& password, + const std::string& token) { httplib::SSLClient client(host, port); client.set_logger( @@ -291,15 +301,18 @@ httplib::Result LoginHttp::loginHttpsJson(const std::string& host, << std::endl; } - return response; + if (!response) + return {}; + + return { true, response->status, response->reason, response->body }; } -httplib::Result LoginHttp::loginHttpJson(const std::string& host, - const std::string& path, - const uint16_t port, - const std::string& email, - const std::string& password, - const std::string& token) { +LoginHttp::HttpResponse LoginHttp::loginHttpJson(const std::string& host, + const std::string& path, + const uint16_t port, + const std::string& email, + const std::string& password, + const std::string& token) { httplib::Client client(host, port); client.set_logger( [this](const auto& req, const auto& res) { LoginHttp::Logger(req, res); }); @@ -332,11 +345,10 @@ httplib::Result LoginHttp::loginHttpJson(const std::string& host, std::cout << "HTTP status: " << to_string(response.error()) << std::endl; } - if (response && response->status == Success && !parseJsonResponse(response->body)) { - return response; - } + if (!response) + return {}; - return response; + return { true, response->status, response->reason, response->body }; } bool LoginHttp::parseJsonResponse(const std::string& body) { @@ -373,4 +385,4 @@ bool LoginHttp::parseJsonResponse(const std::string& body) { this->worlds = to_string(playdata["worlds"]); return true; -} \ No newline at end of file +} diff --git a/src/framework/net/httplogin.h b/src/framework/net/httplogin.h index becf82599..04bd6831e 100644 --- a/src/framework/net/httplogin.h +++ b/src/framework/net/httplogin.h @@ -22,11 +22,11 @@ #pragma once -#ifndef CPPHTTPLIB_OPENSSL_SUPPORT -# define CPPHTTPLIB_OPENSSL_SUPPORT -#endif +#include +#include +#include + #include -#include class LoginHttp final : public LuaObject { @@ -51,23 +51,32 @@ public: uint16_t port, const std::string& email, const std::string& password, int request_id, bool httpLogin, const std::string& token); - httplib::Result loginHttpsJson(const std::string& host, - const std::string& path, uint16_t port, - const std::string& email, - const std::string& password, - const std::string& token); - - httplib::Result loginHttpJson(const std::string& host, - const std::string& path, uint16_t port, - const std::string& email, - const std::string& password, - const std::string& token); - void cancel(); enum Result : int { Success = 200, Error = -1 }; private: + struct HttpResponse { + bool connected{ false }; + int status{ Error }; + std::string reason; + std::string body; + + explicit operator bool() const { return connected; } + }; + + HttpResponse loginHttpsJson(const std::string& host, + const std::string& path, uint16_t port, + const std::string& email, + const std::string& password, + const std::string& token); + + HttpResponse loginHttpJson(const std::string& host, + const std::string& path, uint16_t port, + const std::string& email, + const std::string& password, + const std::string& token); + std::string characters; std::string worlds; std::string session; diff --git a/src/framework/platform/cocoaview.h b/src/framework/platform/cocoaview.h new file mode 100644 index 000000000..86f452b02 --- /dev/null +++ b/src/framework/platform/cocoaview.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2025 OTClient + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#ifdef __OBJC__ +#define Point MacPoint +#define Size MacSize +#define Rect MacRect + +#ifndef GL_SILENCE_DEPRECATION +#define GL_SILENCE_DEPRECATION +#endif +#import +#import + +#undef Point +#undef Size +#undef Rect + +#ifdef __cplusplus +class CocoaWindow; +#else +typedef void CocoaWindow; +#endif + +@interface OTOpenGLView : NSOpenGLView +@property (nonatomic, assign) CocoaWindow* platformWindow; +@property (nonatomic) BOOL acceptsInput; +@property (nonatomic) CGFloat scrollAccumX; +@property (nonatomic) CGFloat scrollAccumY; +@end + +@interface OTWindowDelegate : NSObject +@property (nonatomic, assign) CocoaWindow* platformWindow; +@end + +#endif diff --git a/src/framework/platform/cocoaview.mm b/src/framework/platform/cocoaview.mm new file mode 100644 index 000000000..cfc2945d7 --- /dev/null +++ b/src/framework/platform/cocoaview.mm @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2010-2025 OTClient + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifdef __APPLE__ + +#include "cocoawindow.h" +#include "cocoaview.h" +#include + +#define Point MacPoint +#define Size MacSize +#define Rect MacRect + +#import + +#undef Point +#undef Size +#undef Rect + +@interface OTOpenGLView () +- (void)dispatchMouseMove:(NSEvent*)event; +- (void)dispatchMouseButton:(Fw::MouseButton)button pressed:(bool)pressed event:(NSEvent*)event; +@end + +@implementation OTOpenGLView + +- (instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(NSOpenGLPixelFormat*)format { + self = [super initWithFrame:frameRect pixelFormat:format]; + if (self) { + _acceptsInput = YES; + NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | + NSTrackingMouseMoved | + NSTrackingActiveAlways | + NSTrackingInVisibleRect; + NSTrackingArea* trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect + options:options + owner:self + userInfo:nil]; + [self addTrackingArea:trackingArea]; + } + return self; +} + +- (BOOL)acceptsFirstResponder { + return _acceptsInput; +} + +- (BOOL)canBecomeKeyView { + return _acceptsInput; +} + +- (void)keyDown:(NSEvent*)event { + if (!_platformWindow) return; + + // Skip OS-level key repeats - game handles repeats via fireKeysPress() + if ([event isARepeat]) { + if ([event characters] && [[event characters] length] > 0) { + std::string chars = [[event characters] UTF8String]; + Fw::Key key = _platformWindow->translateKeyCode([event keyCode]); + bool isSpecial = _platformWindow->isSpecialKey(key); + unsigned int modifiers = [event modifierFlags]; + if (!isSpecial && !(modifiers & (NSEventModifierFlagCommand | NSEventModifierFlagControl))) { + _platformWindow->handleTextInput(chars); + } + } + return; + } + + unsigned short keyCode = [event keyCode]; + unsigned int modifiers = [event modifierFlags]; + + if ((modifiers & NSEventModifierFlagCommand) && keyCode == kVK_ANSI_Q) { + _platformWindow->handleClose(); + return; + } + + std::string chars; + if ([event characters] && [[event characters] length] > 0) { + chars = [[event characters] UTF8String]; + } + + _platformWindow->handleKeyDown(keyCode, modifiers, chars); + + Fw::Key key = _platformWindow->translateKeyCode(keyCode); + bool isSpecial = _platformWindow->isSpecialKey(key); + + if (chars.length() > 0 && !isSpecial && !(modifiers & (NSEventModifierFlagCommand | NSEventModifierFlagControl))) { + _platformWindow->handleTextInput(chars); + } +} + +- (void)keyUp:(NSEvent*)event { + if (!_platformWindow) return; + _platformWindow->handleKeyUp([event keyCode], [event modifierFlags]); +} + +- (void)flagsChanged:(NSEvent*)event { + if (!_platformWindow) return; + _platformWindow->handleFlagsChanged([event modifierFlags]); +} + +- (Point)convertMousePosition:(NSEvent*)event { + NSPoint loc = [self convertPoint:[event locationInWindow] fromView:nil]; + NSRect bounds = [self bounds]; + return Point(static_cast(loc.x), static_cast(bounds.size.height - loc.y)); +} + +- (void)dispatchMouseMove:(NSEvent*)event { + if (!_platformWindow) return; + _platformWindow->handleMouseMove([self convertMousePosition:event]); +} + +- (void)dispatchMouseButton:(Fw::MouseButton)button pressed:(bool)pressed event:(NSEvent*)event { + if (!_platformWindow) return; + _platformWindow->handleMouseButton(button, pressed, [self convertMousePosition:event]); +} + +- (void)mouseDown:(NSEvent*)event { + [self dispatchMouseButton:Fw::MouseLeftButton pressed:true event:event]; +} + +- (void)mouseUp:(NSEvent*)event { + [self dispatchMouseButton:Fw::MouseLeftButton pressed:false event:event]; +} + +- (void)rightMouseDown:(NSEvent*)event { + [self dispatchMouseButton:Fw::MouseRightButton pressed:true event:event]; +} + +- (void)rightMouseUp:(NSEvent*)event { + [self dispatchMouseButton:Fw::MouseRightButton pressed:false event:event]; +} + +- (void)otherMouseDown:(NSEvent*)event { + if ([event buttonNumber] == 2) { + [self dispatchMouseButton:Fw::MouseMidButton pressed:true event:event]; + } +} + +- (void)otherMouseUp:(NSEvent*)event { + if ([event buttonNumber] == 2) { + [self dispatchMouseButton:Fw::MouseMidButton pressed:false event:event]; + } +} + +- (void)mouseMoved:(NSEvent*)event { + [self dispatchMouseMove:event]; +} + +- (void)mouseDragged:(NSEvent*)event { + [self dispatchMouseMove:event]; +} + +- (void)rightMouseDragged:(NSEvent*)event { + [self dispatchMouseMove:event]; +} + +- (void)otherMouseDragged:(NSEvent*)event { + [self dispatchMouseMove:event]; +} + +- (void)scrollWheel:(NSEvent*)event { + if (!_platformWindow) return; + CGFloat deltaX = [event scrollingDeltaX]; + CGFloat deltaY = [event scrollingDeltaY]; + + if ([event hasPreciseScrollingDeltas]) { + _scrollAccumX += deltaX; + _scrollAccumY += deltaY; + int intDX = static_cast(_scrollAccumX); + int intDY = static_cast(_scrollAccumY); + if (intDX != 0 || intDY != 0) { + _platformWindow->handleMouseScroll(intDX, intDY); + _scrollAccumX -= intDX; + _scrollAccumY -= intDY; + } + } else { + _platformWindow->handleMouseScroll(static_cast(deltaX), static_cast(deltaY)); + } +} + +- (void)mouseEntered:(NSEvent*)event { + (void)event; +} + +- (void)mouseExited:(NSEvent*)event { + (void)event; +} + +@end + +@implementation OTWindowDelegate + +- (void)windowDidResize:(NSNotification*)notification { + if (!_platformWindow) return; + NSWindow* window = [notification object]; + NSRect contentRect = [[window contentView] frame]; + _platformWindow->handleResize(static_cast(contentRect.size.width), + static_cast(contentRect.size.height)); +} + +- (void)windowDidMove:(NSNotification*)notification { + if (!_platformWindow) return; + NSWindow* window = [notification object]; + NSRect frame = [window frame]; + NSScreen* screen = [window screen]; + if (screen) { + NSRect screenFrame = [screen frame]; + int y = static_cast(screenFrame.origin.y + screenFrame.size.height - frame.origin.y - frame.size.height); + _platformWindow->handleMove(static_cast(frame.origin.x), y); + } +} + +- (void)windowDidBecomeKey:(NSNotification*)notification { + (void)notification; + if (!_platformWindow) return; + _platformWindow->handleFocusChange(true); +} + +- (void)windowDidResignKey:(NSNotification*)notification { + (void)notification; + if (!_platformWindow) return; + _platformWindow->handleFocusChange(false); +} + +- (BOOL)windowShouldClose:(NSWindow*)sender { + (void)sender; + if (_platformWindow) { + _platformWindow->handleClose(); + } + return NO; +} + +- (void)windowWillEnterFullScreen:(NSNotification*)notification { + (void)notification; +} + +- (void)windowDidEnterFullScreen:(NSNotification*)notification { + (void)notification; +} + +- (void)windowWillExitFullScreen:(NSNotification*)notification { + (void)notification; +} + +- (void)windowDidExitFullScreen:(NSNotification*)notification { + (void)notification; +} + +@end + +#endif diff --git a/src/framework/platform/cocoawindow.h b/src/framework/platform/cocoawindow.h new file mode 100644 index 000000000..d84d768f1 --- /dev/null +++ b/src/framework/platform/cocoawindow.h @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2010-2025 OTClient + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "platformwindow.h" + +#include + +#ifdef __OBJC__ +@class NSWindow; +@class NSOpenGLView; +@class NSOpenGLContext; +@class NSCursor; +@class OTOpenGLView; +@class OTWindowDelegate; +#else +typedef void NSWindow; +typedef void NSOpenGLView; +typedef void NSOpenGLContext; +typedef void NSCursor; +typedef void OTOpenGLView; +typedef void OTWindowDelegate; +#endif + +class CocoaWindow : public PlatformWindow +{ +public: + /** + * Creates a new Cocoa window instance. + * Initializes member variables and key mappings for macOS input handling. + */ + CocoaWindow(); + + /** + * Destroys the Cocoa window and releases all associated resources. + */ + ~CocoaWindow(); + + /** + * Initializes the window and OpenGL context. + * Creates the native NSWindow, OpenGL view, and event handlers. + */ + void init() override; + + /** + * Terminates the window and releases all resources. + * Destroys the NSWindow, OpenGL context, and event handlers. + */ + void terminate() override; + + /** + * Moves the window to the specified position. + * @param pos The new top-left position of the window. + */ + void move(const Point& pos) override; + + /** + * Resizes the window to the specified size. + * @param size The new size of the window. + */ + void resize(const Size& size) override; + + /** Makes the window visible on screen. */ + void show() override; + + /** Hides the window from screen. */ + void hide() override; + + /** Maximizes the window to fill the screen. */ + void maximize() override; + + /** + * Processes pending window events. + * Pumps the event loop and dispatches pending events. + */ + void poll() override; + + /** Swaps the OpenGL front and back buffers. */ + void swapBuffers() override; + + /** Shows the mouse cursor. */ + void showMouse() override; + + /** Hides the mouse cursor. */ + void hideMouse() override; + + /** + * Sets the mouse cursor to a predefined style. + * @param cursorId The cursor style identifier. + */ + void setMouseCursor(int cursorId) override; + + /** Restores the default mouse cursor. */ + void restoreMouseCursor() override; + + /** + * Sets the window title. + * @param title The title string to display. + */ + void setTitle(std::string_view title) override; + + /** + * Sets the minimum window size. + * @param minimumSize The minimum allowed window dimensions. + */ + void setMinimumSize(const Size& minimumSize) override; + + /** + * Toggles fullscreen mode. + * @param fullscreen True to enter fullscreen, false to exit. + */ + void setFullscreen(bool fullscreen) override; + + /** + * Enables or disables vertical sync. + * @param enable True to enable v-sync, false to disable. + */ + void setVerticalSync(bool enable) override; + + /** + * Sets the window icon from an image file. + * @param file Path to the icon image file. + */ + void setIcon(const std::string& file) override; + + /** + * Copies text to the system clipboard. + * @param text The text to copy to the clipboard. + */ + void setClipboardText(std::string_view text) override; + + /** + * Gets the primary display size. + * @return The dimensions of the main display in pixels. + */ + Size getDisplaySize() override; + + /** + * Retrieves text from the system clipboard. + * @return The clipboard text content, or empty string if unavailable. + */ + std::string getClipboardText() override; + + /** + * Returns a string identifying the platform type. + * @return Platform identifier string. + */ + std::string getPlatformType() override; + + /** + * Handles key down events from the native event loop. + * @param keyCode The macOS virtual key code. + * @param modifiers The current modifier flags. + * @param characters The character string for this key event. + */ + void handleKeyDown(unsigned short keyCode, unsigned int modifiers, const std::string& characters); + + /** + * Handles key up events from the native event loop. + * @param keyCode The macOS virtual key code. + * @param modifiers The current modifier flags. + */ + void handleKeyUp(unsigned short keyCode, unsigned int modifiers); + + /** Handles changes to modifier key states. */ + void handleFlagsChanged(unsigned int modifiers); + + /** + * Handles mouse button events. + * @param button The mouse button identifier. + * @param pressed True if button was pressed, false if released. + * @param position The mouse position in window coordinates. + */ + void handleMouseButton(Fw::MouseButton button, bool pressed, const Point& position); + + /** + * Handles mouse movement events. + * @param position The new mouse position in window coordinates. + */ + void handleMouseMove(const Point& position); + + /** + * Handles mouse scroll wheel events. + * @param deltaX Horizontal scroll delta. + * @param deltaY Vertical scroll delta. + */ + void handleMouseScroll(int deltaX, int deltaY); + + /** + * Handles window resize events. + * @param width The new window width. + * @param height The new window height. + */ + void handleResize(int width, int height); + + /** + * Handles window move events. + * @param x The new x position. + * @param y The new y position. + */ + void handleMove(int x, int y); + + /** Handles window close requests. */ + void handleClose(); + + /** + * Handles focus change events. + * @param focused True if window gained focus, false if lost. + */ + void handleFocusChange(bool focused); + + /** + * Handles text input events for character composition. + * @param text The input text string. + */ + void handleTextInput(const std::string& text); + + /** + * Translates a macOS virtual key code to the framework key enum. + * @param keyCode The macOS virtual key code. + * @return The corresponding framework key identifier. + */ + Fw::Key translateKeyCode(unsigned short keyCode); + + /** + * Checks if a key is a special function key. + * @param key The key to check. + * @return True if the key is a special key (function keys, etc.). + */ + bool isSpecialKey(Fw::Key key); + +protected: + /** + * Loads a custom mouse cursor from an image. + * @param image The image to create the cursor from. + * @param hotSpot The hotspot point for the cursor. + * @return The cursor ID on success, -1 on failure. + */ + int internalLoadMouseCursor(const ImagePtr& image, const Point& hotSpot) override; + +private: + /** + * Creates the native NSWindow and initializes all UI components. + */ + void internalCreateWindow(); + + /** + * Creates the OpenGL context and configures it for rendering. + */ + void internalCreateGLContext(); + + /** + * Initializes the key code mapping table for macOS key codes. + */ + void internalInitKeyMap(); + + /** + * Updates the current modifier key state. + * @param modifiers The new modifier flags. + */ + void updateModifiers(unsigned int modifiers); + + NSWindow* m_window; + OTOpenGLView* m_glView; + OTWindowDelegate* m_delegate; + NSOpenGLContext* m_glContext; + + std::vector m_cursors; + NSCursor* m_currentCursor; + NSCursor* m_defaultCursor; + bool m_cursorHidden; + bool m_cursorInWindow; + + unsigned int m_lastModifiers; + std::array m_commandKeyDown{}; +}; diff --git a/src/framework/platform/cocoawindow.mm b/src/framework/platform/cocoawindow.mm new file mode 100644 index 000000000..2d3cf30dd --- /dev/null +++ b/src/framework/platform/cocoawindow.mm @@ -0,0 +1,824 @@ +/* + * Copyright (c) 2010-2025 OTClient + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifdef __APPLE__ + +#define GLEW_STATIC +#define GLEW_NO_GLU +#include + +#define Point MacPoint +#define Size MacSize +#define Rect MacRect + +#define GL_SILENCE_DEPRECATION +#import +#import +#import +#import + +#undef Point +#undef Size +#undef Rect + +#include "cocoawindow.h" +#include "cocoaview.h" +#include +#include + +CocoaWindow::CocoaWindow() +{ + m_window = nil; + m_glView = nil; + m_delegate = nil; + m_glContext = nil; + m_currentCursor = nil; + m_defaultCursor = nil; + m_cursorHidden = false; + m_cursorInWindow = false; + m_lastModifiers = 0; + m_minimumSize = Size(600, 480); + m_size = Size(600, 480); + + internalInitKeyMap(); +} + +CocoaWindow::~CocoaWindow() +{ + terminate(); +} + +void CocoaWindow::internalInitKeyMap() +{ + m_keyMap[kVK_Escape] = Fw::KeyEscape; + m_keyMap[kVK_Tab] = Fw::KeyTab; + m_keyMap[kVK_Return] = Fw::KeyEnter; + m_keyMap[kVK_Delete] = Fw::KeyBackspace; + m_keyMap[kVK_Space] = Fw::KeySpace; + + m_keyMap[kVK_PageUp] = Fw::KeyPageUp; + m_keyMap[kVK_PageDown] = Fw::KeyPageDown; + m_keyMap[kVK_Home] = Fw::KeyHome; + m_keyMap[kVK_End] = Fw::KeyEnd; + m_keyMap[kVK_ForwardDelete] = Fw::KeyDelete; + + m_keyMap[kVK_UpArrow] = Fw::KeyUp; + m_keyMap[kVK_DownArrow] = Fw::KeyDown; + m_keyMap[kVK_LeftArrow] = Fw::KeyLeft; + m_keyMap[kVK_RightArrow] = Fw::KeyRight; + + m_keyMap[kVK_CapsLock] = Fw::KeyCapsLock; + + m_keyMap[kVK_Control] = Fw::KeyCtrl; + m_keyMap[kVK_RightControl] = Fw::KeyCtrl; + m_keyMap[kVK_Shift] = Fw::KeyShift; + m_keyMap[kVK_RightShift] = Fw::KeyShift; + m_keyMap[kVK_Option] = Fw::KeyAlt; + m_keyMap[kVK_RightOption] = Fw::KeyAlt; + m_keyMap[kVK_Command] = Fw::KeyMeta; + m_keyMap[kVK_RightCommand] = Fw::KeyMeta; + + m_keyMap[kVK_ANSI_A] = Fw::KeyA; + m_keyMap[kVK_ANSI_B] = Fw::KeyB; + m_keyMap[kVK_ANSI_C] = Fw::KeyC; + m_keyMap[kVK_ANSI_D] = Fw::KeyD; + m_keyMap[kVK_ANSI_E] = Fw::KeyE; + m_keyMap[kVK_ANSI_F] = Fw::KeyF; + m_keyMap[kVK_ANSI_G] = Fw::KeyG; + m_keyMap[kVK_ANSI_H] = Fw::KeyH; + m_keyMap[kVK_ANSI_I] = Fw::KeyI; + m_keyMap[kVK_ANSI_J] = Fw::KeyJ; + m_keyMap[kVK_ANSI_K] = Fw::KeyK; + m_keyMap[kVK_ANSI_L] = Fw::KeyL; + m_keyMap[kVK_ANSI_M] = Fw::KeyM; + m_keyMap[kVK_ANSI_N] = Fw::KeyN; + m_keyMap[kVK_ANSI_O] = Fw::KeyO; + m_keyMap[kVK_ANSI_P] = Fw::KeyP; + m_keyMap[kVK_ANSI_Q] = Fw::KeyQ; + m_keyMap[kVK_ANSI_R] = Fw::KeyR; + m_keyMap[kVK_ANSI_S] = Fw::KeyS; + m_keyMap[kVK_ANSI_T] = Fw::KeyT; + m_keyMap[kVK_ANSI_U] = Fw::KeyU; + m_keyMap[kVK_ANSI_V] = Fw::KeyV; + m_keyMap[kVK_ANSI_W] = Fw::KeyW; + m_keyMap[kVK_ANSI_X] = Fw::KeyX; + m_keyMap[kVK_ANSI_Y] = Fw::KeyY; + m_keyMap[kVK_ANSI_Z] = Fw::KeyZ; + + m_keyMap[kVK_ANSI_0] = Fw::Key0; + m_keyMap[kVK_ANSI_1] = Fw::Key1; + m_keyMap[kVK_ANSI_2] = Fw::Key2; + m_keyMap[kVK_ANSI_3] = Fw::Key3; + m_keyMap[kVK_ANSI_4] = Fw::Key4; + m_keyMap[kVK_ANSI_5] = Fw::Key5; + m_keyMap[kVK_ANSI_6] = Fw::Key6; + m_keyMap[kVK_ANSI_7] = Fw::Key7; + m_keyMap[kVK_ANSI_8] = Fw::Key8; + m_keyMap[kVK_ANSI_9] = Fw::Key9; + + m_keyMap[kVK_ANSI_Minus] = Fw::KeyMinus; + m_keyMap[kVK_ANSI_Equal] = Fw::KeyEqual; + m_keyMap[kVK_ANSI_LeftBracket] = Fw::KeyLeftBracket; + m_keyMap[kVK_ANSI_RightBracket] = Fw::KeyRightBracket; + m_keyMap[kVK_ANSI_Backslash] = Fw::KeyBackslash; + m_keyMap[kVK_ANSI_Semicolon] = Fw::KeySemicolon; + m_keyMap[kVK_ANSI_Quote] = Fw::KeyApostrophe; + m_keyMap[kVK_ANSI_Comma] = Fw::KeyComma; + m_keyMap[kVK_ANSI_Period] = Fw::KeyPeriod; + m_keyMap[kVK_ANSI_Slash] = Fw::KeySlash; + m_keyMap[kVK_ANSI_Grave] = Fw::KeyGrave; + + m_keyMap[kVK_F1] = Fw::KeyF1; + m_keyMap[kVK_F2] = Fw::KeyF2; + m_keyMap[kVK_F3] = Fw::KeyF3; + m_keyMap[kVK_F4] = Fw::KeyF4; + m_keyMap[kVK_F5] = Fw::KeyF5; + m_keyMap[kVK_F6] = Fw::KeyF6; + m_keyMap[kVK_F7] = Fw::KeyF7; + m_keyMap[kVK_F8] = Fw::KeyF8; + m_keyMap[kVK_F9] = Fw::KeyF9; + m_keyMap[kVK_F10] = Fw::KeyF10; + m_keyMap[kVK_F11] = Fw::KeyF11; + m_keyMap[kVK_F12] = Fw::KeyF12; + + m_keyMap[kVK_ANSI_Keypad0] = Fw::KeyNumpad0; + m_keyMap[kVK_ANSI_Keypad1] = Fw::KeyNumpad1; + m_keyMap[kVK_ANSI_Keypad2] = Fw::KeyNumpad2; + m_keyMap[kVK_ANSI_Keypad3] = Fw::KeyNumpad3; + m_keyMap[kVK_ANSI_Keypad4] = Fw::KeyNumpad4; + m_keyMap[kVK_ANSI_Keypad5] = Fw::KeyNumpad5; + m_keyMap[kVK_ANSI_Keypad6] = Fw::KeyNumpad6; + m_keyMap[kVK_ANSI_Keypad7] = Fw::KeyNumpad7; + m_keyMap[kVK_ANSI_Keypad8] = Fw::KeyNumpad8; + m_keyMap[kVK_ANSI_Keypad9] = Fw::KeyNumpad9; + m_keyMap[kVK_ANSI_KeypadEnter] = Fw::KeyEnter; +} + +Fw::Key CocoaWindow::translateKeyCode(unsigned short keyCode) +{ + auto it = m_keyMap.find(keyCode); + if (it != m_keyMap.end()) + return it->second; + return Fw::KeyUnknown; +} + +bool CocoaWindow::isSpecialKey(Fw::Key key) +{ + return key == Fw::KeyTab || key == Fw::KeyEnter || key == Fw::KeyEscape || + key == Fw::KeyBackspace || key == Fw::KeyDelete || + key == Fw::KeyUp || key == Fw::KeyDown || key == Fw::KeyLeft || key == Fw::KeyRight || + key == Fw::KeyHome || key == Fw::KeyEnd || key == Fw::KeyPageUp || key == Fw::KeyPageDown || + (key >= Fw::KeyF1 && key <= Fw::KeyF12); +} + +void CocoaWindow::init() +{ + @autoreleasepool { + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp finishLaunching]; + + internalCreateWindow(); + internalCreateGLContext(); + + glewExperimental = GL_TRUE; + + m_defaultCursor = [NSCursor arrowCursor]; + m_currentCursor = m_defaultCursor; + + m_created = true; + } +} + +void CocoaWindow::internalCreateWindow() +{ + NSRect frame = NSMakeRect(0, 0, m_size.width(), m_size.height()); + NSUInteger styleMask = NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskResizable; + + m_window = [[NSWindow alloc] initWithContentRect:frame + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:NO]; + + [m_window setTitle:@"OTClient"]; + [m_window center]; + [m_window setMinSize:NSMakeSize(m_minimumSize.width(), m_minimumSize.height())]; + + m_delegate = [[OTWindowDelegate alloc] init]; + [m_delegate setPlatformWindow:this]; + [m_window setDelegate:m_delegate]; + + NSRect contentRect = [[m_window contentView] frame]; + m_size = Size(static_cast(contentRect.size.width), + static_cast(contentRect.size.height)); + + NSRect windowFrame = [m_window frame]; + NSScreen* screen = [m_window screen]; + if (screen) { + NSRect screenFrame = [screen frame]; + m_position = Point(static_cast(windowFrame.origin.x), + static_cast(screenFrame.size.height - windowFrame.origin.y - windowFrame.size.height)); + } +} + +void CocoaWindow::internalCreateGLContext() +{ + NSOpenGLPixelFormatAttribute attrs[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFADepthSize, 24, + NSOpenGLPFAStencilSize, 8, + NSOpenGLPFAColorSize, 32, + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersionLegacy, + NSOpenGLPFAAccelerated, + NSOpenGLPFANoRecovery, + 0 + }; + + NSOpenGLPixelFormat* pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; + if (!pixelFormat) { + g_logger.fatal("Failed to create OpenGL pixel format"); + return; + } + + int width = m_size.width(); + int height = m_size.height(); + NSRect viewFrame = NSMakeRect(0, 0, width, height); + + g_logger.info("Creating GL view with frame: {}x{}", width, height); + + m_glView = [[OTOpenGLView alloc] initWithFrame:viewFrame pixelFormat:pixelFormat]; + [m_glView setPlatformWindow:this]; + [m_glView setWantsBestResolutionOpenGLSurface:NO]; + [m_glView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + m_glContext = [m_glView openGLContext]; + [m_glContext makeCurrentContext]; + + [m_window setContentSize:NSMakeSize(width, height)]; + [m_window setContentView:m_glView]; + [m_window makeFirstResponder:m_glView]; + + GLint swapInterval = 1; + [m_glContext setValues:&swapInterval forParameter:NSOpenGLContextParameterSwapInterval]; + m_vsync = true; + + NSRect newFrame = [[m_window contentView] frame]; + g_logger.info("GL context created, content view frame: {}x{}", (int)newFrame.size.width, (int)newFrame.size.height); +} + +void CocoaWindow::terminate() +{ + @autoreleasepool { + if (m_glContext) { + [NSOpenGLContext clearCurrentContext]; + m_glContext = nil; + } + + if (m_glView) { + [m_glView setPlatformWindow:nullptr]; + m_glView = nil; + } + + if (m_delegate) { + [m_delegate setPlatformWindow:nullptr]; + m_delegate = nil; + } + + if (m_window) { + [m_window close]; + m_window = nil; + } + + m_cursors.clear(); + m_created = false; + } +} + +void CocoaWindow::move(const Point& pos) +{ + @autoreleasepool { + if (!m_window) return; + + NSScreen* screen = [m_window screen]; + if (!screen) screen = [NSScreen mainScreen]; + NSRect screenFrame = [screen frame]; + + NSRect frame = [m_window frame]; + frame.origin.x = pos.x; + frame.origin.y = screenFrame.size.height - pos.y - frame.size.height; + + [m_window setFrame:frame display:YES]; + } +} + +void CocoaWindow::resize(const Size& size) +{ + @autoreleasepool { + if (!m_window) return; + + NSRect frame = [m_window frame]; + NSRect contentRect = [m_window contentRectForFrameRect:frame]; + CGFloat titleBarHeight = frame.size.height - contentRect.size.height; + + frame.size.width = size.width(); + frame.size.height = size.height() + titleBarHeight; + + [m_window setFrame:frame display:YES]; + } +} + +void CocoaWindow::show() +{ + @autoreleasepool { + if (!m_window) { + g_logger.info("CocoaWindow::show() - m_window is nil!"); + return; + } + + g_logger.info("CocoaWindow::show() - making window visible"); + + [m_window setIsVisible:YES]; + [m_window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + + [m_glView setNeedsDisplay:YES]; + [NSApp updateWindows]; + + NSEvent* event; + while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode + dequeue:YES])) { + [NSApp sendEvent:event]; + } + + m_visible = true; + g_logger.info("CocoaWindow::show() - done, visible={}", m_visible); + } +} + +void CocoaWindow::hide() +{ + @autoreleasepool { + if (!m_window) return; + [m_window orderOut:nil]; + m_visible = false; + } +} + +void CocoaWindow::maximize() +{ + @autoreleasepool { + if (!m_window) return; + + if (!m_maximized) { + updateUnmaximizedCoords(); + [m_window zoom:nil]; + m_maximized = true; + } + } +} + +void CocoaWindow::poll() +{ + @autoreleasepool { + fireKeysPress(); + + NSEvent* event; + while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:nil + inMode:NSDefaultRunLoopMode + dequeue:YES])) { + [NSApp sendEvent:event]; + } + } +} + +void CocoaWindow::swapBuffers() +{ + @autoreleasepool { + if (m_glContext) { + [m_glContext flushBuffer]; + } + } +} + +void CocoaWindow::showMouse() +{ + @autoreleasepool { + if (m_cursorHidden) { + [NSCursor unhide]; + m_cursorHidden = false; + } + } +} + +void CocoaWindow::hideMouse() +{ + @autoreleasepool { + if (!m_cursorHidden) { + [NSCursor hide]; + m_cursorHidden = true; + } + } +} + +void CocoaWindow::setMouseCursor(int cursorId) +{ + @autoreleasepool { + if (cursorId >= 0 && cursorId < static_cast(m_cursors.size())) { + m_currentCursor = m_cursors[cursorId]; + [m_currentCursor set]; + } + } +} + +void CocoaWindow::restoreMouseCursor() +{ + @autoreleasepool { + m_currentCursor = m_defaultCursor; + [m_currentCursor set]; + } +} + +void CocoaWindow::setTitle(std::string_view title) +{ + @autoreleasepool { + if (!m_window) return; + NSString* nsTitle = [[NSString alloc] initWithBytes:title.data() + length:title.size() + encoding:NSUTF8StringEncoding]; + [m_window setTitle:nsTitle]; + } +} + +void CocoaWindow::setMinimumSize(const Size& minimumSize) +{ + @autoreleasepool { + m_minimumSize = minimumSize; + if (m_window) { + [m_window setMinSize:NSMakeSize(minimumSize.width(), minimumSize.height())]; + } + } +} + +void CocoaWindow::setFullscreen(bool fullscreen) +{ + @autoreleasepool { + if (!m_window) return; + + if (fullscreen != m_fullscreen) { + if (fullscreen) { + updateUnmaximizedCoords(); + } + [m_window toggleFullScreen:nil]; + m_fullscreen = fullscreen; + } + } +} + +void CocoaWindow::setVerticalSync(bool enable) +{ + @autoreleasepool { + if (m_glContext) { + GLint swapInterval = enable ? 1 : 0; + [m_glContext setValues:&swapInterval forParameter:NSOpenGLContextParameterSwapInterval]; + m_vsync = enable; + } + } +} + +void CocoaWindow::setIcon(const std::string& file) +{ + @autoreleasepool { + auto image = Image::load(file); + if (!image) { + g_logger.traceError("unable to load icon {}", file); + return; + } + + int width = image->getWidth(); + int height = image->getHeight(); + const uint8_t* pixels = image->getPixelData(); + + NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:nullptr + pixelsWide:width + pixelsHigh:height + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:width * 4 + bitsPerPixel:32]; + + memcpy([rep bitmapData], pixels, width * height * 4); + + CGFloat scale = [[NSScreen mainScreen] backingScaleFactor]; + NSSize sizeInPoints = NSMakeSize(width / scale, height / scale); + [rep setSize:sizeInPoints]; + + NSImage* nsImage = [[NSImage alloc] initWithSize:sizeInPoints]; + [nsImage addRepresentation:rep]; + [NSApp setApplicationIconImage:nsImage]; + } +} + +void CocoaWindow::setClipboardText(std::string_view text) +{ + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + NSString* nsText = [[NSString alloc] initWithBytes:text.data() + length:text.size() + encoding:NSUTF8StringEncoding]; + [pasteboard setString:nsText forType:NSPasteboardTypeString]; + } +} + +Size CocoaWindow::getDisplaySize() +{ + @autoreleasepool { + NSScreen* screen = [NSScreen mainScreen]; + NSRect frame = [screen frame]; + return Size(static_cast(frame.size.width), static_cast(frame.size.height)); + } +} + +std::string CocoaWindow::getClipboardText() +{ + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + NSString* text = [pasteboard stringForType:NSPasteboardTypeString]; + if (text) { + return std::string([text UTF8String]); + } + return {}; + } +} + +std::string CocoaWindow::getPlatformType() +{ + return "COCOA-MACOS"; +} + +int CocoaWindow::internalLoadMouseCursor(const ImagePtr& image, const Point& hotSpot) +{ + @autoreleasepool { + int width = image->getWidth(); + int height = image->getHeight(); + const uint8_t* pixels = image->getPixelData(); + + NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:nullptr + pixelsWide:width + pixelsHigh:height + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:width * 4 + bitsPerPixel:32]; + + memcpy([rep bitmapData], pixels, width * height * 4); + + NSImage* nsImage = [[NSImage alloc] initWithSize:NSMakeSize(width, height)]; + [nsImage addRepresentation:rep]; + + NSCursor* cursor = [[NSCursor alloc] initWithImage:nsImage + hotSpot:NSMakePoint(hotSpot.x, hotSpot.y)]; + + m_cursors.push_back(cursor); + return static_cast(m_cursors.size()) - 1; + } +} + +void CocoaWindow::handleKeyDown(unsigned short keyCode, unsigned int modifiers, const std::string& characters) +{ + (void)characters; + updateModifiers(modifiers); + Fw::Key key = translateKeyCode(keyCode); + if (key >= Fw::KeyLast) + return; + + const bool cmdPressed = (modifiers & NSEventModifierFlagCommand) != 0; + const auto keyIndex = static_cast(key); + if (cmdPressed && key != Fw::KeyMeta && key != Fw::KeyCtrl && key != Fw::KeyAlt && key != Fw::KeyShift) { + m_commandKeyDown[keyIndex] = true; + } + processKeyDown(key); +} + +void CocoaWindow::handleKeyUp(unsigned short keyCode, unsigned int modifiers) +{ + updateModifiers(modifiers); + Fw::Key key = translateKeyCode(keyCode); + if (key < Fw::KeyLast) { + const auto keyIndex = static_cast(key); + m_commandKeyDown[keyIndex] = false; + } + processKeyUp(key); +} + +void CocoaWindow::handleFlagsChanged(unsigned int modifiers) +{ + updateModifiers(modifiers); + + bool cmdPressed = (modifiers & NSEventModifierFlagCommand) != 0; + bool cmdWasPressed = (m_lastModifiers & NSEventModifierFlagCommand) != 0; + if (cmdPressed != cmdWasPressed) { + if (cmdPressed) + processKeyDown(Fw::KeyMeta); + else + processKeyUp(Fw::KeyMeta); + if (!cmdPressed) { + for (size_t keyIndex = 0; keyIndex < m_commandKeyDown.size(); ++keyIndex) { + if (!m_commandKeyDown[keyIndex]) + continue; + m_commandKeyDown[keyIndex] = false; + processKeyUp(static_cast(keyIndex)); + } + } + } + + bool optPressed = (modifiers & NSEventModifierFlagOption) != 0; + bool optWasPressed = (m_lastModifiers & NSEventModifierFlagOption) != 0; + if (optPressed != optWasPressed) { + if (optPressed) + processKeyDown(Fw::KeyAlt); + else + processKeyUp(Fw::KeyAlt); + } + + bool ctrlPressed = (modifiers & NSEventModifierFlagControl) != 0; + bool ctrlWasPressed = (m_lastModifiers & NSEventModifierFlagControl) != 0; + if (ctrlPressed != ctrlWasPressed) { + if (ctrlPressed) + processKeyDown(Fw::KeyCtrl); + else + processKeyUp(Fw::KeyCtrl); + } + + bool shiftPressed = (modifiers & NSEventModifierFlagShift) != 0; + bool shiftWasPressed = (m_lastModifiers & NSEventModifierFlagShift) != 0; + if (shiftPressed != shiftWasPressed) { + if (shiftPressed) + processKeyDown(Fw::KeyShift); + else + processKeyUp(Fw::KeyShift); + } + + m_lastModifiers = modifiers; +} + +void CocoaWindow::updateModifiers(unsigned int modifiers) +{ + if (modifiers & NSEventModifierFlagCommand) + m_inputEvent.keyboardModifiers |= Fw::KeyboardPrimaryModifier; + else + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardPrimaryModifier; + + if (modifiers & NSEventModifierFlagOption) + m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier; + else + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardAltModifier; + + if (modifiers & NSEventModifierFlagControl) + m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier; + else + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardCtrlModifier; + + if (modifiers & NSEventModifierFlagShift) + m_inputEvent.keyboardModifiers |= Fw::KeyboardShiftModifier; + else + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardShiftModifier; +} + +void CocoaWindow::handleMouseButton(Fw::MouseButton button, bool pressed, const Point& position) +{ + m_inputEvent.mousePos = position; + + if (pressed) { + m_mouseButtonStates |= (1u << button); + m_inputEvent.reset(Fw::MousePressInputEvent); + m_inputEvent.mouseButton = button; + } else { + m_mouseButtonStates &= ~(1u << button); + m_inputEvent.reset(Fw::MouseReleaseInputEvent); + m_inputEvent.mouseButton = button; + } + + if (m_onInputEvent) + m_onInputEvent(m_inputEvent); +} + +void CocoaWindow::handleMouseMove(const Point& position) +{ + m_inputEvent.reset(Fw::MouseMoveInputEvent); + m_inputEvent.mousePos = position; + + if (m_onInputEvent) + m_onInputEvent(m_inputEvent); +} + +void CocoaWindow::handleMouseScroll(int deltaX, int deltaY) +{ + (void)deltaX; + m_inputEvent.reset(Fw::MouseWheelInputEvent); + + if (deltaY != 0) { + m_inputEvent.wheelDirection = deltaY > 0 ? Fw::MouseWheelUp : Fw::MouseWheelDown; + if (m_onInputEvent) + m_onInputEvent(m_inputEvent); + } +} + +void CocoaWindow::handleResize(int width, int height) +{ + m_size = Size(width, height); + + if (m_glContext) { + [m_glContext update]; + } + + if (m_onResize) + m_onResize(m_size); +} + +void CocoaWindow::handleMove(int x, int y) +{ + m_position = Point(x, y); + updateUnmaximizedCoords(); +} + +void CocoaWindow::handleClose() +{ + if (m_onClose) + m_onClose(); +} + +void CocoaWindow::handleFocusChange(bool focused) +{ + m_focused = focused; + if (!focused) { + releaseAllKeys(); + } +} + +void CocoaWindow::handleTextInput(const std::string& text) +{ + if (text.empty()) return; + + for (size_t i = 0; i < text.size(); ) { + unsigned char c = text[i]; + int charLen = 1; + + if ((c & 0x80) == 0) { + charLen = 1; + } else if ((c & 0xE0) == 0xC0) { + charLen = 2; + } else if ((c & 0xF0) == 0xE0) { + charLen = 3; + } else if ((c & 0xF8) == 0xF0) { + charLen = 4; + } + + if (i + charLen <= text.size()) { + std::string character = text.substr(i, charLen); + + m_inputEvent.reset(Fw::KeyTextInputEvent); + m_inputEvent.keyText = character; + + if (m_onInputEvent) + m_onInputEvent(m_inputEvent); + } + + i += charLen; + } +} + +#endif diff --git a/src/framework/platform/platformwindow.cpp b/src/framework/platform/platformwindow.cpp index 4ada4cdff..3816c3398 100644 --- a/src/framework/platform/platformwindow.cpp +++ b/src/framework/platform/platformwindow.cpp @@ -34,6 +34,9 @@ AndroidWindow window; #elif defined __EMSCRIPTEN__ #include "browserwindow.h" BrowserWindow window; +#elif defined __APPLE__ +#include "cocoawindow.h" +CocoaWindow window; #else #include "x11window.h" #include @@ -86,20 +89,34 @@ void PlatformWindow::processKeyDown(Fw::Key keyCode) if (keyCode == Fw::KeyUnknown) return; - if (keyCode == Fw::KeyCtrl) { - m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier; - return; #if defined(__APPLE__) - } else if (keyCode == Fw::KeyMeta) { - m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier; + if (keyCode == Fw::KeyMeta) { + m_inputEvent.keyboardModifiers |= Fw::KeyboardPrimaryModifier; return; -#else } if (keyCode == Fw::KeyAlt) { m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier; return; -#endif } + if (keyCode == Fw::KeyCtrl) { + m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier; + return; + } +#else + if (keyCode == Fw::KeyCtrl) { + m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier; + m_inputEvent.keyboardModifiers |= Fw::KeyboardPrimaryModifier; + return; + } + if (keyCode == Fw::KeyAlt) { + m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier; + return; + } + if (keyCode == Fw::KeyMeta) { + m_inputEvent.keyboardModifiers |= Fw::KeyboardMetaModifier; + return; + } +#endif if (keyCode == Fw::KeyShift) { m_inputEvent.keyboardModifiers |= Fw::KeyboardShiftModifier; return; @@ -131,20 +148,34 @@ void PlatformWindow::processKeyUp(Fw::Key keyCode) if (keyCode == Fw::KeyUnknown) return; - if (keyCode == Fw::KeyCtrl) { - m_inputEvent.keyboardModifiers &= ~Fw::KeyboardCtrlModifier; - return; #if defined(__APPLE__) - } else if (keyCode == Fw::KeyMeta) { - m_inputEvent.keyboardModifiers &= ~Fw::KeyboardAltModifier; + if (keyCode == Fw::KeyMeta) { + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardPrimaryModifier; return; -#else } if (keyCode == Fw::KeyAlt) { m_inputEvent.keyboardModifiers &= ~Fw::KeyboardAltModifier; return; -#endif } + if (keyCode == Fw::KeyCtrl) { + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardCtrlModifier; + return; + } +#else + if (keyCode == Fw::KeyCtrl) { + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardCtrlModifier; + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardPrimaryModifier; + return; + } + if (keyCode == Fw::KeyAlt) { + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardAltModifier; + return; + } + if (keyCode == Fw::KeyMeta) { + m_inputEvent.keyboardModifiers &= ~Fw::KeyboardMetaModifier; + return; + } +#endif if (keyCode == Fw::KeyShift) { m_inputEvent.keyboardModifiers &= ~Fw::KeyboardShiftModifier; return; @@ -211,4 +242,4 @@ void PlatformWindow::fireKeysPress() keyInfo.lastTicks = now; } } -} \ No newline at end of file +} diff --git a/src/framework/platform/win32window.cpp b/src/framework/platform/win32window.cpp index d4b2f2749..c41b821a6 100644 --- a/src/framework/platform/win32window.cpp +++ b/src/framework/platform/win32window.cpp @@ -587,12 +587,12 @@ Fw::Key WIN32Window::retranslateVirtualKey(const WPARAM wParam, const LPARAM lPa if (m_keyMap.contains(wParam)) key = m_keyMap[wParam]; - // actually ignore alt/ctrl/shift keys, they is states are already stored in m_inputEvent.keyboardModifiers + // actually ignore modifier keys, their states are already stored in m_inputEvent.keyboardModifiers #if defined(__APPLE__) if (key == Fw::KeyMeta || key == Fw::KeyCtrl || key == Fw::KeyShift) key = Fw::KeyUnknown; #else - if (key == Fw::KeyAlt || key == Fw::KeyCtrl || key == Fw::KeyShift) + if (key == Fw::KeyAlt || key == Fw::KeyCtrl || key == Fw::KeyShift || key == Fw::KeyMeta) key = Fw::KeyUnknown; #endif @@ -604,8 +604,10 @@ Fw::Key WIN32Window::retranslateVirtualKey(const WPARAM wParam, const LPARAM lPa LRESULT WIN32Window::windowProc(const HWND hWnd, const uint32_t uMsg, const WPARAM wParam, const LPARAM lParam) { m_inputEvent.keyboardModifiers = 0; - if (IsKeyDown(VK_CONTROL)) + if (IsKeyDown(VK_CONTROL)) { m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier; + m_inputEvent.keyboardModifiers |= Fw::KeyboardPrimaryModifier; + } if (IsKeyDown(VK_SHIFT)) m_inputEvent.keyboardModifiers |= Fw::KeyboardShiftModifier; #if defined(__APPLE__) @@ -615,6 +617,8 @@ LRESULT WIN32Window::windowProc(const HWND hWnd, const uint32_t uMsg, const WPAR if (IsKeyDown(VK_MENU)) m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier; #endif + if (IsKeyDown(VK_LWIN) || IsKeyDown(VK_RWIN)) + m_inputEvent.keyboardModifiers |= Fw::KeyboardMetaModifier; bool signalKeyEvent = false; switch (uMsg) { diff --git a/src/framework/sound/soundmanager.cpp b/src/framework/sound/soundmanager.cpp index fe3f98f05..09edec00e 100644 --- a/src/framework/sound/soundmanager.cpp +++ b/src/framework/sound/soundmanager.cpp @@ -272,7 +272,7 @@ SoundSourcePtr SoundManager::createSoundSource(const std::string& name) streamSource->setRelative(true); streamSource->setPosition(Point(-128, 0)); combinedSource->addSource(streamSource); - m_streamFiles[streamSource] = g_asyncDispatcher.submit_task([=]() -> SoundFilePtr { + m_streamFiles[streamSource] = g_asyncDispatcher->submit_task([=]() -> SoundFilePtr { stdext::timer a; try { return SoundFile::loadSoundFile(filename); @@ -287,7 +287,7 @@ SoundSourcePtr SoundManager::createSoundSource(const std::string& name) streamSource->setRelative(true); streamSource->setPosition(Point(128, 0)); combinedSource->addSource(streamSource); - m_streamFiles[streamSource] = g_asyncDispatcher.submit_task([=]() -> SoundFilePtr { + m_streamFiles[streamSource] = g_asyncDispatcher->submit_task([=]() -> SoundFilePtr { try { return SoundFile::loadSoundFile(filename); } catch (std::exception& e) { @@ -299,7 +299,7 @@ SoundSourcePtr SoundManager::createSoundSource(const std::string& name) source = combinedSource; #else const auto& streamSource = std::make_shared(); - m_streamFiles[streamSource] = g_asyncDispatcher.submit_task([=]() -> SoundFilePtr { + m_streamFiles[streamSource] = g_asyncDispatcher->submit_task([=]() -> SoundFilePtr { try { return SoundFile::loadSoundFile(filename); } catch (std::exception& e) { diff --git a/src/framework/ui/uitextedit.cpp b/src/framework/ui/uitextedit.cpp index 1ac33616e..361c4ac70 100644 --- a/src/framework/ui/uitextedit.cpp +++ b/src/framework/ui/uitextedit.cpp @@ -1380,6 +1380,8 @@ bool UITextEdit::onKeyPress(const uint8_t keyCode, const int keyboardModifiers, if (UIWidget::onKeyPress(keyCode, keyboardModifiers, autoRepeatTicks)) return true; + const bool primaryOnly = Fw::isPrimaryModifierOnly(keyboardModifiers); + if (keyboardModifiers == Fw::KeyboardNoModifier) { if (keyCode == Fw::KeyDelete && getProp(PropEditable)) { if (hasSelection() || !m_text.empty()) { @@ -1463,7 +1465,7 @@ bool UITextEdit::onKeyPress(const uint8_t keyCode, const int keyboardModifiers, moveCursorVertically(false); return true; } - } else if (keyboardModifiers == Fw::KeyboardCtrlModifier) { + } else if (primaryOnly) { if (keyCode == Fw::KeyV && getProp(PropEditable)) { paste(g_window.getClipboardText()); return true; diff --git a/src/framework/ui/uiwidget.cpp b/src/framework/ui/uiwidget.cpp index 420071932..bc5a804af 100644 --- a/src/framework/ui/uiwidget.cpp +++ b/src/framework/ui/uiwidget.cpp @@ -35,6 +35,7 @@ #include "framework/html/htmlmanager.h" #include "framework/html/htmlnode.h" #include "framework/otml/otmlnode.h" +#include "framework/graphics/coordsbuffer.h" #include #include @@ -58,6 +59,8 @@ UIWidget::UIWidget() m_positions.set(Unit::Auto); m_clickTimer.stop(); + m_textUnderline = std::make_shared(); + initBaseStyle(); initText(); initImage(); diff --git a/src/framework/ui/uiwidget.h b/src/framework/ui/uiwidget.h index 54d133550..7a702dfc8 100644 --- a/src/framework/ui/uiwidget.h +++ b/src/framework/ui/uiwidget.h @@ -26,6 +26,7 @@ #include #include +#include #include #include diff --git a/src/framework/ui/uiwidgetbasestyle.cpp b/src/framework/ui/uiwidgetbasestyle.cpp index 3e2aad6db..a15ef6f1a 100644 --- a/src/framework/ui/uiwidgetbasestyle.cpp +++ b/src/framework/ui/uiwidgetbasestyle.cpp @@ -621,6 +621,7 @@ void UIWidget::parseBaseStyle(const OTMLNodePtr& styleNode) setFloat(type); } else if (node->tag() == "clear") { auto v = node->value(); + stdext::tolower(v); ClearType clear = ClearType::None; if (v == "left") clear = ClearType::Left; else if (v == "right") clear = ClearType::Right; diff --git a/src/framework/util/point.h b/src/framework/util/point.h index c3bc1e84d..c99d7d55e 100644 --- a/src/framework/util/point.h +++ b/src/framework/util/point.h @@ -115,5 +115,10 @@ struct fmt::formatter, char> { } }; -using Point = TPoint; -using PointF = TPoint; +namespace Fw { + using Point = TPoint; + using PointF = TPoint; +} + +using Fw::Point; +using Fw::PointF; diff --git a/src/framework/util/size.h b/src/framework/util/size.h index d8cc0e054..5f21f9212 100644 --- a/src/framework/util/size.h +++ b/src/framework/util/size.h @@ -134,5 +134,10 @@ private: T wd, ht; }; -using Size = TSize; -using SizeF = TSize; +namespace Fw { + using Size = TSize; + using SizeF = TSize; +} + +using Fw::Size; +using Fw::SizeF; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 38d7c60b7..214772f3a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -38,6 +38,16 @@ function(otclient_add_gtest TARGET_NAME) FRAMEWORK_XML ) + if(ASAN_ENABLED) + if(MSVC) + target_compile_options(${TARGET_NAME} PRIVATE /fsanitize=address) + target_link_options(${TARGET_NAME} PRIVATE /fsanitize=address) + else() + target_compile_options(${TARGET_NAME} PRIVATE -fsanitize=address) + target_link_options(${TARGET_NAME} PRIVATE -fsanitize=address) + endif() + endif() + if(MSVC) target_compile_options(${TARGET_NAME} PRIVATE /utf-8) if(BUILD_STATIC_LIBRARY) diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 000000000..3deb4e36c --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,5 @@ +{ + "overlay-ports": [ + "overlay-ports" + ] +}