mirror of
https://github.com/mehah/otclient
synced 2026-08-15 16:29:06 -04:00
feat(platform): add native Cocoa/AppKit windowing for macOS (#1559)
New Features: - Native macOS app with Cocoa windowing, OpenGL view, improved input, clipboard and cursor handling. - Platform-aware keyboard and shortcut behavior with clearer primary/meta/control semantics and normalized key combos. Bug Fixes: - Expanded detection for "connection refused" network errors. Chores: - macOS build/bundle and packaging improvements; new dependency overlay added; CI now runs macOS build when relevant. Tests: - AddressSanitizer support added for test builds.
This commit is contained in:
parent
6a478408ee
commit
388b3ba2ea
57 changed files with 2648 additions and 342 deletions
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
|
@ -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]
|
||||
|
|
|
|||
137
.github/workflows/reusable-build-macos.yml
vendored
Normal file
137
.github/workflows/reusable-build-macos.yml
vendored
Normal file
|
|
@ -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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -196,6 +196,7 @@ pip-log.txt
|
|||
#################
|
||||
|
||||
/otclient
|
||||
/OTClient.app
|
||||
/modules/otclientrc.lua
|
||||
src/framework/graphics/dx/
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
32
cmake/Info.plist.in
Normal file
32
cmake/Info.plist.in
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>@MACOSX_BUNDLE_DISPLAY_NAME@</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>@MACOSX_BUNDLE_EXECUTABLE_NAME@</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>@MACOSX_BUNDLE_IDENTIFIER@</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>@MACOSX_BUNDLE_BUNDLE_NAME@</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>@MACOSX_BUNDLE_SHORT_VERSION_STRING@</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>@MACOSX_BUNDLE_VERSION@</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>@MACOSX_BUNDLE_ICON_FILE@</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>@MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION@</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>@MACOSX_BUNDLE_COPYRIGHT@</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
cmake/OTClient.icns
Normal file
BIN
cmake/OTClient.icns
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 510 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,5 +34,6 @@ Module
|
|||
dofile 'objectpool'
|
||||
|
||||
Keybind.init()
|
||||
|
||||
initPlatformKeyDescs()
|
||||
|
||||
@onUnload: Keybind.terminate()
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
51
overlay-ports/inih/CMakeLists.txt
Normal file
51
overlay-ports/inih/CMakeLists.txt
Normal file
|
|
@ -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
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
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
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/cpp>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
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()
|
||||
62
overlay-ports/inih/portfile.cmake
Normal file
62
overlay-ports/inih/portfile.cmake
Normal file
|
|
@ -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)
|
||||
60
overlay-ports/inih/unofficial-inihConfig.cmake.in
Normal file
60
overlay-ports/inih/unofficial-inihConfig.cmake.in
Normal file
|
|
@ -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 ####
|
||||
###################
|
||||
7
overlay-ports/inih/usage
Normal file
7
overlay-ports/inih/usage
Normal file
|
|
@ -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
|
||||
12
overlay-ports/inih/vcpkg.json
Normal file
12
overlay-ports/inih/vcpkg.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
$<$<AND:$<PLATFORM_ID:Linux>,$<NOT:$<BOOL:${ANDROID}>>>: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"
|
||||
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/../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()
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
MapView::MapView() : m_lightView(std::make_unique<LightView>(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<void> 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);
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FileStream_m>(g_resources.openFile(m_lastFileName));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
#include "application.h"
|
||||
|
||||
#include "asyncdispatcher.h"
|
||||
#include <gitinfo.h>
|
||||
|
||||
#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);
|
||||
|
|
|
|||
|
|
@ -38,4 +38,4 @@ uint8_t getThreadCount() {
|
|||
return std::clamp<int_fast8_t>(std::thread::hardware_concurrency() - 1, MIN_THREADS, MAX_THREADS);
|
||||
}
|
||||
|
||||
AsyncDispatcher g_asyncDispatcher{ getThreadCount() };
|
||||
std::unique_ptr<AsyncDispatcher> g_asyncDispatcher = std::make_unique<AsyncDispatcher>(getThreadCount());
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@
|
|||
#include <cstddef>
|
||||
|
||||
#include <BS_thread_pool.hpp>
|
||||
#include <memory>
|
||||
|
||||
using AsyncDispatcher = decltype(BS::thread_pool{ std::size_t{} });
|
||||
|
||||
extern AsyncDispatcher g_asyncDispatcher;
|
||||
extern std::unique_ptr<AsyncDispatcher> g_asyncDispatcher;
|
||||
|
|
|
|||
|
|
@ -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<ThreadTask>());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> 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<DrawPoolType, 2> 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<std::vector<uint8_t>>(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();
|
||||
|
|
|
|||
|
|
@ -220,6 +220,6 @@ void ModuleManager::enableAutoReload() {
|
|||
return;
|
||||
|
||||
processing.store(true);
|
||||
g_asyncDispatcher.detach_task(action);
|
||||
g_asyncDispatcher->detach_task(action);
|
||||
}, 500);
|
||||
}
|
||||
|
|
@ -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<std::string>& 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<std::string>& 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<std::string>& args) { // c
|
|||
}
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binary == m_binaryPath)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
#include "painter.h"
|
||||
#include "textureatlas.h"
|
||||
#include "coordsbuffer.h"
|
||||
|
||||
DrawPool* DrawPool::create(const DrawPoolType type)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@
|
|||
#define GL_APICALL
|
||||
#define EGLAPI
|
||||
#include <GLES3/gl3.h>
|
||||
#elif defined(__APPLE__)
|
||||
#define GLEW_STATIC
|
||||
#define GLEW_NO_GLU
|
||||
#include <GL/glew.h>
|
||||
#else
|
||||
#ifndef _MSC_VER
|
||||
#define GLEW_STATIC
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@
|
|||
|
||||
#include "framework/platform/staticdata.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
|
||||
template<typename T>
|
||||
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<typename T>
|
||||
inline bool luavalue_cast_integer_from_double(const int index, T& v)
|
||||
{
|
||||
static_assert(std::is_integral_v<T>);
|
||||
|
||||
double d;
|
||||
if (!luavalue_cast(index, d))
|
||||
return false;
|
||||
if (!std::isfinite(d) || std::trunc(d) != d)
|
||||
return false;
|
||||
|
||||
const double typeMin = static_cast<double>(std::numeric_limits<T>::lowest());
|
||||
const double typeMax = static_cast<double>(std::numeric_limits<T>::max());
|
||||
const double exactMin = std::numeric_limits<T>::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<T>(d);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// float
|
||||
inline int push_luavalue(const float f) { push_luavalue(static_cast<double>(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<double>(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<double>(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<sizeof(unsigned long) == 8, unsigned long, std::uint64_t>;
|
||||
|
|
@ -100,6 +137,7 @@ using lua_unsigned_long = lua_u64;
|
|||
|
||||
static_assert(sizeof(lua_u64) == 8, "lua_u64 must be 64-bit");
|
||||
|
||||
template<typename T = unsigned long> requires (!std::is_same_v<T, unsigned long long>)
|
||||
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<typename T = unsigned long> requires (!std::is_same_v<T, unsigned long long>)
|
||||
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<unsigned long>(temp);
|
||||
return r;
|
||||
return detail::luavalue_cast_integer_from_double(index, v);
|
||||
}
|
||||
|
||||
template<typename T = lua_u64, std::enable_if_t<!std::is_same_v<T, unsigned long>, int> = 0>
|
||||
template<typename T = lua_u64> requires (!std::is_same_v<T, unsigned long>)
|
||||
inline int push_luavalue(lua_u64 v)
|
||||
{
|
||||
push_luavalue(static_cast<double>(v));
|
||||
return 1;
|
||||
}
|
||||
|
||||
template<typename T = lua_u64, std::enable_if_t<!std::is_same_v<T, unsigned long>, int> = 0>
|
||||
template<typename T = lua_u64> requires (!std::is_same_v<T, unsigned long> && !std::is_same_v<T, unsigned long long>)
|
||||
inline bool luavalue_cast(const int idx, lua_u64& v)
|
||||
{
|
||||
double d;
|
||||
const bool r = luavalue_cast(idx, d);
|
||||
v = static_cast<lua_u64>(d);
|
||||
return r;
|
||||
return detail::luavalue_cast_integer_from_double(idx, v);
|
||||
}
|
||||
|
||||
// string
|
||||
|
|
|
|||
|
|
@ -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 <httplib.h>
|
||||
|
||||
#include "httplogin.h"
|
||||
#include <framework/core/asyncdispatcher.h>
|
||||
#include <framework/core/eventdispatcher.h>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
# define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#endif
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <framework/luaengine/luaobject.h>
|
||||
#include <httplib.h>
|
||||
|
||||
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;
|
||||
|
|
|
|||
57
src/framework/platform/cocoaview.h
Normal file
57
src/framework/platform/cocoaview.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/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 <Cocoa/Cocoa.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#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 <NSWindowDelegate>
|
||||
@property (nonatomic, assign) CocoaWindow* platformWindow;
|
||||
@end
|
||||
|
||||
#endif
|
||||
271
src/framework/platform/cocoaview.mm
Normal file
271
src/framework/platform/cocoaview.mm
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/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 <string>
|
||||
|
||||
#define Point MacPoint
|
||||
#define Size MacSize
|
||||
#define Rect MacRect
|
||||
|
||||
#import <Carbon/Carbon.h>
|
||||
|
||||
#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<int>(loc.x), static_cast<int>(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<int>(_scrollAccumX);
|
||||
int intDY = static_cast<int>(_scrollAccumY);
|
||||
if (intDX != 0 || intDY != 0) {
|
||||
_platformWindow->handleMouseScroll(intDX, intDY);
|
||||
_scrollAccumX -= intDX;
|
||||
_scrollAccumY -= intDY;
|
||||
}
|
||||
} else {
|
||||
_platformWindow->handleMouseScroll(static_cast<int>(deltaX), static_cast<int>(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<int>(contentRect.size.width),
|
||||
static_cast<int>(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<int>(screenFrame.origin.y + screenFrame.size.height - frame.origin.y - frame.size.height);
|
||||
_platformWindow->handleMove(static_cast<int>(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
|
||||
296
src/framework/platform/cocoawindow.h
Normal file
296
src/framework/platform/cocoawindow.h
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/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 <array>
|
||||
|
||||
#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<NSCursor*> m_cursors;
|
||||
NSCursor* m_currentCursor;
|
||||
NSCursor* m_defaultCursor;
|
||||
bool m_cursorHidden;
|
||||
bool m_cursorInWindow;
|
||||
|
||||
unsigned int m_lastModifiers;
|
||||
std::array<bool, Fw::KeyLast> m_commandKeyDown{};
|
||||
};
|
||||
824
src/framework/platform/cocoawindow.mm
Normal file
824
src/framework/platform/cocoawindow.mm
Normal file
|
|
@ -0,0 +1,824 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/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 <GL/glew.h>
|
||||
|
||||
#define Point MacPoint
|
||||
#define Size MacSize
|
||||
#define Rect MacRect
|
||||
|
||||
#define GL_SILENCE_DEPRECATION
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
#import <QuartzCore/CVDisplayLink.h>
|
||||
|
||||
#undef Point
|
||||
#undef Size
|
||||
#undef Rect
|
||||
|
||||
#include "cocoawindow.h"
|
||||
#include "cocoaview.h"
|
||||
#include <framework/core/eventdispatcher.h>
|
||||
#include <framework/graphics/image.h>
|
||||
|
||||
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<int>(contentRect.size.width),
|
||||
static_cast<int>(contentRect.size.height));
|
||||
|
||||
NSRect windowFrame = [m_window frame];
|
||||
NSScreen* screen = [m_window screen];
|
||||
if (screen) {
|
||||
NSRect screenFrame = [screen frame];
|
||||
m_position = Point(static_cast<int>(windowFrame.origin.x),
|
||||
static_cast<int>(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<int>(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<int>(frame.size.width), static_cast<int>(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<int>(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<size_t>(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<size_t>(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<Fw::Key>(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
|
||||
|
|
@ -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 <framework/core/clock.h>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<StreamSoundSource>();
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <framework/platform/platformwindow.h>
|
||||
#include <framework/util/stats.h>
|
||||
|
||||
|
|
@ -58,6 +59,8 @@ UIWidget::UIWidget()
|
|||
m_positions.set(Unit::Auto);
|
||||
m_clickTimer.stop();
|
||||
|
||||
m_textUnderline = std::make_shared<CoordsBuffer>();
|
||||
|
||||
initBaseStyle();
|
||||
initText();
|
||||
initImage();
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
#include <framework/core/timer.h>
|
||||
#include <framework/graphics/declarations.h>
|
||||
#include <framework/graphics/coordsbuffer.h>
|
||||
#include <framework/html/declarations.h>
|
||||
#include <framework/luaengine/luaobject.h>
|
||||
|
||||
|
|
|
|||
|
|
@ -621,6 +621,7 @@ void UIWidget::parseBaseStyle(const OTMLNodePtr& styleNode)
|
|||
setFloat(type);
|
||||
} else if (node->tag() == "clear") {
|
||||
auto v = node->value<std::string>();
|
||||
stdext::tolower(v);
|
||||
ClearType clear = ClearType::None;
|
||||
if (v == "left") clear = ClearType::Left;
|
||||
else if (v == "right") clear = ClearType::Right;
|
||||
|
|
|
|||
|
|
@ -115,5 +115,10 @@ struct fmt::formatter<TPoint<T>, char> {
|
|||
}
|
||||
};
|
||||
|
||||
using Point = TPoint<int>;
|
||||
using PointF = TPoint<float>;
|
||||
namespace Fw {
|
||||
using Point = TPoint<int>;
|
||||
using PointF = TPoint<float>;
|
||||
}
|
||||
|
||||
using Fw::Point;
|
||||
using Fw::PointF;
|
||||
|
|
|
|||
|
|
@ -134,5 +134,10 @@ private:
|
|||
T wd, ht;
|
||||
};
|
||||
|
||||
using Size = TSize<int>;
|
||||
using SizeF = TSize<float>;
|
||||
namespace Fw {
|
||||
using Size = TSize<int>;
|
||||
using SizeF = TSize<float>;
|
||||
}
|
||||
|
||||
using Fw::Size;
|
||||
using Fw::SizeF;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
5
vcpkg-configuration.json
Normal file
5
vcpkg-configuration.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"overlay-ports": [
|
||||
"overlay-ports"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue