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 |
No files matched your search
+118
-56
@@ -1,12 +1,18 @@
|
||||
name: iaito CI
|
||||
|
||||
env:
|
||||
R2V: 6.2.2
|
||||
R2GHIDRA_VERSION: 6.2.2
|
||||
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,8 +21,51 @@ 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:
|
||||
@@ -35,34 +84,31 @@ 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@v7
|
||||
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@v7
|
||||
@@ -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,7 +138,8 @@ 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:
|
||||
@@ -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,6 +185,7 @@ 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@v7
|
||||
@@ -196,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
|
||||
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
|
||||
- uses: actions/setup-python@v7
|
||||
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
|
||||
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
|
||||
@@ -264,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
|
||||
@@ -286,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
|
||||
@@ -322,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
|
||||
@@ -410,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:
|
||||
@@ -440,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:
|
||||
@@ -489,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.2' ; VERSION_MAJOR=6; VERSION_MINOR=2; VERSION_PATCH=2; VERSION_NUMBER=60202; 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.2 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.2"
|
||||
echo "6.2.6"
|
||||
exit 0
|
||||
fi
|
||||
echo "iaito-6.2.2 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.2"
|
||||
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.2
|
||||
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.
|
||||
+32
-4
@@ -4,7 +4,7 @@ TARGET = iaito
|
||||
|
||||
IAITO_VERSION_MAJOR = 6
|
||||
IAITO_VERSION_MINOR = 2
|
||||
IAITO_VERSION_PATCH = 2
|
||||
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 \
|
||||
@@ -255,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 \
|
||||
@@ -314,6 +319,7 @@ SOURCES += \
|
||||
common/HighDpiPixmap.cpp \
|
||||
widgets/GraphGridLayout.cpp \
|
||||
widgets/HexWidget.cpp \
|
||||
widgets/HexPatWidget.cpp \
|
||||
widgets/AddressScrollBar.cpp \
|
||||
widgets/R2AIWidget.cpp \
|
||||
widgets/R2McpWidget.cpp \
|
||||
@@ -333,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 \
|
||||
@@ -355,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 \
|
||||
@@ -408,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 \
|
||||
@@ -421,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 \
|
||||
@@ -434,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 \
|
||||
@@ -459,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 \
|
||||
@@ -526,6 +544,8 @@ 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 \
|
||||
@@ -542,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 \
|
||||
@@ -567,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 \
|
||||
@@ -595,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 \
|
||||
@@ -627,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)
|
||||
@@ -535,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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -154,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
|
||||
@@ -687,14 +687,15 @@ void Configuration::setVisualNavbarResolution(int divisor)
|
||||
Configuration::MemoryScrollBarMode Configuration::getMemoryScrollBarMode() const
|
||||
{
|
||||
const int mode
|
||||
= s.value("memoryScrollBarMode", static_cast<int>(MemoryScrollBarMode::Hidden)).toInt();
|
||||
= 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::Bounded):
|
||||
return MemoryScrollBarMode::Bounded;
|
||||
default:
|
||||
case static_cast<int>(MemoryScrollBarMode::Hidden):
|
||||
return MemoryScrollBarMode::Hidden;
|
||||
case static_cast<int>(MemoryScrollBarMode::Bounded):
|
||||
default:
|
||||
return MemoryScrollBarMode::Bounded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1163,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>();
|
||||
|
||||
@@ -236,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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+371
-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):
|
||||
@@ -206,12 +229,6 @@ QString panelDisplayName(const IaitoDockWidget *dock)
|
||||
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)
|
||||
@@ -321,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);
|
||||
@@ -349,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();
|
||||
@@ -490,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,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;
|
||||
}
|
||||
|
||||
@@ -580,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) {
|
||||
@@ -732,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;
|
||||
@@ -761,6 +851,7 @@ void DockManager::detachDockTab(IaitoDockWidget *dock, const QPoint &globalPos)
|
||||
return;
|
||||
}
|
||||
|
||||
restoreMaximizedPanel();
|
||||
backend->removeDock(dock);
|
||||
dock->setParent(mainWindow);
|
||||
backend->setDockFloating(dock, true);
|
||||
@@ -1332,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;
|
||||
}
|
||||
}
|
||||
@@ -1449,6 +1749,7 @@ void DockManager::startDockWidgetDrag(
|
||||
return;
|
||||
}
|
||||
|
||||
restoreMaximizedPanel();
|
||||
dockTabDragActive = true;
|
||||
dockDragOffset = offset;
|
||||
updateDropOverlay(globalPos);
|
||||
@@ -1744,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) {
|
||||
@@ -1820,6 +2139,7 @@ bool DockManager::handleEvent(QObject *watched, QEvent *event)
|
||||
clearDockDrag();
|
||||
dockDragTabBar = tabBar;
|
||||
dockDragWidget = dockForDockTab(tabBar, index);
|
||||
setActiveDock(dockDragWidget);
|
||||
dockDragStartGlobalPos = mouseEventGlobalPos(mouseEvent);
|
||||
}
|
||||
break;
|
||||
@@ -1857,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;
|
||||
|
||||
+290
-6
@@ -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"
|
||||
|
||||
@@ -1320,6 +1321,24 @@ 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();
|
||||
@@ -1602,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;
|
||||
}
|
||||
|
||||
@@ -3307,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();
|
||||
@@ -3328,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);
|
||||
@@ -3442,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;
|
||||
@@ -3658,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();
|
||||
@@ -3818,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;
|
||||
@@ -4744,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());
|
||||
}
|
||||
@@ -4842,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)) {
|
||||
@@ -4849,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"));
|
||||
@@ -4870,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())));
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#define MONOTHREAD 1
|
||||
|
||||
#include "common/BasicInstructionHighlighter.h"
|
||||
#include "common/ExceptionInfo.h"
|
||||
#include "core/IaitoCommon.h"
|
||||
#include "core/IaitoDescriptions.h"
|
||||
|
||||
@@ -464,6 +465,7 @@ public:
|
||||
void suspendDebug();
|
||||
|
||||
private:
|
||||
int exceptionJsonSupport = -1;
|
||||
void finishStopDebug();
|
||||
|
||||
public:
|
||||
@@ -553,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();
|
||||
|
||||
@@ -580,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();
|
||||
@@ -590,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)
|
||||
|
||||
+159
-18
@@ -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,9 +59,11 @@
|
||||
#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"
|
||||
@@ -1072,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);
|
||||
@@ -1151,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);
|
||||
@@ -1208,6 +1247,7 @@ void MainWindow::initUI()
|
||||
connectMenuStatusTips(ui->menuEdit);
|
||||
connectMenuStatusTips(ui->menuCode);
|
||||
connectMenuStatusTips(ui->menuAnalysis);
|
||||
connectMenuStatusTips(ui->menuData);
|
||||
connectMenuStatusTips(ui->menuTools);
|
||||
connectMenuStatusTips(ui->menuPlugins);
|
||||
|
||||
@@ -1543,12 +1583,12 @@ 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),
|
||||
@@ -1578,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;
|
||||
@@ -1599,6 +1651,7 @@ void MainWindow::initDocks()
|
||||
ui->menuCode->addSeparator();
|
||||
ui->menuCode->addActions(makeActionList(codeDocks));
|
||||
ui->menuAnalysis->addActions(makeActionList(analysisDocks));
|
||||
ui->menuData->addActions(makeActionList(dataDocks));
|
||||
QList<IaitoDockWidget *> windowDocks2 = {
|
||||
consoleDock,
|
||||
};
|
||||
@@ -1612,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
|
||||
@@ -1678,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);
|
||||
@@ -2433,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);
|
||||
@@ -2460,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)) {
|
||||
@@ -2538,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;
|
||||
}
|
||||
}
|
||||
@@ -2547,6 +2606,11 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
|
||||
memoryDockWidget->raiseMemoryWidget();
|
||||
}
|
||||
|
||||
void MainWindow::showDataAnalysis(RVA start, RVA end)
|
||||
{
|
||||
dataAnalysisDock->showRange(start, end);
|
||||
}
|
||||
|
||||
void MainWindow::gotoOffset(const QString &offset)
|
||||
{
|
||||
ut64 address;
|
||||
@@ -2590,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();
|
||||
});
|
||||
@@ -2717,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()) {
|
||||
@@ -2752,6 +2851,7 @@ void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
|
||||
QFont font;
|
||||
font.setBold(true);
|
||||
action->setFont(font);
|
||||
action->setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2760,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]() {
|
||||
@@ -3111,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;
|
||||
@@ -3134,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();
|
||||
@@ -3201,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
|
||||
@@ -3505,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()
|
||||
@@ -4109,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;
|
||||
@@ -4142,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;
|
||||
}
|
||||
@@ -4162,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class RelocsWidget;
|
||||
class CommentsWidget;
|
||||
class StringsWidget;
|
||||
class FlagsWidget;
|
||||
class GlobalVariablesWidget;
|
||||
class Dashboard;
|
||||
class QLineEdit;
|
||||
class QRubberBand;
|
||||
@@ -66,6 +67,7 @@ class FilesWidget;
|
||||
class FilesystemWidget;
|
||||
class BinariesWidget;
|
||||
class ZoomWidget;
|
||||
class DataAnalysisWidget;
|
||||
class XrefsWidget;
|
||||
class RefsWidget;
|
||||
class CodeInfoWidget;
|
||||
@@ -151,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(
|
||||
@@ -340,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;
|
||||
@@ -366,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;
|
||||
@@ -415,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();
|
||||
@@ -445,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
|
||||
|
||||
@@ -130,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"/>
|
||||
|
||||
+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">
|
||||
|
||||
@@ -48,15 +48,19 @@ 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);
|
||||
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);
|
||||
|
||||
@@ -77,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);
|
||||
@@ -90,7 +98,12 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
|
||||
layout->addWidget(m_logTextEdit);
|
||||
|
||||
connect(m_filterLineEdit, &QLineEdit::textChanged, this, &PackageManagerDialog::filterPackages);
|
||||
connect(m_showAllPlatformsCheckBox, &QCheckBox::toggled, this, [this]() { refreshPackages(); });
|
||||
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);
|
||||
@@ -128,21 +141,18 @@ void PackageManagerDialog::refreshPackages()
|
||||
// Then list packages
|
||||
QProcess listProc;
|
||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
||||
if (m_showAllPlatformsCheckBox->isChecked()) {
|
||||
environment.insert("R2PM_PLATFORM", "any");
|
||||
} else {
|
||||
environment.remove("R2PM_PLATFORM");
|
||||
}
|
||||
environment.remove("R2PM_PLATFORM");
|
||||
listProc.setProcessEnvironment(environment);
|
||||
listProc.start("r2pm", QStringList() << "-sj");
|
||||
listProc.start("r2pm", QStringList() << "-Asj");
|
||||
if (!listProc.waitForFinished(30000)) {
|
||||
QMessageBox::warning(this, tr("Error"), processError(listProc, 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();
|
||||
@@ -158,19 +168,11 @@ void PackageManagerDialog::refreshPackages()
|
||||
if (desc == "") {
|
||||
desc = obj["desc"].toString();
|
||||
}
|
||||
QStringList platformTags;
|
||||
QStringList platformNames;
|
||||
QStringList platforms;
|
||||
for (const QJsonValue &platformValue : obj["platforms"].toArray()) {
|
||||
const QString platform = platformValue.toString();
|
||||
if (platform == "windows") {
|
||||
platformTags << "w";
|
||||
platformNames << tr("Windows");
|
||||
} else if (platform == "unix") {
|
||||
platformTags << "u";
|
||||
platformNames << tr("Unix");
|
||||
} else if (platform == "qjs") {
|
||||
platformTags << "j";
|
||||
platformNames << tr("QuickJS");
|
||||
if (!platform.isEmpty()) {
|
||||
platforms << platform;
|
||||
}
|
||||
}
|
||||
int row = m_tableWidget->rowCount();
|
||||
@@ -180,11 +182,13 @@ 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(platformTags.join(' '));
|
||||
itemPlatforms->setToolTip(platformNames.join(", "));
|
||||
itemPlatforms->setTextAlignment(Qt::AlignCenter);
|
||||
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);
|
||||
@@ -216,29 +220,41 @@ void PackageManagerDialog::filterPackages(const QString &text)
|
||||
|| m_tableWidget->item(row, 1)->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;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ private slots:
|
||||
|
||||
private:
|
||||
QLineEdit *m_filterLineEdit;
|
||||
QCheckBox *m_binaryPackagesCheckBox;
|
||||
QCheckBox *m_showAllPlatformsCheckBox;
|
||||
QTableWidget *m_tableWidget;
|
||||
QPushButton *m_refreshButton;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -170,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
|
||||
@@ -644,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);
|
||||
@@ -694,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);
|
||||
@@ -733,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()));
|
||||
@@ -878,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
|
||||
@@ -902,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;
|
||||
@@ -1403,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()
|
||||
@@ -1625,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;
|
||||
|
||||
@@ -42,6 +42,35 @@
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,8 +25,8 @@ spacing=6
|
||||
sliderGroove=4
|
||||
|
||||
[palette]
|
||||
accent=#039be5
|
||||
accentText=#ffffff
|
||||
accent=#c6dff2
|
||||
accentText=#00162e
|
||||
background=#fafcfe
|
||||
border=#bdbdbd
|
||||
mutedText=#7d7d7d
|
||||
|
||||
@@ -161,7 +161,8 @@ void AddressScrollBar::applyMode()
|
||||
void AddressScrollBar::applyRange()
|
||||
{
|
||||
const RVA span = rangeEnd - rangeStart;
|
||||
unit = span > MaxSliderSpan ? (span + MaxSliderSpan - 1) / MaxSliderSpan : 1;
|
||||
// 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)));
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,408 @@
|
||||
#include "DecompilerTextEdit.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <QAbstractTextDocumentLayout>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QSet>
|
||||
#include <QSignalBlocker>
|
||||
#include <QStyle>
|
||||
#include <QStyleOption>
|
||||
#include <QTextBlock>
|
||||
|
||||
DecompilerTextEdit::DecompilerTextEdit(QWidget *parent)
|
||||
: QPlainTextEdit(parent)
|
||||
, foldArea(new QWidget(this))
|
||||
{
|
||||
setLineWrapMode(QPlainTextEdit::NoWrap);
|
||||
foldArea->setObjectName(QStringLiteral("decompilerFoldArea"));
|
||||
foldArea->setAccessibleName(tr("Code folding"));
|
||||
foldArea->setToolTip(tr("Click an arrow to collapse or expand a { ... } block"));
|
||||
foldArea->setMouseTracking(true);
|
||||
foldArea->installEventFilter(this);
|
||||
connect(this, &QPlainTextEdit::updateRequest, this, [this](const QRect &rect, int dy) {
|
||||
if (foldArea->height() != viewport()->height()) {
|
||||
updateFoldAreaGeometry();
|
||||
}
|
||||
if (dy) {
|
||||
foldArea->scroll(0, dy);
|
||||
} else {
|
||||
foldArea->update(0, rect.y(), foldArea->width(), rect.height());
|
||||
}
|
||||
});
|
||||
connect(this, &QPlainTextEdit::cursorPositionChanged, this, &DecompilerTextEdit::revealCursor);
|
||||
updateFoldAreaGeometry();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::setDecompiledCode(
|
||||
const QString &text, const QString &source, bool preserveFolds)
|
||||
{
|
||||
QScopedValueRollback<bool> guard(updatingCode, true);
|
||||
QString normalizedSource = source;
|
||||
normalizedSource.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
|
||||
normalizedSource.replace(QLatin1Char('\r'), QLatin1Char('\n'));
|
||||
preserveFolds = preserveFolds && normalizedSource == foldingSource;
|
||||
const QTextCursor previous = textCursor();
|
||||
const int previousLine = previous.blockNumber();
|
||||
const QString sourceLine = foldingSource.section(QLatin1Char('\n'), previousLine, previousLine);
|
||||
const int sourceColumn
|
||||
= qMax(0, previous.positionInBlock() - (previous.block().text().size() - sourceLine.size()));
|
||||
QSet<QPair<int, int>> collapsed;
|
||||
if (preserveFolds) {
|
||||
for (const FoldRegion ®ion : regions) {
|
||||
if (region.collapsed) {
|
||||
collapsed.insert({region.first, region.last});
|
||||
}
|
||||
}
|
||||
}
|
||||
regions.clear();
|
||||
setPlainText(text);
|
||||
foldingSource = normalizedSource;
|
||||
findFoldRegions(normalizedSource);
|
||||
for (FoldRegion ®ion : regions) {
|
||||
region.collapsed = collapsed.contains({region.first, region.last});
|
||||
}
|
||||
if (preserveFolds) {
|
||||
const QTextBlock block = document()->findBlockByNumber(previousLine);
|
||||
if (block.isValid()) {
|
||||
const int prefix = block.text().size() - sourceLine.size();
|
||||
QTextCursor cursor(block);
|
||||
cursor.setPosition(
|
||||
block.position() + qBound(0, prefix + sourceColumn, block.length() - 1));
|
||||
setTextCursor(cursor);
|
||||
}
|
||||
}
|
||||
applyFolding();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::findFoldRegions(const QString &source)
|
||||
{
|
||||
enum State { Code, LineComment, BlockComment, Quoted, RawString } state = Code;
|
||||
QVector<QPair<int, int>> openings;
|
||||
matchingBraces.clear();
|
||||
// Translate source positions past optional address and disassembly columns.
|
||||
QVector<int> lineOffsets;
|
||||
QTextBlock block = document()->begin();
|
||||
int sourcePosition = 0;
|
||||
for (const QString &text : source.split(QLatin1Char('\n'))) {
|
||||
lineOffsets.append(block.position() + block.text().size() - text.size() - sourcePosition);
|
||||
sourcePosition += text.size() + 1;
|
||||
block = block.next();
|
||||
}
|
||||
QChar quote;
|
||||
QString rawEnd;
|
||||
int line = 0;
|
||||
for (int i = 0; i < source.size(); ++i) {
|
||||
const QChar c = source.at(i);
|
||||
const QChar next = i + 1 < source.size() ? source.at(i + 1) : QChar();
|
||||
if (c == QLatin1Char('\n')) {
|
||||
++line;
|
||||
}
|
||||
if (state == LineComment) {
|
||||
if (c == QLatin1Char('\n') && (i == 0 || source.at(i - 1) != QLatin1Char('\\'))) {
|
||||
state = Code;
|
||||
}
|
||||
} else if (state == BlockComment) {
|
||||
if (c == QLatin1Char('*') && next == QLatin1Char('/')) {
|
||||
state = Code;
|
||||
++i;
|
||||
}
|
||||
} else if (state == Quoted) {
|
||||
if (c == QLatin1Char('\\') && !next.isNull()) {
|
||||
if (next == QLatin1Char('\n')) {
|
||||
++line;
|
||||
}
|
||||
++i;
|
||||
} else if (c == quote) {
|
||||
state = Code;
|
||||
}
|
||||
} else if (state == RawString) {
|
||||
if (source.mid(i, rawEnd.size()) == rawEnd) {
|
||||
i += rawEnd.size() - 1;
|
||||
state = Code;
|
||||
}
|
||||
} else if (c == QLatin1Char('/') && next == QLatin1Char('/')) {
|
||||
state = LineComment;
|
||||
++i;
|
||||
} else if (c == QLatin1Char('/') && next == QLatin1Char('*')) {
|
||||
state = BlockComment;
|
||||
++i;
|
||||
} else if (c == QLatin1Char('R') && next == QLatin1Char('"')) {
|
||||
const int length = source.mid(i + 2, 17).indexOf(QLatin1Char('('));
|
||||
const QString delimiter = source.mid(i + 2, qMax(0, length));
|
||||
if (length >= 0 && std::none_of(delimiter.cbegin(), delimiter.cend(), [](QChar c) {
|
||||
return c.isSpace() || c == QLatin1Char('\\') || c == QLatin1Char(')');
|
||||
})) {
|
||||
rawEnd = QLatin1Char(')') + delimiter + QLatin1Char('"');
|
||||
i += length + 2;
|
||||
state = RawString;
|
||||
}
|
||||
} else if (c == QLatin1Char('"') || c == QLatin1Char('\'')) {
|
||||
quote = c;
|
||||
state = Quoted;
|
||||
} else if (c == QLatin1Char('{')) {
|
||||
openings.append({line, i + lineOffsets.at(line)});
|
||||
} else if (c == QLatin1Char('}') && !openings.isEmpty()) {
|
||||
const auto opening = openings.takeLast();
|
||||
const int closing = i + lineOffsets.at(line);
|
||||
matchingBraces.insert(opening.second, closing);
|
||||
matchingBraces.insert(closing, opening.second);
|
||||
if (line > opening.first + 1) {
|
||||
regions.append({opening.first, line});
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(regions.begin(), regions.end(), [](const FoldRegion &a, const FoldRegion &b) {
|
||||
return a.first != b.first ? a.first < b.first : a.last > b.last;
|
||||
});
|
||||
}
|
||||
|
||||
int DecompilerTextEdit::foldForLine(int line) const
|
||||
{
|
||||
auto it = std::lower_bound(
|
||||
regions.cbegin(), regions.cend(), line, [](const FoldRegion ®ion, int line) {
|
||||
return region.first < line;
|
||||
});
|
||||
return it != regions.cend() && it->first == line ? int(it - regions.cbegin()) : -1;
|
||||
}
|
||||
|
||||
bool DecompilerTextEdit::isFolded(int line) const
|
||||
{
|
||||
const int index = foldForLine(line);
|
||||
return index >= 0 && regions.at(index).collapsed;
|
||||
}
|
||||
|
||||
bool DecompilerTextEdit::isFoldable(int line) const
|
||||
{
|
||||
return foldForLine(line) >= 0;
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::toggleFold(int line)
|
||||
{
|
||||
const int index = foldForLine(line);
|
||||
if (index >= 0) {
|
||||
const bool collapsed = !regions.at(index).collapsed;
|
||||
// One gutter arrow controls all braces opened on the same line.
|
||||
for (int i = index; i < regions.size() && regions.at(i).first == line; ++i) {
|
||||
regions[i].collapsed = collapsed;
|
||||
}
|
||||
applyFolding();
|
||||
}
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::toggleFoldAtCursor()
|
||||
{
|
||||
const int line = textCursor().blockNumber();
|
||||
int index = foldForLine(line);
|
||||
if (index < 0) {
|
||||
for (int i = 0; i < regions.size(); ++i) {
|
||||
if (regions.at(i).first < line && regions.at(i).last >= line) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
toggleFold(regions.at(index).first);
|
||||
}
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::jumpToMatchingBrace()
|
||||
{
|
||||
const QTextCursor cursor = textCursor();
|
||||
const auto match = matchingBraces.lowerBound(cursor.position());
|
||||
if (match == matchingBraces.cend()
|
||||
|| match.key() >= cursor.block().position() + cursor.block().length() - 1) {
|
||||
return;
|
||||
}
|
||||
QTextCursor target(document());
|
||||
target.setPosition(match.value());
|
||||
setTextCursor(target);
|
||||
ensureCursorVisible();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::collapseAll()
|
||||
{
|
||||
for (FoldRegion ®ion : regions) {
|
||||
region.collapsed = true;
|
||||
}
|
||||
applyFolding();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::expandAll()
|
||||
{
|
||||
for (FoldRegion ®ion : regions) {
|
||||
region.collapsed = false;
|
||||
}
|
||||
applyFolding();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::applyFolding()
|
||||
{
|
||||
QVector<int> depth(blockCount() + 1, 0);
|
||||
for (const FoldRegion ®ion : regions) {
|
||||
if (region.collapsed && region.last < blockCount()) {
|
||||
++depth[region.first + 1];
|
||||
--depth[region.last];
|
||||
}
|
||||
}
|
||||
int hidden = 0;
|
||||
for (QTextBlock block = document()->begin(); block.isValid(); block = block.next()) {
|
||||
hidden += depth.at(block.blockNumber());
|
||||
block.setVisible(hidden == 0);
|
||||
block.setLineCount(hidden ? 0 : 1);
|
||||
}
|
||||
QTextBlock cursorBlock = textCursor().block();
|
||||
if (!cursorBlock.isVisible()) {
|
||||
while (cursorBlock.isValid() && !cursorBlock.isVisible()) {
|
||||
cursorBlock = cursorBlock.previous();
|
||||
}
|
||||
QSignalBlocker blocker(this);
|
||||
setTextCursor(QTextCursor(cursorBlock));
|
||||
}
|
||||
document()->markContentsDirty(0, document()->characterCount());
|
||||
auto *layout = document()->documentLayout();
|
||||
emit layout->documentSizeChanged(layout->documentSize());
|
||||
viewport()->update();
|
||||
foldArea->update();
|
||||
emit foldingChanged();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::revealCursor()
|
||||
{
|
||||
if (updatingCode) {
|
||||
return;
|
||||
}
|
||||
const int line = textCursor().blockNumber();
|
||||
QSet<int> openings;
|
||||
for (const FoldRegion ®ion : regions) {
|
||||
if (region.collapsed && region.first < line && region.last > line) {
|
||||
openings.insert(region.first);
|
||||
}
|
||||
}
|
||||
if (!openings.isEmpty()) {
|
||||
for (FoldRegion ®ion : regions) {
|
||||
if (openings.contains(region.first)) {
|
||||
region.collapsed = false;
|
||||
}
|
||||
}
|
||||
applyFolding();
|
||||
}
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::updateFoldAreaGeometry()
|
||||
{
|
||||
const int width = qMax(16, fontMetrics().height());
|
||||
if (viewportMargins().left() != width) {
|
||||
setViewportMargins(width, 0, 0, 0);
|
||||
}
|
||||
const QRect rect = viewport()->geometry();
|
||||
foldArea->setGeometry(contentsRect().left(), rect.top(), width, rect.height());
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QPlainTextEdit::resizeEvent(event);
|
||||
updateFoldAreaGeometry();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::changeEvent(QEvent *event)
|
||||
{
|
||||
QPlainTextEdit::changeEvent(event);
|
||||
if (event->type() == QEvent::FontChange) {
|
||||
updateFoldAreaGeometry();
|
||||
}
|
||||
foldArea->update();
|
||||
}
|
||||
|
||||
int DecompilerTextEdit::foldAt(const QPoint &point) const
|
||||
{
|
||||
const QPoint viewportPoint = foldArea->mapTo(this, point) - viewport()->pos();
|
||||
const QTextCursor cursor = cursorForPosition(QPoint(0, viewportPoint.y()));
|
||||
const QRect rect = cursorRect(cursor);
|
||||
if (!cursor.block().isVisible() || viewportPoint.y() < rect.top()
|
||||
|| viewportPoint.y() > rect.bottom()) {
|
||||
return -1;
|
||||
}
|
||||
return foldForLine(cursor.blockNumber());
|
||||
}
|
||||
|
||||
bool DecompilerTextEdit::eventFilter(QObject *object, QEvent *event)
|
||||
{
|
||||
if (object == foldArea) {
|
||||
if (event->type() == QEvent::Paint) {
|
||||
paintFoldArea();
|
||||
return true;
|
||||
}
|
||||
if (event->type() == QEvent::MouseMove) {
|
||||
auto *mouse = static_cast<QMouseEvent *>(event);
|
||||
foldArea->setCursor(
|
||||
foldAt(mouse->pos()) >= 0 ? Qt::PointingHandCursor : Qt::ArrowCursor);
|
||||
return true;
|
||||
}
|
||||
if (event->type() == QEvent::MouseButtonPress) {
|
||||
auto *mouse = static_cast<QMouseEvent *>(event);
|
||||
const int index = foldAt(mouse->pos());
|
||||
if (mouse->button() == Qt::LeftButton && index >= 0) {
|
||||
toggleFold(regions.at(index).first);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event->type() == QEvent::MouseButtonRelease
|
||||
|| event->type() == QEvent::MouseButtonDblClick) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QPlainTextEdit::eventFilter(object, event);
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::paintFoldArea()
|
||||
{
|
||||
QPainter painter(foldArea);
|
||||
painter.fillRect(foldArea->rect(), palette().alternateBase());
|
||||
for (QTextBlock block = firstVisibleBlock(); block.isValid(); block = block.next()) {
|
||||
if (!block.isVisible()) {
|
||||
continue;
|
||||
}
|
||||
const QRectF rect = blockBoundingGeometry(block).translated(contentOffset());
|
||||
if (rect.top() >= viewport()->height()) {
|
||||
break;
|
||||
}
|
||||
const int index = foldForLine(block.blockNumber());
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
QStyleOption option;
|
||||
option.initFrom(foldArea);
|
||||
option.rect = QRect(
|
||||
0, qRound(rect.top()), foldArea->width(), qRound(blockBoundingRect(block).height()));
|
||||
option.state |= QStyle::State_Children;
|
||||
if (!regions.at(index).collapsed) {
|
||||
option.state |= QStyle::State_Open;
|
||||
}
|
||||
style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, &painter, foldArea);
|
||||
}
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPlainTextEdit::paintEvent(event);
|
||||
QPainter painter(viewport());
|
||||
painter.setPen(palette().color(QPalette::Mid));
|
||||
for (const FoldRegion ®ion : regions) {
|
||||
if (!region.collapsed) {
|
||||
continue;
|
||||
}
|
||||
const QTextBlock block = document()->findBlockByNumber(region.first);
|
||||
if (!block.isVisible()) {
|
||||
continue;
|
||||
}
|
||||
QTextCursor cursor(block);
|
||||
cursor.movePosition(QTextCursor::EndOfBlock);
|
||||
const QRect rect = cursorRect(cursor);
|
||||
if (rect.intersects(viewport()->rect())) {
|
||||
painter
|
||||
.drawText(rect.topLeft() + QPoint(3, fontMetrics().ascent()), QStringLiteral(" ..."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef DECOMPILERTEXTEDIT_H
|
||||
#define DECOMPILERTEXTEDIT_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QVector>
|
||||
|
||||
class DecompilerTextEdit : public QPlainTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DecompilerTextEdit(QWidget *parent = nullptr);
|
||||
|
||||
void setDecompiledCode(const QString &text, const QString &source, bool preserveFolds);
|
||||
bool isFolded(int line) const;
|
||||
bool isFoldable(int line) const;
|
||||
|
||||
public slots:
|
||||
void toggleFold(int line);
|
||||
void toggleFoldAtCursor();
|
||||
void jumpToMatchingBrace();
|
||||
void collapseAll();
|
||||
void expandAll();
|
||||
|
||||
signals:
|
||||
void foldingChanged();
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void changeEvent(QEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
struct FoldRegion
|
||||
{
|
||||
int first;
|
||||
int last;
|
||||
bool collapsed = false;
|
||||
};
|
||||
|
||||
QWidget *foldArea;
|
||||
QVector<FoldRegion> regions;
|
||||
QMap<int, int> matchingBraces;
|
||||
QString foldingSource;
|
||||
bool updatingCode = false;
|
||||
|
||||
int foldForLine(int line) const;
|
||||
int foldAt(const QPoint &point) const;
|
||||
void findFoldRegions(const QString &source);
|
||||
void applyFolding();
|
||||
void revealCursor();
|
||||
void updateFoldAreaGeometry();
|
||||
void paintFoldArea();
|
||||
};
|
||||
|
||||
#endif // DECOMPILERTEXTEDIT_H
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "DecompilerWidget.h"
|
||||
#include "DecompilerTextEdit.h"
|
||||
#include "common/ShortcutManager.h"
|
||||
#include "menus/ContextMenuIcons.h"
|
||||
#include "menus/DecompilerContextMenu.h"
|
||||
#include "ui_DecompilerWidget.h"
|
||||
|
||||
@@ -18,7 +20,9 @@
|
||||
#include <QPushButton>
|
||||
|
||||
#include "dialogs/ShortcutKeysDialog.h"
|
||||
#include <algorithm>
|
||||
#include <QAbstractSlider>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
@@ -31,11 +35,13 @@
|
||||
#include <QLineEdit>
|
||||
#include <QObject>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QRegularExpression>
|
||||
#include <QScrollBar>
|
||||
#include <QSet>
|
||||
#include <QTextBlock>
|
||||
#include <QTextBlockUserData>
|
||||
#include <QTextEdit>
|
||||
#include <QTextLayout>
|
||||
#include <QUrl>
|
||||
#include <QWheelEvent>
|
||||
|
||||
@@ -55,7 +61,12 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
|
||||
, code(r_codemeta_new(""), &r_codemeta_free)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
// Keep a keyboard cursor in the read-only decompilation listing.
|
||||
ui->textEdit->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
|
||||
setFocusProxy(ui->textEdit);
|
||||
setupSeekHistoryBar(true);
|
||||
showAddressColumn = Config()->getDecompilerShowAddresses();
|
||||
showDisassemblyColumn = Config()->getDecompilerShowDisassembly();
|
||||
setObjectName(main ? main->getUniqueObjectName(getWidgetType()) : getWidgetType());
|
||||
updateWindowTitle();
|
||||
|
||||
@@ -63,8 +74,19 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
|
||||
// Event filter to intercept key, mouse double click, and right click in the textbox
|
||||
ui->textEdit->viewport()->installEventFilter(this);
|
||||
ui->textEdit->installEventFilter(this);
|
||||
// Needed so hovering a deep link shows a pointing-hand cursor without a press
|
||||
ui->textEdit->setCursor(Qt::ArrowCursor);
|
||||
ui->textEdit->viewport()->setCursor(Qt::ArrowCursor);
|
||||
ui->textEdit->setMouseTracking(true);
|
||||
ui->textEdit->viewport()->setMouseTracking(true);
|
||||
for (QScrollBar *scrollBar :
|
||||
{ui->textEdit->horizontalScrollBar(), ui->textEdit->verticalScrollBar()}) {
|
||||
connect(
|
||||
scrollBar,
|
||||
&QScrollBar::valueChanged,
|
||||
this,
|
||||
&DecompilerWidget::refreshDecompilerHover,
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
setupFonts();
|
||||
colorsUpdatedSlot();
|
||||
@@ -117,8 +139,55 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
|
||||
|
||||
connect(Core(), &IaitoCore::breakpointsChanged, this, &DecompilerWidget::updateBreakpoints);
|
||||
mCtxMenu->addSeparator();
|
||||
using ContextMenuIcons::Icon;
|
||||
using ContextMenuIcons::setIcon;
|
||||
auto bindEditorAction = [this](const char *id, QAction *action) {
|
||||
ShortcutMgr()->bindAction(id, action);
|
||||
action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||
action->setAutoRepeat(false);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
action->setShortcutVisibleInContextMenu(true);
|
||||
#endif
|
||||
ui->textEdit->addAction(action);
|
||||
};
|
||||
auto *followAction = new QAction(tr("Follow reference"), this);
|
||||
bindEditorAction("decompiler.followReference", followAction);
|
||||
setIcon(this, followAction, Icon::Navigation, QColor(103, 80, 164));
|
||||
connect(followAction, &QAction::triggered, this, &DecompilerWidget::seekToReference);
|
||||
mCtxMenu->insertAction(mCtxMenu->actions().first(), followAction);
|
||||
|
||||
const QColor foldingColor(191, 128, 32);
|
||||
QMenu *foldingMenu = mCtxMenu->addMenu(tr("Code folding"));
|
||||
setIcon(this, foldingMenu->menuAction(), Icon::Fold, foldingColor);
|
||||
QAction *toggleFoldAction = foldingMenu->addAction(
|
||||
tr("Collapse/expand block"), ui->textEdit, &DecompilerTextEdit::toggleFoldAtCursor);
|
||||
setIcon(this, toggleFoldAction, Icon::Fold, foldingColor);
|
||||
bindEditorAction("decompiler.toggleFold", toggleFoldAction);
|
||||
auto *matchBraceAction = foldingMenu->addAction(
|
||||
tr("Jump to matching brace"), ui->textEdit, &DecompilerTextEdit::jumpToMatchingBrace);
|
||||
bindEditorAction("decompiler.matchBrace", matchBraceAction);
|
||||
setIcon(this, matchBraceAction, Icon::Navigation, QColor(103, 80, 164));
|
||||
setIcon(
|
||||
this,
|
||||
foldingMenu
|
||||
->addAction(tr("Collapse all blocks"), ui->textEdit, &DecompilerTextEdit::collapseAll),
|
||||
Icon::Collapse,
|
||||
foldingColor);
|
||||
setIcon(
|
||||
this,
|
||||
foldingMenu->addAction(tr("Expand all blocks"), ui->textEdit, &DecompilerTextEdit::expandAll),
|
||||
Icon::Expand,
|
||||
foldingColor);
|
||||
connect(ui->textEdit, &DecompilerTextEdit::foldingChanged, this, [this]() {
|
||||
updateCursorContext();
|
||||
updateSelection();
|
||||
refreshDecompilerHover();
|
||||
});
|
||||
mCtxMenu->addAction(&syncAction);
|
||||
addActions(mCtxMenu->actions());
|
||||
setIcon(this, &syncAction, Icon::Sync, QColor(103, 80, 164));
|
||||
auto menuActions = mCtxMenu->actions();
|
||||
menuActions.removeOne(followAction);
|
||||
addActions(menuActions);
|
||||
|
||||
ui->progressLabel->setVisible(false);
|
||||
ui->decompileButton->setVisible(false);
|
||||
@@ -160,6 +229,10 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
|
||||
connect(Core(), &IaitoCore::commentsChanged, this, &DecompilerWidget::refreshIfChanged);
|
||||
connect(Core(), &IaitoCore::instructionChanged, this, &DecompilerWidget::refreshIfChanged);
|
||||
connect(Core(), &IaitoCore::refreshCodeViews, this, &DecompilerWidget::doRefresh);
|
||||
connect(Core(), &IaitoCore::refreshAll, this, &DecompilerWidget::invalidateTokenTargets);
|
||||
connect(Core(), &IaitoCore::flagsChanged, this, &DecompilerWidget::invalidateTokenTargets);
|
||||
connect(Core(), &IaitoCore::functionsChanged, this, &DecompilerWidget::invalidateTokenTargets);
|
||||
connect(Core(), &IaitoCore::functionRenamed, this, &DecompilerWidget::invalidateTokenTargets);
|
||||
|
||||
// Esc to seek backward
|
||||
QAction *seekPrevAction = new QAction(this);
|
||||
@@ -167,10 +240,23 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
|
||||
seekPrevAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||
addAction(seekPrevAction);
|
||||
connect(seekPrevAction, &QAction::triggered, seekable, &IaitoSeekable::seekPrev);
|
||||
|
||||
QAction *switchAction = new QAction(this);
|
||||
ShortcutMgr()->bindAction("decompiler.switchToDisassembly", switchAction);
|
||||
switchAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||
addAction(switchAction);
|
||||
connect(switchAction, &QAction::triggered, this, [this] {
|
||||
mainWindow->showMemoryWidget(MemoryWidgetType::Disassembly);
|
||||
});
|
||||
}
|
||||
|
||||
DecompilerWidget::~DecompilerWidget() = default;
|
||||
|
||||
QWidget *DecompilerWidget::widgetToFocusOnRaise()
|
||||
{
|
||||
return ui->textEdit;
|
||||
}
|
||||
|
||||
QString DecompilerWidget::getWidgetType()
|
||||
{
|
||||
return "DecompilerWidget";
|
||||
@@ -183,10 +269,186 @@ Decompiler *DecompilerWidget::getCurrentDecompiler()
|
||||
|
||||
QString DecompilerWidget::deepLinkAt(const QPoint &pos)
|
||||
{
|
||||
const QTextCursor cursor = ui->textEdit->cursorForPosition(pos);
|
||||
const QTextCursor cursor = cursorOnCharacter(pos);
|
||||
return DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
|
||||
}
|
||||
|
||||
QTextCursor DecompilerWidget::cursorOnCharacter(const QPoint &pos) const
|
||||
{
|
||||
if (!ui->textEdit->viewport()->rect().contains(pos)) {
|
||||
return {};
|
||||
}
|
||||
QTextCursor cursor = ui->textEdit->cursorForPosition(pos);
|
||||
const QTextBlock block = cursor.block();
|
||||
const QTextLine line = block.layout()->lineForTextPosition(cursor.positionInBlock());
|
||||
if (!line.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const QRect caret = ui->textEdit->cursorRect(cursor);
|
||||
const qreal x = pos.x() - caret.left() + line.cursorToX(cursor.positionInBlock());
|
||||
const QRectF bounds = line.naturalTextRect();
|
||||
if (x < bounds.left() || x >= bounds.right() || pos.y() < caret.top()
|
||||
|| pos.y() >= caret.top() + caret.height()) {
|
||||
return {};
|
||||
}
|
||||
const int column = line.xToCursor(x, QTextLine::CursorOnCharacter);
|
||||
const QString text = block.text();
|
||||
if (column >= text.size() || text.at(column).isSpace()) {
|
||||
return {};
|
||||
}
|
||||
cursor.setPosition(block.position() + column);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
static QString decompilerSymbolName(QString name)
|
||||
{
|
||||
// Plain-text decompilers strip r2 prefixes and turn punctuation into underscores.
|
||||
static const QRegularExpression prefix(R"(^(?:(?:sym|imp|reloc)[._])+)");
|
||||
static const QRegularExpression punctuation(R"([^\p{L}\p{N}_])");
|
||||
name = name.section(QLatin1Char('('), 0, 0).trimmed();
|
||||
name.remove(prefix);
|
||||
name.replace(punctuation, QStringLiteral("_"));
|
||||
while (name.startsWith(QLatin1Char('_'))) {
|
||||
name.remove(0, 1);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
void DecompilerWidget::invalidateTokenTargets()
|
||||
{
|
||||
symbolTargets.clear();
|
||||
outgoingXRefsCache.clear();
|
||||
symbolTargetsReady = false;
|
||||
refreshDecompilerHover();
|
||||
}
|
||||
|
||||
void DecompilerWidget::cacheSymbolTarget(const QString &name, RVA address)
|
||||
{
|
||||
const QString key = decompilerSymbolName(name);
|
||||
if (key.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto it = symbolTargets.find(key);
|
||||
if (it == symbolTargets.end()) {
|
||||
symbolTargets.insert(key, address);
|
||||
} else if (it.value() != address) {
|
||||
// A shortened name must not silently navigate to the wrong overload/import.
|
||||
it.value() = RVA_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
RVA DecompilerWidget::symbolTarget(const QString &name, RVA offset)
|
||||
{
|
||||
if (!symbolTargetsReady) {
|
||||
auto core = Core()->core();
|
||||
r_flag_foreach(
|
||||
core->flags,
|
||||
[](RFlagItem *flag, void *user) {
|
||||
auto *widget = static_cast<DecompilerWidget *>(user);
|
||||
widget->cacheSymbolTarget(QString::fromUtf8(flag->name), flag->addr);
|
||||
widget->cacheSymbolTarget(QString::fromUtf8(flag->realname), flag->addr);
|
||||
return true;
|
||||
},
|
||||
this);
|
||||
RListIter *iter;
|
||||
RAnalFunction *function;
|
||||
IaitoRListForeach(core->anal->fcns, iter, RAnalFunction, function)
|
||||
{
|
||||
cacheSymbolTarget(QString::fromUtf8(function->name), function->addr);
|
||||
}
|
||||
symbolTargetsReady = true;
|
||||
}
|
||||
const QString key = decompilerSymbolName(name);
|
||||
const RVA target = symbolTargets.value(key, RVA_INVALID);
|
||||
if (target != RVA_INVALID || offset == RVA_INVALID) {
|
||||
return target;
|
||||
}
|
||||
auto refs = outgoingXRefsCache.constFind(offset);
|
||||
if (refs == outgoingXRefsCache.cend()) {
|
||||
refs = outgoingXRefsCache.insert(offset, Core()->getXRefs(offset, false, false));
|
||||
}
|
||||
RVA referenceTarget = RVA_INVALID;
|
||||
for (const XrefDescription &ref : refs.value()) {
|
||||
if (decompilerSymbolName(ref.to_str) == key) {
|
||||
if (referenceTarget != RVA_INVALID && referenceTarget != ref.to) {
|
||||
return RVA_INVALID;
|
||||
}
|
||||
referenceTarget = ref.to;
|
||||
}
|
||||
}
|
||||
return referenceTarget;
|
||||
}
|
||||
|
||||
RVA DecompilerWidget::tokenTarget(QTextCursor &cursor)
|
||||
{
|
||||
if (cursor.isNull()) {
|
||||
return RVA_INVALID;
|
||||
}
|
||||
const size_t pos = static_cast<size_t>(cursor.position());
|
||||
RVA offset = RVA_INVALID;
|
||||
size_t offsetStart = 0;
|
||||
#if R2_ABIVERSION >= 40
|
||||
RCodeMetaItem *annotation;
|
||||
R_VEC_FOREACH(&code->annotations, annotation)
|
||||
{
|
||||
#else
|
||||
void *iter;
|
||||
r_vector_foreach(&code->annotations, iter)
|
||||
{
|
||||
RCodeMetaItem *annotation = reinterpret_cast<RCodeMetaItem *>(iter);
|
||||
#endif
|
||||
if (annotation->start > pos || annotation->end <= pos) {
|
||||
continue;
|
||||
}
|
||||
if (annotation->type == R_CODEMETA_TYPE_OFFSET && annotation->start >= offsetStart) {
|
||||
offsetStart = annotation->start;
|
||||
offset = annotation->offset.offset;
|
||||
}
|
||||
if (r_codemeta_item_is_reference(annotation)) {
|
||||
cursor.setPosition(static_cast<int>(annotation->start));
|
||||
cursor.setPosition(static_cast<int>(annotation->end), QTextCursor::KeepAnchor);
|
||||
return annotation->reference.offset;
|
||||
}
|
||||
if (r_codemeta_item_is_variable(annotation)) {
|
||||
return RVA_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
// Decompilers without reference annotations still expose addresses and symbols.
|
||||
static const QRegularExpression tokens(
|
||||
R"(\b(?:0[xX][0-9a-fA-F]+|[\p{L}_$][\p{L}\p{N}_.$]*(?:::[\p{L}_$~][\p{L}\p{N}_.$]*)*)\b)",
|
||||
QRegularExpression::UseUnicodePropertiesOption);
|
||||
auto matches = tokens.globalMatch(cursor.block().text());
|
||||
while (matches.hasNext()) {
|
||||
const QRegularExpressionMatch match = matches.next();
|
||||
if (cursor.positionInBlock() < match.capturedStart()
|
||||
|| cursor.positionInBlock() >= match.capturedEnd()) {
|
||||
continue;
|
||||
}
|
||||
const QString token = match.captured();
|
||||
const int start = cursor.block().position() + match.capturedStart();
|
||||
cursor.setPosition(start);
|
||||
cursor.setPosition(start + match.capturedLength(), QTextCursor::KeepAnchor);
|
||||
if (token.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
bool ok = false;
|
||||
const RVA address = token.toULongLong(&ok, 16);
|
||||
return ok ? address : RVA_INVALID;
|
||||
}
|
||||
{
|
||||
RCoreLocked core = Core()->core();
|
||||
const QByteArray name = token.toUtf8();
|
||||
if (RFlagItem *flag = r_flag_get(core->flags, name.constData())) {
|
||||
return flag->addr;
|
||||
}
|
||||
if (RAnalFunction *function = r_anal_get_function_byname(core->anal, name.constData())) {
|
||||
return function->addr;
|
||||
}
|
||||
}
|
||||
return symbolTarget(token, offset);
|
||||
}
|
||||
return RVA_INVALID;
|
||||
}
|
||||
|
||||
ut64 DecompilerWidget::offsetForPosition(size_t pos)
|
||||
{
|
||||
ut64 closestPos = UT64_MAX;
|
||||
@@ -241,6 +503,14 @@ size_t DecompilerWidget::positionForOffset(ut64 offset)
|
||||
closestPos = annotation->start;
|
||||
closestOffset = annotation->offset.offset;
|
||||
}
|
||||
if (closestPos != UT64_MAX) {
|
||||
// Some decompilers start offset annotations on the preceding newline.
|
||||
QTextCursor cursor(ui->textEdit->document());
|
||||
cursor.setPosition(static_cast<int>(closestPos));
|
||||
if (cursor.atBlockEnd() && cursor.block().next().isValid()) {
|
||||
closestPos++;
|
||||
}
|
||||
}
|
||||
return closestPos;
|
||||
}
|
||||
|
||||
@@ -290,8 +560,8 @@ void DecompilerWidget::gatherBreakpointInfo(RCodeMeta &codeDecompiled, size_t st
|
||||
if (annotation->type != R_CODEMETA_TYPE_OFFSET) {
|
||||
continue;
|
||||
}
|
||||
if ((startPos <= annotation->start && annotation->start < endPos)
|
||||
|| (startPos < annotation->end && annotation->end < endPos)) {
|
||||
if (annotation->start < endPos && annotation->end > startPos
|
||||
&& annotation->offset.offset != RVA_INVALID) {
|
||||
firstOffset = (annotation->offset.offset < firstOffset) ? annotation->offset.offset
|
||||
: firstOffset;
|
||||
}
|
||||
@@ -311,7 +581,9 @@ void DecompilerWidget::gatherBreakpointInfo(RCodeMeta &codeDecompiled, size_t st
|
||||
|
||||
void DecompilerWidget::refreshIfChanged(RVA addr)
|
||||
{
|
||||
if (addressInRange(addr)) {
|
||||
if (addressInRange(addr)
|
||||
|| (decompiledFunctionAddr != RVA_INVALID
|
||||
&& Core()->getFunctionStart(addr) == decompiledFunctionAddr)) {
|
||||
doRefresh();
|
||||
}
|
||||
}
|
||||
@@ -430,7 +702,7 @@ void DecompilerWidget::refreshDecompiler()
|
||||
QTextCursor DecompilerWidget::getCursorForAddress(RVA addr)
|
||||
{
|
||||
size_t pos = positionForOffset(addr);
|
||||
if (pos == UT64_MAX || pos == 0) {
|
||||
if (pos == UT64_MAX) {
|
||||
return QTextCursor();
|
||||
}
|
||||
QTextCursor cursor = ui->textEdit->textCursor();
|
||||
@@ -441,10 +713,20 @@ QTextCursor DecompilerWidget::getCursorForAddress(RVA addr)
|
||||
void DecompilerWidget::decompilationFinished(RCodeMeta *codeDecompiled)
|
||||
{
|
||||
bool isDisplayReset = false;
|
||||
int cursorLineOffset = 0;
|
||||
int cursorColumn = 0;
|
||||
bool restoreCursor = false;
|
||||
if (previousFunctionAddr == decompiledFunctionAddr) {
|
||||
scrollerHorizontal = ui->textEdit->horizontalScrollBar()->sliderPosition();
|
||||
scrollerVertical = ui->textEdit->verticalScrollBar()->sliderPosition();
|
||||
isDisplayReset = true;
|
||||
const QTextCursor cursor = ui->textEdit->textCursor();
|
||||
const QTextCursor addressCursor = getCursorForAddress(seekable->getOffset());
|
||||
if (!addressCursor.isNull()) {
|
||||
cursorLineOffset = cursor.blockNumber() - addressCursor.blockNumber();
|
||||
cursorColumn = cursor.positionInBlock();
|
||||
restoreCursor = true;
|
||||
}
|
||||
}
|
||||
|
||||
ui->progressLabel->setVisible(false);
|
||||
@@ -465,7 +747,24 @@ void DecompilerWidget::decompilationFinished(RCodeMeta *codeDecompiled)
|
||||
highestOffsetInCode = 0;
|
||||
return;
|
||||
}
|
||||
updateCursorPosition();
|
||||
if (restoreCursor) {
|
||||
const QTextCursor addressCursor = getCursorForAddress(seekable->getOffset());
|
||||
const QTextBlock block = ui->textEdit->document()->findBlockByNumber(
|
||||
addressCursor.blockNumber() + cursorLineOffset);
|
||||
if (!addressCursor.isNull() && block.isValid()) {
|
||||
connectCursorPositionChanged(false);
|
||||
QTextCursor cursor(block);
|
||||
cursor.setPosition(block.position() + qMin(cursorColumn, block.length() - 1));
|
||||
ui->textEdit->setTextCursor(cursor);
|
||||
updateCursorContext();
|
||||
updateSelection();
|
||||
connectCursorPositionChanged(true);
|
||||
} else {
|
||||
updateCursorPosition();
|
||||
}
|
||||
} else {
|
||||
updateCursorPosition();
|
||||
}
|
||||
highlightPC();
|
||||
highlightBreakpoints();
|
||||
lowestOffsetInCode = RVA_MAX;
|
||||
@@ -913,26 +1212,31 @@ void DecompilerWidget::connectCursorPositionChanged(bool connectPositionChange)
|
||||
|
||||
void DecompilerWidget::cursorPositionChanged()
|
||||
{
|
||||
const RVA offset = updateCursorContext();
|
||||
// Do not perform seeks along with the cursor while selecting multiple lines
|
||||
if (!ui->textEdit->textCursor().selectedText().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t pos = ui->textEdit->textCursor().position();
|
||||
setAnnotationsAtCursor(pos);
|
||||
setInfoForBreakpoints();
|
||||
|
||||
RVA offset = offsetForPosition(pos);
|
||||
if (offset != RVA_INVALID && offset != seekable->getOffset()) {
|
||||
seekFromCursor = true;
|
||||
seekable->seek(offset);
|
||||
mCtxMenu->setOffset(offset);
|
||||
seekFromCursor = false;
|
||||
}
|
||||
updateSourceControls();
|
||||
updateSelection();
|
||||
}
|
||||
|
||||
RVA DecompilerWidget::updateCursorContext()
|
||||
{
|
||||
const size_t pos = ui->textEdit->textCursor().position();
|
||||
setAnnotationsAtCursor(pos);
|
||||
setInfoForBreakpoints();
|
||||
const RVA offset = offsetForPosition(pos);
|
||||
mCtxMenu->setOffset(offset);
|
||||
return offset;
|
||||
}
|
||||
|
||||
void DecompilerWidget::seekChanged()
|
||||
{
|
||||
if (seekFromCursor) {
|
||||
@@ -958,6 +1262,7 @@ void DecompilerWidget::updateCursorPosition()
|
||||
QTextCursor cursor = ui->textEdit->textCursor();
|
||||
cursor.setPosition(pos);
|
||||
ui->textEdit->setTextCursor(cursor);
|
||||
updateCursorContext();
|
||||
updateSelection();
|
||||
connectCursorPositionChanged(true);
|
||||
}
|
||||
@@ -985,7 +1290,7 @@ void DecompilerWidget::updateSelection()
|
||||
QString scrHighlight = Core()->getConfig("scr.highlight");
|
||||
extraSelections.append(createHighlightSelections(ui->textEdit, scrHighlight));
|
||||
|
||||
ui->textEdit->setExtraSelections(extraSelections);
|
||||
setDecompilerSelections(extraSelections);
|
||||
// Highlight PC after updating the selected line
|
||||
highlightPC();
|
||||
}
|
||||
@@ -998,6 +1303,7 @@ QString DecompilerWidget::getWindowTitle() const
|
||||
void DecompilerWidget::fontsUpdatedSlot()
|
||||
{
|
||||
setupFonts();
|
||||
refreshDecompilerHover();
|
||||
}
|
||||
|
||||
void DecompilerWidget::colorsUpdatedSlot()
|
||||
@@ -1009,20 +1315,99 @@ void DecompilerWidget::colorsUpdatedSlot()
|
||||
if (syntaxHighlighter) {
|
||||
QMetaObject::invokeMethod(syntaxHighlighter, "rehighlight", Qt::QueuedConnection);
|
||||
}
|
||||
setDecompilerSelections(ui->textEdit->extraSelections());
|
||||
}
|
||||
|
||||
void DecompilerWidget::showDecompilerContextMenu(const QPoint &pt)
|
||||
{
|
||||
mCtxMenu->exec(ui->textEdit->mapToGlobal(pt));
|
||||
updateCursorContext();
|
||||
mCtxMenu->exec(ui->textEdit->viewport()->mapToGlobal(pt));
|
||||
}
|
||||
|
||||
void DecompilerWidget::seekToReference()
|
||||
{
|
||||
QTextCursor cursor = ui->textEdit->textCursor();
|
||||
const QString link = DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
|
||||
if (!link.isEmpty()) {
|
||||
DeepLink::handle(mainWindow, link);
|
||||
return;
|
||||
}
|
||||
const RVA target = tokenTarget(cursor);
|
||||
if (target != RVA_INVALID) {
|
||||
seekable->seek(target);
|
||||
return;
|
||||
}
|
||||
size_t pos = ui->textEdit->textCursor().position();
|
||||
RVA offset = offsetForPosition(pos);
|
||||
seekable->seekToReference(offset);
|
||||
}
|
||||
|
||||
void DecompilerWidget::setDecompilerSelections(QList<QTextEdit::ExtraSelection> selections)
|
||||
{
|
||||
constexpr int hoverProperty = QTextFormat::UserProperty;
|
||||
selections.erase(
|
||||
std::remove_if(
|
||||
selections.begin(),
|
||||
selections.end(),
|
||||
[](const QTextEdit::ExtraSelection &s) { return s.format.boolProperty(hoverProperty); }),
|
||||
selections.end());
|
||||
if (hoveredToken.hasSelection()) {
|
||||
QTextEdit::ExtraSelection hover;
|
||||
hover.cursor = hoveredToken;
|
||||
hover.format.setBackground(ConfigColor("wordHighlight"));
|
||||
hover.format.setProperty(hoverProperty, true);
|
||||
selections.append(hover);
|
||||
}
|
||||
ui->textEdit->setExtraSelections(selections);
|
||||
}
|
||||
|
||||
void DecompilerWidget::setHoveredToken(const QTextCursor &cursor)
|
||||
{
|
||||
if (hoveredToken == cursor) {
|
||||
return;
|
||||
}
|
||||
hoveredToken = cursor;
|
||||
setDecompilerSelections(ui->textEdit->extraSelections());
|
||||
}
|
||||
|
||||
void DecompilerWidget::updateDecompilerHover(const QPoint &pos, Qt::MouseButtons buttons)
|
||||
{
|
||||
QTextCursor hover;
|
||||
Qt::CursorShape shape = Qt::ArrowCursor;
|
||||
if (buttons & Qt::LeftButton) {
|
||||
shape = Qt::IBeamCursor;
|
||||
} else {
|
||||
QTextCursor cursor = cursorOnCharacter(pos);
|
||||
const QString text = cursor.block().text();
|
||||
const QString link = DeepLink::linkAt(text, cursor.positionInBlock());
|
||||
if (!link.isEmpty()) {
|
||||
const int start = cursor.block().position()
|
||||
+ text.lastIndexOf(link, cursor.positionInBlock());
|
||||
cursor.setPosition(start);
|
||||
cursor.setPosition(start + link.size(), QTextCursor::KeepAnchor);
|
||||
hover = cursor;
|
||||
} else if (tokenTarget(cursor) != RVA_INVALID) {
|
||||
hover = cursor;
|
||||
}
|
||||
if (hover.hasSelection()) {
|
||||
shape = Qt::PointingHandCursor;
|
||||
}
|
||||
}
|
||||
ui->textEdit->viewport()->setCursor(shape);
|
||||
setHoveredToken(hover);
|
||||
}
|
||||
|
||||
void DecompilerWidget::refreshDecompilerHover()
|
||||
{
|
||||
if (ui->textEdit->viewport()->underMouse()) {
|
||||
updateDecompilerHover(
|
||||
ui->textEdit->viewport()->mapFromGlobal(QCursor::pos()), QApplication::mouseButtons());
|
||||
} else {
|
||||
ui->textEdit->viewport()->setCursor(Qt::ArrowCursor);
|
||||
setHoveredToken({});
|
||||
}
|
||||
}
|
||||
|
||||
bool DecompilerWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Wheel && (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
|
||||
@@ -1042,6 +1427,23 @@ bool DecompilerWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
if (event->type() == QEvent::KeyPress
|
||||
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
|
||||
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
static const struct
|
||||
{
|
||||
const char *shortcut;
|
||||
QTextCursor::MoveOperation movement;
|
||||
} movements[] = {
|
||||
{"decompiler.cursorLeft", QTextCursor::Left},
|
||||
{"decompiler.cursorDown", QTextCursor::Down},
|
||||
{"decompiler.cursorUp", QTextCursor::Up},
|
||||
{"decompiler.cursorRight", QTextCursor::Right},
|
||||
};
|
||||
for (const auto &movement : movements) {
|
||||
if (ShortcutMgr()->matches(movement.shortcut, keyEvent)) {
|
||||
ui->textEdit->moveCursor(movement.movement);
|
||||
keyEvent->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (keyEvent->modifiers() == Qt::NoModifier && keyEvent->key() == Qt::Key_M) {
|
||||
// Prepare context and compute offset for current decompiler line
|
||||
size_t pos = ui->textEdit->textCursor().position();
|
||||
@@ -1068,6 +1470,10 @@ bool DecompilerWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
if (event->type() == QEvent::MouseButtonDblClick
|
||||
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
if (mouseEvent->button() != Qt::LeftButton || followedTokenOnClick) {
|
||||
return true;
|
||||
}
|
||||
ui->textEdit->setTextCursor(ui->textEdit->cursorForPosition(mouseEvent->pos()));
|
||||
seekToReference();
|
||||
return true;
|
||||
@@ -1075,29 +1481,57 @@ bool DecompilerWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
if (event->type() == QEvent::MouseButtonPress
|
||||
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
followedTokenOnClick = false;
|
||||
if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() == Qt::NoModifier) {
|
||||
tokenPressPosition = mouseEvent->pos();
|
||||
QTextCursor cursor = cursorOnCharacter(tokenPressPosition);
|
||||
pressedTokenTarget = tokenTarget(cursor);
|
||||
}
|
||||
updateDecompilerHover(mouseEvent->pos(), mouseEvent->buttons());
|
||||
if (mouseEvent->button() == Qt::RightButton && !ui->textEdit->textCursor().hasSelection()) {
|
||||
ui->textEdit->setTextCursor(ui->textEdit->cursorForPosition(mouseEvent->pos()));
|
||||
size_t pos = ui->textEdit->textCursor().position();
|
||||
this->currentOffset = offsetForPosition(pos);
|
||||
updateCursorContext();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (event->type() == QEvent::MouseButtonRelease
|
||||
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
const RVA target = pressedTokenTarget;
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
updateDecompilerHover(mouseEvent->pos(), mouseEvent->buttons());
|
||||
if (mouseEvent->button() == Qt::LeftButton && !ui->textEdit->textCursor().hasSelection()) {
|
||||
const QString link = deepLinkAt(mouseEvent->pos());
|
||||
if (!link.isEmpty()) {
|
||||
DeepLink::handle(mainWindow, link);
|
||||
return true;
|
||||
}
|
||||
QTextCursor cursor = cursorOnCharacter(mouseEvent->pos());
|
||||
if (target != RVA_INVALID
|
||||
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
|
||||
< QApplication::startDragDistance()
|
||||
&& target == tokenTarget(cursor)) {
|
||||
followedTokenOnClick = true;
|
||||
seekable->seek(target);
|
||||
refreshDecompilerHover();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event->type() == QEvent::MouseMove
|
||||
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
const bool overLink = !deepLinkAt(mouseEvent->pos()).isEmpty();
|
||||
ui->textEdit->viewport()->setCursor(overLink ? Qt::PointingHandCursor : Qt::IBeamCursor);
|
||||
if ((mouseEvent->buttons() & Qt::LeftButton)
|
||||
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
|
||||
>= QApplication::startDragDistance()) {
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
}
|
||||
updateDecompilerHover(mouseEvent->pos(), mouseEvent->buttons());
|
||||
}
|
||||
if (event->type() == QEvent::Leave && obj == ui->textEdit->viewport()) {
|
||||
ui->textEdit->viewport()->setCursor(Qt::ArrowCursor);
|
||||
setHoveredToken({});
|
||||
}
|
||||
return MemoryDockWidget::eventFilter(obj, event);
|
||||
}
|
||||
@@ -1139,7 +1573,7 @@ bool DecompilerWidget::colorLine(QTextEdit::ExtraSelection extraSelection)
|
||||
{
|
||||
QList<QTextEdit::ExtraSelection> extraSelections = ui->textEdit->extraSelections();
|
||||
extraSelections.append(extraSelection);
|
||||
ui->textEdit->setExtraSelections(extraSelections);
|
||||
setDecompilerSelections(extraSelections);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1270,7 +1704,7 @@ static RCodeMeta *copyCodeMeta(RCodeMeta *source)
|
||||
return copy;
|
||||
}
|
||||
|
||||
struct DecompilerAddressLine
|
||||
struct DecompilerColumnLine
|
||||
{
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
@@ -1279,11 +1713,12 @@ struct DecompilerAddressLine
|
||||
size_t shiftBefore = 0;
|
||||
size_t shiftInLine = 0;
|
||||
RVA address = RVA_INVALID;
|
||||
QString disassembly = {};
|
||||
};
|
||||
|
||||
static QVector<DecompilerAddressLine> collectAddressLines(const QString &text)
|
||||
static QVector<DecompilerColumnLine> collectAddressLines(const QString &text)
|
||||
{
|
||||
QVector<DecompilerAddressLine> lines;
|
||||
QVector<DecompilerColumnLine> lines;
|
||||
int start = 0;
|
||||
while (start < text.size()) {
|
||||
int newline = text.indexOf(QLatin1Char('\n'), start);
|
||||
@@ -1296,7 +1731,7 @@ static QVector<DecompilerAddressLine> collectAddressLines(const QString &text)
|
||||
return lines;
|
||||
}
|
||||
|
||||
static bool assignLineAddresses(QVector<DecompilerAddressLine> &lines, RCodeMeta *code)
|
||||
static bool assignLineAddresses(QVector<DecompilerColumnLine> &lines, RCodeMeta *code)
|
||||
{
|
||||
bool hasAddress = false;
|
||||
#if R2_ABIVERSION >= 40
|
||||
@@ -1312,8 +1747,8 @@ static bool assignLineAddresses(QVector<DecompilerAddressLine> &lines, RCodeMeta
|
||||
if (annotation->type != R_CODEMETA_TYPE_OFFSET || annotation->offset.offset == RVA_INVALID) {
|
||||
continue;
|
||||
}
|
||||
for (DecompilerAddressLine &line : lines) {
|
||||
if (annotation->start >= line.nextStart || annotation->end <= line.start) {
|
||||
for (DecompilerColumnLine &line : lines) {
|
||||
if (annotation->start >= line.end || annotation->end <= line.start) {
|
||||
continue;
|
||||
}
|
||||
if (annotation->start < line.offsetAnnotationStart) {
|
||||
@@ -1321,15 +1756,14 @@ static bool assignLineAddresses(QVector<DecompilerAddressLine> &lines, RCodeMeta
|
||||
line.offsetAnnotationStart = annotation->start;
|
||||
hasAddress = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasAddress;
|
||||
}
|
||||
|
||||
static size_t shiftForStartPosition(const QVector<DecompilerAddressLine> &lines, size_t pos)
|
||||
static size_t shiftForStartPosition(const QVector<DecompilerColumnLine> &lines, size_t pos)
|
||||
{
|
||||
for (const DecompilerAddressLine &line : lines) {
|
||||
for (const DecompilerColumnLine &line : lines) {
|
||||
if (pos < line.nextStart) {
|
||||
return line.shiftInLine;
|
||||
}
|
||||
@@ -1337,9 +1771,9 @@ static size_t shiftForStartPosition(const QVector<DecompilerAddressLine> &lines,
|
||||
return lines.isEmpty() ? 0 : lines.last().shiftInLine;
|
||||
}
|
||||
|
||||
static size_t shiftForEndPosition(const QVector<DecompilerAddressLine> &lines, size_t pos)
|
||||
static size_t shiftForEndPosition(const QVector<DecompilerColumnLine> &lines, size_t pos)
|
||||
{
|
||||
for (const DecompilerAddressLine &line : lines) {
|
||||
for (const DecompilerColumnLine &line : lines) {
|
||||
if (pos == line.start) {
|
||||
return line.shiftBefore;
|
||||
}
|
||||
@@ -1350,33 +1784,62 @@ static size_t shiftForEndPosition(const QVector<DecompilerAddressLine> &lines, s
|
||||
return lines.isEmpty() ? 0 : lines.last().shiftInLine;
|
||||
}
|
||||
|
||||
static RCodeMeta *copyCodeMetaWithAddressColumn(RCodeMeta *source)
|
||||
static RCodeMeta *copyCodeMetaWithColumns(
|
||||
RCodeMeta *source,
|
||||
bool showAddresses,
|
||||
bool showDisassembly,
|
||||
QHash<RVA, QString> &disassemblyCache)
|
||||
{
|
||||
RCodeMeta *display = copyCodeMeta(source);
|
||||
QString text(display->code);
|
||||
QVector<DecompilerAddressLine> lines = collectAddressLines(text);
|
||||
QVector<DecompilerColumnLine> lines = collectAddressLines(text);
|
||||
if (!assignLineAddresses(lines, display)) {
|
||||
return display;
|
||||
}
|
||||
|
||||
int addressWidth = RAddressString(0).size();
|
||||
for (const DecompilerAddressLine &line : lines) {
|
||||
int disassemblyWidth = 0;
|
||||
for (DecompilerColumnLine &line : lines) {
|
||||
if (line.address != RVA_INVALID) {
|
||||
addressWidth
|
||||
= std::max(addressWidth, static_cast<int>(RAddressString(line.address).size()));
|
||||
if (showDisassembly) {
|
||||
auto instruction = disassemblyCache.constFind(line.address);
|
||||
if (instruction == disassemblyCache.constEnd()) {
|
||||
const QString assembly
|
||||
= Core()
|
||||
->cmdRawAt(
|
||||
"pi 1 @e:scr.color=0,scr.html=false,asm.pseudo=false,"
|
||||
"asm.esil=false,asm.addr=false",
|
||||
line.address)
|
||||
.simplified();
|
||||
instruction = disassemblyCache.insert(line.address, assembly);
|
||||
}
|
||||
line.disassembly = instruction.value();
|
||||
disassemblyWidth
|
||||
= std::max(disassemblyWidth, static_cast<int>(line.disassembly.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString rendered;
|
||||
const int prefixWidth = addressWidth + 2;
|
||||
const int prefixWidth = (showAddresses ? addressWidth + 2 : 0)
|
||||
+ (showDisassembly ? disassemblyWidth + 3 : 0);
|
||||
rendered.reserve(text.size() + lines.size() * prefixWidth);
|
||||
size_t inserted = 0;
|
||||
for (DecompilerAddressLine &line : lines) {
|
||||
QString prefix
|
||||
= line.address == RVA_INVALID
|
||||
? QString(addressWidth, QLatin1Char(' '))
|
||||
: RAddressString(line.address).rightJustified(addressWidth, QLatin1Char(' '));
|
||||
prefix.append(QStringLiteral(" "));
|
||||
for (DecompilerColumnLine &line : lines) {
|
||||
QString prefix;
|
||||
if (showAddresses) {
|
||||
prefix
|
||||
= line.address == RVA_INVALID
|
||||
? QString(addressWidth, QLatin1Char(' '))
|
||||
: RAddressString(line.address).rightJustified(addressWidth, QLatin1Char(' '));
|
||||
prefix.append(QStringLiteral(" "));
|
||||
}
|
||||
if (showDisassembly) {
|
||||
prefix.append(line.disassembly.leftJustified(disassemblyWidth, QLatin1Char(' ')));
|
||||
prefix.append(QStringLiteral(" | "));
|
||||
}
|
||||
line.shiftBefore = inserted;
|
||||
line.shiftInLine = inserted + static_cast<size_t>(prefix.size());
|
||||
inserted += static_cast<size_t>(prefix.size());
|
||||
@@ -1400,8 +1863,10 @@ static RCodeMeta *copyCodeMetaWithAddressColumn(RCodeMeta *source)
|
||||
RCodeMetaItem *annotation = reinterpret_cast<RCodeMetaItem *>(iter);
|
||||
#endif
|
||||
if (annotation->type == R_CODEMETA_TYPE_OFFSET) {
|
||||
for (const DecompilerAddressLine &line : lines) {
|
||||
if (annotation->start >= line.nextStart || annotation->end <= line.start) {
|
||||
for (const DecompilerColumnLine &line : lines) {
|
||||
// pdc ranges can start on the preceding newline. Only extend
|
||||
// the range to the prefix of a line with annotated text.
|
||||
if (annotation->start >= line.end || annotation->end <= line.start) {
|
||||
continue;
|
||||
}
|
||||
annotation->start = line.start + line.shiftBefore;
|
||||
@@ -1422,19 +1887,28 @@ static RCodeMeta *copyCodeMetaWithAddressColumn(RCodeMeta *source)
|
||||
|
||||
void DecompilerWidget::setCode(RCodeMeta *newCode)
|
||||
{
|
||||
symbolTargets.clear();
|
||||
outgoingXRefsCache.clear();
|
||||
symbolTargetsReady = false;
|
||||
sourceCode.reset(newCode);
|
||||
updateDisplayedCode();
|
||||
disassemblyCache.clear();
|
||||
updateDisplayedCode(previousFunctionAddr == decompiledFunctionAddr);
|
||||
}
|
||||
|
||||
void DecompilerWidget::updateDisplayedCode()
|
||||
void DecompilerWidget::updateDisplayedCode(bool preserveFolds)
|
||||
{
|
||||
hoveredToken = {};
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
connectCursorPositionChanged(false);
|
||||
mCtxMenu->setAnnotationHere(nullptr);
|
||||
if (syntaxHighlighter) {
|
||||
syntaxHighlighter->setDocument(nullptr);
|
||||
}
|
||||
this->code.reset(
|
||||
showAddressColumn ? copyCodeMetaWithAddressColumn(sourceCode.get())
|
||||
: copyCodeMeta(sourceCode.get()));
|
||||
showAddressColumn || showDisassemblyColumn
|
||||
? copyCodeMetaWithColumns(
|
||||
sourceCode.get(), showAddressColumn, showDisassemblyColumn, disassemblyCache)
|
||||
: copyCodeMeta(sourceCode.get()));
|
||||
QString text = remapAnnotationOffsetsToQString(*this->code);
|
||||
setHighlighter(hasCodeMetaSyntaxHighlights(this->code.get()));
|
||||
|
||||
@@ -1443,7 +1917,8 @@ void DecompilerWidget::updateDisplayedCode()
|
||||
// issues), temporarily detach the highlighter, update the document, then
|
||||
// reattach and schedule a queued rehighlight. This minimizes the risk of
|
||||
// crashes in the regex engine during synchronous document updates.
|
||||
this->ui->textEdit->setPlainText(text);
|
||||
ui->textEdit->setDecompiledCode(text, QString::fromUtf8(sourceCode->code), preserveFolds);
|
||||
updateCursorContext();
|
||||
connectCursorPositionChanged(true);
|
||||
|
||||
if (syntaxHighlighter) {
|
||||
@@ -1452,6 +1927,7 @@ void DecompilerWidget::updateDisplayedCode()
|
||||
// event has been processed to avoid re-entrancy.
|
||||
QMetaObject::invokeMethod(syntaxHighlighter, "rehighlight", Qt::QueuedConnection);
|
||||
}
|
||||
refreshDecompilerHover();
|
||||
}
|
||||
|
||||
void DecompilerWidget::setAddressColumnVisible(bool visible)
|
||||
@@ -1461,11 +1937,26 @@ void DecompilerWidget::setAddressColumnVisible(bool visible)
|
||||
}
|
||||
showAddressColumn = visible;
|
||||
Config()->setDecompilerShowAddresses(visible);
|
||||
refreshDisplayColumns();
|
||||
}
|
||||
|
||||
void DecompilerWidget::setDisassemblyColumnVisible(bool visible)
|
||||
{
|
||||
if (showDisassemblyColumn == visible) {
|
||||
return;
|
||||
}
|
||||
showDisassemblyColumn = visible;
|
||||
Config()->setDecompilerShowDisassembly(visible);
|
||||
refreshDisplayColumns();
|
||||
}
|
||||
|
||||
void DecompilerWidget::refreshDisplayColumns()
|
||||
{
|
||||
int horizontalPosition = ui->textEdit->horizontalScrollBar()->sliderPosition();
|
||||
int verticalPosition = ui->textEdit->verticalScrollBar()->sliderPosition();
|
||||
updateDisplayedCode();
|
||||
updateCursorPosition();
|
||||
setAnnotationsAtCursor(ui->textEdit->textCursor().position());
|
||||
setInfoForBreakpoints();
|
||||
highlightPC();
|
||||
highlightBreakpoints();
|
||||
ui->textEdit->horizontalScrollBar()->setSliderPosition(horizontalPosition);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define DECOMPILERWIDGET_H
|
||||
|
||||
#include <memory>
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
#include <QSyntaxHighlighter>
|
||||
#include <QTextEdit>
|
||||
@@ -37,6 +38,8 @@ public slots:
|
||||
void highlightPC();
|
||||
void setAddressColumnVisible(bool visible);
|
||||
bool isAddressColumnVisible() const { return showAddressColumn; }
|
||||
void setDisassemblyColumnVisible(bool visible);
|
||||
bool isDisassemblyColumnVisible() const { return showDisassemblyColumn; }
|
||||
private slots:
|
||||
/**
|
||||
* @brief Copy to clipboard what's needed depending on the state of text
|
||||
@@ -90,6 +93,15 @@ private:
|
||||
QPointer<QSyntaxHighlighter> syntaxHighlighter;
|
||||
bool decompilerSelectionEnabled;
|
||||
bool showAddressColumn = false;
|
||||
bool showDisassemblyColumn = false;
|
||||
QHash<RVA, QString> disassemblyCache;
|
||||
QHash<QString, RVA> symbolTargets;
|
||||
QHash<RVA, QList<XrefDescription>> outgoingXRefsCache;
|
||||
bool symbolTargetsReady = false;
|
||||
QTextCursor hoveredToken;
|
||||
QPoint tokenPressPosition;
|
||||
RVA pressedTokenTarget = RVA_INVALID;
|
||||
bool followedTokenOnClick = false;
|
||||
|
||||
ut64 currentOffset = UT64_MAX;
|
||||
/**
|
||||
@@ -114,12 +126,12 @@ private:
|
||||
* Specifies the lowest offset of instructions among all the instructions in
|
||||
* the decompiled function.
|
||||
*/
|
||||
RVA lowestOffsetInCode;
|
||||
RVA lowestOffsetInCode = RVA_MAX;
|
||||
/**
|
||||
* Specifies the highest offset of instructions among all the instructions
|
||||
* in the decompiled function.
|
||||
*/
|
||||
RVA highestOffsetInCode;
|
||||
RVA highestOffsetInCode = 0;
|
||||
|
||||
/**
|
||||
* @brief Gets the current decompiler selected by the user.
|
||||
@@ -129,6 +141,7 @@ private:
|
||||
Decompiler *getCurrentDecompiler();
|
||||
|
||||
void refreshOptionsCombo();
|
||||
void refreshDisplayColumns();
|
||||
|
||||
/**
|
||||
* @brief Calls the function doRefresh() if the address specified is a part
|
||||
@@ -164,6 +177,15 @@ private:
|
||||
* - Program Counter(PC) while debugging
|
||||
*/
|
||||
void updateSelection();
|
||||
void setDecompilerSelections(QList<QTextEdit::ExtraSelection> selections);
|
||||
void setHoveredToken(const QTextCursor &cursor);
|
||||
void updateDecompilerHover(const QPoint &pos, Qt::MouseButtons buttons);
|
||||
void refreshDecompilerHover();
|
||||
QTextCursor cursorOnCharacter(const QPoint &pos) const;
|
||||
RVA tokenTarget(QTextCursor &cursor);
|
||||
void invalidateTokenTargets();
|
||||
void cacheSymbolTarget(const QString &name, RVA address);
|
||||
RVA symbolTarget(const QString &name, RVA offset);
|
||||
/**
|
||||
* @brief Connect/Disconnect SIGNAL-SLOT connection that deals with changes
|
||||
* in cursor position.
|
||||
@@ -175,6 +197,7 @@ private:
|
||||
* @param connectPositionChange
|
||||
*/
|
||||
void connectCursorPositionChanged(bool connectPositionChange);
|
||||
RVA updateCursorContext();
|
||||
/**
|
||||
* @brief Find the current global offset in sync and update cursor
|
||||
* to the position specified by this offset (found using positionForOffset()
|
||||
@@ -183,6 +206,7 @@ private:
|
||||
void updateCursorPosition();
|
||||
|
||||
QString getWindowTitle() const override;
|
||||
QWidget *widgetToFocusOnRaise() override;
|
||||
|
||||
/**
|
||||
* @brief Event filter that intercept the following events:
|
||||
@@ -290,7 +314,7 @@ private:
|
||||
bool addressInRange(RVA addr);
|
||||
|
||||
void setCode(RCodeMeta *code);
|
||||
void updateDisplayedCode();
|
||||
void updateDisplayedCode(bool preserveFolds = true);
|
||||
|
||||
void setHighlighter(bool codeMetaHighlighter);
|
||||
bool isSourceDecompilerSelected() const;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="textEdit">
|
||||
<widget class="DecompilerTextEdit" name="textEdit">
|
||||
<property name="lineWrapMode">
|
||||
<enum>QPlainTextEdit::NoWrap</enum>
|
||||
</property>
|
||||
@@ -150,6 +150,13 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DecompilerTextEdit</class>
|
||||
<extends>QPlainTextEdit</extends>
|
||||
<header>widgets/DecompilerTextEdit.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "common/TempConfig.h"
|
||||
#include "core/Iaito.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "widgets/ExceptionBar.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
@@ -117,6 +118,11 @@ DisassemblerGraphView::DisassemblerGraphView(
|
||||
{
|
||||
highlight_token = nullptr;
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
exceptionBar = new ExceptionBar(this);
|
||||
layout->addWidget(exceptionBar);
|
||||
connect(exceptionBar, &ExceptionBar::seekRequested, this, [this](quint64 address) {
|
||||
this->seekable->seek(address);
|
||||
});
|
||||
// Signals that require a refresh all
|
||||
connect(Core(), &IaitoCore::refreshAll, this, &DisassemblerGraphView::refreshView);
|
||||
connect(Core(), &IaitoCore::commentsChanged, this, &DisassemblerGraphView::refreshView);
|
||||
@@ -153,6 +159,13 @@ DisassemblerGraphView::DisassemblerGraphView(
|
||||
contextMenu->addAction(&actionExportGraph);
|
||||
contextMenu->addMenu(layoutMenu);
|
||||
addBasicBlockContentMenu();
|
||||
showExceptionsAction = contextMenu->addAction(tr("Show exception regions"));
|
||||
showExceptionsAction->setCheckable(true);
|
||||
showExceptionsAction->setChecked(true);
|
||||
showExceptionsAction->setToolTip(tr(
|
||||
"Colored bubbles mark protected blocks. Dashed arrows show possible exception transfers."));
|
||||
connect(showExceptionsAction, &QAction::toggled, this, &DisassemblerGraphView::refreshView);
|
||||
exceptionMenu = blockMenu->addMenu(tr("Exception handlers"));
|
||||
if (mainWindow) {
|
||||
if (QAction *overviewAction = mainWindow->getOverviewAction()) {
|
||||
contextMenu->addAction(overviewAction);
|
||||
@@ -338,6 +351,12 @@ void DisassemblerGraphView::loadCurrentGraph()
|
||||
disassembly_blocks.clear();
|
||||
blocks.clear();
|
||||
outgoingXRefsCache.clear();
|
||||
exceptionEdges.clear();
|
||||
exceptionShapes.clear();
|
||||
exceptionRegions = fcn ? Core()->getExceptionRegions(currentFcnAddr) : QList<ExceptionRegion>();
|
||||
exceptionBar->setRegions(exceptionRegions, seekable->getOffset());
|
||||
setViewportMargins(0, exceptionRegions.isEmpty() ? 0 : exceptionBar->sizeHint().height(), 0, 0);
|
||||
showExceptionsAction->setVisible(!exceptionRegions.isEmpty());
|
||||
|
||||
if (highlight_token) {
|
||||
delete highlight_token;
|
||||
@@ -426,6 +445,9 @@ void DisassemblerGraphView::loadCurrentGraph()
|
||||
|
||||
addBlock(gb);
|
||||
}
|
||||
if (showExceptionsAction->isChecked()) {
|
||||
addExceptionBlocks();
|
||||
}
|
||||
cleanupEdges(blocks);
|
||||
|
||||
if (!func["blocks"].toArray().isEmpty()) {
|
||||
@@ -433,6 +455,146 @@ void DisassemblerGraphView::loadCurrentGraph()
|
||||
}
|
||||
}
|
||||
|
||||
void DisassemblerGraphView::addExceptionBlocks()
|
||||
{
|
||||
for (const auto ®ion : exceptionRegions) {
|
||||
if (!blockForAddress(region.handler)) {
|
||||
// The handler may be in another function or not analyzed yet. Show
|
||||
// a navigation node without creating or changing analysis blocks.
|
||||
DisassemblyBlock db;
|
||||
db.entry = region.handler;
|
||||
db.size = 1;
|
||||
db.externalHandler = true;
|
||||
appendBlockContentLine(db, tr("Handler outside this function"), region.handler, 1);
|
||||
appendBlockContentLine(db, tr("Double-click to open"), region.handler, 1);
|
||||
disassembly_blocks[db.entry] = db;
|
||||
GraphBlock block;
|
||||
block.entry = db.entry;
|
||||
addBlock(block);
|
||||
}
|
||||
}
|
||||
for (auto &item : disassembly_blocks) {
|
||||
auto &db = item.second;
|
||||
QStringList tags;
|
||||
for (const auto ®ion : exceptionRegions) {
|
||||
if (!db.externalHandler && region.protects(db.entry, db.size)) {
|
||||
tags << tr("TRY #%1").arg(region.index);
|
||||
const auto *handler = blockForAddress(region.handler);
|
||||
auto &edges = blocks.at(db.entry).edges;
|
||||
if (handler && handler->entry != db.entry
|
||||
&& std::none_of(edges.begin(), edges.end(), [handler](const GraphEdge &edge) {
|
||||
return edge.target == handler->entry;
|
||||
})) {
|
||||
edges.emplace_back(handler->entry);
|
||||
exceptionEdges[{db.entry, handler->entry}] = region.color();
|
||||
}
|
||||
}
|
||||
if (region.handler >= db.entry && region.handler - db.entry < db.size) {
|
||||
tags << tr("HANDLER #%1").arg(region.index);
|
||||
}
|
||||
}
|
||||
if (!tags.isEmpty()) {
|
||||
db.header_text = Text(
|
||||
QStringLiteral("[%1] %2")
|
||||
.arg(RAddressString(db.entry), tags.join(QStringLiteral(" · "))),
|
||||
ConfigColor(ADDR),
|
||||
Qt::transparent);
|
||||
}
|
||||
prepareGraphNode(blocks.at(db.entry));
|
||||
}
|
||||
}
|
||||
|
||||
void DisassemblerGraphView::graphLayoutChanged()
|
||||
{
|
||||
exceptionShapes.clear();
|
||||
if (exceptionRegions.isEmpty() || !showExceptionsAction->isChecked()) {
|
||||
return;
|
||||
}
|
||||
// Keep the bubbles inside image export and scrollable graph bounds.
|
||||
for (auto &item : blocks) {
|
||||
item.second.x += 12;
|
||||
item.second.y += 12;
|
||||
for (auto &edge : item.second.edges) {
|
||||
edge.polyline.translate(12, 12);
|
||||
}
|
||||
}
|
||||
width += 24;
|
||||
height += 24;
|
||||
QSet<QPair<quint64, quint64>> seen;
|
||||
for (const auto ®ion : exceptionRegions) {
|
||||
const auto range = qMakePair(region.from, region.to);
|
||||
if (seen.contains(range)) {
|
||||
continue;
|
||||
}
|
||||
seen.insert(range);
|
||||
QRectF bounds;
|
||||
for (const auto &item : blocks) {
|
||||
const auto &block = item.second;
|
||||
const auto &db = disassembly_blocks.at(block.entry);
|
||||
if (db.externalHandler || !region.protects(db.entry, db.size)) {
|
||||
continue;
|
||||
}
|
||||
bounds = bounds.united(QRectF(block.x, block.y, block.width, block.height));
|
||||
}
|
||||
if (bounds.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// One envelope covers every protected block, including disconnected
|
||||
// blocks and the space between them. Membership comes from the entire
|
||||
// half-open metadata range, not just its boundary instructions.
|
||||
QPainterPath shape;
|
||||
shape.addRoundedRect(bounds.adjusted(-8, -8, 8, 8), 12, 12);
|
||||
for (const auto &item : blocks) {
|
||||
const auto &block = item.second;
|
||||
const auto &db = disassembly_blocks.at(block.entry);
|
||||
if (db.externalHandler || !region.protects(db.entry, db.size)) {
|
||||
QPainterPath excluded;
|
||||
excluded.addRect(
|
||||
QRectF(block.x - 2, block.y - 2, block.width + 4, block.height + 4));
|
||||
shape = shape.subtracted(excluded);
|
||||
}
|
||||
}
|
||||
exceptionShapes.append({region, shape});
|
||||
}
|
||||
// Keep inner regions readable over their enclosing regions.
|
||||
std::stable_sort(
|
||||
exceptionShapes.begin(),
|
||||
exceptionShapes.end(),
|
||||
[](const ExceptionShape &a, const ExceptionShape &b) {
|
||||
const QRectF ar = a.path.boundingRect();
|
||||
const QRectF br = b.path.boundingRect();
|
||||
return ar.width() * ar.height() > br.width() * br.height();
|
||||
});
|
||||
}
|
||||
|
||||
void DisassemblerGraphView::drawBackground(QPainter &p, bool interactive)
|
||||
{
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
for (const auto &shape : exceptionShapes) {
|
||||
const bool active = interactive && shape.region.contains(seekable->getOffset());
|
||||
QColor fill = shape.region.color();
|
||||
fill.setAlpha(active ? 48 : 22);
|
||||
p.setBrush(fill);
|
||||
QPen outline(shape.region.color(), active ? 2 : 1);
|
||||
outline.setStyle(Qt::DashLine);
|
||||
p.setPen(outline);
|
||||
p.drawPath(shape.path);
|
||||
}
|
||||
}
|
||||
|
||||
QString DisassemblerGraphView::exceptionTooltip(RVA address, RVA size) const
|
||||
{
|
||||
QStringList details;
|
||||
for (const auto ®ion : exceptionRegions) {
|
||||
if (region.touches(address, size)) {
|
||||
details << region.description();
|
||||
}
|
||||
}
|
||||
return details.join(QStringLiteral("\n\n"))
|
||||
.toHtmlEscaped()
|
||||
.replace(QLatin1Char('\n'), QStringLiteral("<br>"));
|
||||
}
|
||||
|
||||
DisassemblerGraphView::EdgeConfigurationMapping DisassemblerGraphView::getEdgeConfigurations()
|
||||
{
|
||||
EdgeConfigurationMapping result;
|
||||
@@ -478,8 +640,7 @@ DisassemblerGraphView::MinimapBars DisassemblerGraphView::getMinimapBars()
|
||||
}
|
||||
}
|
||||
};
|
||||
const bool hasTitleBar = Config()->getGraphBlockEntryOffset()
|
||||
&& !db.header_text.lines.empty();
|
||||
const bool hasTitleBar = !db.header_text.lines.empty();
|
||||
if (hasTitleBar) {
|
||||
y += int(db.header_text.lines.size()) * charHeight;
|
||||
} else {
|
||||
@@ -682,7 +843,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block,
|
||||
free(s);
|
||||
}
|
||||
|
||||
const bool hasTitleBar = Config()->getGraphBlockEntryOffset() && !db.header_text.lines.empty();
|
||||
const bool hasTitleBar = !db.header_text.lines.empty();
|
||||
|
||||
// Draw basic block body with the default background.
|
||||
p.drawRect(blockRect);
|
||||
@@ -733,7 +894,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block,
|
||||
const int luma = (titleBg.red() * 299 + titleBg.green() * 587 + titleBg.blue() * 114)
|
||||
/ 1000;
|
||||
const QColor titleTextColor = (luma > 128) ? QColor(Qt::black) : QColor(Qt::white);
|
||||
const QString titleText = "[" + RAddressString(db.entry) + "]";
|
||||
const QString titleText = db.header_text.ToQString();
|
||||
QFont titleFont = Config()->getFont();
|
||||
titleFont.setBold(true);
|
||||
p.setPen(titleTextColor);
|
||||
@@ -907,7 +1068,11 @@ GraphView::EdgeConfiguration DisassemblerGraphView::edgeConfiguration(
|
||||
{
|
||||
EdgeConfiguration ec;
|
||||
DisassemblyBlock &db = disassembly_blocks[from.entry];
|
||||
if (to->entry == db.true_path) {
|
||||
const auto exception = exceptionEdges.find({from.entry, to->entry});
|
||||
if (exception != exceptionEdges.end()) {
|
||||
ec.color = exception->second;
|
||||
ec.lineStyle = Qt::DashLine;
|
||||
} else if (to->entry == db.true_path) {
|
||||
ec.color = brtrueColor;
|
||||
} else if (to->entry == db.false_path) {
|
||||
ec.color = brfalseColor;
|
||||
@@ -1144,6 +1309,7 @@ void DisassemblerGraphView::applyAddressRangeSelection(RVA start, RVA end)
|
||||
|
||||
void DisassemblerGraphView::onSeekChanged(RVA addr)
|
||||
{
|
||||
exceptionBar->setAddress(addr);
|
||||
blockMenu->setOffset(addr);
|
||||
DisassemblyBlock *db = blockForAddress(addr);
|
||||
bool switchFunction = false;
|
||||
@@ -1172,8 +1338,13 @@ void DisassemblerGraphView::takeTrue()
|
||||
|
||||
if (db->true_path != RVA_INVALID) {
|
||||
seekable->seek(db->true_path);
|
||||
} else if (!blocks[db->entry].edges.empty()) {
|
||||
seekable->seek(blocks[db->entry].edges[0].target);
|
||||
} else {
|
||||
for (const auto &edge : blocks[db->entry].edges) {
|
||||
if (!exceptionEdges.count({db->entry, edge.target})) {
|
||||
seekable->seek(edge.target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1186,8 +1357,13 @@ void DisassemblerGraphView::takeFalse()
|
||||
|
||||
if (db->false_path != RVA_INVALID) {
|
||||
seekable->seek(db->false_path);
|
||||
} else if (!blocks[db->entry].edges.empty()) {
|
||||
seekable->seek(blocks[db->entry].edges[0].target);
|
||||
} else {
|
||||
for (const auto &edge : blocks[db->entry].edges) {
|
||||
if (!exceptionEdges.count({db->entry, edge.target})) {
|
||||
seekable->seek(edge.target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1538,6 +1714,7 @@ void DisassemblerGraphView::blockContextMenuRequested(
|
||||
? offset
|
||||
: static_cast<RVA>(block.entry);
|
||||
blockMenu->setOffset(menuOffset);
|
||||
exceptionBar->populateMenu(exceptionMenu, block.entry, disassembly_blocks.at(block.entry).size);
|
||||
actionUnhighlight.setVisible(Core()->getBBHighlighter()->getBasicBlock(block.entry));
|
||||
actionUnhighlightInstruction.setVisible(
|
||||
instrForAddress(menuOffset) && Core()->getBIHighlighter()->getBasicInstruction(menuOffset));
|
||||
@@ -1574,6 +1751,11 @@ void DisassemblerGraphView::blockDoubleClicked(
|
||||
GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
if (disassembly_blocks.at(block.entry).externalHandler) {
|
||||
seekable->seek(block.entry);
|
||||
refreshView();
|
||||
return;
|
||||
}
|
||||
seekable->seekToReference(getAddrForMouseEvent(block, &pos));
|
||||
}
|
||||
|
||||
@@ -1581,8 +1763,15 @@ void DisassemblerGraphView::blockHelpEvent(
|
||||
GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos)
|
||||
{
|
||||
Instr *instr = getInstrForMouseEvent(block, &pos);
|
||||
const auto &db = disassembly_blocks.at(block.entry);
|
||||
const QString exceptions = exceptionTooltip(
|
||||
instr ? instr->addr : db.entry, instr ? qMax<ut64>(instr->size, 1) : db.size);
|
||||
if (!instr || instr->fullText.lines.empty()) {
|
||||
if (!instr) {
|
||||
if (!exceptions.isEmpty()) {
|
||||
QToolTip::showText(event->globalPos(), exceptions, viewport());
|
||||
return;
|
||||
}
|
||||
QToolTip::hideText();
|
||||
event->ignore();
|
||||
return;
|
||||
@@ -1604,6 +1793,10 @@ void DisassemblerGraphView::blockHelpEvent(
|
||||
}
|
||||
}
|
||||
|
||||
if (!exceptions.isEmpty()) {
|
||||
QToolTip::showText(event->globalPos(), exceptions, viewport());
|
||||
return;
|
||||
}
|
||||
if (instr->fullText.lines.empty()) {
|
||||
QToolTip::hideText();
|
||||
event->ignore();
|
||||
@@ -1616,6 +1809,26 @@ void DisassemblerGraphView::blockHelpEvent(
|
||||
bool DisassemblerGraphView::helpEvent(QHelpEvent *event)
|
||||
{
|
||||
if (!GraphView::helpEvent(event)) {
|
||||
const QPoint position = viewToLogicalCoordinates(event->pos());
|
||||
QStringList details;
|
||||
for (const auto &shape : exceptionShapes) {
|
||||
if (shape.path.contains(position)) {
|
||||
for (const auto ®ion : exceptionRegions) {
|
||||
if (region.from == shape.region.from && region.to == shape.region.to) {
|
||||
details << region.description();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!details.isEmpty()) {
|
||||
QToolTip::showText(
|
||||
event->globalPos(),
|
||||
details.join(QStringLiteral("\n\n"))
|
||||
.toHtmlEscaped()
|
||||
.replace(QLatin1Char('\n'), QStringLiteral("<br>")),
|
||||
viewport());
|
||||
return true;
|
||||
}
|
||||
QToolTip::hideText();
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QHash>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QShortcut>
|
||||
#include <QStringList>
|
||||
#include <QWidget>
|
||||
@@ -18,6 +19,7 @@
|
||||
class QTextEdit;
|
||||
class FallbackSyntaxHighlighter;
|
||||
class QJsonArray;
|
||||
class ExceptionBar;
|
||||
|
||||
class DisassemblerGraphView : public IaitoGraphView
|
||||
{
|
||||
@@ -90,6 +92,7 @@ class DisassemblerGraphView : public IaitoGraphView
|
||||
ut64 false_path = 0;
|
||||
bool terminal = false;
|
||||
bool indirectcall = false;
|
||||
bool externalHandler = false;
|
||||
};
|
||||
|
||||
enum class BasicBlockContent {
|
||||
@@ -159,6 +162,8 @@ public slots:
|
||||
void applyAddressRangeSelection(RVA start, RVA end);
|
||||
|
||||
protected:
|
||||
void drawBackground(QPainter &p, bool interactive) override;
|
||||
void graphLayoutChanged() override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void blockContextMenuRequested(
|
||||
GraphView::GraphBlock &block, QContextMenuEvent *event, QPoint pos) override;
|
||||
@@ -188,6 +193,19 @@ private:
|
||||
QMenu *contextMenu;
|
||||
BasicBlockContent basicBlockContent = BasicBlockContent::Disassembly;
|
||||
QHash<RVA, QList<XrefDescription>> outgoingXRefsCache;
|
||||
QList<ExceptionRegion> exceptionRegions;
|
||||
struct ExceptionShape
|
||||
{
|
||||
ExceptionRegion region;
|
||||
QPainterPath path;
|
||||
};
|
||||
QList<ExceptionShape> exceptionShapes;
|
||||
std::map<std::pair<ut64, ut64>, QColor> exceptionEdges;
|
||||
ExceptionBar *exceptionBar;
|
||||
QMenu *exceptionMenu;
|
||||
QAction *showExceptionsAction;
|
||||
void addExceptionBlocks();
|
||||
QString exceptionTooltip(RVA address, RVA size) const;
|
||||
|
||||
void connectSeekChanged(bool disconnect);
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
#include "common/TempConfig.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "dialogs/ShortcutKeysDialog.h"
|
||||
#include "menus/ContextMenuIcons.h"
|
||||
#include "menus/DisassemblyContextMenu.h"
|
||||
|
||||
#include "widgets/ExceptionBar.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
@@ -29,6 +31,7 @@
|
||||
#include <QSplitter>
|
||||
#include <QTextBlock>
|
||||
#include <QTextBlockUserData>
|
||||
#include <QTextLayout>
|
||||
#include <QToolTip>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWheelEvent>
|
||||
@@ -67,22 +70,29 @@ static int getMaxVisibleDisassemblyLines(QPlainTextEdit *textEdit)
|
||||
return qMax(1, int(std::ceil(availableHeight / lineHeight)));
|
||||
}
|
||||
|
||||
static QString tokenAtCursor(QTextCursor cursor)
|
||||
static QTextCursor tokenRangeAtCursor(QTextCursor cursor)
|
||||
{
|
||||
const int clickedCharPos = cursor.positionInBlock();
|
||||
cursor.select(QTextCursor::BlockUnderCursor);
|
||||
const QString line = cursor.selectedText().replace("\xc2\xa0", " ");
|
||||
const QString line = cursor.block().text().replace("\xc2\xa0", " ");
|
||||
|
||||
static const QRegularExpression tokenRegExp(R"(\b(?<!\.)([^\s]+)\b(?!\.))");
|
||||
QRegularExpressionMatchIterator i = tokenRegExp.globalMatch(line);
|
||||
while (i.hasNext()) {
|
||||
QRegularExpressionMatch match = i.next();
|
||||
if (match.capturedStart() <= clickedCharPos && match.capturedEnd() > clickedCharPos) {
|
||||
return match.captured();
|
||||
const int start = cursor.block().position() + match.capturedStart();
|
||||
cursor.setPosition(start);
|
||||
cursor.setPosition(start + match.capturedLength(), QTextCursor::KeepAnchor);
|
||||
return cursor;
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
return {};
|
||||
}
|
||||
|
||||
static QString tokenAtCursor(const QTextCursor &cursor)
|
||||
{
|
||||
return tokenRangeAtCursor(cursor).selectedText();
|
||||
}
|
||||
|
||||
static QString wordAtCursor(QTextCursor cursor)
|
||||
@@ -176,7 +186,17 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
|
||||
auto *container = new QWidget;
|
||||
container->setMinimumHeight(0);
|
||||
container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored);
|
||||
auto *containerLayout = new QHBoxLayout(container);
|
||||
auto *outerLayout = new QVBoxLayout(container);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
outerLayout->setSpacing(0);
|
||||
exceptionBar = new ExceptionBar(container);
|
||||
outerLayout->addWidget(exceptionBar);
|
||||
connect(exceptionBar, &ExceptionBar::seekRequested, this, [this](quint64 address) {
|
||||
seekable->seek(address);
|
||||
});
|
||||
exceptionMenu = mCtxMenu->addMenu(tr("Exception handlers"));
|
||||
auto *containerLayout = new QHBoxLayout;
|
||||
outerLayout->addLayout(containerLayout, 1);
|
||||
containerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
containerLayout->setSpacing(0);
|
||||
containerLayout->addWidget(splitter);
|
||||
@@ -221,6 +241,11 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
|
||||
// Event filter to intercept double clicks in the textbox
|
||||
// and showing tooltips when hovering above those offsets
|
||||
mDisasTextEdit->viewport()->installEventFilter(this);
|
||||
connect(
|
||||
mDisasTextEdit->horizontalScrollBar(),
|
||||
&QScrollBar::valueChanged,
|
||||
this,
|
||||
&DisassemblyWidget::refreshDisassemblyHover);
|
||||
|
||||
// Set Disas context menu
|
||||
mDisasTextEdit->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
@@ -283,6 +308,25 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
|
||||
this,
|
||||
&DisassemblyWidget::on_refreshContents);
|
||||
|
||||
connect(
|
||||
mCtxMenu,
|
||||
&DisassemblyContextMenu::flagAboutToChange,
|
||||
this,
|
||||
&DisassemblyWidget::updateContextMenuState);
|
||||
mCtxMenu->registerFlagShortcut();
|
||||
auto *followAction = new QAction(tr("Follow reference"), this);
|
||||
ShortcutMgr()->bindAction("disasm.jumpToOffset", followAction);
|
||||
followAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||
followAction->setAutoRepeat(false);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
followAction->setShortcutVisibleInContextMenu(true);
|
||||
#endif
|
||||
ContextMenuIcons::setIcon(
|
||||
this, followAction, ContextMenuIcons::Icon::Navigation, QColor(103, 80, 164));
|
||||
connect(followAction, &QAction::triggered, this, [this]() {
|
||||
jumpToOffsetUnderCursor(mDisasTextEdit->textCursor());
|
||||
});
|
||||
mCtxMenu->insertAction(mCtxMenu->actions().first(), followAction);
|
||||
addActions(mCtxMenu->actions());
|
||||
|
||||
#define ADD_ACTION(id, ctx, slot) \
|
||||
@@ -298,8 +342,18 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
|
||||
mainWindow->showMemoryWidget(MemoryWidgetType::Graph);
|
||||
})
|
||||
|
||||
ADD_ACTION("disasm.switchToDecompiler", Qt::WidgetWithChildrenShortcut, [this] {
|
||||
mainWindow->showMemoryWidget(MemoryWidgetType::Decompiler);
|
||||
})
|
||||
|
||||
ADD_ACTION("disasm.seekPrev", Qt::WidgetWithChildrenShortcut, &DisassemblyWidget::seekPrev)
|
||||
|
||||
ADD_ACTION("disasm.cursorLeft", Qt::WidgetWithChildrenShortcut, [this]() {
|
||||
mDisasTextEdit->moveCursor(QTextCursor::Left);
|
||||
})
|
||||
ADD_ACTION("disasm.cursorRight", Qt::WidgetWithChildrenShortcut, [this]() {
|
||||
mDisasTextEdit->moveCursor(QTextCursor::Right);
|
||||
})
|
||||
ADD_ACTION("disasm.cursorDown", Qt::WidgetWithChildrenShortcut, [this]() {
|
||||
moveCursorRelative(false, false);
|
||||
})
|
||||
@@ -317,13 +371,15 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
|
||||
|
||||
void DisassemblyWidget::setPreviewMode(bool previewMode)
|
||||
{
|
||||
setSeekHistoryBarEnabled(!previewMode);
|
||||
mDisasTextEdit->setContextMenuPolicy(previewMode ? Qt::NoContextMenu : Qt::CustomContextMenu);
|
||||
mCtxMenu->setEnabled(!previewMode);
|
||||
for (auto action : mCtxMenu->actions()) {
|
||||
action->setEnabled(!previewMode);
|
||||
}
|
||||
for (auto action : actions()) {
|
||||
if (action->shortcut() == Qt::Key_Space || action->shortcut() == Qt::Key_Escape) {
|
||||
if (action->shortcut() == Qt::Key_Space || action->shortcut() == Qt::Key_Tab
|
||||
|| action->shortcut() == Qt::Key_Escape) {
|
||||
action->setEnabled(!previewMode);
|
||||
}
|
||||
}
|
||||
@@ -498,6 +554,7 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
|
||||
return;
|
||||
}
|
||||
|
||||
setHoveredToken({});
|
||||
topOffset = Core()->alignInstructionAddress(topOffset);
|
||||
|
||||
if (maxLines <= 0) {
|
||||
@@ -511,6 +568,9 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
|
||||
breakpoints = Core()->getBreakpointsAddresses();
|
||||
|
||||
// Retrieve disassembly lines
|
||||
if (!cached) {
|
||||
exceptionRegions = Core()->getExceptionRegions();
|
||||
}
|
||||
outgoingXRefsCache.clear();
|
||||
const QList<DisassemblyLine> oldLines = lines;
|
||||
if (cached) {
|
||||
@@ -566,6 +626,7 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
|
||||
}
|
||||
}
|
||||
|
||||
updateExceptionBar(!cached);
|
||||
if (!cached || !updateDocumentIncrementally(oldLines, newVisible)) {
|
||||
rebuildDocument(newVisible);
|
||||
}
|
||||
@@ -573,6 +634,24 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
|
||||
addressScrollBar->setViewport(maxLines, bottomOffset > topOffset ? bottomOffset - topOffset : 1);
|
||||
addressScrollBar->setAddress(topOffset);
|
||||
addressScrollBar->setSeekAddress(seekable->getOffset());
|
||||
refreshDisassemblyHover();
|
||||
}
|
||||
|
||||
void DisassemblyWidget::updateExceptionBar(bool force)
|
||||
{
|
||||
const RVA address = seekable->getOffset();
|
||||
const RVA function = Core()->getFunctionStart(address);
|
||||
if (force || exceptionFunction != function) {
|
||||
exceptionFunction = function;
|
||||
QList<ExceptionRegion> regions;
|
||||
if (function != RVA_INVALID && !exceptionRegions.isEmpty()) {
|
||||
const auto blocks = Core()->cmdj("afbj @ " + RAddressString(function)).array();
|
||||
regions = ExceptionRegion::forBlocks(exceptionRegions, blocks);
|
||||
}
|
||||
exceptionBar->setRegions(regions, address);
|
||||
} else {
|
||||
exceptionBar->setAddress(address);
|
||||
}
|
||||
}
|
||||
|
||||
void DisassemblyWidget::decorateBlock(
|
||||
@@ -580,6 +659,14 @@ void DisassemblyWidget::decorateBlock(
|
||||
{
|
||||
QTextCursor cursor(block);
|
||||
QTextBlockFormat f;
|
||||
for (const auto ®ion : exceptionRegions) {
|
||||
if (region.contains(line.offset) || region.handler == line.offset) {
|
||||
QColor color = region.color();
|
||||
color.setAlpha(region.handler == line.offset ? 65 : 26);
|
||||
f.setBackground(color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Core()->isBreakpoint(breakpoints, line.offset)) {
|
||||
f.setBackground(ConfigColor("gui.breakpoint_background"));
|
||||
} else {
|
||||
@@ -644,7 +731,7 @@ bool DisassemblyWidget::documentMatchesLines(const QList<DisassemblyLine> &ref,
|
||||
return true;
|
||||
}
|
||||
|
||||
// Scrolls by editing only the rows that changed and blitting the rest of the view
|
||||
// Scrolls by editing only the rows that changed, retaining the other text layouts.
|
||||
bool DisassemblyWidget::updateDocumentIncrementally(
|
||||
const QList<DisassemblyLine> &oldLines, int newVisible)
|
||||
{
|
||||
@@ -682,19 +769,15 @@ bool DisassemblyWidget::updateDocumentIncrementally(
|
||||
return false;
|
||||
}
|
||||
|
||||
QAbstractTextDocumentLayout *layout = doc->documentLayout();
|
||||
const bool layoutSignalsBlocked = layout->blockSignals(true);
|
||||
connectCursorPositionChanged(true);
|
||||
mDisasTextEdit->setLockScroll(true);
|
||||
mDisasTextEdit->setUpdatesEnabled(false);
|
||||
|
||||
QTextCursor cursor(doc);
|
||||
const QTextBlockFormat regular = cursor.blockFormat();
|
||||
BasicBlockColor bbColor;
|
||||
qreal delta = 0;
|
||||
cursor.beginEditBlock();
|
||||
if (shift > 0) {
|
||||
delta = mDisasTextEdit->blockTop(doc->findBlockByNumber(shift))
|
||||
- mDisasTextEdit->blockTop(doc->firstBlock());
|
||||
cursor.movePosition(QTextCursor::Start);
|
||||
cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, shift);
|
||||
cursor.removeSelectedText();
|
||||
@@ -735,29 +818,18 @@ bool DisassemblyWidget::updateDocumentIncrementally(
|
||||
doc->findBlockByNumber(i).clearLayout();
|
||||
}
|
||||
doc->lastBlock().clearLayout();
|
||||
if (shift < 0) {
|
||||
delta = -(
|
||||
mDisasTextEdit->blockTop(doc->findBlockByNumber(-shift))
|
||||
- mDisasTextEdit->blockTop(doc->firstBlock()));
|
||||
}
|
||||
layout->blockSignals(layoutSignalsBlocked);
|
||||
mDisasTextEdit->setLockScroll(false);
|
||||
connectCursorPositionChanged(false);
|
||||
|
||||
// Editing the document also invalidates layouts, selections and dirty regions.
|
||||
// Repaint from the updated document instead of moving stale viewport pixels.
|
||||
mDisasTextEdit->setUpdatesEnabled(true);
|
||||
|
||||
visibleLines = newVisible;
|
||||
if (!documentMatchesLines(lines, newVisible)) {
|
||||
return false; // the full rebuild repairs whatever went wrong
|
||||
}
|
||||
|
||||
// Blit everything below the document margin so only the new rows get painted
|
||||
QWidget *viewport = mDisasTextEdit->viewport();
|
||||
const int margin = qMax(0, qCeil(mDisasTextEdit->textOffset()));
|
||||
if (qAbs(delta - qRound(delta)) > 0.01) {
|
||||
viewport->update();
|
||||
} else {
|
||||
viewport->scroll(
|
||||
0, -qRound(delta), QRect(0, margin, viewport->width(), viewport->height() - margin));
|
||||
}
|
||||
updateCursorPosition();
|
||||
if (readCurrentDisassemblyOffset() == seekable->getOffset()) {
|
||||
// The cursor did not move, extend the highlights to the new rows
|
||||
@@ -987,28 +1059,13 @@ void DisassemblyWidget::clearBasicBlockColorCache()
|
||||
|
||||
void DisassemblyWidget::highlightCurrentLine()
|
||||
{
|
||||
updateExceptionBar();
|
||||
QList<QTextEdit::ExtraSelection> extraSelections;
|
||||
QColor highlightColor = ConfigColor("lineHighlight");
|
||||
|
||||
// Highlight the current word
|
||||
QTextCursor cursor = mDisasTextEdit->textCursor();
|
||||
auto clickedCharPos = cursor.positionInBlock();
|
||||
// Select the line (BlockUnderCursor matches a line with current
|
||||
// implementation)
|
||||
cursor.select(QTextCursor::BlockUnderCursor);
|
||||
// Remove any non-breakable space from the current line
|
||||
QString searchString = cursor.selectedText().replace("\xc2\xa0", " ");
|
||||
// Cut the line in "tokens" that can be highlighted
|
||||
static const QRegularExpression tokenRegExp(R"(\b(?<!\.)([^\s]+)\b(?!\.))");
|
||||
QRegularExpressionMatchIterator i = tokenRegExp.globalMatch(searchString);
|
||||
while (i.hasNext()) {
|
||||
QRegularExpressionMatch match = i.next();
|
||||
// Current token is under our cursor, select this one
|
||||
if (match.capturedStart() <= clickedCharPos && match.capturedEnd() > clickedCharPos) {
|
||||
curHighlightedWord = match.captured();
|
||||
break;
|
||||
}
|
||||
}
|
||||
curHighlightedWord = tokenAtCursor(cursor);
|
||||
|
||||
// Highlight the current line
|
||||
QTextEdit::ExtraSelection highlightSelection;
|
||||
@@ -1039,7 +1096,7 @@ void DisassemblyWidget::highlightCurrentLine()
|
||||
QString scrHighlight = Core()->getConfig("scr.highlight");
|
||||
extraSelections.append(createHighlightSelections(mDisasTextEdit, scrHighlight));
|
||||
|
||||
mDisasTextEdit->setExtraSelections(extraSelections);
|
||||
setDisassemblySelections(extraSelections);
|
||||
}
|
||||
|
||||
void DisassemblyWidget::highlightPCLine()
|
||||
@@ -1076,7 +1133,7 @@ void DisassemblyWidget::highlightPCLine()
|
||||
QList<QTextEdit::ExtraSelection> currentSelections = mDisasTextEdit->extraSelections();
|
||||
currentSelections.append(pcSelections);
|
||||
|
||||
mDisasTextEdit->setExtraSelections(currentSelections);
|
||||
setDisassemblySelections(currentSelections);
|
||||
}
|
||||
|
||||
void DisassemblyWidget::showDisasContextMenu(const QPoint &pt)
|
||||
@@ -1087,12 +1144,6 @@ void DisassemblyWidget::showDisasContextMenu(const QPoint &pt)
|
||||
}
|
||||
updateContextMenuState();
|
||||
|
||||
RVA selectionStart = RVA_INVALID;
|
||||
int selectionSize = 0;
|
||||
if (getSelectedInstructionRange(&selectionStart, &selectionSize)) {
|
||||
mCtxMenu->setSelectionRange(selectionStart, selectionSize);
|
||||
}
|
||||
|
||||
mCtxMenu->exec(mDisasTextEdit->mapToGlobal(pt));
|
||||
}
|
||||
|
||||
@@ -1259,8 +1310,7 @@ void DisassemblyWidget::updateCursorPosition()
|
||||
if (parked.position() != 0 || parked.hasSelection()) {
|
||||
mDisasTextEdit->moveCursor(QTextCursor::Start);
|
||||
}
|
||||
mDisasTextEdit->setExtraSelections(
|
||||
createSameWordsSelections(mDisasTextEdit, curHighlightedWord));
|
||||
setDisassemblySelections(createSameWordsSelections(mDisasTextEdit, curHighlightedWord));
|
||||
} else {
|
||||
RVA currentCursorOffset = readCurrentDisassemblyOffset();
|
||||
QTextCursor originalCursor = mDisasTextEdit->textCursor();
|
||||
@@ -1285,7 +1335,7 @@ void DisassemblyWidget::updateCursorPosition()
|
||||
break;
|
||||
} else if (lineOffset != RVA_INVALID && lineOffset > offset) {
|
||||
mDisasTextEdit->moveCursor(QTextCursor::Start);
|
||||
mDisasTextEdit->setExtraSelections({});
|
||||
setDisassemblySelections({});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1354,11 +1404,19 @@ void DisassemblyWidget::cursorPositionChanged()
|
||||
|
||||
void DisassemblyWidget::updateContextMenuState()
|
||||
{
|
||||
const RVA offset = readCurrentDisassemblyOffset();
|
||||
if (offset != RVA_INVALID) {
|
||||
mCtxMenu->setOffset(offset);
|
||||
}
|
||||
exceptionBar->populateMenu(exceptionMenu, offset);
|
||||
QTextCursor cursor = mDisasTextEdit->textCursor();
|
||||
bool hasSelection = cursor.hasSelection();
|
||||
|
||||
mCtxMenu->setCanCopy(hasSelection);
|
||||
mCtxMenu->setSelectionRange(RVA_INVALID, 0);
|
||||
RVA selectionStart = RVA_INVALID;
|
||||
int selectionSize = 0;
|
||||
getSelectedInstructionRange(&selectionStart, &selectionSize);
|
||||
mCtxMenu->setSelectionRange(selectionStart, selectionSize);
|
||||
|
||||
if (hasSelection) {
|
||||
// A word is selected so use it
|
||||
@@ -1449,8 +1507,18 @@ void DisassemblyWidget::copyBytes()
|
||||
|
||||
void DisassemblyWidget::jumpToOffsetUnderCursor(const QTextCursor &cursor)
|
||||
{
|
||||
RVA offset = readDisassemblyOffset(cursor);
|
||||
seekable->seekToReference(offset);
|
||||
const QString link = DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
|
||||
if (!link.isEmpty()) {
|
||||
DeepLink::handle(mainWindow, link);
|
||||
return;
|
||||
}
|
||||
const RVA target = tokenTarget(cursor);
|
||||
const RVA offset = readDisassemblyOffset(cursor);
|
||||
if (target != RVA_INVALID && target != offset) {
|
||||
seekable->seek(target);
|
||||
} else {
|
||||
seekable->seekToReference(offset);
|
||||
}
|
||||
}
|
||||
|
||||
QList<XrefDescription> DisassemblyWidget::getOutgoingXRefs(RVA offset)
|
||||
@@ -1490,36 +1558,109 @@ RVA DisassemblyWidget::xrefTargetForToken(RVA offset, const QString &token)
|
||||
return RVA_INVALID;
|
||||
}
|
||||
|
||||
bool DisassemblyWidget::isActionableTokenAt(const QPoint &pos)
|
||||
RVA DisassemblyWidget::tokenTarget(const QTextCursor &cursor)
|
||||
{
|
||||
QTextCursor cursor = mDisasTextEdit->cursorForPosition(pos);
|
||||
const RVA offset = readDisassemblyOffset(cursor);
|
||||
if (offset == RVA_INVALID) {
|
||||
return false;
|
||||
return RVA_INVALID;
|
||||
}
|
||||
|
||||
const QString token = tokenAtCursor(cursor);
|
||||
if (token.trimmed().isEmpty()) {
|
||||
return false;
|
||||
return RVA_INVALID;
|
||||
}
|
||||
|
||||
return xrefTargetForToken(offset, token) != RVA_INVALID;
|
||||
if (token.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
bool isAddress = false;
|
||||
const RVA address = token.toULongLong(&isAddress, 16);
|
||||
if (isAddress) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
RCoreLocked core = Core()->core();
|
||||
const QByteArray name = token.toUtf8();
|
||||
if (RFlagItem *flag = r_flag_get(core->flags, name.constData())) {
|
||||
return flag->addr;
|
||||
}
|
||||
if (RAnalFunction *function = r_anal_get_function_byname(core->anal, name.constData())) {
|
||||
return function->addr;
|
||||
}
|
||||
}
|
||||
|
||||
return xrefTargetForToken(offset, token);
|
||||
}
|
||||
|
||||
QString DisassemblyWidget::deepLinkAt(const QPoint &pos)
|
||||
{
|
||||
const QTextCursor cursor = mDisasTextEdit->cursorForPosition(pos);
|
||||
const QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(pos);
|
||||
return DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
|
||||
}
|
||||
|
||||
void DisassemblyWidget::updateDisassemblyCursor(const QPoint &pos, Qt::MouseButtons buttons)
|
||||
void DisassemblyWidget::setDisassemblySelections(QList<QTextEdit::ExtraSelection> selections)
|
||||
{
|
||||
constexpr int hoverProperty = QTextFormat::UserProperty;
|
||||
selections.erase(
|
||||
std::remove_if(
|
||||
selections.begin(),
|
||||
selections.end(),
|
||||
[](const QTextEdit::ExtraSelection &s) { return s.format.boolProperty(hoverProperty); }),
|
||||
selections.end());
|
||||
if (hoveredToken.hasSelection()) {
|
||||
QTextEdit::ExtraSelection hover;
|
||||
hover.cursor = hoveredToken;
|
||||
hover.format.setBackground(ConfigColor("wordHighlight"));
|
||||
hover.format.setProperty(hoverProperty, true);
|
||||
selections.append(hover);
|
||||
}
|
||||
mDisasTextEdit->setExtraSelections(selections);
|
||||
}
|
||||
|
||||
void DisassemblyWidget::setHoveredToken(const QTextCursor &cursor)
|
||||
{
|
||||
if (hoveredToken == cursor) {
|
||||
return;
|
||||
}
|
||||
hoveredToken = cursor;
|
||||
setDisassemblySelections(mDisasTextEdit->extraSelections());
|
||||
}
|
||||
|
||||
void DisassemblyWidget::updateDisassemblyHover(const QPoint &pos, Qt::MouseButtons buttons)
|
||||
{
|
||||
QTextCursor hover;
|
||||
Qt::CursorShape shape = Qt::ArrowCursor;
|
||||
if (buttons & Qt::LeftButton) {
|
||||
mDisasTextEdit->viewport()->setCursor(Qt::IBeamCursor);
|
||||
} else if (isActionableTokenAt(pos) || !deepLinkAt(pos).isEmpty()) {
|
||||
mDisasTextEdit->viewport()->setCursor(Qt::PointingHandCursor);
|
||||
shape = Qt::IBeamCursor;
|
||||
} else {
|
||||
QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(pos);
|
||||
const QString text = cursor.block().text();
|
||||
const QString link = DeepLink::linkAt(text, cursor.positionInBlock());
|
||||
if (!link.isEmpty()) {
|
||||
const int start = cursor.block().position()
|
||||
+ text.lastIndexOf(link, cursor.positionInBlock());
|
||||
cursor.setPosition(start);
|
||||
cursor.setPosition(start + link.size(), QTextCursor::KeepAnchor);
|
||||
hover = cursor;
|
||||
} else if (tokenTarget(cursor) != RVA_INVALID) {
|
||||
hover = tokenRangeAtCursor(cursor);
|
||||
}
|
||||
if (hover.hasSelection()) {
|
||||
shape = Qt::PointingHandCursor;
|
||||
}
|
||||
}
|
||||
mDisasTextEdit->viewport()->setCursor(shape);
|
||||
setHoveredToken(hover);
|
||||
}
|
||||
|
||||
void DisassemblyWidget::refreshDisassemblyHover()
|
||||
{
|
||||
if (mDisasTextEdit->viewport()->underMouse()) {
|
||||
updateDisassemblyHover(
|
||||
mDisasTextEdit->viewport()->mapFromGlobal(QCursor::pos()), QApplication::mouseButtons());
|
||||
} else {
|
||||
mDisasTextEdit->viewport()->setCursor(Qt::ArrowCursor);
|
||||
setHoveredToken({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1527,40 +1668,68 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Resize && obj == mDisasTextEdit->viewport()) {
|
||||
QMetaObject::invokeMethod(this, [this]() { updateMaxLines(); }, Qt::QueuedConnection);
|
||||
} else if (event->type() == QEvent::MouseButtonPress && obj == mDisasTextEdit->viewport()) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
followedTokenOnClick = false;
|
||||
if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() == Qt::NoModifier) {
|
||||
tokenPressPosition = mouseEvent->pos();
|
||||
pressedTokenTarget = tokenTarget(mDisasTextEdit->cursorOnCharacter(tokenPressPosition));
|
||||
}
|
||||
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
|
||||
} else if (event->type() == QEvent::MouseMove && obj == mDisasTextEdit->viewport()) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
updateDisassemblyCursor(mouseEvent->pos(), mouseEvent->buttons());
|
||||
if ((mouseEvent->buttons() & Qt::LeftButton)
|
||||
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
|
||||
>= QApplication::startDragDistance()) {
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
}
|
||||
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
|
||||
} else if (event->type() == QEvent::MouseButtonRelease && obj == mDisasTextEdit->viewport()) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
const RVA target = pressedTokenTarget;
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
if (mouseEvent->button() == Qt::LeftButton && !mDisasTextEdit->textCursor().hasSelection()) {
|
||||
const QString link = deepLinkAt(mouseEvent->pos());
|
||||
if (!link.isEmpty()) {
|
||||
DeepLink::handle(mainWindow, link);
|
||||
return true;
|
||||
}
|
||||
if (target != RVA_INVALID
|
||||
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
|
||||
< QApplication::startDragDistance()
|
||||
&& target == tokenTarget(mDisasTextEdit->cursorOnCharacter(mouseEvent->pos()))) {
|
||||
followedTokenOnClick = true;
|
||||
seekable->seek(target);
|
||||
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
updateDisassemblyCursor(mouseEvent->pos(), mouseEvent->buttons());
|
||||
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
|
||||
} else if (event->type() == QEvent::Leave && obj == mDisasTextEdit->viewport()) {
|
||||
mDisasTextEdit->viewport()->setCursor(Qt::ArrowCursor);
|
||||
setHoveredToken({});
|
||||
} else if (
|
||||
event->type() == QEvent::MouseButtonDblClick
|
||||
&& (obj == mDisasTextEdit || obj == mDisasTextEdit->viewport())) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
pressedTokenTarget = RVA_INVALID;
|
||||
if (mouseEvent->button() != Qt::LeftButton || followedTokenOnClick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QTextCursor &cursor = mDisasTextEdit->cursorForPosition(
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
mouseEvent->position().toPoint()
|
||||
#else
|
||||
QPoint(mouseEvent->x(), mouseEvent->y())
|
||||
#endif
|
||||
);
|
||||
const QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(mouseEvent->pos());
|
||||
jumpToOffsetUnderCursor(cursor);
|
||||
|
||||
return true;
|
||||
} else if (event->type() == QEvent::ToolTip && obj == mDisasTextEdit->viewport()) {
|
||||
QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
|
||||
|
||||
QTextCursor cursorForWord = mDisasTextEdit->cursorForPosition(helpEvent->pos());
|
||||
QTextCursor cursorForWord = mDisasTextEdit->cursorOnCharacter(helpEvent->pos());
|
||||
if (cursorForWord.isNull()) {
|
||||
QToolTip::hideText();
|
||||
return true;
|
||||
}
|
||||
const QString registerTooltip = PreviewTooltip::buildRegisterPreview(
|
||||
wordAtCursor(cursorForWord));
|
||||
if (!registerTooltip.isEmpty()) {
|
||||
@@ -1568,9 +1737,8 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString token = tokenAtCursor(cursorForWord);
|
||||
RVA offsetFrom = readDisassemblyOffset(cursorForWord);
|
||||
RVA offsetTo = xrefTargetForToken(offsetFrom, token);
|
||||
RVA offsetTo = tokenTarget(cursorForWord);
|
||||
|
||||
// Only if the offset we point *to* is different from the one the
|
||||
// cursor is currently on *and* the former is a valid offset, we are
|
||||
@@ -1582,6 +1750,22 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
}
|
||||
}
|
||||
|
||||
if (offsetTo == RVA_INVALID || offsetTo == offsetFrom) {
|
||||
QStringList details;
|
||||
for (const auto ®ion : exceptionRegions) {
|
||||
if (region.contains(offsetFrom) || region.handler == offsetFrom) {
|
||||
details << region.description();
|
||||
}
|
||||
}
|
||||
if (!details.isEmpty()) {
|
||||
QToolTip::showText(
|
||||
helpEvent->globalPos(),
|
||||
details.join(QStringLiteral("\n\n"))
|
||||
.toHtmlEscaped()
|
||||
.replace(QLatin1Char('\n'), QStringLiteral("<br>")),
|
||||
this);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1603,11 +1787,6 @@ void DisassemblyWidget::keyPressEvent(QKeyEvent *event)
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ShortcutMgr()->matches("disasm.jumpToOffset", event)) {
|
||||
const QTextCursor cursor = mDisasTextEdit->textCursor();
|
||||
jumpToOffsetUnderCursor(cursor);
|
||||
}
|
||||
|
||||
MemoryDockWidget::keyPressEvent(event);
|
||||
}
|
||||
|
||||
@@ -1654,6 +1833,7 @@ void DisassemblyWidget::colorsUpdatedSlot()
|
||||
{
|
||||
PreviewTooltip::applyStyleSheet(this);
|
||||
setupColors();
|
||||
setDisassemblySelections(mDisasTextEdit->extraSelections());
|
||||
// Skip the expensive refreshDisasm() (which re-runs disassembleLines)
|
||||
// when only the interface palette changed; the cached line HTML still
|
||||
// carries valid radare2 colors, so a repaint is enough.
|
||||
@@ -1682,9 +1862,36 @@ void DisassemblyWidget::setupColors()
|
||||
.arg(ConfigColor("btext").name()));
|
||||
}
|
||||
|
||||
qreal DisassemblyTextEdit::blockTop(const QTextBlock &block) const
|
||||
// Mouse hits must land on text, rather than snap to a nearby caret position.
|
||||
QTextCursor DisassemblyTextEdit::cursorOnCharacter(const QPoint &pos) const
|
||||
{
|
||||
return blockBoundingGeometry(block).top();
|
||||
if (!viewport()->rect().contains(pos)) {
|
||||
return {};
|
||||
}
|
||||
const QTextBlock block = cursorForPosition(pos).block();
|
||||
if (!block.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const QPointF localPos = QPointF(pos)
|
||||
- blockBoundingGeometry(block).translated(contentOffset()).topLeft();
|
||||
const QTextLayout *layout = block.layout();
|
||||
for (int i = 0; i < layout->lineCount(); ++i) {
|
||||
const QTextLine line = layout->lineAt(i);
|
||||
const QRectF rect = line.naturalTextRect();
|
||||
if (localPos.x() < rect.left() || localPos.x() >= rect.right() || localPos.y() < rect.top()
|
||||
|| localPos.y() >= rect.bottom()) {
|
||||
continue;
|
||||
}
|
||||
const int column = line.xToCursor(localPos.x(), QTextLine::CursorOnCharacter);
|
||||
const QString text = block.text();
|
||||
if (column >= text.size() || text.at(column).isSpace()) {
|
||||
return {};
|
||||
}
|
||||
QTextCursor cursor(block);
|
||||
cursor.setPosition(block.position() + column);
|
||||
return cursor;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Viewport rectangles of every block, in document order
|
||||
@@ -1698,11 +1905,6 @@ QList<QRectF> DisassemblyTextEdit::blockRects() const
|
||||
return rects;
|
||||
}
|
||||
|
||||
qreal DisassemblyTextEdit::textOffset() const
|
||||
{
|
||||
return (blockBoundingGeometry(document()->begin()).topLeft() + contentOffset()).y();
|
||||
}
|
||||
|
||||
bool DisassemblyTextEdit::viewportEvent(QEvent *event)
|
||||
{
|
||||
switch (event->type()) {
|
||||
@@ -1735,6 +1937,9 @@ void DisassemblyTextEdit::scrollContentsBy(int dx, int dy)
|
||||
{
|
||||
if (!lockScroll) {
|
||||
QPlainTextEdit::scrollContentsBy(dx, dy);
|
||||
// Qt scrolls existing pixels, including the function lines fixed to the viewport.
|
||||
// Repaint the whole viewport to erase their shifted copies.
|
||||
viewport()->update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1755,7 +1960,7 @@ void DisassemblyTextEdit::mousePressEvent(QMouseEvent *event)
|
||||
|
||||
void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
// The viewport is opaque so scrolling can blit it, paint its background
|
||||
// The viewport is opaque, so every damaged area needs its background painted.
|
||||
QPainter(viewport()).fillRect(event->rect(), backgroundColor);
|
||||
QPlainTextEdit::paintEvent(event);
|
||||
|
||||
@@ -1794,7 +1999,7 @@ void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
|
||||
return;
|
||||
}
|
||||
|
||||
// Endpoints only depend on visible rows so blitted lines match a fresh paint
|
||||
// Derive endpoints from the visible rows.
|
||||
const auto &functionRanges = disas->getFunctionRanges();
|
||||
for (auto range = functionRanges.cbegin(); range != functionRanges.cend(); ++range) {
|
||||
auto firstLine = rowSpan.lowerBound(range.key());
|
||||
@@ -1813,7 +2018,7 @@ void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
|
||||
|
||||
void DisassemblyWidget::seekPrev()
|
||||
{
|
||||
Core()->seekPrev();
|
||||
seekable->seekPrev();
|
||||
}
|
||||
|
||||
/*********************
|
||||
|
||||
@@ -22,6 +22,7 @@ class DisassemblyContextMenu;
|
||||
class DisassemblyLeftPanel;
|
||||
class QWheelEvent;
|
||||
class AddressScrollBar;
|
||||
class ExceptionBar;
|
||||
|
||||
class DisassemblyWidget : public MemoryDockWidget
|
||||
{
|
||||
@@ -93,6 +94,7 @@ private:
|
||||
qreal wheelRemainder;
|
||||
|
||||
QString curHighlightedWord;
|
||||
QTextCursor hoveredToken;
|
||||
|
||||
struct BasicBlockColor
|
||||
{
|
||||
@@ -103,6 +105,11 @@ private:
|
||||
|
||||
QList<BasicBlockColor> basicBlockColorCache;
|
||||
QHash<RVA, QList<XrefDescription>> outgoingXRefsCache;
|
||||
QList<ExceptionRegion> exceptionRegions;
|
||||
RVA exceptionFunction = RVA_INVALID;
|
||||
ExceptionBar *exceptionBar;
|
||||
QMenu *exceptionMenu;
|
||||
void updateExceptionBar(bool force = false);
|
||||
|
||||
/**
|
||||
* offset of lines below the first line of the current seek
|
||||
@@ -110,6 +117,9 @@ private:
|
||||
int cursorLineOffset;
|
||||
int cursorCharOffset;
|
||||
bool seekFromCursor;
|
||||
QPoint tokenPressPosition;
|
||||
RVA pressedTokenTarget = RVA_INVALID;
|
||||
bool followedTokenOnClick = false;
|
||||
bool applyingAddressRangeSelection = false;
|
||||
bool publishingAddressRangeSelection = false;
|
||||
|
||||
@@ -148,8 +158,11 @@ private:
|
||||
void jumpToOffsetUnderCursor(const QTextCursor &);
|
||||
QList<XrefDescription> getOutgoingXRefs(RVA offset);
|
||||
RVA xrefTargetForToken(RVA offset, const QString &token);
|
||||
bool isActionableTokenAt(const QPoint &pos);
|
||||
void updateDisassemblyCursor(const QPoint &pos, Qt::MouseButtons buttons);
|
||||
RVA tokenTarget(const QTextCursor &cursor);
|
||||
void setDisassemblySelections(QList<QTextEdit::ExtraSelection> selections);
|
||||
void setHoveredToken(const QTextCursor &cursor);
|
||||
void updateDisassemblyHover(const QPoint &pos, Qt::MouseButtons buttons);
|
||||
void refreshDisassemblyHover();
|
||||
QString deepLinkAt(const QPoint &pos);
|
||||
};
|
||||
|
||||
@@ -167,8 +180,7 @@ public:
|
||||
void setLockScroll(bool lock) { this->lockScroll = lock; }
|
||||
void setBackgroundColor(const QColor &color) { backgroundColor = color; }
|
||||
|
||||
qreal textOffset() const;
|
||||
qreal blockTop(const QTextBlock &block) const;
|
||||
QTextCursor cursorOnCharacter(const QPoint &pos) const;
|
||||
QList<QRectF> blockRects() const;
|
||||
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "ExceptionBar.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QMenu>
|
||||
#include <QToolButton>
|
||||
|
||||
ExceptionBar::ExceptionBar(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||
auto *layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
navigator = new QToolButton(this);
|
||||
navigator->setAutoRaise(true);
|
||||
navigator->setPopupMode(QToolButton::InstantPopup);
|
||||
navigator->setMenu(new QMenu(navigator));
|
||||
navigator->setToolTip(tr("Navigate protected ranges and exception handlers (read-only)"));
|
||||
layout->addWidget(navigator);
|
||||
layout->addStretch();
|
||||
hide();
|
||||
}
|
||||
|
||||
void ExceptionBar::addRegion(QMenu *menu, const ExceptionRegion ®ion)
|
||||
{
|
||||
auto *entry = menu->addMenu(QStringLiteral("#%1 · %2").arg(region.index).arg(region.label()));
|
||||
entry->setToolTipsVisible(true);
|
||||
entry->menuAction()->setToolTip(region.description());
|
||||
const auto add = [this, entry](const QString &title, quint64 address) {
|
||||
auto *action = entry->addAction(QStringLiteral("%1 · 0x%2").arg(title).arg(address, 0, 16));
|
||||
connect(action, &QAction::triggered, this, [this, address]() {
|
||||
emit seekRequested(address);
|
||||
});
|
||||
};
|
||||
add(tr("Go to handler"), region.handler);
|
||||
add(tr("Go to try start"), region.from);
|
||||
add(tr("Go to try end (exclusive)"), region.to);
|
||||
// In PE metadata filter values 0 and 1 are constants, not code addresses.
|
||||
if (region.filter > 1 && region.filter != ExceptionRegion::Invalid) {
|
||||
add(tr("Go to filter"), region.filter);
|
||||
}
|
||||
}
|
||||
|
||||
void ExceptionBar::setRegions(const QList<ExceptionRegion> &value, quint64 address)
|
||||
{
|
||||
regions = value;
|
||||
navigator->menu()->clear();
|
||||
for (const auto ®ion : regions) {
|
||||
addRegion(navigator->menu(), region);
|
||||
}
|
||||
navigator->setText(tr("Exceptions (%1)").arg(regions.size()));
|
||||
setVisible(!regions.isEmpty());
|
||||
setAddress(address);
|
||||
}
|
||||
|
||||
void ExceptionBar::setAddress(quint64 address)
|
||||
{
|
||||
QStringList details;
|
||||
details << tr("Navigate this function's protected ranges and exception handlers (read-only)");
|
||||
for (const auto ®ion : regions) {
|
||||
if (region.contains(address) || region.handler == address) {
|
||||
details << region.description();
|
||||
}
|
||||
}
|
||||
navigator->setToolTip(details.join(QStringLiteral("\n\n"))
|
||||
.toHtmlEscaped()
|
||||
.replace(QLatin1Char('\n'), QStringLiteral("<br>")));
|
||||
}
|
||||
|
||||
void ExceptionBar::populateMenu(QMenu *menu, quint64 address, quint64 size)
|
||||
{
|
||||
menu->clear();
|
||||
for (const auto ®ion : regions) {
|
||||
if (region.touches(address, size)) {
|
||||
addRegion(menu, region);
|
||||
}
|
||||
}
|
||||
menu->menuAction()->setVisible(!menu->isEmpty());
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef EXCEPTIONBAR_H
|
||||
#define EXCEPTIONBAR_H
|
||||
|
||||
#include "common/ExceptionInfo.h"
|
||||
#include <QWidget>
|
||||
|
||||
class QToolButton;
|
||||
class QMenu;
|
||||
|
||||
// Shared, read-only exception navigator for code views.
|
||||
class ExceptionBar : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ExceptionBar(QWidget *parent = nullptr);
|
||||
void setRegions(const QList<ExceptionRegion> ®ions, quint64 address);
|
||||
void setAddress(quint64 address);
|
||||
void populateMenu(QMenu *menu, quint64 address, quint64 size = 1);
|
||||
|
||||
signals:
|
||||
void seekRequested(quint64 address);
|
||||
|
||||
private:
|
||||
QList<ExceptionRegion> regions;
|
||||
QToolButton *navigator;
|
||||
void addRegion(QMenu *menu, const ExceptionRegion ®ion);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -95,6 +95,9 @@ ExportsProxyModel::ExportsProxyModel(ExportsModel *source_model, QObject *parent
|
||||
|
||||
bool ExportsProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
|
||||
{
|
||||
if (!filterAcceptsType(row, parent)) {
|
||||
return false;
|
||||
}
|
||||
QModelIndex index = sourceModel()->index(row, 0, parent);
|
||||
auto exp = index.data(ExportsModel::ExportDescriptionRole).value<ExportDescription>();
|
||||
return exp.name.contains(FILTER_REGEX);
|
||||
@@ -138,7 +141,7 @@ ExportsWidget::ExportsWidget(MainWindow *main)
|
||||
|
||||
exportsModel = new ExportsModel(&exports, this);
|
||||
exportsProxyModel = new ExportsProxyModel(exportsModel, this);
|
||||
setModels(exportsProxyModel);
|
||||
setModels(exportsProxyModel, ExportsModel::NameColumn, ExportsModel::TypeColumn);
|
||||
ui->treeView->sortByColumn(ExportsModel::OffsetColumn, Qt::AscendingOrder);
|
||||
|
||||
QShortcut *toggle_shortcut = ShortcutMgr()->registerShortcut("widget.toggleExports", main);
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
#include "FilesystemWidget.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "common/SourceLineReference.h"
|
||||
#include "core/Iaito.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include <algorithm>
|
||||
#include <QApplication>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QFontDatabase>
|
||||
#include <QGridLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QSet>
|
||||
#include <QSignalBlocker>
|
||||
#include <QStyle>
|
||||
|
||||
FilesystemTreeModel::FilesystemTreeModel(QObject *parent)
|
||||
: QStandardItemModel(parent)
|
||||
@@ -22,65 +29,90 @@ void FilesystemTreeModel::setRootPath(const QString &path)
|
||||
|
||||
void FilesystemTreeModel::refresh()
|
||||
{
|
||||
clear();
|
||||
setHorizontalHeaderLabels(QStringList() << tr("Name") << tr("Size") << tr("Type"));
|
||||
removeRows(0, rowCount());
|
||||
if (!rootPath.isEmpty()) {
|
||||
populateDirectory(invisibleRootItem(), rootPath);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonDocument FilesystemTreeModel::parseMdCommand(const QString &path)
|
||||
QList<FileInfo> FilesystemTreeModel::readDirectory(const QString &path)
|
||||
{
|
||||
QString sanitizedPath = IaitoCore::sanitizeStringForCommand(path);
|
||||
QString output = Core()->cmdRaw(QString("mdj %1").arg(sanitizedPath));
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Failed to parse mdj output:" << error.errorString();
|
||||
return QJsonDocument();
|
||||
// Use the API so source paths, including spaces and command metacharacters, stay intact.
|
||||
QList<FileInfo> entries;
|
||||
RCoreLocked core = Core()->core();
|
||||
RListIter *iter;
|
||||
if (path == QStringLiteral("/")) {
|
||||
RFSRoot *root;
|
||||
IaitoRListForeach(core->fs->roots, iter, RFSRoot, root)
|
||||
{
|
||||
const QString mountPath = QString::fromUtf8(root->path);
|
||||
if (mountPath != QStringLiteral("/")) {
|
||||
entries.append({mountPath, QStringLiteral("directory"), 0});
|
||||
}
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
const QByteArray pathBytes = path.toUtf8();
|
||||
RList *files = r_fs_dir(core->fs, pathBytes.constData());
|
||||
RFSFile *file;
|
||||
IaitoRListForeach(files, iter, RFSFile, file)
|
||||
{
|
||||
const QString name = QString::fromUtf8(file->name);
|
||||
if (name.isEmpty() || name == QStringLiteral(".") || name == QStringLiteral("..")) {
|
||||
continue;
|
||||
}
|
||||
entries.append(
|
||||
{name,
|
||||
file->type == 'd' || file->type == 'm' ? QStringLiteral("directory")
|
||||
: QStringLiteral("file"),
|
||||
file->size});
|
||||
}
|
||||
r_list_free(files);
|
||||
std::sort(entries.begin(), entries.end(), [](const FileInfo &a, const FileInfo &b) {
|
||||
if (a.type != b.type) {
|
||||
return a.type == QStringLiteral("directory");
|
||||
}
|
||||
return QString::localeAwareCompare(a.name, b.name) < 0;
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
void FilesystemTreeModel::populateDirectory(QStandardItem *parentItem, const QString &path)
|
||||
{
|
||||
QJsonDocument doc = parseMdCommand(path);
|
||||
if (!doc.isArray()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array = doc.array();
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject())
|
||||
continue;
|
||||
QJsonObject obj = value.toObject();
|
||||
|
||||
QString name = obj["name"].toString();
|
||||
QString type = obj["type"].toString();
|
||||
quint64 size = obj["size"].toVariant().toULongLong();
|
||||
for (const auto &entry : readDirectory(path)) {
|
||||
const QString &name = entry.name;
|
||||
const QString &type = entry.type;
|
||||
|
||||
QList<QStandardItem *> items;
|
||||
QStandardItem *nameItem = new QStandardItem(name);
|
||||
nameItem->setData(path + "/" + name, Qt::UserRole); // Store full path
|
||||
const QString fullPath = name.startsWith(QLatin1Char('/'))
|
||||
? name
|
||||
: QDir::cleanPath(path + QLatin1Char('/') + name);
|
||||
nameItem->setData(fullPath, PathRole);
|
||||
nameItem->setToolTip(fullPath);
|
||||
items << nameItem;
|
||||
items << new QStandardItem(QString::number(size));
|
||||
items << new QStandardItem(QString::number(entry.size));
|
||||
items << new QStandardItem(type);
|
||||
|
||||
if (type == "directory") {
|
||||
nameItem->setIcon(qApp->style()->standardIcon(QStyle::SP_DirIcon));
|
||||
nameItem->setData(false, LoadedRole);
|
||||
// Add a dummy child to make it expandable
|
||||
nameItem->appendRow(new QStandardItem("Loading..."));
|
||||
auto *placeholder = new QStandardItem(tr("Loading..."));
|
||||
placeholder->setEnabled(false);
|
||||
nameItem->appendRow(placeholder);
|
||||
} else {
|
||||
nameItem->setIcon(qApp->style()->standardIcon(QStyle::SP_FileIcon));
|
||||
}
|
||||
|
||||
for (auto *item : items) {
|
||||
item->setEditable(false);
|
||||
}
|
||||
parentItem->appendRow(items);
|
||||
}
|
||||
}
|
||||
|
||||
FilesystemWidget::FilesystemWidget(MainWindow *main)
|
||||
: IaitoDockWidget(main)
|
||||
, mainWindow(main)
|
||||
{
|
||||
setWindowTitle(tr("Filesystem"));
|
||||
setObjectName("FilesystemWidget");
|
||||
@@ -90,7 +122,12 @@ FilesystemWidget::FilesystemWidget(MainWindow *main)
|
||||
setupFilesystemTree();
|
||||
setupActions();
|
||||
|
||||
refreshMountpoints();
|
||||
refreshDeferrer = createRefreshDeferrer([this]() { refresh(); });
|
||||
connect(Core(), &IaitoCore::refreshAll, this, &FilesystemWidget::refresh);
|
||||
connect(Core(), &IaitoCore::functionsChanged, this, &FilesystemWidget::refresh);
|
||||
connect(Core(), &IaitoCore::functionRenamed, this, &FilesystemWidget::refresh);
|
||||
connect(Core(), &IaitoCore::codeRebased, this, &FilesystemWidget::refresh);
|
||||
refresh();
|
||||
}
|
||||
|
||||
FilesystemWidget::~FilesystemWidget() {}
|
||||
@@ -107,9 +144,10 @@ void FilesystemWidget::setupUI()
|
||||
QVBoxLayout *mountLayout = new QVBoxLayout(mountpointsGroup);
|
||||
|
||||
mountpointsList = new QListWidget(this);
|
||||
mountpointsList->setMaximumHeight(80);
|
||||
mountLayout->addWidget(mountpointsList);
|
||||
|
||||
QHBoxLayout *mountControlsLayout = new QHBoxLayout();
|
||||
auto *mountControlsLayout = new QGridLayout();
|
||||
fsTypeCombo = new QComboBox(this);
|
||||
mountPathEdit = new QLineEdit(this);
|
||||
mountPathEdit->setPlaceholderText(tr("Mount path (e.g., /mnt)"));
|
||||
@@ -120,12 +158,12 @@ void FilesystemWidget::setupUI()
|
||||
mountButton = new QPushButton(tr("Mount"), this);
|
||||
umountButton = new QPushButton(tr("Umount"), this);
|
||||
|
||||
mountControlsLayout->addWidget(fsTypeCombo);
|
||||
mountControlsLayout->addWidget(mountPathEdit);
|
||||
mountControlsLayout->addWidget(offsetEdit);
|
||||
mountControlsLayout->addWidget(optionsEdit);
|
||||
mountControlsLayout->addWidget(mountButton);
|
||||
mountControlsLayout->addWidget(umountButton);
|
||||
mountControlsLayout->addWidget(fsTypeCombo, 0, 0);
|
||||
mountControlsLayout->addWidget(mountPathEdit, 0, 1);
|
||||
mountControlsLayout->addWidget(mountButton, 0, 2);
|
||||
mountControlsLayout->addWidget(offsetEdit, 1, 0);
|
||||
mountControlsLayout->addWidget(optionsEdit, 1, 1);
|
||||
mountControlsLayout->addWidget(umountButton, 1, 2);
|
||||
|
||||
mountLayout->addLayout(mountControlsLayout);
|
||||
|
||||
@@ -135,11 +173,44 @@ void FilesystemWidget::setupUI()
|
||||
filesystemGroup = new QGroupBox(tr("Filesystem Browser"), this);
|
||||
QVBoxLayout *fsLayout = new QVBoxLayout(filesystemGroup);
|
||||
|
||||
auto *navigation = new QHBoxLayout();
|
||||
auto *upButton = new QPushButton(tr("Up"), this);
|
||||
auto *sourcesButton = new QPushButton(tr("Source Files"), this);
|
||||
auto *refreshButton = new QPushButton(tr("Refresh"), this);
|
||||
browserPathEdit = new QLineEdit(QStringLiteral("/"), this);
|
||||
browserPathEdit->setObjectName(QStringLiteral("filesystemPath"));
|
||||
navigation->addWidget(upButton);
|
||||
navigation->addWidget(browserPathEdit, 1);
|
||||
navigation->addWidget(sourcesButton);
|
||||
navigation->addWidget(refreshButton);
|
||||
fsLayout->addLayout(navigation);
|
||||
connect(upButton, &QPushButton::clicked, this, [this]() {
|
||||
setBrowserPath(QDir::cleanPath(treeModel->getRootPath() + QStringLiteral("/..")));
|
||||
});
|
||||
connect(browserPathEdit, &QLineEdit::returnPressed, this, [this]() {
|
||||
setBrowserPath(browserPathEdit->text());
|
||||
});
|
||||
connect(sourcesButton, &QPushButton::clicked, this, &FilesystemWidget::showSourceFiles);
|
||||
connect(refreshButton, &QPushButton::clicked, this, &FilesystemWidget::refresh);
|
||||
|
||||
filesystemTree = new QTreeView(this);
|
||||
filesystemTree->setObjectName(QStringLiteral("filesystemTree"));
|
||||
treeModel = new FilesystemTreeModel(this);
|
||||
filesystemTree->setModel(treeModel);
|
||||
filesystemTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
filesystemTree->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
filesystemTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
filesystemTree->setUniformRowHeights(true);
|
||||
filesystemTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
filesystemTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
filesystemTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
filesystemTree->header()->setStretchLastSection(false);
|
||||
connect(filesystemTree, &QTreeView::expanded, this, &FilesystemWidget::onTreeExpanded);
|
||||
connect(
|
||||
filesystemTree->selectionModel(),
|
||||
&QItemSelectionModel::currentChanged,
|
||||
this,
|
||||
&FilesystemWidget::onTreeSelectionChanged);
|
||||
fsLayout->addWidget(filesystemTree);
|
||||
|
||||
QHBoxLayout *fsControlsLayout = new QHBoxLayout();
|
||||
@@ -153,6 +224,49 @@ void FilesystemWidget::setupUI()
|
||||
|
||||
splitter->addWidget(filesystemGroup);
|
||||
|
||||
previewGroup = new QGroupBox(tr("File Preview"), this);
|
||||
auto *previewLayout = new QVBoxLayout(previewGroup);
|
||||
previewStatus = new QLabel(tr("Select a file to preview it."), previewGroup);
|
||||
previewStatus->setTextFormat(Qt::PlainText);
|
||||
previewStatus->setWordWrap(true);
|
||||
previewLayout->addWidget(previewStatus);
|
||||
previewTabs = new QTabWidget(previewGroup);
|
||||
sourceFunctions = new QTreeWidget(previewTabs);
|
||||
sourceFunctions->setObjectName(QStringLiteral("sourceFunctions"));
|
||||
sourceFunctions->setHeaderLabels({tr("Function"), tr("Address")});
|
||||
sourceLines = new QTreeWidget(previewTabs);
|
||||
sourceLines->setObjectName(QStringLiteral("sourceLines"));
|
||||
sourceLines->setHeaderLabels({tr("Address"), tr("Line"), tr("Source")});
|
||||
sourceLines->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
auto navigate = [](QTreeWidgetItem *item, int) {
|
||||
const QVariant address = item->data(0, Qt::UserRole);
|
||||
if (address.isValid()) {
|
||||
Core()->seekAndShow(address.toULongLong());
|
||||
}
|
||||
};
|
||||
for (auto *tree : {sourceFunctions, sourceLines}) {
|
||||
tree->setRootIsDecorated(false);
|
||||
tree->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
tree->setUniformRowHeights(true);
|
||||
connect(tree, &QTreeWidget::itemClicked, this, navigate);
|
||||
connect(tree, &QTreeWidget::itemActivated, this, navigate);
|
||||
}
|
||||
sourceLines->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
sourceLines->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
fileContents = new QPlainTextEdit(previewTabs);
|
||||
fileContents->setObjectName(QStringLiteral("filesystemContents"));
|
||||
fileContents->setReadOnly(true);
|
||||
fileContents->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
fileContents->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||||
previewTabs->addTab(sourceFunctions, tr("Functions"));
|
||||
previewTabs->addTab(sourceLines, tr("Source Lines"));
|
||||
previewTabs->addTab(fileContents, tr("Contents"));
|
||||
previewLayout->addWidget(previewTabs);
|
||||
splitter->addWidget(previewGroup);
|
||||
splitter->setStretchFactor(1, 2);
|
||||
splitter->setStretchFactor(2, 1);
|
||||
splitter->setSizes({170, 400, 230});
|
||||
|
||||
mainLayout->addWidget(splitter);
|
||||
setWidget(container);
|
||||
|
||||
@@ -173,7 +287,9 @@ void FilesystemWidget::setupUI()
|
||||
|
||||
void FilesystemWidget::setupMountpointsList()
|
||||
{
|
||||
// Will be populated in refreshMountpoints
|
||||
connect(mountpointsList, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) {
|
||||
setBrowserPath(item->data(Qt::UserRole).toString());
|
||||
});
|
||||
}
|
||||
|
||||
void FilesystemWidget::setupFilesystemTree()
|
||||
@@ -189,6 +305,7 @@ void FilesystemWidget::setupActions()
|
||||
|
||||
connect(viewAction, &QAction::triggered, this, [this]() {
|
||||
QModelIndex index = filesystemTree->currentIndex();
|
||||
index = index.sibling(index.row(), 0);
|
||||
if (index.isValid()) {
|
||||
QString path = index.data(Qt::UserRole).toString();
|
||||
if (path.isEmpty()) {
|
||||
@@ -200,6 +317,7 @@ void FilesystemWidget::setupActions()
|
||||
|
||||
connect(deleteAction, &QAction::triggered, this, [this]() {
|
||||
QModelIndex index = filesystemTree->currentIndex();
|
||||
index = index.sibling(index.row(), 0);
|
||||
if (index.isValid()) {
|
||||
QString path = index.data(Qt::UserRole).toString();
|
||||
if (path.isEmpty()) {
|
||||
@@ -211,6 +329,7 @@ void FilesystemWidget::setupActions()
|
||||
|
||||
connect(mallocAction, &QAction::triggered, this, [this]() {
|
||||
QModelIndex index = filesystemTree->currentIndex();
|
||||
index = index.sibling(index.row(), 0);
|
||||
if (index.isValid()) {
|
||||
QString path = index.data(Qt::UserRole).toString();
|
||||
if (path.isEmpty()) {
|
||||
@@ -267,19 +386,16 @@ QList<PluginInfo> FilesystemWidget::parsePlugins(const QJsonDocument &doc)
|
||||
|
||||
void FilesystemWidget::refreshMountpoints()
|
||||
{
|
||||
QString output = Core()->cmdRaw("mj");
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Failed to parse mj output:" << error.errorString();
|
||||
const QJsonDocument doc = Core()->cmdj("mj");
|
||||
if (!doc.isObject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mountpointsList->clear();
|
||||
fsTypeCombo->clear();
|
||||
|
||||
const QList<MountpointInfo> mountpoints = parseMountpoints(doc);
|
||||
for (const MountpointInfo &mountpoint : mountpoints) {
|
||||
currentMountpoints = parseMountpoints(doc);
|
||||
for (const MountpointInfo &mountpoint : currentMountpoints) {
|
||||
QString details
|
||||
= QString("%1 @ 0x%2").arg(mountpoint.plugin, QString::number(mountpoint.offset, 16));
|
||||
if (!mountpoint.options.isEmpty()) {
|
||||
@@ -300,6 +416,142 @@ void FilesystemWidget::refreshMountpoints()
|
||||
}
|
||||
}
|
||||
|
||||
void FilesystemWidget::refresh()
|
||||
{
|
||||
if (!refreshDeferrer->attemptRefresh(nullptr)) {
|
||||
return;
|
||||
}
|
||||
const QString selectedPath = previewPath;
|
||||
QSet<QString> expandedPaths;
|
||||
auto visit = [this](auto &&self, const QModelIndex &parent, const auto &action) -> void {
|
||||
for (int row = 0; row < treeModel->rowCount(parent); ++row) {
|
||||
const QModelIndex index = treeModel->index(row, 0, parent);
|
||||
action(index);
|
||||
self(self, index, action);
|
||||
}
|
||||
};
|
||||
visit(visit, QModelIndex(), [this, &expandedPaths](const QModelIndex &index) {
|
||||
if (filesystemTree->isExpanded(index)) {
|
||||
expandedPaths.insert(index.data(FilesystemTreeModel::PathRole).toString());
|
||||
}
|
||||
});
|
||||
const QSignalBlocker selectionBlocker(filesystemTree->selectionModel());
|
||||
refreshMountpoints();
|
||||
treeModel->refresh();
|
||||
bool selectedFileExists = false;
|
||||
visit(
|
||||
visit,
|
||||
QModelIndex(),
|
||||
[this, &expandedPaths, &selectedPath, &selectedFileExists](const QModelIndex &index) {
|
||||
const QString path = index.data(FilesystemTreeModel::PathRole).toString();
|
||||
if (expandedPaths.contains(path)) {
|
||||
filesystemTree->expand(index);
|
||||
}
|
||||
if (!selectedPath.isEmpty() && path == selectedPath) {
|
||||
filesystemTree->setCurrentIndex(index);
|
||||
selectedFileExists = true;
|
||||
}
|
||||
});
|
||||
if (selectedFileExists) {
|
||||
viewFileContents(selectedPath);
|
||||
} else {
|
||||
clearPreview();
|
||||
}
|
||||
}
|
||||
|
||||
void FilesystemWidget::setBrowserPath(const QString &path)
|
||||
{
|
||||
QString absolutePath = path;
|
||||
if (!absolutePath.startsWith(QLatin1Char('/'))) {
|
||||
absolutePath.prepend(QLatin1Char('/'));
|
||||
}
|
||||
absolutePath = QDir::cleanPath(absolutePath);
|
||||
browserPathEdit->setText(absolutePath);
|
||||
clearPreview();
|
||||
treeModel->setRootPath(absolutePath);
|
||||
const bool sourceDirectory = isSourceFile(absolutePath + QLatin1Char('/'));
|
||||
createDirButton->setEnabled(!sourceDirectory);
|
||||
createFileButton->setEnabled(!sourceDirectory);
|
||||
}
|
||||
|
||||
bool FilesystemWidget::isSourceFile(const QString &path) const
|
||||
{
|
||||
for (const auto &mount : currentMountpoints) {
|
||||
const QString prefix = mount.path == QStringLiteral("/")
|
||||
? QStringLiteral("/cl/")
|
||||
: mount.path + QStringLiteral("/cl/");
|
||||
if (mount.plugin == QStringLiteral("r2") && path.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FilesystemWidget::showSourceFiles()
|
||||
{
|
||||
refreshMountpoints();
|
||||
QString mountPath;
|
||||
QSet<QString> usedPaths;
|
||||
for (const auto &mount : currentMountpoints) {
|
||||
usedPaths.insert(mount.path);
|
||||
if (mount.plugin == QStringLiteral("r2") && mountPath.isEmpty()) {
|
||||
mountPath = mount.path;
|
||||
}
|
||||
}
|
||||
if (mountPath.isEmpty()) {
|
||||
mountPath = QStringLiteral("/r2");
|
||||
for (int suffix = 1; usedPaths.contains(mountPath); ++suffix) {
|
||||
mountPath = QStringLiteral("/r2_%1").arg(suffix);
|
||||
}
|
||||
const QByteArray pathBytes = mountPath.toUtf8();
|
||||
RCoreLocked core = Core()->core();
|
||||
if (!r_fs_mount(core->fs, "r2", pathBytes.constData(), 0)) {
|
||||
previewStatus->setText(tr("Failed to mount the r2 filesystem."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
refreshMountpoints();
|
||||
setBrowserPath(mountPath);
|
||||
bool hasSourceDirectory = false;
|
||||
for (int row = 0; row < treeModel->rowCount(); ++row) {
|
||||
hasSourceDirectory |= treeModel->index(row, 0).data().toString() == QStringLiteral("cl");
|
||||
}
|
||||
if (!hasSourceDirectory) {
|
||||
previewStatus->setText(tr("This r2 filesystem does not provide a /cl source directory."));
|
||||
return;
|
||||
}
|
||||
const QString sourcePath = QDir::cleanPath(mountPath + QStringLiteral("/cl"));
|
||||
setBrowserPath(sourcePath);
|
||||
if (treeModel->rowCount() == 0) {
|
||||
previewStatus->setText(
|
||||
tr("No source references are available. The binary needs source "
|
||||
"line information and an r2 filesystem with /cl support."));
|
||||
}
|
||||
}
|
||||
|
||||
void FilesystemWidget::clearPreview()
|
||||
{
|
||||
previewPath.clear();
|
||||
sourceFunctions->clear();
|
||||
sourceLines->clear();
|
||||
fileContents->clear();
|
||||
previewStatus->setText(tr("Select a file to preview it."));
|
||||
previewTabs->setTabEnabled(0, false);
|
||||
previewTabs->setTabEnabled(1, false);
|
||||
previewTabs->setCurrentWidget(fileContents);
|
||||
}
|
||||
|
||||
void FilesystemWidget::onTreeSelectionChanged(const QModelIndex &index)
|
||||
{
|
||||
const QModelIndex nameIndex = index.sibling(index.row(), 0);
|
||||
if (nameIndex.isValid()
|
||||
&& nameIndex.sibling(nameIndex.row(), 2).data().toString() == QStringLiteral("file")) {
|
||||
viewFileContents(nameIndex.data(FilesystemTreeModel::PathRole).toString());
|
||||
} else {
|
||||
clearPreview();
|
||||
}
|
||||
}
|
||||
|
||||
void FilesystemWidget::onMountButtonClicked()
|
||||
{
|
||||
QString fsType = fsTypeCombo->currentText();
|
||||
@@ -351,7 +603,7 @@ void FilesystemWidget::onMountButtonClicked()
|
||||
}
|
||||
|
||||
refreshMountpoints();
|
||||
treeModel->refresh();
|
||||
setBrowserPath(path);
|
||||
}
|
||||
|
||||
void FilesystemWidget::onUmountButtonClicked()
|
||||
@@ -383,12 +635,13 @@ void FilesystemWidget::onUmountButtonClicked()
|
||||
}
|
||||
|
||||
refreshMountpoints();
|
||||
treeModel->refresh();
|
||||
setBrowserPath(QStringLiteral("/"));
|
||||
}
|
||||
|
||||
void FilesystemWidget::onCreateDirButtonClicked()
|
||||
{
|
||||
QModelIndex current = filesystemTree->currentIndex();
|
||||
current = current.sibling(current.row(), 0);
|
||||
QString currentPath = "/";
|
||||
if (current.isValid()) {
|
||||
QString path = current.data(Qt::UserRole).toString();
|
||||
@@ -416,6 +669,7 @@ void FilesystemWidget::onCreateDirButtonClicked()
|
||||
void FilesystemWidget::onCreateFileButtonClicked()
|
||||
{
|
||||
QModelIndex current = filesystemTree->currentIndex();
|
||||
current = current.sibling(current.row(), 0);
|
||||
QString currentPath = "/";
|
||||
if (current.isValid()) {
|
||||
QString path = current.data(Qt::UserRole).toString();
|
||||
@@ -447,11 +701,10 @@ void FilesystemWidget::onTreeItemDoubleClicked(const QModelIndex &index)
|
||||
|
||||
QString type = index.sibling(index.row(), 2).data(Qt::DisplayRole).toString();
|
||||
|
||||
if (type == "file") {
|
||||
QString path = index.data(Qt::UserRole).toString();
|
||||
if (path.isEmpty()) {
|
||||
path = index.data(Qt::DisplayRole).toString();
|
||||
}
|
||||
const QString path = index.sibling(index.row(), 0).data(Qt::UserRole).toString();
|
||||
if (type == "directory") {
|
||||
setBrowserPath(path);
|
||||
} else if (type == "file") {
|
||||
viewFileContents(path);
|
||||
}
|
||||
}
|
||||
@@ -466,7 +719,8 @@ void FilesystemWidget::onTreeExpanded(const QModelIndex &index)
|
||||
return;
|
||||
|
||||
// Check if it has a dummy child
|
||||
if (item->rowCount() == 1 && item->child(0)->text() == "Loading...") {
|
||||
if (!item->data(FilesystemTreeModel::LoadedRole).toBool()) {
|
||||
item->setData(true, FilesystemTreeModel::LoadedRole);
|
||||
// Remove dummy and load real children
|
||||
item->removeRow(0);
|
||||
QString path = item->data(Qt::UserRole).toString();
|
||||
@@ -483,23 +737,94 @@ void FilesystemWidget::onTreeContextMenu(const QPoint &pos)
|
||||
return;
|
||||
|
||||
QString type = index.sibling(index.row(), 2).data(Qt::DisplayRole).toString();
|
||||
filesystemTree->setCurrentIndex(index.sibling(index.row(), 0));
|
||||
|
||||
QMenu menu(this);
|
||||
if (type == "file") {
|
||||
menu.addAction(viewAction);
|
||||
menu.addAction(deleteAction);
|
||||
menu.addAction(mallocAction);
|
||||
const QString path = index.sibling(index.row(), 0).data(Qt::UserRole).toString();
|
||||
if (!isSourceFile(path)) {
|
||||
menu.addAction(deleteAction);
|
||||
menu.addAction(mallocAction);
|
||||
}
|
||||
}
|
||||
menu.exec(filesystemTree->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void FilesystemWidget::viewFileContents(const QString &path)
|
||||
{
|
||||
QString sanitizedPath = IaitoCore::sanitizeStringForCommand(path);
|
||||
QString cmd = QString("mc %1").arg(sanitizedPath);
|
||||
QString output = Core()->cmdRaw(cmd.toUtf8().constData());
|
||||
if (!output.isEmpty()) {
|
||||
QMessageBox::information(this, tr("File Contents"), output);
|
||||
QWidget *previousTab = previewPath == path ? previewTabs->currentWidget() : nullptr;
|
||||
clearPreview();
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const bool sourceFile = isSourceFile(path);
|
||||
const QByteArray pathBytes = path.toUtf8();
|
||||
QByteArray contents;
|
||||
bool truncated = false;
|
||||
{
|
||||
RCoreLocked core = Core()->core();
|
||||
RFSFile *file = r_fs_open(core->fs, pathBytes.constData(), false);
|
||||
if (!file) {
|
||||
previewStatus->setText(tr("Cannot open %1").arg(path));
|
||||
return;
|
||||
}
|
||||
// Virtual r2 files are already populated by open. Other plugins need a read.
|
||||
const int previewLimit = 1024 * 1024;
|
||||
const int length = int(qMin<quint64>(file->size, previewLimit));
|
||||
int bytesRead = length;
|
||||
if (!file->data && length > 0) {
|
||||
bytesRead = r_fs_read(core->fs, file, 0, length);
|
||||
}
|
||||
if (file->data && bytesRead >= 0) {
|
||||
const int available = int(qMin<quint64>(file->size, bytesRead));
|
||||
contents = QByteArray(reinterpret_cast<const char *>(file->data), available);
|
||||
truncated = file->size > quint64(available);
|
||||
}
|
||||
r_fs_close(core->fs, file);
|
||||
r_fs_file_free(file);
|
||||
if (bytesRead < 0) {
|
||||
previewStatus->setText(tr("Cannot read %1").arg(path));
|
||||
return;
|
||||
}
|
||||
}
|
||||
previewPath = path;
|
||||
const QString text = QString::fromUtf8(contents);
|
||||
fileContents->setPlainText(text);
|
||||
previewStatus->setText(truncated ? tr("%1 (preview truncated to 1 MiB)").arg(path) : path);
|
||||
if (!sourceFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
QSet<RVA> functions;
|
||||
const auto references = SourceLineReference::parse(text);
|
||||
{
|
||||
RCoreLocked core = Core()->core();
|
||||
for (const auto &reference : references) {
|
||||
auto *line = new QTreeWidgetItem(
|
||||
sourceLines,
|
||||
{RAddressString(reference.address),
|
||||
QString::number(reference.line),
|
||||
reference.code});
|
||||
line->setData(0, Qt::UserRole, QVariant::fromValue(reference.address));
|
||||
RAnalFunction *function = r_anal_get_fcn_in(core->anal, reference.address, 0);
|
||||
if (function && !functions.contains(function->addr)) {
|
||||
functions.insert(function->addr);
|
||||
auto *item = new QTreeWidgetItem(
|
||||
sourceFunctions,
|
||||
{QString::fromUtf8(function->name), RAddressString(function->addr)});
|
||||
item->setData(0, Qt::UserRole, QVariant::fromValue(RVA(function->addr)));
|
||||
}
|
||||
}
|
||||
}
|
||||
previewTabs->setTabEnabled(0, true);
|
||||
previewTabs->setTabEnabled(1, true);
|
||||
previewTabs->setCurrentWidget(
|
||||
previousTab ? previousTab : (functions.isEmpty() ? sourceLines : sourceFunctions));
|
||||
sourceFunctions->resizeColumnToContents(0);
|
||||
if (functions.isEmpty() && !references.isEmpty()) {
|
||||
previewStatus->setText(
|
||||
tr("%1 — no analyzed functions; click a source address to view code.").arg(path));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QStandardItem>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTabWidget>
|
||||
#include <QTreeView>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
@@ -42,7 +44,6 @@ struct FileInfo
|
||||
QString name;
|
||||
QString type; // 'f' for file, 'd' for directory
|
||||
quint64 size;
|
||||
QString timestamp; // if available
|
||||
};
|
||||
|
||||
class FilesystemTreeModel : public QStandardItemModel
|
||||
@@ -55,10 +56,13 @@ public:
|
||||
void setRootPath(const QString &path);
|
||||
void refresh();
|
||||
void populateDirectory(QStandardItem *parentItem, const QString &path);
|
||||
QString getRootPath() const { return rootPath; }
|
||||
|
||||
enum Roles { PathRole = Qt::UserRole, LoadedRole };
|
||||
|
||||
private:
|
||||
QString rootPath;
|
||||
QJsonDocument parseMdCommand(const QString &path);
|
||||
QList<FileInfo> readDirectory(const QString &path);
|
||||
};
|
||||
|
||||
class FilesystemWidget : public IaitoDockWidget
|
||||
@@ -69,6 +73,9 @@ public:
|
||||
explicit FilesystemWidget(MainWindow *main);
|
||||
~FilesystemWidget();
|
||||
|
||||
public slots:
|
||||
void showSourceFiles();
|
||||
|
||||
private slots:
|
||||
void refreshMountpoints();
|
||||
void onMountButtonClicked();
|
||||
@@ -78,6 +85,8 @@ private slots:
|
||||
void onTreeItemDoubleClicked(const QModelIndex &index);
|
||||
void onTreeContextMenu(const QPoint &pos);
|
||||
void onTreeExpanded(const QModelIndex &index);
|
||||
void refresh();
|
||||
void onTreeSelectionChanged(const QModelIndex &index);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
@@ -91,8 +100,9 @@ private:
|
||||
void viewFileContents(const QString &path);
|
||||
void deleteFile(const QString &path);
|
||||
void loadIntoMalloc(const QString &path);
|
||||
|
||||
MainWindow *mainWindow;
|
||||
void setBrowserPath(const QString &path);
|
||||
bool isSourceFile(const QString &path) const;
|
||||
void clearPreview();
|
||||
|
||||
// Mountpoints section
|
||||
QGroupBox *mountpointsGroup;
|
||||
@@ -110,6 +120,17 @@ private:
|
||||
FilesystemTreeModel *treeModel;
|
||||
QPushButton *createDirButton;
|
||||
QPushButton *createFileButton;
|
||||
QLineEdit *browserPathEdit;
|
||||
|
||||
QGroupBox *previewGroup;
|
||||
QLabel *previewStatus;
|
||||
QTabWidget *previewTabs;
|
||||
QTreeWidget *sourceFunctions;
|
||||
QTreeWidget *sourceLines;
|
||||
QPlainTextEdit *fileContents;
|
||||
QString previewPath;
|
||||
QList<MountpointInfo> currentMountpoints;
|
||||
RefreshDeferrer *refreshDeferrer;
|
||||
|
||||
// Context menu actions
|
||||
QAction *viewAction;
|
||||
|
||||
+52
-16
@@ -1,4 +1,5 @@
|
||||
#include "FlagsWidget.h"
|
||||
#include "NameListView.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "common/ShortcutManager.h"
|
||||
#include "core/MainWindow.h"
|
||||
@@ -161,33 +162,39 @@ FlagsWidget::FlagsWidget(MainWindow *main)
|
||||
flags_model = new FlagsModel(&flags, this);
|
||||
flags_proxy_model = new FlagsSortFilterProxyModel(flags_model, this);
|
||||
connect(
|
||||
ui->filterLineEdit,
|
||||
&QLineEdit::textChanged,
|
||||
ui->quickFilterView,
|
||||
&QuickFilterView::filterTextChanged,
|
||||
flags_proxy_model,
|
||||
&QSortFilterProxyModel::setFilterWildcard);
|
||||
&AddressableFilterProxyModel::setFilterWildcard);
|
||||
ui->flagsTreeView->setMainWindow(mainWindow);
|
||||
ui->flagsTreeView->setModel(static_cast<QAbstractItemModel *>(flags_proxy_model));
|
||||
auto names = new NameListView(
|
||||
ui->flagsTreeView,
|
||||
ui->flagsTreeView->getItemContextMenu(),
|
||||
flags_proxy_model,
|
||||
FlagsModel::NAME,
|
||||
objectName());
|
||||
ui->quickFilterView->addActionButton(names->toggleAction());
|
||||
ui->flagsTreeView->sortByColumn(FlagsModel::OFFSET, Qt::AscendingOrder);
|
||||
connect(ui->flagsTreeView, &QTreeView::clicked, this, &FlagsWidget::selectFlagRange);
|
||||
connect(ui->flagsTreeView, &QTreeView::activated, this, &FlagsWidget::selectFlagRange);
|
||||
|
||||
// Ctrl-F to move the focus to the Filter search box
|
||||
QShortcut *searchShortcut = ShortcutMgr()->registerShortcut("list.showFilter", this);
|
||||
connect(searchShortcut, &QShortcut::activated, ui->filterLineEdit, [this]() {
|
||||
ui->filterLineEdit->setFocus();
|
||||
});
|
||||
connect(searchShortcut, &QShortcut::activated, ui->quickFilterView, &QuickFilterView::showFilter);
|
||||
searchShortcut->setContext(Qt::WidgetWithChildrenShortcut);
|
||||
|
||||
// Esc to clear the filter entry
|
||||
QShortcut *clearShortcut = ShortcutMgr()->registerShortcut("list.clearFilter", this);
|
||||
connect(clearShortcut, &QShortcut::activated, [this] {
|
||||
if (ui->filterLineEdit->text().isEmpty()) {
|
||||
ui->flagsTreeView->setFocus();
|
||||
} else {
|
||||
ui->filterLineEdit->setText("");
|
||||
}
|
||||
});
|
||||
connect(clearShortcut, &QShortcut::activated, [this] { ui->quickFilterView->clearFilter(); });
|
||||
connect(
|
||||
ui->quickFilterView,
|
||||
&QuickFilterView::filterClosed,
|
||||
ui->flagsTreeView,
|
||||
static_cast<void (QWidget::*)()>(&QWidget::setFocus));
|
||||
clearShortcut->setContext(Qt::WidgetWithChildrenShortcut);
|
||||
|
||||
connect(ui->filterLineEdit, &QLineEdit::textChanged, this, [this] {
|
||||
connect(ui->quickFilterView, &QuickFilterView::filterTextChanged, this, [this] {
|
||||
tree->showItemsNumber(flags_proxy_model->rowCount());
|
||||
});
|
||||
|
||||
@@ -204,6 +211,21 @@ FlagsWidget::FlagsWidget(MainWindow *main)
|
||||
menu->addSeparator();
|
||||
menu->addAction(ui->actionRename);
|
||||
menu->addAction(ui->actionDelete);
|
||||
connect(menu, &QMenu::aboutToShow, this, [this] {
|
||||
const bool hasFlag = ui->flagsTreeView->itemAddress(ui->flagsTreeView->currentIndex())
|
||||
!= RVA_INVALID;
|
||||
ui->actionRename->setEnabled(hasFlag);
|
||||
ui->actionDelete->setEnabled(hasFlag);
|
||||
});
|
||||
connect(
|
||||
ui->flagsTreeView->selectionModel(),
|
||||
&QItemSelectionModel::currentChanged,
|
||||
this,
|
||||
[this](const QModelIndex &index) {
|
||||
const bool hasFlag = ui->flagsTreeView->itemAddress(index) != RVA_INVALID;
|
||||
ui->actionRename->setEnabled(hasFlag);
|
||||
ui->actionDelete->setEnabled(hasFlag);
|
||||
});
|
||||
addAction(ui->actionRename);
|
||||
addAction(ui->actionDelete);
|
||||
}
|
||||
@@ -214,6 +236,20 @@ FlagsWidget::~FlagsWidget()
|
||||
delete flags_model;
|
||||
}
|
||||
|
||||
void FlagsWidget::selectFlagRange(const QModelIndex &index)
|
||||
{
|
||||
const QVariant description = index.data(FlagsModel::FlagDescriptionRole);
|
||||
if (!description.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto flag = description.value<FlagDescription>();
|
||||
Core()->clearAddressRangeSelection();
|
||||
if (flag.size > 0 && flag.size < 1024 && flag.size <= RVA_MAX - flag.offset) {
|
||||
Core()->setAddressRangeSelection(flag.offset, flag.offset + flag.size - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void FlagsWidget::on_flagspaceCombo_currentTextChanged(const QString &arg1)
|
||||
{
|
||||
Q_UNUSED(arg1);
|
||||
@@ -224,7 +260,7 @@ void FlagsWidget::on_flagspaceCombo_currentTextChanged(const QString &arg1)
|
||||
void FlagsWidget::on_actionRename_triggered()
|
||||
{
|
||||
const QModelIndex index = ui->flagsTreeView->selectionModel()->currentIndex();
|
||||
if (!index.isValid()) {
|
||||
if (!index.isValid() || !index.data(FlagsModel::FlagDescriptionRole).isValid()) {
|
||||
return;
|
||||
}
|
||||
FlagDescription flag = index.data(FlagsModel::FlagDescriptionRole).value<FlagDescription>();
|
||||
@@ -245,7 +281,7 @@ void FlagsWidget::on_actionRename_triggered()
|
||||
void FlagsWidget::on_actionDelete_triggered()
|
||||
{
|
||||
const QModelIndex index = ui->flagsTreeView->selectionModel()->currentIndex();
|
||||
if (!index.isValid()) {
|
||||
if (!index.isValid() || !index.data(FlagsModel::FlagDescriptionRole).isValid()) {
|
||||
return;
|
||||
}
|
||||
FlagDescription flag = index.data(FlagsModel::FlagDescriptionRole).value<FlagDescription>();
|
||||
|
||||
@@ -70,6 +70,7 @@ public:
|
||||
~FlagsWidget();
|
||||
|
||||
private slots:
|
||||
void selectFlagRange(const QModelIndex &index);
|
||||
void on_flagspaceCombo_currentTextChanged(const QString &arg1);
|
||||
|
||||
void on_actionRename_triggered();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="flagsTreeView">
|
||||
<widget class="AddressableItemView" name="flagsTreeView">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">IaitoTreeView::item
|
||||
{
|
||||
@@ -71,14 +71,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="filterLineEdit">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Quick Filter</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QuickFilterView" name="quickFilterView" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="flagspaceLabel">
|
||||
@@ -119,7 +112,13 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AddressableItemList<></class>
|
||||
<class>QuickFilterView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/QuickFilterView.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
<container>1</container>
|
||||
|
||||
@@ -530,7 +530,7 @@ FunctionsWidget::FunctionsWidget(MainWindow *main)
|
||||
functionModel = new FunctionModel(
|
||||
&functions, &importAddresses, &mainAdress, false, default_font, highlight_font, this);
|
||||
functionProxyModel = new FunctionSortFilterProxyModel(functionModel, this);
|
||||
setModels(functionProxyModel);
|
||||
setModels(functionProxyModel, FunctionModel::NameColumn);
|
||||
ui->treeView->sortByColumn(FunctionModel::NameColumn, Qt::AscendingOrder);
|
||||
|
||||
connect(Config(), &Configuration::fontsUpdated, this, [this]() {
|
||||
@@ -593,6 +593,21 @@ FunctionsWidget::FunctionsWidget(MainWindow *main)
|
||||
itemConextMenu->setActionIcon(
|
||||
actionSetPinMenu, AddressableItemContextMenu::MenuIcon::Pin, QColor(103, 80, 164));
|
||||
itemConextMenu->setWholeFunction(true);
|
||||
connect(itemConextMenu, &QMenu::aboutToShow, this, [this] {
|
||||
const bool hasFunction = ui->treeView->itemAddress(ui->treeView->currentIndex())
|
||||
!= RVA_INVALID;
|
||||
actionRename.setEnabled(hasFunction);
|
||||
actionUndefine.setEnabled(hasFunction);
|
||||
actionSetColorMenu->setEnabled(hasFunction);
|
||||
actionSetPinMenu->setEnabled(hasFunction);
|
||||
});
|
||||
connect(
|
||||
ui->treeView->selectionModel(),
|
||||
&QItemSelectionModel::currentChanged,
|
||||
this,
|
||||
[this](const QModelIndex &index) {
|
||||
actionRename.setEnabled(ui->treeView->itemAddress(index) != RVA_INVALID);
|
||||
});
|
||||
|
||||
addActions(itemConextMenu->actions());
|
||||
|
||||
@@ -685,7 +700,7 @@ void FunctionsWidget::onActionFunctionsRenameTriggered()
|
||||
{
|
||||
// Get selected item in functions tree view
|
||||
const QModelIndex index = ui->treeView->selectionModel()->currentIndex();
|
||||
if (!index.isValid()) {
|
||||
if (!index.isValid() || !index.data(FunctionModel::FunctionDescriptionRole).isValid()) {
|
||||
return;
|
||||
}
|
||||
FunctionDescription function
|
||||
@@ -712,12 +727,7 @@ void FunctionsWidget::onActionFunctionsRenameTriggered()
|
||||
|
||||
void FunctionsWidget::onActionFunctionsUndefineTriggered()
|
||||
{
|
||||
const auto selection = ui->treeView->selectionModel()->selection().indexes();
|
||||
std::vector<RVA> offsets;
|
||||
offsets.reserve(selection.size());
|
||||
for (const auto &index : selection) {
|
||||
offsets.push_back(functionProxyModel->address(index));
|
||||
}
|
||||
const auto offsets = ui->treeView->selectedAddresses();
|
||||
for (RVA offset : offsets) {
|
||||
Core()->delFunction(offset);
|
||||
}
|
||||
@@ -725,15 +735,7 @@ void FunctionsWidget::onActionFunctionsUndefineTriggered()
|
||||
|
||||
void FunctionsWidget::onActionFunctionColorPicked(const QString &r2Color)
|
||||
{
|
||||
const auto selection = ui->treeView->selectionModel()->selection().indexes();
|
||||
std::vector<RVA> offsets;
|
||||
offsets.reserve(selection.size());
|
||||
for (const auto &index : selection) {
|
||||
RVA off = functionProxyModel->address(index);
|
||||
if (std::find(offsets.begin(), offsets.end(), off) == offsets.end()) {
|
||||
offsets.push_back(off);
|
||||
}
|
||||
}
|
||||
const auto offsets = ui->treeView->selectedAddresses();
|
||||
bool affectsView = false;
|
||||
RVA seek = Core()->getOffset();
|
||||
for (RVA offset : offsets) {
|
||||
@@ -755,15 +757,7 @@ void FunctionsWidget::onActionFunctionColorPicked(const QString &r2Color)
|
||||
|
||||
void FunctionsWidget::onActionFunctionPinPicked(const QString &emoji)
|
||||
{
|
||||
const auto selection = ui->treeView->selectionModel()->selection().indexes();
|
||||
std::vector<RVA> offsets;
|
||||
offsets.reserve(selection.size());
|
||||
for (const auto &index : selection) {
|
||||
RVA off = functionProxyModel->address(index);
|
||||
if (std::find(offsets.begin(), offsets.end(), off) == offsets.end()) {
|
||||
offsets.push_back(off);
|
||||
}
|
||||
}
|
||||
const auto offsets = ui->treeView->selectedAddresses();
|
||||
bool affectsView = false;
|
||||
RVA seek = Core()->getOffset();
|
||||
for (RVA offset : offsets) {
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "GlobalVariablesWidget.h"
|
||||
|
||||
#include "common/Helpers.h"
|
||||
#include "ui_GlobalVariablesWidget.h"
|
||||
#include "ui_ListDockWidget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
|
||||
GlobalVariablesModel::GlobalVariablesModel(QObject *parent)
|
||||
: AddressableItemModel<QAbstractListModel>(parent)
|
||||
{}
|
||||
|
||||
int GlobalVariablesModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : variables.size();
|
||||
}
|
||||
|
||||
int GlobalVariablesModel::columnCount(const QModelIndex &) const
|
||||
{
|
||||
return ColumnCount;
|
||||
}
|
||||
|
||||
QVariant GlobalVariablesModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= variables.size()) {
|
||||
return {};
|
||||
}
|
||||
const auto &variable = variables.at(index.row());
|
||||
if (role == DescriptionRole) {
|
||||
return QVariant::fromValue(variable);
|
||||
}
|
||||
if (role != Qt::DisplayRole && role != Qt::EditRole) {
|
||||
return {};
|
||||
}
|
||||
switch (index.column()) {
|
||||
case AddressColumn:
|
||||
return role == Qt::EditRole ? QVariant::fromValue(variable.address)
|
||||
: QVariant(RAddressString(variable.address));
|
||||
case NameColumn:
|
||||
return variable.name;
|
||||
case TypeColumn:
|
||||
return variable.type;
|
||||
case SizeColumn:
|
||||
return role == Qt::EditRole ? QVariant::fromValue(variable.size)
|
||||
: QVariant(RSizeString(variable.size));
|
||||
case CommentColumn:
|
||||
return Core()->getCommentAt(variable.address);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QVariant GlobalVariablesModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
|
||||
return {};
|
||||
}
|
||||
switch (section) {
|
||||
case AddressColumn:
|
||||
return tr("Address");
|
||||
case NameColumn:
|
||||
return tr("Name");
|
||||
case TypeColumn:
|
||||
return tr("Type");
|
||||
case SizeColumn:
|
||||
return tr("Size");
|
||||
case CommentColumn:
|
||||
return tr("Comment");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
RVA GlobalVariablesModel::address(const QModelIndex &index) const
|
||||
{
|
||||
return index.isValid() && index.row() >= 0 && index.row() < variables.size()
|
||||
? variables.at(index.row()).address
|
||||
: RVA_INVALID;
|
||||
}
|
||||
|
||||
QString GlobalVariablesModel::name(const QModelIndex &index) const
|
||||
{
|
||||
return index.isValid() && index.row() >= 0 && index.row() < variables.size()
|
||||
? variables.at(index.row()).name
|
||||
: QString();
|
||||
}
|
||||
|
||||
void GlobalVariablesModel::setVariables(QList<GlobalVariableDescription> variables)
|
||||
{
|
||||
beginResetModel();
|
||||
this->variables = std::move(variables);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
GlobalVariablesWidget::GlobalVariablesWidget(MainWindow *main)
|
||||
: ListDockWidget(main)
|
||||
, controls(new Ui::GlobalVariablesWidget)
|
||||
, model(new GlobalVariablesModel(this))
|
||||
, proxy(new AddressableFilterProxyModel(model, this))
|
||||
{
|
||||
setObjectName(QStringLiteral("GlobalVariablesWidget"));
|
||||
setWindowTitle(tr("Global Variables"));
|
||||
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxy->setFilterKeyColumn(-1);
|
||||
proxy->setSortRole(Qt::EditRole);
|
||||
setModels(proxy, GlobalVariablesModel::NameColumn, GlobalVariablesModel::TypeColumn);
|
||||
ui->treeView->sortByColumn(GlobalVariablesModel::AddressColumn, Qt::AscendingOrder);
|
||||
|
||||
auto toolbar = new QWidget(this);
|
||||
controls->setupUi(toolbar);
|
||||
ui->verticalLayout->insertWidget(0, toolbar);
|
||||
auto menu = ui->treeView->getItemContextMenu();
|
||||
menu->addSeparator();
|
||||
auto addAction = menu->addAction(tr("Register…"));
|
||||
addAction->setToolTip(tr("Register a global variable"));
|
||||
addAction->setObjectName(QStringLiteral("actionAddGlobalVariable"));
|
||||
addAction->setProperty("addressIndependent", true);
|
||||
editAction = menu->addAction(tr("Edit…"));
|
||||
editAction->setToolTip(tr("Edit the global variable name and type"));
|
||||
editAction->setObjectName(QStringLiteral("actionEditGlobalVariable"));
|
||||
deleteAction = menu->addAction(tr("Delete"));
|
||||
deleteAction->setToolTip(tr("Delete the global variable"));
|
||||
deleteAction->setObjectName(QStringLiteral("actionDeleteGlobalVariable"));
|
||||
auto refreshAction = menu->addAction(tr("Refresh"));
|
||||
refreshAction->setObjectName(QStringLiteral("actionRefreshGlobalVariables"));
|
||||
refreshAction->setProperty("addressIndependent", true);
|
||||
controls->addButton->setDefaultAction(addAction);
|
||||
controls->editButton->setDefaultAction(editAction);
|
||||
controls->deleteButton->setDefaultAction(deleteAction);
|
||||
controls->refreshButton->setDefaultAction(refreshAction);
|
||||
editAction->setEnabled(false);
|
||||
deleteAction->setEnabled(false);
|
||||
|
||||
connect(addAction, &QAction::triggered, this, [this]() { editVariable(true); });
|
||||
connect(editAction, &QAction::triggered, this, [this]() { editVariable(false); });
|
||||
connect(deleteAction, &QAction::triggered, this, &GlobalVariablesWidget::deleteVariable);
|
||||
connect(refreshAction, &QAction::triggered, this, &GlobalVariablesWidget::refreshVariables);
|
||||
const auto updateActions = [this]() {
|
||||
const bool valid = currentVariable().address != RVA_INVALID;
|
||||
editAction->setEnabled(valid);
|
||||
deleteAction->setEnabled(valid);
|
||||
};
|
||||
connect(ui->treeView->selectionModel(), &QItemSelectionModel::currentChanged, this, updateActions);
|
||||
connect(model, &QAbstractItemModel::modelReset, this, updateActions);
|
||||
|
||||
refreshDeferrer = createRefreshDeferrer([this]() { refreshVariables(); });
|
||||
connect(Core(), &IaitoCore::flagsChanged, this, &GlobalVariablesWidget::refreshVariables);
|
||||
connect(Core(), &IaitoCore::refreshAll, this, &GlobalVariablesWidget::refreshVariables);
|
||||
connect(Core(), &IaitoCore::codeRebased, this, &GlobalVariablesWidget::refreshVariables);
|
||||
connect(Core(), &IaitoCore::commentsChanged, this, [this]() {
|
||||
qhelpers::emitColumnChanged(model, GlobalVariablesModel::CommentColumn);
|
||||
});
|
||||
connect(
|
||||
this, &IaitoDockWidget::becameVisibleToUser, this, &GlobalVariablesWidget::refreshVariables);
|
||||
refreshVariables();
|
||||
}
|
||||
|
||||
GlobalVariablesWidget::~GlobalVariablesWidget()
|
||||
{
|
||||
ui->treeView->setModel(static_cast<QAbstractItemModel *>(nullptr));
|
||||
}
|
||||
|
||||
void GlobalVariablesWidget::refreshVariables()
|
||||
{
|
||||
if (!refreshDeferrer->attemptRefresh(nullptr)) {
|
||||
return;
|
||||
}
|
||||
model->setVariables(Core()->getAllGlobalVariables());
|
||||
for (int column = 0; column < GlobalVariablesModel::ColumnCount; ++column) {
|
||||
ui->treeView->resizeColumnToContents(column);
|
||||
}
|
||||
}
|
||||
|
||||
GlobalVariableDescription GlobalVariablesWidget::currentVariable() const
|
||||
{
|
||||
return ui->treeView->currentIndex()
|
||||
.data(GlobalVariablesModel::DescriptionRole)
|
||||
.value<GlobalVariableDescription>();
|
||||
}
|
||||
|
||||
void GlobalVariablesWidget::editVariable(bool create)
|
||||
{
|
||||
const auto variable = currentVariable();
|
||||
if (!create && variable.address == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
QDialog dialog(this);
|
||||
dialog.setObjectName(QStringLiteral("globalVariableDialog"));
|
||||
dialog.setWindowTitle(create ? tr("Register Global Variable") : tr("Edit Global Variable"));
|
||||
auto layout = new QFormLayout(&dialog);
|
||||
auto address
|
||||
= new QLineEdit(RAddressString(create ? Core()->getOffset() : variable.address), &dialog);
|
||||
address->setObjectName(QStringLiteral("globalVariableAddress"));
|
||||
address->setReadOnly(!create);
|
||||
address->setToolTip(tr("An address or radare2 expression, such as sym.counter or $$ + 8."));
|
||||
auto name = new QLineEdit(create ? QString() : variable.name, &dialog);
|
||||
name->setObjectName(QStringLiteral("globalVariableName"));
|
||||
auto type = new QComboBox(&dialog);
|
||||
type->setObjectName(QStringLiteral("globalVariableType"));
|
||||
type->setEditable(true);
|
||||
QStringList types;
|
||||
for (const auto &description : Core()->getAllTypes()) {
|
||||
types.append(description.type);
|
||||
}
|
||||
// Primitive types in current r2 are returned in the types member of tj.
|
||||
const auto primitiveTypes = Core()->cmdj("tj").object()[QStringLiteral("types")].toArray();
|
||||
for (const auto &value : primitiveTypes) {
|
||||
const QString typeName = value.toObject()[QStringLiteral("type")].toString();
|
||||
types.append(typeName);
|
||||
}
|
||||
types.removeDuplicates();
|
||||
types.sort();
|
||||
for (const auto &typeName : types) {
|
||||
type->addItem(typeName, typeName);
|
||||
}
|
||||
type->setCurrentText(create ? QStringLiteral("int") : variable.type);
|
||||
auto error = new QLabel(&dialog);
|
||||
error->setObjectName(QStringLiteral("globalVariableError"));
|
||||
error->setWordWrap(true);
|
||||
error->hide();
|
||||
auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||||
layout->addRow(tr("Address:"), address);
|
||||
layout->addRow(tr("Name:"), name);
|
||||
layout->addRow(tr("Type:"), type);
|
||||
layout->addRow(error);
|
||||
layout->addRow(buttons);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, [&]() {
|
||||
RVA at;
|
||||
QString message;
|
||||
if (!Core()->tryMath(address->text(), at) || at == RVA_INVALID) {
|
||||
message = tr("Enter a valid address or radare2 expression.");
|
||||
} else {
|
||||
if (Core()->setGlobalVariable(
|
||||
at,
|
||||
name->text().trimmed(),
|
||||
type->currentText().trimmed(),
|
||||
create ? QString() : variable.name,
|
||||
&message)) {
|
||||
dialog.accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
error->setText(message);
|
||||
error->show();
|
||||
});
|
||||
name->setFocus();
|
||||
name->selectAll();
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void GlobalVariablesWidget::deleteVariable()
|
||||
{
|
||||
const auto variable = currentVariable();
|
||||
if (variable.address == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
if (!Core()->deleteGlobalVariable(variable.address, variable.name)) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
tr("Delete Global Variable"),
|
||||
tr("Could not delete the global variable. Refresh the list and check that no other "
|
||||
"global variable shares its address."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#ifndef GLOBALVARIABLESWIDGET_H
|
||||
#define GLOBALVARIABLESWIDGET_H
|
||||
|
||||
#include "ListDockWidget.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
namespace Ui {
|
||||
class GlobalVariablesWidget;
|
||||
}
|
||||
|
||||
class GlobalVariablesModel : public AddressableItemModel<QAbstractListModel>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Column { AddressColumn, NameColumn, TypeColumn, SizeColumn, CommentColumn, ColumnCount };
|
||||
enum Role { DescriptionRole = Qt::UserRole };
|
||||
|
||||
explicit GlobalVariablesModel(QObject *parent = nullptr);
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
int columnCount(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;
|
||||
RVA address(const QModelIndex &index) const override;
|
||||
QString name(const QModelIndex &index) const override;
|
||||
void setVariables(QList<GlobalVariableDescription> variables);
|
||||
|
||||
private:
|
||||
QList<GlobalVariableDescription> variables;
|
||||
};
|
||||
|
||||
class GlobalVariablesWidget : public ListDockWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GlobalVariablesWidget(MainWindow *main);
|
||||
~GlobalVariablesWidget() override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::GlobalVariablesWidget> controls;
|
||||
GlobalVariablesModel *model;
|
||||
AddressableFilterProxyModel *proxy;
|
||||
RefreshDeferrer *refreshDeferrer;
|
||||
QAction *editAction;
|
||||
QAction *deleteAction;
|
||||
|
||||
void refreshVariables();
|
||||
void editVariable(bool create);
|
||||
void deleteVariable();
|
||||
GlobalVariableDescription currentVariable() const;
|
||||
};
|
||||
|
||||
#endif // GLOBALVARIABLESWIDGET_H
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>GlobalVariablesWidget</class>
|
||||
<widget class="QWidget" name="GlobalVariablesWidget">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<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="QToolButton" name="addButton"/></item>
|
||||
<item><widget class="QToolButton" name="editButton"/></item>
|
||||
<item><widget class="QToolButton" name="deleteButton"/></item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
<property name="sizeHint" stdset="0"><size><width>0</width><height>0</height></size></property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item><widget class="QToolButton" name="refreshButton"/></item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -144,6 +144,7 @@ void GraphView::contextMenuEvent(QContextMenuEvent *event)
|
||||
void GraphView::computeGraphPlacement()
|
||||
{
|
||||
graphLayoutSystem->CalculateLayout(blocks, entry, width, height);
|
||||
graphLayoutChanged();
|
||||
setCacheDirty();
|
||||
clampViewOffset();
|
||||
viewport()->update();
|
||||
@@ -371,6 +372,10 @@ void GraphView::paint(QPainter &p, QPoint offset, QRect viewport, qreal scale, b
|
||||
p.setWindow(window);
|
||||
QRectF windowF(window.x(), window.y(), window.width(), window.height());
|
||||
|
||||
p.save();
|
||||
drawBackground(p, interactive);
|
||||
p.restore();
|
||||
|
||||
for (auto &blockIt : blocks) {
|
||||
GraphBlock &block = blockIt.second;
|
||||
|
||||
|
||||
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user