mirror of
https://github.com/radareorg/iaito.git
synced 2026-11-07 18:36:29 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4badc5fd4 | ||
|
|
49b4b19c21 | ||
|
|
068520f923 | ||
|
|
f723ee3dee | ||
|
|
ce1f9181cf | ||
|
|
a043f93e72 | ||
|
|
c0cb4a5123 | ||
|
|
db64c517a4 | ||
|
|
9602d98307 | ||
|
|
d85739b5aa | ||
|
|
1828006c2b | ||
|
|
aff259387b | ||
|
|
fe80cff908 | ||
|
|
ae183f2c43 | ||
|
|
9ae80e4d62 | ||
|
|
e920c6aba8 | ||
|
|
5875f989d8 | ||
|
|
d95daf392f | ||
|
|
6c4997ac29 | ||
|
|
a8d239febd | ||
|
|
6b52aa0be1 | ||
|
|
19c3c07b99 | ||
|
|
ef8aba25e8 | ||
|
|
c2f042643a | ||
|
|
baf1afaca0 | ||
|
|
de8f2b476e | ||
|
|
bc6c362bd5 | ||
|
|
bff22b12bd | ||
|
|
63f2bd57cd | ||
|
|
5518aaa29d | ||
|
|
73856c6c32 | ||
|
|
018c210777 | ||
|
|
59a47845f0 | ||
|
|
11b530a526 | ||
|
|
f6b792020a | ||
|
|
c50dc3313f | ||
|
|
0e5a49c331 | ||
|
|
74c12685a0 | ||
|
|
93af6edfe9 | ||
|
|
977578a91e | ||
|
|
e980b1cb48 | ||
|
|
b5edef36bc | ||
|
|
1208624c77 | ||
|
|
eff435fc29 | ||
|
|
af1f536b0f | ||
|
|
6affec227d | ||
|
|
e0a120ad08 | ||
|
|
3383cc1d92 | ||
|
|
7eade2df0c | ||
|
|
bd1105f45d | ||
|
|
a7532b4d80 | ||
|
|
11240a1a0f | ||
|
|
ddb7bdb428 | ||
|
|
b912778e62 | ||
|
|
dc1fde07f5 | ||
|
|
1e089d9c37 | ||
|
|
e0e9690fd5 | ||
|
|
bbbea0adcd | ||
|
|
f8ef719e31 | ||
|
|
e560d5f88c | ||
|
|
3f9e2f78f9 | ||
|
|
d29c291ff7 | ||
|
|
e62e0a9d70 | ||
|
|
0ccc495308 | ||
|
|
95ca7a071f | ||
|
|
4e100c4fc3 | ||
|
|
abf90e6eaa | ||
|
|
dd28d5b367 | ||
|
|
d7fabe89f2 | ||
|
|
069b64679c | ||
|
|
7a8701f030 | ||
|
|
dafb693b59 | ||
|
|
8df94a0b91 | ||
|
|
e9c99b6da4 | ||
|
|
144dcce52f | ||
|
|
f6c5cf0cb8 | ||
|
|
c3ecfea9bb | ||
|
|
5c7dbaca9c | ||
|
|
2982ce12f9 | ||
|
|
eaf3b1ae36 | ||
|
|
c2da3b5c88 | ||
|
|
815267962d | ||
|
|
60da11e8d9 | ||
|
|
244596bcb0 | ||
|
|
27be4123c3 | ||
|
|
158ba57ae5 | ||
|
|
bb0c8ab6d4 | ||
|
|
e35d443079 | ||
|
|
55a401b3dc | ||
|
|
0ecf546b80 | ||
|
|
4a14b8ddfb |
No files matched your search
+158
-65
@@ -1,12 +1,18 @@
|
||||
name: iaito CI
|
||||
|
||||
env:
|
||||
R2V: 6.2.0
|
||||
R2GHIDRA_VERSION: 6.2.0
|
||||
R2V: 6.2.6
|
||||
R2GHIDRA_VERSION: 6.2.6
|
||||
QT_MAC_INSTALL_VERSION: 6.11.*
|
||||
QT_WIN_INSTALL_VERSION: 6.10.*
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Existing version tag to build and publish
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -15,14 +21,57 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
source:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
sha: ${{ steps.source.outputs.sha }}
|
||||
r2: ${{ steps.source.outputs.R2V }}
|
||||
r2ghidra: ${{ steps.source.outputs.R2GHIDRA_VERSION }}
|
||||
qt_mac: ${{ steps.source.outputs.QT_MAC_INSTALL_VERSION }}
|
||||
qt_win: ${{ steps.source.outputs.QT_WIN_INSTALL_VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.release_tag && format('refs/tags/{0}', inputs.release_tag) || github.sha }}
|
||||
persist-credentials: false
|
||||
- name: Resolve build source and dependencies
|
||||
id: source
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "$RELEASE_TAG" ]; then
|
||||
[[ "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
test "$(./configure -qV)" = "$RELEASE_TAG"
|
||||
fi
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
python3 - <<'PYTHON'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
workflow = Path('.github/workflows/ci.yml').read_text()
|
||||
keys = ('R2V', 'R2GHIDRA_VERSION', 'QT_MAC_INSTALL_VERSION', 'QT_WIN_INSTALL_VERSION')
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
|
||||
for key in keys:
|
||||
value = os.environ[key]
|
||||
if os.environ['RELEASE_TAG']:
|
||||
match = re.search(r'^ ' + key + r': ([0-9.*]+)$', workflow, re.M)
|
||||
if not match:
|
||||
raise SystemExit(f'Missing dependency version: {key}')
|
||||
value = match.group(1)
|
||||
print(f'{key}={value}', file=output)
|
||||
PYTHON
|
||||
|
||||
acr-linux-r2git:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: apt dependencies
|
||||
@@ -35,37 +84,34 @@ jobs:
|
||||
run: |
|
||||
./configure --prefix=/usr
|
||||
make -j4
|
||||
- name: command-line checks
|
||||
run: python3 tests/commandline/test_commandline.py build/iaito
|
||||
- name: packaging
|
||||
run: make -j -C dist/debian
|
||||
indent-check:
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: Install indent-check dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgraphviz-dev mesa-common-dev ninja-build libqt5svg5-dev qttools5-dev qttools5-dev-tools qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools
|
||||
- name: install r2
|
||||
run: |
|
||||
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2_${{env.R2V}}_amd64.deb
|
||||
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-dev_${{env.R2V}}_amd64.deb
|
||||
sudo dpkg -i *.deb
|
||||
- name: configure iaito
|
||||
run: ./configure --prefix=/usr
|
||||
persist-credentials: false
|
||||
- name: install clang-format
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends clang-format-20
|
||||
- name: make indent
|
||||
run: make indent && git diff --exit-code
|
||||
run: make indent-host && git diff --exit-code
|
||||
acr-linux:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: source
|
||||
env:
|
||||
R2V: ${{ needs.source.outputs.r2 }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.source.outputs.sha }}
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: apt dependencies
|
||||
@@ -82,6 +128,9 @@ jobs:
|
||||
run: ./configure --prefix=/usr --with-qmake6
|
||||
- name: build iaito
|
||||
run: make -j4
|
||||
- name: command-line checks
|
||||
if: ${{ hashFiles('tests/commandline/test_commandline.py') != '' }}
|
||||
run: python3 tests/commandline/test_commandline.py build/iaito
|
||||
- name: packaging
|
||||
run: make -C dist/debian
|
||||
- uses: actions/upload-artifact@v7
|
||||
@@ -89,13 +138,14 @@ jobs:
|
||||
name: iaito-amd64.deb
|
||||
path: dist/debian/*/*.deb
|
||||
acr-linux-nodebugger:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: apt dependencies
|
||||
@@ -112,7 +162,15 @@ jobs:
|
||||
run: ./configure --prefix=/usr --with-qmake6 --without-debugger
|
||||
- name: build iaito
|
||||
run: make -j4
|
||||
- name: command-line checks
|
||||
run: python3 tests/commandline/test_commandline.py build/iaito
|
||||
acr-macos:
|
||||
needs: source
|
||||
env:
|
||||
R2V: ${{ needs.source.outputs.r2 }}
|
||||
R2GHIDRA_VERSION: ${{ needs.source.outputs.r2ghidra }}
|
||||
QT_MAC_INSTALL_VERSION: ${{ needs.source.outputs.qt_mac }}
|
||||
QT_WIN_INSTALL_VERSION: ${{ needs.source.outputs.qt_win }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -127,9 +185,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.source.outputs.sha }}
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: uname
|
||||
@@ -145,10 +204,41 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{github.token}}
|
||||
run: |
|
||||
curl -Lo radare2.pkg "https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{matrix.arch}}-${{env.R2V}}.pkg"
|
||||
curl -Lo r2ai.tar.gz "$(gh api /repos/radareorg/r2ai/releases/latest --jq .tarball_url)"
|
||||
curl -Lo r2ghidra.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra-${{env.R2GHIDRA_VERSION}}-macos-${{matrix.arch_alt||matrix.arch}}.zip"
|
||||
curl -Lo r2ghidra_sleigh.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra_sleigh-${{env.R2GHIDRA_VERSION}}.zip"
|
||||
validate_download() {
|
||||
case "$2" in
|
||||
tar.gz) tar -tzf "$1" >/dev/null 2>&1 ;;
|
||||
zip) unzip -tq "$1" >/dev/null 2>&1 ;;
|
||||
*) test -s "$1" ;;
|
||||
esac
|
||||
}
|
||||
download() {
|
||||
local output="$1"
|
||||
local url="$2"
|
||||
local type="$3"
|
||||
local attempt
|
||||
|
||||
for ((attempt = 1; attempt <= 3; attempt++)); do
|
||||
rm -f "${output}.tmp"
|
||||
if curl --fail --location --silent --show-error \
|
||||
--connect-timeout 30 --max-time 300 --output "${output}.tmp" "$url" \
|
||||
&& validate_download "${output}.tmp" "$type"; then
|
||||
mv "${output}.tmp" "$output"
|
||||
return 0
|
||||
fi
|
||||
if [ "$attempt" -lt 3 ]; then
|
||||
echo "Download of ${output} failed (attempt ${attempt}/3); retrying in 10s"
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
echo "::error::Unable to download a valid ${output} after 3 attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
r2ai_version="$(gh api /repos/radareorg/r2ai/releases/latest --jq .tag_name)"
|
||||
download radare2.pkg "https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{matrix.arch}}-${{env.R2V}}.pkg" pkg
|
||||
download r2ai.tar.gz "https://github.com/radareorg/r2ai/archive/refs/tags/${r2ai_version}.tar.gz" tar.gz
|
||||
download r2ghidra.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra-${{env.R2GHIDRA_VERSION}}-macos-${{matrix.arch_alt||matrix.arch}}.zip" zip
|
||||
download r2ghidra_sleigh.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra_sleigh-${{env.R2GHIDRA_VERSION}}.zip" zip
|
||||
make unpack-comps
|
||||
- name: install r2
|
||||
run: sudo installer -pkg dist/macos/radare2.pkg -target /
|
||||
@@ -165,55 +255,40 @@ jobs:
|
||||
name: iaito-macos-${{ matrix.arch }}
|
||||
path: dist/macos/iaito.zip
|
||||
meson:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04]
|
||||
python-version: [3.12.x]
|
||||
system-deps: [true]
|
||||
cc-override: [default]
|
||||
cxx-override: [default]
|
||||
# Prevent one job from pausing the rest
|
||||
fail-fast: false
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- name: apt dependencies
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgraphviz-dev mesa-common-dev ninja-build libqt5svg5-dev qttools5-dev qttools5-dev-tools qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools
|
||||
- name: install r2
|
||||
run: git clone --depth=1 https://github.com/radareorg/radare2 r2git && CFLAGS=-O0 r2git/sys/install.sh
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: install dependencies
|
||||
if: contains(matrix.os, 'macos')
|
||||
run: pip3 install meson ninja
|
||||
- name: install Qt
|
||||
if: contains(matrix.os, 'macos')
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{env.QT_MAC_INSTALL_VERSION}}
|
||||
- name: macos build
|
||||
if: contains(matrix.os, 'macos')
|
||||
run: |
|
||||
meson setup -Dwith_qt6=true build src
|
||||
ninja -C build -j4
|
||||
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2_${{env.R2V}}_amd64.deb
|
||||
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-dev_${{env.R2V}}_amd64.deb
|
||||
sudo dpkg -i *.deb
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: meson+ninja ubuntu
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
pip install meson
|
||||
meson setup build src
|
||||
ninja -C build -j4
|
||||
w64-meson:
|
||||
needs: source
|
||||
env:
|
||||
R2V: ${{ needs.source.outputs.r2 }}
|
||||
QT_WIN_INSTALL_VERSION: ${{ needs.source.outputs.qt_win }}
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.source.outputs.sha }}
|
||||
submodules: false
|
||||
persist-credentials: false
|
||||
- name: Preparing msvc toolchain
|
||||
@@ -233,6 +308,8 @@ jobs:
|
||||
curl -L -o r2.zip https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{env.R2V}}-w64.zip
|
||||
7z x r2.zip
|
||||
mv radare2-${{env.R2V}}-w64 radare2
|
||||
# XXX workaround for r2-6.2.4 headers not being msvc c++ friendly (fixed in r2 master)
|
||||
sed -i 's/(RStrs) { NULL, NULL }/r_strs_new (NULL, NULL)/' radare2/include/libr/r_cmd.h
|
||||
echo "%CD%\radare2\bin" >> $GITHUB_PATH
|
||||
- name: configure
|
||||
shell: cmd
|
||||
@@ -255,26 +332,28 @@ jobs:
|
||||
path: iaito.zip
|
||||
|
||||
check_release:
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/master') }}
|
||||
outputs:
|
||||
is_release: ${{ steps.release.outputs.is }}
|
||||
tag_name: ${{ steps.release.outputs.tag }}
|
||||
needs:
|
||||
- source
|
||||
- acr-linux
|
||||
- acr-macos
|
||||
- w64-meson
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.source.outputs.sha }}
|
||||
fetch-depth: 0 # Download all git history and tags
|
||||
- name: Check if is a release
|
||||
run: git describe --exact-match --tags ${{ github.sha }} | awk 'BEGIN{tag="-";r="no"}/^[0-9]+\.[0-9]+/{tag=$0;r="yes"};END{print "is="r;print "tag="tag}' >> ${GITHUB_OUTPUT}
|
||||
run: git describe --exact-match --tags HEAD | awk 'BEGIN{tag="-";r="no"}/^[0-9]+\.[0-9]+/{tag=$0;r="yes"};END{print "is="r;print "tag="tag}' >> ${GITHUB_OUTPUT}
|
||||
id: release
|
||||
|
||||
macos-sign:
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.check_release.outputs.is_release == 'yes' }}
|
||||
if: ${{ needs.check_release.outputs.is_release == 'yes' }}
|
||||
needs:
|
||||
- check_release
|
||||
runs-on: macos-latest
|
||||
@@ -291,6 +370,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: refs/tags/${{ needs.check_release.outputs.tag_name }}
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -379,21 +460,28 @@ jobs:
|
||||
run: security delete-keychain release.keychain
|
||||
|
||||
release:
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.check_release.outputs.is_release == 'yes' }}
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ needs.check_release.outputs.is_release == 'yes' }}
|
||||
needs:
|
||||
- macos-sign
|
||||
- check_release
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: refs/tags/${{ needs.check_release.outputs.tag_name }}
|
||||
fetch-depth: 0 # Download all git history and tags
|
||||
- name: Extract version
|
||||
run: echo "string=`./configure -qV`" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
- name: Prepare release notes
|
||||
run: ./scripts/release-notes.sh | tee ./RELEASE_NOTES.md
|
||||
env:
|
||||
RELEASE_TAG: ${{ needs.check_release.outputs.tag_name }}
|
||||
run: |
|
||||
previous=$(git describe --tags --abbrev=0 "${RELEASE_TAG}^")
|
||||
./scripts/release-notes.sh "$previous" "$RELEASE_TAG" | tee ./RELEASE_NOTES.md
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
@@ -409,10 +497,8 @@ jobs:
|
||||
tag_name: ${{ needs.check_release.outputs.tag_name }}
|
||||
release_name: ${{ steps.version.outputs.string }}
|
||||
body_path: ./RELEASE_NOTES.md
|
||||
draft: false
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Prepare release notes
|
||||
run: ./scripts/release-notes.sh | tee ./RELEASE_NOTES.md
|
||||
- name: Upload Linux asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
@@ -458,3 +544,10 @@ jobs:
|
||||
asset_path: dist/artifacts/iaito-w64.exe/iaito.exe
|
||||
asset_name: iaito-${{ steps.version.outputs.string }}.exe
|
||||
asset_content_type: application/zip
|
||||
- name: Publish complete release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_ID: ${{ steps.create_release.outputs.id }}
|
||||
run: |
|
||||
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
|
||||
-F draft=false -f make_latest=legacy --jq .html_url
|
||||
@@ -13,7 +13,7 @@ name: Semgrep
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Scan
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
||||
container:
|
||||
|
||||
@@ -3,6 +3,9 @@ AStyle*
|
||||
./config.mk
|
||||
config.mk
|
||||
|
||||
# Generated configuration header (qmake / Meson)
|
||||
IaitoConfig.h
|
||||
|
||||
# OSX files
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
include config.mk
|
||||
-include config.mk
|
||||
|
||||
ifeq ($(shell uname),Darwin)
|
||||
BIN=build/iaito.app/Contents/MacOS/iaito
|
||||
@@ -128,7 +128,11 @@ endif
|
||||
user-uninstall:
|
||||
$(MAKE) uninstall DESTDIR=/ PREFIX=${HOME}/.local MANDIR=${HOME}/.local/share/man
|
||||
|
||||
ifneq ($(shell uname),Darwin)
|
||||
run: install-desktop-user
|
||||
endif
|
||||
|
||||
run:
|
||||
rarun2 libpath=$(shell r2 -H R2_LIBDIR) program=$(BIN)
|
||||
|
||||
install-desktop-user:
|
||||
|
||||
@@ -83,6 +83,9 @@ make
|
||||
make run
|
||||
```
|
||||
|
||||
On macOS, `make run` launches the app from `build/iaito.app` without installing it.
|
||||
Registering the `iaito://` URI handler is optional; see [deep links](doc/deeplinks.md#macos).
|
||||
|
||||
To install the app in your home:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -129,12 +129,12 @@ done
|
||||
: ${INSTALL_PROGRAM_STRIP:=${INSTALL} -m 755 -s}
|
||||
: ${INSTALL_MAN:=${INSTALL} -m 444}
|
||||
: ${INSTALL_LIB:=${INSTALL} -m 755 -c}
|
||||
PKGNAME='iaito' ; VERSION='6.2.0' ; VERSION_MAJOR=6; VERSION_MINOR=2; VERSION_PATCH=0; VERSION_NUMBER=60200; CONTACT_MAIL="[email protected]" ; CONTACT_NAME="pancake" ; CONTACT="pancake <[email protected]>" ;
|
||||
PKGNAME='iaito' ; VERSION='6.2.6' ; VERSION_MAJOR=6; VERSION_MINOR=2; VERSION_PATCH=6; VERSION_NUMBER=60206; CONTACT_MAIL="[email protected]" ; CONTACT_NAME="pancake" ; CONTACT="pancake <[email protected]>" ;
|
||||
}
|
||||
|
||||
show_usage() {
|
||||
cat <<EOF2
|
||||
'configure' configures iaito-6.2.0 to adapt to many kinds of systems.
|
||||
'configure' configures iaito-6.2.6 to adapt to many kinds of systems.
|
||||
|
||||
Usage: ./configure [OPTION]... [VAR=VALUE]...
|
||||
|
||||
@@ -212,10 +212,10 @@ ocho() {
|
||||
|
||||
show_version() {
|
||||
if [ "$QUIET" = 1 ]; then
|
||||
echo "6.2.0"
|
||||
echo "6.2.6"
|
||||
exit 0
|
||||
fi
|
||||
echo "iaito-6.2.0 configuration script done with acr v2.2.8.
|
||||
echo "iaito-6.2.6 configuration script done with acr v2.2.8.
|
||||
The 'Free Software Foundation' message is only for autodetection.
|
||||
Originally written by pancake <nopcode.org>."
|
||||
exit 0
|
||||
@@ -244,7 +244,7 @@ case $flag in
|
||||
show_version ; ;;
|
||||
-r|--r|--report)
|
||||
echo "PKGNAME: iaito"
|
||||
echo "VERSION: 6.2.0"
|
||||
echo "VERSION: 6.2.6"
|
||||
echo "AUTHOR: pancake"
|
||||
echo "EMAIL: [email protected]"
|
||||
echo "LANGS: c++"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
PKGNAME iaito
|
||||
VERSION 6.2.0
|
||||
VERSION 6.2.6
|
||||
CONTACT pancake ; [email protected]
|
||||
|
||||
LANG_CXX!
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Data Analysis
|
||||
|
||||
Open **View → Data → Analysis**. The dock is hidden in the default layout and when
|
||||
first added to an older saved layout. Hidden docks do not sample data. **Zoom** is
|
||||
also available under **View → Data**.
|
||||
|
||||
- **Algorithms** enables multiple byte metrics. Hover over a named legend entry
|
||||
for its definition; click it to edit its color or visibility. Clicking the legend
|
||||
does not remove the series.
|
||||
- **Options → Resolution (buckets)** selects 32, 64, 128, 256, 512, 1024, 2048 or
|
||||
4096 buckets. Lower values give coarser graphs and less work. The default is 1024.
|
||||
- **Options** also controls lines, filled lines or vertical bars, separate tracks,
|
||||
greyscale/rainbow/theme colors, custom colors, and structural markers.
|
||||
- Click the graph to seek. Drag to select bytes. Wheel or `+`/`-` zooms;
|
||||
Shift+wheel or the arrow keys pans. `Home` fits all data, and `Esc` clears selection.
|
||||
- The bottom entropy overview shows the complete address span. Drag its window
|
||||
to pan, drag its edges to resize the range, or double-click to fit all data.
|
||||
- **Range** fits the current section, segment, map, block or selection.
|
||||
**Edit range** accepts radare2 expressions and inclusive endpoints.
|
||||
- **Follow** keeps the current seek address visible. Offset synchronization and
|
||||
the global address-selection synchronization setting control integration with
|
||||
disassembly and hexdump widgets.
|
||||
- **Find marker** searches sections, segments, maps, flags (including search hits)
|
||||
and comments. The graph context menu provides comments, flags and “Show in”.
|
||||
Hover details appear after the pointer rests for 650 ms.
|
||||
- Hexdump selections, sections and segments have direct analysis actions.
|
||||
Other addressable widgets can use **Show in → Data Analysis**.
|
||||
|
||||
The graph uses the current radare2 address space: virtual addresses with `io.va`
|
||||
enabled, otherwise physical offsets in the active descriptor. Gaps and failed reads
|
||||
do not contribute invented bytes. Each bucket reads at most 4096 bytes from centered
|
||||
windows in readable spans. Tooltips, the status line and exported CSV report how
|
||||
many bytes were actually read. Zoom in for complete coverage of a region. Entropy
|
||||
is measured per bucket, so very small buckets have a lower attainable entropy.
|
||||
|
||||
Multiple overlaid series use a normalized percentage scale. Entropy's native scale
|
||||
is 0–8 bits/byte, unique bytes 0–256, and mean byte 0–255. A single series or separate
|
||||
tracks uses its native scale. CSV exports normalized values plus exact bucket bounds
|
||||
and coverage. Display settings are saved with the layout.
|
||||
|
||||
Other widgets can call `MainWindow::showDataAnalysis(start, end)` or
|
||||
`DataAnalysisWidget::showRange(start, end)` with **inclusive** endpoints. Internally,
|
||||
`DataAnalysis::Range` and graph signals use half-open ranges.
|
||||
|
||||
Run the focused tests with:
|
||||
|
||||
```sh
|
||||
make -C tests/dataanalysis -j8 > /dev/null
|
||||
QT_QPA_PLATFORM=offscreen tests/dataanalysis/build/test_dataanalysis
|
||||
```
|
||||
@@ -210,6 +210,13 @@ The app bundle declares the scheme in `Info.plist` via `CFBundleURLTypes`
|
||||
the bundle is installed; `scripts/deeplink/register.sh` forces a refresh with
|
||||
`lsregister -f <iaito.app>`. The handler is dropped when the bundle is removed.
|
||||
|
||||
`make run` launches the build without registering the URI handler. To optionally
|
||||
register the local bundle without installing it, run:
|
||||
|
||||
```sh
|
||||
sh scripts/deeplink/register.sh "$PWD/build/iaito.app"
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
`scripts/deeplink/register.bat` writes the `HKCU\Software\Classes\iaito` keys
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Structures in the hexdump
|
||||
|
||||
Open the hexdump sidebar's **Structures** tab, choose an r2 type, enter an address
|
||||
or expression, and press **Inspect**. **At cursor** uses the first selected byte
|
||||
or the current cursor. You can also right-click the hexdump and choose **Inspect
|
||||
structure here**; a type already linked to that address is selected automatically.
|
||||
|
||||
The tree shows each field's declared type, value, address, and size. Expand nested
|
||||
structures, unions, arrays, and pointers to explore them. Selecting a field selects
|
||||
its bytes in the hexdump. Field colors match the byte highlights. The inspected
|
||||
structure stays anchored while you navigate, and **Refresh** preserves expanded
|
||||
fields and the selected row while rereading memory and type definitions.
|
||||
|
||||
Types come from r2's type database, including imported C definitions and typedefs.
|
||||
The tree uses stored member offsets and the current `asm.bits` and `cfg.bigendian`
|
||||
settings. Large arrays are divided into expandable groups. Unmapped pointers and
|
||||
recursive references stop expansion; invalid or overflowing layouts are reported.
|
||||
Array expressions such as `uint16_t[16]` can be inspected directly.
|
||||
|
||||
**Pin type** creates an r2 type link (`tl`) at the inspected address. The pinned
|
||||
address list also includes existing type links and command annotations (`Cr`) from
|
||||
the current r2 session. Define a named typedef before pinning an array expression.
|
||||
|
||||
**Pin command…** stores a command at the address using the selected byte range or
|
||||
the chosen type's size. Double-click a pin to navigate to its address. Select a
|
||||
command pin and press **Run command** to display its output. **Unpin** removes only
|
||||
the selected type link or command annotation. Pins use r2's existing project data
|
||||
and are saved with the project. iaito also writes `iaito-memory-pins.json` inside the
|
||||
project directory to preserve types and pins omitted by some r2 binary project versions;
|
||||
keep that companion file when copying a project.
|
||||
+36
-4
@@ -4,7 +4,7 @@ TARGET = iaito
|
||||
|
||||
IAITO_VERSION_MAJOR = 6
|
||||
IAITO_VERSION_MINOR = 2
|
||||
IAITO_VERSION_PATCH = 0
|
||||
IAITO_VERSION_PATCH = 6
|
||||
|
||||
CONFIG += c++20
|
||||
QMAKE_CXXFLAGS += -std=c++20
|
||||
@@ -164,6 +164,8 @@ IAITO_R2GHIDRA_STATIC {
|
||||
QMAKE_SUBSTITUTES += IaitoConfig.h.in
|
||||
|
||||
SOURCES += \
|
||||
common/ExceptionInfo.cpp \
|
||||
widgets/ExceptionBar.cpp \
|
||||
Main.cpp \
|
||||
core/Iaito.cpp \
|
||||
dialogs/EditStringDialog.cpp \
|
||||
@@ -204,6 +206,7 @@ SOURCES += \
|
||||
widgets/ExportsWidget.cpp \
|
||||
widgets/FlagsWidget.cpp \
|
||||
widgets/FunctionsWidget.cpp \
|
||||
widgets/GlobalVariablesWidget.cpp \
|
||||
widgets/ImportsWidget.cpp \
|
||||
widgets/CodeEditor.cpp \
|
||||
widgets/Omnibar.cpp \
|
||||
@@ -230,6 +233,7 @@ SOURCES += \
|
||||
common/SvgIconEngine.cpp \
|
||||
common/SyntaxHighlighter.cpp \
|
||||
widgets/DecompilerWidget.cpp \
|
||||
widgets/DecompilerTextEdit.cpp \
|
||||
widgets/VisualNavbar.cpp \
|
||||
widgets/GraphView.cpp \
|
||||
dialogs/settings/SettingsDialog.cpp \
|
||||
@@ -243,6 +247,7 @@ SOURCES += \
|
||||
widgets/VTablesWidget.cpp \
|
||||
widgets/TypesWidget.cpp \
|
||||
widgets/HeadersWidget.cpp \
|
||||
widgets/InterfacesWidget.cpp \
|
||||
widgets/SearchWidget.cpp \
|
||||
IaitoApplication.cpp \
|
||||
common/DeepLink.cpp \
|
||||
@@ -254,6 +259,7 @@ SOURCES += \
|
||||
widgets/BinariesWidget.cpp \
|
||||
widgets/FilesWidget.cpp \
|
||||
widgets/FilesystemWidget.cpp \
|
||||
common/SourceLineReference.cpp \
|
||||
widgets/GraphWidget.cpp \
|
||||
widgets/OverviewWidget.cpp \
|
||||
common/JsonTreeItem.cpp \
|
||||
@@ -313,6 +319,8 @@ SOURCES += \
|
||||
common/HighDpiPixmap.cpp \
|
||||
widgets/GraphGridLayout.cpp \
|
||||
widgets/HexWidget.cpp \
|
||||
widgets/HexPatWidget.cpp \
|
||||
widgets/AddressScrollBar.cpp \
|
||||
widgets/R2AIWidget.cpp \
|
||||
widgets/R2McpWidget.cpp \
|
||||
common/SelectionHighlight.cpp \
|
||||
@@ -331,6 +339,8 @@ SOURCES += \
|
||||
common/R2retdecDecompiler.cpp \
|
||||
menus/AddressableItemContextMenu.cpp \
|
||||
common/AddressableItemModel.cpp \
|
||||
common/NameListModel.cpp \
|
||||
widgets/NameListView.cpp \
|
||||
widgets/ListDockWidget.cpp \
|
||||
dialogs/MultitypeFileSaveDialog.cpp \
|
||||
widgets/BoolToggleDelegate.cpp \
|
||||
@@ -353,12 +363,18 @@ SOURCES += \
|
||||
dialogs/AssemblerDialog.cpp \
|
||||
dialogs/ScriptManagerDialog.cpp \
|
||||
dialogs/ScriptManagerWidget.cpp \
|
||||
widgets/ZoomWidget.cpp
|
||||
widgets/ZoomWidget.cpp \
|
||||
common/DataAnalysis.cpp \
|
||||
common/TypeLayout.cpp \
|
||||
widgets/DataAnalysisView.cpp \
|
||||
widgets/DataAnalysisWidget.cpp
|
||||
|
||||
GRAPHVIZ_SOURCES = \
|
||||
widgets/GraphvizLayout.cpp
|
||||
|
||||
HEADERS += \
|
||||
common/ExceptionInfo.h \
|
||||
widgets/ExceptionBar.h \
|
||||
common/R2Shims.h \
|
||||
core/Iaito.h \
|
||||
core/IaitoCommon.h \
|
||||
@@ -406,6 +422,7 @@ HEADERS += \
|
||||
widgets/ExportsWidget.h \
|
||||
widgets/FlagsWidget.h \
|
||||
widgets/FunctionsWidget.h \
|
||||
widgets/GlobalVariablesWidget.h \
|
||||
widgets/ImportsWidget.h \
|
||||
widgets/Omnibar.h \
|
||||
widgets/RelocsWidget.h \
|
||||
@@ -419,6 +436,7 @@ HEADERS += \
|
||||
widgets/CodeInfoWidget.h \
|
||||
menus/DisassemblyContextMenu.h \
|
||||
menus/DecompilerContextMenu.h \
|
||||
menus/ContextMenuIcons.h \
|
||||
menus/ColorPickerMenu.h \
|
||||
menus/PinPickerMenu.h \
|
||||
widgets/DisassemblyWidget.h \
|
||||
@@ -432,6 +450,7 @@ HEADERS += \
|
||||
common/SvgIconEngine.h \
|
||||
common/SyntaxHighlighter.h \
|
||||
widgets/DecompilerWidget.h \
|
||||
widgets/DecompilerTextEdit.h \
|
||||
widgets/VisualNavbar.h \
|
||||
widgets/GraphView.h \
|
||||
dialogs/settings/SettingsDialog.h \
|
||||
@@ -448,6 +467,7 @@ HEADERS += \
|
||||
widgets/VTablesWidget.h \
|
||||
widgets/TypesWidget.h \
|
||||
widgets/HeadersWidget.h \
|
||||
widgets/InterfacesWidget.h \
|
||||
widgets/SearchWidget.h \
|
||||
dialogs/R2PluginsDialog.h \
|
||||
widgets/IaitoDockWidget.h \
|
||||
@@ -456,6 +476,7 @@ HEADERS += \
|
||||
widgets/BinariesWidget.h \
|
||||
widgets/FilesWidget.h \
|
||||
widgets/FilesystemWidget.h \
|
||||
common/SourceLineReference.h \
|
||||
widgets/GraphWidget.h \
|
||||
widgets/OverviewWidget.h \
|
||||
common/JsonTreeItem.h \
|
||||
@@ -523,6 +544,9 @@ HEADERS += \
|
||||
widgets/GraphLayout.h \
|
||||
widgets/GraphGridLayout.h \
|
||||
widgets/HexWidget.h \
|
||||
widgets/HexPatWidget.h \
|
||||
common/SparseHexLayout.h \
|
||||
widgets/AddressScrollBar.h \
|
||||
widgets/R2AIWidget.h \
|
||||
widgets/R2McpWidget.h \
|
||||
common/SelectionHighlight.h \
|
||||
@@ -538,6 +562,8 @@ HEADERS += \
|
||||
common/R2retdecDecompiler.h \
|
||||
menus/AddressableItemContextMenu.h \
|
||||
common/AddressableItemModel.h \
|
||||
common/NameListModel.h \
|
||||
widgets/NameListView.h \
|
||||
widgets/ListDockWidget.h \
|
||||
widgets/AddressableItemList.h \
|
||||
dialogs/MultitypeFileSaveDialog.h \
|
||||
@@ -563,11 +589,17 @@ HEADERS += \
|
||||
dialogs/AssemblerDialog.h \
|
||||
dialogs/ScriptManagerDialog.h \
|
||||
dialogs/ScriptManagerWidget.h \
|
||||
widgets/ZoomWidget.h
|
||||
widgets/ZoomWidget.h \
|
||||
common/DataAnalysis.h \
|
||||
common/TypeLayout.h \
|
||||
widgets/DataAnalysisView.h \
|
||||
widgets/DataAnalysisWidget.h
|
||||
|
||||
GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h
|
||||
|
||||
FORMS += \
|
||||
widgets/GlobalVariablesWidget.ui \
|
||||
widgets/DataAnalysisWidget.ui \
|
||||
dialogs/AboutDialog.ui \
|
||||
dialogs/EditStringDialog.ui \
|
||||
dialogs/Base64EnDecodedWriteDialog.ui \
|
||||
@@ -591,6 +623,7 @@ FORMS += \
|
||||
widgets/FlagsWidget.ui \
|
||||
widgets/StringsWidget.ui \
|
||||
widgets/HexdumpWidget.ui \
|
||||
widgets/HexPatWidget.ui \
|
||||
dialogs/SaveProjectDialog.ui \
|
||||
dialogs/settings/SettingsDialog.ui \
|
||||
dialogs/settings/AppearanceOptionsWidget.ui \
|
||||
@@ -623,7 +656,6 @@ FORMS += \
|
||||
dialogs/SetToDataDialog.ui \
|
||||
dialogs/EditVariablesDialog.ui \
|
||||
widgets/IaitoTreeView.ui \
|
||||
widgets/ComboQuickFilterView.ui \
|
||||
dialogs/HexdumpRangeDialog.ui \
|
||||
dialogs/WelcomeDialog.ui \
|
||||
dialogs/EditMethodDialog.ui \
|
||||
|
||||
@@ -172,8 +172,7 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
#endif
|
||||
#endif
|
||||
if (!parseCommandLineOptions()) {
|
||||
QCoreApplication::exit();
|
||||
// std::exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!versionCheck()) {
|
||||
@@ -338,9 +337,11 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
|
||||
IaitoApplication::~IaitoApplication()
|
||||
{
|
||||
SamplesDB::waitForPending();
|
||||
Plugins()->destroyPlugins();
|
||||
delete mainWindow;
|
||||
if (mainWindow) {
|
||||
SamplesDB::waitForPending();
|
||||
Plugins()->destroyPlugins();
|
||||
delete mainWindow;
|
||||
}
|
||||
}
|
||||
|
||||
void IaitoApplication::launchNewInstance(const QStringList &args)
|
||||
@@ -466,8 +467,7 @@ bool IaitoApplication::parseCommandLineOptions()
|
||||
cmd_parser.addPositionalArgument("filename", QObject::tr("Filename to open."));
|
||||
|
||||
QCommandLineOption environmentOption(
|
||||
"H",
|
||||
QObject::tr("List environment variables, or print the value of a selected variable."));
|
||||
"H", QObject::tr("List environment variables, or print the value of a selected variable."));
|
||||
cmd_parser.addOption(environmentOption);
|
||||
|
||||
QCommandLineOption analOption(
|
||||
@@ -491,6 +491,10 @@ bool IaitoApplication::parseCommandLineOptions()
|
||||
QObject::tr("base address"));
|
||||
cmd_parser.addOption(baddrOption);
|
||||
|
||||
QCommandLineOption mapaddrOption(
|
||||
{"m", "map"}, QObject::tr("Map the file at a specific address"), QObject::tr("map address"));
|
||||
cmd_parser.addOption(mapaddrOption);
|
||||
|
||||
QCommandLineOption scriptOption("i", QObject::tr("Run script file"), QObject::tr("file"));
|
||||
cmd_parser.addOption(scriptOption);
|
||||
|
||||
@@ -532,7 +536,16 @@ bool IaitoApplication::parseCommandLineOptions()
|
||||
cmd_parser.addOption(debugOption);
|
||||
#endif
|
||||
|
||||
cmd_parser.process(*this);
|
||||
// process() exits without destroying QApplication for --version.
|
||||
const bool parsed = cmd_parser.parse(arguments());
|
||||
if (parsed && cmd_parser.isSet("version")) {
|
||||
printf("%s %s\n", qPrintable(applicationName()), qPrintable(applicationVersion()));
|
||||
exitCode = 0;
|
||||
return false;
|
||||
}
|
||||
if (!parsed || cmd_parser.isSet("help") || cmd_parser.isSet("help-all")) {
|
||||
cmd_parser.process(*this);
|
||||
}
|
||||
|
||||
IaitoCommandLineOptions opts;
|
||||
opts.args = cmd_parser.positionalArguments();
|
||||
@@ -592,6 +605,13 @@ bool IaitoApplication::parseCommandLineOptions()
|
||||
opts.fileOpenOptions.binLoadAddr = baddr;
|
||||
}
|
||||
}
|
||||
if (cmd_parser.isSet(mapaddrOption)) {
|
||||
bool ok;
|
||||
RVA mapaddr = cmd_parser.value(mapaddrOption).toULongLong(&ok, 0);
|
||||
if (ok) {
|
||||
opts.fileOpenOptions.mapAddr = mapaddr;
|
||||
}
|
||||
}
|
||||
switch (opts.analLevel) {
|
||||
case AutomaticAnalysisLevel::Ask:
|
||||
break;
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
~IaitoApplication();
|
||||
|
||||
MainWindow *getMainWindow() { return mainWindow; }
|
||||
int getExitCode() const { return exitCode; }
|
||||
|
||||
void launchNewInstance(const QStringList &args = {});
|
||||
|
||||
@@ -51,7 +52,8 @@ private:
|
||||
|
||||
private:
|
||||
bool m_FileAlreadyDropped;
|
||||
MainWindow *mainWindow;
|
||||
MainWindow *mainWindow = nullptr;
|
||||
int exitCode = 1;
|
||||
IaitoCommandLineOptions clOptions;
|
||||
};
|
||||
|
||||
|
||||
+5
-1
@@ -42,7 +42,8 @@ static bool printEnvironmentVariables(int argc, char *argv[])
|
||||
}
|
||||
extraPluginDirs.append(environmentPluginDirs);
|
||||
}
|
||||
struct EnvironmentVariable {
|
||||
struct EnvironmentVariable
|
||||
{
|
||||
const char *name;
|
||||
QByteArray value;
|
||||
};
|
||||
@@ -153,6 +154,9 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
|
||||
IaitoApplication a(argc, argv);
|
||||
if (!a.getMainWindow()) {
|
||||
return a.getExitCode();
|
||||
}
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
|
||||
@@ -12,17 +12,20 @@ AddressableFilterProxyModel::AddressableFilterProxyModel(
|
||||
: AddressableItemModel<QSortFilterProxyModel>(parent)
|
||||
{
|
||||
setSourceModel(sourceModel);
|
||||
addressableSourceModel = sourceModel;
|
||||
}
|
||||
|
||||
RVA AddressableFilterProxyModel::address(const QModelIndex &index) const
|
||||
{
|
||||
return addressableSourceModel->address(this->mapToSource(index));
|
||||
const auto source = mapToSource(index);
|
||||
return addressableSourceModel && source.isValid() ? addressableSourceModel->address(source)
|
||||
: RVA_INVALID;
|
||||
}
|
||||
|
||||
QString AddressableFilterProxyModel::name(const QModelIndex &index) const
|
||||
{
|
||||
return addressableSourceModel->name(this->mapToSource(index));
|
||||
const auto source = mapToSource(index);
|
||||
return addressableSourceModel && source.isValid() ? addressableSourceModel->name(source)
|
||||
: QString();
|
||||
}
|
||||
|
||||
void AddressableFilterProxyModel::setSourceModel(QAbstractItemModel *)
|
||||
@@ -32,6 +35,45 @@ void AddressableFilterProxyModel::setSourceModel(QAbstractItemModel *)
|
||||
|
||||
void AddressableFilterProxyModel::setSourceModel(AddressableItemModelI *sourceModel)
|
||||
{
|
||||
ParentClass::setSourceModel(sourceModel->asItemModel());
|
||||
addressableSourceModel = sourceModel;
|
||||
ParentClass::setSourceModel(sourceModel ? sourceModel->asItemModel() : nullptr);
|
||||
}
|
||||
|
||||
void AddressableFilterProxyModel::setTypeFilterColumn(int column)
|
||||
{
|
||||
if (typeColumn != column) {
|
||||
emit filterAboutToChange();
|
||||
typeColumn = column;
|
||||
invalidateFilter();
|
||||
emit filterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AddressableFilterProxyModel::setTypeFilter(const QVariant &type)
|
||||
{
|
||||
if (selectedType != type) {
|
||||
emit filterAboutToChange();
|
||||
selectedType = type;
|
||||
invalidateFilter();
|
||||
emit filterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AddressableFilterProxyModel::setFilterWildcard(const QString &pattern)
|
||||
{
|
||||
emit filterAboutToChange();
|
||||
ParentClass::setFilterWildcard(pattern);
|
||||
emit filterChanged();
|
||||
}
|
||||
|
||||
bool AddressableFilterProxyModel::filterAcceptsType(int row, const QModelIndex &parent) const
|
||||
{
|
||||
return typeColumn < 0 || !selectedType.isValid()
|
||||
|| sourceModel()->index(row, typeColumn, parent).data().toString()
|
||||
== selectedType.toString();
|
||||
}
|
||||
|
||||
bool AddressableFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
|
||||
{
|
||||
return filterAcceptsType(row, parent) && ParentClass::filterAcceptsRow(row, parent);
|
||||
}
|
||||
@@ -43,6 +43,8 @@ public:
|
||||
|
||||
class IAITO_EXPORT AddressableFilterProxyModel : public AddressableItemModel<QSortFilterProxyModel>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using ParentClass = AddressableItemModel<QSortFilterProxyModel>;
|
||||
|
||||
public:
|
||||
@@ -51,10 +53,26 @@ public:
|
||||
RVA address(const QModelIndex &index) const override;
|
||||
QString name(const QModelIndex &) const override;
|
||||
void setSourceModel(AddressableItemModelI *sourceModel);
|
||||
void setTypeFilterColumn(int column);
|
||||
void setTypeFilter(const QVariant &type);
|
||||
QVariant typeFilter() const { return selectedType; }
|
||||
|
||||
public slots:
|
||||
void setFilterWildcard(const QString &pattern);
|
||||
|
||||
signals:
|
||||
void filterAboutToChange();
|
||||
void filterChanged();
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int row, const QModelIndex &parent) const override;
|
||||
bool filterAcceptsType(int row, const QModelIndex &parent) const;
|
||||
|
||||
private:
|
||||
void setSourceModel(QAbstractItemModel *sourceModel) override; // Don't use this directly
|
||||
AddressableItemModelI *addressableSourceModel;
|
||||
AddressableItemModelI *addressableSourceModel = nullptr;
|
||||
int typeColumn = -1;
|
||||
QVariant selectedType;
|
||||
};
|
||||
|
||||
#endif // ADDRESSABLEITEMMODEL_H
|
||||
@@ -47,7 +47,11 @@ static inline int variantTypeId(const QVariant &v)
|
||||
constexpr int VisualNavbarThicknessDefault = 15;
|
||||
constexpr int VisualNavbarThicknessMin = 8;
|
||||
constexpr int VisualNavbarThicknessMax = 128;
|
||||
constexpr int VisualNavbarResolutionMax = 32;
|
||||
constexpr const char *AddressRangeSelectionSyncKey = "syncAddressRangeSelection";
|
||||
constexpr const char *DisassemblyJumpLineAdaptiveSpacingKey = "disassemblyJumpLineAdaptiveSpacing";
|
||||
constexpr const char *DisassemblyJumpLineSpacingKey = "disassemblyJumpLineSpacing";
|
||||
constexpr int DisassemblyJumpLineSpacingDefault = 20;
|
||||
} // namespace
|
||||
|
||||
/* Map with names of themes associated with its color palette
|
||||
@@ -585,13 +589,13 @@ const QFont Configuration::getFont() const
|
||||
return font;
|
||||
}
|
||||
|
||||
const QFont Configuration::getSmallFont() const
|
||||
const QFont Configuration::getSmallFont(bool zoomed) const
|
||||
{
|
||||
if (IaitoStyle::instance() && IaitoStyle::instance()->theme().fonts.custom) {
|
||||
return getFont();
|
||||
return zoomed ? getFont() : getBaseFont();
|
||||
}
|
||||
QFont font = getBaseFont();
|
||||
qreal eff = getZoomFactor() * 0.85;
|
||||
qreal eff = (zoomed ? getZoomFactor() : 1.0) * 0.85;
|
||||
if (font.pixelSize() > 0) {
|
||||
font.setPixelSize(qMax(int(font.pixelSize() * eff), 8));
|
||||
} else {
|
||||
@@ -665,6 +669,45 @@ void Configuration::setVisualNavbarThickness(int thickness)
|
||||
emit visualNavbarThicknessChanged(thickness);
|
||||
}
|
||||
|
||||
int Configuration::getVisualNavbarResolution() const
|
||||
{
|
||||
return qBound(1, s.value("visualNavbarResolution", 1).toInt(), VisualNavbarResolutionMax);
|
||||
}
|
||||
|
||||
void Configuration::setVisualNavbarResolution(int divisor)
|
||||
{
|
||||
divisor = qBound(1, divisor, VisualNavbarResolutionMax);
|
||||
if (getVisualNavbarResolution() == divisor) {
|
||||
return;
|
||||
}
|
||||
s.setValue("visualNavbarResolution", divisor);
|
||||
emit visualNavbarResolutionChanged(divisor);
|
||||
}
|
||||
|
||||
Configuration::MemoryScrollBarMode Configuration::getMemoryScrollBarMode() const
|
||||
{
|
||||
const int mode
|
||||
= s.value("memoryScrollBarMode", static_cast<int>(MemoryScrollBarMode::Bounded)).toInt();
|
||||
switch (mode) {
|
||||
case static_cast<int>(MemoryScrollBarMode::Spring):
|
||||
return MemoryScrollBarMode::Spring;
|
||||
case static_cast<int>(MemoryScrollBarMode::Hidden):
|
||||
return MemoryScrollBarMode::Hidden;
|
||||
case static_cast<int>(MemoryScrollBarMode::Bounded):
|
||||
default:
|
||||
return MemoryScrollBarMode::Bounded;
|
||||
}
|
||||
}
|
||||
|
||||
void Configuration::setMemoryScrollBarMode(MemoryScrollBarMode mode)
|
||||
{
|
||||
if (getMemoryScrollBarMode() == mode) {
|
||||
return;
|
||||
}
|
||||
s.setValue("memoryScrollBarMode", static_cast<int>(mode));
|
||||
emit memoryScrollBarModeChanged(mode);
|
||||
}
|
||||
|
||||
QString Configuration::getLastThemeOf(const IaitoInterfaceTheme &currInterfaceTheme) const
|
||||
{
|
||||
return s.value("lastThemeOf." + currInterfaceTheme.name, Config()->getColorTheme()).toString();
|
||||
@@ -878,6 +921,30 @@ void Configuration::resetToDefaultAsmOptions()
|
||||
for (auto it = asmOptions.cbegin(); it != asmOptions.cend(); it++) {
|
||||
setConfig(it.key(), it.value());
|
||||
}
|
||||
setDisassemblyJumpLineAdaptiveSpacingEnabled(true);
|
||||
setDisassemblyJumpLineSpacing(DisassemblyJumpLineSpacingDefault);
|
||||
}
|
||||
|
||||
bool Configuration::getDisassemblyJumpLineAdaptiveSpacingEnabled() const
|
||||
{
|
||||
return s.value(DisassemblyJumpLineAdaptiveSpacingKey, true).toBool();
|
||||
}
|
||||
|
||||
void Configuration::setDisassemblyJumpLineAdaptiveSpacingEnabled(bool enabled)
|
||||
{
|
||||
s.setValue(DisassemblyJumpLineAdaptiveSpacingKey, enabled);
|
||||
}
|
||||
|
||||
int Configuration::getDisassemblyJumpLineSpacing() const
|
||||
{
|
||||
const int spacing
|
||||
= s.value(DisassemblyJumpLineSpacingKey, DisassemblyJumpLineSpacingDefault).toInt();
|
||||
return spacing > 0 ? spacing : DisassemblyJumpLineSpacingDefault;
|
||||
}
|
||||
|
||||
void Configuration::setDisassemblyJumpLineSpacing(int spacing)
|
||||
{
|
||||
s.setValue(DisassemblyJumpLineSpacingKey, qMax(1, spacing));
|
||||
}
|
||||
|
||||
void Configuration::applySavedAsmOptions()
|
||||
@@ -1097,6 +1164,16 @@ void Configuration::setDecompilerShowAddresses(bool enabled)
|
||||
s.setValue("decompilerShowAddresses", enabled);
|
||||
}
|
||||
|
||||
bool Configuration::getDecompilerShowDisassembly()
|
||||
{
|
||||
return s.value("decompilerShowDisassembly", false).toBool();
|
||||
}
|
||||
|
||||
void Configuration::setDecompilerShowDisassembly(bool enabled)
|
||||
{
|
||||
s.setValue("decompilerShowDisassembly", enabled);
|
||||
}
|
||||
|
||||
bool Configuration::getBitmapTransparentState()
|
||||
{
|
||||
return s.value("bitmapGraphExportTransparency", false).value<bool>();
|
||||
|
||||
@@ -36,6 +36,8 @@ class IAITO_EXPORT Configuration : public QObject
|
||||
public:
|
||||
enum class VisualNavbarLocation { Top = 0, Bottom, Left, Right, SuperTop, SuperBottom };
|
||||
Q_ENUM(VisualNavbarLocation)
|
||||
enum class MemoryScrollBarMode { Spring = 0, Bounded, Hidden };
|
||||
Q_ENUM(MemoryScrollBarMode)
|
||||
|
||||
private:
|
||||
QPalette nativePalette;
|
||||
@@ -118,8 +120,9 @@ public:
|
||||
* @brief Gets a smaller variant of the configured font, suitable for
|
||||
* list/tree views. Uses 85% of the normal font size.
|
||||
* @return the configured font at reduced size, adjusted by zoom level
|
||||
* unless zoomed is false
|
||||
*/
|
||||
const QFont getSmallFont() const;
|
||||
const QFont getSmallFont(bool zoomed = true) const;
|
||||
void setFont(const QFont &font);
|
||||
qreal getZoomFactor() const;
|
||||
void setZoomFactor(qreal zoom);
|
||||
@@ -127,6 +130,10 @@ public:
|
||||
void setVisualNavbarLocation(VisualNavbarLocation location);
|
||||
int getVisualNavbarThickness() const;
|
||||
void setVisualNavbarThickness(int thickness);
|
||||
int getVisualNavbarResolution() const;
|
||||
void setVisualNavbarResolution(int divisor);
|
||||
MemoryScrollBarMode getMemoryScrollBarMode() const;
|
||||
void setMemoryScrollBarMode(MemoryScrollBarMode mode);
|
||||
|
||||
// Colors
|
||||
bool windowColorIsDark();
|
||||
@@ -167,6 +174,10 @@ public:
|
||||
|
||||
// Asm Options
|
||||
void resetToDefaultAsmOptions();
|
||||
bool getDisassemblyJumpLineAdaptiveSpacingEnabled() const;
|
||||
void setDisassemblyJumpLineAdaptiveSpacingEnabled(bool enabled);
|
||||
int getDisassemblyJumpLineSpacing() const;
|
||||
void setDisassemblyJumpLineSpacing(int spacing);
|
||||
|
||||
QString getColorTheme() const { return s.value("theme", "iaito").toString(); }
|
||||
void setColorTheme(const QString &theme);
|
||||
@@ -225,6 +236,8 @@ public:
|
||||
void setDecompilerRunInBackground(bool enabled);
|
||||
bool getDecompilerShowAddresses();
|
||||
void setDecompilerShowAddresses(bool enabled);
|
||||
bool getDecompilerShowDisassembly();
|
||||
void setDecompilerShowDisassembly(bool enabled);
|
||||
|
||||
bool getAddressRangeSelectionSyncEnabled() const;
|
||||
void setAddressRangeSelectionSyncEnabled(bool enabled);
|
||||
@@ -286,6 +299,8 @@ signals:
|
||||
void interfaceThemeChanged();
|
||||
void visualNavbarLocationChanged(VisualNavbarLocation location);
|
||||
void visualNavbarThicknessChanged(int thickness);
|
||||
void visualNavbarResolutionChanged(int divisor);
|
||||
void memoryScrollBarModeChanged(MemoryScrollBarMode mode);
|
||||
void addressRangeSelectionSyncEnabledChanged(bool enabled);
|
||||
#ifdef IAITO_ENABLE_KSYNTAXHIGHLIGHTING
|
||||
void kSyntaxHighlightingThemeChanged();
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "DataAnalysis.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace DataAnalysis {
|
||||
|
||||
Range sizedRange(RVA start, RVA size)
|
||||
{
|
||||
return {start, start + qMin(size, RVA_MAX - start)};
|
||||
}
|
||||
|
||||
Range intersect(Range a, Range b)
|
||||
{
|
||||
const RVA start = qMax(a.start, b.start);
|
||||
return {start, qMax(start, qMin(a.end, b.end))};
|
||||
}
|
||||
|
||||
Range bucket(Range range, int index, int count)
|
||||
{
|
||||
if (!range.valid() || count <= 0 || index < 0 || index >= count) {
|
||||
return {};
|
||||
}
|
||||
// Avoid both floating point address rounding and multiplication overflow.
|
||||
auto boundary = [range, count](RVA i) {
|
||||
return range.start + (range.size() / count) * i + (range.size() % count) * i / count;
|
||||
};
|
||||
return {boundary(index), boundary(index + 1)};
|
||||
}
|
||||
|
||||
Range centered(Range domain, RVA address, RVA size)
|
||||
{
|
||||
if (!domain.valid()) {
|
||||
return {};
|
||||
}
|
||||
size = qBound<RVA>(1, size, domain.size());
|
||||
address = qBound(domain.start, address, domain.end - 1);
|
||||
const RVA start = address - qMin(address - domain.start, size / 2);
|
||||
const RVA clamped = qMin(start, domain.end - size);
|
||||
return {clamped, clamped + size};
|
||||
}
|
||||
|
||||
QVector<Range> mergeRanges(QVector<Range> ranges)
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), [](Range a, Range b) { return a.start < b.start; });
|
||||
QVector<Range> result;
|
||||
for (auto range : ranges) {
|
||||
if (!range.valid()) {
|
||||
continue;
|
||||
}
|
||||
if (result.isEmpty() || result.last().end < range.start) {
|
||||
result.append(range);
|
||||
} else {
|
||||
result.last().end = qMax(result.last().end, range.end);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Sample sample(
|
||||
Range range,
|
||||
const QVector<Range> &readable,
|
||||
const std::function<QByteArray(RVA, int)> &read,
|
||||
int budget)
|
||||
{
|
||||
Sample result;
|
||||
result.range = range;
|
||||
if (!range.valid() || budget <= 0) {
|
||||
return result;
|
||||
}
|
||||
QVector<Range> spans;
|
||||
for (auto candidate : readable) {
|
||||
const Range span = intersect(range, candidate);
|
||||
if (span.valid()) {
|
||||
spans.append(span);
|
||||
result.covered += span.size();
|
||||
}
|
||||
}
|
||||
std::array<int, 256> histogram{};
|
||||
RVA remaining = result.covered;
|
||||
for (auto span : spans) {
|
||||
if (budget <= 0) {
|
||||
break;
|
||||
}
|
||||
// Allocate the budget proportionally among disjoint readable spans.
|
||||
const int share = qMax(1, int((long double) span.size() / remaining * budget));
|
||||
const int length = int(qMin<RVA>(span.size(), qMin(budget, share)));
|
||||
const RVA address = span.start + (span.size() - length) / 2;
|
||||
const QByteArray data = read(address, length).left(length);
|
||||
for (unsigned char byte : data) {
|
||||
++histogram[byte];
|
||||
}
|
||||
result.bytes += data.size();
|
||||
budget -= length;
|
||||
remaining -= span.size();
|
||||
}
|
||||
if (!result.bytes) {
|
||||
return result;
|
||||
}
|
||||
auto &v = result.values;
|
||||
for (int byte = 0; byte < 256; ++byte) {
|
||||
const int n = histogram[byte];
|
||||
if (!n) {
|
||||
continue;
|
||||
}
|
||||
const double p = double(n) / result.bytes;
|
||||
v[Entropy] -= p * std::log2(p) / 8.0;
|
||||
v[Printable] += (byte >= 32 && byte <= 126) ? p : 0;
|
||||
v[Zero] += byte == 0 ? p : 0;
|
||||
v[FF] += byte == 255 ? p : 0;
|
||||
v[Unique] += 1.0 / 256;
|
||||
v[Mean] += p * byte / 255;
|
||||
v[Alpha] += ((byte >= 'a' && byte <= 'z') || (byte >= 'A' && byte <= 'Z')) ? p : 0;
|
||||
v[Digit] += (byte >= '0' && byte <= '9') ? p : 0;
|
||||
v[Space] += (byte == ' ' || (byte >= 9 && byte <= 13)) ? p : 0;
|
||||
v[Control] += (byte < 32 || byte == 127) ? p : 0;
|
||||
v[High] += byte >= 128 ? p : 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace DataAnalysis
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef DATAANALYSIS_H
|
||||
#define DATAANALYSIS_H
|
||||
|
||||
#include "core/IaitoCommon.h"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <QByteArray>
|
||||
#include <QVector>
|
||||
|
||||
namespace DataAnalysis {
|
||||
|
||||
// Half-open ranges, including when the exclusive end is RVA_MAX.
|
||||
struct Range
|
||||
{
|
||||
RVA start = 0;
|
||||
RVA end = 0;
|
||||
bool valid() const { return start < end; }
|
||||
RVA size() const { return end - start; }
|
||||
bool contains(RVA address) const { return start <= address && address < end; }
|
||||
bool operator==(const Range &other) const { return start == other.start && end == other.end; }
|
||||
bool operator!=(const Range &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
enum Metric {
|
||||
Entropy,
|
||||
Printable,
|
||||
Zero,
|
||||
FF,
|
||||
Unique,
|
||||
Mean,
|
||||
Alpha,
|
||||
Digit,
|
||||
Space,
|
||||
Control,
|
||||
High,
|
||||
Count
|
||||
};
|
||||
|
||||
struct Sample
|
||||
{
|
||||
Range range;
|
||||
RVA covered = 0;
|
||||
int bytes = 0;
|
||||
std::array<double, Count> values{}; // Normalized to [0, 1]; entropy / 8.
|
||||
};
|
||||
|
||||
Range sizedRange(RVA start, RVA size);
|
||||
Range intersect(Range a, Range b);
|
||||
Range bucket(Range range, int index, int count);
|
||||
Range centered(Range domain, RVA address, RVA size);
|
||||
QVector<Range> mergeRanges(QVector<Range> ranges);
|
||||
|
||||
// Bounded, deterministic sampling. Reads only known readable spans; missing bytes
|
||||
// never contribute fabricated 0xff data. Large buckets use centered windows.
|
||||
Sample sample(
|
||||
Range range,
|
||||
const QVector<Range> &readable,
|
||||
const std::function<QByteArray(RVA, int)> &read,
|
||||
int budget = 4096);
|
||||
|
||||
} // namespace DataAnalysis
|
||||
|
||||
#endif
|
||||
@@ -337,7 +337,7 @@ QString linkAt(const QString &line, int posInLine)
|
||||
auto it = re.globalMatch(line);
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch m = it.next();
|
||||
if (posInLine >= m.capturedStart() && posInLine <= m.capturedEnd()) {
|
||||
if (posInLine >= m.capturedStart() && posInLine < m.capturedEnd()) {
|
||||
return m.captured();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#include "ExceptionInfo.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <QCoreApplication>
|
||||
#include <QJsonObject>
|
||||
#include <QVariant>
|
||||
|
||||
static QString exceptionTr(const char *text)
|
||||
{
|
||||
return QCoreApplication::translate("ExceptionInfo", text);
|
||||
}
|
||||
|
||||
static quint64 exceptionAddress(const QJsonValue &value)
|
||||
{
|
||||
bool ok = false;
|
||||
quint64 result = ExceptionRegion::Invalid;
|
||||
if (value.isString()) {
|
||||
result = value.toString().toULongLong(&ok, 0);
|
||||
} else if (
|
||||
value.isDouble() && value.toDouble() >= 0
|
||||
&& std::floor(value.toDouble()) == value.toDouble()) {
|
||||
result = value.toVariant().toULongLong(&ok);
|
||||
}
|
||||
return ok ? result : ExceptionRegion::Invalid;
|
||||
}
|
||||
|
||||
bool ExceptionRegion::protects(quint64 address, quint64 size) const
|
||||
{
|
||||
// Subtraction avoids overflowing a block's end near the address-space limit.
|
||||
return size && address < to && (address >= from || from - address < size);
|
||||
}
|
||||
|
||||
bool ExceptionRegion::touches(quint64 address, quint64 size) const
|
||||
{
|
||||
return protects(address, size)
|
||||
|| (handler != Invalid && handler >= address && handler - address < size);
|
||||
}
|
||||
|
||||
QString ExceptionRegion::label() const
|
||||
{
|
||||
if (kind == QStringLiteral("cleanup")) {
|
||||
return exceptionTr("Cleanup / finally");
|
||||
}
|
||||
const QString name = kind == QStringLiteral("filter") ? exceptionTr("Filter")
|
||||
: exceptionTr("Catch");
|
||||
return QStringLiteral("%1 (%2)").arg(
|
||||
name,
|
||||
catchAll ? exceptionTr("all exceptions")
|
||||
: type.isEmpty() ? exceptionTr("unknown type")
|
||||
: type);
|
||||
}
|
||||
|
||||
QString ExceptionRegion::description() const
|
||||
{
|
||||
QString text = exceptionTr("#%1 · %2\nProtected: [0x%3, 0x%4)\nHandler: 0x%5")
|
||||
.arg(index)
|
||||
.arg(label())
|
||||
.arg(from, 0, 16)
|
||||
.arg(to, 0, 16)
|
||||
.arg(handler, 0, 16);
|
||||
if (source != Invalid) {
|
||||
text += exceptionTr("\nMetadata source: 0x%1").arg(source, 0, 16);
|
||||
}
|
||||
if (filter && filter != Invalid) {
|
||||
text += exceptionTr("\nFilter value / address: 0x%1").arg(filter, 0, 16);
|
||||
}
|
||||
if (typeFilter) {
|
||||
text += exceptionTr("\nType filter: %1").arg(typeFilter);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
QColor ExceptionRegion::color() const
|
||||
{
|
||||
// Stable across views and independent of metadata order; shared ranges share a color.
|
||||
const quint64 hash = (from >> 2) ^ (to >> 1) ^ from;
|
||||
return QColor::fromHsv(int(hash % 12) * 30, 145, 210);
|
||||
}
|
||||
|
||||
QList<ExceptionRegion> ExceptionRegion::parse(const QJsonArray &array)
|
||||
{
|
||||
QList<ExceptionRegion> result;
|
||||
for (int i = 0; i < array.size(); ++i) {
|
||||
const auto object = array.at(i).toObject();
|
||||
ExceptionRegion region;
|
||||
region.from = exceptionAddress(object.value(QStringLiteral("from")));
|
||||
region.to = exceptionAddress(object.value(QStringLiteral("to")));
|
||||
region.handler = exceptionAddress(object.value(QStringLiteral("handler")));
|
||||
if (region.from == Invalid || region.to == Invalid || region.from >= region.to
|
||||
|| region.handler == Invalid) {
|
||||
continue;
|
||||
}
|
||||
region.index = exceptionAddress(object.value(QStringLiteral("index")));
|
||||
if (region.index == Invalid) {
|
||||
region.index = quint64(i);
|
||||
}
|
||||
region.source = exceptionAddress(object.value(QStringLiteral("source")));
|
||||
region.filter = exceptionAddress(object.value(QStringLiteral("filter")));
|
||||
region.kind = object.value(QStringLiteral("kind")).toString();
|
||||
region.type = object.value(QStringLiteral("type")).toString();
|
||||
region.typeFilter = object.value(QStringLiteral("typeFilter")).toVariant().toLongLong();
|
||||
region.catchAll = object.value(QStringLiteral("catchAll")).toBool();
|
||||
result.append(region);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QList<ExceptionRegion> ExceptionRegion::forBlocks(
|
||||
const QList<ExceptionRegion> ®ions, const QJsonArray &blocks)
|
||||
{
|
||||
QList<ExceptionRegion> result;
|
||||
for (const auto ®ion : regions) {
|
||||
for (const auto &value : blocks) {
|
||||
const auto block = value.toObject();
|
||||
const auto address = exceptionAddress(block.value(QStringLiteral("addr")));
|
||||
const auto size = exceptionAddress(block.value(QStringLiteral("size")));
|
||||
if (address != Invalid && size != Invalid && region.touches(address, size)) {
|
||||
result.append(region);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef EXCEPTIONINFO_H
|
||||
#define EXCEPTIONINFO_H
|
||||
|
||||
#include <limits>
|
||||
#include <QColor>
|
||||
#include <QJsonArray>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
// Addresses in iwj use half-open ranges: [from, to).
|
||||
struct ExceptionRegion
|
||||
{
|
||||
static constexpr quint64 Invalid = std::numeric_limits<quint64>::max();
|
||||
quint64 index = 0;
|
||||
quint64 source = Invalid;
|
||||
quint64 from = Invalid;
|
||||
quint64 to = Invalid;
|
||||
quint64 handler = Invalid;
|
||||
quint64 filter = 0;
|
||||
QString kind;
|
||||
QString type;
|
||||
qint64 typeFilter = 0;
|
||||
bool catchAll = false;
|
||||
|
||||
bool contains(quint64 address) const { return from <= address && address < to; }
|
||||
bool protects(quint64 address, quint64 size) const;
|
||||
bool touches(quint64 address, quint64 size) const;
|
||||
QString label() const;
|
||||
QString description() const;
|
||||
QColor color() const;
|
||||
static QList<ExceptionRegion> parse(const QJsonArray &array);
|
||||
static QList<ExceptionRegion> forBlocks(
|
||||
const QList<ExceptionRegion> ®ions, const QJsonArray &blocks);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cmath>
|
||||
#include <QAbstractButton>
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QCryptographicHash>
|
||||
@@ -151,6 +152,28 @@ void bindFont(QWidget *view, bool smallFont, bool monospace)
|
||||
QObject::connect(Config(), &Configuration::fontsUpdated, view, apply);
|
||||
}
|
||||
|
||||
// Monospace code font scaled to the size of the regular UI font
|
||||
QFont logFont()
|
||||
{
|
||||
QFont f = Config()->getBaseFont();
|
||||
const QFont ui = QApplication::font();
|
||||
if (ui.pixelSize() > 0) {
|
||||
f.setPixelSize(ui.pixelSize());
|
||||
} else if (ui.pointSizeF() > 0) {
|
||||
f.setPointSizeF(ui.pointSizeF());
|
||||
}
|
||||
f.setStyleHint(QFont::Monospace);
|
||||
return f;
|
||||
}
|
||||
|
||||
void bindLogFont(QWidget *view)
|
||||
{
|
||||
view->setFont(logFont());
|
||||
QObject::connect(Config(), &Configuration::fontsUpdated, view, [view]() {
|
||||
view->setFont(logFont());
|
||||
});
|
||||
}
|
||||
|
||||
SizePolicyMinMax forceWidth(QWidget *widget, int width)
|
||||
{
|
||||
SizePolicyMinMax r;
|
||||
|
||||
@@ -67,6 +67,8 @@ IAITO_EXPORT void setVerticalScrollMode(QAbstractItemView *tw);
|
||||
IAITO_EXPORT void setCheckedWithoutSignals(QAbstractButton *button, bool checked);
|
||||
|
||||
IAITO_EXPORT void bindFont(QWidget *view, bool smallFont = true, bool monospace = false);
|
||||
IAITO_EXPORT QFont logFont();
|
||||
IAITO_EXPORT void bindLogFont(QWidget *view);
|
||||
|
||||
struct IAITO_EXPORT SizePolicyMinMax
|
||||
{
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#include "IaitoSeekable.h"
|
||||
#include "core/MainWindow.h"
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
IaitoSeekable::IaitoSeekable(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
widgetOffset = Core()->getOffset();
|
||||
connect(Core(), &IaitoCore::seekChanged, this, &IaitoSeekable::onCoreSeekChanged);
|
||||
}
|
||||
|
||||
@@ -13,35 +11,98 @@ IaitoSeekable::~IaitoSeekable() {}
|
||||
|
||||
void IaitoSeekable::setSynchronization(bool sync)
|
||||
{
|
||||
if (synchronized == sync) {
|
||||
return;
|
||||
}
|
||||
const RVA address = Core()->getOffset();
|
||||
synchronized = sync;
|
||||
onCoreSeekChanged(Core()->getOffset());
|
||||
seekHistory = {address};
|
||||
historyIndex = 0;
|
||||
updateSeek(address);
|
||||
emit syncChanged();
|
||||
emit historyChanged();
|
||||
}
|
||||
|
||||
void IaitoSeekable::onCoreSeekChanged(RVA addr)
|
||||
{
|
||||
if (synchronized && widgetOffset != addr) {
|
||||
updateSeek(addr, true);
|
||||
if (synchronized) {
|
||||
updateSeek(addr);
|
||||
emit historyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void IaitoSeekable::updateSeek(RVA addr, bool localOnly)
|
||||
void IaitoSeekable::seek(RVA addr)
|
||||
{
|
||||
previousOffset = widgetOffset;
|
||||
widgetOffset = addr;
|
||||
if (synchronized && !localOnly) {
|
||||
Core()->seek(addr);
|
||||
if (addr == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
if (synchronized) {
|
||||
Core()->seek(addr);
|
||||
return;
|
||||
}
|
||||
if (addr == widgetOffset) {
|
||||
return;
|
||||
}
|
||||
seekHistory.resize(historyIndex + 1);
|
||||
seekHistory.append(addr);
|
||||
++historyIndex;
|
||||
updateSeek(addr);
|
||||
emit historyChanged();
|
||||
}
|
||||
|
||||
void IaitoSeekable::updateSeek(RVA addr)
|
||||
{
|
||||
if (addr == RVA_INVALID || addr == widgetOffset) {
|
||||
return;
|
||||
}
|
||||
widgetOffset = addr;
|
||||
emit seekableSeekChanged(addr);
|
||||
}
|
||||
|
||||
bool IaitoSeekable::canSeekPrev() const
|
||||
{
|
||||
if (synchronized) {
|
||||
return Core()->core()->io->undo.undos > 0;
|
||||
}
|
||||
return historyIndex > 0;
|
||||
}
|
||||
|
||||
bool IaitoSeekable::canSeekNext() const
|
||||
{
|
||||
if (synchronized) {
|
||||
return Core()->core()->io->undo.redos > 0;
|
||||
}
|
||||
return historyIndex + 1 < seekHistory.size();
|
||||
}
|
||||
|
||||
QVector<RVA> IaitoSeekable::localHistory(bool forward) const
|
||||
{
|
||||
QVector<RVA> result;
|
||||
if (!synchronized) {
|
||||
for (int i = historyIndex; i >= 0 && i < seekHistory.size(); i += forward ? 1 : -1) {
|
||||
result.append(seekHistory.at(i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void IaitoSeekable::seekPrev()
|
||||
{
|
||||
if (synchronized) {
|
||||
Core()->seekPrev();
|
||||
} else {
|
||||
this->seek(previousOffset);
|
||||
} else if (canSeekPrev()) {
|
||||
updateSeek(seekHistory.at(--historyIndex));
|
||||
emit historyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void IaitoSeekable::seekNext()
|
||||
{
|
||||
if (synchronized) {
|
||||
Core()->seekNext();
|
||||
} else if (canSeekNext()) {
|
||||
updateSeek(seekHistory.at(++historyIndex));
|
||||
emit historyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Iaito.h"
|
||||
#include <QVector>
|
||||
|
||||
class MainWindow;
|
||||
|
||||
@@ -20,7 +21,7 @@ public:
|
||||
* In any case, IaitoSeekable::seekableSeekChanged is emitted.
|
||||
* @param addr the location to seek at.
|
||||
*/
|
||||
void seek(RVA addr) { updateSeek(addr, false); }
|
||||
void seek(RVA addr);
|
||||
|
||||
/**
|
||||
* @brief setSynchronization sets
|
||||
@@ -44,6 +45,10 @@ public:
|
||||
*/
|
||||
bool isSynchronized();
|
||||
|
||||
bool canSeekPrev() const;
|
||||
bool canSeekNext() const;
|
||||
QVector<RVA> localHistory(bool forward) const;
|
||||
|
||||
/**
|
||||
* @brief seekToReference will seek to the function or the object which is
|
||||
* referenced in a given offset
|
||||
@@ -56,6 +61,7 @@ public slots:
|
||||
* @brief seekPrev seeks to last location.
|
||||
*/
|
||||
void seekPrev();
|
||||
void seekNext();
|
||||
|
||||
/**
|
||||
* @brief toggleSyncWithCore toggles
|
||||
@@ -75,11 +81,8 @@ private:
|
||||
*/
|
||||
RVA widgetOffset = RVA_INVALID;
|
||||
|
||||
/**
|
||||
* @brief previousOffset last seek location.
|
||||
* @todo maybe use an actual history?
|
||||
*/
|
||||
RVA previousOffset = RVA_INVALID;
|
||||
QVector<RVA> seekHistory;
|
||||
int historyIndex = 0;
|
||||
|
||||
/**
|
||||
* @brief synchronized tells with the seekable's offset is
|
||||
@@ -89,12 +92,11 @@ private:
|
||||
|
||||
/**
|
||||
* @brief internal method for changing the seek
|
||||
* @param localOnly whether the seek should be updated globally if
|
||||
* synchronized
|
||||
*/
|
||||
void updateSeek(RVA addr, bool localOnly);
|
||||
void updateSeek(RVA addr);
|
||||
|
||||
signals:
|
||||
void seekableSeekChanged(RVA addr);
|
||||
void syncChanged();
|
||||
void historyChanged();
|
||||
};
|
||||
@@ -0,0 +1,436 @@
|
||||
#include "NameListModel.h"
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
namespace {
|
||||
|
||||
QString compactName(const QString &name, const QString &previous)
|
||||
{
|
||||
int prefix = 0;
|
||||
const int limit = qMin(name.size(), previous.size());
|
||||
while (prefix < limit && name.at(prefix) == previous.at(prefix)) {
|
||||
++prefix;
|
||||
}
|
||||
if (prefix > 0 && prefix < name.size() && name.at(prefix).isLowSurrogate()) {
|
||||
--prefix;
|
||||
}
|
||||
return prefix > 2 && prefix < name.size() ? QStringLiteral("▶ ") + name.mid(prefix) : name;
|
||||
}
|
||||
|
||||
QStringList nameParts(const QString &name)
|
||||
{
|
||||
QStringList parts;
|
||||
int start = 0;
|
||||
int templates = 0;
|
||||
int parentheses = 0;
|
||||
int brackets = 0;
|
||||
for (int i = 0; i < name.size(); ++i) {
|
||||
const QChar c = name.at(i);
|
||||
// Operators and their signatures belong to the leaf, including operator< and operator().
|
||||
if (!templates && !parentheses && i == start && name.mid(i, 8) == QLatin1String("operator")
|
||||
&& (i + 8 == name.size()
|
||||
|| (!name.at(i + 8).isLetterOrNumber() && name.at(i + 8) != QLatin1Char('_')))) {
|
||||
break;
|
||||
}
|
||||
if (c == QLatin1Char('<')) {
|
||||
++templates;
|
||||
} else if (c == QLatin1Char('>') && templates) {
|
||||
--templates;
|
||||
} else if (c == QLatin1Char('(')) {
|
||||
++parentheses;
|
||||
} else if (c == QLatin1Char(')') && parentheses) {
|
||||
--parentheses;
|
||||
} else if (c == QLatin1Char('[')) {
|
||||
++brackets;
|
||||
} else if (c == QLatin1Char(']') && brackets) {
|
||||
--brackets;
|
||||
}
|
||||
if (templates || parentheses || brackets) {
|
||||
continue;
|
||||
}
|
||||
int separator = 0;
|
||||
if (c == QLatin1Char(':') && i + 1 < name.size() && name.at(i + 1) == c) {
|
||||
separator = 2;
|
||||
} else if (c == QLatin1Char('-') && i + 1 < name.size() && name.at(i + 1) == QLatin1Char('>')) {
|
||||
separator = 2;
|
||||
} else if (c == QLatin1Char('.') || c == QLatin1Char('/') || c == QLatin1Char('$')) {
|
||||
separator = 1;
|
||||
}
|
||||
if (separator) {
|
||||
i += separator - 1;
|
||||
if (i + 1 < name.size()) {
|
||||
parts.append(name.mid(start, i + 1 - start));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.append(name.mid(start));
|
||||
return parts;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NameListModel::NameListModel(AddressableItemModelI *source, int nameColumn, QObject *parent)
|
||||
: AddressableItemModel<QAbstractProxyModel>(parent)
|
||||
, nameColumn(nameColumn)
|
||||
{
|
||||
setSourceModel(source->asItemModel());
|
||||
}
|
||||
|
||||
NameListModel::~NameListModel() = default;
|
||||
|
||||
void NameListModel::setSourceModel(QAbstractItemModel *source)
|
||||
{
|
||||
beginRebuild();
|
||||
if (sourceModel()) {
|
||||
disconnect(sourceModel(), nullptr, this, nullptr);
|
||||
}
|
||||
QAbstractProxyModel::setSourceModel(source);
|
||||
sourceProvider = dynamic_cast<AddressableItemModelI *>(source);
|
||||
if (source) {
|
||||
if (auto filter = qobject_cast<AddressableFilterProxyModel *>(source)) {
|
||||
// QSortFilterProxyModel can remove thousands of disjoint row ranges for one query.
|
||||
// Rebuild the presentation once, after the entire filter operation has finished.
|
||||
connect(filter, &AddressableFilterProxyModel::filterAboutToChange, this, [this] {
|
||||
++filterChangeDepth;
|
||||
beginRebuild();
|
||||
});
|
||||
connect(filter, &AddressableFilterProxyModel::filterChanged, this, [this] {
|
||||
--filterChangeDepth;
|
||||
endRebuild();
|
||||
});
|
||||
}
|
||||
connect(source, &QAbstractItemModel::modelAboutToBeReset, this, &NameListModel::beginRebuild);
|
||||
connect(source, &QAbstractItemModel::modelReset, this, &NameListModel::endRebuild);
|
||||
connect(
|
||||
source, &QAbstractItemModel::layoutAboutToBeChanged, this, &NameListModel::beginRebuild);
|
||||
connect(source, &QAbstractItemModel::layoutChanged, this, &NameListModel::endRebuild);
|
||||
connect(
|
||||
source, &QAbstractItemModel::rowsAboutToBeInserted, this, &NameListModel::beginRebuild);
|
||||
connect(source, &QAbstractItemModel::rowsInserted, this, &NameListModel::endRebuild);
|
||||
connect(source, &QAbstractItemModel::rowsAboutToBeRemoved, this, &NameListModel::beginRebuild);
|
||||
connect(source, &QAbstractItemModel::rowsRemoved, this, &NameListModel::endRebuild);
|
||||
connect(source, &QAbstractItemModel::rowsAboutToBeMoved, this, &NameListModel::beginRebuild);
|
||||
connect(source, &QAbstractItemModel::rowsMoved, this, &NameListModel::endRebuild);
|
||||
connect(source, &QAbstractItemModel::dataChanged, this, &NameListModel::sourceDataChanged);
|
||||
connect(
|
||||
source, &QAbstractItemModel::headerDataChanged, this, &NameListModel::headerDataChanged);
|
||||
connect(source, &QObject::destroyed, this, [this] {
|
||||
sourceProvider = nullptr;
|
||||
beginRebuild();
|
||||
endRebuild();
|
||||
});
|
||||
}
|
||||
endRebuild();
|
||||
}
|
||||
|
||||
NameListModel::Node *NameListModel::node(const QModelIndex &index) const
|
||||
{
|
||||
return index.isValid() ? static_cast<Node *>(index.internalPointer()) : nullptr;
|
||||
}
|
||||
|
||||
QModelIndex NameListModel::nodeIndex(Node *item, int column) const
|
||||
{
|
||||
return item && item != &root ? createIndex(item->row, column, item) : QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex NameListModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (row < 0 || column < 0 || column >= columnCount() || (parent.isValid() && parent.column())) {
|
||||
return {};
|
||||
}
|
||||
const Node *item = parent.isValid() ? node(parent) : &root;
|
||||
return row < int(item->children.size()) ? nodeIndex(item->children[row].get(), column)
|
||||
: QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex NameListModel::parent(const QModelIndex &index) const
|
||||
{
|
||||
auto item = node(index);
|
||||
return item ? nodeIndex(item->parent) : QModelIndex();
|
||||
}
|
||||
|
||||
int NameListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid() && parent.column()) {
|
||||
return 0;
|
||||
}
|
||||
const Node *item = parent.isValid() ? node(parent) : &root;
|
||||
return int(item->children.size());
|
||||
}
|
||||
|
||||
int NameListModel::columnCount(const QModelIndex &) const
|
||||
{
|
||||
return sourceProvider ? sourceModel()->columnCount() : 0;
|
||||
}
|
||||
|
||||
bool NameListModel::hasChildren(const QModelIndex &parent) const
|
||||
{
|
||||
return rowCount(parent) > 0;
|
||||
}
|
||||
|
||||
QModelIndex NameListModel::mapToSource(const QModelIndex &index) const
|
||||
{
|
||||
auto item = node(index);
|
||||
return sourceProvider && item && item->source.isValid()
|
||||
? item->source.sibling(item->source.row(), index.column())
|
||||
: QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex NameListModel::mapFromSource(const QModelIndex &index) const
|
||||
{
|
||||
return index.isValid()
|
||||
? nodeIndex(sourceNodes.value(index.sibling(index.row(), 0)), index.column())
|
||||
: QModelIndex();
|
||||
}
|
||||
|
||||
QVariant NameListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
auto item = node(index);
|
||||
if (!item) {
|
||||
return {};
|
||||
}
|
||||
if (!item->source.isValid()) {
|
||||
if (role == GroupPathRole) {
|
||||
return item->groupPath;
|
||||
}
|
||||
if (index.column() == nameColumn) {
|
||||
if (role == Qt::DisplayRole) {
|
||||
return item->label;
|
||||
}
|
||||
if (role == Qt::ToolTipRole) {
|
||||
return QStringLiteral("<qt>%1</qt>").arg(item->groupPath.toHtmlEscaped());
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (index.column() == nameColumn && item->named) {
|
||||
if (role == Qt::ToolTipRole) {
|
||||
const auto tooltip = mapToSource(index).data(NameToolTipRole);
|
||||
if (tooltip.isValid()) {
|
||||
return tooltip;
|
||||
}
|
||||
return QStringLiteral("<qt>%1</qt>").arg(item->fullName.toHtmlEscaped());
|
||||
}
|
||||
if (role == Qt::DisplayRole) {
|
||||
QString label = item->label;
|
||||
if (compact && item->row > 0) {
|
||||
auto previous = item->parent->children[item->row - 1].get();
|
||||
if (previous->named) {
|
||||
label = compactName(label, previous->label);
|
||||
}
|
||||
}
|
||||
const QString original = mapToSource(index).data().toString();
|
||||
// Keep decorations represented as text, such as a function's pin emoji.
|
||||
if (original.endsWith(item->fullName)) {
|
||||
return original.left(original.size() - item->fullName.size()) + label;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return mapToSource(index).data(role);
|
||||
}
|
||||
|
||||
Qt::ItemFlags NameListModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return Qt::NoItemFlags;
|
||||
}
|
||||
auto source = mapToSource(index);
|
||||
return source.isValid() ? source.flags() : Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
QVariant NameListModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
return sourceProvider ? sourceModel()->headerData(section, orientation, role) : QVariant();
|
||||
}
|
||||
|
||||
void NameListModel::sort(int column, Qt::SortOrder order)
|
||||
{
|
||||
if (sourceProvider) {
|
||||
sourceModel()->sort(column, order);
|
||||
}
|
||||
}
|
||||
|
||||
RVA NameListModel::address(const QModelIndex &index) const
|
||||
{
|
||||
const auto source = mapToSource(index);
|
||||
return sourceProvider && source.isValid() ? sourceProvider->address(source) : RVA_INVALID;
|
||||
}
|
||||
|
||||
QString NameListModel::name(const QModelIndex &index) const
|
||||
{
|
||||
const auto source = mapToSource(index);
|
||||
return sourceProvider && source.isValid() ? sourceProvider->name(source) : QString();
|
||||
}
|
||||
|
||||
void NameListModel::beginRebuild()
|
||||
{
|
||||
if (!resetting) {
|
||||
resetting = true;
|
||||
beginResetModel();
|
||||
}
|
||||
}
|
||||
|
||||
void NameListModel::endRebuild()
|
||||
{
|
||||
if (resetting && !filterChangeDepth) {
|
||||
rebuild();
|
||||
resetting = false;
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
|
||||
void NameListModel::appendSource(
|
||||
Node *parent, const QModelIndex &source, const QString &label, bool named)
|
||||
{
|
||||
auto item = std::make_unique<Node>();
|
||||
item->parent = parent;
|
||||
item->row = int(parent->children.size());
|
||||
item->source = source;
|
||||
item->label = label;
|
||||
item->named = named;
|
||||
if (named) {
|
||||
item->fullName = sourceProvider->name(source);
|
||||
}
|
||||
auto ptr = item.get();
|
||||
sourceNodes.insert(source, ptr);
|
||||
parent->children.push_back(std::move(item));
|
||||
// Preserve existing class/member relationships and unnamed function detail rows.
|
||||
if (sourceModel()->hasChildren(source)) {
|
||||
for (int row = 0; row < sourceModel()->rowCount(source); ++row) {
|
||||
auto child = sourceModel()->index(row, 0, source);
|
||||
QString childName = sourceProvider->name(child);
|
||||
const bool childNamed = !childName.isEmpty() && childName != ptr->fullName;
|
||||
if (childNamed) {
|
||||
for (const QString &separator :
|
||||
{QStringLiteral("::"),
|
||||
QStringLiteral("."),
|
||||
QStringLiteral("/"),
|
||||
QStringLiteral("$"),
|
||||
QStringLiteral("->")}) {
|
||||
const QString prefix = ptr->fullName + separator;
|
||||
if (childName.startsWith(prefix)) {
|
||||
childName.remove(0, prefix.size());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
appendSource(ptr, child, childName, childNamed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NameListModel::rebuild()
|
||||
{
|
||||
sourceNodes.clear();
|
||||
groups.clear();
|
||||
root.children.clear();
|
||||
if (!sourceProvider) {
|
||||
return;
|
||||
}
|
||||
for (int row = 0; row < sourceModel()->rowCount(); ++row) {
|
||||
auto source = sourceModel()->index(row, 0);
|
||||
const QString fullName = sourceProvider->name(source);
|
||||
Node *parent = &root;
|
||||
QString label = fullName;
|
||||
if (hierarchy) {
|
||||
const auto parts = nameParts(fullName);
|
||||
QString path;
|
||||
for (int part = 0; part + 1 < parts.size(); ++part) {
|
||||
path += parts[part];
|
||||
auto group = groups.value(path);
|
||||
if (!group) {
|
||||
auto item = std::make_unique<Node>();
|
||||
item->parent = parent;
|
||||
item->row = int(parent->children.size());
|
||||
item->label = parts[part];
|
||||
item->groupPath = path;
|
||||
group = item.get();
|
||||
parent->children.push_back(std::move(item));
|
||||
groups.insert(path, group);
|
||||
}
|
||||
parent = group;
|
||||
}
|
||||
label = parts.last();
|
||||
}
|
||||
appendSource(parent, source, label, true);
|
||||
}
|
||||
collapseSingleGroups(&root);
|
||||
}
|
||||
|
||||
void NameListModel::collapseSingleGroups(Node *parent)
|
||||
{
|
||||
for (auto &child : parent->children) {
|
||||
if (child->source.isValid()) {
|
||||
continue; // Class/member relationships belong to the source model.
|
||||
}
|
||||
collapseSingleGroups(child.get());
|
||||
if (child->children.size() == 1 && child->children.front()->source.isValid()) {
|
||||
auto group = std::move(child);
|
||||
child = std::move(group->children.front());
|
||||
child->label.prepend(group->label);
|
||||
child->parent = parent;
|
||||
child->row = group->row;
|
||||
groups.remove(group->groupPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NameListModel::sourceDataChanged(const QModelIndex &first, const QModelIndex &last)
|
||||
{
|
||||
if (resetting) {
|
||||
return;
|
||||
}
|
||||
for (int row = first.row(); row <= last.row(); ++row) {
|
||||
auto source = sourceModel()->index(row, 0, first.parent());
|
||||
auto item = sourceNodes.value(source);
|
||||
if (item && item->named && item->fullName != sourceProvider->name(source)) {
|
||||
beginRebuild();
|
||||
endRebuild();
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (int row = first.row(); row <= last.row(); ++row) {
|
||||
auto source = sourceModel()->index(row, 0, first.parent());
|
||||
auto item = sourceNodes.value(source);
|
||||
if (item) {
|
||||
emit dataChanged(nodeIndex(item, first.column()), nodeIndex(item, last.column()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NameListModel::namesChanged(Node *parent)
|
||||
{
|
||||
if (!parent->children.empty()) {
|
||||
emit dataChanged(
|
||||
nodeIndex(parent->children.front().get(), nameColumn),
|
||||
nodeIndex(parent->children.back().get(), nameColumn),
|
||||
{Qt::DisplayRole});
|
||||
for (const auto &child : parent->children) {
|
||||
namesChanged(child.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NameListModel::setCompact(bool enabled)
|
||||
{
|
||||
if (compact != enabled) {
|
||||
compact = enabled;
|
||||
namesChanged(&root);
|
||||
}
|
||||
}
|
||||
|
||||
void NameListModel::setHierarchy(bool enabled)
|
||||
{
|
||||
if (hierarchy != enabled) {
|
||||
beginRebuild();
|
||||
hierarchy = enabled;
|
||||
endRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex NameListModel::groupIndex(const QString &path) const
|
||||
{
|
||||
return nodeIndex(groups.value(path));
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef NAMELISTMODEL_H
|
||||
#define NAMELISTMODEL_H
|
||||
|
||||
#include "AddressableItemModel.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <QAbstractProxyModel>
|
||||
#include <QHash>
|
||||
|
||||
// Presentation only: all addresses, descriptions and edits belong to the source model.
|
||||
class IAITO_EXPORT NameListModel : public AddressableItemModel<QAbstractProxyModel>
|
||||
{
|
||||
public:
|
||||
enum {
|
||||
GroupPathRole = Qt::UserRole + 0x1000,
|
||||
// A complete, HTML-escaped name tooltip supplied by models with alternate spellings.
|
||||
NameToolTipRole
|
||||
};
|
||||
|
||||
NameListModel(AddressableItemModelI *source, int nameColumn, QObject *parent = nullptr);
|
||||
~NameListModel() override;
|
||||
|
||||
void setSourceModel(QAbstractItemModel *source) override;
|
||||
QModelIndex mapToSource(const QModelIndex &index) const override;
|
||||
QModelIndex mapFromSource(const QModelIndex &index) const override;
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override;
|
||||
QModelIndex parent(const QModelIndex &index) const override;
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
int columnCount(const QModelIndex &parent = {}) const override;
|
||||
bool hasChildren(const QModelIndex &parent = {}) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(
|
||||
int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
|
||||
RVA address(const QModelIndex &index) const override;
|
||||
QString name(const QModelIndex &index) const override;
|
||||
|
||||
void setCompact(bool enabled);
|
||||
void setHierarchy(bool enabled);
|
||||
bool isHierarchy() const { return hierarchy; }
|
||||
QModelIndex groupIndex(const QString &path) const;
|
||||
|
||||
private:
|
||||
struct Node
|
||||
{
|
||||
Node *parent = nullptr;
|
||||
int row = 0;
|
||||
QModelIndex source;
|
||||
QString label;
|
||||
QString fullName;
|
||||
bool named = false;
|
||||
QString groupPath;
|
||||
std::vector<std::unique_ptr<Node>> children;
|
||||
};
|
||||
|
||||
AddressableItemModelI *sourceProvider = nullptr;
|
||||
int nameColumn;
|
||||
bool compact = false;
|
||||
bool hierarchy = false;
|
||||
bool resetting = false;
|
||||
int filterChangeDepth = 0;
|
||||
Node root;
|
||||
QHash<QModelIndex, Node *> sourceNodes;
|
||||
QHash<QString, Node *> groups;
|
||||
|
||||
Node *node(const QModelIndex &index) const;
|
||||
QModelIndex nodeIndex(Node *node, int column = 0) const;
|
||||
void beginRebuild();
|
||||
void endRebuild();
|
||||
void rebuild();
|
||||
void collapseSingleGroups(Node *parent);
|
||||
void appendSource(Node *parent, const QModelIndex &source, const QString &label, bool named);
|
||||
void sourceDataChanged(const QModelIndex &first, const QModelIndex &last);
|
||||
void namesChanged(Node *parent);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "R2DecaiDecompiler.h"
|
||||
#include "Iaito.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
@@ -48,7 +49,20 @@ static QStringList parseDecaiEvalLines(const QString &output)
|
||||
|
||||
QStringList R2DecaiDecompiler::listOptions()
|
||||
{
|
||||
QString output = Core()->cmd("decai -e");
|
||||
// r2js console.log writes to stdout instead of the command's output buffer.
|
||||
const QByteArray script = R"((function() {
|
||||
var log = console.log;
|
||||
var lines = [];
|
||||
console.log = function() { lines.push(Array.prototype.join.call(arguments, " ")); };
|
||||
try {
|
||||
var output = r2.cmd("decai -e");
|
||||
if (output) { lines.push(output); }
|
||||
} finally {
|
||||
console.log = log;
|
||||
}
|
||||
r2.log(lines.join("\n"));
|
||||
})())";
|
||||
QString output = Core()->cmd(("js base64:" + script.toBase64()).constData());
|
||||
return parseDecaiEvalLines(output);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,61 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
QT_TR_NOOP("Close tab"),
|
||||
{},
|
||||
QKeySequence::Close});
|
||||
#ifdef Q_OS_MACOS
|
||||
// Qt maps the Command key to CTRL on macOS.
|
||||
const auto panelModifier = Qt::CTRL;
|
||||
#else
|
||||
const auto panelModifier = Qt::META;
|
||||
#endif
|
||||
t.append(
|
||||
{"global.maximizePanel",
|
||||
ShortcutScope::Global,
|
||||
QT_TR_NOOP("Toggle panel maximization"),
|
||||
{QKeySequence(panelModifier | Qt::Key_Return),
|
||||
QKeySequence(panelModifier | Qt::Key_Enter)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"global.selectPanelTab1",
|
||||
ShortcutScope::Global,
|
||||
QT_TR_NOOP("Select panel tab 1"),
|
||||
{QKeySequence(panelModifier | Qt::Key_1)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"global.selectPanelTab2",
|
||||
ShortcutScope::Global,
|
||||
QT_TR_NOOP("Select panel tab 2"),
|
||||
{QKeySequence(panelModifier | Qt::Key_2)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"global.selectPanelTab3",
|
||||
ShortcutScope::Global,
|
||||
QT_TR_NOOP("Select panel tab 3"),
|
||||
{QKeySequence(panelModifier | Qt::Key_3)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"global.selectPanelTab4",
|
||||
ShortcutScope::Global,
|
||||
QT_TR_NOOP("Select panel tab 4"),
|
||||
{QKeySequence(panelModifier | Qt::Key_4)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"global.selectPanelTab5",
|
||||
ShortcutScope::Global,
|
||||
QT_TR_NOOP("Select panel tab 5"),
|
||||
{QKeySequence(panelModifier | Qt::Key_5)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"global.focusNextFrame",
|
||||
ShortcutScope::Global,
|
||||
QT_TR_NOOP("Focus next frame"),
|
||||
{QKeySequence(panelModifier | Qt::Key_J)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"global.focusPreviousFrame",
|
||||
ShortcutScope::Global,
|
||||
QT_TR_NOOP("Focus previous frame"),
|
||||
{QKeySequence(panelModifier | Qt::Key_K)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"global.back",
|
||||
ShortcutScope::Global,
|
||||
@@ -254,6 +309,12 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
QT_TR_NOOP("Set to data (resize)"),
|
||||
{QKeySequence(Qt::Key_Asterisk)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"disasm.flag",
|
||||
ShortcutScope::Disassembly,
|
||||
QT_TR_NOOP("Add / edit flag"),
|
||||
{QKeySequence(Qt::Key_F)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"disasm.rename",
|
||||
ShortcutScope::Disassembly,
|
||||
@@ -276,7 +337,7 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
{"disasm.linkType",
|
||||
ShortcutScope::Disassembly,
|
||||
QT_TR_NOOP("Link type"),
|
||||
{QKeySequence(Qt::Key_L)},
|
||||
{QKeySequence(Qt::SHIFT | Qt::Key_L)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"disasm.addBreakpoint",
|
||||
@@ -308,12 +369,30 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
QT_TR_NOOP("Switch to graph view"),
|
||||
{QKeySequence(Qt::Key_Space)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"disasm.switchToDecompiler",
|
||||
ShortcutScope::Disassembly,
|
||||
QT_TR_NOOP("Switch to decompiler view"),
|
||||
{QKeySequence(Qt::Key_Tab)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"disasm.seekPrev",
|
||||
ShortcutScope::Disassembly,
|
||||
QT_TR_NOOP("Seek to previous"),
|
||||
{QKeySequence(Qt::Key_Escape)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"disasm.cursorLeft",
|
||||
ShortcutScope::Disassembly,
|
||||
QT_TR_NOOP("Move cursor left"),
|
||||
{QKeySequence(Qt::Key_H), QKeySequence(Qt::Key_Left)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"disasm.cursorRight",
|
||||
ShortcutScope::Disassembly,
|
||||
QT_TR_NOOP("Move cursor right"),
|
||||
{QKeySequence(Qt::Key_L), QKeySequence(Qt::Key_Right)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"disasm.cursorDown",
|
||||
ShortcutScope::Disassembly,
|
||||
@@ -341,8 +420,8 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
t.append(
|
||||
{"disasm.jumpToOffset",
|
||||
ShortcutScope::Disassembly,
|
||||
QT_TR_NOOP("Jump to offset under cursor"),
|
||||
{QKeySequence(Qt::Key_Return)},
|
||||
QT_TR_NOOP("Follow reference under cursor"),
|
||||
{QKeySequence(Qt::Key_Return), QKeySequence(Qt::Key_Enter)},
|
||||
Unknown});
|
||||
|
||||
t.append(
|
||||
@@ -351,6 +430,30 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
QT_TR_NOOP("Copy"),
|
||||
{},
|
||||
QKeySequence::Copy});
|
||||
t.append(
|
||||
{"decompiler.cursorLeft",
|
||||
ShortcutScope::Decompiler,
|
||||
QT_TR_NOOP("Move cursor left"),
|
||||
{QKeySequence(Qt::Key_H)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.cursorDown",
|
||||
ShortcutScope::Decompiler,
|
||||
QT_TR_NOOP("Move cursor down"),
|
||||
{QKeySequence(Qt::Key_J)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.cursorUp",
|
||||
ShortcutScope::Decompiler,
|
||||
QT_TR_NOOP("Move cursor up"),
|
||||
{QKeySequence(Qt::Key_K)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.cursorRight",
|
||||
ShortcutScope::Decompiler,
|
||||
QT_TR_NOOP("Move cursor right"),
|
||||
{QKeySequence(Qt::Key_L)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.comment",
|
||||
ShortcutScope::Decompiler,
|
||||
@@ -381,6 +484,24 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
QT_TR_NOOP("Edit variables"),
|
||||
{QKeySequence(Qt::Key_Y)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.followReference",
|
||||
ShortcutScope::Decompiler,
|
||||
QT_TR_NOOP("Follow reference under cursor"),
|
||||
{QKeySequence(Qt::Key_Return), QKeySequence(Qt::Key_Enter)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.matchBrace",
|
||||
ShortcutScope::Decompiler,
|
||||
QT_TR_NOOP("Jump to matching brace"),
|
||||
{QKeySequence(Qt::Key_Percent), QKeySequence(Qt::SHIFT | Qt::Key_Percent)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.toggleFold",
|
||||
ShortcutScope::Decompiler,
|
||||
QT_TR_NOOP("Collapse/expand block"),
|
||||
{QKeySequence(Qt::Key_Space)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.toggleBreakpoint",
|
||||
ShortcutScope::Decompiler,
|
||||
@@ -399,6 +520,12 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
QT_TR_NOOP("Seek to previous"),
|
||||
{QKeySequence(Qt::Key_Escape)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"decompiler.switchToDisassembly",
|
||||
ShortcutScope::Decompiler,
|
||||
QT_TR_NOOP("Switch to disassembly view"),
|
||||
{QKeySequence(Qt::Key_Tab)},
|
||||
Unknown});
|
||||
|
||||
t.append(
|
||||
{"addressable.copyAddress",
|
||||
@@ -492,6 +619,12 @@ const QVector<ShortcutDescriptor> &catalog()
|
||||
{QKeySequence(Qt::SHIFT | Qt::Key_L)},
|
||||
Unknown});
|
||||
t.append({"hex.copy", ShortcutScope::Hexdump, QT_TR_NOOP("Copy"), {}, QKeySequence::Copy});
|
||||
t.append(
|
||||
{"hex.flag",
|
||||
ShortcutScope::Hexdump,
|
||||
QT_TR_NOOP("Add / edit flag"),
|
||||
{QKeySequence(Qt::Key_F)},
|
||||
Unknown});
|
||||
t.append(
|
||||
{"hex.copyAddress",
|
||||
ShortcutScope::Hexdump,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "SourceLineReference.h"
|
||||
|
||||
QList<SourceLineReference> SourceLineReference::parse(const QString &contents)
|
||||
{
|
||||
QList<SourceLineReference> result;
|
||||
for (const QString &row : contents.split(QLatin1Char('\n'))) {
|
||||
const int firstTab = row.indexOf(QLatin1Char('\t'));
|
||||
const int secondTab = row.indexOf(QLatin1Char('\t'), firstTab + 1);
|
||||
if (firstTab <= 0 || secondTab < 0) {
|
||||
continue;
|
||||
}
|
||||
const QString addressText = row.left(firstTab);
|
||||
if (!addressText.startsWith(QStringLiteral("0x"))) {
|
||||
continue;
|
||||
}
|
||||
bool addressOk = false;
|
||||
bool lineOk = false;
|
||||
const quint64 address = addressText.mid(2).toULongLong(&addressOk, 16);
|
||||
const int line = row.mid(firstTab + 1, secondTab - firstTab - 1).toInt(&lineOk);
|
||||
if (!addressOk || !lineOk || line < 0) {
|
||||
continue;
|
||||
}
|
||||
QString code = row.mid(secondTab + 1);
|
||||
if (code.endsWith(QLatin1Char('\r'))) {
|
||||
code.chop(1);
|
||||
}
|
||||
result.append({address, line, code});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
struct SourceLineReference
|
||||
{
|
||||
quint64 address;
|
||||
int line;
|
||||
QString code;
|
||||
|
||||
// fs.r2's /cl files contain address, line number and source text, separated by tabs.
|
||||
static QList<SourceLineReference> parse(const QString &contents);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
#ifndef SPARSEHEXLAYOUT_H
|
||||
#define SPARSEHEXLAYOUT_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace SparseHexLayout {
|
||||
|
||||
// Offsets are relative to a bounded read buffer, never absolute addresses.
|
||||
struct Row
|
||||
{
|
||||
int offset;
|
||||
int size;
|
||||
int fill = -1;
|
||||
bool continues = false;
|
||||
};
|
||||
|
||||
inline int uniformPrefix(const unsigned char *bytes, int size)
|
||||
{
|
||||
const uint64_t pattern = bytes[0] ? UINT64_MAX : 0;
|
||||
int i = 0;
|
||||
while (size - i >= int(sizeof(pattern))) {
|
||||
uint64_t word;
|
||||
std::memcpy(&word, bytes + i, sizeof(word));
|
||||
if (word != pattern) {
|
||||
break;
|
||||
}
|
||||
i += sizeof(word);
|
||||
}
|
||||
while (i < size && bytes[i] == bytes[0]) {
|
||||
++i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
inline std::vector<Row> build(
|
||||
const unsigned char *bytes, int size, int rowSize, int itemSize, int maxRows)
|
||||
{
|
||||
std::vector<Row> rows;
|
||||
if (size <= 0 || rowSize <= 0 || itemSize <= 0 || maxRows <= 0) {
|
||||
return rows;
|
||||
}
|
||||
rows.reserve(std::min(maxRows, size / rowSize + 2));
|
||||
int begin = 0;
|
||||
int pos = 0;
|
||||
while (pos < size && int(rows.size()) < maxRows) {
|
||||
int step = itemSize;
|
||||
if (bytes[pos] == 0 || bytes[pos] == 0xff) {
|
||||
const int run = uniformPrefix(bytes + pos, size - pos);
|
||||
if (run > rowSize * 2) {
|
||||
if (pos > begin) {
|
||||
rows.push_back({begin, pos - begin});
|
||||
if (int(rows.size()) == maxRows) {
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
// Keep multi-byte items intact, including at the end of a read.
|
||||
const int folded = run - run % itemSize;
|
||||
rows.push_back({pos, folded, bytes[pos], pos + run == size});
|
||||
begin = pos = pos + folded;
|
||||
continue;
|
||||
}
|
||||
// Consume short runs once too; rescanning every suffix is quadratic
|
||||
// when the user selects a large number of columns.
|
||||
step = std::max(itemSize, run - run % itemSize);
|
||||
}
|
||||
step = std::min(step, size - pos);
|
||||
while (step > 0 && int(rows.size()) < maxRows) {
|
||||
const int count = std::min(step, rowSize - (pos - begin));
|
||||
pos += count;
|
||||
step -= count;
|
||||
if (pos - begin == rowSize) {
|
||||
rows.push_back({begin, rowSize});
|
||||
begin = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos > begin && int(rows.size()) < maxRows) {
|
||||
rows.push_back({begin, pos - begin});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
} // namespace SparseHexLayout
|
||||
|
||||
#endif // SPARSEHEXLAYOUT_H
|
||||
@@ -0,0 +1,253 @@
|
||||
#include "TypeLayout.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <QRegularExpression>
|
||||
|
||||
void TypeLayout::load(const QString &text, int pointerByteSize)
|
||||
{
|
||||
database.clear();
|
||||
sizes.clear();
|
||||
pointerSize = qBound(1, pointerByteSize, 8);
|
||||
for (const QString &line : text.split(QLatin1Char('\n'))) {
|
||||
const int separator = line.indexOf(QLatin1Char('='));
|
||||
if (separator > 0) {
|
||||
database.insert(line.left(separator), line.mid(separator + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QStringList TypeLayout::names() const
|
||||
{
|
||||
QStringList result;
|
||||
for (auto it = database.cbegin(); it != database.cend(); ++it) {
|
||||
if (it.value() == QLatin1String("struct") || it.value() == QLatin1String("union")
|
||||
|| it.value() == QLatin1String("typedef") || it.value() == QLatin1String("type")
|
||||
|| it.value() == QLatin1String("enum")) {
|
||||
result.append(it.key());
|
||||
}
|
||||
}
|
||||
result.sort(Qt::CaseInsensitive);
|
||||
return result;
|
||||
}
|
||||
|
||||
QMap<quint64, QString> TypeLayout::links() const
|
||||
{
|
||||
QMap<quint64, QString> result;
|
||||
for (auto it = database.cbegin(); it != database.cend(); ++it) {
|
||||
if (it.key().startsWith(QLatin1String("link."))) {
|
||||
bool ok = false;
|
||||
const quint64 address = it.key().mid(5).toULongLong(&ok, 16);
|
||||
if (ok) {
|
||||
result.insert(address, it.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString TypeLayout::resolve(QString type) const
|
||||
{
|
||||
static const QRegularExpression qualifiers(QStringLiteral("\\b(const|volatile|restrict)\\b"));
|
||||
static const QRegularExpression tag(QStringLiteral("^(struct|union|enum)\\s+"));
|
||||
QStringList visited;
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
type.remove(qualifiers);
|
||||
type = type.simplified();
|
||||
type.remove(tag);
|
||||
if (visited.contains(type)) {
|
||||
return QString();
|
||||
}
|
||||
visited.append(type);
|
||||
if (database.value(type) != QLatin1String("typedef")) {
|
||||
return type;
|
||||
}
|
||||
type = database.value(QStringLiteral("typedef.") + type);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool TypeLayout::array(const QString &type, QString *element, quint64 *count)
|
||||
{
|
||||
static const QRegularExpression expression(QStringLiteral("^(.*?)\\[(\\d+)\\](.*)$"));
|
||||
const auto match = expression.match(type);
|
||||
if (!match.hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
*count = match.captured(2).toULongLong(&ok);
|
||||
*element = (match.captured(1) + match.captured(3)).trimmed();
|
||||
return ok;
|
||||
}
|
||||
|
||||
QString TypeLayout::kind(const QString &type) const
|
||||
{
|
||||
const QString resolved = resolve(type);
|
||||
if (resolved.endsWith(QLatin1Char('*'))) {
|
||||
return QStringLiteral("pointer");
|
||||
}
|
||||
QString element;
|
||||
quint64 count;
|
||||
if (array(resolved, &element, &count)) {
|
||||
return QStringLiteral("array");
|
||||
}
|
||||
return database.value(resolved);
|
||||
}
|
||||
|
||||
QVector<TypeLayout::Field> TypeLayout::fields(const QString &type) const
|
||||
{
|
||||
return fields(type, {resolve(type)});
|
||||
}
|
||||
|
||||
QVector<TypeLayout::Field> TypeLayout::fields(const QString &type, const QStringList &ancestors) const
|
||||
{
|
||||
const QString resolved = resolve(type);
|
||||
const QString category = kind(resolved);
|
||||
QVector<Field> result;
|
||||
if (category != QLatin1String("struct") && category != QLatin1String("union")) {
|
||||
return result;
|
||||
}
|
||||
const QString prefix = category + QLatin1Char('.') + resolved;
|
||||
const QStringList members = database.value(prefix).split(QLatin1Char(','), Qt::SkipEmptyParts);
|
||||
bool allZero = true;
|
||||
for (const QString &name : members) {
|
||||
const QStringList details
|
||||
= database.value(prefix + QLatin1Char('.') + name).split(QLatin1Char(','));
|
||||
if (details.size() < 3) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const quint64 offset = details.at(1).toULongLong(&ok, 0);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
const quint64 count = details.at(2).toULongLong(&ok, 0);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
result.append({name, details.first(), offset, qMax<quint64>(1, count)});
|
||||
allZero &= offset == 0;
|
||||
}
|
||||
// Some bundled r2 definitions omit offsets entirely. Their members are packed.
|
||||
if (allZero && category == QLatin1String("struct") && result.size() > 1) {
|
||||
quint64 offset = 0;
|
||||
for (Field &field : result) {
|
||||
field.offset = offset;
|
||||
const quint64 bytes = size(field.type, ancestors);
|
||||
if (bytes && field.count <= (std::numeric_limits<quint64>::max() - offset) / bytes) {
|
||||
offset += bytes * field.count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
quint64 TypeLayout::size(const QString &type) const
|
||||
{
|
||||
const auto cached = sizes.constFind(type);
|
||||
if (cached != sizes.cend()) {
|
||||
return cached.value();
|
||||
}
|
||||
const quint64 result = size(type, {});
|
||||
sizes.insert(type, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
quint64 TypeLayout::size(const QString &type, QStringList ancestors) const
|
||||
{
|
||||
const QString resolved = resolve(type);
|
||||
if (resolved.isEmpty() || ancestors.size() >= 32 || ancestors.contains(resolved)) {
|
||||
return 0;
|
||||
}
|
||||
ancestors.append(resolved);
|
||||
const QString category = kind(resolved);
|
||||
if (category == QLatin1String("pointer")) {
|
||||
return pointerSize;
|
||||
}
|
||||
QString element;
|
||||
quint64 count;
|
||||
if (array(resolved, &element, &count)) {
|
||||
const quint64 stride = size(element, ancestors);
|
||||
return stride && count <= std::numeric_limits<quint64>::max() / stride ? stride * count : 0;
|
||||
}
|
||||
if (category == QLatin1String("enum")) {
|
||||
return 4;
|
||||
}
|
||||
if (category == QLatin1String("struct") || category == QLatin1String("union")) {
|
||||
quint64 end = 0;
|
||||
for (const Field &field : fields(resolved, ancestors)) {
|
||||
const quint64 stride = size(field.type, ancestors);
|
||||
if (!stride
|
||||
|| field.count > (std::numeric_limits<quint64>::max() - field.offset) / stride) {
|
||||
return 0;
|
||||
}
|
||||
end = qMax(end, field.offset + stride * field.count);
|
||||
}
|
||||
return end;
|
||||
}
|
||||
bool ok = false;
|
||||
const qint64 bits = database.value(QStringLiteral("type.") + resolved + QStringLiteral(".size"))
|
||||
.toLongLong(&ok);
|
||||
return ok && bits > 0 ? quint64(bits) / 8 : 0;
|
||||
}
|
||||
|
||||
quint64 TypeLayout::unsignedValue(const QByteArray &bytes, bool bigEndian)
|
||||
{
|
||||
quint64 result = 0;
|
||||
for (int i = 0; i < qMin(8, int(bytes.size())); ++i) {
|
||||
const int shift = (bigEndian ? bytes.size() - i - 1 : i) * 8;
|
||||
if (shift < 64) {
|
||||
result |= quint64(static_cast<unsigned char>(bytes.at(i))) << shift;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString TypeLayout::value(const QString &type, const QByteArray &bytes, bool bigEndian) const
|
||||
{
|
||||
const QString resolved = resolve(type);
|
||||
const quint64 number = unsignedValue(bytes, bigEndian);
|
||||
if (kind(resolved) == QLatin1String("pointer")) {
|
||||
return QStringLiteral("0x%1").arg(number, 0, 16);
|
||||
}
|
||||
if (resolved == QLatin1String("float") && bytes.size() == 4) {
|
||||
const quint32 raw = quint32(number);
|
||||
float value;
|
||||
memcpy(&value, &raw, sizeof(value));
|
||||
return QString::number(value, 'g', 8);
|
||||
}
|
||||
if (resolved == QLatin1String("double") && bytes.size() == 8) {
|
||||
double value;
|
||||
memcpy(&value, &number, sizeof(value));
|
||||
return QString::number(value, 'g', 16);
|
||||
}
|
||||
if (bytes.size() > 8) {
|
||||
return QString::fromLatin1(bytes.toHex(' '));
|
||||
}
|
||||
const bool isUnsigned = resolved.startsWith(QLatin1String("uint"))
|
||||
|| resolved.startsWith(QLatin1String("unsigned"));
|
||||
quint64 extended = number;
|
||||
if (!isUnsigned && !bytes.isEmpty() && bytes.size() < 8
|
||||
&& (number & (quint64(1) << (bytes.size() * 8 - 1)))) {
|
||||
extended |= std::numeric_limits<quint64>::max() << (bytes.size() * 8);
|
||||
}
|
||||
qint64 signedNumber;
|
||||
memcpy(&signedNumber, &extended, sizeof(signedNumber));
|
||||
QString result = isUnsigned ? QString::number(number) : QString::number(signedNumber);
|
||||
if (kind(resolved) == QLatin1String("enum")) {
|
||||
const QString prefix = QStringLiteral("enum.") + resolved + QLatin1Char('.');
|
||||
for (auto it = database.cbegin(); it != database.cend(); ++it) {
|
||||
bool ok = false;
|
||||
const qint64 candidate = it.value().toLongLong(&ok, 0);
|
||||
if (it.key().startsWith(prefix) && ok && candidate == signedNumber) {
|
||||
result += QStringLiteral(" (%1)").arg(it.key().mid(prefix.size()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resolved == QLatin1String("char") && number >= 32 && number < 127) {
|
||||
result += QStringLiteral(" '%1'").arg(QChar(ushort(number)));
|
||||
}
|
||||
return result + QStringLiteral(" (0x%1)").arg(number, 0, 16);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef TYPELAYOUT_H
|
||||
#define TYPELAYOUT_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
// A snapshot of r2's type database. Keep stored offsets: pf formats and tsj can
|
||||
// lose padding, and JSON numbers cannot represent every 64-bit type-link address.
|
||||
class TypeLayout
|
||||
{
|
||||
public:
|
||||
struct Field
|
||||
{
|
||||
QString name;
|
||||
QString type;
|
||||
quint64 offset = 0;
|
||||
quint64 count = 1;
|
||||
};
|
||||
|
||||
void load(const QString &database, int pointerSize);
|
||||
QStringList names() const;
|
||||
QMap<quint64, QString> links() const;
|
||||
QString resolve(QString type) const;
|
||||
QString kind(const QString &type) const;
|
||||
QVector<Field> fields(const QString &type) const;
|
||||
quint64 size(const QString &type) const;
|
||||
QString value(const QString &type, const QByteArray &bytes, bool bigEndian) const;
|
||||
static quint64 unsignedValue(const QByteArray &bytes, bool bigEndian);
|
||||
static bool array(const QString &type, QString *element, quint64 *count);
|
||||
|
||||
private:
|
||||
QHash<QString, QString> database;
|
||||
int pointerSize = 8;
|
||||
mutable QHash<QString, quint64> sizes;
|
||||
QVector<Field> fields(const QString &type, const QStringList &ancestors) const;
|
||||
quint64 size(const QString &type, QStringList ancestors) const;
|
||||
};
|
||||
|
||||
#endif // TYPELAYOUT_H
|
||||
@@ -713,13 +713,19 @@ void IaitoStyle::drawControl(
|
||||
return;
|
||||
}
|
||||
const bool accent = m_theme.chromeStyle == ChromeStyle::Accent;
|
||||
const bool accentTabs = m_theme.chromeStyle == ChromeStyle::AccentTabs;
|
||||
painter->save();
|
||||
const bool selected = tab->state & State_Selected;
|
||||
const bool hovered = (tab->state & State_MouseOver) && !selected
|
||||
&& (tab->state & State_Enabled);
|
||||
QColor bg;
|
||||
QColor fg = m_theme.text.normal;
|
||||
if (accent) {
|
||||
if (accentTabs) {
|
||||
bg = selected ? m_theme.accent.normal
|
||||
: hovered ? m_theme.surface.hovered
|
||||
: m_theme.background.normal;
|
||||
fg = selected ? m_theme.accentText.normal : m_theme.text.normal;
|
||||
} else if (accent) {
|
||||
bg = selected ? m_theme.background.normal
|
||||
: hovered ? m_theme.accent.hovered
|
||||
: m_theme.accent.normal;
|
||||
|
||||
@@ -216,12 +216,26 @@ Skin skinFromString(const QString &s)
|
||||
|
||||
QString chromeToString(ChromeStyle c)
|
||||
{
|
||||
return c == ChromeStyle::Accent ? QStringLiteral("accent") : QStringLiteral("flat");
|
||||
switch (c) {
|
||||
case ChromeStyle::Accent:
|
||||
return QStringLiteral("accent");
|
||||
case ChromeStyle::AccentTabs:
|
||||
return QStringLiteral("accent-tabs");
|
||||
case ChromeStyle::Flat:
|
||||
return QStringLiteral("flat");
|
||||
}
|
||||
return QStringLiteral("flat");
|
||||
}
|
||||
|
||||
ChromeStyle chromeFromString(const QString &s)
|
||||
{
|
||||
return s == QLatin1String("accent") ? ChromeStyle::Accent : ChromeStyle::Flat;
|
||||
if (s == QLatin1String("accent")) {
|
||||
return ChromeStyle::Accent;
|
||||
}
|
||||
if (s == QLatin1String("accent-tabs")) {
|
||||
return ChromeStyle::AccentTabs;
|
||||
}
|
||||
return ChromeStyle::Flat;
|
||||
}
|
||||
|
||||
QString hintingToString(int h)
|
||||
|
||||
@@ -46,6 +46,7 @@ struct ThemeFonts
|
||||
enum class ChromeStyle {
|
||||
Flat,
|
||||
Accent,
|
||||
AccentTabs,
|
||||
};
|
||||
|
||||
enum class Skin {
|
||||
|
||||
+379
-50
@@ -22,6 +22,7 @@
|
||||
#include <QPoint>
|
||||
#include <QRect>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QSet>
|
||||
#include <QSizeGrip>
|
||||
#include <QTabBar>
|
||||
#include <QTimer>
|
||||
@@ -43,6 +44,28 @@ static const char *dockHandleResizeGripProperty = "iaitoDockHandleResizeGrip";
|
||||
static const char *dockTabCloseButtonIndexProperty = "iaitoDockTabCloseButtonIndex";
|
||||
static const char *dockTabBarConfiguredProperty = "iaitoDockTabBarConfigured";
|
||||
|
||||
// Four opaque strips outline the active frame without changing its layout.
|
||||
// Keeping them narrow avoids translucent child compositing artifacts on macOS.
|
||||
class DockFocusBorder : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit DockFocusBorder(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setObjectName(QStringLiteral("activeFrameBorder"));
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), palette().highlight());
|
||||
}
|
||||
};
|
||||
|
||||
// Translucent highlight, in accent color, of the region a dragged panel would
|
||||
// occupy if dropped now. Transparent to mouse input so it never interferes with
|
||||
// the drag. Has two modes (see updateDropOverlay):
|
||||
@@ -132,6 +155,10 @@ QWidget *newDockDragHandleTitleBar(QWidget *parent, bool withControls, bool with
|
||||
// Tabbed panel: the tab already shows the title and the close-on-hover
|
||||
// button, so the bar is just a thin grip for dragging the panel around.
|
||||
handle->setFixedHeight(8);
|
||||
// Without a size hint the dock layout lays the content out under the grip
|
||||
auto *gripLayout = new QHBoxLayout(handle);
|
||||
gripLayout->setContentsMargins(0, 0, 0, 0);
|
||||
gripLayout->addSpacerItem(new QSpacerItem(0, 8, QSizePolicy::Expanding, QSizePolicy::Fixed));
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -195,15 +222,13 @@ QString panelDisplayName(const IaitoDockWidget *dock)
|
||||
name.chop(3);
|
||||
name = name.trimmed();
|
||||
}
|
||||
while (name.endsWith(QChar(0x2026))) {
|
||||
name.chop(1);
|
||||
name = name.trimmed();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
QString normalizedDockTabText(QString text)
|
||||
{
|
||||
text.remove(QLatin1Char('&'));
|
||||
return text.trimmed();
|
||||
}
|
||||
|
||||
QPoint mouseEventGlobalPos(const QMouseEvent *event)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
@@ -313,26 +338,54 @@ DockManager::DockManager(MainWindow *mainWindow)
|
||||
, backend(new QtDockBackend(mainWindow))
|
||||
{
|
||||
dockWidgets.reserve(20);
|
||||
connect(mainWindow, &QMainWindow::tabifiedDockWidgetActivated, this, [this](QDockWidget *dock) {
|
||||
auto *panel = qobject_cast<IaitoDockWidget *>(dock);
|
||||
if (dockWidgets.contains(panel)) {
|
||||
setActiveDock(panel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
DockManager::~DockManager()
|
||||
{
|
||||
for (auto border : activeFrameBorders) {
|
||||
delete border;
|
||||
}
|
||||
delete dropOverlay;
|
||||
delete backend;
|
||||
}
|
||||
|
||||
void DockManager::addWidget(IaitoDockWidget *widget)
|
||||
{
|
||||
restoreMaximizedPanel();
|
||||
dockWidgets.push_back(widget);
|
||||
connect(widget, &QDockWidget::visibilityChanged, this, [this](bool) {
|
||||
connect(widget, &QDockWidget::visibilityChanged, this, [this, widget](bool) {
|
||||
if (!dockRelayoutInProgress) {
|
||||
const bool hidden = widget->isHidden();
|
||||
if (!maximizedLayoutState.isEmpty()
|
||||
&& ((widget == maximizedDock && hidden)
|
||||
|| (widget != maximizedDock && !hidden && !widget->isFloating()))) {
|
||||
// Closing the expanded panel or opening another panel leaves
|
||||
// maximization while preserving the requested visibility.
|
||||
restoreMaximizedPanel();
|
||||
widget->setVisible(!hidden);
|
||||
if (!hidden) {
|
||||
widget->raise();
|
||||
}
|
||||
}
|
||||
emit layoutMutated();
|
||||
}
|
||||
requestUpdateActiveFrame();
|
||||
});
|
||||
connect(widget, &QDockWidget::topLevelChanged, this, [this](bool) {
|
||||
connect(widget, &QDockWidget::topLevelChanged, this, [this, widget](bool floating) {
|
||||
if (!dockRelayoutInProgress) {
|
||||
if (!maximizedLayoutState.isEmpty()) {
|
||||
restoreMaximizedPanel();
|
||||
widget->setFloating(floating);
|
||||
}
|
||||
emit layoutMutated();
|
||||
}
|
||||
requestUpdateActiveFrame();
|
||||
});
|
||||
requestUpdateTabBars();
|
||||
emit panelAdded(widget);
|
||||
@@ -341,8 +394,12 @@ void DockManager::addWidget(IaitoDockWidget *widget)
|
||||
|
||||
void DockManager::removeWidget(IaitoDockWidget *widget)
|
||||
{
|
||||
restoreMaximizedPanel();
|
||||
dockWidgets.removeAll(widget);
|
||||
pluginDocks.removeAll(widget);
|
||||
if (activeDock == widget) {
|
||||
setActiveDock(nullptr);
|
||||
}
|
||||
requestUpdateTabBars();
|
||||
emit panelRemoved(widget);
|
||||
emit layoutMutated();
|
||||
@@ -482,14 +539,78 @@ void DockManager::updateDockTabBars()
|
||||
for (auto *dockWidget : dockWidgets) {
|
||||
configureDockWidget(dockWidget);
|
||||
}
|
||||
QSet<IaitoDockWidget *> claimed;
|
||||
for (auto *tabBar : backend->dockTabBars()) {
|
||||
if (updateEmptyDockTabBar(tabBar)) {
|
||||
continue;
|
||||
}
|
||||
if (configureDockTabBar(tabBar)) {
|
||||
stampTabBar(tabBar, claimed);
|
||||
configureDockTabBar(tabBar);
|
||||
}
|
||||
updateActiveFrame();
|
||||
}
|
||||
|
||||
void DockManager::setActiveDock(IaitoDockWidget *dock)
|
||||
{
|
||||
if (activeDock == dock) {
|
||||
return;
|
||||
}
|
||||
activeDock = dock;
|
||||
requestUpdateActiveFrame();
|
||||
}
|
||||
|
||||
void DockManager::requestUpdateActiveFrame()
|
||||
{
|
||||
if (activeFrameUpdateScheduled) {
|
||||
return;
|
||||
}
|
||||
activeFrameUpdateScheduled = true;
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
activeFrameUpdateScheduled = false;
|
||||
updateActiveFrame();
|
||||
});
|
||||
}
|
||||
|
||||
void DockManager::updateActiveFrame()
|
||||
{
|
||||
auto *dock = activeDock.data();
|
||||
if (!dock || dock->visibleRegion().isEmpty()) {
|
||||
for (auto border : activeFrameBorders) {
|
||||
if (border) {
|
||||
border->hide();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QWidget *owner = dock->window();
|
||||
QRect frame(dock->mapTo(owner, QPoint()), dock->size());
|
||||
for (auto *tabBar : backend->dockTabBars()) {
|
||||
if (isMainDockTabBar(tabBar) && tabBar->window() == owner
|
||||
&& dockForDockTab(tabBar, tabBar->currentIndex()) == dock) {
|
||||
frame = frame.united(QRect(tabBar->mapTo(owner, QPoint()), tabBar->size()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const int width = 2;
|
||||
const QRect edges[] = {
|
||||
QRect(frame.left(), frame.top(), frame.width(), width),
|
||||
QRect(frame.left(), frame.bottom() - width + 1, frame.width(), width),
|
||||
QRect(frame.left(), frame.top(), width, frame.height()),
|
||||
QRect(frame.right() - width + 1, frame.top(), width, frame.height()),
|
||||
};
|
||||
while (activeFrameBorders.size() < 4) {
|
||||
activeFrameBorders.append(nullptr);
|
||||
}
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
auto &border = activeFrameBorders[i];
|
||||
if (!border) {
|
||||
border = new DockFocusBorder(owner);
|
||||
} else if (border->parentWidget() != owner) {
|
||||
border->setParent(owner);
|
||||
}
|
||||
border->setGeometry(edges[i]);
|
||||
border->show();
|
||||
border->raise();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +681,7 @@ bool DockManager::configureDockTabBar(QTabBar *tabBar)
|
||||
|
||||
bool DockManager::isMainDockTabBar(QTabBar *tabBar) const
|
||||
{
|
||||
if (!tabBar || tabBar->count() == 0 || !tabBar->isVisible()) {
|
||||
if (!tabBar || tabBar->count() == 0 || tabBar->visibleRegion().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -572,30 +693,6 @@ bool DockManager::isMainDockTabBar(QTabBar *tabBar) const
|
||||
return true;
|
||||
}
|
||||
|
||||
void DockManager::stampTabBar(QTabBar *tabBar, QSet<IaitoDockWidget *> &claimed)
|
||||
{
|
||||
// Qt labels each tab with the dock's window title. Resolve every tab to its
|
||||
// dock once (claiming across all bars so duplicate titles map bijectively)
|
||||
// and record the dock pointer in tabData, so later lookups are exact instead
|
||||
// of relying on title-string matching.
|
||||
for (int i = 0; i < tabBar->count(); ++i) {
|
||||
const QString want = normalizedDockTabText(tabBar->tabText(i));
|
||||
if (want.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (auto *dock : dockWidgets) {
|
||||
if (claimed.contains(dock)) {
|
||||
continue;
|
||||
}
|
||||
if (normalizedDockTabText(panelDisplayName(dock)) == want) {
|
||||
tabBar->setTabData(i, QVariant::fromValue<QObject *>(dock));
|
||||
claimed.insert(dock);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DockManager::updateDockTabCloseButtons(QTabBar *tabBar, int hoveredIndex)
|
||||
{
|
||||
if (!tabBar) {
|
||||
@@ -724,6 +821,7 @@ void DockManager::showDockContextMenu(QWidget *parent, IaitoDockWidget *dock, co
|
||||
|
||||
void DockManager::placeDockTab(IaitoDockWidget *dock, DockDropKind kind)
|
||||
{
|
||||
restoreMaximizedPanel();
|
||||
IaitoDockWidget *targetDock = dockSplitReference(dock);
|
||||
if (!targetDock) {
|
||||
return;
|
||||
@@ -753,6 +851,7 @@ void DockManager::detachDockTab(IaitoDockWidget *dock, const QPoint &globalPos)
|
||||
return;
|
||||
}
|
||||
|
||||
restoreMaximizedPanel();
|
||||
backend->removeDock(dock);
|
||||
dock->setParent(mainWindow);
|
||||
backend->setDockFloating(dock, true);
|
||||
@@ -1324,27 +1423,236 @@ bool DockManager::closeCurrentTab()
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DockManager::selectCurrentPanelTab(int index)
|
||||
{
|
||||
if (index < 0 || dockTabDragActive) {
|
||||
return false;
|
||||
}
|
||||
// Expanded panels temporarily hide their sibling tabs. Restore the group
|
||||
// to resolve its visual order, then expand the newly selected panel again.
|
||||
const bool wasMaximized = !maximizedLayoutState.isEmpty();
|
||||
restoreMaximizedPanel();
|
||||
QPointer<IaitoDockWidget> dock = activeDock;
|
||||
if (!dock || dock->isHidden()) {
|
||||
dock = dockForWidget(QApplication::focusWidget());
|
||||
}
|
||||
|
||||
if (wasMaximized) {
|
||||
flushNativeDockLayout();
|
||||
}
|
||||
IaitoDockWidget *target = nullptr;
|
||||
if (dock && !dock->isHidden()) {
|
||||
for (auto *tabBar : backend->dockTabBars()) {
|
||||
if (!isMainDockTabBar(tabBar) || index >= tabBar->count()) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < tabBar->count(); ++i) {
|
||||
if (dockForDockTab(tabBar, i) == dock) {
|
||||
target = dockForDockTab(tabBar, index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target) {
|
||||
target->raiseMemoryWidget();
|
||||
setActiveDock(target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wasMaximized) {
|
||||
toggleCurrentPanelMaximized();
|
||||
}
|
||||
return target != nullptr;
|
||||
}
|
||||
|
||||
bool DockManager::focusNextFrame(bool forward)
|
||||
{
|
||||
if (dockTabDragActive) {
|
||||
return false;
|
||||
}
|
||||
const bool wasMaximized = !maximizedLayoutState.isEmpty();
|
||||
restoreMaximizedPanel();
|
||||
QPointer<IaitoDockWidget> current = activeDock;
|
||||
if (!current || current->isHidden()) {
|
||||
current = dockForWidget(QApplication::focusWidget());
|
||||
}
|
||||
if (wasMaximized) {
|
||||
flushNativeDockLayout();
|
||||
}
|
||||
|
||||
struct Frame
|
||||
{
|
||||
IaitoDockWidget *panel;
|
||||
QRect rect;
|
||||
};
|
||||
QList<Frame> frames;
|
||||
for (auto *dock : dockWidgets) {
|
||||
// Inactive tabs are parked offscreen by Qt. Only the selected panel
|
||||
// represents a frame; floating windows are outside the main layout.
|
||||
if (dock->isFloating() || dock->parentWidget() != mainWindow || !dock->isEnabled()
|
||||
|| dock->visibleRegion().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
QRect rect = dock->geometry();
|
||||
for (auto *tabBar : backend->dockTabBars()) {
|
||||
if (isMainDockTabBar(tabBar) && dockForDockTab(tabBar, tabBar->currentIndex()) == dock) {
|
||||
// Include the tab bar so tabbed and standalone frames in the
|
||||
// same row sort together regardless of their content offset.
|
||||
rect = rect.united(tabBar->geometry());
|
||||
break;
|
||||
}
|
||||
}
|
||||
frames.append({dock, rect});
|
||||
}
|
||||
std::stable_sort(frames.begin(), frames.end(), [](const Frame &a, const Frame &b) {
|
||||
return a.rect.top() == b.rect.top() ? a.rect.left() < b.rect.left()
|
||||
: a.rect.top() < b.rect.top();
|
||||
});
|
||||
|
||||
QPointer<IaitoDockWidget> target;
|
||||
QPointer<QWidget> focus;
|
||||
if (!frames.isEmpty()) {
|
||||
int index = -1;
|
||||
for (int i = 0; i < frames.size(); ++i) {
|
||||
if (frames.at(i).panel == current) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const int next = index < 0 ? (forward ? 0 : frames.size() - 1)
|
||||
: (index + (forward ? 1 : frames.size() - 1)) % frames.size();
|
||||
target = frames.at(next).panel;
|
||||
focus = target->focusWidget();
|
||||
target->raiseMemoryWidget();
|
||||
setActiveDock(target);
|
||||
}
|
||||
if (wasMaximized) {
|
||||
toggleCurrentPanelMaximized();
|
||||
}
|
||||
if (target) {
|
||||
target->activateWindow();
|
||||
if (focus && focus->isEnabled() && focus->isVisibleTo(target)) {
|
||||
focus->setFocus(Qt::ShortcutFocusReason);
|
||||
if (maximizedDock == target) {
|
||||
maximizedFocusWidget = focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
return target != nullptr;
|
||||
}
|
||||
|
||||
IaitoDockWidget *DockManager::dockForWidget(QWidget *widget) const
|
||||
{
|
||||
for (; widget; widget = widget->parentWidget()) {
|
||||
if (auto *tabBar = qobject_cast<QTabBar *>(widget)) {
|
||||
if (isMainDockTabBar(tabBar)) {
|
||||
return dockForDockTab(tabBar, tabBar->currentIndex());
|
||||
}
|
||||
}
|
||||
if (auto *dock = qobject_cast<IaitoDockWidget *>(widget)) {
|
||||
if (dockWidgets.contains(dock)) {
|
||||
return dock;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QByteArray DockManager::saveState() const
|
||||
{
|
||||
// Maximization is temporary: saved layouts retain the tiled arrangement.
|
||||
return maximizedLayoutState.isEmpty() ? backend->saveState() : maximizedLayoutState;
|
||||
}
|
||||
|
||||
bool DockManager::toggleCurrentPanelMaximized()
|
||||
{
|
||||
if (!maximizedLayoutState.isEmpty()) {
|
||||
restoreMaximizedPanel();
|
||||
return true;
|
||||
}
|
||||
if (dockTabDragActive) {
|
||||
return false;
|
||||
}
|
||||
IaitoDockWidget *dock = activeDock;
|
||||
if (!dock || dock->isHidden()) {
|
||||
dock = dockForWidget(QApplication::focusWidget());
|
||||
}
|
||||
if (!dock || dock->isHidden()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QScopedValueRollback<bool> relayoutGuard(dockRelayoutInProgress, true);
|
||||
maximizedLayoutState = backend->saveState();
|
||||
maximizedDock = dock;
|
||||
QWidget *focus = QApplication::focusWidget();
|
||||
maximizedFocusWidget = focus && (focus == dock || dock->isAncestorOf(focus)) ? focus : nullptr;
|
||||
maximizedDockMaximumSize = dock->maximumSize();
|
||||
dock->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
|
||||
|
||||
const bool wasAnimated = mainWindow->isAnimated();
|
||||
mainWindow->setAnimated(false);
|
||||
if (dock->isFloating()) {
|
||||
backend->setDockFloating(dock, false);
|
||||
}
|
||||
if (backend->areaOf(dock) == Qt::NoDockWidgetArea) {
|
||||
backend->addDock(dock, Qt::TopDockWidgetArea);
|
||||
}
|
||||
for (auto *other : dockWidgets) {
|
||||
if (other != dock && !other->isFloating()) {
|
||||
other->hide();
|
||||
}
|
||||
}
|
||||
dock->raiseMemoryWidget();
|
||||
if (maximizedFocusWidget) {
|
||||
maximizedFocusWidget->setFocus(Qt::ShortcutFocusReason);
|
||||
}
|
||||
setActiveDock(dock);
|
||||
mainWindow->setAnimated(wasAnimated);
|
||||
requestUpdateTabBars();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DockManager::restoreMaximizedPanel()
|
||||
{
|
||||
if (maximizedLayoutState.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QScopedValueRollback<bool> relayoutGuard(dockRelayoutInProgress, true);
|
||||
const QByteArray state = maximizedLayoutState;
|
||||
const QPointer<IaitoDockWidget> dock = maximizedDock;
|
||||
const QPointer<QWidget> focus = maximizedFocusWidget;
|
||||
maximizedLayoutState.clear();
|
||||
maximizedDock.clear();
|
||||
maximizedFocusWidget.clear();
|
||||
if (dock) {
|
||||
dock->setMaximumSize(maximizedDockMaximumSize);
|
||||
}
|
||||
const bool wasAnimated = mainWindow->isAnimated();
|
||||
mainWindow->setAnimated(false);
|
||||
backend->restoreState(state);
|
||||
mainWindow->setAnimated(wasAnimated);
|
||||
if (dock && !dock->isHidden()) {
|
||||
dock->raiseMemoryWidget();
|
||||
if (focus) {
|
||||
focus->setFocus(Qt::ShortcutFocusReason);
|
||||
}
|
||||
setActiveDock(dock);
|
||||
}
|
||||
requestUpdateTabBars();
|
||||
}
|
||||
|
||||
IaitoDockWidget *DockManager::dockForDockTab(QTabBar *tabBar, int index) const
|
||||
{
|
||||
if (!tabBar || index < 0 || index >= tabBar->count()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (QObject *object = tabBar->tabData(index).value<QObject *>()) {
|
||||
if (auto *dock = qobject_cast<IaitoDockWidget *>(object)) {
|
||||
if (dockWidgets.contains(dock)) {
|
||||
return dock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QString requestedText = normalizedDockTabText(tabBar->tabText(index));
|
||||
if (requestedText.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// QMainWindow owns tabData: Qt 5/6 store the dock's QWidget address as a
|
||||
// quintptr and use it to preserve the active tab during layout changes and
|
||||
// saveState(). Read it without overwriting it or guessing from tab titles.
|
||||
const quintptr dockId = tabBar->tabData(index).value<quintptr>();
|
||||
for (auto *dock : dockWidgets) {
|
||||
if (dock && normalizedDockTabText(panelDisplayName(dock)) == requestedText) {
|
||||
if (reinterpret_cast<quintptr>(static_cast<QWidget *>(dock)) == dockId) {
|
||||
return dock;
|
||||
}
|
||||
}
|
||||
@@ -1441,6 +1749,7 @@ void DockManager::startDockWidgetDrag(
|
||||
return;
|
||||
}
|
||||
|
||||
restoreMaximizedPanel();
|
||||
dockTabDragActive = true;
|
||||
dockDragOffset = offset;
|
||||
updateDropOverlay(globalPos);
|
||||
@@ -1736,6 +2045,24 @@ void DockManager::clearDockDrag()
|
||||
|
||||
bool DockManager::handleEvent(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::FocusIn || event->type() == QEvent::MouseButtonPress) {
|
||||
if (auto *dock = dockForWidget(qobject_cast<QWidget *>(watched))) {
|
||||
setActiveDock(dock);
|
||||
}
|
||||
}
|
||||
if (watched == activeDock || watched == mainWindow) {
|
||||
switch (event->type()) {
|
||||
case QEvent::Move:
|
||||
case QEvent::Resize:
|
||||
case QEvent::Show:
|
||||
case QEvent::Hide:
|
||||
case QEvent::LayoutRequest:
|
||||
requestUpdateActiveFrame();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dockTabDragActive) {
|
||||
if (event->type() == QEvent::KeyPress
|
||||
&& static_cast<QKeyEvent *>(event)->key() == Qt::Key_Escape) {
|
||||
@@ -1812,6 +2139,7 @@ bool DockManager::handleEvent(QObject *watched, QEvent *event)
|
||||
clearDockDrag();
|
||||
dockDragTabBar = tabBar;
|
||||
dockDragWidget = dockForDockTab(tabBar, index);
|
||||
setActiveDock(dockDragWidget);
|
||||
dockDragStartGlobalPos = mouseEventGlobalPos(mouseEvent);
|
||||
}
|
||||
break;
|
||||
@@ -1849,6 +2177,7 @@ bool DockManager::handleEvent(QObject *watched, QEvent *event)
|
||||
case QEvent::Show:
|
||||
case QEvent::Resize:
|
||||
case QEvent::LayoutRequest: {
|
||||
requestUpdateActiveFrame();
|
||||
QPointer<QTabBar> guardedTabBar(tabBar);
|
||||
QTimer::singleShot(0, this, [this, guardedTabBar]() {
|
||||
if (guardedTabBar) {
|
||||
|
||||
+19
-2
@@ -3,13 +3,14 @@
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QPoint>
|
||||
#include <QPointer>
|
||||
#include <QRect>
|
||||
#include <QSet>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
@@ -71,6 +72,11 @@ public:
|
||||
void updateDockTabBars();
|
||||
void requestUpdateTabBars();
|
||||
bool closeCurrentTab();
|
||||
bool selectCurrentPanelTab(int index);
|
||||
bool focusNextFrame(bool forward);
|
||||
bool toggleCurrentPanelMaximized();
|
||||
void restoreMaximizedPanel();
|
||||
QByteArray saveState() const;
|
||||
|
||||
bool handleEvent(QObject *watched, QEvent *event);
|
||||
|
||||
@@ -82,11 +88,14 @@ signals:
|
||||
void layoutMutated();
|
||||
|
||||
private:
|
||||
void setActiveDock(IaitoDockWidget *dock);
|
||||
void requestUpdateActiveFrame();
|
||||
void updateActiveFrame();
|
||||
IaitoDockWidget *dockForWidget(QWidget *widget) const;
|
||||
void configureDockWidget(QDockWidget *dock);
|
||||
bool updateEmptyDockTabBar(QTabBar *tabBar);
|
||||
bool configureDockTabBar(QTabBar *tabBar);
|
||||
bool isMainDockTabBar(QTabBar *tabBar) const;
|
||||
void stampTabBar(QTabBar *tabBar, QSet<IaitoDockWidget *> &claimed);
|
||||
void updateDockTabCloseButtons(QTabBar *tabBar, int hoveredIndex = -1);
|
||||
QToolButton *dockTabCloseButton(QTabBar *tabBar);
|
||||
void closeDockTab(QTabBar *tabBar, int index);
|
||||
@@ -122,6 +131,14 @@ private:
|
||||
QList<IaitoDockWidget *> pluginDocks;
|
||||
QMap<QString, std::function<IaitoDockWidget *(MainWindow *)>> widgetTypeToConstructorMap;
|
||||
|
||||
QPointer<IaitoDockWidget> activeDock;
|
||||
QList<QPointer<QWidget>> activeFrameBorders;
|
||||
bool activeFrameUpdateScheduled = false;
|
||||
QPointer<IaitoDockWidget> maximizedDock;
|
||||
QPointer<QWidget> maximizedFocusWidget;
|
||||
QByteArray maximizedLayoutState;
|
||||
QSize maximizedDockMaximumSize;
|
||||
|
||||
QPointer<QTabBar> dockDragTabBar;
|
||||
QPointer<QWidget> dockDragHandle;
|
||||
QPointer<IaitoDockWidget> dockDragWidget;
|
||||
|
||||
+373
-23
@@ -21,6 +21,7 @@
|
||||
#include "common/R2Shims.h"
|
||||
#include "common/R2Task.h"
|
||||
#include "common/TempConfig.h"
|
||||
#include "common/TypeLayout.h"
|
||||
#include "core/Iaito.h"
|
||||
#include "plugins/PluginManager.h"
|
||||
|
||||
@@ -177,14 +178,14 @@ static InstructionProbeResult probeInstructionAddress(RCore *core, RVA address,
|
||||
{
|
||||
InstructionProbeResult result;
|
||||
ut8 buf[64];
|
||||
int readSize = r_io_read_at(core->io, address, buf, probeSize);
|
||||
if (readSize < 1) {
|
||||
probeSize = qBound(1, probeSize, (int) sizeof(buf));
|
||||
if (!r_io_read_at(core->io, address, buf, probeSize)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
RAnalOp op;
|
||||
r_anal_op_init(&op);
|
||||
int ret = r_anal_op(core->anal, &op, address, buf, readSize, R_ARCH_OP_MASK_BASIC);
|
||||
int ret = r_anal_op(core->anal, &op, address, buf, probeSize, R_ARCH_OP_MASK_BASIC);
|
||||
result.size = op.size > 0 ? op.size : 1;
|
||||
result.valid = ret > 0 && op.size > 0 && op.type != R_ANAL_OP_TYPE_ILL;
|
||||
r_anal_op_fini(&op);
|
||||
@@ -776,8 +777,20 @@ bool IaitoCore::loadFile(
|
||||
r_config_set_i(core->config, "io.va", va);
|
||||
r_config_set_b(core->config, "bin.cache", bincache);
|
||||
|
||||
// An empty "map offset" field results in 0, but the callers that don't go
|
||||
// through the dialog may pass RVA_INVALID. Normalize both to "not given".
|
||||
const bool haveMapAddr = (mapaddr != 0 && mapaddr != RVA_INVALID);
|
||||
const ut64 realMapAddr = haveMapAddr ? mapaddr : 0;
|
||||
// Address where the contents of the file are mapped (bin.laddr).
|
||||
// Headerless files are mapped there as a whole, binaries with sections
|
||||
// are mapped by RBin and ignore it. When there's no map offset use the
|
||||
// base address, so a headerless file loaded at a given base address is
|
||||
// also mapped there instead of exposing its contents somewhere else.
|
||||
// (this is what r2 warns about with "Don't use -B on unknown files")
|
||||
const ut64 loadAddr = haveMapAddr ? realMapAddr : (baddr == RVA_INVALID ? 0 : baddr);
|
||||
|
||||
Core()->loadIaitoRC(0);
|
||||
RIODesc *f = core_file_open_strip_file_uri(core, path.toUtf8().constData(), perms, mapaddr);
|
||||
RIODesc *f = core_file_open_strip_file_uri(core, path.toUtf8().constData(), perms, loadAddr);
|
||||
if (!f) {
|
||||
R_LOG_ERROR("r_core_file_open failed");
|
||||
return false;
|
||||
@@ -789,7 +802,17 @@ bool IaitoCore::loadFile(
|
||||
|
||||
bool haveEntry = true;
|
||||
if (loadbin && va) {
|
||||
if (!r_core_bin_load(core, path.toUtf8().constData(), baddr)) {
|
||||
// When a map offset is given, RBin must use it as base address too.
|
||||
// Otherwise all the addresses coming from the bin object (strings,
|
||||
// symbols, sections, imports, ...) are relative to the base address
|
||||
// stored in the binary, while its contents are mapped somewhere
|
||||
// else, so every widget ends up pointing outside of the io maps.
|
||||
// r2 does something similar with `oba 0 <mapaddr>` when using -m.
|
||||
ut64 binAddr = baddr;
|
||||
if (haveMapAddr && baddr == RVA_INVALID) {
|
||||
binAddr = realMapAddr;
|
||||
}
|
||||
if (!r_core_bin_load(core, path.toUtf8().constData(), binAddr)) {
|
||||
R_LOG_ERROR("Cannot find rbin information");
|
||||
}
|
||||
|
||||
@@ -806,13 +829,10 @@ bool IaitoCore::loadFile(
|
||||
#endif
|
||||
} else {
|
||||
// Loading shellcodes here
|
||||
ut64 addr = mapaddr != 0 ? mapaddr : baddr;
|
||||
char *at = strdup(QString::number(addr).toUtf8().constData());
|
||||
r_core_cmdf(core, "'-e bin.laddr=%s", at);
|
||||
r_core_cmdf(core, "'om 3 %s", at);
|
||||
r_core_cmdf(core, "'s %s", at);
|
||||
r_core_cmdf(core, "'-e bin.laddr=0x%" PFMT64x, loadAddr);
|
||||
r_core_cmdf(core, "'om %d 0x%" PFMT64x, f->fd, loadAddr);
|
||||
r_core_cmdf(core, "'s 0x%" PFMT64x, loadAddr);
|
||||
haveEntry = false;
|
||||
free(at);
|
||||
}
|
||||
r_core_bin_export_info(core, R_MODE_SET);
|
||||
|
||||
@@ -823,8 +843,16 @@ bool IaitoCore::loadFile(
|
||||
*/
|
||||
auto debug = r_config_get_b(core->config, "cfg.debug");
|
||||
|
||||
if (haveEntry && !debug && r_flag_get(core->flags, "entry0")) {
|
||||
r_core_cmd0(core, "'s entry0");
|
||||
if (!debug) {
|
||||
if (haveMapAddr) {
|
||||
// like `r2 -m`, seek where the file has been mapped
|
||||
r_core_seek(core, realMapAddr, true);
|
||||
} else if (haveEntry && r_flag_get(core->flags, "entry0")) {
|
||||
r_core_cmd0(core, "'s entry0");
|
||||
} else if (loadAddr) {
|
||||
// no entrypoint, at least seek where the contents are
|
||||
r_core_seek(core, loadAddr, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (perms & R_PERM_W) {
|
||||
@@ -864,10 +892,14 @@ bool IaitoCore::tryFile(QString path, bool rw)
|
||||
bool IaitoCore::mapFile(QString path, RVA mapaddr)
|
||||
{
|
||||
CORE_LOCK();
|
||||
RVA addr = mapaddr != RVA_INVALID ? mapaddr : 0;
|
||||
ut64 baddr = Core()->getFileInfo().object()["bin"].toObject()["baddr"].toVariant().toULongLong();
|
||||
const bool haveMapAddr = (mapaddr != 0 && mapaddr != RVA_INVALID);
|
||||
const RVA addr = haveMapAddr ? mapaddr : 0;
|
||||
if (core_file_open_strip_file_uri(core, path.toUtf8().constData(), R_PERM_RX, addr)) {
|
||||
r_core_bin_load(core, path.toUtf8().constData(), baddr);
|
||||
// Load the bin information at the address where the file is mapped,
|
||||
// otherwise its strings, symbols and sections are shown at addresses
|
||||
// that don't match the contents of the io maps. Without a map address
|
||||
// let RBin use the base address defined by the file itself.
|
||||
r_core_bin_load(core, path.toUtf8().constData(), haveMapAddr ? addr : UT64_MAX);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -1289,12 +1321,47 @@ RVA IaitoCore::getOffset()
|
||||
return ADDRESS_OF(core_);
|
||||
}
|
||||
|
||||
QString IaitoCore::getCommandAnnotation(RVA address, RVA *size)
|
||||
{
|
||||
CORE_LOCK();
|
||||
if (size) {
|
||||
*size = 0;
|
||||
}
|
||||
RAnalMetaItem *item = r_meta_get_at(core->anal, address, R_META_TYPE_RUN, size);
|
||||
return item && item->str ? QString::fromUtf8(item->str) : QString();
|
||||
}
|
||||
|
||||
void IaitoCore::setCommandAnnotation(RVA address, RVA size, const QString &command)
|
||||
{
|
||||
CORE_LOCK();
|
||||
// Store the command verbatim without passing it through the r2 command parser.
|
||||
r_meta_set(core->anal, R_META_TYPE_RUN, address, size, command.toUtf8().constData());
|
||||
emit refreshCodeViews();
|
||||
}
|
||||
|
||||
ut64 IaitoCore::math(const QString &expr)
|
||||
{
|
||||
CORE_LOCK();
|
||||
return r_num_math(core ? core->num : NULL, expr.toUtf8().constData());
|
||||
}
|
||||
|
||||
bool IaitoCore::tryMath(const QString &expr, ut64 &value)
|
||||
{
|
||||
const QByteArray expression = expr.trimmed().toUtf8();
|
||||
if (expression.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CORE_LOCK();
|
||||
RNum *number = core ? core->num : nullptr;
|
||||
if (!number) {
|
||||
return false;
|
||||
}
|
||||
const char *error = nullptr;
|
||||
value = r_num_math_err(number, expression.constData(), &error);
|
||||
return !error && !number->dbz && !r_num_failed(number);
|
||||
}
|
||||
|
||||
ut64 IaitoCore::num(const QString &expr)
|
||||
{
|
||||
CORE_LOCK();
|
||||
@@ -1554,7 +1621,12 @@ RVA IaitoCore::getLastFunctionInstruction(RVA addr)
|
||||
if (!fcn) {
|
||||
return RVA_INVALID;
|
||||
}
|
||||
#if R2_ABIVERSION >= 168
|
||||
RAnalBlock **lastBBPtr = RVecAnalBlockPtr_last(&fcn->bbs);
|
||||
RAnalBlock *lastBB = lastBBPtr ? *lastBBPtr : nullptr;
|
||||
#else
|
||||
RAnalBlock *lastBB = (RAnalBlock *) r_list_last(fcn->bbs);
|
||||
#endif
|
||||
return lastBB ? lastBB->addr + r_anal_bb_offset_inst(lastBB, lastBB->ninstr - 1) : RVA_INVALID;
|
||||
}
|
||||
|
||||
@@ -3259,6 +3331,26 @@ QList<RMutaPluginDescription> IaitoCore::getRMutaPluginDescriptions()
|
||||
return ret;
|
||||
}
|
||||
|
||||
QList<ExceptionRegion> IaitoCore::getExceptionRegions(RVA function)
|
||||
{
|
||||
if (exceptionJsonSupport < 0) {
|
||||
// Older iw implementations print flag commands even for iwj. Probe help
|
||||
// once, without calling unsupported commands or depending on a new ABI.
|
||||
const QString help = cmd("i?");
|
||||
const QRegularExpression jsonHelp(QStringLiteral("\\biw\\[[^\\]\\n]*j"));
|
||||
exceptionJsonSupport = help.contains(jsonHelp) ? 1 : 0;
|
||||
}
|
||||
if (!exceptionJsonSupport) {
|
||||
return {};
|
||||
}
|
||||
const auto regions = ExceptionRegion::parse(cmdj("iwj").array());
|
||||
if (function == RVA_INVALID || regions.isEmpty()) {
|
||||
return regions;
|
||||
}
|
||||
const auto blocks = cmdj("afbj @ " + RAddressString(function)).array();
|
||||
return ExceptionRegion::forBlocks(regions, blocks);
|
||||
}
|
||||
|
||||
QList<FunctionDescription> IaitoCore::getAllFunctions()
|
||||
{
|
||||
CORE_LOCK();
|
||||
@@ -3280,7 +3372,11 @@ QList<FunctionDescription> IaitoCore::getAllFunctions()
|
||||
function.nlocals = r_anal_var_count(core->anal, fcn, 'b', 0)
|
||||
+ r_anal_var_count(core->anal, fcn, 'r', 0)
|
||||
+ r_anal_var_count(core->anal, fcn, 's', 0);
|
||||
#if R2_ABIVERSION >= 168
|
||||
function.nbbs = RVecAnalBlockPtr_length(&fcn->bbs);
|
||||
#else
|
||||
function.nbbs = r_list_length(fcn->bbs);
|
||||
#endif
|
||||
function.calltype = fcn->callconv ? QString::fromUtf8(fcn->callconv) : QString();
|
||||
function.name = fcn->name ? QString::fromUtf8(fcn->name) : QString();
|
||||
function.edges = r_anal_function_count_edges(fcn, nullptr);
|
||||
@@ -3394,13 +3490,22 @@ QList<SymbolDescription> IaitoCore::getAllSymbols()
|
||||
if (core && core->bin && core->bin->cur && core->bin->cur->BO) {
|
||||
RVecRBinSymbol *symbols = r_bin_get_symbols_vec(core->bin);
|
||||
if (symbols) {
|
||||
const char *language = r_config_get(core->config, "bin.lang");
|
||||
const bool keepLibrary = r_config_get_b(core->config, "bin.demangle.pfxlib");
|
||||
RBinSymbol *bs;
|
||||
R_VEC_FOREACH(symbols, bs)
|
||||
{
|
||||
QString type = QString(bs->bind) + " " + QString(bs->type);
|
||||
SymbolDescription symbol;
|
||||
symbol.vaddr = bs->vaddr;
|
||||
symbol.name = QString(r_bin_name_tostring(bs->name));
|
||||
const char *rawName = r_bin_name_tostring2(bs->name, 'o');
|
||||
symbol.name = QString::fromUtf8(rawName);
|
||||
symbol.demangledName = QString::fromUtf8(r_bin_name_tostring2(bs->name, 'd'));
|
||||
if (symbol.demangledName == symbol.name) {
|
||||
char *demangled
|
||||
= r_bin_demangle(core->bin->cur, language, rawName, bs->vaddr, keepLibrary);
|
||||
symbol.demangledName = QString::fromUtf8(demangled);
|
||||
free(demangled);
|
||||
}
|
||||
symbol.bind = QString(bs->bind);
|
||||
symbol.type = QString(bs->type);
|
||||
ret << symbol;
|
||||
@@ -3510,9 +3615,15 @@ QList<RelocDescription> IaitoCore::getAllRelocs()
|
||||
if (core && core->bin && core->bin->cur && core->bin->cur->BO) {
|
||||
RBinReloc *br;
|
||||
auto relocs = core->bin->cur->BO->relocs;
|
||||
//// RBIter iter;
|
||||
if (!relocs) {
|
||||
return ret;
|
||||
}
|
||||
#if R2_ABIVERSION >= 136
|
||||
R_VEC_FOREACH(relocs, br)
|
||||
#else
|
||||
RRBNode *iter;
|
||||
r_crbtree_foreach(relocs, iter, RBinReloc, br)
|
||||
#endif
|
||||
{
|
||||
RelocDescription reloc;
|
||||
|
||||
@@ -3604,6 +3715,121 @@ QList<FlagDescription> IaitoCore::getAllFlags(QString flagspace)
|
||||
return ret;
|
||||
}
|
||||
|
||||
QList<GlobalVariableDescription> IaitoCore::getAllGlobalVariables()
|
||||
{
|
||||
CORE_LOCK();
|
||||
QList<GlobalVariableDescription> variables;
|
||||
const QJsonArray globals = cmdj("avgj").array();
|
||||
for (const auto &value : globals) {
|
||||
const QJsonObject object = value.toObject();
|
||||
GlobalVariableDescription variable;
|
||||
variable.name = object[RJsonKey::name].toString();
|
||||
const auto flag = r_flag_get(core->flags, variable.name.toUtf8().constData());
|
||||
if (!flag) {
|
||||
continue;
|
||||
}
|
||||
// Qt JSON numbers cannot preserve every address above INT64_MAX.
|
||||
variable.address = flag->addr;
|
||||
variable.type = object[RJsonKey::type].toString();
|
||||
variable.size = object[RJsonKey::size].toVariant().toULongLong();
|
||||
variables.append(variable);
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
bool IaitoCore::setGlobalVariable(
|
||||
RVA address,
|
||||
const QString &name,
|
||||
const QString &type,
|
||||
const QString &previousName,
|
||||
QString *errorMessage)
|
||||
{
|
||||
CORE_LOCK();
|
||||
const auto fail = [errorMessage](const QString &message) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = message;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const QByteArray nameBytes = name.toUtf8();
|
||||
if (address == RVA_INVALID || name.contains(QChar::Null)
|
||||
|| !r_name_check(nameBytes.constData())) {
|
||||
return fail(
|
||||
tr("Enter a valid address and name (letters, digits, underscores, dots or colons)."));
|
||||
}
|
||||
|
||||
RFlagItem *previous = nullptr;
|
||||
if (!previousName.isEmpty()) {
|
||||
previous = r_flag_get(core->flags, previousName.toUtf8().constData());
|
||||
if (!previous || previous->addr != address || !previous->space
|
||||
|| QString::fromUtf8(previous->space->name) != QStringLiteral("globals")) {
|
||||
return fail(tr("The global variable no longer exists. Refresh the list and try again."));
|
||||
}
|
||||
} else {
|
||||
for (const auto &variable : getAllGlobalVariables()) {
|
||||
if (variable.address == address) {
|
||||
return fail(tr("A global variable is already registered at this address."));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const auto flag = r_flag_get(core->flags, nameBytes.constData())) {
|
||||
if (flag != previous) {
|
||||
return fail(tr("This name is already used by another flag or variable."));
|
||||
}
|
||||
}
|
||||
|
||||
if (type.isEmpty() || type.contains(QLatin1Char('\n')) || type.contains(QLatin1Char('\r'))
|
||||
|| type.contains(QChar::Null)) {
|
||||
return fail(tr("Choose a type for the global variable."));
|
||||
}
|
||||
|
||||
const QString currentName = previous ? previousName : name;
|
||||
cmdRawAt(QStringLiteral("'avg %1 %2").arg(type, currentName), address);
|
||||
const auto typedFlag = r_flag_get(core->flags, currentName.toUtf8().constData());
|
||||
const auto meta = typedFlag ? r_flag_get_meta(core->flags, typedFlag->id) : nullptr;
|
||||
if (!typedFlag || typedFlag->addr != address || !meta || QString::fromUtf8(meta->type) != type) {
|
||||
return fail(
|
||||
tr("Could not apply the selected type. Check that it exists and has a valid size."));
|
||||
}
|
||||
if (previous && name != previousName) {
|
||||
cmdRaw("fs+globals");
|
||||
cmdRaw(QStringLiteral("fr %1 %2").arg(previousName, name));
|
||||
cmdRaw("fs-");
|
||||
}
|
||||
|
||||
const auto flag = r_flag_get(core->flags, nameBytes.constData());
|
||||
if (!flag || flag->addr != address) {
|
||||
return fail(tr("Could not update the global variable."));
|
||||
}
|
||||
emit flagsChanged();
|
||||
emit refreshCodeViews();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IaitoCore::deleteGlobalVariable(RVA address, const QString &name)
|
||||
{
|
||||
CORE_LOCK();
|
||||
const QByteArray nameBytes = name.toUtf8();
|
||||
const auto flag = r_flag_get(core->flags, nameBytes.constData());
|
||||
if (!flag || flag->addr != address || !flag->space
|
||||
|| QString::fromUtf8(flag->space->name) != QStringLiteral("globals")) {
|
||||
return false;
|
||||
}
|
||||
// avg- removes shared metadata as well as a flag, so ambiguous addresses are unsafe.
|
||||
for (const auto &variable : getAllGlobalVariables()) {
|
||||
if (variable.address == address && variable.name != name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
cmdRawAt("avg-", address);
|
||||
if (r_flag_get(core->flags, nameBytes.constData())) {
|
||||
return false;
|
||||
}
|
||||
emit flagsChanged();
|
||||
emit refreshCodeViews();
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<SectionDescription> IaitoCore::getAllSections()
|
||||
{
|
||||
CORE_LOCK();
|
||||
@@ -3764,6 +3990,10 @@ QList<BinClassDescription> IaitoCore::getAllClassesFromBin()
|
||||
BinClassMethodDescription meth;
|
||||
|
||||
meth.name = methObject[RJsonKey::name].toString();
|
||||
meth.mangledName = methObject[QStringLiteral("rawname")].toString();
|
||||
if (meth.name.isEmpty()) {
|
||||
meth.name = meth.mangledName;
|
||||
}
|
||||
meth.addr = methObject[RJsonKey::addr].toVariant().toULongLong();
|
||||
|
||||
cls.methods << meth;
|
||||
@@ -4690,8 +4920,10 @@ void IaitoCore::addFlag(RVA offset, QString name, RVA size, QString color, QStri
|
||||
{
|
||||
CORE_LOCK();
|
||||
name = sanitizeStringForCommand(name);
|
||||
auto fi = r_flag_set(this->core()->flags, name.toStdString().c_str(), offset, size);
|
||||
auto fi = r_flag_set(this->core()->flags, name.toStdString().c_str(), offset, 0);
|
||||
if (fi) {
|
||||
// r_flag_set takes a 32-bit size, while flag ranges can span 64-bit sizes.
|
||||
fi->size = size;
|
||||
if (!color.isEmpty()) {
|
||||
r_flag_item_set_color(this->core()->flags, fi, color.toStdString().c_str());
|
||||
}
|
||||
@@ -4788,6 +5020,15 @@ void IaitoCore::loadPDB(const QString &file)
|
||||
cmdRaw0(QStringLiteral("idp ") + sanitizeStringForCommand(file));
|
||||
}
|
||||
|
||||
static QString memoryPinsProjectPath(const QString &root, const QString &name)
|
||||
{
|
||||
QString path = QDir(root).filePath(name);
|
||||
if (QFileInfo(path).isFile()) {
|
||||
path += QStringLiteral(".d");
|
||||
}
|
||||
return QDir(path).filePath(QStringLiteral("iaito-memory-pins.json"));
|
||||
}
|
||||
|
||||
void IaitoCore::openProject(const QString &name)
|
||||
{
|
||||
if (!isProjectNameValid(name)) {
|
||||
@@ -4795,9 +5036,61 @@ void IaitoCore::openProject(const QString &name)
|
||||
return;
|
||||
}
|
||||
|
||||
const QString pinsPath = memoryPinsProjectPath(getConfig("dir.projects"), name);
|
||||
bool ok = cmdRaw0(QStringLiteral("'P ") + name); // + "@e:scr.interactive=false");
|
||||
if (ok) {
|
||||
notes = QString::fromUtf8(QByteArray::fromBase64(cmdRaw("Pnj").toUtf8()));
|
||||
// Some r2 binary project versions omit types, type links and command metadata.
|
||||
QFile file(pinsPath);
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
const QJsonObject saved = QJsonDocument::fromJson(file.readAll()).object();
|
||||
if (saved.value("version").toInt() == 1) {
|
||||
{
|
||||
CORE_LOCK();
|
||||
const QJsonObject types = saved.value("types").toObject();
|
||||
for (auto it = types.begin(); it != types.end(); ++it) {
|
||||
const QByteArray key = it.key().toUtf8();
|
||||
if (it.value().isString()
|
||||
&& !sdb_const_get(core->anal->sdb_types, key.constData(), 0)) {
|
||||
sdb_set(
|
||||
core->anal->sdb_types,
|
||||
key.constData(),
|
||||
it.value().toString().toUtf8().constData(),
|
||||
0);
|
||||
}
|
||||
}
|
||||
}
|
||||
TypeLayout layout;
|
||||
layout.load(cmdRaw("tk"), getConfigi("asm.bits") / 8);
|
||||
const auto existing = layout.links();
|
||||
const QStringList names = layout.names();
|
||||
CORE_LOCK();
|
||||
for (const QJsonValue &entry : saved.value("links").toArray()) {
|
||||
const QJsonObject link = entry.toObject();
|
||||
bool valid = false;
|
||||
const RVA address = link.value("address").toString().toULongLong(&valid, 16);
|
||||
const QString type = link.value("type").toString();
|
||||
if (valid && names.contains(type) && !existing.contains(address)) {
|
||||
r_type_set_link(core->anal->sdb_types, type.toUtf8().constData(), address);
|
||||
}
|
||||
}
|
||||
for (const QJsonValue &entry : saved.value("commands").toArray()) {
|
||||
const QJsonObject pin = entry.toObject();
|
||||
bool validAddress = false;
|
||||
bool validSize = false;
|
||||
const RVA address
|
||||
= pin.value("address").toString().toULongLong(&validAddress, 16);
|
||||
const RVA size = pin.value("size").toString().toULongLong(&validSize, 16);
|
||||
const QString command = pin.value("command").toString();
|
||||
if (validAddress && validSize && size && size - 1 <= RVA_MAX - address
|
||||
&& !command.isEmpty()
|
||||
&& !r_meta_get_at(core->anal, address, R_META_TYPE_RUN, nullptr)) {
|
||||
r_meta_set(
|
||||
core->anal, R_META_TYPE_RUN, address, size, command.toUtf8().constData());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QMessageBox::critical(
|
||||
nullptr, tr("Error"), tr("Cannot open project. See console for details"));
|
||||
@@ -4816,13 +5109,58 @@ void IaitoCore::saveProject(const QString &name)
|
||||
}
|
||||
|
||||
Core()->setConfig("scr.interactive", false);
|
||||
const bool ok = cmdRaw0(QStringLiteral("'Ps ") + name.trimmed());
|
||||
bool ok = cmdRaw0(QStringLiteral("'Ps ") + name.trimmed());
|
||||
if (ok) {
|
||||
TypeLayout layout;
|
||||
const QString database = cmdRaw("tk");
|
||||
layout.load(database, getConfigi("asm.bits") / 8);
|
||||
QJsonArray entries;
|
||||
const auto links = layout.links();
|
||||
for (auto it = links.cbegin(); it != links.cend(); ++it) {
|
||||
entries.append(
|
||||
QJsonObject{{"address", QString::number(it.key(), 16)}, {"type", it.value()}});
|
||||
}
|
||||
QJsonObject types;
|
||||
const QStringList names = layout.names();
|
||||
for (const QString &line : database.split(QLatin1Char('\n'))) {
|
||||
const int separator = line.indexOf(QLatin1Char('='));
|
||||
const QString key = line.left(separator);
|
||||
if (separator > 0
|
||||
&& (names.contains(key) || key.startsWith("type.") || key.startsWith("struct.")
|
||||
|| key.startsWith("union.") || key.startsWith("enum.")
|
||||
|| key.startsWith("typedef."))) {
|
||||
types.insert(key, line.mid(separator + 1));
|
||||
}
|
||||
}
|
||||
QJsonArray commands;
|
||||
static const QRegularExpression commandPin(
|
||||
QStringLiteral("^Cr\\s+\\d+\\s+(0x[0-9a-fA-F]+)\\s+#"));
|
||||
for (const QString &line : cmdRaw("Cr*").split(QLatin1Char('\n'))) {
|
||||
const auto match = commandPin.match(line);
|
||||
if (match.hasMatch()) {
|
||||
const RVA address = match.captured(1).toULongLong(nullptr, 16);
|
||||
RVA size = 0;
|
||||
const QString command = getCommandAnnotation(address, &size);
|
||||
commands.append(
|
||||
QJsonObject{
|
||||
{"address", QString::number(address, 16)},
|
||||
{"size", QString::number(size, 16)},
|
||||
{"command", command}});
|
||||
}
|
||||
}
|
||||
QSaveFile file(memoryPinsProjectPath(getConfig("dir.projects"), name.trimmed()));
|
||||
const QByteArray data
|
||||
= QJsonDocument(
|
||||
QJsonObject{
|
||||
{"version", 1}, {"links", entries}, {"types", types}, {"commands", commands}})
|
||||
.toJson();
|
||||
ok = file.open(QIODevice::WriteOnly) && file.write(data) == data.size() && file.commit();
|
||||
}
|
||||
if (!ok) {
|
||||
QMessageBox::critical(
|
||||
nullptr,
|
||||
tr("Error"),
|
||||
tr("Cannot save project. Ensure the project name doesnt have any "
|
||||
"special or uppercase character"));
|
||||
tr("Cannot save the project or its memory pins. See console for details."));
|
||||
}
|
||||
#if 0
|
||||
cmdRaw(QStringLiteral("Pnj %1").arg(QString(notes.toUtf8().toBase64())));
|
||||
@@ -4835,6 +5173,18 @@ void IaitoCore::deleteProject(const QString &name)
|
||||
cmdRaw0("'P-" + name);
|
||||
}
|
||||
|
||||
bool IaitoCore::isProjectDirty()
|
||||
{
|
||||
CORE_LOCK();
|
||||
return R_DIRTY_CHECK(core->config) || R_DIRTY_CHECK(core->anal) || R_DIRTY_CHECK(core->flags);
|
||||
}
|
||||
|
||||
void IaitoCore::markProjectClean()
|
||||
{
|
||||
CORE_LOCK();
|
||||
r_core_project_undirty(core);
|
||||
}
|
||||
|
||||
bool IaitoCore::isProjectNameValid(const QString &name)
|
||||
{
|
||||
// see is_valid_project_name() in libr/core/project.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#define MONOTHREAD 1
|
||||
|
||||
#include "common/BasicInstructionHighlighter.h"
|
||||
#include "common/ExceptionInfo.h"
|
||||
#include "core/IaitoCommon.h"
|
||||
#include "core/IaitoDescriptions.h"
|
||||
|
||||
@@ -357,6 +358,7 @@ public:
|
||||
|
||||
/* Math functions */
|
||||
ut64 math(const QString &expr);
|
||||
bool tryMath(const QString &expr, ut64 &value);
|
||||
ut64 num(const QString &expr);
|
||||
QString itoa(ut64 num, int rdx = 16);
|
||||
|
||||
@@ -463,6 +465,7 @@ public:
|
||||
void suspendDebug();
|
||||
|
||||
private:
|
||||
int exceptionJsonSupport = -1;
|
||||
void finishStopDebug();
|
||||
|
||||
public:
|
||||
@@ -552,6 +555,8 @@ public:
|
||||
void loadPDB(const QString &file);
|
||||
|
||||
QByteArray ioRead(RVA addr, int len);
|
||||
QString getCommandAnnotation(RVA address, RVA *size = nullptr);
|
||||
void setCommandAnnotation(RVA address, RVA size, const QString &command);
|
||||
|
||||
QList<RVA> getSeekHistory();
|
||||
|
||||
@@ -564,6 +569,8 @@ public:
|
||||
void openProject(const QString &name);
|
||||
void saveProject(const QString &name);
|
||||
void deleteProject(const QString &name);
|
||||
bool isProjectDirty();
|
||||
void markProjectClean();
|
||||
static bool isProjectNameValid(const QString &name);
|
||||
|
||||
/* Widgets */
|
||||
@@ -577,6 +584,7 @@ public:
|
||||
QList<RDebugPluginDescription> getRDebugPluginDescriptions();
|
||||
QList<RMutaPluginDescription> getRMutaPluginDescriptions();
|
||||
QList<FunctionDescription> getAllFunctions();
|
||||
QList<ExceptionRegion> getExceptionRegions(RVA function = RVA_INVALID);
|
||||
QList<ImportDescription> getAllImports();
|
||||
QList<ExportDescription> getAllExports();
|
||||
QList<SymbolDescription> getAllSymbols();
|
||||
@@ -587,6 +595,14 @@ public:
|
||||
QList<StringDescription> getAllStrings();
|
||||
QList<FlagspaceDescription> getAllFlagspaces();
|
||||
QList<FlagDescription> getAllFlags(QString flagspace = QString());
|
||||
QList<GlobalVariableDescription> getAllGlobalVariables();
|
||||
bool setGlobalVariable(
|
||||
RVA address,
|
||||
const QString &name,
|
||||
const QString &type,
|
||||
const QString &previousName = QString(),
|
||||
QString *errorMessage = nullptr);
|
||||
bool deleteGlobalVariable(RVA address, const QString &name);
|
||||
QList<SectionDescription> getAllSections();
|
||||
QList<SegmentDescription> getAllSegments();
|
||||
QList<EntrypointDescription> getAllEntrypoint();
|
||||
|
||||
@@ -98,6 +98,7 @@ struct SymbolDescription
|
||||
QString bind;
|
||||
QString type;
|
||||
QString name;
|
||||
QString demangledName;
|
||||
};
|
||||
|
||||
struct CommentDescription
|
||||
@@ -137,6 +138,14 @@ struct FlagDescription
|
||||
QString realname;
|
||||
};
|
||||
|
||||
struct GlobalVariableDescription
|
||||
{
|
||||
RVA address = RVA_INVALID;
|
||||
RVA size = 0;
|
||||
QString name;
|
||||
QString type;
|
||||
};
|
||||
|
||||
struct SectionDescription
|
||||
{
|
||||
RVA vaddr = 0;
|
||||
@@ -264,6 +273,7 @@ struct BinClassBaseClassDescription
|
||||
struct BinClassMethodDescription
|
||||
{
|
||||
QString name;
|
||||
QString mangledName;
|
||||
RVA addr = RVA_INVALID;
|
||||
st64 vtableOffset = -1;
|
||||
};
|
||||
@@ -425,6 +435,7 @@ Q_DECLARE_METATYPE(RelocDescription)
|
||||
Q_DECLARE_METATYPE(StringDescription)
|
||||
Q_DECLARE_METATYPE(FlagspaceDescription)
|
||||
Q_DECLARE_METATYPE(FlagDescription)
|
||||
Q_DECLARE_METATYPE(GlobalVariableDescription)
|
||||
Q_DECLARE_METATYPE(XrefDescription)
|
||||
Q_DECLARE_METATYPE(EntrypointDescription)
|
||||
Q_DECLARE_METATYPE(RBinPluginDescription)
|
||||
|
||||
+513
-80
@@ -48,6 +48,7 @@
|
||||
#include "widgets/ConsoleWidget.h"
|
||||
#include "widgets/CustomCommandWidget.h"
|
||||
#include "widgets/Dashboard.h"
|
||||
#include "widgets/DataAnalysisWidget.h"
|
||||
#include "widgets/DebugActions.h"
|
||||
#include "widgets/DecompilerWidget.h"
|
||||
#include "widgets/DisassemblerGraphView.h"
|
||||
@@ -58,11 +59,14 @@
|
||||
#include "widgets/FilesystemWidget.h"
|
||||
#include "widgets/FlagsWidget.h"
|
||||
#include "widgets/FunctionsWidget.h"
|
||||
#include "widgets/GlobalVariablesWidget.h"
|
||||
#include "widgets/GraphView.h"
|
||||
#include "widgets/GraphWidget.h"
|
||||
#include "widgets/HeadersWidget.h"
|
||||
#include "widgets/HexPatWidget.h"
|
||||
#include "widgets/HexdumpWidget.h"
|
||||
#include "widgets/ImportsWidget.h"
|
||||
#include "widgets/InterfacesWidget.h"
|
||||
#include "widgets/MapsWidget.h"
|
||||
#include "widgets/MemoryMapWidget.h"
|
||||
#include "widgets/Omnibar.h"
|
||||
@@ -98,6 +102,7 @@
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QCryptographicHash>
|
||||
#include <QCursor>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
@@ -160,6 +165,21 @@
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsView>
|
||||
|
||||
static void addHashData(QCryptographicHash &hash, const void *data, int size)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
|
||||
hash.addData(QByteArrayView(reinterpret_cast<const char *>(data), size));
|
||||
#else
|
||||
hash.addData(reinterpret_cast<const char *>(data), size);
|
||||
#endif
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static void addHashValue(QCryptographicHash &hash, const T &value)
|
||||
{
|
||||
addHashData(hash, &value, static_cast<int>(sizeof(value)));
|
||||
}
|
||||
|
||||
template<class T>
|
||||
T *getNewInstance(MainWindow *m)
|
||||
{
|
||||
@@ -684,6 +704,49 @@ QString r2QuotedFileArg(const QString &path)
|
||||
return QStringLiteral("\"%1\"").arg(result);
|
||||
}
|
||||
|
||||
QString classdumpFileExtension(const QString &language)
|
||||
{
|
||||
if (language == QLatin1String("java")) {
|
||||
return QStringLiteral("java");
|
||||
}
|
||||
if (language == QLatin1String("kotlin")) {
|
||||
return QStringLiteral("kt");
|
||||
}
|
||||
if (language == QLatin1String("swift")) {
|
||||
return QStringLiteral("swift");
|
||||
}
|
||||
if (language == QLatin1String("cxx") || language == QLatin1String("c++")
|
||||
|| language == QLatin1String("objc") || language == QLatin1String("objective-c")
|
||||
|| language == QLatin1String("c")) {
|
||||
return QStringLiteral("h");
|
||||
}
|
||||
return QStringLiteral("txt");
|
||||
}
|
||||
|
||||
QStringList classdumpLanguages()
|
||||
{
|
||||
QStringList languages;
|
||||
const QString output = Core()->cmdRaw("iccl").trimmed();
|
||||
if (!output.contains(QStringLiteral("invalid"), Qt::CaseInsensitive)
|
||||
&& !output.contains(QStringLiteral("unknown"), Qt::CaseInsensitive)
|
||||
&& !output.contains(QStringLiteral("0x"), Qt::CaseInsensitive)
|
||||
&& !output.contains(QLatin1Char('{')) && !output.contains(QLatin1Char(';'))) {
|
||||
static const QRegularExpression languagePattern(QStringLiteral("^[A-Za-z0-9_+.-]+$"));
|
||||
for (const QString &line :
|
||||
output.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts)) {
|
||||
const QString language = line.trimmed();
|
||||
if (languagePattern.match(language).hasMatch() && !language.startsWith(QLatin1Char('-'))
|
||||
&& !languages.contains(language)) {
|
||||
languages << language;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (languages.isEmpty()) {
|
||||
languages << QStringLiteral("cxx") << QStringLiteral("java") << QStringLiteral("swift");
|
||||
}
|
||||
return languages;
|
||||
}
|
||||
|
||||
QString buildAnalyzePluginStatusText(
|
||||
const QString &name, const QString &args, const QString &description)
|
||||
{
|
||||
@@ -916,7 +979,7 @@ void MainWindow::initUI()
|
||||
widget->setFloating(true);
|
||||
widget->raiseMemoryWidget();
|
||||
});
|
||||
QAction *assemblerAction = new QAction(tr("Assembler..."), this);
|
||||
QAction *assemblerAction = new QAction(tr("Assembler"), this);
|
||||
setAppMenuIcon(this, assemblerAction, AppMenuIcon::Assembler, QColor(216, 88, 64));
|
||||
assemblerAction->setToolTip(tr("Assemble and disassemble bytes at an address"));
|
||||
assemblerAction->setStatusTip(
|
||||
@@ -937,7 +1000,7 @@ void MainWindow::initUI()
|
||||
dialog->activateWindow();
|
||||
});
|
||||
if (!Core()->cmd("Lc~^r2mcp").trimmed().isEmpty()) {
|
||||
QAction *r2mcpAction = new QAction(tr("r2mcp..."), this);
|
||||
QAction *r2mcpAction = new QAction(tr("r2mcp"), this);
|
||||
setAppMenuIcon(this, r2mcpAction, AppMenuIcon::Web, QColor(67, 160, 71));
|
||||
r2mcpAction->setToolTip(tr("Manage the r2mcp HTTP MCP server"));
|
||||
r2mcpAction->setStatusTip(tr("Start, stop, and configure the r2mcp HTTP MCP server"));
|
||||
@@ -961,16 +1024,8 @@ void MainWindow::initUI()
|
||||
});
|
||||
ui->actionCommitChanges->setEnabled(false);
|
||||
connect(Core(), &IaitoCore::ioCacheChanged, ui->actionCommitChanges, &QAction::setEnabled);
|
||||
connect(ui->actionUndoWrite, &QAction::triggered, this, [this]() {
|
||||
core->cmdRaw("wcu");
|
||||
emit core->refreshCodeViews();
|
||||
updateWriteUndoRedoActions();
|
||||
});
|
||||
connect(ui->actionRedoWrite, &QAction::triggered, this, [this]() {
|
||||
core->cmdRaw("wcU");
|
||||
emit core->refreshCodeViews();
|
||||
updateWriteUndoRedoActions();
|
||||
});
|
||||
connect(ui->actionUndoWrite, &QAction::triggered, this, &MainWindow::undoWriteCache);
|
||||
connect(ui->actionRedoWrite, &QAction::triggered, this, &MainWindow::redoWriteCache);
|
||||
updateWriteUndoRedoActions();
|
||||
|
||||
m_dockManager->constructorMap().insert(GraphWidget::getWidgetType(), getNewInstance<GraphWidget>);
|
||||
@@ -1020,6 +1075,34 @@ void MainWindow::initUI()
|
||||
}
|
||||
});
|
||||
|
||||
auto *maximizePanelAction = new QAction(tr("Maximize/restore current panel"), this);
|
||||
maximizePanelAction->setAutoRepeat(false);
|
||||
ShortcutMgr()->bindAction("global.maximizePanel", maximizePanelAction);
|
||||
ui->menuWindowLayout->insertAction(ui->actionDefault, maximizePanelAction);
|
||||
connect(maximizePanelAction, &QAction::triggered, m_dockManager, [this]() {
|
||||
m_dockManager->toggleCurrentPanelMaximized();
|
||||
});
|
||||
|
||||
for (int index = 0; index < 5; ++index) {
|
||||
auto *shortcut = ShortcutMgr()->registerShortcut(
|
||||
QStringLiteral("global.selectPanelTab%1").arg(index + 1), this);
|
||||
shortcut->setAutoRepeat(false);
|
||||
connect(shortcut, &QShortcut::activated, m_dockManager, [this, index]() {
|
||||
m_dockManager->selectCurrentPanelTab(index);
|
||||
});
|
||||
}
|
||||
|
||||
auto *nextFrameShortcut = ShortcutMgr()->registerShortcut("global.focusNextFrame", this);
|
||||
nextFrameShortcut->setAutoRepeat(false);
|
||||
connect(nextFrameShortcut, &QShortcut::activated, m_dockManager, [this]() {
|
||||
m_dockManager->focusNextFrame(true);
|
||||
});
|
||||
auto *previousFrameShortcut = ShortcutMgr()->registerShortcut("global.focusPreviousFrame", this);
|
||||
previousFrameShortcut->setAutoRepeat(false);
|
||||
connect(previousFrameShortcut, &QShortcut::activated, m_dockManager, [this]() {
|
||||
m_dockManager->focusNextFrame(false);
|
||||
});
|
||||
|
||||
connect(ui->actionZoomIn, &QAction::triggered, this, &MainWindow::onZoomIn);
|
||||
connect(ui->actionZoomOut, &QAction::triggered, this, &MainWindow::onZoomOut);
|
||||
connect(ui->actionZoomReset, &QAction::triggered, this, &MainWindow::onZoomReset);
|
||||
@@ -1034,6 +1117,7 @@ void MainWindow::initUI()
|
||||
|
||||
connect(core, &IaitoCore::projectSaved, this, &MainWindow::projectSaved);
|
||||
connect(core, &IaitoCore::toggleDebugView, this, &MainWindow::toggleDebugView);
|
||||
connect(core, &IaitoCore::debugTaskStateChanged, this, &MainWindow::resumePendingClose);
|
||||
connect(core, &IaitoCore::newMessage, this->consoleDock, &ConsoleWidget::addOutput);
|
||||
connect(core, &IaitoCore::newDebugMessage, this->consoleDock, &ConsoleWidget::addDebugOutput);
|
||||
connect(
|
||||
@@ -1098,6 +1182,14 @@ void MainWindow::initUI()
|
||||
addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, r2aiDock);
|
||||
m_dockManager->addPluginDock(r2aiDock);
|
||||
}
|
||||
{
|
||||
auto *hexPatDock = new HexPatWidget(this);
|
||||
addWidget(hexPatDock);
|
||||
ui->menuTools->insertAction(ui->actionStart_Web_Server, hexPatDock->toggleViewAction());
|
||||
addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, hexPatDock);
|
||||
m_dockManager->addPluginDock(hexPatDock);
|
||||
connect(ui->menuTools, &QMenu::aboutToShow, hexPatDock, &HexPatWidget::updateAvailability);
|
||||
}
|
||||
|
||||
// Check if plugins are loaded and display tooltips accordingly
|
||||
ui->menuWindows->setToolTipsVisible(true);
|
||||
@@ -1155,6 +1247,7 @@ void MainWindow::initUI()
|
||||
connectMenuStatusTips(ui->menuEdit);
|
||||
connectMenuStatusTips(ui->menuCode);
|
||||
connectMenuStatusTips(ui->menuAnalysis);
|
||||
connectMenuStatusTips(ui->menuData);
|
||||
connectMenuStatusTips(ui->menuTools);
|
||||
connectMenuStatusTips(ui->menuPlugins);
|
||||
|
||||
@@ -1180,7 +1273,7 @@ void MainWindow::initToolBar()
|
||||
refreshEditIcon();
|
||||
connect(Config(), &Configuration::interfaceThemeChanged, editBtn, refreshEditIcon);
|
||||
QMenu *editMenu = new QMenu(editBtn);
|
||||
editMenu->addAction(tr("Rename function..."), this, [this]() {
|
||||
editMenu->addAction(tr("Rename function…"), this, [this]() {
|
||||
const RVA off = Core()->getOffset();
|
||||
RAnalFunction *fcn = Core()->functionIn(off);
|
||||
if (!fcn) {
|
||||
@@ -1201,7 +1294,7 @@ void MainWindow::initToolBar()
|
||||
Core()->renameFunction(fcn->addr, newName);
|
||||
}
|
||||
});
|
||||
editMenu->addAction(tr("Rename / set flag..."), this, [this]() {
|
||||
editMenu->addAction(tr("Rename / set flag…"), this, [this]() {
|
||||
FlagDialog dialog(Core()->getOffset(), 1, this);
|
||||
dialog.exec();
|
||||
});
|
||||
@@ -1218,7 +1311,7 @@ void MainWindow::initToolBar()
|
||||
});
|
||||
editMenu->addAction(tr("Delete flag"), this, []() { Core()->delFlag(Core()->getOffset()); });
|
||||
editMenu->addSeparator();
|
||||
editMenu->addAction(tr("Add / edit comment..."), this, [this]() {
|
||||
editMenu->addAction(tr("Add / edit comment…"), this, [this]() {
|
||||
CommentsDialog::addOrEditComment(Core()->getOffset(), this);
|
||||
});
|
||||
editMenu->addSeparator();
|
||||
@@ -1293,7 +1386,7 @@ void MainWindow::initToolBar()
|
||||
DebugActions *debugActions = new DebugActions(ui->mainToolBar, this);
|
||||
|
||||
// Debug menu
|
||||
auto debugViewAction = ui->menuDebug->addAction(tr("View"));
|
||||
auto debugViewAction = ui->menuDebug->addAction(tr("Debug Panels"));
|
||||
setAppMenuIcon(this, debugViewAction, AppMenuIcon::View, QColor(126, 87, 194));
|
||||
debugViewAction->setMenu(ui->menuAddDebugWidgets);
|
||||
ui->menuDebug->addSeparator();
|
||||
@@ -1490,14 +1583,15 @@ void MainWindow::initDocks()
|
||||
QList<IaitoDockWidget *> ioDocks
|
||||
= {filesDock = new FilesWidget(this),
|
||||
binariesDock = new BinariesWidget(this),
|
||||
filesystemDock = new FilesystemWidget(this),
|
||||
mapsDock = new MapsWidget(this)};
|
||||
QList<IaitoDockWidget *> infoDocks = {
|
||||
classesDock = new ClassesWidget(this),
|
||||
entrypointDock = new EntrypointWidget(this),
|
||||
exportsDock = new ExportsWidget(this),
|
||||
flagsDock = new FlagsWidget(this),
|
||||
headersDock = new HeadersWidget(this),
|
||||
importsDock = new ImportsWidget(this),
|
||||
interfacesDock = new InterfacesWidget(this),
|
||||
relocsDock = new RelocsWidget(this),
|
||||
resourcesDock = new ResourcesWidget(this),
|
||||
sdbDock = new SdbWidget(this),
|
||||
@@ -1524,8 +1618,20 @@ void MainWindow::initDocks()
|
||||
r2GraphDock = new R2GraphWidget(this),
|
||||
callGraphDock = new CallGraphWidget(this, false),
|
||||
globalCallGraphDock = new CallGraphWidget(this, true),
|
||||
};
|
||||
QList<IaitoDockWidget *> dataDocks = {
|
||||
flagsDock = new FlagsWidget(this),
|
||||
#if R2_ABIVERSION >= 154
|
||||
// avgj and C type metadata require post-6.2.4 radare2.
|
||||
globalVariablesDock = new GlobalVariablesWidget(this),
|
||||
#endif
|
||||
dataAnalysisDock = new DataAnalysisWidget(this),
|
||||
zoomDock = new ZoomWidget(this),
|
||||
};
|
||||
dataAnalysisDock->toggleViewAction()->setText(tr("Analysis"));
|
||||
connect(dataAnalysisDock, &QWidget::windowTitleChanged, this, [this]() {
|
||||
dataAnalysisDock->toggleViewAction()->setText(tr("Analysis"));
|
||||
});
|
||||
|
||||
auto makeActionList = [this](QList<IaitoDockWidget *> docks) {
|
||||
QList<QAction *> result;
|
||||
@@ -1541,10 +1647,11 @@ void MainWindow::initDocks()
|
||||
return result;
|
||||
};
|
||||
|
||||
searchDock->toggleViewAction()->setText(tr("Search..."));
|
||||
searchDock->toggleViewAction()->setText(tr("Search"));
|
||||
ui->menuCode->addSeparator();
|
||||
ui->menuCode->addActions(makeActionList(codeDocks));
|
||||
ui->menuAnalysis->addActions(makeActionList(analysisDocks));
|
||||
ui->menuData->addActions(makeActionList(dataDocks));
|
||||
QList<IaitoDockWidget *> windowDocks2 = {
|
||||
consoleDock,
|
||||
};
|
||||
@@ -1558,19 +1665,17 @@ void MainWindow::initDocks()
|
||||
ui->menuView->insertSeparator(ui->menuZoom->menuAction());
|
||||
ui->menuAddInfoWidgets->addActions(makeActionList(infoDocks));
|
||||
ui->menuAddIoWidgets->addActions(makeActionList(ioDocks));
|
||||
QAction *actionFilesystem = new QAction("Filesystem", this);
|
||||
connect(actionFilesystem, &QAction::triggered, this, [this]() {
|
||||
if (!filesystemDock) {
|
||||
filesystemDock = new FilesystemWidget(this);
|
||||
addExtraWidget(filesystemDock);
|
||||
}
|
||||
QAction *actionSourceFiles = new QAction(tr("Source Files"), this);
|
||||
connect(actionSourceFiles, &QAction::triggered, this, [this]() {
|
||||
filesystemDock->show();
|
||||
filesystemDock->raise();
|
||||
filesystemDock->showSourceFiles();
|
||||
});
|
||||
ui->menuAddIoWidgets->addAction(actionFilesystem);
|
||||
ui->menuCode->addAction(actionSourceFiles);
|
||||
ui->menuAddDebugWidgets->addActions(makeActionList(debugDocks));
|
||||
|
||||
auto uniqueDocks = mainViewDocks + windowDocks2 + infoDocks + analysisDocks;
|
||||
uniqueDocks += codeDocks + ioDocks + debugDocks;
|
||||
uniqueDocks += codeDocks + ioDocks + debugDocks + dataDocks;
|
||||
uniqueDocks.append(overviewDock);
|
||||
for (auto dock : uniqueDocks) {
|
||||
if (dock) { // ignore nullptr used as separators
|
||||
@@ -1624,6 +1729,7 @@ void MainWindow::applyTopLevelMenuIcons()
|
||||
setAppMenuIcon(this, ui->menuAddInfoWidgets, AppMenuIcon::Info, viewColor);
|
||||
setAppMenuIcon(this, ui->menuAddIoWidgets, AppMenuIcon::Storage, viewColor);
|
||||
setAppMenuIcon(this, ui->menuAnalysis, AppMenuIcon::Analysis, analysisColor);
|
||||
setAppMenuIcon(this, ui->menuData, AppMenuIcon::Storage, viewColor);
|
||||
setAppMenuIcon(this, ui->menuAddAnother, AppMenuIcon::Extra, viewColor);
|
||||
setAppMenuIcon(this, ui->menuZoom, AppMenuIcon::ZoomIn, viewColor);
|
||||
setAppMenuIcon(this, ui->actionZoomIn, AppMenuIcon::ZoomIn, viewColor);
|
||||
@@ -2056,11 +2162,15 @@ void MainWindow::finalizeOpen()
|
||||
Config()->adjustColorThemeDarkness();
|
||||
setViewLayout(getViewLayout(core->currentlyDebugging ? LAYOUT_DEBUG : LAYOUT_DEFAULT));
|
||||
|
||||
// Raise the preferred memory view now and again after Qt's deferred layout
|
||||
// pass: restoreState() re-applies the saved active tab on the next event
|
||||
// loop turn, so a synchronous raise alone gets overridden.
|
||||
// Raise the preferred left and memory views now and again after Qt's deferred layout
|
||||
// pass: restoreState() re-applies the saved active tabs on the next event loop turn,
|
||||
// so a synchronous raise alone gets overridden.
|
||||
functionsDock->raise();
|
||||
raiseDefaultMemoryWidget();
|
||||
QTimer::singleShot(0, this, [this]() { raiseDefaultMemoryWidget(); });
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
functionsDock->raise();
|
||||
raiseDefaultMemoryWidget();
|
||||
});
|
||||
consoleDock->hide();
|
||||
// Wire up status bar updates for all seekable views
|
||||
// Disassembly widgets
|
||||
@@ -2101,6 +2211,10 @@ void MainWindow::finalizeOpen()
|
||||
}
|
||||
}
|
||||
|
||||
// Initial loading and analysis establish the session baseline. Only changes made after the
|
||||
// file is ready should require a project-save decision when closing.
|
||||
core->markProjectClean();
|
||||
|
||||
// Signal that the UI is ready for background tasks to run safely.
|
||||
// Use a single-shot queued invocation so that uiReady is emitted after the
|
||||
// current initialization events are processed (avoids re-entrancy into
|
||||
@@ -2160,46 +2274,131 @@ void MainWindow::closeEvent(QCloseEvent *event)
|
||||
return;
|
||||
}
|
||||
if (this->filename == "") {
|
||||
QMainWindow::closeEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are uncommitted changes
|
||||
if (!ioModesController.askCommitUnsavedChanges()) {
|
||||
// if false, Cancel was chosen
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton ret = QMessageBox::question(
|
||||
this,
|
||||
APPNAME,
|
||||
tr("Do you really want to exit?\nSave your project before closing!"),
|
||||
(QMessageBox::StandardButtons) (QMessageBox::Save | QMessageBox::Discard
|
||||
| QMessageBox::Cancel));
|
||||
if (ret == QMessageBox::Cancel) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
if (ret == QMessageBox::Discard) {
|
||||
event->ignore();
|
||||
QMainWindow::closeEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ret == QMessageBox::Save && !saveProject(true)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
// discard (do not save)
|
||||
if (core->currentlyDebugging) {
|
||||
core->stopDebug();
|
||||
} else {
|
||||
saveSettings();
|
||||
QMainWindow::closeEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingCloseAction != PendingCloseAction::None) {
|
||||
if (core->currentlyDebugging) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
if (!completePendingClose()) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
QMainWindow::closeEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool projectDirty = core->isProjectDirty();
|
||||
const bool cachedWrites = !ioModesController.allChangesComitted();
|
||||
const bool debugging = core->currentlyDebugging;
|
||||
const bool hasSavableChanges = projectDirty || cachedWrites;
|
||||
|
||||
if (!hasSavableChanges && !debugging) {
|
||||
saveSettings();
|
||||
QMainWindow::closeEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList sessionState;
|
||||
if (projectDirty) {
|
||||
sessionState << tr("- Project changes have not been saved.");
|
||||
}
|
||||
if (cachedWrites) {
|
||||
sessionState << tr("- Cached writes have not been committed to the file.");
|
||||
}
|
||||
if (debugging) {
|
||||
sessionState << tr("- A debugging session is active and will be stopped.");
|
||||
}
|
||||
|
||||
QMessageBox messageBox(this);
|
||||
messageBox.setIcon(QMessageBox::Question);
|
||||
messageBox.setWindowTitle(APPNAME);
|
||||
messageBox.setText(
|
||||
hasSavableChanges ? tr("Save session changes before closing?")
|
||||
: tr("Stop debugging and close?"));
|
||||
messageBox.setInformativeText(
|
||||
tr("The current session contains:\n%1").arg(sessionState.join(QLatin1Char('\n'))));
|
||||
if (hasSavableChanges) {
|
||||
messageBox.setStandardButtons(
|
||||
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
|
||||
messageBox.setDefaultButton(QMessageBox::Save);
|
||||
} else {
|
||||
messageBox.setStandardButtons(QMessageBox::Close | QMessageBox::Cancel);
|
||||
messageBox.setDefaultButton(QMessageBox::Close);
|
||||
}
|
||||
|
||||
const auto choice = static_cast<QMessageBox::StandardButton>(messageBox.exec());
|
||||
if (choice == QMessageBox::Cancel) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice == QMessageBox::Save && cachedWrites) {
|
||||
core->commitWriteCache();
|
||||
if (!ioModesController.allChangesComitted()) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
tr("Commit failed"),
|
||||
tr("The cached writes could not be committed. The session will remain open."));
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
} else if (choice == QMessageBox::Discard && cachedWrites) {
|
||||
core->cmdRaw("wcr");
|
||||
emit core->refreshCodeViews();
|
||||
}
|
||||
|
||||
pendingCloseAction = choice == QMessageBox::Save ? PendingCloseAction::Save
|
||||
: PendingCloseAction::Discard;
|
||||
pendingProjectSave = choice == QMessageBox::Save && projectDirty;
|
||||
|
||||
if (debugging) {
|
||||
core->stopDebug();
|
||||
event->ignore();
|
||||
resumePendingClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!completePendingClose()) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
QMainWindow::closeEvent(event);
|
||||
}
|
||||
|
||||
bool MainWindow::completePendingClose()
|
||||
{
|
||||
const PendingCloseAction action = pendingCloseAction;
|
||||
const bool saveProjectChanges = pendingProjectSave;
|
||||
pendingCloseAction = PendingCloseAction::None;
|
||||
pendingProjectSave = false;
|
||||
|
||||
if (action == PendingCloseAction::Save && saveProjectChanges && !saveProject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::resumePendingClose()
|
||||
{
|
||||
if (pendingCloseAction == PendingCloseAction::None || core->currentlyDebugging
|
||||
|| pendingCloseScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingCloseScheduled = true;
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
pendingCloseScheduled = false;
|
||||
close();
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QMainWindow::paintEvent(event);
|
||||
@@ -2286,6 +2485,10 @@ void MainWindow::restoreDocks()
|
||||
tabifyDockWidget(functionsDock, symbolsDock);
|
||||
tabifyDockWidget(functionsDock, importsDock);
|
||||
tabifyDockWidget(functionsDock, exportsDock);
|
||||
if (globalVariablesDock) {
|
||||
tabifyDockWidget(functionsDock, globalVariablesDock);
|
||||
globalVariablesDock->hide();
|
||||
}
|
||||
|
||||
// main area
|
||||
tabifyDockWidget(dashboardDock, entrypointDock);
|
||||
@@ -2313,6 +2516,8 @@ void MainWindow::restoreDocks()
|
||||
filesDock->hide();
|
||||
binariesDock->hide();
|
||||
tabifyDockWidget(dashboardDock, zoomDock);
|
||||
tabifyDockWidget(dashboardDock, dataAnalysisDock);
|
||||
dataAnalysisDock->hide();
|
||||
for (const auto &it : m_dockManager->docks()) {
|
||||
// Check whether or not current widgets is graph, hexdump or disasm
|
||||
if (isExtraMemoryWidget(it)) {
|
||||
@@ -2391,7 +2596,8 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
|
||||
for (auto &dock : m_dockManager->docks()) {
|
||||
if (auto memoryWidget = qobject_cast<MemoryDockWidget *>(dock)) {
|
||||
if (memoryWidget->getType() == type && memoryWidget->getSeekable()->isSynchronized()) {
|
||||
memoryWidget->tryRaiseMemoryWidget();
|
||||
// Explicit view requests should also open an empty graph.
|
||||
memoryWidget->raiseMemoryWidget();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2400,17 +2606,23 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
|
||||
memoryDockWidget->raiseMemoryWidget();
|
||||
}
|
||||
|
||||
void MainWindow::showDataAnalysis(RVA start, RVA end)
|
||||
{
|
||||
dataAnalysisDock->showRange(start, end);
|
||||
}
|
||||
|
||||
void MainWindow::gotoOffset(const QString &offset)
|
||||
{
|
||||
if (offset.isEmpty()) {
|
||||
ut64 address;
|
||||
if (!core->tryMath(offset, address)) {
|
||||
return;
|
||||
}
|
||||
if (auto memoryWidget = getLastMemoryWidget()) {
|
||||
memoryWidget->getSeekable()->seek(Core()->math(offset));
|
||||
memoryWidget->getSeekable()->seek(address);
|
||||
memoryWidget->raiseMemoryWidget();
|
||||
return;
|
||||
}
|
||||
Core()->seekAndShow(offset);
|
||||
Core()->seekAndShow(address);
|
||||
}
|
||||
|
||||
QMenu *MainWindow::createShowInMenu(QWidget *parent, RVA address, AddressTypeHint addressType)
|
||||
@@ -2442,6 +2654,16 @@ QMenu *MainWindow::createShowInMenu(QWidget *parent, RVA address, AddressTypeHin
|
||||
}
|
||||
QAction *action = new QAction(memoryWidget->windowTitle(), menu);
|
||||
connect(action, &QAction::triggered, this, [memoryWidget, address]() {
|
||||
if (auto *analysis = qobject_cast<DataAnalysisWidget *>(memoryWidget)) {
|
||||
if (Core()->hasAddressRangeSelection()
|
||||
&& Core()->getAddressRangeSelectionStart() <= address
|
||||
&& address <= Core()->getAddressRangeSelectionEnd()) {
|
||||
analysis->showRange(
|
||||
Core()->getAddressRangeSelectionStart(),
|
||||
Core()->getAddressRangeSelectionEnd());
|
||||
return;
|
||||
}
|
||||
}
|
||||
memoryWidget->getSeekable()->seek(address);
|
||||
memoryWidget->raiseMemoryWidget();
|
||||
});
|
||||
@@ -2569,12 +2791,37 @@ void MainWindow::initBackForwardMenu()
|
||||
|
||||
void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
|
||||
{
|
||||
QPointer<IaitoSeekable> seekable = navigationSeekable();
|
||||
if (seekable && !seekable->isSynchronized()) {
|
||||
menu->clear();
|
||||
int steps = 0;
|
||||
for (RVA address : seekable->localHistory(redo)) {
|
||||
const QString label = steps == 0
|
||||
? tr("Current position (%1)").arg(RAddressString(address))
|
||||
: RAddressString(address);
|
||||
QAction *action = menu->addAction(label);
|
||||
action->setEnabled(steps != 0);
|
||||
connect(action, &QAction::triggered, menu, [seekable, steps, redo]() {
|
||||
if (!seekable) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < steps; ++i) {
|
||||
if (redo) {
|
||||
seekable->seekNext();
|
||||
} else {
|
||||
seekable->seekPrev();
|
||||
}
|
||||
}
|
||||
});
|
||||
++steps;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Not too long so that whole screen doesn't get covered,
|
||||
// not too short so that reasonable length c++ names can be seen most of the
|
||||
// time
|
||||
const int MAX_NAME_LENGTH = 64;
|
||||
|
||||
auto hist = Core()->cmdj("sj");
|
||||
bool history = true;
|
||||
QList<QAction *> actions;
|
||||
for (auto item : Core()->cmdj("sj").array()) {
|
||||
@@ -2604,6 +2851,7 @@ void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
|
||||
QFont font;
|
||||
font.setBold(true);
|
||||
action->setFont(font);
|
||||
action->setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2612,7 +2860,7 @@ void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
|
||||
}
|
||||
menu->clear();
|
||||
menu->addActions(actions);
|
||||
int steps = 1;
|
||||
int steps = 0;
|
||||
for (QAction *item : menu->actions()) {
|
||||
if (redo) {
|
||||
connect(item, &QAction::triggered, item, [steps]() {
|
||||
@@ -2837,9 +3085,116 @@ void MainWindow::updateSaveProjectAction()
|
||||
void MainWindow::updateWriteUndoRedoActions()
|
||||
{
|
||||
const bool cacheEnabled = core->isIOCacheEnabled();
|
||||
const bool hasUndo = cacheEnabled && !core->cmdj("wcj").array().isEmpty();
|
||||
ui->actionUndoWrite->setEnabled(hasUndo);
|
||||
ui->actionRedoWrite->setEnabled(false);
|
||||
bool hasUndo = false;
|
||||
const QByteArray fingerprint = currentWriteCacheFingerprint(&hasUndo);
|
||||
if (!writeRedoStack.isEmpty() && (!cacheEnabled || fingerprint != writeRedoCacheFingerprint)) {
|
||||
writeRedoStack.clear();
|
||||
writeRedoCacheFingerprint.clear();
|
||||
}
|
||||
ui->actionUndoWrite->setEnabled(cacheEnabled && hasUndo);
|
||||
ui->actionRedoWrite->setEnabled(cacheEnabled && !writeRedoStack.isEmpty());
|
||||
}
|
||||
|
||||
QByteArray MainWindow::currentWriteCacheFingerprint(bool *hasUndo)
|
||||
{
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
RCoreLocked locked = core->core();
|
||||
RIO *io = locked->io;
|
||||
quintptr descriptor = reinterpret_cast<quintptr>(io->desc);
|
||||
addHashValue(hash, descriptor);
|
||||
addHashValue(hash, io->cache.mode);
|
||||
int layerCount = r_list_length(io->cache.layers);
|
||||
addHashValue(hash, layerCount);
|
||||
|
||||
RIOCacheLayer *activeLayer = static_cast<RIOCacheLayer *>(r_list_last(io->cache.layers));
|
||||
if (hasUndo) {
|
||||
*hasUndo = activeLayer && RVecRIOCacheItemPtr_length(activeLayer->vec) > 0;
|
||||
}
|
||||
RListIter *layerIterator;
|
||||
void *layerData;
|
||||
r_list_foreach(io->cache.layers, layerIterator, layerData)
|
||||
{
|
||||
RIOCacheLayer *layer = static_cast<RIOCacheLayer *>(layerData);
|
||||
size_t itemCount = RVecRIOCacheItemPtr_length(layer->vec);
|
||||
addHashValue(hash, itemCount);
|
||||
RIOCacheItem **itemIterator;
|
||||
R_VEC_FOREACH(layer->vec, itemIterator)
|
||||
{
|
||||
RIOCacheItem *item = *itemIterator;
|
||||
quint64 address = r_itv_begin(item->itv);
|
||||
quint64 size = r_itv_size(item->itv);
|
||||
addHashValue(hash, address);
|
||||
addHashValue(hash, size);
|
||||
addHashValue(hash, item->written);
|
||||
addHashData(hash, item->data, static_cast<int>(size));
|
||||
addHashData(hash, item->odata, static_cast<int>(size));
|
||||
}
|
||||
}
|
||||
return hash.result();
|
||||
}
|
||||
|
||||
void MainWindow::undoWriteCache()
|
||||
{
|
||||
updateWriteUndoRedoActions();
|
||||
if (!ui->actionUndoWrite->isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
WriteRedoEntry entry;
|
||||
bool undone = false;
|
||||
{
|
||||
RCoreLocked locked = core->core();
|
||||
RIOCacheLayer *layer = static_cast<RIOCacheLayer *>(r_list_last(locked->io->cache.layers));
|
||||
if (layer && R_VEC_START_ITER(layer->vec) != R_VEC_END_ITER(layer->vec)) {
|
||||
RIOCacheItem *item = *(R_VEC_END_ITER(layer->vec) - 1);
|
||||
const int size = static_cast<int>(r_itv_size(item->itv));
|
||||
entry.address = r_itv_begin(item->itv);
|
||||
entry.data = QByteArray(reinterpret_cast<const char *>(item->data), size);
|
||||
undone = r_io_cache_undo(locked->io);
|
||||
if (undone) {
|
||||
r_core_block_read(locked);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!undone) {
|
||||
return;
|
||||
}
|
||||
|
||||
writeRedoStack.append(entry);
|
||||
writeRedoCacheFingerprint = currentWriteCacheFingerprint();
|
||||
emit core->refreshCodeViews();
|
||||
updateWriteUndoRedoActions();
|
||||
}
|
||||
|
||||
void MainWindow::redoWriteCache()
|
||||
{
|
||||
updateWriteUndoRedoActions();
|
||||
if (!ui->actionRedoWrite->isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const WriteRedoEntry &entry = writeRedoStack.constLast();
|
||||
bool redone = false;
|
||||
{
|
||||
RCoreLocked locked = core->core();
|
||||
redone = r_io_write_at(
|
||||
locked->io,
|
||||
entry.address,
|
||||
reinterpret_cast<const ut8 *>(entry.data.constData()),
|
||||
entry.data.size());
|
||||
if (redone) {
|
||||
r_core_block_read(locked);
|
||||
}
|
||||
}
|
||||
if (!redone) {
|
||||
QMessageBox::warning(this, tr("Redo Write"), tr("The cached write could not be restored."));
|
||||
return;
|
||||
}
|
||||
|
||||
writeRedoStack.removeLast();
|
||||
writeRedoCacheFingerprint = currentWriteCacheFingerprint();
|
||||
emit core->refreshCodeViews();
|
||||
updateWriteUndoRedoActions();
|
||||
}
|
||||
|
||||
void MainWindow::enableDebugWidgetsMenu(bool enable)
|
||||
@@ -2856,7 +3211,7 @@ IaitoLayout MainWindow::getViewLayout()
|
||||
{
|
||||
IaitoLayout layout;
|
||||
layout.geometry = saveGeometry();
|
||||
layout.state = saveState();
|
||||
layout.state = m_dockManager->saveState();
|
||||
|
||||
for (auto dock : m_dockManager->docks()) {
|
||||
QVariantMap properties;
|
||||
@@ -2879,6 +3234,7 @@ IaitoLayout MainWindow::getViewLayout(const QString &name)
|
||||
|
||||
void MainWindow::setViewLayout(const IaitoLayout &layout)
|
||||
{
|
||||
m_dockManager->restoreMaximizedPanel();
|
||||
bool isDefault = layout.state.isEmpty() || layout.geometry.isEmpty();
|
||||
bool isDebug = Core()->currentlyDebugging;
|
||||
bool wasAnimated = isAnimated();
|
||||
@@ -2946,6 +3302,10 @@ void MainWindow::setViewLayout(const IaitoLayout &layout)
|
||||
// other
|
||||
}
|
||||
for (auto dock : newDocks) {
|
||||
// Data analysis scans are opt-in, including when upgrading a saved layout.
|
||||
if (dock == dataAnalysisDock) {
|
||||
continue;
|
||||
}
|
||||
dockOnMainArea(dock);
|
||||
// Show any new docks by default.
|
||||
// Showing new builtin docks helps discovering features added in
|
||||
@@ -3250,12 +3610,31 @@ void MainWindow::on_actionQuit_triggered()
|
||||
|
||||
void MainWindow::on_actionBackward_triggered()
|
||||
{
|
||||
core->seekPrev();
|
||||
if (auto *seekable = navigationSeekable()) {
|
||||
seekable->seekPrev();
|
||||
} else {
|
||||
core->seekPrev();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionForward_triggered()
|
||||
{
|
||||
core->seekNext();
|
||||
if (auto *seekable = navigationSeekable()) {
|
||||
seekable->seekNext();
|
||||
} else {
|
||||
core->seekNext();
|
||||
}
|
||||
}
|
||||
|
||||
IaitoSeekable *MainWindow::navigationSeekable(QWidget *origin) const
|
||||
{
|
||||
for (QWidget *widget = origin ? origin : QApplication::focusWidget(); widget;
|
||||
widget = widget->parentWidget()) {
|
||||
if (auto *dock = qobject_cast<AddressableDockWidget *>(widget)) {
|
||||
return dock->getSeekable();
|
||||
}
|
||||
}
|
||||
return navigationDock && navigationDock->isVisible() ? navigationDock->getSeekable() : nullptr;
|
||||
}
|
||||
|
||||
void MainWindow::on_actionDisasAdd_comment_triggered()
|
||||
@@ -3712,6 +4091,43 @@ void MainWindow::on_actionDump_triggered()
|
||||
.arg(QDir::toNativeSeparators(filePath)));
|
||||
}
|
||||
|
||||
void MainWindow::on_actionExportClassdump_triggered()
|
||||
{
|
||||
QStringList filters;
|
||||
QMap<QString, QString> languageByFilter;
|
||||
for (const QString &language : classdumpLanguages()) {
|
||||
const QString extension = classdumpFileExtension(language);
|
||||
const QString filter = tr("%1 class dump (*.%2)").arg(language, extension);
|
||||
filters << filter;
|
||||
languageByFilter[filter] = language;
|
||||
}
|
||||
filters << tr("All files (*)");
|
||||
|
||||
QFileDialog dialog(this, tr("Export class dump"));
|
||||
dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
dialog.setFileMode(QFileDialog::AnyFile);
|
||||
dialog.setNameFilters(filters);
|
||||
dialog.selectFile(QStringLiteral("classdump.%1")
|
||||
.arg(classdumpFileExtension(languageByFilter.value(filters.first()))));
|
||||
dialog.setDefaultSuffix(classdumpFileExtension(languageByFilter.value(filters.first())));
|
||||
|
||||
if (!dialog.exec()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString selectedFilter = dialog.selectedNameFilter();
|
||||
const QString language
|
||||
= languageByFilter.value(selectedFilter, languageByFilter.value(filters.first()));
|
||||
QFile file(dialog.selectedFiles().first());
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Cannot open file for writing."));
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream fileOut(&file);
|
||||
fileOut << Core()->cmd(QStringLiteral("icc %1").arg(language));
|
||||
}
|
||||
|
||||
void MainWindow::on_actionExportDWARF_triggered()
|
||||
{
|
||||
QFileDialog dialog(this, tr("Export DWARF information"));
|
||||
@@ -3817,10 +4233,10 @@ void MainWindow::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
switch (event->button()) {
|
||||
case Qt::BackButton:
|
||||
core->seekPrev();
|
||||
on_actionBackward_triggered();
|
||||
break;
|
||||
case Qt::ForwardButton:
|
||||
core->seekNext();
|
||||
on_actionForward_triggered();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -3850,6 +4266,15 @@ static bool isTextInputFocused()
|
||||
|
||||
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::FocusIn) {
|
||||
for (QWidget *widget = qobject_cast<QWidget *>(watched); widget;
|
||||
widget = widget->parentWidget()) {
|
||||
if (auto *dock = qobject_cast<AddressableDockWidget *>(widget)) {
|
||||
navigationDock = dock;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_dockManager && m_dockManager->handleEvent(watched, event)) {
|
||||
return true;
|
||||
}
|
||||
@@ -3870,7 +4295,15 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event)
|
||||
if (event->type() == QEvent::MouseButtonPress) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
if (mouseEvent->button() == Qt::ForwardButton || mouseEvent->button() == Qt::BackButton) {
|
||||
mousePressEvent(mouseEvent);
|
||||
if (auto *seekable = navigationSeekable(qobject_cast<QWidget *>(watched))) {
|
||||
if (mouseEvent->button() == Qt::BackButton) {
|
||||
seekable->seekPrev();
|
||||
} else {
|
||||
seekable->seekNext();
|
||||
}
|
||||
} else {
|
||||
mousePressEvent(mouseEvent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,14 @@ class AsciiHighlighter;
|
||||
class VisualNavbar;
|
||||
class FunctionsWidget;
|
||||
class ImportsWidget;
|
||||
class InterfacesWidget;
|
||||
class ExportsWidget;
|
||||
class SymbolsWidget;
|
||||
class RelocsWidget;
|
||||
class CommentsWidget;
|
||||
class StringsWidget;
|
||||
class FlagsWidget;
|
||||
class GlobalVariablesWidget;
|
||||
class Dashboard;
|
||||
class QLineEdit;
|
||||
class QRubberBand;
|
||||
@@ -65,6 +67,7 @@ class FilesWidget;
|
||||
class FilesystemWidget;
|
||||
class BinariesWidget;
|
||||
class ZoomWidget;
|
||||
class DataAnalysisWidget;
|
||||
class XrefsWidget;
|
||||
class RefsWidget;
|
||||
class CodeInfoWidget;
|
||||
@@ -150,6 +153,7 @@ public:
|
||||
QString getUniqueObjectName(const QString &widgetType) const;
|
||||
void showMemoryWidget();
|
||||
void showMemoryWidget(MemoryWidgetType type);
|
||||
void showDataAnalysis(RVA start, RVA end);
|
||||
void gotoOffset(const QString &offset);
|
||||
enum class AddressTypeHint { Function, Data, Unknown };
|
||||
QMenu *createShowInMenu(
|
||||
@@ -260,6 +264,7 @@ private slots:
|
||||
|
||||
void on_actionExport_as_code_triggered();
|
||||
void on_actionDump_triggered();
|
||||
void on_actionExportClassdump_triggered();
|
||||
void on_actionExportDWARF_triggered();
|
||||
void on_actionExportProjectArchive_triggered();
|
||||
|
||||
@@ -284,9 +289,22 @@ private slots:
|
||||
void repositionSuperBottomNavbar();
|
||||
|
||||
private:
|
||||
enum class PendingCloseAction { None, Save, Discard };
|
||||
|
||||
struct WriteRedoEntry
|
||||
{
|
||||
RVA address;
|
||||
QByteArray data;
|
||||
};
|
||||
|
||||
IaitoCore *core;
|
||||
|
||||
void runCommandShortcut(const QString &command, bool needsInput);
|
||||
void undoWriteCache();
|
||||
void redoWriteCache();
|
||||
QByteArray currentWriteCacheFingerprint(bool *hasUndo = nullptr);
|
||||
bool completePendingClose();
|
||||
void resumePendingClose();
|
||||
|
||||
bool tabsOnTop;
|
||||
ut64 hexdumpTopOffset;
|
||||
@@ -300,7 +318,12 @@ private:
|
||||
QToolButton *webserverButton = nullptr;
|
||||
bool webserverRunning = false;
|
||||
QByteArray emptyState;
|
||||
QList<WriteRedoEntry> writeRedoStack;
|
||||
QByteArray writeRedoCacheFingerprint;
|
||||
IOModesController ioModesController;
|
||||
PendingCloseAction pendingCloseAction = PendingCloseAction::None;
|
||||
bool pendingProjectSave = false;
|
||||
bool pendingCloseScheduled = false;
|
||||
|
||||
Configuration *configuration;
|
||||
|
||||
@@ -310,6 +333,7 @@ private:
|
||||
EntrypointWidget *entrypointDock = nullptr;
|
||||
FunctionsWidget *functionsDock = nullptr;
|
||||
ImportsWidget *importsDock = nullptr;
|
||||
InterfacesWidget *interfacesDock = nullptr;
|
||||
ExportsWidget *exportsDock = nullptr;
|
||||
HeadersWidget *headersDock = nullptr;
|
||||
TypesWidget *typesDock = nullptr;
|
||||
@@ -319,6 +343,7 @@ private:
|
||||
CommentsWidget *commentsDock = nullptr;
|
||||
StringsWidget *stringsDock = nullptr;
|
||||
FlagsWidget *flagsDock = nullptr;
|
||||
GlobalVariablesWidget *globalVariablesDock = nullptr;
|
||||
Dashboard *dashboardDock = nullptr;
|
||||
SdbWidget *sdbDock = nullptr;
|
||||
SectionsWidget *sectionsDock = nullptr;
|
||||
@@ -345,6 +370,7 @@ private:
|
||||
CallGraphWidget *callGraphDock = nullptr;
|
||||
CallGraphWidget *globalCallGraphDock = nullptr;
|
||||
ZoomWidget *zoomDock = nullptr;
|
||||
DataAnalysisWidget *dataAnalysisDock = nullptr;
|
||||
XrefsWidget *xrefsDock = nullptr;
|
||||
RefsWidget *refsDock = nullptr;
|
||||
CodeInfoWidget *codeInfoDock = nullptr;
|
||||
@@ -394,6 +420,7 @@ private:
|
||||
* @param redo set to false for undo history, true for redo.
|
||||
*/
|
||||
void updateHistoryMenu(QMenu *menu, bool redo = false);
|
||||
IaitoSeekable *navigationSeekable(QWidget *origin = nullptr) const;
|
||||
void updateSaveProjectAction();
|
||||
void updateWriteUndoRedoActions();
|
||||
void rebuildRecentScriptsMenu();
|
||||
@@ -424,6 +451,7 @@ private:
|
||||
|
||||
MemoryDockWidget *lastSyncMemoryWidget = nullptr;
|
||||
MemoryDockWidget *lastMemoryWidget = nullptr;
|
||||
QPointer<AddressableDockWidget> navigationDock;
|
||||
int functionDockWidthToRestore = 0;
|
||||
|
||||
// True when the main window UI has been fully initialized and it's safe to
|
||||
|
||||
+43
-28
@@ -30,7 +30,7 @@
|
||||
</property>
|
||||
<widget class="QMenu" name="addExtraWidgets">
|
||||
<property name="title">
|
||||
<string>Add extra...</string>
|
||||
<string>New Panel</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menuBar">
|
||||
@@ -59,7 +59,7 @@
|
||||
</property>
|
||||
<widget class="QMenu" name="menuImport">
|
||||
<property name="title">
|
||||
<string>Import...</string>
|
||||
<string>Import</string>
|
||||
</property>
|
||||
<addaction name="actionMap"/>
|
||||
<addaction name="actionImportPDB"/>
|
||||
@@ -71,10 +71,11 @@
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuExport">
|
||||
<property name="title">
|
||||
<string>Export...</string>
|
||||
<string>Export</string>
|
||||
</property>
|
||||
<addaction name="actionExport_as_code"/>
|
||||
<addaction name="actionDump"/>
|
||||
<addaction name="actionExportClassdump"/>
|
||||
<addaction name="actionExportDWARF"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionExportProjectArchive"/>
|
||||
@@ -129,9 +130,15 @@
|
||||
<string>Analysis</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuData">
|
||||
<property name="title">
|
||||
<string>Data</string>
|
||||
</property>
|
||||
</widget>
|
||||
<addaction name="actionHighlight"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menuAnalysis"/>
|
||||
<addaction name="menuData"/>
|
||||
<addaction name="menuAddInfoWidgets"/>
|
||||
<addaction name="menuAddIoWidgets"/>
|
||||
<addaction name="separator"/>
|
||||
@@ -225,12 +232,12 @@
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAddInfoWidgets">
|
||||
<property name="title">
|
||||
<string>Info...</string>
|
||||
<string>Bin headers</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAddIoWidgets">
|
||||
<property name="title">
|
||||
<string>IO...</string>
|
||||
<string>I/O Panels</string>
|
||||
</property>
|
||||
<!--
|
||||
<addaction name="actionBinaries"/>
|
||||
@@ -241,12 +248,12 @@
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAddDebugWidgets">
|
||||
<property name="title">
|
||||
<string>Debug...</string>
|
||||
<string>Debug Panels</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAddAnother">
|
||||
<property name="title">
|
||||
<string>Add another</string>
|
||||
<string>New Panel</string>
|
||||
</property>
|
||||
<addaction name="actionExtraDecompiler"/>
|
||||
<addaction name="actionExtraDisassembly"/>
|
||||
@@ -342,7 +349,7 @@
|
||||
</action>
|
||||
<action name="actionNew">
|
||||
<property name="text">
|
||||
<string>Open...</string>
|
||||
<string>Open…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionClose">
|
||||
@@ -380,12 +387,12 @@
|
||||
</action>
|
||||
<action name="actionMap">
|
||||
<property name="text">
|
||||
<string>Map file at...</string>
|
||||
<string>Map file at…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionManageAddressSpace">
|
||||
<property name="text">
|
||||
<string>Manage files...</string>
|
||||
<string>Manage files</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Manage files, maps, binaries, filesystems, and address space options</string>
|
||||
@@ -738,7 +745,7 @@
|
||||
</action>
|
||||
<action name="actionRun_Script">
|
||||
<property name="text">
|
||||
<string>Radare Script...</string>
|
||||
<string>Radare Script…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Run a radare2 script file once</string>
|
||||
@@ -749,7 +756,7 @@
|
||||
</action>
|
||||
<action name="actionRunR2jsScript">
|
||||
<property name="text">
|
||||
<string>r2js script...</string>
|
||||
<string>r2js script…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Run an r2js script file once</string>
|
||||
@@ -760,7 +767,7 @@
|
||||
</action>
|
||||
<action name="actionScripts">
|
||||
<property name="text">
|
||||
<string>Script Editor...</string>
|
||||
<string>Script Editor</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Open the script editor</string>
|
||||
@@ -838,7 +845,7 @@
|
||||
</action>
|
||||
<action name="actionSettings">
|
||||
<property name="text">
|
||||
<string>Settings...</string>
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionPackageManagerPrefs">
|
||||
@@ -848,7 +855,7 @@
|
||||
</action>
|
||||
<action name="actionSaveAs">
|
||||
<property name="text">
|
||||
<string>Save Project...</string>
|
||||
<string>Save Project…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionGraph">
|
||||
@@ -861,12 +868,12 @@
|
||||
</action>
|
||||
<action name="actionImportPDB">
|
||||
<property name="text">
|
||||
<string>PDB file...</string>
|
||||
<string>PDB file…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionImportSymbols">
|
||||
<property name="text">
|
||||
<string>Binary symbols...</string>
|
||||
<string>Binary symbols…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Import symbols from another binary object file</string>
|
||||
@@ -912,7 +919,7 @@
|
||||
</action>
|
||||
<action name="actionSwitchJumpTable">
|
||||
<property name="text">
|
||||
<string>SwitchJumpTable...</string>
|
||||
<string>SwitchJumpTable…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Create or edit a switch / jump table at the current dispatcher</string>
|
||||
@@ -923,20 +930,28 @@
|
||||
</action>
|
||||
<action name="actionExport_as_code">
|
||||
<property name="text">
|
||||
<string>Disassembly/code...</string>
|
||||
<string>Disassembly/code…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDump">
|
||||
<property name="text">
|
||||
<string>Binary range...</string>
|
||||
<string>Binary range…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Dump a range of bytes to a file</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExportClassdump">
|
||||
<property name="text">
|
||||
<string>Class dump…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Export class information using icc</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionImportDWARF">
|
||||
<property name="text">
|
||||
<string>DWARF information...</string>
|
||||
<string>DWARF information…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Import DWARF information from another binary object file</string>
|
||||
@@ -944,7 +959,7 @@
|
||||
</action>
|
||||
<action name="actionImportFLIRT">
|
||||
<property name="text">
|
||||
<string>FLIRT signatures...</string>
|
||||
<string>FLIRT signatures…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Load and scan FLIRT signature files</string>
|
||||
@@ -952,12 +967,12 @@
|
||||
</action>
|
||||
<action name="actionImportProjectArchive">
|
||||
<property name="text">
|
||||
<string>Project archive...</string>
|
||||
<string>Project archive…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExportDWARF">
|
||||
<property name="text">
|
||||
<string>DWARF information...</string>
|
||||
<string>DWARF information…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Export DWARF source line information</string>
|
||||
@@ -965,7 +980,7 @@
|
||||
</action>
|
||||
<action name="actionExportProjectArchive">
|
||||
<property name="text">
|
||||
<string>Project archive...</string>
|
||||
<string>Project archive…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExtraHexdump">
|
||||
@@ -1051,7 +1066,7 @@
|
||||
</action>
|
||||
<action name="actionPatchInstruction">
|
||||
<property name="text">
|
||||
<string>Patch Instruction...</string>
|
||||
<string>Patch Instruction…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Patch the instruction at the current address</string>
|
||||
@@ -1059,7 +1074,7 @@
|
||||
</action>
|
||||
<action name="actionPatchString">
|
||||
<property name="text">
|
||||
<string>Patch String...</string>
|
||||
<string>Patch String…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Patch a string at the current address</string>
|
||||
@@ -1067,7 +1082,7 @@
|
||||
</action>
|
||||
<action name="actionPatchBytes">
|
||||
<property name="text">
|
||||
<string>Patch Bytes...</string>
|
||||
<string>Patch Bytes…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Patch bytes at the current address</string>
|
||||
|
||||
@@ -21,7 +21,7 @@ AsyncTaskDialog::AsyncTaskDialog(AsyncTask::Ptr task, QWidget *parent)
|
||||
setWindowTitle(title);
|
||||
}
|
||||
|
||||
qhelpers::bindFont(ui->logTextEdit, false, true);
|
||||
qhelpers::bindLogFont(ui->logTextEdit);
|
||||
ui->logTextEdit->setStyleSheet(
|
||||
QStringLiteral("QPlainTextEdit { background-color: %1; color: %2; }")
|
||||
.arg(ConfigColor("gui.background").name(), ConfigColor("btext").name()));
|
||||
|
||||
+44
-39
@@ -3,13 +3,12 @@
|
||||
#include <QAction>
|
||||
#include <QColor>
|
||||
#include <QColorDialog>
|
||||
#include <QEvent>
|
||||
#include <QMenu>
|
||||
|
||||
#include "core/Iaito.h"
|
||||
#include <QAbstractButton>
|
||||
#include <QIntValidator>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpressionValidator>
|
||||
|
||||
// Forwarding constructor: default flag size when only offset & parent provided
|
||||
FlagDialog::FlagDialog(RVA offset, QWidget *parent)
|
||||
@@ -38,15 +37,14 @@ FlagDialog::FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent)
|
||||
connect(ui->colorEdit, &QLineEdit::textChanged, this, [this](const QString &color) {
|
||||
ui->colorPreview->setStyleSheet(QStringLiteral("background-color: %1;").arg(color));
|
||||
});
|
||||
RFlagItem *flag = r_flag_get_i(Core()->core()->flags, offset);
|
||||
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, offset);
|
||||
if (flag) {
|
||||
flagName = QString(flag->name);
|
||||
flagOffset = flag->addr;
|
||||
}
|
||||
|
||||
auto size_validator = new QIntValidator(ui->sizeEdit);
|
||||
size_validator->setBottom(1);
|
||||
ui->sizeEdit->setValidator(size_validator);
|
||||
ui->sizeEdit->setValidator(new QRegularExpressionValidator(
|
||||
QRegularExpression(QStringLiteral("[0-9]{1,20}")), ui->sizeEdit));
|
||||
// Pre-fill size field: use defaultSize for new flags, override with existing for edits
|
||||
ui->sizeEdit->setText(QString::number(defaultSize));
|
||||
if (flag) {
|
||||
@@ -61,15 +59,31 @@ FlagDialog::FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Enable color picker on color field
|
||||
ui->colorEdit->installEventFilter(this);
|
||||
connect(ui->colorButton, &QPushButton::clicked, this, [this]() {
|
||||
QColor color
|
||||
= QColorDialog::getColor(QColor(ui->colorEdit->text()), this, tr("Select flag color"));
|
||||
if (color.isValid()) {
|
||||
ui->colorEdit->setText(color.name());
|
||||
}
|
||||
});
|
||||
if (flag) {
|
||||
setWindowTitle(tr("Edit Flag"));
|
||||
ui->nameEdit->setText(flag->name);
|
||||
ui->labelAction->setText(tr("Edit flag at %1").arg(RAddressString(offset)));
|
||||
} else {
|
||||
ui->labelAction->setText(tr("Add flag at %1").arg(RAddressString(offset)));
|
||||
}
|
||||
|
||||
auto updateAcceptButton = [this]() {
|
||||
bool validSize = false;
|
||||
const ut64 size = ui->sizeEdit->text().toULongLong(&validSize);
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)
|
||||
->setEnabled(!ui->nameEdit->text().trimmed().isEmpty() && validSize && size > 0);
|
||||
};
|
||||
connect(ui->nameEdit, &QLineEdit::textChanged, this, updateAcceptButton);
|
||||
connect(ui->sizeEdit, &QLineEdit::textChanged, this, updateAcceptButton);
|
||||
updateAcceptButton();
|
||||
|
||||
// Connect slots for OK/Cancel
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &FlagDialog::buttonBoxAccepted);
|
||||
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &FlagDialog::buttonBoxRejected);
|
||||
@@ -88,34 +102,27 @@ FlagDialog::~FlagDialog() {}
|
||||
|
||||
void FlagDialog::buttonBoxAccepted()
|
||||
{
|
||||
RVA size = ui->sizeEdit->text().toULongLong();
|
||||
QString name = ui->nameEdit->text();
|
||||
bool validSize = false;
|
||||
RVA size = ui->sizeEdit->text().toULongLong(&validSize);
|
||||
QString name = ui->nameEdit->text().trimmed();
|
||||
if (name.isEmpty() || !validSize || size == 0) {
|
||||
return;
|
||||
}
|
||||
QString color = ui->colorEdit->text();
|
||||
QString comment = ui->commentEdit->text();
|
||||
if (flagOffset != RVA_INVALID) {
|
||||
// Existing flag: remove old flag by name
|
||||
Core()->delFlag(flagName);
|
||||
// If a name is provided, recreate/update the flag with new attributes
|
||||
if (!name.isEmpty()) {
|
||||
Core()->addFlag(offset, name, size, color, comment);
|
||||
}
|
||||
} else {
|
||||
// New flag: only add if name is provided
|
||||
if (!name.isEmpty()) {
|
||||
Core()->addFlag(offset, name, size, color, comment);
|
||||
}
|
||||
}
|
||||
close();
|
||||
setResult(QDialog::Accepted);
|
||||
Core()->addFlag(offset, name, size, color, comment);
|
||||
accept();
|
||||
}
|
||||
|
||||
void FlagDialog::buttonBoxRejected()
|
||||
{
|
||||
close();
|
||||
this->setResult(QDialog::Rejected);
|
||||
reject();
|
||||
}
|
||||
|
||||
std::pair<QAction *, QAction *> FlagDialog::addFlagMenuActions(
|
||||
std::tuple<QAction *, QAction *, QAction *> FlagDialog::addFlagMenuActions(
|
||||
QMenu *menu,
|
||||
QWidget *dialogParent,
|
||||
RVA addr,
|
||||
@@ -145,19 +152,17 @@ std::pair<QAction *, QAction *> FlagDialog::addFlagMenuActions(
|
||||
}
|
||||
});
|
||||
|
||||
return {addAction, editAction};
|
||||
}
|
||||
|
||||
bool FlagDialog::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
// Open color picker when clicking on color field
|
||||
if (watched == ui->colorEdit && event->type() == QEvent::MouseButtonPress) {
|
||||
QColor initial = QColor(ui->colorEdit->text());
|
||||
QColor c = QColorDialog::getColor(initial, this, tr("Select flag color"));
|
||||
if (c.isValid()) {
|
||||
ui->colorEdit->setText(c.name());
|
||||
QAction *deleteAction = menu->addAction(tr("Delete flag"));
|
||||
deleteAction->setEnabled(existing != nullptr);
|
||||
QObject::connect(deleteAction, &QAction::triggered, dialogParent, [addr, onChanged]() {
|
||||
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, addr);
|
||||
if (flag) {
|
||||
Core()->delFlag(QString::fromUtf8(flag->name));
|
||||
if (onChanged) {
|
||||
onChanged();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return QDialog::eventFilter(watched, event);
|
||||
});
|
||||
|
||||
return {addAction, editAction, deleteAction};
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "core/IaitoCommon.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <tuple>
|
||||
#include <QDialog>
|
||||
|
||||
class QAction;
|
||||
@@ -19,18 +19,14 @@ class FlagDialog : public QDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// offset: address of flag; defaultSize: initial size to pre-fill (number of bytes)
|
||||
// Constructor: offset is address; defaultSize optional size for new flags
|
||||
// defaultSize pre-fills the byte count for new flags.
|
||||
explicit FlagDialog(RVA offset, QWidget *parent = nullptr);
|
||||
explicit FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent = nullptr);
|
||||
~FlagDialog();
|
||||
|
||||
// Append an "Add flag..." / "Edit flag..." action pair to menu, wired to open
|
||||
// FlagDialog at addr. Whichever action is inapplicable (add when a flag already
|
||||
// exists, edit when none does) is created disabled. onChanged is invoked when
|
||||
// the dialog is accepted (typically a refresh callback). Returns the two
|
||||
// actions so callers can attach icons, etc.
|
||||
static std::pair<QAction *, QAction *> addFlagMenuActions(
|
||||
// Append add, edit and delete actions at addr, disabling inapplicable actions.
|
||||
// onChanged is called after accepting the dialog or deleting the flag.
|
||||
static std::tuple<QAction *, QAction *, QAction *> addFlagMenuActions(
|
||||
QMenu *menu,
|
||||
QWidget *dialogParent,
|
||||
RVA addr,
|
||||
@@ -48,9 +44,6 @@ private:
|
||||
RVA offset;
|
||||
QString flagName;
|
||||
ut64 flagOffset;
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
};
|
||||
|
||||
#endif // FLAGDIALOG_H
|
||||
@@ -107,6 +107,13 @@
|
||||
<item>
|
||||
<widget class="QLineEdit" name="colorEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="colorButton">
|
||||
<property name="text">
|
||||
<string>Choose color...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="colorPreview">
|
||||
<property name="minimumSize">
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
#include "common/AsyncTask.h"
|
||||
#include "ui_InitialOptionsDialog.h"
|
||||
|
||||
#include "common/Helpers.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "dialogs/AsyncTaskDialog.h"
|
||||
#include "dialogs/NewFileDialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCloseEvent>
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QElapsedTimer>
|
||||
@@ -27,6 +25,7 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "common/AnalTask.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "core/Iaito.h"
|
||||
|
||||
static QPlainTextEdit *s_analysisLogWidget = nullptr;
|
||||
@@ -189,6 +188,52 @@ void InitialOptionsDialog::updateCPUComboBox()
|
||||
ui->cpuComboBox->lineEdit()->setText(currentText);
|
||||
}
|
||||
|
||||
void InitialOptionsDialog::savePersistentOptions()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.beginGroup("InitialOptions");
|
||||
settings.setValue("analLevel", ui->analSlider->value());
|
||||
AnalysisCommands item;
|
||||
foreach (item, analysisCommands) {
|
||||
settings.setValue("anal/" + item.commandDesc.command, item.checkbox->isChecked());
|
||||
}
|
||||
settings.setValue("writeEnabled", ui->writeCheckBox->isChecked());
|
||||
settings.setValue("loadBinInfo", ui->binCheckBox->isChecked());
|
||||
settings.setValue("loadBinCache", ui->binCacheCheckBox->isChecked());
|
||||
settings.setValue("demangle", ui->demangleCheckBox->isChecked());
|
||||
settings.setValue("analVars", ui->varCheckBox->isChecked());
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
void InitialOptionsDialog::restorePersistentOptions()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.beginGroup("InitialOptions");
|
||||
if (!settings.contains("analLevel")) {
|
||||
settings.endGroup();
|
||||
return;
|
||||
}
|
||||
int level = settings.value("analLevel").toInt();
|
||||
if (level == 6) {
|
||||
AnalysisCommands item;
|
||||
foreach (item, analysisCommands) {
|
||||
item.checkbox->setChecked(
|
||||
settings.value("anal/" + item.commandDesc.command, item.checked).toBool());
|
||||
}
|
||||
}
|
||||
ui->analSlider->setValue(level);
|
||||
ui->writeCheckBox->setChecked(
|
||||
settings.value("writeEnabled", ui->writeCheckBox->isChecked()).toBool());
|
||||
ui->binCheckBox->setChecked(
|
||||
settings.value("loadBinInfo", ui->binCheckBox->isChecked()).toBool());
|
||||
ui->binCacheCheckBox->setChecked(
|
||||
settings.value("loadBinCache", ui->binCacheCheckBox->isChecked()).toBool());
|
||||
ui->demangleCheckBox->setChecked(
|
||||
settings.value("demangle", ui->demangleCheckBox->isChecked()).toBool());
|
||||
ui->varCheckBox->setChecked(settings.value("analVars", ui->varCheckBox->isChecked()).toBool());
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
QList<QString> InitialOptionsDialog::getAnalysisCommands(const InitialOptions &options)
|
||||
{
|
||||
QList<QString> commands;
|
||||
@@ -287,6 +332,10 @@ void InitialOptionsDialog::loadOptions(const InitialOptions &options)
|
||||
ui->binCacheCheckBox->setChecked(options.loadBinCache);
|
||||
ui->demangleCheckBox->setChecked(options.demangle);
|
||||
ui->varCheckBox->setChecked(options.analVars);
|
||||
|
||||
if (!debugMode && options.script.isEmpty()) {
|
||||
restorePersistentOptions();
|
||||
}
|
||||
}
|
||||
|
||||
void InitialOptionsDialog::setTooltipWithConfigHelp(QWidget *w, const char *config)
|
||||
@@ -451,7 +500,7 @@ void InitialOptionsDialog::setupAndStartAnalysis()
|
||||
|
||||
QPlainTextEdit logText;
|
||||
logText.setReadOnly(true);
|
||||
qhelpers::bindFont(&logText, false, true);
|
||||
logText.setFont(qhelpers::logFont());
|
||||
layout.addWidget(&logText);
|
||||
|
||||
QPushButton interruptBtn(tr("Interrupt"));
|
||||
@@ -684,14 +733,10 @@ void InitialOptionsDialog::setupAndStartAnalysis()
|
||||
void InitialOptionsDialog::on_okButton_clicked()
|
||||
{
|
||||
ui->okButton->setEnabled(false);
|
||||
savePersistentOptions();
|
||||
setupAndStartAnalysis();
|
||||
}
|
||||
|
||||
void InitialOptionsDialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
event->accept();
|
||||
}
|
||||
|
||||
QString InitialOptionsDialog::analysisDescription(int level)
|
||||
{
|
||||
// TODO: replace this with meaningful descriptions
|
||||
|
||||
@@ -35,9 +35,6 @@ private slots:
|
||||
void updatePDBLayout();
|
||||
void updateScriptLayout();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::InitialOptionsDialog> ui;
|
||||
|
||||
@@ -52,6 +49,8 @@ private:
|
||||
QList<RAsmPluginDescription> asmPlugins;
|
||||
|
||||
void updateCPUComboBox();
|
||||
void savePersistentOptions();
|
||||
void restorePersistentOptions();
|
||||
struct AnalysisCommands
|
||||
{
|
||||
CommandDescription commandDesc;
|
||||
|
||||
@@ -197,8 +197,6 @@ NewFileDialog::NewFileDialog(MainWindow *main)
|
||||
setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));
|
||||
setAcceptDrops(true);
|
||||
applyListFonts();
|
||||
ui->recentsListWidget->viewport()->installEventFilter(this);
|
||||
ui->projectsListWidget->viewport()->installEventFilter(this);
|
||||
ui->recentsListWidget->addAction(ui->actionRemove_item);
|
||||
ui->recentsListWidget->addAction(ui->actionClear_all);
|
||||
ui->projectsListWidget->addAction(ui->actionRemove_project);
|
||||
@@ -507,39 +505,11 @@ void NewFileDialog::on_exportProjectButton_clicked()
|
||||
|
||||
void NewFileDialog::applyListFonts()
|
||||
{
|
||||
ui->recentsListWidget->setFont(Config()->getSmallFont());
|
||||
ui->projectsListWidget->setFont(Config()->getSmallFont());
|
||||
}
|
||||
|
||||
void NewFileDialog::zoomFonts(int delta)
|
||||
{
|
||||
if (delta == 0) {
|
||||
return;
|
||||
const QFont font = QApplication::font();
|
||||
const auto views = findChildren<QAbstractItemView *>();
|
||||
for (QAbstractItemView *view : views) {
|
||||
view->setFont(font);
|
||||
}
|
||||
Config()->setZoomFactor(Config()->getZoomFactor() + (delta > 0 ? 0.1 : -0.1));
|
||||
applyListFonts();
|
||||
}
|
||||
|
||||
void NewFileDialog::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
if (event->modifiers() & (Qt::ControlModifier | Qt::MetaModifier)) {
|
||||
zoomFonts(event->angleDelta().y());
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QDialog::wheelEvent(event);
|
||||
}
|
||||
|
||||
bool NewFileDialog::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Wheel) {
|
||||
auto *wheelEvent = static_cast<QWheelEvent *>(event);
|
||||
if (wheelEvent->modifiers() & (Qt::ControlModifier | Qt::MetaModifier)) {
|
||||
zoomFonts(wheelEvent->angleDelta().y());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QDialog::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void NewFileDialog::dragEnterEvent(QDragEnterEvent *event)
|
||||
|
||||
@@ -74,10 +74,8 @@ private slots:
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void dragEnterEvent(QDragEnterEvent *event);
|
||||
void dropEvent(QDropEvent *event);
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::NewFileDialog> ui;
|
||||
@@ -125,7 +123,6 @@ private:
|
||||
void setDisableAndHideWidget(QWidget *w, bool disable_and_hide = true);
|
||||
void setSpacerEnabled(QSpacerItem *s, bool enabled, int w = 10, int h = 10);
|
||||
|
||||
void zoomFonts(int delta);
|
||||
void applyListFonts();
|
||||
|
||||
static const int MaxRecentFiles = 5;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// PackageManagerDialog.cpp
|
||||
#include "PackageManagerDialog.h"
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QJsonArray>
|
||||
@@ -9,6 +10,7 @@
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
@@ -19,6 +21,14 @@
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
static QString processError(QProcess &process, const QString &message)
|
||||
{
|
||||
const QString details = QString::fromLocal8Bit(
|
||||
process.readAllStandardOutput() + process.readAllStandardError())
|
||||
.trimmed();
|
||||
return details.isEmpty() ? message : message + "\n\n" + details;
|
||||
}
|
||||
|
||||
void CheckBoxDelegate::paint(
|
||||
QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
@@ -38,20 +48,30 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_process(new QProcess(this))
|
||||
{
|
||||
setWindowTitle(tr("Package Manager"));
|
||||
setWindowTitle(tr("Radare2 Package Manager (r2pm)"));
|
||||
resize(900, 600);
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
auto *filterLayout = new QHBoxLayout();
|
||||
m_filterLineEdit = new QLineEdit(this);
|
||||
m_filterLineEdit->setPlaceholderText(tr("Filter packages..."));
|
||||
m_filterLineEdit->setMinimumHeight(32);
|
||||
layout->addWidget(m_filterLineEdit);
|
||||
m_binaryPackagesCheckBox = new QCheckBox(tr("Binary packages"), this);
|
||||
m_binaryPackagesCheckBox->setToolTip(
|
||||
tr("Install ready-to-use packages, including scripts, without building from source."));
|
||||
m_showAllPlatformsCheckBox = new QCheckBox(tr("Show all platforms"), this);
|
||||
filterLayout->addWidget(m_filterLineEdit);
|
||||
filterLayout->addWidget(m_binaryPackagesCheckBox);
|
||||
filterLayout->addWidget(m_showAllPlatformsCheckBox);
|
||||
layout->addLayout(filterLayout);
|
||||
|
||||
m_tableWidget = new QTableWidget(this);
|
||||
m_tableWidget->setColumnCount(3);
|
||||
m_tableWidget->setHorizontalHeaderLabels({tr("Installed"), tr("Package"), tr("Description")});
|
||||
m_tableWidget->setColumnCount(4);
|
||||
m_tableWidget->setHorizontalHeaderLabels(
|
||||
{tr("Installed"), tr("Package"), tr("Platforms"), tr("Description")});
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
|
||||
m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_tableWidget->setSortingEnabled(true);
|
||||
@@ -61,11 +81,15 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
|
||||
|
||||
auto *buttonLayout = new QHBoxLayout();
|
||||
m_refreshButton = new QPushButton(tr("Refresh"), this);
|
||||
m_installButton = new QPushButton(tr("Install"), this);
|
||||
m_uninstallButton = new QPushButton(tr("Uninstall"), this);
|
||||
m_installButton = new QPushButton(tr("Install"), this);
|
||||
for (auto *button : {m_refreshButton, m_uninstallButton, m_installButton}) {
|
||||
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
}
|
||||
buttonLayout->addWidget(m_refreshButton);
|
||||
buttonLayout->addWidget(m_installButton);
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(m_uninstallButton);
|
||||
buttonLayout->addWidget(m_installButton);
|
||||
layout->addLayout(buttonLayout);
|
||||
|
||||
m_logTextEdit = new QTextEdit(this);
|
||||
@@ -74,6 +98,12 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
|
||||
layout->addWidget(m_logTextEdit);
|
||||
|
||||
connect(m_filterLineEdit, &QLineEdit::textChanged, this, &PackageManagerDialog::filterPackages);
|
||||
connect(m_binaryPackagesCheckBox, &QCheckBox::toggled, this, [this]() {
|
||||
filterPackages(m_filterLineEdit->text());
|
||||
});
|
||||
connect(m_showAllPlatformsCheckBox, &QCheckBox::toggled, this, [this]() {
|
||||
filterPackages(m_filterLineEdit->text());
|
||||
});
|
||||
connect(m_refreshButton, &QPushButton::clicked, this, &PackageManagerDialog::refreshPackages);
|
||||
connect(m_installButton, &QPushButton::clicked, this, &PackageManagerDialog::installPackage);
|
||||
connect(m_uninstallButton, &QPushButton::clicked, this, &PackageManagerDialog::uninstallPackage);
|
||||
@@ -104,23 +134,30 @@ void PackageManagerDialog::refreshPackages()
|
||||
QProcess updateProc;
|
||||
updateProc.start("r2pm", QStringList() << "-U");
|
||||
if (!updateProc.waitForFinished(30000)) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to run r2pm -U"));
|
||||
QMessageBox::warning(
|
||||
this, tr("Error"), processError(updateProc, tr("Failed to run r2pm -U.")));
|
||||
return;
|
||||
}
|
||||
// Then list packages
|
||||
QProcess listProc;
|
||||
listProc.start("r2pm", QStringList() << "-sj");
|
||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
||||
environment.remove("R2PM_PLATFORM");
|
||||
listProc.setProcessEnvironment(environment);
|
||||
listProc.start("r2pm", QStringList() << "-Asj");
|
||||
if (!listProc.waitForFinished(30000)) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to run r2pm -sj"));
|
||||
QMessageBox::warning(
|
||||
this, tr("Error"), processError(listProc, tr("Failed to run r2pm -Asj.")));
|
||||
return;
|
||||
}
|
||||
QByteArray out = listProc.readAllStandardOutput();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(out);
|
||||
if (!doc.isArray()) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -sj"));
|
||||
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -Asj"));
|
||||
return;
|
||||
}
|
||||
QJsonArray array = doc.array();
|
||||
const bool sortingEnabled = m_tableWidget->isSortingEnabled();
|
||||
m_tableWidget->setSortingEnabled(false);
|
||||
m_tableWidget->setRowCount(0);
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject())
|
||||
@@ -131,6 +168,13 @@ void PackageManagerDialog::refreshPackages()
|
||||
if (desc == "") {
|
||||
desc = obj["desc"].toString();
|
||||
}
|
||||
QStringList platforms;
|
||||
for (const QJsonValue &platformValue : obj["platforms"].toArray()) {
|
||||
const QString platform = platformValue.toString();
|
||||
if (!platform.isEmpty()) {
|
||||
platforms << platform;
|
||||
}
|
||||
}
|
||||
int row = m_tableWidget->rowCount();
|
||||
m_tableWidget->insertRow(row);
|
||||
QTableWidgetItem *itemInstalled = new QTableWidgetItem();
|
||||
@@ -138,12 +182,20 @@ void PackageManagerDialog::refreshPackages()
|
||||
itemInstalled->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
m_tableWidget->setItem(row, 0, itemInstalled);
|
||||
QTableWidgetItem *itemName = new QTableWidgetItem(name);
|
||||
itemName->setData(Qt::UserRole, obj["binary"].toBool());
|
||||
itemName->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
m_tableWidget->setItem(row, 1, itemName);
|
||||
QTableWidgetItem *itemPlatforms = new QTableWidgetItem(platforms.join(' '));
|
||||
itemPlatforms->setData(Qt::UserRole, obj["supported"].toBool(true));
|
||||
itemPlatforms->setToolTip(platforms.join(", "));
|
||||
itemPlatforms->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
itemPlatforms->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
m_tableWidget->setItem(row, 2, itemPlatforms);
|
||||
QTableWidgetItem *itemDesc = new QTableWidgetItem(desc);
|
||||
itemDesc->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
m_tableWidget->setItem(row, 2, itemDesc);
|
||||
m_tableWidget->setItem(row, 3, itemDesc);
|
||||
}
|
||||
m_tableWidget->setSortingEnabled(sortingEnabled);
|
||||
filterPackages(m_filterLineEdit->text());
|
||||
}
|
||||
|
||||
@@ -166,30 +218,43 @@ void PackageManagerDialog::filterPackages(const QString &text)
|
||||
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
|
||||
bool visible = text.isEmpty()
|
||||
|| m_tableWidget->item(row, 1)->text().contains(text, Qt::CaseInsensitive)
|
||||
|| m_tableWidget->item(row, 2)->text().contains(text, Qt::CaseInsensitive);
|
||||
|| m_tableWidget->item(row, 2)->text().contains(text, Qt::CaseInsensitive)
|
||||
|| m_tableWidget->item(row, 3)->text().contains(text, Qt::CaseInsensitive);
|
||||
if (m_binaryPackagesCheckBox->isChecked()) {
|
||||
visible = visible && m_tableWidget->item(row, 1)->data(Qt::UserRole).toBool();
|
||||
}
|
||||
if (!m_showAllPlatformsCheckBox->isChecked()) {
|
||||
visible = visible && m_tableWidget->item(row, 2)->data(Qt::UserRole).toBool();
|
||||
}
|
||||
m_tableWidget->setRowHidden(row, !visible);
|
||||
}
|
||||
if (m_tableWidget->currentRow() >= 0
|
||||
&& m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
|
||||
m_tableWidget->setCurrentItem(nullptr);
|
||||
m_tableWidget->clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void PackageManagerDialog::installPackage()
|
||||
{
|
||||
if (m_process->state() != QProcess::NotRunning)
|
||||
return;
|
||||
if (m_tableWidget->currentRow() < 0) {
|
||||
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
|
||||
QMessageBox::information(this, tr("Install"), tr("Please select a package to install."));
|
||||
return;
|
||||
}
|
||||
QString pkg = m_tableWidget->item(m_tableWidget->currentRow(), 1)->text();
|
||||
m_logTextEdit->clear();
|
||||
m_logTextEdit->setVisible(true);
|
||||
m_process->start("r2pm", QStringList() << "-ci" << pkg);
|
||||
const QString flags = m_binaryPackagesCheckBox->isChecked() ? "-bi" : "-ci";
|
||||
m_process->start("r2pm", QStringList() << flags << pkg);
|
||||
}
|
||||
|
||||
void PackageManagerDialog::uninstallPackage()
|
||||
{
|
||||
if (m_process->state() != QProcess::NotRunning)
|
||||
return;
|
||||
if (m_tableWidget->currentRow() < 0) {
|
||||
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
|
||||
QMessageBox::information(this, tr("Uninstall"), tr("Please select a package to uninstall."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
class QLineEdit;
|
||||
class QCheckBox;
|
||||
class QTableWidget;
|
||||
class QPushButton;
|
||||
class QTextEdit;
|
||||
@@ -42,6 +43,8 @@ private slots:
|
||||
|
||||
private:
|
||||
QLineEdit *m_filterLineEdit;
|
||||
QCheckBox *m_binaryPackagesCheckBox;
|
||||
QCheckBox *m_showAllPlatformsCheckBox;
|
||||
QTableWidget *m_tableWidget;
|
||||
QPushButton *m_refreshButton;
|
||||
QPushButton *m_installButton;
|
||||
@@ -52,4 +55,4 @@ private:
|
||||
void populateInstalledPackages();
|
||||
};
|
||||
|
||||
#endif // PACKAGEMANAGERDIALOG_H
|
||||
#endif // PACKAGEMANAGERDIALOG_H
|
||||
@@ -78,16 +78,23 @@ void R2PluginsDialog::loadPlugin()
|
||||
void R2PluginsDialog::refreshPluginDescriptions()
|
||||
{
|
||||
ui->RBinTreeWidget->clear();
|
||||
ui->RDemangleTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRBinPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
item->setText(1, plugin.description);
|
||||
item->setText(2, plugin.license);
|
||||
item->setText(3, plugin.type);
|
||||
ui->RBinTreeWidget->addTopLevelItem(item);
|
||||
if (plugin.type == QStringLiteral("dem")) {
|
||||
ui->RDemangleTreeWidget->addTopLevelItem(item);
|
||||
} else {
|
||||
item->setText(3, plugin.type);
|
||||
ui->RBinTreeWidget->addTopLevelItem(item);
|
||||
}
|
||||
}
|
||||
ui->RBinTreeWidget->sortByColumn(0, Qt::AscendingOrder);
|
||||
qhelpers::adjustColumns(ui->RBinTreeWidget, 0);
|
||||
ui->RDemangleTreeWidget->sortByColumn(0, Qt::AscendingOrder);
|
||||
qhelpers::adjustColumns(ui->RDemangleTreeWidget, 0);
|
||||
|
||||
ui->RIOTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRIOPluginDescriptions()) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>4</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="RBinTab">
|
||||
<attribute name="title">
|
||||
@@ -60,6 +60,42 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="RDemangleTab">
|
||||
<attribute name="title">
|
||||
<string>Demangle</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_11">
|
||||
<item>
|
||||
<widget class="QLabel" name="RDemangleLabel">
|
||||
<property name="text">
|
||||
<string>Demangle symbol names for supported languages and compilers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="RDemangleTreeWidget">
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Description</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>License</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="RIOTab">
|
||||
<attribute name="title">
|
||||
<string>RIO</string>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="toTreeWidget">
|
||||
<widget class="AddressableItemView" name="toTreeWidget">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
@@ -73,7 +73,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="fromTreeWidget">
|
||||
<widget class="AddressableItemView" name="fromTreeWidget">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
@@ -142,7 +142,7 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AddressableItemList<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
</customwidget>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QFontDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
@@ -82,6 +83,17 @@ AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
|
||||
->addItem(tr("Bottom"), static_cast<int>(Configuration::VisualNavbarLocation::Bottom));
|
||||
ui->visualNavbarLocationComboBox->addItem(
|
||||
tr("SuperBottom"), static_cast<int>(Configuration::VisualNavbarLocation::SuperBottom));
|
||||
{
|
||||
// Filling the list must not write the first entry into the settings
|
||||
QSignalBlocker blocker(ui->memoryScrollBarComboBox);
|
||||
ui->memoryScrollBarComboBox
|
||||
->addItem(tr("Hidden"), static_cast<int>(Configuration::MemoryScrollBarMode::Hidden));
|
||||
ui->memoryScrollBarComboBox->addItem(
|
||||
tr("Spring (unbounded)"), static_cast<int>(Configuration::MemoryScrollBarMode::Spring));
|
||||
ui->memoryScrollBarComboBox->addItem(
|
||||
tr("Bounded (navigation bar range)"),
|
||||
static_cast<int>(Configuration::MemoryScrollBarMode::Bounded));
|
||||
}
|
||||
updateFromConfig();
|
||||
|
||||
QStringList langs = Config()->getAvailableTranslations();
|
||||
@@ -133,6 +145,11 @@ AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
|
||||
connect(Config(), &Configuration::visualNavbarThicknessChanged, this, [this](int) {
|
||||
updateVisualNavbarFromConfig();
|
||||
});
|
||||
connect(
|
||||
Config(),
|
||||
&Configuration::memoryScrollBarModeChanged,
|
||||
this,
|
||||
[this](Configuration::MemoryScrollBarMode) { updateVisualNavbarFromConfig(); });
|
||||
connect(Config(), &Configuration::colorsUpdated, this, [this]() {
|
||||
updateVisualNavbarFromConfig();
|
||||
});
|
||||
@@ -235,6 +252,7 @@ void AppearanceOptionsWidget::updateVisualNavbarFromConfig()
|
||||
{
|
||||
QSignalBlocker locationBlocker(ui->visualNavbarLocationComboBox);
|
||||
QSignalBlocker thicknessBlocker(ui->visualNavbarThicknessSpinBox);
|
||||
QSignalBlocker scrollBarBlocker(ui->memoryScrollBarComboBox);
|
||||
|
||||
auto location = static_cast<int>(Config()->getVisualNavbarLocation());
|
||||
int index = ui->visualNavbarLocationComboBox->findData(location);
|
||||
@@ -243,6 +261,11 @@ void AppearanceOptionsWidget::updateVisualNavbarFromConfig()
|
||||
}
|
||||
|
||||
ui->visualNavbarThicknessSpinBox->setValue(Config()->getVisualNavbarThickness());
|
||||
const int scrollBarMode = static_cast<int>(Config()->getMemoryScrollBarMode());
|
||||
const int scrollBarIndex = ui->memoryScrollBarComboBox->findData(scrollBarMode);
|
||||
if (scrollBarIndex >= 0) {
|
||||
ui->memoryScrollBarComboBox->setCurrentIndex(scrollBarIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::onFontZoomBoxValueChanged(int zoom)
|
||||
@@ -311,10 +334,25 @@ void AppearanceOptionsWidget::on_visualNavbarThicknessSpinBox_valueChanged(int v
|
||||
Config()->setVisualNavbarThickness(value);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_memoryScrollBarComboBox_currentIndexChanged(int index)
|
||||
{
|
||||
Config()->setMemoryScrollBarMode(
|
||||
static_cast<Configuration::MemoryScrollBarMode>(
|
||||
ui->memoryScrollBarComboBox->itemData(index).toInt()));
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_editButton_clicked()
|
||||
{
|
||||
QString theme = ui->colorComboBox->currentText();
|
||||
if (!ThemeWorker().isCustomTheme(theme)) {
|
||||
if (!confirmSystemThemeCopy(theme) || !copyColorTheme(theme)) {
|
||||
return;
|
||||
}
|
||||
theme = ui->colorComboBox->currentText();
|
||||
}
|
||||
|
||||
ColorThemeEditDialog dial;
|
||||
dial.setWindowTitle(tr("Theme Editor - <%1>").arg(ui->colorComboBox->currentText()));
|
||||
dial.setWindowTitle(tr("Theme Editor - <%1>").arg(theme));
|
||||
dial.exec();
|
||||
ui->colorComboBox->updateFromConfig(false);
|
||||
}
|
||||
@@ -333,7 +371,15 @@ void AppearanceOptionsWidget::selectInterfaceThemeByName(const QString &name)
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceEditButton_clicked()
|
||||
{
|
||||
InterfaceThemeEditDialog dialog(ui->themeComboBox->currentText(), this);
|
||||
QString theme = ui->themeComboBox->currentText();
|
||||
if (!Configuration::isCustomInterfaceTheme(theme)) {
|
||||
if (!confirmSystemThemeCopy(theme) || !copyInterfaceTheme(theme)) {
|
||||
return;
|
||||
}
|
||||
theme = ui->themeComboBox->currentText();
|
||||
}
|
||||
|
||||
InterfaceThemeEditDialog dialog(theme, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
selectInterfaceThemeByName(dialog.savedName());
|
||||
}
|
||||
@@ -341,8 +387,11 @@ void AppearanceOptionsWidget::on_ifaceEditButton_clicked()
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceCopyButton_clicked()
|
||||
{
|
||||
const QString current = ui->themeComboBox->currentText();
|
||||
copyInterfaceTheme(ui->themeComboBox->currentText());
|
||||
}
|
||||
|
||||
bool AppearanceOptionsWidget::copyInterfaceTheme(const QString &theme)
|
||||
{
|
||||
QString newName;
|
||||
do {
|
||||
newName = QInputDialog::getText(
|
||||
@@ -350,10 +399,10 @@ void AppearanceOptionsWidget::on_ifaceCopyButton_clicked()
|
||||
tr("Enter theme name"),
|
||||
tr("Name:"),
|
||||
QLineEdit::Normal,
|
||||
current + tr(" - copy"))
|
||||
theme + tr(" - copy"))
|
||||
.trimmed();
|
||||
if (newName.isEmpty()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!Configuration::isValidThemeName(newName)) {
|
||||
QMessageBox::warning(this, tr("Theme Copy"), tr("Invalid theme name."));
|
||||
@@ -367,16 +416,23 @@ void AppearanceOptionsWidget::on_ifaceCopyButton_clicked()
|
||||
|
||||
const QString dst
|
||||
= QDir(Configuration::userThemesDir()).filePath(newName + QStringLiteral(".theme"));
|
||||
if (Configuration::isCustomInterfaceTheme(current)) {
|
||||
QFile::copy(
|
||||
QDir(Configuration::userThemesDir()).filePath(current + QStringLiteral(".theme")), dst);
|
||||
bool copied = false;
|
||||
if (Configuration::isCustomInterfaceTheme(theme)) {
|
||||
copied = QFile::copy(
|
||||
QDir(Configuration::userThemesDir()).filePath(theme + QStringLiteral(".theme")), dst);
|
||||
} else {
|
||||
Theme t = current == QStringLiteral("Native") ? IaitoStyle::instance()->theme()
|
||||
: Theme::builtin(current);
|
||||
Theme t = theme == QStringLiteral("Native") ? IaitoStyle::instance()->theme()
|
||||
: Theme::builtin(theme);
|
||||
t.name = newName;
|
||||
t.save(dst);
|
||||
copied = QFileInfo::exists(dst);
|
||||
}
|
||||
if (!copied) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Cannot copy theme."));
|
||||
return false;
|
||||
}
|
||||
selectInterfaceThemeByName(newName);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceRenameButton_clicked()
|
||||
@@ -478,15 +534,18 @@ void AppearanceOptionsWidget::on_ifaceImportButton_clicked()
|
||||
void AppearanceOptionsWidget::updateInterfaceModificationButtons(const QString &theme)
|
||||
{
|
||||
bool editable = Configuration::isCustomInterfaceTheme(theme);
|
||||
ui->ifaceEditButton->setEnabled(editable);
|
||||
ui->ifaceEditButton->setEnabled(!theme.isEmpty());
|
||||
ui->ifaceRenameButton->setEnabled(editable);
|
||||
ui->ifaceDeleteButton->setEnabled(editable);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_copyButton_clicked()
|
||||
{
|
||||
QString currColorTheme = ui->colorComboBox->currentText();
|
||||
copyColorTheme(ui->colorComboBox->currentText());
|
||||
}
|
||||
|
||||
bool AppearanceOptionsWidget::copyColorTheme(const QString &theme)
|
||||
{
|
||||
QString newThemeName;
|
||||
do {
|
||||
newThemeName = QInputDialog::getText(
|
||||
@@ -494,10 +553,10 @@ void AppearanceOptionsWidget::on_copyButton_clicked()
|
||||
tr("Enter theme name"),
|
||||
tr("Name:"),
|
||||
QLineEdit::Normal,
|
||||
currColorTheme + tr(" - copy"))
|
||||
theme + tr(" - copy"))
|
||||
.trimmed();
|
||||
if (newThemeName.isEmpty()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (ThemeWorker().isThemeExist(newThemeName)) {
|
||||
QMessageBox::information(
|
||||
@@ -507,9 +566,14 @@ void AppearanceOptionsWidget::on_copyButton_clicked()
|
||||
}
|
||||
} while (true);
|
||||
|
||||
ThemeWorker().copy(currColorTheme, newThemeName);
|
||||
QString err = ThemeWorker().copy(theme, newThemeName);
|
||||
if (!err.isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"), err);
|
||||
return false;
|
||||
}
|
||||
Config()->setColorTheme(newThemeName);
|
||||
updateThemeFromConfig(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_deleteButton_clicked()
|
||||
@@ -616,11 +680,26 @@ void AppearanceOptionsWidget::onLanguageComboBoxCurrentIndexChanged(int index)
|
||||
void AppearanceOptionsWidget::updateModificationButtons(const QString &theme)
|
||||
{
|
||||
bool editable = ThemeWorker().isCustomTheme(theme);
|
||||
ui->editButton->setEnabled(editable);
|
||||
ui->editButton->setEnabled(!theme.isEmpty());
|
||||
ui->deleteButton->setEnabled(editable);
|
||||
ui->renameButton->setEnabled(editable);
|
||||
}
|
||||
|
||||
bool AppearanceOptionsWidget::confirmSystemThemeCopy(const QString &theme)
|
||||
{
|
||||
QMessageBox prompt(this);
|
||||
prompt.setIcon(QMessageBox::Question);
|
||||
prompt.setWindowTitle(tr("Edit Theme"));
|
||||
prompt.setText(
|
||||
tr("The system theme <b>%1</b> cannot be edited directly.").arg(theme.toHtmlEscaped()));
|
||||
prompt.setInformativeText(tr("Would you like to make a copy and start editing it?"));
|
||||
QPushButton *copyButton = prompt.addButton(tr("Make a Copy"), QMessageBox::AcceptRole);
|
||||
prompt.addButton(QMessageBox::Cancel);
|
||||
prompt.setDefaultButton(copyButton);
|
||||
prompt.exec();
|
||||
return prompt.clickedButton() == copyButton;
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::updateFromConfig()
|
||||
{
|
||||
updateFontFromConfig();
|
||||
|
||||
@@ -38,6 +38,7 @@ private slots:
|
||||
void on_themeComboBox_currentIndexChanged(int index);
|
||||
void on_visualNavbarLocationComboBox_currentIndexChanged(int index);
|
||||
void on_visualNavbarThicknessSpinBox_valueChanged(int value);
|
||||
void on_memoryScrollBarComboBox_currentIndexChanged(int index);
|
||||
void on_copyButton_clicked();
|
||||
void on_deleteButton_clicked();
|
||||
|
||||
@@ -69,6 +70,9 @@ private slots:
|
||||
void onLanguageComboBoxCurrentIndexChanged(int index);
|
||||
|
||||
private:
|
||||
bool confirmSystemThemeCopy(const QString &theme);
|
||||
bool copyColorTheme(const QString &theme);
|
||||
bool copyInterfaceTheme(const QString &theme);
|
||||
void updateModificationButtons(const QString &theme);
|
||||
void updateInterfaceModificationButtons(const QString &theme);
|
||||
void selectInterfaceThemeByName(const QString &name);
|
||||
|
||||
@@ -324,6 +324,30 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="memoryScrollBarLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="memoryScrollBarLabel">
|
||||
<property name="text">
|
||||
<string>Hexdump and disassembly scrollbar</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="memoryScrollBarComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Hidden: no scrollbar. Spring: the handle rests in the middle and scrolls faster the further and longer it is dragged. Bounded: a regular scrollbar over the navigation bar range.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
|
||||
@@ -17,6 +17,12 @@ AsmOptionsWidget::AsmOptionsWidget(SettingsDialog *dialog)
|
||||
, ui(new Ui::AsmOptionsWidget)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->jumpLineSpacingComboBox->blockSignals(true);
|
||||
for (int spacing : {2, 4, 6, 8, 10, 12, 16, 20, 24, 32}) {
|
||||
ui->jumpLineSpacingComboBox->addItem(tr("%1 px").arg(spacing), spacing);
|
||||
}
|
||||
ui->jumpLineSpacingComboBox->blockSignals(false);
|
||||
|
||||
// Initialize "Relative to" combobox for asm.addr.relto values
|
||||
ui->relToComboBox->setEditable(true);
|
||||
ui->relToComboBox->blockSignals(true);
|
||||
@@ -116,6 +122,13 @@ AsmOptionsWidget::AsmOptionsWidget(SettingsDialog *dialog)
|
||||
connect(ui->offsetCheckBox, &QCheckBox::toggled, this, &AsmOptionsWidget::offsetCheckBoxToggled);
|
||||
connect(
|
||||
ui->relOffsetCheckBox, &QCheckBox::toggled, this, &AsmOptionsWidget::relOffCheckBoxToggled);
|
||||
connect(ui->linesCheckBox, &QCheckBox::toggled, ui->jumpLineSpacingLabel, &QWidget::setEnabled);
|
||||
connect(ui->linesCheckBox, &QCheckBox::toggled, ui->jumpLineSpacingComboBox, &QWidget::setEnabled);
|
||||
connect(
|
||||
ui->linesCheckBox,
|
||||
&QCheckBox::toggled,
|
||||
ui->adaptiveJumpLineSpacingCheckBox,
|
||||
&QWidget::setEnabled);
|
||||
connect(Core(), &IaitoCore::asmOptionsChanged, this, &AsmOptionsWidget::updateAsmOptionsFromVars);
|
||||
updateAsmOptionsFromVars();
|
||||
}
|
||||
@@ -183,6 +196,19 @@ void AsmOptionsWidget::updateAsmOptionsFromVars()
|
||||
qhelpers::setCheckedWithoutSignals(
|
||||
confCheckbox->checkBox, Config()->getConfigBool(confCheckbox->config));
|
||||
}
|
||||
const bool adaptiveSpacing = Config()->getDisassemblyJumpLineAdaptiveSpacingEnabled();
|
||||
qhelpers::setCheckedWithoutSignals(ui->adaptiveJumpLineSpacingCheckBox, adaptiveSpacing);
|
||||
ui->jumpLineSpacingLabel->setText(
|
||||
adaptiveSpacing ? tr("Maximum spacing:") : tr("Fixed spacing:"));
|
||||
ui->jumpLineSpacingComboBox->blockSignals(true);
|
||||
const int spacingIndex = ui->jumpLineSpacingComboBox->findData(
|
||||
Config()->getDisassemblyJumpLineSpacing());
|
||||
ui->jumpLineSpacingComboBox->setCurrentIndex(spacingIndex < 0 ? 0 : spacingIndex);
|
||||
ui->jumpLineSpacingComboBox->blockSignals(false);
|
||||
const bool jumpLinesEnabled = Config()->getConfigBool("asm.lines");
|
||||
ui->adaptiveJumpLineSpacingCheckBox->setEnabled(jumpLinesEnabled);
|
||||
ui->jumpLineSpacingLabel->setEnabled(jumpLinesEnabled);
|
||||
ui->jumpLineSpacingComboBox->setEnabled(jumpLinesEnabled);
|
||||
// Update "Relative to" combobox based on asm.reloff
|
||||
bool reloffEnabled = Config()->getConfigBool("asm.reloff");
|
||||
ui->relToComboBox->blockSignals(true);
|
||||
@@ -242,6 +268,22 @@ void AsmOptionsWidget::on_nbytesSpinBox_valueChanged(int value)
|
||||
triggerAsmOptionsChanged();
|
||||
}
|
||||
|
||||
void AsmOptionsWidget::on_jumpLineSpacingComboBox_currentIndexChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
Config()->setDisassemblyJumpLineSpacing(ui->jumpLineSpacingComboBox->itemData(index).toInt());
|
||||
triggerAsmOptionsChanged();
|
||||
}
|
||||
|
||||
void AsmOptionsWidget::on_adaptiveJumpLineSpacingCheckBox_toggled(bool checked)
|
||||
{
|
||||
Config()->setDisassemblyJumpLineAdaptiveSpacingEnabled(checked);
|
||||
ui->jumpLineSpacingLabel->setText(checked ? tr("Maximum spacing:") : tr("Fixed spacing:"));
|
||||
triggerAsmOptionsChanged();
|
||||
}
|
||||
|
||||
void AsmOptionsWidget::on_syntaxComboBox_currentIndexChanged(int index)
|
||||
{
|
||||
Config()->setConfig(
|
||||
|
||||
@@ -45,6 +45,8 @@ private slots:
|
||||
void on_asmTabsSpinBox_valueChanged(int value);
|
||||
void on_asmTabsOffSpinBox_valueChanged(int value);
|
||||
void on_nbytesSpinBox_valueChanged(int value);
|
||||
void on_jumpLineSpacingComboBox_currentIndexChanged(int index);
|
||||
void on_adaptiveJumpLineSpacingCheckBox_toggled(bool checked);
|
||||
|
||||
void on_bytesCheckBox_toggled(bool checked);
|
||||
void on_varsubCheckBox_toggled(bool checked);
|
||||
|
||||
@@ -434,6 +434,34 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="adaptiveJumpLineSpacingCheckBox">
|
||||
<property name="text">
|
||||
<string>Fit jump lines to gutter</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Shrink jump line spacing as needed to keep every lane visible</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="jumpLineSpacingLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="jumpLineSpacingLabel">
|
||||
<property name="text">
|
||||
<string>Maximum spacing:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="jumpLineSpacingComboBox">
|
||||
<property name="toolTip">
|
||||
<string>Maximum spacing when fitting to the gutter, or exact spacing otherwise</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="fcnlinesCheckBox">
|
||||
<property name="text">
|
||||
|
||||
@@ -125,7 +125,8 @@ InterfaceThemeEditDialog::InterfaceThemeEditDialog(const QString &themeName, QWi
|
||||
chromeCombo = new QComboBox(this);
|
||||
chromeCombo->addItem(tr("Flat"));
|
||||
chromeCombo->addItem(tr("Accent"));
|
||||
chromeCombo->setCurrentIndex(m_base.chromeStyle == ChromeStyle::Accent ? 1 : 0);
|
||||
chromeCombo->addItem(tr("Accent tabs"));
|
||||
chromeCombo->setCurrentIndex(static_cast<int>(m_base.chromeStyle));
|
||||
connect(skinCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int) {
|
||||
previewTimer->start();
|
||||
});
|
||||
@@ -202,7 +203,7 @@ Theme InterfaceThemeEditDialog::buildTheme() const
|
||||
{
|
||||
Theme t = Theme::fromChrome(m_vars, m_dark);
|
||||
t.skin = skinCombo->currentIndex() == 1 ? Skin::Bevel : Skin::Modern;
|
||||
t.chromeStyle = chromeCombo->currentIndex() == 1 ? ChromeStyle::Accent : ChromeStyle::Flat;
|
||||
t.chromeStyle = static_cast<ChromeStyle>(chromeCombo->currentIndex());
|
||||
for (int i = 0; i < metricSpins.size(); ++i) {
|
||||
t.metrics.*(kEditorMetrics[i].field) = metricSpins[i]->value();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="#888" stroke-width="1.8" stroke-linecap="round">
|
||||
<path d="M9 5h11M9 12h11M9 19h11"/>
|
||||
<path d="M4 5h.5M4 12h.5M4 19h.5" stroke-width="3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 263 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="#888" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 5v14m0-7h5m-5 7h5M10 5h9m-5 7h5m-5 7h5"/>
|
||||
<rect x="3" y="3" width="4" height="4" fill="#888" stroke="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 319 B |
@@ -210,6 +210,9 @@ void AddressableItemContextMenu::onActionAddComment()
|
||||
|
||||
void AddressableItemContextMenu::aboutToShowSlot()
|
||||
{
|
||||
if (!hasTarget) {
|
||||
return;
|
||||
}
|
||||
if (actionShowInMenu->menu()) {
|
||||
actionShowInMenu->menu()->deleteLater();
|
||||
}
|
||||
@@ -225,6 +228,9 @@ void AddressableItemContextMenu::setHasTarget(bool hasTarget)
|
||||
{
|
||||
this->hasTarget = hasTarget;
|
||||
for (const auto &action : this->actions()) {
|
||||
action->setEnabled(hasTarget);
|
||||
// View options also apply to group rows and empty lists.
|
||||
if (!action->property("addressIndependent").toBool()) {
|
||||
action->setEnabled(hasTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#ifndef CONTEXTMENUICONS_H
|
||||
#define CONTEXTMENUICONS_H
|
||||
|
||||
#include <QAction>
|
||||
#include <QColor>
|
||||
#include <QIcon>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QPolygonF>
|
||||
#include <QVector>
|
||||
#include <QWidget>
|
||||
|
||||
namespace ContextMenuIcons {
|
||||
|
||||
enum class Icon {
|
||||
Copy,
|
||||
Navigation,
|
||||
XRefs,
|
||||
Comment,
|
||||
Rename,
|
||||
Delete,
|
||||
Edit,
|
||||
View,
|
||||
Breakpoint,
|
||||
Debug,
|
||||
Fold,
|
||||
Collapse,
|
||||
Expand,
|
||||
Sync
|
||||
};
|
||||
|
||||
inline void setIcon(const QWidget *widget, QAction *action, Icon icon, const QColor &color)
|
||||
{
|
||||
const qreal ratio = widget->devicePixelRatioF();
|
||||
const int size = 16;
|
||||
QPixmap pixmap(size * ratio, size * ratio);
|
||||
pixmap.setDevicePixelRatio(ratio);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QColor accent = color;
|
||||
accent.setAlpha(205);
|
||||
painter.setPen(QPen(accent, 1.8, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
|
||||
switch (icon) {
|
||||
case Icon::Copy:
|
||||
painter.drawRect(QRectF(5, 3.5, 7, 9));
|
||||
painter.drawRect(QRectF(3, 5.5, 7, 7));
|
||||
break;
|
||||
case Icon::Navigation:
|
||||
painter.drawLine(QPointF(4, 8), QPointF(11, 8));
|
||||
painter.drawPolyline(
|
||||
QPolygonF(QVector<QPointF>{QPointF(8, 5), QPointF(11, 8), QPointF(8, 11)}));
|
||||
break;
|
||||
case Icon::XRefs:
|
||||
painter.drawEllipse(QPointF(5, 8), 2.0, 2.0);
|
||||
painter.drawEllipse(QPointF(11, 5), 2.0, 2.0);
|
||||
painter.drawEllipse(QPointF(11, 11), 2.0, 2.0);
|
||||
painter.drawLine(QPointF(6.7, 7), QPointF(9.3, 5.8));
|
||||
painter.drawLine(QPointF(6.7, 9), QPointF(9.3, 10.2));
|
||||
break;
|
||||
case Icon::Comment:
|
||||
painter.drawRoundedRect(QRectF(3.5, 4, 9, 7), 1.5, 1.5);
|
||||
painter.drawLine(QPointF(6, 11), QPointF(4.5, 13));
|
||||
break;
|
||||
case Icon::Rename:
|
||||
painter.drawLine(QPointF(4, 12), QPointF(12, 4));
|
||||
painter.drawLine(QPointF(10, 4), QPointF(12, 6));
|
||||
painter.drawLine(QPointF(4, 12), QPointF(7, 11));
|
||||
break;
|
||||
case Icon::Delete:
|
||||
painter.drawLine(QPointF(4, 5), QPointF(12, 5));
|
||||
painter.drawLine(QPointF(6, 3.5), QPointF(10, 3.5));
|
||||
painter.drawRoundedRect(QRectF(5, 6, 6, 7), 1.0, 1.0);
|
||||
painter.drawLine(QPointF(7, 7.5), QPointF(7, 11.5));
|
||||
painter.drawLine(QPointF(9, 7.5), QPointF(9, 11.5));
|
||||
break;
|
||||
case Icon::Edit:
|
||||
painter.drawRect(QRectF(4, 4, 8, 8));
|
||||
painter.drawLine(QPointF(6, 7), QPointF(10, 7));
|
||||
painter.drawLine(QPointF(6, 9), QPointF(9, 9));
|
||||
break;
|
||||
case Icon::View:
|
||||
painter.drawEllipse(QRectF(3, 5, 10, 6));
|
||||
painter.drawEllipse(QPointF(8, 8), 1.8, 1.8);
|
||||
break;
|
||||
case Icon::Breakpoint:
|
||||
painter.setBrush(accent);
|
||||
painter.drawEllipse(QPointF(8, 8), 4.0, 4.0);
|
||||
break;
|
||||
case Icon::Debug:
|
||||
painter.drawLine(QPointF(8, 3), QPointF(8, 13));
|
||||
painter.drawLine(QPointF(4, 5), QPointF(12, 5));
|
||||
painter.drawLine(QPointF(4, 11), QPointF(12, 11));
|
||||
painter.drawRoundedRect(QRectF(5, 4, 6, 8), 1.5, 1.5);
|
||||
break;
|
||||
case Icon::Fold:
|
||||
painter.drawPolyline(
|
||||
QPolygonF(QVector<QPointF>{QPointF(4, 3), QPointF(8, 7), QPointF(12, 3)}));
|
||||
painter.drawPolyline(
|
||||
QPolygonF(QVector<QPointF>{QPointF(4, 13), QPointF(8, 9), QPointF(12, 13)}));
|
||||
break;
|
||||
case Icon::Collapse:
|
||||
case Icon::Expand:
|
||||
painter.drawRoundedRect(QRectF(3, 3, 10, 10), 1.5, 1.5);
|
||||
painter.drawLine(QPointF(5.5, 8), QPointF(10.5, 8));
|
||||
if (icon == Icon::Expand) {
|
||||
painter.drawLine(QPointF(8, 5.5), QPointF(8, 10.5));
|
||||
}
|
||||
break;
|
||||
case Icon::Sync:
|
||||
painter.drawLine(QPointF(3, 5), QPointF(12, 5));
|
||||
painter.drawPolyline(
|
||||
QPolygonF(QVector<QPointF>{QPointF(10, 3), QPointF(12, 5), QPointF(10, 7)}));
|
||||
painter.drawLine(QPointF(13, 11), QPointF(4, 11));
|
||||
painter.drawPolyline(
|
||||
QPolygonF(QVector<QPointF>{QPointF(6, 9), QPointF(4, 11), QPointF(6, 13)}));
|
||||
break;
|
||||
}
|
||||
|
||||
painter.end();
|
||||
action->setIcon(QIcon(pixmap));
|
||||
action->setIconVisibleInMenu(true);
|
||||
}
|
||||
|
||||
} // namespace ContextMenuIcons
|
||||
|
||||
#endif // CONTEXTMENUICONS_H
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "DecompilerContextMenu.h"
|
||||
#include "ContextMenuIcons.h"
|
||||
#include "MainWindow.h"
|
||||
#include "common/DeepLink.h"
|
||||
#include "common/ShortcutManager.h"
|
||||
@@ -19,6 +20,7 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
|
||||
, curHighlightedWord(QString())
|
||||
, offset(0)
|
||||
, decompiledFunctionAddress(RVA_INVALID)
|
||||
, firstOffsetInLine(RVA_INVALID)
|
||||
, isTogglingBreakpoints(false)
|
||||
, annotationHere(nullptr)
|
||||
, actionCopy(tr("Copy"), this)
|
||||
@@ -33,7 +35,8 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
|
||||
, actionDeleteName(tr("Delete <name>"), this)
|
||||
, actionEditFunctionVariables(tr("Edit variable <name of variable>"), this)
|
||||
, actionXRefs(tr("Show X-Refs"), this)
|
||||
, actionShowAddress(tr("Show address"), this)
|
||||
, actionShowAddress(tr("Show addresses"), this)
|
||||
, actionShowDisassembly(tr("Show disassembly"), this)
|
||||
, actionToggleBreakpoint(tr("Add/remove breakpoint"), this)
|
||||
, actionAdvancedBreakpoint(tr("Advanced breakpoint"), this)
|
||||
, breakpointsInLineMenu(new QMenu(this))
|
||||
@@ -54,6 +57,7 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
|
||||
|
||||
setActionXRefs();
|
||||
setActionShowAddress();
|
||||
setActionShowDisassembly();
|
||||
|
||||
setActionEditFunctionVariables();
|
||||
|
||||
@@ -64,6 +68,36 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
|
||||
// actionEditAnnotation.setVisible(true);
|
||||
// actionEditAnnotation.setEnabled(true);
|
||||
|
||||
using ContextMenuIcons::Icon;
|
||||
using ContextMenuIcons::setIcon;
|
||||
const QColor copyColor(56, 142, 60);
|
||||
const QColor navigationColor(103, 80, 164);
|
||||
const QColor annotationColor(67, 160, 71);
|
||||
const QColor analysisColor(0, 137, 190);
|
||||
const QColor viewColor(191, 128, 32);
|
||||
const QColor debugColor(216, 88, 64);
|
||||
|
||||
setIcon(this, &actionCopy, Icon::Copy, copyColor);
|
||||
setIcon(this, &actionCopyInstructionAddress, Icon::Copy, copyColor);
|
||||
setIcon(this, &actionCopyReferenceAddress, Icon::Copy, copyColor);
|
||||
setIcon(this, &actionCopyDeepLink, Icon::Copy, copyColor);
|
||||
setIcon(this, &actionShowInSubmenu, Icon::Navigation, navigationColor);
|
||||
setIcon(this, &actionAddComment, Icon::Comment, annotationColor);
|
||||
setIcon(this, &actionDeleteComment, Icon::Delete, debugColor);
|
||||
setIcon(this, &actionRenameThingHere, Icon::Rename, annotationColor);
|
||||
setIcon(this, &actionDeleteName, Icon::Delete, debugColor);
|
||||
setIcon(this, &actionEditFunctionVariables, Icon::Edit, analysisColor);
|
||||
setIcon(this, &actionXRefs, Icon::XRefs, analysisColor);
|
||||
setIcon(this, &actionShowAddress, Icon::View, viewColor);
|
||||
setIcon(this, &actionShowDisassembly, Icon::View, viewColor);
|
||||
setIcon(this, breakpointMenu->menuAction(), Icon::Breakpoint, debugColor);
|
||||
setIcon(this, &actionToggleBreakpoint, Icon::Breakpoint, debugColor);
|
||||
setIcon(this, &actionAdvancedBreakpoint, Icon::Edit, debugColor);
|
||||
setIcon(this, debugMenu->menuAction(), Icon::Debug, debugColor);
|
||||
setIcon(this, &actionContinueUntil, Icon::Navigation, debugColor);
|
||||
setIcon(this, &actionSetPC, Icon::Debug, debugColor);
|
||||
setIcon(this, &actionEditAnnotation, Icon::Edit, annotationColor);
|
||||
|
||||
setShortcutContextInActions(this);
|
||||
|
||||
connect(
|
||||
@@ -100,6 +134,8 @@ void DecompilerContextMenu::setDecompiledFunctionAddress(RVA functionAddr)
|
||||
void DecompilerContextMenu::setFirstOffsetInLine(RVA firstOffset)
|
||||
{
|
||||
firstOffsetInLine = firstOffset;
|
||||
actionAddComment.setEnabled(firstOffset != RVA_INVALID);
|
||||
actionDeleteComment.setEnabled(firstOffset != RVA_INVALID);
|
||||
}
|
||||
|
||||
RVA DecompilerContextMenu::getFirstOffsetInLine()
|
||||
@@ -117,6 +153,8 @@ void DecompilerContextMenu::setupBreakpointsInLineMenu()
|
||||
breakpointsInLineMenu->clear();
|
||||
for (auto curOffset : this->availableBreakpoints) {
|
||||
QAction *action = breakpointsInLineMenu->addAction(RAddressString(curOffset));
|
||||
action->setIcon(actionToggleBreakpoint.icon());
|
||||
action->setIconVisibleInMenu(true);
|
||||
connect(action, &QAction::triggered, this, [this, curOffset] {
|
||||
BreakpointsDialog::editBreakpoint(Core()->getBreakpointAt(curOffset), this);
|
||||
});
|
||||
@@ -163,14 +201,15 @@ void DecompilerContextMenu::aboutToHideSlot()
|
||||
void DecompilerContextMenu::aboutToShowSlot()
|
||||
{
|
||||
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
|
||||
actionShowAddress.setText(
|
||||
decompilerWidget && decompilerWidget->isAddressColumnVisible() ? tr("Hide address")
|
||||
: tr("Show address"));
|
||||
actionShowAddress.setChecked(decompilerWidget && decompilerWidget->isAddressColumnVisible());
|
||||
actionShowDisassembly.setChecked(
|
||||
decompilerWidget && decompilerWidget->isDisassemblyColumnVisible());
|
||||
|
||||
actionAddComment.setVisible(true);
|
||||
actionAddComment.setEnabled(firstOffsetInLine != RVA_INVALID);
|
||||
if (this->firstOffsetInLine != RVA_MAX) {
|
||||
actionShowInSubmenu.setVisible(true);
|
||||
QString comment = Core()->cmdRawAt("CC.", this->firstOffsetInLine);
|
||||
actionAddComment.setVisible(true);
|
||||
if (comment.isEmpty()) {
|
||||
actionDeleteComment.setVisible(false);
|
||||
actionAddComment.setText(tr("Add Comment"));
|
||||
@@ -180,7 +219,7 @@ void DecompilerContextMenu::aboutToShowSlot()
|
||||
}
|
||||
} else {
|
||||
actionShowInSubmenu.setVisible(false);
|
||||
actionAddComment.setVisible(false);
|
||||
actionAddComment.setText(tr("Add Comment"));
|
||||
actionDeleteComment.setVisible(false);
|
||||
}
|
||||
|
||||
@@ -310,6 +349,8 @@ void DecompilerContextMenu::setActionShowInSubmenu()
|
||||
|
||||
void DecompilerContextMenu::setActionAddComment()
|
||||
{
|
||||
actionAddComment.setObjectName(QStringLiteral("actionAddComment"));
|
||||
actionAddComment.setEnabled(firstOffsetInLine != RVA_INVALID);
|
||||
connect(
|
||||
&actionAddComment,
|
||||
&QAction::triggered,
|
||||
@@ -321,6 +362,7 @@ void DecompilerContextMenu::setActionAddComment()
|
||||
|
||||
void DecompilerContextMenu::setActionDeleteComment()
|
||||
{
|
||||
actionDeleteComment.setObjectName(QStringLiteral("actionDeleteComment"));
|
||||
connect(
|
||||
&actionDeleteComment,
|
||||
&QAction::triggered,
|
||||
@@ -338,15 +380,32 @@ void DecompilerContextMenu::setActionXRefs()
|
||||
|
||||
void DecompilerContextMenu::setActionShowAddress()
|
||||
{
|
||||
connect(&actionShowAddress, &QAction::triggered, this, [this]() {
|
||||
actionShowAddress.setObjectName(QStringLiteral("actionShowAddresses"));
|
||||
actionShowAddress.setCheckable(true);
|
||||
actionShowAddress.setToolTip(tr("Show the address associated with each decompiled line."));
|
||||
connect(&actionShowAddress, &QAction::triggered, this, [this](bool checked) {
|
||||
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
|
||||
if (decompilerWidget) {
|
||||
decompilerWidget->setAddressColumnVisible(!decompilerWidget->isAddressColumnVisible());
|
||||
decompilerWidget->setAddressColumnVisible(checked);
|
||||
}
|
||||
});
|
||||
addAction(&actionShowAddress);
|
||||
}
|
||||
|
||||
void DecompilerContextMenu::setActionShowDisassembly()
|
||||
{
|
||||
actionShowDisassembly.setObjectName(QStringLiteral("actionShowDisassembly"));
|
||||
actionShowDisassembly.setCheckable(true);
|
||||
actionShowDisassembly.setToolTip(tr("Show disassembly beside the decompiled code, like pdca."));
|
||||
connect(&actionShowDisassembly, &QAction::triggered, this, [this](bool checked) {
|
||||
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
|
||||
if (decompilerWidget) {
|
||||
decompilerWidget->setDisassemblyColumnVisible(checked);
|
||||
}
|
||||
});
|
||||
addAction(&actionShowDisassembly);
|
||||
}
|
||||
|
||||
void DecompilerContextMenu::setActionRenameThingHere()
|
||||
{
|
||||
ShortcutMgr()->bindAction("decompiler.rename", &actionRenameThingHere);
|
||||
@@ -435,12 +494,21 @@ void DecompilerContextMenu::actionCopyInstructionAddressTriggered()
|
||||
|
||||
void DecompilerContextMenu::actionEditAnnotationTriggered()
|
||||
{
|
||||
QString os = Core()->cmdRaw("anos");
|
||||
QString *s = openTextEditDialog(os, this);
|
||||
if (s != nullptr) {
|
||||
Core()->cmdRaw(QStringLiteral("ano=base64:%1").arg(QString(s->toLocal8Bit().toBase64())));
|
||||
this->mainWindow->refreshAll();
|
||||
const RVA functionAddress = decompiledFunctionAddress;
|
||||
if (functionAddress == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
TextEditDialog dialog(Core()->cmdRawAt("anos", functionAddress), this);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
const QString text = dialog.getEditedText();
|
||||
const QString command
|
||||
= text.isEmpty()
|
||||
? QStringLiteral("ano-")
|
||||
: QStringLiteral("ano=base64:%1").arg(QString::fromLatin1(text.toUtf8().toBase64()));
|
||||
Core()->cmdRawAt(command, functionAddress);
|
||||
this->mainWindow->refreshAll();
|
||||
}
|
||||
|
||||
void DecompilerContextMenu::actionCopyReferenceAddressTriggered()
|
||||
@@ -456,11 +524,20 @@ void DecompilerContextMenu::actionCopyDeepLinkTriggered()
|
||||
|
||||
void DecompilerContextMenu::actionAddCommentTriggered()
|
||||
{
|
||||
CommentsDialog::addOrEditComment(this->firstOffsetInLine, this);
|
||||
if (firstOffsetInLine == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
CommentsDialog::addOrEditComment(this->firstOffsetInLine, parentWidget());
|
||||
if (auto *widget = qobject_cast<DecompilerWidget *>(parentWidget())) {
|
||||
widget->setFocus(Qt::OtherFocusReason);
|
||||
}
|
||||
}
|
||||
|
||||
void DecompilerContextMenu::actionDeleteCommentTriggered()
|
||||
{
|
||||
if (firstOffsetInLine == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
Core()->delComment(this->firstOffsetInLine);
|
||||
}
|
||||
|
||||
@@ -676,6 +753,8 @@ void DecompilerContextMenu::updateTargetMenuActions()
|
||||
RAddressString(annotationHere->reference.offset));
|
||||
}
|
||||
auto action = new QAction(name, this);
|
||||
action->setIcon(actionShowInSubmenu.icon());
|
||||
action->setIconVisibleInMenu(true);
|
||||
showTargetMenuActions.append(action);
|
||||
action->setMenu(menu);
|
||||
insertActions(copySeparator, showTargetMenuActions);
|
||||
|
||||
@@ -105,6 +105,7 @@ private:
|
||||
|
||||
QAction actionXRefs;
|
||||
QAction actionShowAddress;
|
||||
QAction actionShowDisassembly;
|
||||
|
||||
QMenu *breakpointMenu;
|
||||
QAction actionToggleBreakpoint;
|
||||
@@ -137,6 +138,7 @@ private:
|
||||
|
||||
void setActionXRefs();
|
||||
void setActionShowAddress();
|
||||
void setActionShowDisassembly();
|
||||
|
||||
void setActionRenameThingHere();
|
||||
void setActionDeleteName();
|
||||
|
||||
@@ -52,8 +52,7 @@ DisassemblyContextMenu::DisassemblyContextMenu(QWidget *parent, MainWindow *main
|
||||
, actionAnalyzeFunction(this)
|
||||
, actionEditFunction(this)
|
||||
, actionRename(this)
|
||||
, actionAddFlag(this)
|
||||
, actionEditFlag(this)
|
||||
, actionFlag(this)
|
||||
, actionAddFlagAtRef(this)
|
||||
, actionEditFlagAtRef(this)
|
||||
, actionSetFunctionVarTypes(this)
|
||||
@@ -91,6 +90,7 @@ DisassemblyContextMenu::DisassemblyContextMenu(QWidget *parent, MainWindow *main
|
||||
, showInSubmenu(this)
|
||||
, actionToggleSelectionSync(this)
|
||||
, actionToggleBBLines(this)
|
||||
, actionToggleFcnLines(this)
|
||||
, actionToggleXRefs(this)
|
||||
, actionToggleVarSummary(this)
|
||||
{
|
||||
@@ -169,6 +169,15 @@ void DisassemblyContextMenu::registerActionShortcuts()
|
||||
ShortcutMgr()->bindAction("disasm.undefineFunction", &actionDeleteFunction);
|
||||
}
|
||||
|
||||
void DisassemblyContextMenu::registerFlagShortcut()
|
||||
{
|
||||
ShortcutMgr()->bindAction("disasm.flag", &actionFlag);
|
||||
actionFlag.setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
actionFlag.setShortcutVisibleInContextMenu(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
DisassemblyContextMenu::~DisassemblyContextMenu() {}
|
||||
|
||||
QIcon DisassemblyContextMenu::makeMenuIcon(MenuIcon icon, const QColor &color) const
|
||||
@@ -448,6 +457,16 @@ void DisassemblyContextMenu::buildRepresentationMenu()
|
||||
Core()->triggerAsmOptionsChanged();
|
||||
});
|
||||
|
||||
actionToggleFcnLines.setText(tr("Function boundaries"));
|
||||
actionToggleFcnLines.setCheckable(true);
|
||||
actionToggleFcnLines.setChecked(Config()->getConfigBool("asm.lines.fcn"));
|
||||
setActionIcon(&actionToggleFcnLines, MenuIcon::View, QColor(191, 128, 32));
|
||||
representationMenu->addAction(&actionToggleFcnLines);
|
||||
connect(&actionToggleFcnLines, &QAction::toggled, this, [](bool checked) {
|
||||
Config()->setConfig("asm.lines.fcn", checked);
|
||||
Core()->triggerAsmOptionsChanged();
|
||||
});
|
||||
|
||||
actionToggleXRefs.setText(tr("Show x-refs"));
|
||||
actionToggleXRefs.setCheckable(true);
|
||||
actionToggleXRefs.setChecked(Config()->getConfigBool("asm.xrefs"));
|
||||
@@ -633,25 +652,25 @@ void DisassemblyContextMenu::addFlagActions()
|
||||
flagMenu = addMenu(tr("Flag"));
|
||||
setMenuIcon(flagMenu, MenuIcon::Tag, flagColor);
|
||||
|
||||
initAction(&actionAddFlag, tr("Add flag here"), nullptr);
|
||||
setActionIcon(&actionAddFlag, MenuIcon::Tag, flagColor);
|
||||
connect(&actionAddFlag, &QAction::triggered, this, [this]() {
|
||||
FlagDialog dlg(offset, 1, this->mainWindow);
|
||||
initAction(&actionFlag, tr("Add / edit flag here"));
|
||||
setActionIcon(&actionFlag, MenuIcon::Tag, flagColor);
|
||||
connect(&actionFlag, &QAction::triggered, this, [this]() {
|
||||
emit flagAboutToChange();
|
||||
const bool hasSelection = selectionStart != RVA_INVALID && selectionByteCount > 0;
|
||||
FlagDialog
|
||||
dlg(hasSelection ? selectionStart : offset,
|
||||
hasSelection ? static_cast<ut64>(selectionByteCount) : 1,
|
||||
mainWindow);
|
||||
if (dlg.exec() == QDialog::Accepted && this->mainWindow) {
|
||||
this->mainWindow->refreshAll();
|
||||
}
|
||||
});
|
||||
flagMenu->addAction(&actionAddFlag);
|
||||
flagMenu->addAction(&actionFlag);
|
||||
|
||||
initAction(&actionEditFlag, tr("Edit flag here"), nullptr);
|
||||
setActionIcon(&actionEditFlag, MenuIcon::Tag, flagColor);
|
||||
connect(&actionEditFlag, &QAction::triggered, this, [this]() {
|
||||
FlagDialog dlg(offset, 1, this->mainWindow);
|
||||
if (dlg.exec() == QDialog::Accepted && this->mainWindow) {
|
||||
this->mainWindow->refreshAll();
|
||||
}
|
||||
});
|
||||
flagMenu->addAction(&actionEditFlag);
|
||||
initAction(&actionDeleteFlag, tr("Delete flag"), SLOT(on_actionDeleteFlag_triggered()));
|
||||
setActionIcon(&actionDeleteFlag, MenuIcon::Tag, QColor(117, 117, 117));
|
||||
flagMenu->addAction(&actionDeleteFlag);
|
||||
flagMenu->addSeparator();
|
||||
|
||||
initAction(&actionAddFlagAtRef, tr("Add flag at reference"), nullptr);
|
||||
setActionIcon(&actionAddFlagAtRef, MenuIcon::Tag, flagColor);
|
||||
@@ -683,11 +702,12 @@ void DisassemblyContextMenu::addFlagActions()
|
||||
void DisassemblyContextMenu::refreshFlagActions()
|
||||
{
|
||||
RFlag *rf = Core()->core()->flags;
|
||||
RFlagItem *here = rf ? r_flag_get_in(rf, offset) : nullptr;
|
||||
actionAddFlag.setText(tr("Add flag at %1").arg(RAddressString(offset)));
|
||||
actionAddFlag.setEnabled(here == nullptr);
|
||||
actionEditFlag.setText(tr("Edit flag at %1").arg(RAddressString(offset)));
|
||||
actionEditFlag.setEnabled(here != nullptr);
|
||||
const RVA address = selectionStart != RVA_INVALID ? selectionStart : offset;
|
||||
RFlagItem *here = rf ? r_flag_get_in(rf, address) : nullptr;
|
||||
actionFlag.setText(
|
||||
(here ? tr("Edit flag at %1") : tr("Add flag at %1")).arg(RAddressString(address)));
|
||||
actionDeleteFlag.setText(tr("Delete flag at %1").arg(RAddressString(address)));
|
||||
actionDeleteFlag.setEnabled(here != nullptr);
|
||||
|
||||
ut64 ref = curHighlightedWord.isEmpty() ? 0 : Core()->num(curHighlightedWord);
|
||||
hasFlagRefAddr = (ref != 0 && ref != offset);
|
||||
@@ -722,9 +742,6 @@ void DisassemblyContextMenu::addEditMenu()
|
||||
setActionIcon(&actionDeleteComment, MenuIcon::Comment, QColor(67, 160, 71));
|
||||
editMenu->addAction(&actionDeleteComment);
|
||||
|
||||
initAction(&actionDeleteFlag, tr("Delete flag"), SLOT(on_actionDeleteFlag_triggered()));
|
||||
setActionIcon(&actionDeleteFlag, MenuIcon::Tag, QColor(117, 117, 117));
|
||||
editMenu->addAction(&actionDeleteFlag);
|
||||
editMenu->addSeparator();
|
||||
|
||||
initAction(&actionEditInstruction, tr("Instruction"), SLOT(on_actionEditInstruction_triggered()));
|
||||
@@ -867,7 +884,6 @@ void DisassemblyContextMenu::buildRenameMenu(ThingUsedHere *tuh)
|
||||
return;
|
||||
}
|
||||
|
||||
actionDeleteFlag.setVisible(false);
|
||||
// TODO: use switch
|
||||
if (tuh->type == ThingUsedHere::Type::Address) {
|
||||
// Adding a flag at a referenced address is handled by the dedicated
|
||||
@@ -891,7 +907,6 @@ void DisassemblyContextMenu::buildRenameMenu(ThingUsedHere *tuh)
|
||||
doRenameInfo.name = tuh->name;
|
||||
doRenameInfo.addr = tuh->offset;
|
||||
actionRename.setText(tr("Rename flag \"%1\" (used here)").arg(doRenameInfo.name));
|
||||
actionDeleteFlag.setVisible(true);
|
||||
} else {
|
||||
qWarning() << "Unexpected renaming type";
|
||||
doRenameAction = RENAME_DO_NOTHING;
|
||||
@@ -1048,6 +1063,9 @@ void DisassemblyContextMenu::aboutToShowSlot()
|
||||
actionToggleBBLines.blockSignals(true);
|
||||
actionToggleBBLines.setChecked(Config()->getConfigBool("asm.lines.bb"));
|
||||
actionToggleBBLines.blockSignals(false);
|
||||
actionToggleFcnLines.blockSignals(true);
|
||||
actionToggleFcnLines.setChecked(Config()->getConfigBool("asm.lines.fcn"));
|
||||
actionToggleFcnLines.blockSignals(false);
|
||||
actionToggleXRefs.blockSignals(true);
|
||||
actionToggleXRefs.setChecked(Config()->getConfigBool("asm.xrefs"));
|
||||
actionToggleXRefs.blockSignals(false);
|
||||
@@ -1389,12 +1407,21 @@ void DisassemblyContextMenu::on_actionAnalyzeFunction_triggered()
|
||||
|
||||
void DisassemblyContextMenu::on_actionEditAnnotation_triggered()
|
||||
{
|
||||
QString os = Core()->cmdRaw("anos");
|
||||
QString *s = openTextEditDialog(os, this);
|
||||
if (s != nullptr) {
|
||||
Core()->cmdRaw(QStringLiteral("ano=base64:%1").arg(QString(s->toLocal8Bit().toBase64())));
|
||||
this->mainWindow->refreshAll();
|
||||
const RVA functionAddress = Core()->getFunctionStart(offset);
|
||||
if (functionAddress == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
TextEditDialog dialog(Core()->cmdRawAt("anos", functionAddress), this);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
const QString text = dialog.getEditedText();
|
||||
const QString command
|
||||
= text.isEmpty()
|
||||
? QStringLiteral("ano-")
|
||||
: QStringLiteral("ano=base64:%1").arg(QString::fromLatin1(text.toUtf8().toBase64()));
|
||||
Core()->cmdRawAt(command, functionAddress);
|
||||
this->mainWindow->refreshAll();
|
||||
}
|
||||
|
||||
void DisassemblyContextMenu::on_actionRename_triggered()
|
||||
@@ -1611,7 +1638,12 @@ void DisassemblyContextMenu::on_actionDeleteComment_triggered()
|
||||
|
||||
void DisassemblyContextMenu::on_actionDeleteFlag_triggered()
|
||||
{
|
||||
Core()->delFlag(offset);
|
||||
emit flagAboutToChange();
|
||||
const RVA address = selectionStart != RVA_INVALID ? selectionStart : offset;
|
||||
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, address);
|
||||
if (flag) {
|
||||
Core()->delFlag(QString::fromUtf8(flag->name));
|
||||
}
|
||||
}
|
||||
|
||||
void DisassemblyContextMenu::on_actionDeleteFunction_triggered()
|
||||
|
||||
@@ -19,7 +19,11 @@ public:
|
||||
DisassemblyContextMenu(QWidget *parent, MainWindow *mainWindow);
|
||||
~DisassemblyContextMenu();
|
||||
|
||||
// The graph view shares this menu and uses F for branch navigation.
|
||||
void registerFlagShortcut();
|
||||
|
||||
signals:
|
||||
void flagAboutToChange();
|
||||
void copy();
|
||||
/**
|
||||
* @brief Signal emitted to copy raw bytes of selected instructions
|
||||
@@ -177,8 +181,7 @@ private:
|
||||
QAction actionAnalyzeFunction;
|
||||
QAction actionEditFunction;
|
||||
QAction actionRename;
|
||||
QAction actionAddFlag;
|
||||
QAction actionEditFlag;
|
||||
QAction actionFlag;
|
||||
QAction actionAddFlagAtRef;
|
||||
QAction actionEditFlagAtRef;
|
||||
RVA flagRefAddr = 0;
|
||||
@@ -241,6 +244,7 @@ private:
|
||||
QAction showInSubmenu;
|
||||
QAction actionToggleSelectionSync;
|
||||
QAction actionToggleBBLines;
|
||||
QAction actionToggleFcnLines;
|
||||
QAction actionToggleXRefs;
|
||||
QAction actionToggleVarSummary;
|
||||
QList<QAction *> showTargetMenuActions;
|
||||
|
||||
@@ -42,6 +42,52 @@
|
||||
<name>radare</name>
|
||||
</developer>
|
||||
<releases>
|
||||
<release version="6.2.6" date="2026-11-06">
|
||||
<description>
|
||||
<p>Fix disasm lines glitch</p>
|
||||
<p>Colorize basic blocks in minigraph</p>
|
||||
<p>Toggle decompiler with tab and graph with space</p>
|
||||
<p>Decai decompiler options</p>
|
||||
<p>Global variable edition and navigation</p>
|
||||
<p>Filesystem browser</p>
|
||||
<p>Several bug fixes and optimizations</p>
|
||||
<p>Zoom panels with meta+enter</p>
|
||||
<p>Switch tabs with cmd+number</p>
|
||||
<p>Cycle dock frames with cmd+JK</p>
|
||||
<p>Hover indicators in disasm and decompile</p>
|
||||
<p>Per-view seek history fixes</p>
|
||||
<p>Folding braced blocks in decompiler</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.2.4" date="2026-10-05">
|
||||
<description>
|
||||
<p>Collapse sparse gaps in the hexdump</p>
|
||||
<p>TryCatch analysis via disasm and graph</p>
|
||||
<p>View symbols, class, imports as tree</p>
|
||||
<p>Compact large symbol names grouped by prefix</p>
|
||||
<p>Optimize listings filtering</p>
|
||||
<p>New advanced data analysis widget</p>
|
||||
<p>Hand cursor over clickable items in disasm</p>
|
||||
<p>Enable the bounded scrollbar by default</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.2.2" date="2026-09-06">
|
||||
<description>
|
||||
<p>Scrollbars in disasm and hexdump</p>
|
||||
<p>Configurable navigation bar block resolution</p>
|
||||
<p>Better column order for a variety of listings</p>
|
||||
<p>Zoomable branch lines with better highlights</p>
|
||||
<p>Fix behaviour when closing load options dialog</p>
|
||||
<p>Fix map and undo/redo write cache actions</p>
|
||||
<p>Add tab for listing demangler plugins</p>
|
||||
<p>Allow to edit system color themes</p>
|
||||
<p>Toggle platform r2pm package listings and better error messages</p>
|
||||
<p>Add function bound lines</p>
|
||||
<p>Adaptative branch lines width</p>
|
||||
<p>Persist load option values across runs</p>
|
||||
<p>Respect map address in bininfo details</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.2.0" date="2026-07-10">
|
||||
<description>
|
||||
<p>Support r2-6.2.0 api changes</p>
|
||||
|
||||
@@ -95,9 +95,8 @@ QVector<QDir> PluginManager::getPluginDirectories() const
|
||||
#else
|
||||
QChar listSeparator = QDir::listSeparator();
|
||||
#endif
|
||||
QStringList extraPluginDirs
|
||||
= QString::fromLocal8Bit(IAITO_EXTRA_PLUGIN_DIRS)
|
||||
.split(listSeparator, IAITO_QT_SKIP_EMPTY_PARTS);
|
||||
QStringList extraPluginDirs = QString::fromLocal8Bit(IAITO_EXTRA_PLUGIN_DIRS)
|
||||
.split(listSeparator, IAITO_QT_SKIP_EMPTY_PARTS);
|
||||
extraPluginDirs.append(
|
||||
QString::fromLocal8Bit(qgetenv("IAITO_EXTRA_PLUGIN_DIRS"))
|
||||
.split(listSeparator, IAITO_QT_SKIP_EMPTY_PARTS));
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
<file>img/icons/delete.svg</file>
|
||||
<file>img/icons/previous.svg</file>
|
||||
<file>img/icons/list.svg</file>
|
||||
<file>img/icons/name-tree.svg</file>
|
||||
<file>img/icons/name-list.svg</file>
|
||||
<file>img/icons/fork.svg</file>
|
||||
<file>img/icons/plugins.svg</file>
|
||||
<file>img/icons/light/plugins.svg</file>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
[meta]
|
||||
name=Cobalt
|
||||
skin=modern
|
||||
chrome=accent
|
||||
chrome=accent-tabs
|
||||
dark=true
|
||||
|
||||
[palette]
|
||||
background=#16213a
|
||||
panel=#0f1830
|
||||
surface=#1f2d4d
|
||||
border=#2c3f66
|
||||
text=#e6ebf5
|
||||
mutedText=#8a97b5
|
||||
accent=#2f5fd0
|
||||
background=#15213b
|
||||
panel=#0d172d
|
||||
surface=#1d2c4a
|
||||
border=#344c78
|
||||
text=#e8edf7
|
||||
mutedText=#98a7c3
|
||||
accent=#3478e5
|
||||
accentText=#ffffff
|
||||
|
||||
[metrics]
|
||||
radius=4
|
||||
radius=5
|
||||
lineWidth=1
|
||||
focusWidth=2
|
||||
spacing=6
|
||||
|
||||
@@ -25,8 +25,8 @@ spacing=6
|
||||
sliderGroove=4
|
||||
|
||||
[palette]
|
||||
accent=#039be5
|
||||
accentText=#ffffff
|
||||
accent=#c6dff2
|
||||
accentText=#00162e
|
||||
background=#fafcfe
|
||||
border=#bdbdbd
|
||||
mutedText=#7d7d7d
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
#include "AddressScrollBar.h"
|
||||
|
||||
#include "common/TempConfig.h"
|
||||
#include "core/Iaito.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEasingCurve>
|
||||
#include <QJsonObject>
|
||||
#include <QVariantAnimation>
|
||||
#include <QWheelEvent>
|
||||
|
||||
namespace {
|
||||
// Half of the travel of the spring handle, zero is the middle of the trough
|
||||
constexpr int SpringRange = 1000;
|
||||
constexpr int SpringPageStep = 400;
|
||||
// Travel kept on both sides of the rest position so the view can always scroll
|
||||
constexpr int SpringMargin = 100;
|
||||
// Largest span the bounded mode maps one byte per slider unit
|
||||
constexpr RVA MaxSliderSpan = RVA(1) << 30;
|
||||
constexpr int SpringTickMs = 16;
|
||||
constexpr int BounceMs = 700;
|
||||
// Screens per second when the handle is pulled all the way, once accelerated
|
||||
constexpr qreal MaxPagesPerSecond = 6.0;
|
||||
// Seconds the handle must be held before reaching full speed
|
||||
constexpr qreal AccelerationSeconds = 3.0;
|
||||
} // namespace
|
||||
|
||||
AddressScrollBar::AddressScrollBar(QWidget *parent)
|
||||
: QScrollBar(Qt::Vertical, parent)
|
||||
, mode(Config()->getMemoryScrollBarMode())
|
||||
, bounce(new QVariantAnimation(this))
|
||||
{
|
||||
springTimer.setInterval(SpringTickMs);
|
||||
springTimer.setTimerType(Qt::PreciseTimer);
|
||||
bounce->setDuration(BounceMs);
|
||||
bounce->setEasingCurve(QEasingCurve::OutElastic);
|
||||
connect(bounce, &QVariantAnimation::valueChanged, this, [this](const QVariant &v) {
|
||||
syncing = true;
|
||||
setValue(v.toInt());
|
||||
syncing = false;
|
||||
});
|
||||
connect(&springTimer, &QTimer::timeout, this, &AddressScrollBar::springTick);
|
||||
connect(this, &QAbstractSlider::sliderPressed, this, &AddressScrollBar::startSpring);
|
||||
connect(this, &QAbstractSlider::sliderReleased, this, &AddressScrollBar::releaseSpring);
|
||||
connect(this, &QAbstractSlider::actionTriggered, this, &AddressScrollBar::onActionTriggered);
|
||||
connect(this, &QAbstractSlider::valueChanged, this, &AddressScrollBar::onValueChanged);
|
||||
connect(
|
||||
Config(),
|
||||
&Configuration::memoryScrollBarModeChanged,
|
||||
this,
|
||||
[this](Configuration::MemoryScrollBarMode newMode) {
|
||||
mode = newMode;
|
||||
applyMode();
|
||||
});
|
||||
connect(Core(), &IaitoCore::refreshAll, this, &AddressScrollBar::updateRange);
|
||||
applyMode();
|
||||
}
|
||||
|
||||
void AddressScrollBar::setViewport(int lines, RVA bytes)
|
||||
{
|
||||
pageLines = qMax(1, lines);
|
||||
pageBytes = qMax<RVA>(1, bytes);
|
||||
if (!isSpring() && !isHiddenMode()) {
|
||||
applyRange();
|
||||
}
|
||||
}
|
||||
|
||||
void AddressScrollBar::setAddress(RVA newAddress)
|
||||
{
|
||||
address = newAddress;
|
||||
if (isSpring() || isHiddenMode()) {
|
||||
return;
|
||||
}
|
||||
int v = 0;
|
||||
if (address >= rangeEnd) {
|
||||
v = maximum();
|
||||
} else if (address > rangeStart) {
|
||||
v = int((address - rangeStart) / unit);
|
||||
}
|
||||
syncing = true;
|
||||
setValue(v);
|
||||
syncing = false;
|
||||
}
|
||||
|
||||
void AddressScrollBar::setSeekAddress(RVA newAddress)
|
||||
{
|
||||
seekAddress = newAddress;
|
||||
updateRest();
|
||||
}
|
||||
|
||||
void AddressScrollBar::updateRange()
|
||||
{
|
||||
if (isHiddenMode()) {
|
||||
return;
|
||||
}
|
||||
RVA from = 0;
|
||||
RVA to = 0;
|
||||
// Same boundaries the navigation bar shows, the whole file when unmapped
|
||||
for (const char *where : {"bin.sections", "io.maps"}) {
|
||||
TempConfig tempConfig;
|
||||
tempConfig.set(QStringLiteral("search.in"), QString::fromLatin1(where));
|
||||
const QJsonObject stats = Core()->cmdj(QStringLiteral("p-j 1")).object();
|
||||
from = stats[QStringLiteral("from")].toVariant().toULongLong();
|
||||
to = stats[QStringLiteral("to")].toVariant().toULongLong();
|
||||
if (to > from) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (to <= from) {
|
||||
from = 0;
|
||||
to = RVA_MAX;
|
||||
}
|
||||
rangeStart = from;
|
||||
rangeEnd = to;
|
||||
if (isSpring()) {
|
||||
updateRest();
|
||||
} else {
|
||||
applyRange();
|
||||
}
|
||||
}
|
||||
|
||||
// Rests the spring handle where the seek lies within the range
|
||||
void AddressScrollBar::updateRest()
|
||||
{
|
||||
if (!isSpring()) {
|
||||
return;
|
||||
}
|
||||
const RVA span = rangeEnd - rangeStart;
|
||||
qreal position = 0.5;
|
||||
if (span > 0 && seekAddress > rangeStart) {
|
||||
position = seekAddress >= rangeEnd ? 1.0 : qreal(seekAddress - rangeStart) / span;
|
||||
}
|
||||
const int travel = SpringRange - SpringMargin;
|
||||
restValue = qRound(-travel + 2 * travel * position);
|
||||
if (!isSliderDown() && bounce->state() != QAbstractAnimation::Running) {
|
||||
syncing = true;
|
||||
setValue(restValue);
|
||||
syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
void AddressScrollBar::applyMode()
|
||||
{
|
||||
springTimer.stop();
|
||||
bounce->stop();
|
||||
springRemainder = 0;
|
||||
setVisible(!isHiddenMode());
|
||||
syncing = true;
|
||||
if (isSpring()) {
|
||||
setRange(-SpringRange, SpringRange);
|
||||
setPageStep(SpringPageStep);
|
||||
setSingleStep(1);
|
||||
setToolTip(tr("Drag away from the seek to scroll, further and longer is faster"));
|
||||
} else {
|
||||
setToolTip(QString());
|
||||
}
|
||||
syncing = false;
|
||||
updateRange();
|
||||
}
|
||||
|
||||
void AddressScrollBar::applyRange()
|
||||
{
|
||||
const RVA span = rangeEnd - rangeStart;
|
||||
// Round up without overflowing when the range spans the full address space.
|
||||
unit = span > MaxSliderSpan ? (span - 1) / MaxSliderSpan + 1 : 1;
|
||||
syncing = true;
|
||||
setRange(0, int(span / unit));
|
||||
setPageStep(qMax<int>(1, int(pageBytes / unit)));
|
||||
setSingleStep(qMax<int>(1, int(pageBytes / pageLines / unit)));
|
||||
syncing = false;
|
||||
setAddress(address);
|
||||
}
|
||||
|
||||
void AddressScrollBar::startSpring()
|
||||
{
|
||||
if (!isSpring()) {
|
||||
return;
|
||||
}
|
||||
bounce->stop();
|
||||
springRemainder = 0;
|
||||
holdTimer.start();
|
||||
tickTimer.start();
|
||||
springTimer.start();
|
||||
}
|
||||
|
||||
void AddressScrollBar::releaseSpring()
|
||||
{
|
||||
if (!isSpring()) {
|
||||
return;
|
||||
}
|
||||
springTimer.stop();
|
||||
bounce->stop();
|
||||
bounce->setStartValue(value());
|
||||
bounce->setEndValue(restValue);
|
||||
bounce->start();
|
||||
}
|
||||
|
||||
void AddressScrollBar::springTick()
|
||||
{
|
||||
const qreal dt = tickTimer.restart() / 1000.0;
|
||||
const int offset = value() - restValue;
|
||||
// Normalized by the travel left on that side, so the edge is always full speed
|
||||
const int travel = qMax(1, offset > 0 ? SpringRange - restValue : SpringRange + restValue);
|
||||
const qreal pull = qBound(-1.0, offset / qreal(travel), 1.0);
|
||||
if (qFuzzyIsNull(pull)) {
|
||||
springRemainder = 0;
|
||||
return;
|
||||
}
|
||||
const qreal hold = holdTimer.elapsed() / 1000.0;
|
||||
const qreal ramp = qMin(1.0, 0.1 + hold / AccelerationSeconds);
|
||||
// Quadratic so that small pulls stay slow enough to read the listing
|
||||
const qreal pagesPerSecond = pull * qAbs(pull) * MaxPagesPerSecond * ramp;
|
||||
springRemainder += pagesPerSecond * pageLines * dt;
|
||||
const int lines = int(springRemainder);
|
||||
if (lines != 0) {
|
||||
springRemainder -= lines;
|
||||
emit scrollRequested(lines);
|
||||
}
|
||||
}
|
||||
|
||||
void AddressScrollBar::onActionTriggered(int action)
|
||||
{
|
||||
if (!isSpring()) {
|
||||
return;
|
||||
}
|
||||
int lines = 0;
|
||||
switch (action) {
|
||||
case SliderSingleStepAdd:
|
||||
lines = 1;
|
||||
break;
|
||||
case SliderSingleStepSub:
|
||||
lines = -1;
|
||||
break;
|
||||
case SliderPageStepAdd:
|
||||
lines = pageLines;
|
||||
break;
|
||||
case SliderPageStepSub:
|
||||
lines = -pageLines;
|
||||
break;
|
||||
case SliderMove:
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Steps scroll the view but never move the handle away from its rest
|
||||
setSliderPosition(restValue);
|
||||
if (lines != 0) {
|
||||
emit scrollRequested(lines);
|
||||
}
|
||||
}
|
||||
|
||||
void AddressScrollBar::onValueChanged(int value)
|
||||
{
|
||||
if (syncing || isSpring()) {
|
||||
return;
|
||||
}
|
||||
const RVA target = rangeStart + RVA(value) * unit;
|
||||
if (target != address) {
|
||||
address = target;
|
||||
emit addressRequested(target);
|
||||
}
|
||||
}
|
||||
|
||||
void AddressScrollBar::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
if (!isSpring()) {
|
||||
QScrollBar::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
const int lines = -(event->angleDelta().y() / 120) * QApplication::wheelScrollLines();
|
||||
if (lines != 0) {
|
||||
emit scrollRequested(qBound(-pageLines, lines, pageLines));
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef ADDRESSSCROLLBAR_H
|
||||
#define ADDRESSSCROLLBAR_H
|
||||
|
||||
#include "common/Configuration.h"
|
||||
#include "core/IaitoCommon.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
|
||||
class QVariantAnimation;
|
||||
|
||||
/**
|
||||
* @brief Vertical scrollbar for views that browse the whole 64 bit address space.
|
||||
*
|
||||
* Spring mode: the handle rests where the current seek lies within the
|
||||
* navigation bar range. Dragging it scrolls with a speed that grows with the
|
||||
* displacement and with the time it is held, and the handle bounces back to
|
||||
* the rest position on release.
|
||||
* Bounded mode: a regular scrollbar mapped over the navigation bar range.
|
||||
* Hidden mode: no scrollbar at all.
|
||||
* The mode follows Configuration::getMemoryScrollBarMode().
|
||||
*/
|
||||
class AddressScrollBar : public QScrollBar
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AddressScrollBar(QWidget *parent = nullptr);
|
||||
|
||||
/// Lines shown per screen and the bytes they cover, used for the steps
|
||||
void setViewport(int lines, RVA bytes);
|
||||
/// Sync the handle with the top address of the view without emitting anything
|
||||
void setAddress(RVA address);
|
||||
/// Current seek of the view, the rest position of the spring handle
|
||||
void setSeekAddress(RVA address);
|
||||
/// Re-fetch the address range of the bounded mode from the core
|
||||
void updateRange();
|
||||
bool isHiddenMode() const { return mode == Configuration::MemoryScrollBarMode::Hidden; }
|
||||
|
||||
signals:
|
||||
/// Spring mode: scroll the view by `lines` (positive scrolls down)
|
||||
void scrollRequested(int lines);
|
||||
/// Bounded mode: show `address` at the top of the view
|
||||
void addressRequested(RVA address);
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
|
||||
private:
|
||||
Configuration::MemoryScrollBarMode mode;
|
||||
int pageLines = 1;
|
||||
RVA pageBytes = 1;
|
||||
RVA address = 0;
|
||||
RVA seekAddress = 0;
|
||||
int restValue = 0;
|
||||
RVA rangeStart = 0;
|
||||
RVA rangeEnd = 0;
|
||||
/// Bytes per slider unit in bounded mode, so any span fits in an int
|
||||
RVA unit = 1;
|
||||
bool syncing = false;
|
||||
QTimer springTimer;
|
||||
QElapsedTimer holdTimer;
|
||||
QElapsedTimer tickTimer;
|
||||
qreal springRemainder = 0;
|
||||
QVariantAnimation *bounce;
|
||||
|
||||
bool isSpring() const { return mode == Configuration::MemoryScrollBarMode::Spring; }
|
||||
void applyMode();
|
||||
void applyRange();
|
||||
void updateRest();
|
||||
void springTick();
|
||||
void startSpring();
|
||||
void releaseSpring();
|
||||
void onActionTriggered(int action);
|
||||
void onValueChanged(int value);
|
||||
};
|
||||
|
||||
#endif // ADDRESSSCROLLBAR_H
|
||||
@@ -4,7 +4,11 @@
|
||||
#include <QAction>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QEvent>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QStyle>
|
||||
#include <QToolBar>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
AddressableDockWidget::AddressableDockWidget(MainWindow *parent)
|
||||
: IaitoDockWidget(parent)
|
||||
@@ -19,9 +23,99 @@ AddressableDockWidget::AddressableDockWidget(MainWindow *parent)
|
||||
dockMenu = new QMenu(this);
|
||||
dockMenu->addAction(&syncAction);
|
||||
|
||||
connect(seekable, &IaitoSeekable::syncChanged, this, [this]() {
|
||||
if (!seekable->isSynchronized()) {
|
||||
setupSeekHistoryBar();
|
||||
}
|
||||
updateSeekHistoryBar();
|
||||
});
|
||||
connect(
|
||||
seekable,
|
||||
&IaitoSeekable::historyChanged,
|
||||
this,
|
||||
&AddressableDockWidget::updateSeekHistoryBar);
|
||||
connect(
|
||||
seekable,
|
||||
&IaitoSeekable::seekableSeekChanged,
|
||||
this,
|
||||
&AddressableDockWidget::updateSeekHistoryBar);
|
||||
connect(this, &QDockWidget::visibilityChanged, this, [this](bool visible) {
|
||||
if (visible) {
|
||||
setupSeekHistoryBar();
|
||||
}
|
||||
});
|
||||
|
||||
setContextMenuPolicy(Qt::ContextMenuPolicy::DefaultContextMenu);
|
||||
}
|
||||
|
||||
void AddressableDockWidget::setupSeekHistoryBar(bool alwaysVisible)
|
||||
{
|
||||
historyBarAlwaysVisible |= alwaysVisible;
|
||||
if (seekHistoryBar || !widget()) {
|
||||
updateSeekHistoryBar();
|
||||
return;
|
||||
}
|
||||
QWidget *contents = widget();
|
||||
seekHistoryBar = new QToolBar(contents);
|
||||
seekHistoryBar->setObjectName(QStringLiteral("seekHistoryBar"));
|
||||
seekHistoryBar->setIconSize(QSize(16, 16));
|
||||
seekHistoryBar->setMovable(false);
|
||||
seekHistoryBar->setFloatable(false);
|
||||
seekHistoryBar->setFocusPolicy(Qt::NoFocus);
|
||||
seekBackAction
|
||||
= seekHistoryBar->addAction(style()->standardIcon(QStyle::SP_ArrowBack), tr("Back"));
|
||||
seekBackAction->setObjectName(QStringLiteral("seekBackAction"));
|
||||
seekForwardAction
|
||||
= seekHistoryBar->addAction(style()->standardIcon(QStyle::SP_ArrowForward), tr("Forward"));
|
||||
seekForwardAction->setObjectName(QStringLiteral("seekForwardAction"));
|
||||
connect(seekBackAction, &QAction::triggered, seekable, &IaitoSeekable::seekPrev);
|
||||
connect(seekForwardAction, &QAction::triggered, seekable, &IaitoSeekable::seekNext);
|
||||
for (QAction *action : {seekBackAction, seekForwardAction}) {
|
||||
seekHistoryBar->widgetForAction(action)->setFocusPolicy(Qt::NoFocus);
|
||||
}
|
||||
seekAddressLabel = new QLabel(seekHistoryBar);
|
||||
seekAddressLabel->setMargin(3);
|
||||
seekHistoryBar->addWidget(seekAddressLabel);
|
||||
|
||||
if (auto *layout = qobject_cast<QVBoxLayout *>(contents->layout())) {
|
||||
layout->insertWidget(0, seekHistoryBar);
|
||||
} else {
|
||||
// Graph-only docks have no content layout in which to insert the bar.
|
||||
auto *container = new QWidget(this);
|
||||
auto *containerLayout = new QVBoxLayout(container);
|
||||
containerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
containerLayout->setSpacing(0);
|
||||
containerLayout->addWidget(seekHistoryBar);
|
||||
containerLayout->addWidget(contents);
|
||||
setWidget(container);
|
||||
contents->show();
|
||||
}
|
||||
updateSeekHistoryBar();
|
||||
}
|
||||
|
||||
void AddressableDockWidget::setSeekHistoryBarEnabled(bool enabled)
|
||||
{
|
||||
historyBarEnabled = enabled;
|
||||
updateSeekHistoryBar();
|
||||
}
|
||||
|
||||
void AddressableDockWidget::updateSeekHistoryBar()
|
||||
{
|
||||
if (!seekHistoryBar) {
|
||||
return;
|
||||
}
|
||||
const bool synced = seekable->isSynchronized();
|
||||
seekHistoryBar->setVisible(historyBarEnabled && (historyBarAlwaysVisible || !synced));
|
||||
seekBackAction->setEnabled(seekable->canSeekPrev());
|
||||
seekForwardAction->setEnabled(seekable->canSeekNext());
|
||||
seekBackAction->setToolTip(synced ? tr("Back in shared seek history") : tr("Back in this view"));
|
||||
seekForwardAction->setToolTip(
|
||||
synced ? tr("Forward in shared seek history") : tr("Forward in this view"));
|
||||
seekAddressLabel->setText(RAddressString(seekable->getOffset()));
|
||||
seekAddressLabel->setToolTip(
|
||||
synced ? tr("Shared offset") : tr("Independent offset for this view"));
|
||||
}
|
||||
|
||||
QVariantMap AddressableDockWidget::serializeViewProprties()
|
||||
{
|
||||
auto result = IaitoDockWidget::serializeViewProprties();
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <QAction>
|
||||
|
||||
class IaitoSeekable;
|
||||
class QLabel;
|
||||
class QToolBar;
|
||||
|
||||
class AddressableDockWidget : public IaitoDockWidget
|
||||
{
|
||||
@@ -28,8 +30,21 @@ protected:
|
||||
QAction syncAction;
|
||||
QMenu *dockMenu = nullptr;
|
||||
|
||||
void setupSeekHistoryBar(bool alwaysVisible = false);
|
||||
void setSeekHistoryBarEnabled(bool enabled);
|
||||
|
||||
virtual QString getWindowTitle() const = 0;
|
||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||
|
||||
private:
|
||||
QToolBar *seekHistoryBar = nullptr;
|
||||
QLabel *seekAddressLabel = nullptr;
|
||||
QAction *seekBackAction = nullptr;
|
||||
QAction *seekForwardAction = nullptr;
|
||||
bool historyBarAlwaysVisible = false;
|
||||
bool historyBarEnabled = true;
|
||||
|
||||
void updateSeekHistoryBar();
|
||||
};
|
||||
|
||||
#endif // ADDRESSABLE_DOCK_WIDGET_H
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemView>
|
||||
#include <QMenu>
|
||||
#include <QSet>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "IaitoDockWidget.h"
|
||||
@@ -45,16 +46,42 @@ public:
|
||||
void setAddressModel(AddressableItemModelI *model)
|
||||
{
|
||||
this->addressableModel = model;
|
||||
setModel(model ? model->asItemModel() : nullptr);
|
||||
}
|
||||
|
||||
BaseListWidget::setModel(this->addressableModel->asItemModel());
|
||||
void setModel(QAbstractItemModel *model) override
|
||||
{
|
||||
BaseListWidget::setModel(model);
|
||||
if (this->selectionModel()) {
|
||||
this->connect(
|
||||
this->selectionModel(),
|
||||
&QItemSelectionModel::currentChanged,
|
||||
this,
|
||||
&AddressableItemList<BaseListWidget>::onSelectedItemChanged);
|
||||
}
|
||||
}
|
||||
|
||||
this->connect(
|
||||
this->selectionModel(),
|
||||
&QItemSelectionModel::currentChanged,
|
||||
this,
|
||||
&AddressableItemList<BaseListWidget>::onSelectedItemChanged);
|
||||
auto cast_model = dynamic_cast<QAbstractItemModel *>(model);
|
||||
IaitoTreeView::setModel(cast_model);
|
||||
RVA itemAddress(const QModelIndex &index) const
|
||||
{
|
||||
auto provider = addressProvider();
|
||||
auto source = mapToProviderIndex(provider, index);
|
||||
return source.isValid() ? provider->address(source) : RVA_INVALID;
|
||||
}
|
||||
|
||||
QList<RVA> selectedAddresses() const
|
||||
{
|
||||
QList<RVA> result;
|
||||
QSet<RVA> seen;
|
||||
if (auto selection = this->selectionModel()) {
|
||||
for (const auto &index : selection->selectedIndexes()) {
|
||||
const RVA address = itemAddress(index);
|
||||
if (address != RVA_INVALID && !seen.contains(address)) {
|
||||
seen.insert(address);
|
||||
result.append(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void setMainWindow(MainWindow *mainWindow)
|
||||
@@ -76,21 +103,17 @@ public:
|
||||
protected:
|
||||
virtual void showItemContextMenu(const QPoint &pt)
|
||||
{
|
||||
auto provider = addressProvider();
|
||||
if (!provider) {
|
||||
if (!itemContextMenu) {
|
||||
return;
|
||||
}
|
||||
auto index = this->currentIndex();
|
||||
if (index.isValid() && itemContextMenu) {
|
||||
QModelIndex pIndex = mapToProviderIndex(provider, index);
|
||||
if (!pIndex.isValid()) {
|
||||
return;
|
||||
}
|
||||
auto offset = provider->address(pIndex);
|
||||
auto name = provider->name(pIndex);
|
||||
itemContextMenu->setTarget(offset, name);
|
||||
itemContextMenu->exec(this->mapToGlobal(pt));
|
||||
const auto index = this->indexAt(pt);
|
||||
if (this->selectionModel()->isSelected(index)) {
|
||||
this->selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
|
||||
} else {
|
||||
this->setCurrentIndex(index);
|
||||
}
|
||||
updateMenuFromItem(index);
|
||||
itemContextMenu->exec(this->viewport()->mapToGlobal(pt));
|
||||
}
|
||||
|
||||
virtual void onItemActivated(const QModelIndex &index)
|
||||
@@ -107,18 +130,21 @@ protected:
|
||||
return;
|
||||
}
|
||||
auto offset = provider->address(pIndex);
|
||||
Core()->seekAndShow(offset);
|
||||
if (offset != RVA_INVALID) {
|
||||
Core()->seekAndShow(offset);
|
||||
}
|
||||
}
|
||||
virtual void onSelectedItemChanged(const QModelIndex &index) { updateMenuFromItem(index); }
|
||||
void updateMenuFromItem(const QModelIndex &index)
|
||||
{
|
||||
auto provider = addressProvider();
|
||||
if (!provider) {
|
||||
if (!provider || !itemContextMenu) {
|
||||
return;
|
||||
}
|
||||
if (index.isValid()) {
|
||||
QModelIndex pIndex = mapToProviderIndex(provider, index);
|
||||
if (!pIndex.isValid()) {
|
||||
if (!pIndex.isValid() || provider->address(pIndex) == RVA_INVALID) {
|
||||
itemContextMenu->clearTarget();
|
||||
return;
|
||||
}
|
||||
auto offset = provider->address(pIndex);
|
||||
@@ -132,42 +158,21 @@ protected:
|
||||
// Handle 'j' and 'k' keys to navigate and seek without pressing Enter
|
||||
void keyPressEvent(QKeyEvent *event) override
|
||||
{
|
||||
if (event->modifiers() == Qt::NoModifier) {
|
||||
if (event->modifiers() == Qt::NoModifier
|
||||
&& (event->key() == Qt::Key_J || event->key() == Qt::Key_K)) {
|
||||
auto selModel = this->selectionModel();
|
||||
const QModelIndex curr = selModel->currentIndex();
|
||||
if (curr.isValid()) {
|
||||
if (event->key() == Qt::Key_J) {
|
||||
// Move down
|
||||
const QModelIndex parent = curr.parent();
|
||||
const int total = this->model()->rowCount(parent);
|
||||
const int newRow = qMin(curr.row() + 1, total - 1);
|
||||
if (newRow != curr.row()) {
|
||||
const QModelIndex newIndex
|
||||
= this->model()->index(newRow, curr.column(), parent);
|
||||
selModel->setCurrentIndex(
|
||||
newIndex,
|
||||
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
|
||||
this->scrollTo(newIndex);
|
||||
onItemActivated(newIndex);
|
||||
this->setFocus();
|
||||
}
|
||||
return;
|
||||
} else if (event->key() == Qt::Key_K) {
|
||||
// Move up
|
||||
if (curr.row() > 0) {
|
||||
const QModelIndex parent = curr.parent();
|
||||
const int newRow = curr.row() - 1;
|
||||
const QModelIndex newIndex
|
||||
= this->model()->index(newRow, curr.column(), parent);
|
||||
selModel->setCurrentIndex(
|
||||
newIndex,
|
||||
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
|
||||
this->scrollTo(newIndex);
|
||||
onItemActivated(newIndex);
|
||||
this->setFocus();
|
||||
}
|
||||
return;
|
||||
auto tree = qobject_cast<QTreeView *>(this);
|
||||
if (selModel && tree && this->currentIndex().isValid()) {
|
||||
const auto next = event->key() == Qt::Key_J
|
||||
? tree->indexBelow(this->currentIndex())
|
||||
: tree->indexAbove(this->currentIndex());
|
||||
if (next.isValid()) {
|
||||
selModel->setCurrentIndex(
|
||||
next, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
|
||||
this->scrollTo(next);
|
||||
onItemActivated(next);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
BaseListWidget::keyPressEvent(event);
|
||||
@@ -228,4 +233,6 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
using AddressableItemView = AddressableItemList<>;
|
||||
|
||||
#endif // ADDRESSABLE_ITEM_LIST_H
|
||||
@@ -28,7 +28,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="breakpointTreeView">
|
||||
<widget class="AddressableItemView" name="breakpointTreeView">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">IaitoTreeView::item
|
||||
{
|
||||
@@ -80,7 +80,7 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AddressableItemList<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
<container>1</container>
|
||||
|
||||
+145
-52
@@ -1,5 +1,8 @@
|
||||
#include "ClassesWidget.h"
|
||||
#include "NameListView.h"
|
||||
#include "QuickFilterView.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "common/NameListModel.h"
|
||||
#include "common/SvgIconEngine.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "dialogs/EditMethodDialog.h"
|
||||
@@ -9,6 +12,29 @@
|
||||
#include <QList>
|
||||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
#include <QShortcut>
|
||||
|
||||
RVA ClassesModel::address(const QModelIndex &index) const
|
||||
{
|
||||
const auto offset = index.data(OffsetRole);
|
||||
return offset.isValid() ? offset.value<RVA>() : RVA_INVALID;
|
||||
}
|
||||
|
||||
QString ClassesModel::name(const QModelIndex &index) const
|
||||
{
|
||||
const QString member = index.sibling(index.row(), NAME).data().toString();
|
||||
if (!index.parent().isValid() || index.data(TypeRole).value<RowType>() == RowType::Base
|
||||
|| index.data(TypeRole).value<RowType>() == RowType::VTable) {
|
||||
return member;
|
||||
}
|
||||
const QString owner = index.parent().data(NameRole).toString();
|
||||
const QString separator = owner.endsWith(';') ? QStringLiteral("->")
|
||||
: owner.contains("::") ? QStringLiteral("::")
|
||||
: owner.contains('/') ? QStringLiteral("/")
|
||||
: owner.contains('.') ? QStringLiteral(".")
|
||||
: QStringLiteral("::");
|
||||
return member.startsWith(owner + separator) ? member : owner + separator + member;
|
||||
}
|
||||
|
||||
QVariant ClassesModel::headerData(int section, Qt::Orientation, int role) const
|
||||
{
|
||||
@@ -44,6 +70,9 @@ void BinClassesModel::setClasses(const QList<BinClassDescription> &classes)
|
||||
|
||||
QModelIndex BinClassesModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (!hasIndex(row, column, parent)) {
|
||||
return {};
|
||||
}
|
||||
if (!parent.isValid()) {
|
||||
return createIndex(row, column,
|
||||
(quintptr) 0); // root function nodes have id = 0
|
||||
@@ -70,6 +99,9 @@ QModelIndex BinClassesModel::parent(const QModelIndex &index) const
|
||||
|
||||
int BinClassesModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid() && parent.column() != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (!parent.isValid()) { // root
|
||||
return classes.count();
|
||||
}
|
||||
@@ -89,6 +121,9 @@ int BinClassesModel::columnCount(const QModelIndex &) const
|
||||
|
||||
QVariant BinClassesModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const BinClassDescription *cls;
|
||||
const BinClassMethodDescription *meth = nullptr;
|
||||
const BinClassFieldDescription *field = nullptr;
|
||||
@@ -136,6 +171,15 @@ QVariant BinClassesModel::data(const QModelIndex &index, int role) const
|
||||
return QVariant::fromValue(meth->addr);
|
||||
case NameRole:
|
||||
return meth->name;
|
||||
case MangledNameRole:
|
||||
return meth->mangledName;
|
||||
case NameListModel::NameToolTipRole:
|
||||
case Qt::ToolTipRole:
|
||||
if (!meth->mangledName.isEmpty() && meth->mangledName != meth->name) {
|
||||
return tr("<qt><b>Name:</b> %1<br><b>Mangled:</b> %2</qt>")
|
||||
.arg(name(index).toHtmlEscaped(), meth->mangledName.toHtmlEscaped());
|
||||
}
|
||||
return QStringLiteral("<qt>%1</qt>").arg(name(index).toHtmlEscaped());
|
||||
case TypeRole:
|
||||
return QVariant::fromValue(RowType::Method);
|
||||
default:
|
||||
@@ -354,6 +398,9 @@ const QVector<AnalClassesModel::Attribute> &AnalClassesModel::getAttrs(const QSt
|
||||
|
||||
QModelIndex AnalClassesModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (!hasIndex(row, column, parent)) {
|
||||
return {};
|
||||
}
|
||||
if (!parent.isValid()) {
|
||||
return createIndex(row, column,
|
||||
(quintptr) 0); // root function nodes have id = 0
|
||||
@@ -380,6 +427,9 @@ QModelIndex AnalClassesModel::parent(const QModelIndex &index) const
|
||||
|
||||
int AnalClassesModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid() && parent.column() != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (!parent.isValid()) { // root
|
||||
return classes.count();
|
||||
}
|
||||
@@ -393,7 +443,8 @@ int AnalClassesModel::rowCount(const QModelIndex &parent) const
|
||||
|
||||
bool AnalClassesModel::hasChildren(const QModelIndex &parent) const
|
||||
{
|
||||
return !parent.isValid() || !parent.parent().isValid();
|
||||
return !parent.isValid() ? !classes.isEmpty()
|
||||
: parent.column() == 0 && parent.internalId() == 0;
|
||||
}
|
||||
|
||||
int AnalClassesModel::columnCount(const QModelIndex &) const
|
||||
@@ -403,6 +454,9 @@ int AnalClassesModel::columnCount(const QModelIndex &) const
|
||||
|
||||
QVariant AnalClassesModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
if (index.internalId() == 0) { // class row
|
||||
if (index.row() >= classes.count()) {
|
||||
return QVariant();
|
||||
@@ -533,13 +587,29 @@ QVariant AnalClassesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
|
||||
ClassesSortFilterProxyModel::ClassesSortFilterProxyModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{}
|
||||
: AddressableFilterProxyModel(nullptr, parent)
|
||||
{
|
||||
setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool ClassesSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
|
||||
{
|
||||
QModelIndex index = sourceModel()->index(row, 0, parent);
|
||||
return index.data(ClassesModel::NameRole).toString().contains(FILTER_REGEX);
|
||||
const auto matches = [this](const QModelIndex &item) {
|
||||
return item.data(ClassesModel::NameRole).toString().contains(FILTER_REGEX)
|
||||
|| item.data(ClassesModel::MangledNameRole).toString().contains(FILTER_REGEX);
|
||||
};
|
||||
if (filterAcceptsType(row, parent)
|
||||
&& (matches(index) || (parent.isValid() && matches(parent)))) {
|
||||
return true;
|
||||
}
|
||||
// Keep the owning class when a member matches, but hide unrelated members.
|
||||
for (int child = 0; child < sourceModel()->rowCount(index); ++child) {
|
||||
if (filterAcceptsRow(child, index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ClassesSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||||
@@ -577,11 +647,6 @@ bool ClassesSortFilterProxyModel::lessThan(const QModelIndex &left, const QModel
|
||||
}
|
||||
}
|
||||
|
||||
bool ClassesSortFilterProxyModel::hasChildren(const QModelIndex &parent) const
|
||||
{
|
||||
return !parent.isValid() || !parent.parent().isValid();
|
||||
}
|
||||
|
||||
ClassesWidget::ClassesWidget(MainWindow *main)
|
||||
: IaitoDockWidget(main)
|
||||
, ui(new Ui::ClassesWidget)
|
||||
@@ -592,9 +657,32 @@ ClassesWidget::ClassesWidget(MainWindow *main)
|
||||
|
||||
proxy_model = new ClassesSortFilterProxyModel(this);
|
||||
ui->classesTreeView->setModel(proxy_model);
|
||||
nameMenu = new QMenu(this);
|
||||
names = new NameListView(
|
||||
ui->classesTreeView, nameMenu, proxy_model, ClassesModel::NAME, objectName());
|
||||
auto filter = new QuickFilterView(this);
|
||||
filter->setTypeFilter(proxy_model, ClassesModel::TYPE);
|
||||
ui->verticalLayout->insertWidget(1, filter);
|
||||
filter->addActionButton(names->toggleAction());
|
||||
connect(
|
||||
filter,
|
||||
&QuickFilterView::filterTextChanged,
|
||||
proxy_model,
|
||||
&AddressableFilterProxyModel::setFilterWildcard);
|
||||
connect(
|
||||
filter,
|
||||
&QuickFilterView::filterClosed,
|
||||
ui->classesTreeView,
|
||||
static_cast<void (QWidget::*)()>(&QWidget::setFocus));
|
||||
auto find = new QShortcut(QKeySequence::Find, this);
|
||||
find->setContext(Qt::WidgetWithChildrenShortcut);
|
||||
connect(find, &QShortcut::activated, filter, &QuickFilterView::showFilter);
|
||||
auto clear = new QShortcut(QKeySequence(Qt::Key_Escape), this);
|
||||
clear->setContext(Qt::WidgetWithChildrenShortcut);
|
||||
connect(clear, &QShortcut::activated, filter, &QuickFilterView::clearFilter);
|
||||
ui->classesTreeView->sortByColumn(ClassesModel::TYPE, Qt::AscendingOrder);
|
||||
|
||||
ui->classSourceCombo->setCurrentIndex(1);
|
||||
ui->classSourceCombo->setCurrentIndex(0);
|
||||
|
||||
connect<void (QComboBox::*)(int)>(
|
||||
ui->classSourceCombo, &QComboBox::currentIndexChanged, this, &ClassesWidget::refreshClasses);
|
||||
@@ -603,11 +691,21 @@ ClassesWidget::ClassesWidget(MainWindow *main)
|
||||
&QTreeView::customContextMenuRequested,
|
||||
this,
|
||||
&ClassesWidget::showContextMenu);
|
||||
connect(Core(), &IaitoCore::refreshAll, this, &ClassesWidget::refreshClasses);
|
||||
connect(Core(), &IaitoCore::codeRebased, this, &ClassesWidget::refreshClasses);
|
||||
|
||||
refreshClasses();
|
||||
}
|
||||
|
||||
ClassesWidget::~ClassesWidget() {}
|
||||
ClassesWidget::~ClassesWidget()
|
||||
{
|
||||
ui->classesTreeView->setModel(nullptr);
|
||||
}
|
||||
|
||||
QModelIndex ClassesWidget::currentSourceIndex() const
|
||||
{
|
||||
return names->presentationModel()->mapToSource(ui->classesTreeView->currentIndex());
|
||||
}
|
||||
|
||||
ClassesWidget::Source ClassesWidget::getSource()
|
||||
{
|
||||
@@ -625,7 +723,7 @@ void ClassesWidget::refreshClasses()
|
||||
case Source::BIN:
|
||||
if (!bin_model) {
|
||||
bin_model = new BinClassesModel(this);
|
||||
proxy_model->setSourceModel(bin_model);
|
||||
proxy_model->setClassesModel(bin_model);
|
||||
delete anal_model;
|
||||
anal_model = nullptr;
|
||||
}
|
||||
@@ -634,7 +732,7 @@ void ClassesWidget::refreshClasses()
|
||||
case Source::ANAL:
|
||||
if (!anal_model) {
|
||||
anal_model = new AnalClassesModel(this);
|
||||
proxy_model->setSourceModel(anal_model);
|
||||
proxy_model->setClassesModel(anal_model);
|
||||
delete bin_model;
|
||||
bin_model = nullptr;
|
||||
}
|
||||
@@ -651,59 +749,54 @@ void ClassesWidget::on_classesTreeView_doubleClicked(const QModelIndex &index)
|
||||
if (!index.isValid())
|
||||
return;
|
||||
|
||||
const auto type = index.data(ClassesModel::TypeRole);
|
||||
if (!type.isValid() || type.value<ClassesModel::RowType>() == ClassesModel::RowType::Class) {
|
||||
return;
|
||||
}
|
||||
|
||||
QVariant offsetData = index.data(ClassesModel::OffsetRole);
|
||||
if (!offsetData.isValid()) {
|
||||
return;
|
||||
}
|
||||
RVA offset = offsetData.value<RVA>();
|
||||
Core()->seekAndShow(offset);
|
||||
if (offset != RVA_INVALID) {
|
||||
Core()->seekAndShow(offset);
|
||||
}
|
||||
}
|
||||
|
||||
void ClassesWidget::showContextMenu(const QPoint &pt)
|
||||
{
|
||||
if (!anal_model) {
|
||||
// no context menu for bin classes
|
||||
return;
|
||||
}
|
||||
|
||||
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
auto type = static_cast<ClassesModel::RowType>(index.data(ClassesModel::TypeRole).toInt());
|
||||
|
||||
ui->classesTreeView->setCurrentIndex(ui->classesTreeView->indexAt(pt));
|
||||
const QModelIndex index = currentSourceIndex();
|
||||
const auto type = index.data(ClassesModel::TypeRole).value<ClassesModel::RowType>();
|
||||
QMenu menu(ui->classesTreeView);
|
||||
|
||||
menu.addAction(ui->newClassAction);
|
||||
|
||||
if (type == ClassesModel::RowType::Class) {
|
||||
menu.addAction(ui->renameClassAction);
|
||||
menu.addAction(ui->deleteClassAction);
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addAction(ui->addMethodAction);
|
||||
|
||||
if (type == ClassesModel::RowType::Method) {
|
||||
menu.addAction(ui->editMethodAction);
|
||||
|
||||
QString className = index.parent().data(ClassesModel::NameRole).toString();
|
||||
QString methodName = index.data(ClassesModel::NameRole).toString();
|
||||
AnalMethodDescription desc;
|
||||
if (Core()->getAnalMethod(className, methodName, &desc)) {
|
||||
if (desc.vtableOffset >= 0) {
|
||||
menu.addAction(ui->seekToVTableAction);
|
||||
if (anal_model) {
|
||||
menu.addAction(ui->newClassAction);
|
||||
if (index.isValid()) {
|
||||
if (type == ClassesModel::RowType::Class) {
|
||||
menu.addAction(ui->renameClassAction);
|
||||
menu.addAction(ui->deleteClassAction);
|
||||
}
|
||||
menu.addAction(ui->addMethodAction);
|
||||
if (type == ClassesModel::RowType::Method) {
|
||||
menu.addAction(ui->editMethodAction);
|
||||
const QString className = index.parent().data(ClassesModel::NameRole).toString();
|
||||
const QString methodName = index.data(ClassesModel::NameRole).toString();
|
||||
AnalMethodDescription desc;
|
||||
if (Core()->getAnalMethod(className, methodName, &desc) && desc.vtableOffset >= 0) {
|
||||
menu.addAction(ui->seekToVTableAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu.exec(ui->classesTreeView->mapToGlobal(pt));
|
||||
menu.addActions(nameMenu->actions());
|
||||
menu.exec(ui->classesTreeView->viewport()->mapToGlobal(pt));
|
||||
}
|
||||
|
||||
void ClassesWidget::on_seekToVTableAction_triggered()
|
||||
{
|
||||
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
|
||||
QModelIndex index = currentSourceIndex();
|
||||
QString className = index.parent().data(ClassesModel::NameRole).toString();
|
||||
|
||||
QList<AnalVTableDescription> vtables = Core()->getAnalClassVTables(className);
|
||||
@@ -726,7 +819,7 @@ void ClassesWidget::on_seekToVTableAction_triggered()
|
||||
|
||||
void ClassesWidget::on_addMethodAction_triggered()
|
||||
{
|
||||
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
|
||||
QModelIndex index = currentSourceIndex();
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
@@ -744,7 +837,7 @@ void ClassesWidget::on_addMethodAction_triggered()
|
||||
|
||||
void ClassesWidget::on_editMethodAction_triggered()
|
||||
{
|
||||
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
|
||||
QModelIndex index = currentSourceIndex();
|
||||
if (!index.isValid()
|
||||
|| index.data(ClassesModel::TypeRole).toInt()
|
||||
!= static_cast<int>(ClassesModel::RowType::Method)) {
|
||||
@@ -767,7 +860,7 @@ void ClassesWidget::on_newClassAction_triggered()
|
||||
|
||||
void ClassesWidget::on_deleteClassAction_triggered()
|
||||
{
|
||||
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
|
||||
QModelIndex index = currentSourceIndex();
|
||||
if (!index.isValid()
|
||||
|| index.data(ClassesModel::TypeRole).toInt()
|
||||
!= static_cast<int>(ClassesModel::RowType::Class)) {
|
||||
@@ -786,7 +879,7 @@ void ClassesWidget::on_deleteClassAction_triggered()
|
||||
|
||||
void ClassesWidget::on_renameClassAction_triggered()
|
||||
{
|
||||
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
|
||||
QModelIndex index = currentSourceIndex();
|
||||
if (!index.isValid()
|
||||
|| index.data(ClassesModel::TypeRole).toInt()
|
||||
!= static_cast<int>(ClassesModel::RowType::Class)) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "IaitoDockWidget.h"
|
||||
#include "common/AddressableItemModel.h"
|
||||
#include "core/Iaito.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
@@ -13,6 +14,8 @@ namespace Ui {
|
||||
class ClassesWidget;
|
||||
}
|
||||
|
||||
class NameListView;
|
||||
class QMenu;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class MainWindow;
|
||||
@@ -21,7 +24,7 @@ class ClassesWidget;
|
||||
/**
|
||||
* @brief Common abstract base class for Bin and Anal classes models
|
||||
*/
|
||||
class ClassesModel : public QAbstractItemModel
|
||||
class ClassesModel : public AddressableItemModel<>
|
||||
{
|
||||
public:
|
||||
enum Columns { NAME = 0, TYPE, OFFSET, VTABLE, COUNT };
|
||||
@@ -60,13 +63,16 @@ public:
|
||||
* by vtable offset
|
||||
*/
|
||||
static const int VTableRole = Qt::UserRole + 3;
|
||||
static const int MangledNameRole = Qt::UserRole + 4;
|
||||
|
||||
explicit ClassesModel(QObject *parent = nullptr)
|
||||
: QAbstractItemModel(parent)
|
||||
: AddressableItemModel<>(parent)
|
||||
{}
|
||||
|
||||
QVariant headerData(
|
||||
int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
RVA address(const QModelIndex &index) const override;
|
||||
QString name(const QModelIndex &index) const override;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ClassesModel::RowType)
|
||||
@@ -158,17 +164,20 @@ public slots:
|
||||
void classAttrsChanged(const QString &cls);
|
||||
};
|
||||
|
||||
class ClassesSortFilterProxyModel : public QSortFilterProxyModel
|
||||
class ClassesSortFilterProxyModel : public AddressableFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClassesSortFilterProxyModel(QObject *parent = nullptr);
|
||||
void setClassesModel(ClassesModel *model)
|
||||
{
|
||||
AddressableFilterProxyModel::setSourceModel(static_cast<AddressableItemModelI *>(model));
|
||||
}
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int row, const QModelIndex &parent) const override;
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
|
||||
bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;
|
||||
};
|
||||
|
||||
class ClassesWidget : public IaitoDockWidget
|
||||
@@ -203,6 +212,9 @@ private:
|
||||
BinClassesModel *bin_model = nullptr;
|
||||
AnalClassesModel *anal_model = nullptr;
|
||||
ClassesSortFilterProxyModel *proxy_model;
|
||||
NameListView *names;
|
||||
QMenu *nameMenu;
|
||||
QModelIndex currentSourceIndex() const;
|
||||
};
|
||||
|
||||
#endif // CLASSESWIDGET_H
|
||||
@@ -244,6 +244,24 @@ void CodeInfoWidget::buildUi()
|
||||
functionTree->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
functionTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
layout->addWidget(functionTree);
|
||||
|
||||
exceptionStatus = new QLabel(functionSection);
|
||||
exceptionStatus->setWordWrap(true);
|
||||
layout->addWidget(exceptionStatus);
|
||||
exceptionTree = new QTreeWidget(functionSection);
|
||||
exceptionTree->setHeaderLabels({tr("Exception"), tr("Address / value")});
|
||||
exceptionTree->setRootIsDecorated(true);
|
||||
exceptionTree->setAlternatingRowColors(true);
|
||||
exceptionTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
exceptionTree->setToolTip(tr("Activate an address to navigate. Range ends are exclusive."));
|
||||
exceptionTree->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
connect(exceptionTree, &QTreeWidget::itemActivated, this, [](QTreeWidgetItem *item, int) {
|
||||
const auto address = item->data(0, Qt::UserRole);
|
||||
if (address.isValid()) {
|
||||
Core()->seek(address.toULongLong());
|
||||
}
|
||||
});
|
||||
layout->addWidget(exceptionTree);
|
||||
}
|
||||
rootLayout->addWidget(functionSection);
|
||||
|
||||
@@ -400,6 +418,7 @@ void CodeInfoWidget::refresh()
|
||||
|
||||
const RVA off = Core()->getOffset();
|
||||
const QString addrStr = RAddressString(off);
|
||||
refreshExceptions(off);
|
||||
|
||||
// Opcode (aoj)
|
||||
QJsonDocument opDoc = Core()->cmdj("aoj");
|
||||
@@ -492,6 +511,62 @@ void CodeInfoWidget::refresh()
|
||||
}
|
||||
}
|
||||
|
||||
void CodeInfoWidget::refreshExceptions(RVA address)
|
||||
{
|
||||
exceptionTree->clear();
|
||||
const RVA function = Core()->getFunctionStart(address);
|
||||
auto regions = Core()->getExceptionRegions(function);
|
||||
if (function == RVA_INVALID) {
|
||||
for (auto it = regions.begin(); it != regions.end();) {
|
||||
if (!it->touches(address, 1)) {
|
||||
it = regions.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
exceptionStatus->setText(
|
||||
regions.isEmpty()
|
||||
? tr("No exception metadata reported for this function.")
|
||||
: tr("Exception handlers (%1) · activate an address to navigate").arg(regions.size()));
|
||||
exceptionTree->setVisible(!regions.isEmpty());
|
||||
exceptionStatus->setVisible(!regions.isEmpty());
|
||||
for (const auto ®ion : regions) {
|
||||
auto *entry = new QTreeWidgetItem(
|
||||
exceptionTree,
|
||||
{QStringLiteral("#%1 · %2").arg(region.index).arg(region.label()),
|
||||
region.contains(address) ? tr("Protects current instruction") : QString()});
|
||||
entry->setToolTip(
|
||||
0,
|
||||
region.description().toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br>")));
|
||||
QPixmap swatch(12, 12);
|
||||
swatch.fill(region.color());
|
||||
entry->setIcon(0, QIcon(swatch));
|
||||
const auto addAddress = [entry](const QString &name, quint64 value) {
|
||||
auto *item = new QTreeWidgetItem(entry, {name, RAddressString(value)});
|
||||
item->setData(0, Qt::UserRole, QVariant::fromValue(value));
|
||||
};
|
||||
addAddress(tr("Try start"), region.from);
|
||||
addAddress(tr("Try end (exclusive)"), region.to);
|
||||
addAddress(tr("Handler"), region.handler);
|
||||
if (region.filter && region.filter != ExceptionRegion::Invalid) {
|
||||
if (region.filter > 1) {
|
||||
addAddress(tr("Filter"), region.filter);
|
||||
} else {
|
||||
new QTreeWidgetItem(entry, {tr("Filter constant"), QString::number(region.filter)});
|
||||
}
|
||||
}
|
||||
if (region.typeFilter) {
|
||||
new QTreeWidgetItem(entry, {tr("Type filter"), QString::number(region.typeFilter)});
|
||||
}
|
||||
if (region.source != ExceptionRegion::Invalid) {
|
||||
addAddress(tr("Metadata source"), region.source);
|
||||
}
|
||||
entry->setExpanded(true);
|
||||
}
|
||||
fitTreeToContent(exceptionTree);
|
||||
}
|
||||
|
||||
void CodeInfoWidget::onPatchInstruction()
|
||||
{
|
||||
if (mainWindow) {
|
||||
|
||||
@@ -57,6 +57,7 @@ private:
|
||||
void buildUi();
|
||||
void populateTree(QTreeWidget *tree, const QJsonValue &value);
|
||||
void addJsonNode(QTreeWidgetItem *parent, const QString &key, const QJsonValue &value);
|
||||
void refreshExceptions(RVA address);
|
||||
|
||||
CodeInfoSection *opcodeSection;
|
||||
CodeInfoSection *blockSection;
|
||||
@@ -73,6 +74,8 @@ private:
|
||||
QTreeWidget *opcodeTree;
|
||||
QTreeWidget *blockTree;
|
||||
QTreeWidget *functionTree;
|
||||
QTreeWidget *exceptionTree;
|
||||
QLabel *exceptionStatus;
|
||||
|
||||
QPushButton *btnPatchInstr;
|
||||
QPushButton *btnPatchBytes;
|
||||
|
||||
@@ -399,7 +399,7 @@ void ColorShowWidget::setColor(const QColor &c)
|
||||
void ColorShowWidget::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPainter p(this);
|
||||
const int miniRectWidth = rect().width() / 2;
|
||||
const int miniRectWidth = qMax(2, rect().width() / 2);
|
||||
for (int y = rect().topLeft().ry(); y < rect().bottomRight().ry(); y++) {
|
||||
for (int x = rect().topLeft().rx(); x < rect().bottomRight().rx(); x++) {
|
||||
p.setPen(
|
||||
@@ -438,7 +438,7 @@ void AlphaChannelBar::paintEvent(QPaintEvent *event)
|
||||
|
||||
barRect.setWidth(barRect.width() - triangleSize);
|
||||
|
||||
const int miniRectWidth = barRect.width() / 2;
|
||||
const int miniRectWidth = qMax(2, barRect.width() / 2);
|
||||
for (int y = barRect.topLeft().ry(); y < barRect.bottomRight().ry(); y++) {
|
||||
for (int x = barRect.topLeft().rx(); x < barRect.bottomRight().rx(); x++) {
|
||||
p.setPen(
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
#include "ComboQuickFilterView.h"
|
||||
#include "ui_ComboQuickFilterView.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
|
||||
ComboQuickFilterView::ComboQuickFilterView(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, ui(new Ui::ComboQuickFilterView)
|
||||
: QuickFilterView(parent)
|
||||
, label(new QLabel(this))
|
||||
, combo(new QComboBox(this))
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
connect(ui->lineEdit, &QLineEdit::textChanged, this, [this](const QString &text) {
|
||||
emit filterTextChanged(text);
|
||||
});
|
||||
}
|
||||
|
||||
ComboQuickFilterView::~ComboQuickFilterView()
|
||||
{
|
||||
delete ui;
|
||||
label->setObjectName(QStringLiteral("label"));
|
||||
combo->setObjectName(QStringLiteral("comboBox"));
|
||||
auto row = qobject_cast<QHBoxLayout *>(layout());
|
||||
row->addWidget(label);
|
||||
row->addWidget(combo);
|
||||
}
|
||||
|
||||
void ComboQuickFilterView::setLabelText(const QString &text)
|
||||
{
|
||||
ui->label->setText(text);
|
||||
label->setText(text);
|
||||
}
|
||||
|
||||
QComboBox *ComboQuickFilterView::comboBox()
|
||||
{
|
||||
return ui->comboBox;
|
||||
}
|
||||
|
||||
void ComboQuickFilterView::showFilter()
|
||||
{
|
||||
show();
|
||||
ui->lineEdit->setFocus();
|
||||
return combo;
|
||||
}
|
||||
|
||||
void ComboQuickFilterView::clearFilter()
|
||||
{
|
||||
ui->lineEdit->setText("");
|
||||
QuickFilterView::clearFilter();
|
||||
combo->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
void ComboQuickFilterView::closeFilter()
|
||||
{
|
||||
ui->lineEdit->setText("");
|
||||
hide();
|
||||
emit filterClosed();
|
||||
combo->setCurrentIndex(0);
|
||||
QuickFilterView::closeFilter();
|
||||
}
|
||||
@@ -1,37 +1,28 @@
|
||||
#ifndef COMBOQUICKFILTERVIEW_H
|
||||
#define COMBOQUICKFILTERVIEW_H
|
||||
|
||||
#include "core/IaitoCommon.h"
|
||||
#include "QuickFilterView.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QWidget>
|
||||
class QLabel;
|
||||
|
||||
namespace Ui {
|
||||
class ComboQuickFilterView;
|
||||
}
|
||||
|
||||
class IAITO_EXPORT ComboQuickFilterView : public QWidget
|
||||
class IAITO_EXPORT ComboQuickFilterView : public QuickFilterView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ComboQuickFilterView(QWidget *parent = nullptr);
|
||||
~ComboQuickFilterView();
|
||||
|
||||
void setLabelText(const QString &text);
|
||||
QComboBox *comboBox();
|
||||
|
||||
public slots:
|
||||
void showFilter();
|
||||
void closeFilter();
|
||||
void clearFilter();
|
||||
|
||||
signals:
|
||||
void filterTextChanged(const QString &text);
|
||||
void filterClosed();
|
||||
void closeFilter();
|
||||
|
||||
private:
|
||||
Ui::ComboQuickFilterView *ui;
|
||||
QLabel *label;
|
||||
QComboBox *combo;
|
||||
};
|
||||
|
||||
#endif // COMBOQUICKFILTERVIEW_H
|
||||
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ComboQuickFilterView</class>
|
||||
<widget class="QWidget" name="ComboQuickFilterView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>378</width>
|
||||
<height>20</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Quick Filter</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -44,6 +44,7 @@ static const char *refreshAllCommandPrefixes[] = {
|
||||
"C",
|
||||
"aa",
|
||||
"af",
|
||||
"avg",
|
||||
"w",
|
||||
};
|
||||
|
||||
@@ -150,7 +151,8 @@ ConsoleWidget::ConsoleWidget(MainWindow *main)
|
||||
historyDownShortcut->setContext(Qt::WidgetShortcut);
|
||||
|
||||
QShortcut *completionShortcut
|
||||
= ShortcutMgr()->registerShortcut("console.complete", ui->r2InputLineEdit);
|
||||
= ShortcutMgr()
|
||||
->registerShortcut("console.complete", ui->r2InputLineEdit, Qt::WidgetShortcut);
|
||||
connect(completionShortcut, &QShortcut::activated, this, &ConsoleWidget::triggerCompletion);
|
||||
|
||||
connect(ui->r2InputLineEdit, &QLineEdit::editingFinished, this, &ConsoleWidget::disableCompletion);
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
#include "DataAnalysisView.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QGestureEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QLinearGradient>
|
||||
#include <QMouseEvent>
|
||||
#include <QNativeGestureEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPinchGesture>
|
||||
#include <QToolTip>
|
||||
#include <QWheelEvent>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
using namespace DataAnalysis;
|
||||
|
||||
DataAnalysisView::DataAnalysisView(bool overview, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, overview(overview)
|
||||
{
|
||||
setMouseTracking(true);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
#ifndef Q_OS_MACOS
|
||||
// On macOS, handle native trackpad events directly to retain their position.
|
||||
grabGesture(Qt::PinchGesture);
|
||||
#endif
|
||||
setMinimumSize(220, overview ? 70 : 180);
|
||||
setAccessibleName(overview ? tr("Data overview") : tr("Data analysis graph"));
|
||||
setAccessibleDescription(
|
||||
overview
|
||||
? tr("Drag the window or use the wheel to pan; drag its edges to resize; pinch to zoom."
|
||||
" Double-click to fit.")
|
||||
: tr("Click to seek; drag to select bytes; wheel to pan; pinch to zoom."
|
||||
" Right-click for comments, flags and range actions."));
|
||||
tooltipTimer.setSingleShot(true);
|
||||
tooltipTimer.setInterval(650);
|
||||
connect(&tooltipTimer, &QTimer::timeout, this, &DataAnalysisView::showTooltip);
|
||||
}
|
||||
|
||||
QString DataAnalysisView::metricName(int metric)
|
||||
{
|
||||
static const char *names[]
|
||||
= {QT_TR_NOOP("Entropy"),
|
||||
QT_TR_NOOP("Printable ASCII"),
|
||||
QT_TR_NOOP("0x00 bytes"),
|
||||
QT_TR_NOOP("0xFF bytes"),
|
||||
QT_TR_NOOP("Unique bytes"),
|
||||
QT_TR_NOOP("Mean byte"),
|
||||
QT_TR_NOOP("ASCII letters"),
|
||||
QT_TR_NOOP("ASCII digits"),
|
||||
QT_TR_NOOP("ASCII whitespace"),
|
||||
QT_TR_NOOP("ASCII control"),
|
||||
QT_TR_NOOP("High bytes")};
|
||||
return metric >= 0 && metric < Count ? tr(names[metric]) : QString();
|
||||
}
|
||||
|
||||
QString DataAnalysisView::metricValue(int metric, double value)
|
||||
{
|
||||
if (metric == Entropy) {
|
||||
return tr("%1 bits/byte").arg(value * 8, 0, 'f', 3);
|
||||
}
|
||||
if (metric == Unique) {
|
||||
return tr("%1 / 256").arg(qRound(value * 256));
|
||||
}
|
||||
if (metric == Mean) {
|
||||
return QString::number(value * 255, 'f', 2);
|
||||
}
|
||||
return tr("%1%").arg(value * 100, 0, 'f', 2);
|
||||
}
|
||||
|
||||
QString DataAnalysisView::metricDescription(int metric)
|
||||
{
|
||||
static const char *descriptions[]
|
||||
= {QT_TR_NOOP(
|
||||
"Shannon entropy: 0–8 bits per byte. Higher values indicate a more varied byte "
|
||||
"distribution. Overlaid values are divided by 8."),
|
||||
QT_TR_NOOP("Percentage of printable ASCII bytes (0x20–0x7e)."),
|
||||
QT_TR_NOOP("Percentage of zero bytes (0x00), often padding or uninitialized data."),
|
||||
QT_TR_NOOP("Percentage of 0xff bytes, often padding or erased storage."),
|
||||
QT_TR_NOOP("Number of distinct byte values observed, out of 256 possible values."),
|
||||
QT_TR_NOOP("Average byte value (0–255). Overlaid values are divided by 255."),
|
||||
QT_TR_NOOP("Percentage of ASCII letters (A–Z and a–z)."),
|
||||
QT_TR_NOOP("Percentage of ASCII decimal digits (0–9)."),
|
||||
QT_TR_NOOP(
|
||||
"Percentage of ASCII whitespace (space, tab, newline, vertical tab, form feed and "
|
||||
"carriage return)."),
|
||||
QT_TR_NOOP("Percentage of ASCII control bytes (0x00–0x1f and 0x7f)."),
|
||||
QT_TR_NOOP("Percentage of bytes with the high bit set (0x80–0xff).")};
|
||||
return metric >= 0 && metric < Count ? tr(descriptions[metric]) : QString();
|
||||
}
|
||||
|
||||
QString DataAnalysisView::markerName(MarkerType type)
|
||||
{
|
||||
static const char *names[]
|
||||
= {QT_TR_NOOP("Section"),
|
||||
QT_TR_NOOP("Segment"),
|
||||
QT_TR_NOOP("Map"),
|
||||
QT_TR_NOOP("Flag"),
|
||||
QT_TR_NOOP("Comment")};
|
||||
return tr(names[type]);
|
||||
}
|
||||
|
||||
Range DataAnalysisView::axisRange() const
|
||||
{
|
||||
return overview ? domain : visibleRange;
|
||||
}
|
||||
|
||||
QRectF DataAnalysisView::plotRect() const
|
||||
{
|
||||
return QRectF(
|
||||
46, overview ? 5 : 30, qMax(1, width() - 60), qMax(1, height() - (overview ? 26 : 59)));
|
||||
}
|
||||
|
||||
qreal DataAnalysisView::xForAddress(RVA address) const
|
||||
{
|
||||
const Range range = axisRange();
|
||||
if (!range.valid()) {
|
||||
return plotRect().left();
|
||||
}
|
||||
const long double distance = address >= range.start ? (long double) (address - range.start)
|
||||
: -(long double) (range.start - address);
|
||||
return plotRect().left() + distance / range.size() * plotRect().width();
|
||||
}
|
||||
|
||||
RVA DataAnalysisView::addressAt(qreal x) const
|
||||
{
|
||||
const Range range = axisRange();
|
||||
if (!range.valid()) {
|
||||
return RVA_INVALID;
|
||||
}
|
||||
const long double fraction = (x - plotRect().left()) / plotRect().width();
|
||||
if (fraction <= 0) {
|
||||
return range.start;
|
||||
}
|
||||
if (fraction >= 1) {
|
||||
return range.end - 1;
|
||||
}
|
||||
return range.start + qMin<RVA>(RVA(fraction * range.size()), range.size() - 1);
|
||||
}
|
||||
|
||||
void DataAnalysisView::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.fillRect(rect(), palette().color(QPalette::Base));
|
||||
const QRectF plot = plotRect();
|
||||
const Range range = axisRange();
|
||||
const QColor text = palette().color(QPalette::Text);
|
||||
QColor grid = text;
|
||||
grid.setAlpha(35);
|
||||
p.setPen(grid);
|
||||
p.drawRect(plot);
|
||||
if (!range.valid()) {
|
||||
p.setPen(text);
|
||||
p.drawText(plot, Qt::AlignCenter, tr("Open a file to analyze its data"));
|
||||
return;
|
||||
}
|
||||
|
||||
const bool split = !overview && separateTracks;
|
||||
const int tracks = split ? qMax(1, int(metrics.size())) : 1;
|
||||
for (int track = 0; track < tracks; ++track) {
|
||||
const qreal top = plot.top() + track * plot.height() / tracks;
|
||||
const qreal bottom = plot.top() + (track + 1) * plot.height() / tracks;
|
||||
const qreal graphTop = top + (split ? 19 : 0);
|
||||
const qreal graphBottom = bottom - (split ? 5 : 0);
|
||||
const qreal graphHeight = qMax(1.0, graphBottom - graphTop);
|
||||
if (!overview) {
|
||||
for (int tick = 0; tick <= 4; ++tick) {
|
||||
const qreal y = graphBottom - graphHeight * tick / 4;
|
||||
p.setPen(grid);
|
||||
p.drawLine(QPointF(plot.left(), y), QPointF(plot.right(), y));
|
||||
if (tracks == 1 || tick == 0 || tick == 4) {
|
||||
int maximum = 100;
|
||||
if ((separateTracks || metrics.size() == 1) && !metrics.isEmpty()) {
|
||||
const int metric = metrics[track];
|
||||
maximum = metric == Entropy ? 8
|
||||
: metric == Mean ? 255
|
||||
: metric == Unique ? 256
|
||||
: 100;
|
||||
}
|
||||
p.setPen(text);
|
||||
QString label = QString::number(maximum * tick / 4.0, 'g', 3);
|
||||
if (maximum == 100 && tick == 4) {
|
||||
label += '%';
|
||||
}
|
||||
p.drawText(
|
||||
QRectF(0, y - 8, plot.left() - 7, 16),
|
||||
Qt::AlignRight | Qt::AlignVCenter,
|
||||
label);
|
||||
}
|
||||
}
|
||||
if (split && !metrics.isEmpty()) {
|
||||
p.setPen(colors[metrics[track]]);
|
||||
p.drawText(
|
||||
QRectF(plot.left() + 5, top, plot.width() - 10, 18),
|
||||
Qt::AlignLeft | Qt::AlignVCenter,
|
||||
metricName(metrics[track]));
|
||||
}
|
||||
}
|
||||
p.save();
|
||||
p.setClipRect(QRectF(plot.left(), graphTop, plot.width(), graphHeight));
|
||||
const QVector<int> drawn = overview ? QVector<int>{Entropy}
|
||||
: split && !metrics.isEmpty() ? QVector<int>{metrics[track]}
|
||||
: metrics;
|
||||
for (int series = 0; series < drawn.size(); ++series) {
|
||||
const int metric = drawn[series];
|
||||
const QColor color = colors[metric];
|
||||
QPainterPath path;
|
||||
qreal firstX = 0, lastX = 0;
|
||||
auto flush = [&]() {
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (overview || style == Areas) {
|
||||
QPainterPath area = path;
|
||||
area.lineTo(lastX, graphBottom);
|
||||
area.lineTo(firstX, graphBottom);
|
||||
area.closeSubpath();
|
||||
QLinearGradient gradient(0, graphTop, 0, graphBottom);
|
||||
QColor fill = color;
|
||||
fill.setAlpha(overview ? 110 : 85);
|
||||
gradient.setColorAt(0, fill);
|
||||
fill.setAlpha(12);
|
||||
gradient.setColorAt(1, fill);
|
||||
p.fillPath(area, gradient);
|
||||
}
|
||||
p.setPen(QPen(color, overview ? 1 : 1.5));
|
||||
p.drawPath(path);
|
||||
path = QPainterPath();
|
||||
};
|
||||
for (const auto &sample : samples) {
|
||||
if (!sample.bytes) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
const qreal left = xForAddress(sample.range.start);
|
||||
const qreal right = xForAddress(sample.range.end);
|
||||
const qreal y = graphBottom - qBound(0.0, sample.values[metric], 1.0) * graphHeight;
|
||||
if (!overview && style == Bars) {
|
||||
const qreal barWidth = (right - left) / qMax(1, int(drawn.size()));
|
||||
QColor fill = color;
|
||||
fill.setAlpha(190);
|
||||
p.fillRect(
|
||||
QRectF(left + series * barWidth, y, qMax(0.6, barWidth), graphBottom - y),
|
||||
fill);
|
||||
} else {
|
||||
if (path.isEmpty()) {
|
||||
firstX = left;
|
||||
path.moveTo(left, y);
|
||||
}
|
||||
path.lineTo((left + right) / 2, y);
|
||||
lastX = right;
|
||||
// Retain single-bucket signals and reach both ends of the range.
|
||||
path.lineTo(right, y);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
}
|
||||
p.restore();
|
||||
}
|
||||
|
||||
// Markers are coalesced by pixel and category to keep dense flag sets cheap to paint.
|
||||
std::array<int, MarkerCount> lastPixel;
|
||||
lastPixel.fill(-1000);
|
||||
qreal labelEnd = plot.left();
|
||||
for (const auto &marker : markers) {
|
||||
if (!markerEnabled[marker.type]) {
|
||||
continue;
|
||||
}
|
||||
const QColor color = colors[(marker.type + 4) % Count];
|
||||
for (RVA address : {marker.range.start, marker.range.end}) {
|
||||
if (address == marker.range.end && marker.type >= Flag) {
|
||||
continue;
|
||||
}
|
||||
if (address < range.start || address > range.end) {
|
||||
continue;
|
||||
}
|
||||
const qreal x = xForAddress(address);
|
||||
const int pixel = qRound(x);
|
||||
if (std::abs(pixel - lastPixel[marker.type]) < 2) {
|
||||
continue;
|
||||
}
|
||||
lastPixel[marker.type] = pixel;
|
||||
QColor line = color;
|
||||
line.setAlpha(95);
|
||||
p.setPen(QPen(line, 1, marker.type >= Flag ? Qt::DotLine : Qt::DashLine));
|
||||
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
|
||||
p.setPen(color);
|
||||
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.top() + 5));
|
||||
}
|
||||
if (!overview && marker.type <= Map && intersect(marker.range, range).valid()) {
|
||||
const qreal left = qMax(plot.left(), xForAddress(marker.range.start));
|
||||
const qreal right = qMin(plot.right(), xForAddress(marker.range.end));
|
||||
if (left >= labelEnd && right - left > 38) {
|
||||
p.setPen(color);
|
||||
const QString label
|
||||
= fontMetrics().elidedText(marker.name, Qt::ElideRight, int(right - left - 6));
|
||||
p.drawText(QRectF(left + 3, 4, right - left - 6, 20), Qt::AlignLeft, label);
|
||||
labelEnd = left + fontMetrics().horizontalAdvance(label) + 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto highlight = [&](Range selected, bool window) {
|
||||
selected = intersect(selected, range);
|
||||
if (!selected.valid()) {
|
||||
return;
|
||||
}
|
||||
const qreal left = xForAddress(selected.start), right = xForAddress(selected.end);
|
||||
QColor fill = palette().color(QPalette::Highlight);
|
||||
fill.setAlpha(window ? 35 : 55);
|
||||
p.fillRect(QRectF(left, plot.top(), qMax(1.0, right - left), plot.height()), fill);
|
||||
p.setPen(QPen(palette().color(QPalette::Highlight), window ? 2 : 1));
|
||||
p.drawRect(QRectF(left, plot.top(), qMax(1.0, right - left), plot.height()));
|
||||
if (window) {
|
||||
p.fillRect(QRectF(left - 2, plot.center().y() - 7, 4, 14), text);
|
||||
p.fillRect(QRectF(right - 2, plot.center().y() - 7, 4, 14), text);
|
||||
}
|
||||
};
|
||||
if (overview) {
|
||||
const QColor shade(0, 0, 0, 65);
|
||||
const qreal left = xForAddress(visibleRange.start), right = xForAddress(visibleRange.end);
|
||||
p.fillRect(
|
||||
QRectF(plot.left(), plot.top(), qMax(0.0, left - plot.left()), plot.height()), shade);
|
||||
p.fillRect(QRectF(right, plot.top(), qMax(0.0, plot.right() - right), plot.height()), shade);
|
||||
highlight(visibleRange, true);
|
||||
}
|
||||
highlight(drag == Select && dragSelection.valid() ? dragSelection : selection, false);
|
||||
if (range.contains(seekAddress)) {
|
||||
const qreal x = xForAddress(seekAddress);
|
||||
p.setPen(QPen(palette().color(QPalette::Highlight), 2));
|
||||
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
|
||||
p.setBrush(palette().color(QPalette::Highlight));
|
||||
p.drawPolygon(QPolygonF(
|
||||
QVector<QPointF>{
|
||||
QPointF(x - 4, plot.top()), QPointF(x + 4, plot.top()), QPointF(x, plot.top() + 6)}));
|
||||
}
|
||||
if (!overview && hoverX >= plot.left() && hoverX <= plot.right()) {
|
||||
p.setPen(QPen(text, 1, Qt::DotLine));
|
||||
p.drawLine(QPointF(hoverX, plot.top()), QPointF(hoverX, plot.bottom()));
|
||||
}
|
||||
p.setPen(text);
|
||||
const int addressWidth = fontMetrics().horizontalAdvance(RAddressString(range.end - 1));
|
||||
const int labels = qBound(1, int(plot.width() / (addressWidth + 24)), 4);
|
||||
for (int i = 0; i <= labels; ++i) {
|
||||
const qreal x = plot.left() + plot.width() * i / labels;
|
||||
const QString label = fontMetrics().elidedText(
|
||||
RAddressString(addressAt(x)), Qt::ElideMiddle, int(plot.width() / (labels + 1)));
|
||||
const qreal width = fontMetrics().horizontalAdvance(label) + 2;
|
||||
const qreal left
|
||||
= qBound(plot.left(), x - width / 2, qMax(plot.left(), plot.right() - width));
|
||||
p.drawText(QRectF(left, plot.bottom() + 4, width, 20), Qt::AlignLeft, label);
|
||||
}
|
||||
const bool hasData = std::any_of(samples.cbegin(), samples.cend(), [](const Sample &sample) {
|
||||
return sample.bytes > 0;
|
||||
});
|
||||
if (!overview && !hasData) {
|
||||
p.drawText(plot, Qt::AlignCenter, loading ? tr("Analyzing…") : tr("No readable data"));
|
||||
}
|
||||
}
|
||||
|
||||
bool DataAnalysisView::event(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Gesture) {
|
||||
auto *gestures = static_cast<QGestureEvent *>(event);
|
||||
if (auto *pinch = static_cast<QPinchGesture *>(gestures->gesture(Qt::PinchGesture))) {
|
||||
if (pinch->state() != Qt::GestureFinished && pinch->state() != Qt::GestureCanceled
|
||||
&& (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged)) {
|
||||
const QPoint position = mapFromGlobal(pinch->centerPoint().toPoint());
|
||||
pinchZoom(pinch->scaleFactor(), position.x());
|
||||
}
|
||||
gestures->accept(pinch);
|
||||
return true;
|
||||
}
|
||||
} else if (event->type() == QEvent::NativeGesture) {
|
||||
auto *gesture = static_cast<QNativeGestureEvent *>(event);
|
||||
if (gesture->gestureType() == Qt::ZoomNativeGesture) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
const qreal x = gesture->position().x();
|
||||
#else
|
||||
const qreal x = gesture->localPos().x();
|
||||
#endif
|
||||
pinchZoom(1.0 + gesture->value(), x);
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
void DataAnalysisView::pinchZoom(qreal scaleFactor, qreal x)
|
||||
{
|
||||
cancelTooltip();
|
||||
drag = None;
|
||||
dragSelection = {};
|
||||
if (std::isfinite(scaleFactor) && scaleFactor > 0 && scaleFactor != 1) {
|
||||
// Magnifying the graph narrows the address range around the gesture.
|
||||
zoom(1.0 / scaleFactor, addressAt(x));
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void DataAnalysisView::zoom(double factor, RVA anchor)
|
||||
{
|
||||
if (!domain.valid() || !visibleRange.valid() || !std::isfinite(factor) || factor <= 0) {
|
||||
return;
|
||||
}
|
||||
long double scaled = (long double) visibleRange.size() * factor;
|
||||
if (factor > 1) {
|
||||
// Small pinch steps must still let the user zoom out from a one-byte range.
|
||||
scaled = std::ceil(scaled);
|
||||
}
|
||||
const RVA size = scaled >= domain.size() ? domain.size() : qMax<RVA>(1, RVA(scaled));
|
||||
anchor = qBound(visibleRange.start, anchor, visibleRange.end - 1);
|
||||
const long double ratio = (long double) (anchor - visibleRange.start) / visibleRange.size();
|
||||
const RVA before = qMin<RVA>(size - 1, RVA(ratio * size));
|
||||
const RVA start = anchor - qMin(anchor - domain.start, before);
|
||||
const RVA clamped = qMin(start, domain.end - size);
|
||||
emit rangeRequested(clamped, clamped + size);
|
||||
}
|
||||
|
||||
void DataAnalysisView::pan(int direction)
|
||||
{
|
||||
if (!visibleRange.valid() || !domain.valid()) {
|
||||
return;
|
||||
}
|
||||
const RVA step = qMax<RVA>(1, visibleRange.size() / 4);
|
||||
const RVA start = direction < 0
|
||||
? visibleRange.start - qMin(step, visibleRange.start - domain.start)
|
||||
: visibleRange.start + qMin(step, domain.end - visibleRange.end);
|
||||
emit rangeRequested(start, start + visibleRange.size());
|
||||
}
|
||||
|
||||
void DataAnalysisView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
cancelTooltip();
|
||||
if (event->button() != Qt::LeftButton || !axisRange().valid()
|
||||
|| !plotRect().contains(event->pos())) {
|
||||
return;
|
||||
}
|
||||
setFocus();
|
||||
pressPosition = event->pos();
|
||||
pressAddress = addressAt(event->pos().x());
|
||||
pressRange = visibleRange;
|
||||
dragSelection = {};
|
||||
if (overview) {
|
||||
if (std::abs(event->pos().x() - xForAddress(visibleRange.start)) < 7) {
|
||||
drag = LeftEdge;
|
||||
} else if (std::abs(event->pos().x() - xForAddress(visibleRange.end)) < 7) {
|
||||
drag = RightEdge;
|
||||
} else {
|
||||
drag = Move;
|
||||
if (!visibleRange.contains(pressAddress)) {
|
||||
pressRange = centered(domain, pressAddress, visibleRange.size());
|
||||
emit rangeRequested(pressRange.start, pressRange.end);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
drag = Select;
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void DataAnalysisView::updateDrag(QPoint position)
|
||||
{
|
||||
const RVA address = addressAt(position.x());
|
||||
if (drag == Select) {
|
||||
dragSelection = {qMin(address, pressAddress), qMax(address, pressAddress) + 1};
|
||||
update();
|
||||
} else if (drag == LeftEdge) {
|
||||
emit rangeRequested(qMin(address, pressRange.end - 1), pressRange.end);
|
||||
} else if (drag == RightEdge) {
|
||||
emit rangeRequested(pressRange.start, qMax(address + 1, pressRange.start + 1));
|
||||
} else if (drag == Move) {
|
||||
const RVA start = address >= pressAddress
|
||||
? pressRange.start
|
||||
+ qMin(address - pressAddress, domain.end - pressRange.end)
|
||||
: pressRange.start
|
||||
- qMin(pressAddress - address, pressRange.start - domain.start);
|
||||
emit rangeRequested(start, start + pressRange.size());
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisView::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
const bool moved = hoverPosition != event->pos();
|
||||
hoverPosition = event->pos();
|
||||
hoverX = event->pos().x();
|
||||
if (drag != None) {
|
||||
updateDrag(event->pos());
|
||||
} else if (plotRect().contains(event->pos())) {
|
||||
if (moved) {
|
||||
cancelTooltip();
|
||||
tooltipTimer.start();
|
||||
}
|
||||
const bool edge = overview
|
||||
&& (std::abs(hoverX - xForAddress(visibleRange.start)) < 7
|
||||
|| std::abs(hoverX - xForAddress(visibleRange.end)) < 7);
|
||||
setCursor(edge ? Qt::SizeHorCursor : overview ? Qt::OpenHandCursor : Qt::CrossCursor);
|
||||
} else {
|
||||
cancelTooltip();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void DataAnalysisView::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton || drag == None) {
|
||||
return;
|
||||
}
|
||||
updateDrag(event->pos());
|
||||
if (drag == Select) {
|
||||
if ((event->pos() - pressPosition).manhattanLength() < QApplication::startDragDistance()) {
|
||||
emit selectionRequested(0, 0);
|
||||
emit seekRequested(nearestMarker(event->pos().x()));
|
||||
} else {
|
||||
emit selectionRequested(dragSelection.start, dragSelection.end);
|
||||
}
|
||||
}
|
||||
drag = None;
|
||||
dragSelection = {};
|
||||
update();
|
||||
}
|
||||
|
||||
void DataAnalysisView::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
cancelTooltip();
|
||||
if (event->button() == Qt::LeftButton && axisRange().valid()) {
|
||||
drag = None;
|
||||
if (overview) {
|
||||
emit rangeRequested(domain.start, domain.end);
|
||||
} else {
|
||||
zoom(0.5, addressAt(event->pos().x()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisView::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
cancelTooltip();
|
||||
const QPoint delta = event->angleDelta();
|
||||
const int movement = delta.y() ? delta.y() : delta.x();
|
||||
if (!movement) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
pan(movement > 0 ? -1 : 1);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void DataAnalysisView::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
cancelTooltip();
|
||||
const RVA center = visibleRange.start + visibleRange.size() / 2;
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Plus:
|
||||
case Qt::Key_Equal:
|
||||
zoom(0.5, center);
|
||||
break;
|
||||
case Qt::Key_Minus:
|
||||
zoom(2, center);
|
||||
break;
|
||||
case Qt::Key_Left:
|
||||
pan(-1);
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
pan(1);
|
||||
break;
|
||||
case Qt::Key_Home:
|
||||
emit rangeRequested(domain.start, domain.end);
|
||||
break;
|
||||
case Qt::Key_Escape:
|
||||
drag = None;
|
||||
emit selectionRequested(0, 0);
|
||||
update();
|
||||
break;
|
||||
default:
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void DataAnalysisView::leaveEvent(QEvent *)
|
||||
{
|
||||
cancelTooltip();
|
||||
hoverX = -1;
|
||||
unsetCursor();
|
||||
QToolTip::hideText();
|
||||
update();
|
||||
}
|
||||
|
||||
void DataAnalysisView::cancelTooltip()
|
||||
{
|
||||
tooltipTimer.stop();
|
||||
QToolTip::hideText();
|
||||
}
|
||||
|
||||
void DataAnalysisView::hideEvent(QHideEvent *event)
|
||||
{
|
||||
cancelTooltip();
|
||||
QWidget::hideEvent(event);
|
||||
}
|
||||
|
||||
RVA DataAnalysisView::nearestMarker(qreal x) const
|
||||
{
|
||||
qreal distance = 5;
|
||||
RVA result = addressAt(x);
|
||||
for (const auto &marker : markers) {
|
||||
if (markerEnabled[marker.type] && axisRange().contains(marker.range.start)) {
|
||||
const qreal candidate = std::abs(x - xForAddress(marker.range.start));
|
||||
if (candidate < distance) {
|
||||
distance = candidate;
|
||||
result = marker.range.start;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void DataAnalysisView::showTooltip()
|
||||
{
|
||||
if (!isVisible() || drag != None || !plotRect().contains(hoverPosition)) {
|
||||
return;
|
||||
}
|
||||
const RVA address = addressAt(hoverPosition.x());
|
||||
if (address == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
QString tip = RAddressString(address);
|
||||
for (const auto &sample : samples) {
|
||||
if (!sample.range.contains(address)) {
|
||||
continue;
|
||||
}
|
||||
tip += tr("\nBucket: %1 – %2 (%3 bytes)\nRead %4 of %5 mapped bytes")
|
||||
.arg(RAddressString(sample.range.start), RAddressString(sample.range.end - 1))
|
||||
.arg(sample.range.size())
|
||||
.arg(sample.bytes)
|
||||
.arg(sample.covered);
|
||||
if (sample.bytes) {
|
||||
for (int metric : metrics) {
|
||||
tip += QString("\n%1: %2")
|
||||
.arg(metricName(metric), metricValue(metric, sample.values[metric]));
|
||||
}
|
||||
} else {
|
||||
tip += tr("\nNo readable data");
|
||||
}
|
||||
break;
|
||||
}
|
||||
int found = 0;
|
||||
for (const auto &marker : markers) {
|
||||
if (markerEnabled[marker.type]
|
||||
&& (marker.range.contains(address)
|
||||
|| std::abs(xForAddress(marker.range.start) - hoverPosition.x()) < 5)) {
|
||||
tip += QString("\n%1: %2 @ %3")
|
||||
.arg(
|
||||
markerName(marker.type),
|
||||
marker.name.left(160),
|
||||
RAddressString(marker.range.start));
|
||||
if (++found == 12) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
QToolTip::showText(mapToGlobal(hoverPosition), tip.toHtmlEscaped().replace('\n', "<br>"), this);
|
||||
}
|
||||
|
||||
void DataAnalysisView::contextMenuEvent(QContextMenuEvent *event)
|
||||
{
|
||||
cancelTooltip();
|
||||
if (axisRange().valid()) {
|
||||
emit contextRequested(nearestMarker(event->pos().x()), event->globalPos());
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#ifndef DATAANALYSISVIEW_H
|
||||
#define DATAANALYSISVIEW_H
|
||||
|
||||
#include "common/DataAnalysis.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
class DataAnalysisView : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum MarkerType { Section, Segment, Map, Flag, Comment, MarkerCount };
|
||||
enum Style { Lines, Areas, Bars };
|
||||
struct Marker
|
||||
{
|
||||
DataAnalysis::Range range;
|
||||
QString name;
|
||||
MarkerType type;
|
||||
};
|
||||
|
||||
explicit DataAnalysisView(bool overview, QWidget *parent = nullptr);
|
||||
static QString metricName(int metric);
|
||||
static QString metricDescription(int metric);
|
||||
static QString metricValue(int metric, double value);
|
||||
static QString markerName(MarkerType type);
|
||||
|
||||
DataAnalysis::Range domain;
|
||||
DataAnalysis::Range visibleRange;
|
||||
DataAnalysis::Range selection;
|
||||
QVector<DataAnalysis::Sample> samples;
|
||||
QVector<Marker> markers;
|
||||
QVector<int> metrics = {DataAnalysis::Entropy, DataAnalysis::Printable, DataAnalysis::Zero};
|
||||
std::array<QColor, DataAnalysis::Count> colors;
|
||||
std::array<bool, MarkerCount> markerEnabled = {true, false, false, true, true};
|
||||
RVA seekAddress = RVA_INVALID;
|
||||
Style style = Areas;
|
||||
bool separateTracks = false;
|
||||
bool loading = false;
|
||||
|
||||
QRectF plotRect() const;
|
||||
RVA addressAt(qreal x) const;
|
||||
qreal xForAddress(RVA address) const;
|
||||
void zoom(double factor, RVA anchor);
|
||||
void pan(int direction);
|
||||
|
||||
signals:
|
||||
void seekRequested(RVA address);
|
||||
void selectionRequested(RVA start, RVA end); // Half-open.
|
||||
void rangeRequested(RVA start, RVA end); // Half-open.
|
||||
void contextRequested(RVA address, QPoint globalPosition);
|
||||
|
||||
protected:
|
||||
bool event(QEvent *) override;
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void mousePressEvent(QMouseEvent *) override;
|
||||
void mouseMoveEvent(QMouseEvent *) override;
|
||||
void mouseReleaseEvent(QMouseEvent *) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *) override;
|
||||
void wheelEvent(QWheelEvent *) override;
|
||||
void keyPressEvent(QKeyEvent *) override;
|
||||
void leaveEvent(QEvent *) override;
|
||||
void hideEvent(QHideEvent *) override;
|
||||
void contextMenuEvent(QContextMenuEvent *) override;
|
||||
|
||||
private:
|
||||
bool overview;
|
||||
enum Drag { None, Select, Move, LeftEdge, RightEdge };
|
||||
Drag drag = None;
|
||||
QPoint pressPosition;
|
||||
RVA pressAddress = 0;
|
||||
DataAnalysis::Range pressRange;
|
||||
DataAnalysis::Range dragSelection;
|
||||
int hoverX = -1;
|
||||
QTimer tooltipTimer;
|
||||
QPoint hoverPosition;
|
||||
|
||||
DataAnalysis::Range axisRange() const;
|
||||
RVA nearestMarker(qreal x) const;
|
||||
void pinchZoom(qreal scaleFactor, qreal x);
|
||||
void updateDrag(QPoint position);
|
||||
void showTooltip();
|
||||
void cancelTooltip();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,966 @@
|
||||
#include "DataAnalysisWidget.h"
|
||||
#include "common/IaitoSeekable.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "dialogs/CommentsDialog.h"
|
||||
#include "dialogs/FlagDialog.h"
|
||||
#include "ui_DataAnalysisWidget.h"
|
||||
|
||||
#include <QActionGroup>
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QColorDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFileDialog>
|
||||
#include <QFormLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSaveFile>
|
||||
#include <QScrollArea>
|
||||
#include <QSet>
|
||||
#include <QTextStream>
|
||||
#include <QToolButton>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace DataAnalysis;
|
||||
|
||||
namespace {
|
||||
RVA number(const QJsonObject &object, const char *key)
|
||||
{
|
||||
return object[QLatin1String(key)].toVariant().toULongLong();
|
||||
}
|
||||
|
||||
QByteArray readData(RVA address, int length)
|
||||
{
|
||||
auto core = Core()->core();
|
||||
QByteArray bytes(length, '\0');
|
||||
int count = 0;
|
||||
while (count < length) {
|
||||
// nread distinguishes short/failed reads from actual 0xff bytes. A read
|
||||
// can end at a submap boundary even when the following map is readable.
|
||||
const int n = r_io_nread_at(
|
||||
core->io,
|
||||
address + count,
|
||||
reinterpret_cast<ut8 *>(bytes.data()) + count,
|
||||
length - count);
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
count += n;
|
||||
}
|
||||
bytes.resize(count);
|
||||
return bytes;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DataAnalysisWidget::DataAnalysisWidget(MainWindow *main)
|
||||
: AddressableDockWidget(main)
|
||||
, ui(new Ui::DataAnalysisWidget)
|
||||
{
|
||||
setObjectName("DataAnalysisWidget");
|
||||
updateWindowTitle();
|
||||
auto *contents = new QWidget(this);
|
||||
ui->setupUi(contents);
|
||||
ui->legend->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
setWidget(contents);
|
||||
plot = new DataAnalysisView(false, contents);
|
||||
plot->setObjectName("analysisPlot");
|
||||
overview = new DataAnalysisView(true, contents);
|
||||
overview->setObjectName("analysisOverview");
|
||||
overview->setFixedHeight(80);
|
||||
auto *scroll = new QScrollArea(contents);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
scroll->setWidget(plot);
|
||||
ui->plotLayout->addWidget(scroll, 1);
|
||||
ui->plotLayout->addWidget(overview);
|
||||
ui->verticalLayout->setStretch(3, 1);
|
||||
setupActions();
|
||||
updateColors();
|
||||
|
||||
refreshDeferrer = createRefreshDeferrer([this]() { reloadTimer.start(); });
|
||||
reloadTimer.setSingleShot(true);
|
||||
reloadTimer.setInterval(100);
|
||||
rangeTimer.setSingleShot(true);
|
||||
rangeTimer.setInterval(80);
|
||||
sampleTimer.setInterval(0);
|
||||
connect(&reloadTimer, &QTimer::timeout, this, &DataAnalysisWidget::reload);
|
||||
connect(&rangeTimer, &QTimer::timeout, this, [this]() {
|
||||
if (!samplingOverview) {
|
||||
beginSampling(false);
|
||||
}
|
||||
});
|
||||
connect(&sampleTimer, &QTimer::timeout, this, &DataAnalysisWidget::sampleBatch);
|
||||
connect(plot, &DataAnalysisView::seekRequested, seekable, &IaitoSeekable::seek);
|
||||
connect(plot, &DataAnalysisView::selectionRequested, this, [this](RVA start, RVA end) {
|
||||
selectRange({start, end}, true);
|
||||
});
|
||||
for (auto *view : {plot, overview}) {
|
||||
connect(view, &DataAnalysisView::rangeRequested, this, [this](RVA start, RVA end) {
|
||||
setRange({start, end});
|
||||
});
|
||||
connect(view, &DataAnalysisView::contextRequested, this, &DataAnalysisWidget::showContextMenu);
|
||||
}
|
||||
connect(seekable, &IaitoSeekable::seekableSeekChanged, this, &DataAnalysisWidget::onSeekChanged);
|
||||
connect(Core(), &IaitoCore::addressRangeSelectionChanged, this, [this](RVA start, RVA end) {
|
||||
if (!publishingSelection && seekable->isSynchronized()
|
||||
&& Config()->getAddressRangeSelectionSyncEnabled()) {
|
||||
selectRange(
|
||||
start != RVA_INVALID && end != RVA_INVALID && end >= start ? Range{start, end + 1}
|
||||
: Range{},
|
||||
false);
|
||||
}
|
||||
});
|
||||
connect(Core(), &IaitoCore::refreshAll, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Core(), &IaitoCore::codeRebased, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Core(), &IaitoCore::ioModeChanged, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Core(), &IaitoCore::ioCacheChanged, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Core(), &IaitoCore::flagsChanged, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Core(), &IaitoCore::commentsChanged, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Core(), &IaitoCore::refreshCodeViews, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Core(), &IaitoCore::instructionChanged, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Core(), &IaitoCore::debugTaskStateChanged, this, &DataAnalysisWidget::scheduleReload);
|
||||
connect(Config(), &Configuration::colorsUpdated, this, &DataAnalysisWidget::updateColors);
|
||||
connect(this, &QDockWidget::visibilityChanged, this, [this](bool visible) {
|
||||
if (!visible) {
|
||||
metadataDirty |= sampleTimer.isActive() || rangeTimer.isActive();
|
||||
sampleTimer.stop();
|
||||
rangeTimer.stop();
|
||||
reloadTimer.stop();
|
||||
} else if (metadataDirty) {
|
||||
scheduleReload();
|
||||
}
|
||||
});
|
||||
scheduleReload();
|
||||
}
|
||||
|
||||
DataAnalysisWidget::~DataAnalysisWidget() = default;
|
||||
|
||||
QWidget *DataAnalysisWidget::widgetToFocusOnRaise()
|
||||
{
|
||||
return plot;
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::setupActions()
|
||||
{
|
||||
auto menuButton = [this](const QString &label, QMenu *menu) {
|
||||
auto *button = new QToolButton(ui->toolbar);
|
||||
button->setText(label);
|
||||
button->setPopupMode(QToolButton::InstantPopup);
|
||||
button->setMenu(menu);
|
||||
ui->toolbar->addWidget(button);
|
||||
};
|
||||
auto *ranges = new QMenu(this);
|
||||
ranges->addAction(tr("All data"), this, [this]() { setRange(plot->domain); });
|
||||
ranges->addAction(tr("Current section"), this, [this]() {
|
||||
fitMarker(DataAnalysisView::Section);
|
||||
});
|
||||
ranges->addAction(tr("Current segment"), this, [this]() {
|
||||
fitMarker(DataAnalysisView::Segment);
|
||||
});
|
||||
ranges->addAction(tr("Current map"), this, [this]() { fitMarker(DataAnalysisView::Map); });
|
||||
ranges->addAction(tr("Current block"), this, [this]() {
|
||||
const int size = Core()->core()->blocksize;
|
||||
setRange(sizedRange(seekable->getOffset(), qMax(1, size)));
|
||||
});
|
||||
selectionAction
|
||||
= ranges->addAction(tr("Fit selection"), this, &DataAnalysisWidget::fitSelection);
|
||||
selectionAction->setEnabled(false);
|
||||
ranges->addSeparator();
|
||||
ranges->addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
|
||||
menuButton(tr("Range"), ranges);
|
||||
auto *algorithms = new QMenu(this);
|
||||
for (int i = 0; i < Count; ++i) {
|
||||
auto *action = algorithms->addAction(DataAnalysisView::metricName(i));
|
||||
action->setCheckable(true);
|
||||
action->setChecked(plot->metrics.contains(i));
|
||||
metricActions[i] = action;
|
||||
action->setToolTip(DataAnalysisView::metricDescription(i));
|
||||
auto *entry = ui->legend->addAction(DataAnalysisView::metricName(i));
|
||||
entry->setToolTip(
|
||||
DataAnalysisView::metricDescription(i) + tr("\nClick to configure this algorithm."));
|
||||
legendActions[i] = entry;
|
||||
connect(entry, &QAction::triggered, this, [this, i]() { configureMetric(i); });
|
||||
connect(action, &QAction::toggled, this, &DataAnalysisWidget::updateMetrics);
|
||||
}
|
||||
menuButton(tr("Algorithms"), algorithms);
|
||||
ui->toolbar
|
||||
->addAction(
|
||||
tr("+"),
|
||||
this,
|
||||
[this]() { plot->zoom(0.5, plot->visibleRange.start + plot->visibleRange.size() / 2); })
|
||||
->setToolTip(tr("Zoom in (+)"));
|
||||
ui->toolbar
|
||||
->addAction(
|
||||
tr("−"),
|
||||
this,
|
||||
[this]() { plot->zoom(2, plot->visibleRange.start + plot->visibleRange.size() / 2); })
|
||||
->setToolTip(tr("Zoom out (-)"));
|
||||
ui->toolbar->addAction(tr("Fit"), this, [this]() { setRange(plot->domain); })
|
||||
->setToolTip(tr("Fit all data (Home)"));
|
||||
followAction = ui->toolbar->addAction(tr("Follow"));
|
||||
followAction->setCheckable(true);
|
||||
followAction->setChecked(true);
|
||||
followAction->setToolTip(tr("Pan to keep the current disassembly or hexdump address visible"));
|
||||
connect(followAction, &QAction::toggled, this, [this](bool checked) {
|
||||
if (checked) {
|
||||
onSeekChanged(seekable->getOffset());
|
||||
}
|
||||
});
|
||||
|
||||
optionsMenu = new QMenu(this);
|
||||
tracksAction = optionsMenu->addAction(tr("Separate algorithm tracks"));
|
||||
tracksAction->setCheckable(true);
|
||||
connect(tracksAction, &QAction::toggled, this, [this](bool checked) {
|
||||
plot->separateTracks = checked;
|
||||
plot->setMinimumHeight(checked ? qMax(180, int(plot->metrics.size()) * 90) : 180);
|
||||
plot->update();
|
||||
});
|
||||
auto *styles = optionsMenu->addMenu(tr("Drawing style"));
|
||||
auto *styleGroup = new QActionGroup(styles);
|
||||
const QStringList styleNames = {tr("Lines"), tr("Filled lines"), tr("Vertical bars")};
|
||||
for (int i = 0; i < styleNames.size(); ++i) {
|
||||
auto *action = styles->addAction(styleNames[i]);
|
||||
styleGroup->addAction(action);
|
||||
action->setCheckable(true);
|
||||
action->setData(i);
|
||||
action->setChecked(i == plot->style);
|
||||
connect(action, &QAction::triggered, this, [this, i]() {
|
||||
plot->style = static_cast<DataAnalysisView::Style>(i);
|
||||
plot->update();
|
||||
});
|
||||
}
|
||||
styles->setObjectName("analysisStyles");
|
||||
auto *colors = optionsMenu->addMenu(tr("Colors"));
|
||||
auto *colorGroup = new QActionGroup(colors);
|
||||
const QStringList modes
|
||||
= {tr("Greyscale"), tr("Rainbow"), tr("Theme single color"), tr("Theme palette")};
|
||||
for (int i = 0; i < modes.size(); ++i) {
|
||||
auto *action = colors->addAction(modes[i]);
|
||||
colorGroup->addAction(action);
|
||||
action->setCheckable(true);
|
||||
action->setData(i);
|
||||
action->setChecked(i == colorMode);
|
||||
connect(action, &QAction::triggered, this, [this, i]() {
|
||||
colorMode = i;
|
||||
updateColors();
|
||||
});
|
||||
}
|
||||
colors->setObjectName("analysisColors");
|
||||
colors->addSeparator();
|
||||
auto *custom = colors->addMenu(tr("Customize algorithm colors"));
|
||||
for (int i = 0; i < Count; ++i) {
|
||||
custom->addAction(DataAnalysisView::metricName(i), this, [this, i]() {
|
||||
const QColor color
|
||||
= QColorDialog::getColor(plot->colors[i], this, DataAnalysisView::metricName(i));
|
||||
if (color.isValid()) {
|
||||
customColors[i] = color;
|
||||
updateColors();
|
||||
}
|
||||
});
|
||||
}
|
||||
custom->addSeparator();
|
||||
custom->addAction(tr("Reset custom colors"), this, [this]() {
|
||||
customColors.fill(QColor());
|
||||
updateColors();
|
||||
});
|
||||
auto *markers = optionsMenu->addMenu(tr("Markers"));
|
||||
markers->setObjectName("analysisMarkers");
|
||||
for (int i = 0; i < DataAnalysisView::MarkerCount; ++i) {
|
||||
auto *action = markers->addAction(
|
||||
DataAnalysisView::markerName(static_cast<DataAnalysisView::MarkerType>(i)));
|
||||
action->setCheckable(true);
|
||||
action->setData(i);
|
||||
action->setChecked(plot->markerEnabled[i]);
|
||||
connect(action, &QAction::toggled, this, [this, i](bool checked) {
|
||||
plot->markerEnabled[i] = overview->markerEnabled[i] = checked;
|
||||
plot->update();
|
||||
overview->update();
|
||||
});
|
||||
}
|
||||
auto *samples = optionsMenu->addMenu(tr("Resolution (buckets)"));
|
||||
samples->setObjectName("analysisResolution");
|
||||
auto *sampleGroup = new QActionGroup(samples);
|
||||
for (int n : {32, 64, 128, 256, 512, 1024, 2048, 4096}) {
|
||||
auto *action = samples->addAction(QString::number(n));
|
||||
sampleGroup->addAction(action);
|
||||
action->setCheckable(true);
|
||||
action->setData(n);
|
||||
action->setChecked(n == resolution);
|
||||
connect(action, &QAction::triggered, this, [this, n]() {
|
||||
resolution = n;
|
||||
beginSampling(samplingOverview);
|
||||
});
|
||||
}
|
||||
optionsMenu->addSeparator();
|
||||
optionsMenu->addAction(&syncAction);
|
||||
optionsMenu->addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
|
||||
optionsMenu->addAction(tr("Refresh"), this, &DataAnalysisWidget::scheduleReload);
|
||||
exportAction = optionsMenu->addAction(
|
||||
tr("Export visible samples as CSV…"), this, &DataAnalysisWidget::exportCsv);
|
||||
exportAction->setEnabled(false);
|
||||
menuButton(tr("Options"), optionsMenu);
|
||||
ui->legend->setToolTip(
|
||||
tr("Click a legend entry to configure its color and visibility."
|
||||
" Overlaid values use a 0–100% scale; entropy is divided by 8."
|
||||
" Separate tracks use native units."));
|
||||
updateMetrics();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::updateMetrics()
|
||||
{
|
||||
plot->metrics.clear();
|
||||
for (int i = 0; i < Count; ++i) {
|
||||
legendActions[i]->setVisible(metricActions[i]->isChecked());
|
||||
if (metricActions[i]->isChecked()) {
|
||||
plot->metrics.append(i);
|
||||
}
|
||||
}
|
||||
overview->metrics = plot->metrics;
|
||||
plot->setMinimumHeight(plot->separateTracks ? qMax(180, int(plot->metrics.size()) * 90) : 180);
|
||||
plot->update();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::updateColors()
|
||||
{
|
||||
const char *keys[]
|
||||
= {"gui.navbar.code",
|
||||
"gui.navbar.str",
|
||||
"gui.navbar.data",
|
||||
"num",
|
||||
"flag",
|
||||
"comment",
|
||||
"flow",
|
||||
"func_var",
|
||||
"reg",
|
||||
"invalid",
|
||||
"other"};
|
||||
QColor base = Config()->getColor("gui.navbar.code");
|
||||
if (!base.isValid()) {
|
||||
base = QColor(120, 150, 230);
|
||||
}
|
||||
QSet<QRgb> used;
|
||||
for (int i = 0; i < Count; ++i) {
|
||||
QColor color;
|
||||
switch (colorMode) {
|
||||
case 0:
|
||||
color = QColor::fromHsv(0, 0, 85 + i * 16);
|
||||
break;
|
||||
case 1:
|
||||
color = QColor::fromHsv((270 + i * 137) % 360, 165, 220);
|
||||
break;
|
||||
case 2:
|
||||
color = QColor::fromHsv(qMax(0, base.hue()), 70 + i * 16, 235 - i * 9);
|
||||
break;
|
||||
default:
|
||||
color = Config()->getColor(keys[i]);
|
||||
if (!color.isValid() || used.contains(color.rgb())
|
||||
|| color == palette().color(QPalette::Base)) {
|
||||
color = QColor::fromHsv((270 + i * 137) % 360, 155, 210);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (customColors[i].isValid()) {
|
||||
color = customColors[i];
|
||||
}
|
||||
used.insert(color.rgb());
|
||||
plot->colors[i] = overview->colors[i] = color;
|
||||
QPixmap swatch(12, 12);
|
||||
swatch.fill(color);
|
||||
metricActions[i]->setIcon(QIcon(swatch));
|
||||
legendActions[i]->setIcon(QIcon(swatch));
|
||||
}
|
||||
plot->update();
|
||||
overview->update();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::configureMetric(int metric)
|
||||
{
|
||||
QDialog dialog(this);
|
||||
dialog.setObjectName("analysisMetricSettings");
|
||||
dialog.setWindowTitle(tr("%1 settings").arg(DataAnalysisView::metricName(metric)));
|
||||
QFormLayout layout(&dialog);
|
||||
QLabel description(DataAnalysisView::metricDescription(metric));
|
||||
description.setWordWrap(true);
|
||||
description.setMaximumWidth(420);
|
||||
QCheckBox visible(tr("Show this algorithm"));
|
||||
visible.setObjectName("metricVisible");
|
||||
visible.setChecked(metricActions[metric]->isChecked());
|
||||
QColor chosen = customColors[metric];
|
||||
QPushButton colorButton(tr("Change color…"));
|
||||
colorButton.setIcon(legendActions[metric]->icon());
|
||||
QPushButton resetButton(tr("Use palette color"));
|
||||
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
layout.addRow(&description);
|
||||
layout.addRow(tr("Color:"), &colorButton);
|
||||
layout.addRow(&resetButton);
|
||||
layout.addRow(&visible);
|
||||
layout.addRow(&buttons);
|
||||
connect(&colorButton, &QPushButton::clicked, &dialog, [&]() {
|
||||
const QColor color = QColorDialog::getColor(
|
||||
chosen.isValid() ? chosen : plot->colors[metric],
|
||||
&dialog,
|
||||
DataAnalysisView::metricName(metric));
|
||||
if (color.isValid()) {
|
||||
chosen = color;
|
||||
QPixmap swatch(12, 12);
|
||||
swatch.fill(color);
|
||||
colorButton.setIcon(QIcon(swatch));
|
||||
colorButton.setText(color.name());
|
||||
}
|
||||
});
|
||||
connect(&resetButton, &QPushButton::clicked, &dialog, [&]() {
|
||||
chosen = QColor();
|
||||
colorButton.setIcon(QIcon());
|
||||
colorButton.setText(tr("Palette default"));
|
||||
});
|
||||
connect(&buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
customColors[metric] = chosen;
|
||||
metricActions[metric]->setChecked(visible.isChecked());
|
||||
updateColors();
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::scheduleReload()
|
||||
{
|
||||
metadataDirty = true;
|
||||
if (refreshDeferrer->attemptRefresh(nullptr)) {
|
||||
sampleTimer.stop();
|
||||
reloadTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::loadMetadata()
|
||||
{
|
||||
using View = DataAnalysisView;
|
||||
virtualAddresses = Core()->getConfigb("io.va");
|
||||
readable.clear();
|
||||
plot->markers.clear();
|
||||
auto addMarker = [this](Range range, const QString &name, View::MarkerType type) {
|
||||
if (range.valid()) {
|
||||
plot->markers.append({range, name, type});
|
||||
}
|
||||
};
|
||||
for (const auto &value : Core()->cmdj("omj").array()) {
|
||||
const QJsonObject map = value.toObject();
|
||||
const RVA start = number(map, "from"), last = number(map, "to");
|
||||
const Range range = last >= start ? Range{start, last == RVA_MAX ? RVA_MAX : last + 1}
|
||||
: Range{};
|
||||
if (virtualAddresses) {
|
||||
addMarker(range, map["name"].toString(), View::Map);
|
||||
if (range.valid() && map["perm"].toString().contains('r')) {
|
||||
readable.append(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!virtualAddresses) {
|
||||
auto core = Core()->core();
|
||||
const RVA size = r_io_size(core->io);
|
||||
if (size != RVA_MAX && size) {
|
||||
readable.append({0, size});
|
||||
}
|
||||
}
|
||||
readable = mergeRanges(readable);
|
||||
const Range domain = readable.isEmpty() ? Range{}
|
||||
: Range{readable.first().start, readable.last().end};
|
||||
const bool changed = domain != plot->domain;
|
||||
plot->domain = overview->domain = domain;
|
||||
if (changed || !plot->visibleRange.valid()) {
|
||||
plot->visibleRange = overview->visibleRange = domain;
|
||||
}
|
||||
for (const auto &entry :
|
||||
{qMakePair(QString("iSj"), View::Section), qMakePair(QString("iSSj"), View::Segment)}) {
|
||||
const QJsonDocument document = Core()->cmdj(entry.first);
|
||||
const QJsonArray objects
|
||||
= document.isArray()
|
||||
? document.array()
|
||||
: document.object()[entry.second == View::Section ? "sections" : "segments"]
|
||||
.toArray();
|
||||
for (const auto &value : objects) {
|
||||
const QJsonObject object = value.toObject();
|
||||
const RVA start = number(object, virtualAddresses ? "vaddr" : "paddr");
|
||||
const RVA size = number(object, virtualAddresses ? "vsize" : "size");
|
||||
addMarker(sizedRange(start, size), object["name"].toString(), entry.second);
|
||||
}
|
||||
}
|
||||
// @F temporarily selects all flag spaces without changing the user's space.
|
||||
for (const auto &value : Core()->cmdj("fj @F:*").array()) {
|
||||
const QJsonObject flag = value.toObject();
|
||||
const RVA address = number(flag, flag.contains("offset") ? "offset" : "addr");
|
||||
addMarker(
|
||||
sizedRange(address, qMax<RVA>(1, number(flag, "size"))),
|
||||
flag["name"].toString(),
|
||||
View::Flag);
|
||||
}
|
||||
for (const auto &comment : Core()->getAllComments("CCu")) {
|
||||
addMarker(sizedRange(comment.offset, 1), comment.name, View::Comment);
|
||||
}
|
||||
std::stable_sort(
|
||||
plot->markers.begin(),
|
||||
plot->markers.end(),
|
||||
[](const View::Marker &a, const View::Marker &b) { return a.range.start < b.range.start; });
|
||||
overview->markers = plot->markers;
|
||||
metadataDirty = false;
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::reload()
|
||||
{
|
||||
if (!refreshDeferrer->attemptRefresh(nullptr) || Core()->isDebugTaskInProgress()) {
|
||||
return;
|
||||
}
|
||||
sampleTimer.stop();
|
||||
rangeTimer.stop();
|
||||
loadMetadata();
|
||||
plot->seekAddress = overview->seekAddress = seekable->getOffset();
|
||||
if (requestedRange.valid()) {
|
||||
const Range requested = intersect(requestedRange, plot->domain);
|
||||
if (requested.valid()) {
|
||||
plot->visibleRange = overview->visibleRange = requested;
|
||||
}
|
||||
requestedRange = {};
|
||||
}
|
||||
if (seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()
|
||||
&& Core()->hasAddressRangeSelection()) {
|
||||
selectRange(
|
||||
{Core()->getAddressRangeSelectionStart(), Core()->getAddressRangeSelectionEnd() + 1},
|
||||
false);
|
||||
}
|
||||
plot->samples.clear();
|
||||
overview->samples.clear();
|
||||
beginSampling(true);
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::beginSampling(bool mini)
|
||||
{
|
||||
rangeTimer.stop();
|
||||
sampleTimer.stop();
|
||||
if (!isVisibleToUser()) {
|
||||
metadataDirty = true;
|
||||
refreshDeferrer->attemptRefresh(nullptr);
|
||||
return;
|
||||
}
|
||||
samplingOverview = mini;
|
||||
pendingRange = mini ? plot->domain : plot->visibleRange;
|
||||
pending.clear();
|
||||
nextBucket = 0;
|
||||
plot->loading = pendingRange.valid();
|
||||
exportAction->setEnabled(false);
|
||||
if (pendingRange.valid()) {
|
||||
const int count = int(qMin<RVA>(mini ? 256 : resolution, pendingRange.size()));
|
||||
pending.resize(count);
|
||||
sampleTimer.start();
|
||||
}
|
||||
updateLabels();
|
||||
plot->update();
|
||||
overview->update();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::sampleBatch()
|
||||
{
|
||||
if (!isVisibleToUser() || Core()->isDebugTaskInProgress()) {
|
||||
sampleTimer.stop();
|
||||
metadataDirty = true;
|
||||
refreshDeferrer->attemptRefresh(nullptr);
|
||||
return;
|
||||
}
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
// Yield to navigation and painting every few milliseconds. A range change
|
||||
// cancels the old job; no stale worker result can overwrite the new range.
|
||||
do {
|
||||
pending[nextBucket]
|
||||
= sample(bucket(pendingRange, nextBucket, pending.size()), readable, readData);
|
||||
++nextBucket;
|
||||
} while (nextBucket < pending.size() && timer.elapsed() < 6);
|
||||
if (nextBucket < pending.size()) {
|
||||
ui->statusLabel->setText(tr("Analyzing %1… %2%")
|
||||
.arg(samplingOverview ? tr("overview") : tr("visible range"))
|
||||
.arg(nextBucket * 100 / pending.size()));
|
||||
return;
|
||||
}
|
||||
sampleTimer.stop();
|
||||
if (samplingOverview) {
|
||||
overview->samples = pending;
|
||||
overview->update();
|
||||
beginSampling(false);
|
||||
} else {
|
||||
plot->samples = pending;
|
||||
plot->loading = false;
|
||||
exportAction->setEnabled(!plot->samples.isEmpty());
|
||||
updateLabels();
|
||||
plot->update();
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::setRange(Range range)
|
||||
{
|
||||
range = intersect(range, plot->domain);
|
||||
if (!range.valid() || range == plot->visibleRange) {
|
||||
return;
|
||||
}
|
||||
plot->visibleRange = overview->visibleRange = range;
|
||||
plot->samples.clear();
|
||||
plot->loading = true;
|
||||
exportAction->setEnabled(false);
|
||||
if (!samplingOverview) {
|
||||
sampleTimer.stop();
|
||||
}
|
||||
rangeTimer.start();
|
||||
updateLabels();
|
||||
plot->update();
|
||||
overview->update();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::showRange(RVA start, RVA end)
|
||||
{
|
||||
if (start == RVA_INVALID || end == RVA_INVALID || end < start) {
|
||||
return;
|
||||
}
|
||||
requestedRange = {start, end + 1};
|
||||
raiseMemoryWidget();
|
||||
seekable->seek(start);
|
||||
if (metadataDirty) {
|
||||
scheduleReload();
|
||||
} else {
|
||||
setRange(requestedRange);
|
||||
requestedRange = {};
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::onSeekChanged(RVA address)
|
||||
{
|
||||
plot->seekAddress = overview->seekAddress = address;
|
||||
if (followAction->isChecked() && plot->domain.contains(address)
|
||||
&& !plot->visibleRange.contains(address)) {
|
||||
setRange(centered(plot->domain, address, plot->visibleRange.size()));
|
||||
}
|
||||
updateLabels();
|
||||
plot->update();
|
||||
overview->update();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::selectRange(Range range, bool publish)
|
||||
{
|
||||
plot->selection = overview->selection = range;
|
||||
selectionAction->setEnabled(range.valid());
|
||||
if (publish && seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()) {
|
||||
publishingSelection = true;
|
||||
if (range.valid()) {
|
||||
Core()->setAddressRangeSelection(range.start, range.end - 1);
|
||||
} else {
|
||||
Core()->clearAddressRangeSelection();
|
||||
}
|
||||
publishingSelection = false;
|
||||
}
|
||||
updateLabels();
|
||||
plot->update();
|
||||
overview->update();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::updateLabels()
|
||||
{
|
||||
const Range range = plot->visibleRange;
|
||||
if (!range.valid()) {
|
||||
ui->rangeLabel->setText(tr("No readable data in the current address space"));
|
||||
ui->statusLabel->setText(tr("Open a file or refresh after mapping data."));
|
||||
return;
|
||||
}
|
||||
QString text = tr("%1: %2 – %3 · %4 bytes · Cursor %5")
|
||||
.arg(
|
||||
virtualAddresses ? tr("Addresses") : tr("File offsets"),
|
||||
RAddressString(range.start),
|
||||
RAddressString(range.end - 1))
|
||||
.arg(range.size())
|
||||
.arg(RAddressString(seekable->getOffset()));
|
||||
if (plot->selection.valid()) {
|
||||
text += tr("\nSelection: %1 – %2 · %3 bytes")
|
||||
.arg(
|
||||
RAddressString(plot->selection.start),
|
||||
RAddressString(plot->selection.end - 1))
|
||||
.arg(plot->selection.size());
|
||||
}
|
||||
ui->rangeLabel->setText(text);
|
||||
if (plot->loading) {
|
||||
ui->statusLabel->setText(tr("Analyzing…"));
|
||||
} else {
|
||||
RVA bytes = 0, covered = 0;
|
||||
for (const auto &sample : plot->samples) {
|
||||
bytes += sample.bytes;
|
||||
covered += sample.covered;
|
||||
}
|
||||
if (!bytes) {
|
||||
ui->statusLabel->setText(tr("No readable bytes in the visible range."));
|
||||
return;
|
||||
}
|
||||
ui->statusLabel->setText(
|
||||
tr("%1 buckets · %2 / %3 mapped bytes read (%4) · Overview: entropy")
|
||||
.arg(plot->samples.size())
|
||||
.arg(bytes)
|
||||
.arg(covered)
|
||||
.arg(bytes == covered ? tr("complete") : tr("sampled or unreadable")));
|
||||
ui->statusLabel->setToolTip(
|
||||
tr("Each bucket reads up to 4096 bytes from centered windows in readable spans."
|
||||
" Zoom in for complete coverage. Unmapped bytes are excluded."
|
||||
" Shannon entropy: 0–8 bits/byte. Overlays use normalized percentages."));
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::fitSelection()
|
||||
{
|
||||
if (plot->selection.valid()) {
|
||||
setRange(plot->selection);
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::fitMarker(DataAnalysisView::MarkerType type)
|
||||
{
|
||||
Range best;
|
||||
for (const auto &marker : plot->markers) {
|
||||
if (marker.type == type && marker.range.contains(seekable->getOffset())
|
||||
&& (!best.valid() || marker.range.size() < best.size())) {
|
||||
best = marker.range;
|
||||
}
|
||||
}
|
||||
if (best.valid()) {
|
||||
setRange(best);
|
||||
} else {
|
||||
ui->statusLabel->setText(
|
||||
tr("No %1 contains the current address.").arg(DataAnalysisView::markerName(type)));
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::editRange()
|
||||
{
|
||||
QDialog dialog(this);
|
||||
dialog.setWindowTitle(tr("Data analysis range"));
|
||||
QFormLayout layout(&dialog);
|
||||
QLineEdit start(RAddressString(plot->visibleRange.start));
|
||||
QLineEdit end(RAddressString(plot->visibleRange.valid() ? plot->visibleRange.end - 1 : 0));
|
||||
QLabel hint(tr("Enter addresses or radare2 expressions. Both endpoints are inclusive."));
|
||||
hint.setWordWrap(true);
|
||||
QLabel error;
|
||||
error.setWordWrap(true);
|
||||
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
layout.addRow(&hint);
|
||||
layout.addRow(tr("Start:"), &start);
|
||||
layout.addRow(tr("End (inclusive):"), &end);
|
||||
layout.addRow(&error);
|
||||
layout.addRow(&buttons);
|
||||
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
connect(&buttons, &QDialogButtonBox::accepted, &dialog, [&]() {
|
||||
RVA first, last;
|
||||
if (!Core()->tryMath(start.text(), first) || !Core()->tryMath(end.text(), last)
|
||||
|| first == RVA_INVALID || last == RVA_INVALID || first > last) {
|
||||
error.setText(tr("Enter a valid start and an end greater than or equal to it."));
|
||||
return;
|
||||
}
|
||||
const Range requested{first, last + 1};
|
||||
if (intersect(requested, plot->domain) != requested) {
|
||||
error.setText(
|
||||
tr("The range must be within %1 – %2.")
|
||||
.arg(RAddressString(plot->domain.start), RAddressString(plot->domain.end - 1)));
|
||||
return;
|
||||
}
|
||||
setRange(requested);
|
||||
dialog.accept();
|
||||
});
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::findMarker()
|
||||
{
|
||||
QDialog dialog(this);
|
||||
dialog.setWindowTitle(tr("Find section, map, flag or comment"));
|
||||
dialog.resize(620, 400);
|
||||
QVBoxLayout layout(&dialog);
|
||||
QLineEdit filter;
|
||||
filter.setPlaceholderText(tr("Filter by name, comment, type or hexadecimal address…"));
|
||||
QListWidget list;
|
||||
QLabel hint(
|
||||
tr("Activate a result to seek. Section, segment and map results also fit the range."
|
||||
" Showing up to 500 matches."));
|
||||
hint.setWordWrap(true);
|
||||
QDialogButtonBox buttons(QDialogButtonBox::Close);
|
||||
layout.addWidget(&filter);
|
||||
layout.addWidget(&list);
|
||||
layout.addWidget(&hint);
|
||||
layout.addWidget(&buttons);
|
||||
// Keep result indices stable while the modal loop receives metadata updates.
|
||||
const auto markers = plot->markers;
|
||||
auto fill = [&]() {
|
||||
list.clear();
|
||||
for (int i = 0; i < markers.size(); ++i) {
|
||||
const auto &marker = markers[i];
|
||||
const QString label = QString("%1 · %2 · %3")
|
||||
.arg(
|
||||
RAddressString(marker.range.start),
|
||||
DataAnalysisView::markerName(marker.type),
|
||||
marker.name);
|
||||
if (label.contains(filter.text(), Qt::CaseInsensitive)) {
|
||||
auto *item = new QListWidgetItem(label, &list);
|
||||
item->setData(Qt::UserRole, i);
|
||||
if (list.count() >= 500) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
connect(&filter, &QLineEdit::textChanged, &dialog, fill);
|
||||
connect(&list, &QListWidget::itemActivated, &dialog, [&](QListWidgetItem *item) {
|
||||
const int index = item->data(Qt::UserRole).toInt();
|
||||
if (index < 0 || index >= markers.size()) {
|
||||
return;
|
||||
}
|
||||
const auto marker = markers[index];
|
||||
seekable->seek(marker.range.start);
|
||||
if (marker.type <= DataAnalysisView::Map) {
|
||||
setRange(marker.range);
|
||||
} else if (!plot->visibleRange.contains(marker.range.start)) {
|
||||
setRange(centered(plot->domain, marker.range.start, plot->visibleRange.size()));
|
||||
}
|
||||
dialog.accept();
|
||||
});
|
||||
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
fill();
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::showContextMenu(RVA address, QPoint position)
|
||||
{
|
||||
QMenu menu(this);
|
||||
menu.addAction(RAddressString(address))->setEnabled(false);
|
||||
menu.addAction(tr("Seek here"), this, [this, address]() { seekable->seek(address); });
|
||||
if (mainWindow) {
|
||||
auto *showIn = mainWindow->createShowInMenu(&menu, address);
|
||||
showIn->setTitle(tr("Show in"));
|
||||
menu.addMenu(showIn);
|
||||
}
|
||||
menu.addAction(tr("Copy address"), this, [address]() {
|
||||
QApplication::clipboard()->setText(RAddressString(address));
|
||||
});
|
||||
menu.addAction(tr("Add or edit comment…"), this, [this, address]() {
|
||||
CommentsDialog::addOrEditComment(address, this);
|
||||
});
|
||||
const Range selection = plot->selection;
|
||||
const RVA flagSize = selection.contains(address) ? selection.end - address : 1;
|
||||
menu.addAction(tr("Add or edit flag…"), this, [this, address, flagSize]() {
|
||||
FlagDialog dialog(address, flagSize, this);
|
||||
dialog.exec();
|
||||
});
|
||||
menu.addSeparator();
|
||||
menu.addAction(selectionAction);
|
||||
menu.addAction(tr("Clear selection"), this, [this]() { selectRange({}, true); });
|
||||
menu.addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
|
||||
menu.addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
|
||||
menu.addAction(tr("Fit all data"), this, [this]() { setRange(plot->domain); });
|
||||
menu.addSeparator();
|
||||
menu.addAction(&syncAction);
|
||||
menu.exec(position);
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::exportCsv()
|
||||
{
|
||||
const auto samples = plot->samples;
|
||||
const auto metrics = plot->metrics;
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this, tr("Export data analysis"), QString(), tr("CSV files (*.csv)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::warning(this, tr("Export failed"), file.errorString());
|
||||
return;
|
||||
}
|
||||
QTextStream out(&file);
|
||||
out << "start,end_exclusive,bytes_read,mapped_bytes";
|
||||
for (int metric : metrics) {
|
||||
QString name = DataAnalysisView::metricName(metric);
|
||||
name.replace('"', "\"\"");
|
||||
out << ",\"" << name << " (normalized 0-1)\"";
|
||||
}
|
||||
out << '\n';
|
||||
for (const auto &sample : samples) {
|
||||
out << RAddressString(sample.range.start) << ',' << RAddressString(sample.range.end) << ','
|
||||
<< sample.bytes << ',' << sample.covered;
|
||||
for (int metric : metrics) {
|
||||
out << ',';
|
||||
if (sample.bytes) {
|
||||
out << QString::number(sample.values[metric], 'g', 12);
|
||||
}
|
||||
}
|
||||
out << '\n';
|
||||
}
|
||||
out.flush();
|
||||
if (!file.commit()) {
|
||||
QMessageBox::warning(this, tr("Export failed"), file.errorString());
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap DataAnalysisWidget::serializeViewProprties()
|
||||
{
|
||||
auto properties = AddressableDockWidget::serializeViewProprties();
|
||||
QVariantList metrics, colors, markers;
|
||||
for (int i = 0; i < Count; ++i) {
|
||||
if (metricActions[i]->isChecked()) {
|
||||
metrics.append(i);
|
||||
}
|
||||
colors.append(customColors[i].isValid() ? customColors[i].name() : QString());
|
||||
}
|
||||
for (bool enabled : plot->markerEnabled) {
|
||||
markers.append(enabled);
|
||||
}
|
||||
properties["metrics"] = metrics;
|
||||
properties["colors"] = colors;
|
||||
properties["markers"] = markers;
|
||||
properties["style"] = int(plot->style);
|
||||
properties["colorMode"] = colorMode;
|
||||
properties["resolution"] = resolution;
|
||||
properties["tracks"] = tracksAction->isChecked();
|
||||
properties["follow"] = followAction->isChecked();
|
||||
return properties;
|
||||
}
|
||||
|
||||
void DataAnalysisWidget::deserializeViewProperties(const QVariantMap &properties)
|
||||
{
|
||||
AddressableDockWidget::deserializeViewProperties(properties);
|
||||
const QVariantList metrics
|
||||
= properties.value("metrics", QVariantList{Entropy, Printable, Zero}).toList();
|
||||
const QVariantList colors = properties.value("colors").toList();
|
||||
for (int i = 0; i < Count; ++i) {
|
||||
metricActions[i]->setChecked(metrics.contains(i));
|
||||
customColors[i] = i < colors.size() ? QColor(colors[i].toString()) : QColor();
|
||||
}
|
||||
auto restoreMenu = [this](const char *name, int selected) {
|
||||
for (auto *action : optionsMenu->findChild<QMenu *>(name)->actions()) {
|
||||
if (action->isCheckable() && action->data().toInt() == selected) {
|
||||
action->trigger();
|
||||
}
|
||||
}
|
||||
};
|
||||
restoreMenu("analysisStyles", qBound(0, properties.value("style", 1).toInt(), 2));
|
||||
restoreMenu("analysisColors", qBound(0, properties.value("colorMode", 3).toInt(), 3));
|
||||
int count = properties.value("resolution", 1024).toInt();
|
||||
if (!QList<int>{32, 64, 128, 256, 512, 1024, 2048, 4096}.contains(count)) {
|
||||
count = 1024;
|
||||
}
|
||||
restoreMenu("analysisResolution", count);
|
||||
const QVariantList markers
|
||||
= properties.value("markers", QVariantList{true, false, false, true, true}).toList();
|
||||
for (auto *action : optionsMenu->findChild<QMenu *>("analysisMarkers")->actions()) {
|
||||
const int index = action->data().toInt();
|
||||
action->setChecked(index < markers.size() && markers[index].toBool());
|
||||
}
|
||||
tracksAction->setChecked(properties.value("tracks", false).toBool());
|
||||
followAction->setChecked(properties.value("follow", true).toBool());
|
||||
updateColors();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
#ifndef DATAANALYSISWIDGET_H
|
||||
#define DATAANALYSISWIDGET_H
|
||||
|
||||
#include "AddressableDockWidget.h"
|
||||
#include "DataAnalysisView.h"
|
||||
|
||||
#include <memory>
|
||||
#include <QTimer>
|
||||
|
||||
namespace Ui {
|
||||
class DataAnalysisWidget;
|
||||
}
|
||||
class QMenu;
|
||||
|
||||
class DataAnalysisWidget : public AddressableDockWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DataAnalysisWidget(MainWindow *main);
|
||||
~DataAnalysisWidget() override;
|
||||
QVariantMap serializeViewProprties() override;
|
||||
void deserializeViewProperties(const QVariantMap &properties) override;
|
||||
|
||||
// Reusable entry point for other widgets. Both endpoints are inclusive.
|
||||
void showRange(RVA start, RVA end);
|
||||
|
||||
protected:
|
||||
QString getWindowTitle() const override { return tr("Data Analysis"); }
|
||||
QWidget *widgetToFocusOnRaise() override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::DataAnalysisWidget> ui;
|
||||
DataAnalysisView *plot;
|
||||
DataAnalysisView *overview;
|
||||
RefreshDeferrer *refreshDeferrer;
|
||||
QTimer reloadTimer;
|
||||
QTimer sampleTimer;
|
||||
QTimer rangeTimer;
|
||||
QVector<DataAnalysis::Range> readable;
|
||||
QVector<DataAnalysis::Sample> pending;
|
||||
DataAnalysis::Range pendingRange;
|
||||
DataAnalysis::Range requestedRange;
|
||||
int nextBucket = 0;
|
||||
int resolution = 1024;
|
||||
int colorMode = 3;
|
||||
bool samplingOverview = false;
|
||||
bool virtualAddresses = true;
|
||||
bool metadataDirty = true;
|
||||
bool publishingSelection = false;
|
||||
QAction *followAction;
|
||||
QAction *tracksAction;
|
||||
QAction *selectionAction;
|
||||
QAction *exportAction;
|
||||
QMenu *optionsMenu;
|
||||
std::array<QAction *, DataAnalysis::Count> metricActions;
|
||||
std::array<QAction *, DataAnalysis::Count> legendActions;
|
||||
std::array<QColor, DataAnalysis::Count> customColors;
|
||||
|
||||
void setupActions();
|
||||
void scheduleReload();
|
||||
void reload();
|
||||
void loadMetadata();
|
||||
void beginSampling(bool mini);
|
||||
void sampleBatch();
|
||||
void setRange(DataAnalysis::Range range);
|
||||
void updateLabels();
|
||||
void updateColors();
|
||||
void updateMetrics();
|
||||
void configureMetric(int metric);
|
||||
void onSeekChanged(RVA address);
|
||||
void selectRange(DataAnalysis::Range range, bool publish);
|
||||
void fitSelection();
|
||||
void editRange();
|
||||
void fitMarker(DataAnalysisView::MarkerType type);
|
||||
void findMarker();
|
||||
void showContextMenu(RVA address, QPoint position);
|
||||
void exportCsv();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DataAnalysisWidget</class>
|
||||
<widget class="QWidget" name="DataAnalysisWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing"><number>4</number></property>
|
||||
<property name="leftMargin"><number>4</number></property>
|
||||
<property name="topMargin"><number>4</number></property>
|
||||
<property name="rightMargin"><number>4</number></property>
|
||||
<property name="bottomMargin"><number>4</number></property>
|
||||
<item><widget class="QToolBar" name="toolbar"><property name="movable"><bool>false</bool></property></widget></item>
|
||||
<item><widget class="QLabel" name="rangeLabel">
|
||||
<property name="text"><string>Open a file to analyze its data</string></property>
|
||||
<property name="wordWrap"><bool>true</bool></property>
|
||||
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
|
||||
</widget></item>
|
||||
<item><widget class="QToolBar" name="legend"><property name="movable"><bool>false</bool></property></widget></item>
|
||||
<item><layout class="QVBoxLayout" name="plotLayout"/></item>
|
||||
<item><widget class="QLabel" name="statusLabel">
|
||||
<property name="wordWrap"><bool>true</bool></property>
|
||||
<property name="text"><string>Click to seek · Drag to select · Wheel or pinch to zoom · Shift+wheel to pan</string></property>
|
||||
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
|
||||
</widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user