mirror of
https://github.com/radareorg/iaito.git
synced 2026-11-07 14:38:16 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4badc5fd4 | ||
|
|
49b4b19c21 | ||
|
|
068520f923 | ||
|
|
f723ee3dee | ||
|
|
ce1f9181cf | ||
|
|
a043f93e72 | ||
|
|
c0cb4a5123 | ||
|
|
db64c517a4 | ||
|
|
9602d98307 | ||
|
|
d85739b5aa | ||
|
|
1828006c2b | ||
|
|
aff259387b | ||
|
|
fe80cff908 | ||
|
|
ae183f2c43 | ||
|
|
9ae80e4d62 | ||
|
|
e920c6aba8 | ||
|
|
5875f989d8 | ||
|
|
d95daf392f | ||
|
|
6c4997ac29 | ||
|
|
a8d239febd | ||
|
|
6b52aa0be1 | ||
|
|
19c3c07b99 | ||
|
|
ef8aba25e8 | ||
|
|
c2f042643a | ||
|
|
baf1afaca0 | ||
|
|
de8f2b476e | ||
|
|
bc6c362bd5 | ||
|
|
bff22b12bd | ||
|
|
63f2bd57cd | ||
|
|
5518aaa29d | ||
|
|
73856c6c32 | ||
|
|
018c210777 | ||
|
|
59a47845f0 | ||
|
|
11b530a526 | ||
|
|
f6b792020a | ||
|
|
c50dc3313f | ||
|
|
0e5a49c331 | ||
|
|
74c12685a0 | ||
|
|
93af6edfe9 | ||
|
|
977578a91e | ||
|
|
e980b1cb48 | ||
|
|
b5edef36bc | ||
|
|
1208624c77 | ||
|
|
eff435fc29 | ||
|
|
af1f536b0f | ||
|
|
6affec227d | ||
|
|
e0a120ad08 | ||
|
|
3383cc1d92 | ||
|
|
7eade2df0c | ||
|
|
bd1105f45d | ||
|
|
a7532b4d80 | ||
|
|
11240a1a0f | ||
|
|
ddb7bdb428 | ||
|
|
b912778e62 | ||
|
|
dc1fde07f5 | ||
|
|
1e089d9c37 | ||
|
|
e0e9690fd5 | ||
|
|
bbbea0adcd | ||
|
|
f8ef719e31 | ||
|
|
e560d5f88c | ||
|
|
3f9e2f78f9 | ||
|
|
d29c291ff7 | ||
|
|
e62e0a9d70 | ||
|
|
0ccc495308 | ||
|
|
95ca7a071f | ||
|
|
4e100c4fc3 | ||
|
|
abf90e6eaa | ||
|
|
dd28d5b367 | ||
|
|
d7fabe89f2 | ||
|
|
069b64679c | ||
|
|
7a8701f030 | ||
|
|
dafb693b59 | ||
|
|
8df94a0b91 | ||
|
|
e9c99b6da4 | ||
|
|
144dcce52f | ||
|
|
f6c5cf0cb8 | ||
|
|
c3ecfea9bb | ||
|
|
5c7dbaca9c | ||
|
|
2982ce12f9 | ||
|
|
eaf3b1ae36 | ||
|
|
c2da3b5c88 | ||
|
|
815267962d | ||
|
|
60da11e8d9 | ||
|
|
244596bcb0 | ||
|
|
27be4123c3 | ||
|
|
158ba57ae5 | ||
|
|
bb0c8ab6d4 | ||
|
|
e35d443079 | ||
|
|
55a401b3dc | ||
|
|
0ecf546b80 | ||
|
|
4a14b8ddfb | ||
|
|
61f47fca35 | ||
|
|
ab9eb65ac3 | ||
|
|
04ca1b2f13 | ||
|
|
24e410c41a | ||
|
|
9c0cf0116c | ||
|
|
3ea7715576 | ||
|
|
455617e037 | ||
|
|
0ff5a468e5 | ||
|
|
fb1b22c35f | ||
|
|
f67b3a624b | ||
|
|
146b28d184 | ||
|
|
f90e9931bb | ||
|
|
36702df4d2 | ||
|
|
e5e991eef1 | ||
|
|
5a2cacbb22 | ||
|
|
9ec68f2426 | ||
|
|
37c69f1be1 | ||
|
|
4907be5edb | ||
|
|
a426633df6 | ||
|
|
585abd51ea | ||
|
|
bc98912424 | ||
|
|
8c54b1650e | ||
|
|
fba1753457 | ||
|
|
7633fc891c | ||
|
|
976b41ffd5 | ||
|
|
d647d4291f | ||
|
|
45e895d746 | ||
|
|
c5b3632379 | ||
|
|
9ebea99c80 | ||
|
|
bd90af5a2a | ||
|
|
f48325b11e | ||
|
|
7a982fb681 | ||
|
|
da160271ad | ||
|
|
db945fc9b1 | ||
|
|
59c69f21c9 | ||
|
|
0dc50a7306 | ||
|
|
33223ed594 | ||
|
|
ed44971875 | ||
|
|
391d1d385e | ||
|
|
acf34585a5 | ||
|
|
7920ad3be6 | ||
|
|
67c2ccdb89 | ||
|
|
2638a61b59 | ||
|
|
cfc83bc92f | ||
|
|
4528457e93 | ||
|
|
60671f810d | ||
|
|
8bd4789a00 | ||
|
|
1a4ae4b22b |
No files matched your search
+182
-76
@@ -1,12 +1,18 @@
|
||||
name: iaito CI
|
||||
|
||||
env:
|
||||
R2V: 6.1.6
|
||||
R2GHIDRA_VERSION: 6.1.6
|
||||
R2V: 6.2.6
|
||||
R2GHIDRA_VERSION: 6.2.6
|
||||
QT_MAC_INSTALL_VERSION: 6.11.*
|
||||
QT_WIN_INSTALL_VERSION: 6.10.*
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Existing version tag to build and publish
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -15,14 +21,57 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
acr-linux-r2git:
|
||||
runs-on: ubuntu-22.04
|
||||
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@v6
|
||||
- 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:
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: apt dependencies
|
||||
@@ -35,37 +84,34 @@ jobs:
|
||||
run: |
|
||||
./configure --prefix=/usr
|
||||
make -j4
|
||||
- name: command-line checks
|
||||
run: python3 tests/commandline/test_commandline.py build/iaito
|
||||
- name: packaging
|
||||
run: make -j -C dist/debian
|
||||
indent-check:
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/checkout@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@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.source.outputs.sha }}
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: apt dependencies
|
||||
@@ -82,6 +128,9 @@ jobs:
|
||||
run: ./configure --prefix=/usr --with-qmake6
|
||||
- name: build iaito
|
||||
run: make -j4
|
||||
- name: command-line checks
|
||||
if: ${{ hashFiles('tests/commandline/test_commandline.py') != '' }}
|
||||
run: python3 tests/commandline/test_commandline.py build/iaito
|
||||
- name: packaging
|
||||
run: make -C dist/debian
|
||||
- uses: actions/upload-artifact@v7
|
||||
@@ -89,13 +138,14 @@ jobs:
|
||||
name: iaito-amd64.deb
|
||||
path: dist/debian/*/*.deb
|
||||
acr-linux-nodebugger:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: apt dependencies
|
||||
@@ -112,7 +162,15 @@ jobs:
|
||||
run: ./configure --prefix=/usr --with-qmake6 --without-debugger
|
||||
- name: build iaito
|
||||
run: make -j4
|
||||
- name: command-line checks
|
||||
run: python3 tests/commandline/test_commandline.py build/iaito
|
||||
acr-macos:
|
||||
needs: source
|
||||
env:
|
||||
R2V: ${{ needs.source.outputs.r2 }}
|
||||
R2GHIDRA_VERSION: ${{ needs.source.outputs.r2ghidra }}
|
||||
QT_MAC_INSTALL_VERSION: ${{ needs.source.outputs.qt_mac }}
|
||||
QT_WIN_INSTALL_VERSION: ${{ needs.source.outputs.qt_win }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -125,11 +183,12 @@ jobs:
|
||||
name: acr-macos-${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.source.outputs.sha }}
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: uname
|
||||
@@ -145,10 +204,41 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{github.token}}
|
||||
run: |
|
||||
curl -Lo radare2.pkg "https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{matrix.arch}}-${{env.R2V}}.pkg"
|
||||
curl -Lo r2ai.tar.gz "$(gh api /repos/radareorg/r2ai/releases/latest --jq .tarball_url)"
|
||||
curl -Lo r2ghidra.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra-${{env.R2GHIDRA_VERSION}}-macos-${{matrix.arch_alt||matrix.arch}}.zip"
|
||||
curl -Lo r2ghidra_sleigh.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra_sleigh-${{env.R2GHIDRA_VERSION}}.zip"
|
||||
validate_download() {
|
||||
case "$2" in
|
||||
tar.gz) tar -tzf "$1" >/dev/null 2>&1 ;;
|
||||
zip) unzip -tq "$1" >/dev/null 2>&1 ;;
|
||||
*) test -s "$1" ;;
|
||||
esac
|
||||
}
|
||||
download() {
|
||||
local output="$1"
|
||||
local url="$2"
|
||||
local type="$3"
|
||||
local attempt
|
||||
|
||||
for ((attempt = 1; attempt <= 3; attempt++)); do
|
||||
rm -f "${output}.tmp"
|
||||
if curl --fail --location --silent --show-error \
|
||||
--connect-timeout 30 --max-time 300 --output "${output}.tmp" "$url" \
|
||||
&& validate_download "${output}.tmp" "$type"; then
|
||||
mv "${output}.tmp" "$output"
|
||||
return 0
|
||||
fi
|
||||
if [ "$attempt" -lt 3 ]; then
|
||||
echo "Download of ${output} failed (attempt ${attempt}/3); retrying in 10s"
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
echo "::error::Unable to download a valid ${output} after 3 attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
r2ai_version="$(gh api /repos/radareorg/r2ai/releases/latest --jq .tag_name)"
|
||||
download radare2.pkg "https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{matrix.arch}}-${{env.R2V}}.pkg" pkg
|
||||
download r2ai.tar.gz "https://github.com/radareorg/r2ai/archive/refs/tags/${r2ai_version}.tar.gz" tar.gz
|
||||
download r2ghidra.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra-${{env.R2GHIDRA_VERSION}}-macos-${{matrix.arch_alt||matrix.arch}}.zip" zip
|
||||
download r2ghidra_sleigh.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra_sleigh-${{env.R2GHIDRA_VERSION}}.zip" zip
|
||||
make unpack-comps
|
||||
- name: install r2
|
||||
run: sudo installer -pkg dist/macos/radare2.pkg -target /
|
||||
@@ -165,55 +255,40 @@ jobs:
|
||||
name: iaito-macos-${{ matrix.arch }}
|
||||
path: dist/macos/iaito.zip
|
||||
meson:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04]
|
||||
python-version: [3.12.x]
|
||||
system-deps: [true]
|
||||
cc-override: [default]
|
||||
cxx-override: [default]
|
||||
# Prevent one job from pausing the rest
|
||||
fail-fast: false
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false # 'recursive' 'true' or 'false'
|
||||
persist-credentials: false
|
||||
- name: apt dependencies
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgraphviz-dev mesa-common-dev ninja-build libqt5svg5-dev qttools5-dev qttools5-dev-tools qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools
|
||||
- name: install r2
|
||||
run: git clone --depth=1 https://github.com/radareorg/radare2 r2git && CFLAGS=-O0 r2git/sys/install.sh
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: install dependencies
|
||||
if: contains(matrix.os, 'macos')
|
||||
run: pip3 install meson ninja
|
||||
- name: install Qt
|
||||
if: contains(matrix.os, 'macos')
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{env.QT_MAC_INSTALL_VERSION}}
|
||||
- name: macos build
|
||||
if: contains(matrix.os, 'macos')
|
||||
run: |
|
||||
meson setup -Dwith_qt6=true build src
|
||||
ninja -C build -j4
|
||||
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2_${{env.R2V}}_amd64.deb
|
||||
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-dev_${{env.R2V}}_amd64.deb
|
||||
sudo dpkg -i *.deb
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.12.x
|
||||
- name: meson+ninja ubuntu
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
pip install meson
|
||||
meson setup build src
|
||||
ninja -C build -j4
|
||||
w64-meson:
|
||||
needs: source
|
||||
env:
|
||||
R2V: ${{ needs.source.outputs.r2 }}
|
||||
QT_WIN_INSTALL_VERSION: ${{ needs.source.outputs.qt_win }}
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.source.outputs.sha }}
|
||||
submodules: false
|
||||
persist-credentials: false
|
||||
- name: Preparing msvc toolchain
|
||||
@@ -233,6 +308,8 @@ jobs:
|
||||
curl -L -o r2.zip https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{env.R2V}}-w64.zip
|
||||
7z x r2.zip
|
||||
mv radare2-${{env.R2V}}-w64 radare2
|
||||
# XXX workaround for r2-6.2.4 headers not being msvc c++ friendly (fixed in r2 master)
|
||||
sed -i 's/(RStrs) { NULL, NULL }/r_strs_new (NULL, NULL)/' radare2/include/libr/r_cmd.h
|
||||
echo "%CD%\radare2\bin" >> $GITHUB_PATH
|
||||
- name: configure
|
||||
shell: cmd
|
||||
@@ -255,33 +332,46 @@ 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@v6
|
||||
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
|
||||
environment: release
|
||||
# Environment secrets for signing:
|
||||
# APPLE_DEVELOPER_CERTIFICATE_P12_BASE64:
|
||||
# Base64 of the p12 export (cert+private key), the certificate must be named
|
||||
# "Developer ID Application: DEVELOPER_NAME (TEAM_ID)"
|
||||
# APPLE_DEVELOPER_CERTIFICATE_PASSWORD: Random password used to encrypt the exported P12
|
||||
# Environment secrets for notarization:
|
||||
# APPLE_DEVELOPER_ID: email of the developer
|
||||
# APPLE_TEAM_ID: Team ID (can be extracted from the certificate name too)
|
||||
# APPLE_APP_SPECIFIC_PASSWORD: https://support.apple.com/en-us/102654
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: refs/tags/${{ needs.check_release.outputs.tag_name }}
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -364,23 +454,34 @@ jobs:
|
||||
with:
|
||||
name: iaito-x64.dmg
|
||||
path: iaito-x64.dmg
|
||||
|
||||
- name: Remove keychain
|
||||
if: always()
|
||||
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@v6
|
||||
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:
|
||||
@@ -396,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:
|
||||
@@ -445,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,11 +13,11 @@ 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:
|
||||
image: returntocorp/semgrep
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- run: semgrep ci
|
||||
@@ -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
|
||||
@@ -71,6 +71,7 @@ mrproper: clean
|
||||
|
||||
.PHONY: install run user-install dist macos clean mrproper install-translations
|
||||
.PHONY: dockindent dockindent-image indent indent-host
|
||||
.PHONY: install-deeplink uninstall-deeplink
|
||||
|
||||
# force qt5 build when QtCreator is installed in user's home
|
||||
ifeq ($(shell test -x ~/Qt/5.*/clang_64/bin/qmake || echo err),)
|
||||
@@ -97,6 +98,8 @@ ifeq ($(shell uname),Darwin)
|
||||
ln -fs '/Applications/iaito.app/Contents/MacOS/iaito' $(DESTDIR)/$(PREFIX)'/bin/iaito'
|
||||
else
|
||||
$(MAKE) -C build install INSTALL_ROOT=$(DESTDIR)
|
||||
# Refresh the desktop DB so the .desktop MimeType registers iaito://
|
||||
-update-desktop-database "$(DESTDIR)$(PREFIX)/share/applications" 2>/dev/null
|
||||
endif
|
||||
$(MAKE) install-man
|
||||
|
||||
@@ -109,6 +112,7 @@ else
|
||||
rm -rf "$(DESTDIR)/$(PREFIX)/bin/iaito"
|
||||
endif
|
||||
rm -f "${DESTDIR}$(MANDIR)/man1/iaito.1"
|
||||
-$(MAKE) uninstall-deeplink
|
||||
|
||||
user-install: build
|
||||
ifeq ($(shell uname),Darwin)
|
||||
@@ -119,11 +123,16 @@ else
|
||||
$(MAKE) -C build install INSTALL_ROOT=/ PREFIX=${HOME}/.local
|
||||
endif
|
||||
$(MAKE) install-man DESTDIR=/ PREFIX=${HOME}/.local MANDIR=${HOME}/.local/share/man
|
||||
$(MAKE) install-deeplink
|
||||
|
||||
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:
|
||||
@@ -131,6 +140,18 @@ install-desktop-user:
|
||||
@mkdir -p $(HOME)/.local/share/icons/hicolor/scalable/apps
|
||||
@cp -f src/org.radare.iaito.desktop $(HOME)/.local/share/applications/org.radare.iaito.desktop
|
||||
@cp -f src/img/org.radare.iaito.svg $(HOME)/.local/share/icons/hicolor/scalable/apps/org.radare.iaito.svg
|
||||
@$(MAKE) install-deeplink
|
||||
|
||||
# Register/unregister the iaito:// deep link URI scheme handler
|
||||
install-deeplink:
|
||||
ifeq ($(shell uname),Darwin)
|
||||
sh scripts/deeplink/register.sh "$(DESTDIR)/Applications/iaito.app"
|
||||
else
|
||||
sh scripts/deeplink/register.sh
|
||||
endif
|
||||
|
||||
uninstall-deeplink:
|
||||
sh scripts/deeplink/unregister.sh
|
||||
|
||||
gdb:
|
||||
gdb --args $(BIN)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
# This script was automatically generated by ACR v2.2.6
|
||||
# This script was automatically generated by ACR v2.2.8
|
||||
# @author: pancake <nopcode.org>
|
||||
# @url: http://www.nopcode.org
|
||||
# @repo: git clone https://github.com/radare/acr
|
||||
@@ -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.1.6' ; VERSION_MAJOR=6; VERSION_MINOR=1; VERSION_PATCH=6; VERSION_NUMBER=60106; 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.1.6 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.1.6"
|
||||
echo "6.2.6"
|
||||
exit 0
|
||||
fi
|
||||
echo "iaito-6.1.6 configuration script done with acr v2.2.6.
|
||||
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.1.6"
|
||||
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.1.6
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,264 @@
|
||||
Iaito Deep Links
|
||||
================
|
||||
|
||||
Iaito registers the `iaito://` URI scheme so binaries, projects, samples and raw
|
||||
bytes can be referenced with stable, shareable links that open a specific
|
||||
location and view. Clicking such a link (from a browser, file manager, chat
|
||||
client, terminal, etc.) launches Iaito and navigates straight to the target.
|
||||
|
||||
The implementation lives in a single, self-contained pair of files,
|
||||
`src/common/DeepLink.{h,cpp}`, so it is easy to find, maintain and remove.
|
||||
|
||||
|
||||
URI format
|
||||
----------
|
||||
|
||||
```
|
||||
iaito://<handler>/<payload>?<query>
|
||||
```
|
||||
|
||||
There are exactly four top-level handlers:
|
||||
|
||||
| Handler | Payload | Opens |
|
||||
|----------------|------------------|------------------------------------------|
|
||||
| `file` | absolute path | a file from disk |
|
||||
| `project` | project name | a saved radare2/iaito project |
|
||||
| `sha256` | 64 hex chars | a locally registered sample by its hash |
|
||||
| `bytes` | hex byte string | an in-memory buffer built from raw bytes |
|
||||
|
||||
Only **absolute paths** are accepted for `file`.
|
||||
|
||||
As a convenience, if the host part of the URI is empty the absolute path is
|
||||
treated as a `file` reference:
|
||||
|
||||
```
|
||||
iaito:///bin/ls is the same as iaito://file/bin/ls
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
iaito://file/tmp/a.out
|
||||
iaito://project/r2
|
||||
iaito://sha256/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
iaito://bytes/554889e54883ec10
|
||||
```
|
||||
|
||||
|
||||
Query parameters
|
||||
----------------
|
||||
|
||||
Parameters are passed as standard URL query parameters (`?a=1&b=2`), not as
|
||||
`#fragments`. Unknown parameters are ignored, so the format can be extended in
|
||||
the future without breaking existing links.
|
||||
|
||||
### Navigation
|
||||
|
||||
| Param | Meaning |
|
||||
|---------|------------------------------------------------------------------------|
|
||||
| `addr` | Any expression radare2 can resolve via `s <expr>` |
|
||||
| `view` | The panel to raise after opening |
|
||||
| `size` | Region length / block size (integer, decimal or `0x` hex) |
|
||||
|
||||
`addr` accepts anything radare2 understands:
|
||||
|
||||
```
|
||||
addr=0x401000
|
||||
addr=sym.main
|
||||
addr=entry0
|
||||
addr=str.Hello
|
||||
```
|
||||
|
||||
`view` may be one of:
|
||||
|
||||
```
|
||||
disasm graph hex decompiler
|
||||
strings imports exports functions
|
||||
```
|
||||
|
||||
`disasm`, `graph`, `hex` and `decompiler` open/raise the corresponding memory
|
||||
widget; the other names raise the matching tool panel by name.
|
||||
|
||||
`size` sets the current block size (radare2 `b`), e.g. `size=64` or `size=0x100`.
|
||||
|
||||
### Analysis (mostly for `bytes` and raw/unknown binaries)
|
||||
|
||||
| Param | Examples |
|
||||
|----------|-----------------------------------------------------|
|
||||
| `arch` | `x86`, `arm`, `arm64`, `mips`, `riscv`, `ppc` |
|
||||
| `bits` | `16`, `32`, `64` |
|
||||
| `endian` | `little`, `big` |
|
||||
| `cpu` | `generic`, `cortex`, `avr`, `68000`, `v850` |
|
||||
| `os` | `linux`, `darwin`, `windows`, `ios`, `android` |
|
||||
| `base` | base/map address, e.g. `0`, `0x1000`, `0x401000` |
|
||||
|
||||
For `bytes` links Iaito builds an in-memory `malloc://` buffer, writes the
|
||||
provided bytes into it and configures the session accordingly:
|
||||
|
||||
| Query param | radare2 config |
|
||||
|-------------|----------------|
|
||||
| `arch` | `asm.arch` |
|
||||
| `bits` | `asm.bits` |
|
||||
| `endian` | `cfg.bigendian`|
|
||||
| `cpu` | `asm.cpu` |
|
||||
| `os` | `asm.os` |
|
||||
| `base` | `bin.baddr` |
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Open a file and jump to an address:
|
||||
|
||||
```
|
||||
iaito://file/tmp/a.out?addr=0x401000
|
||||
```
|
||||
|
||||
Open a project and show the graph view at `main`:
|
||||
|
||||
```
|
||||
iaito://project/r2?addr=sym.main&view=graph
|
||||
```
|
||||
|
||||
Open a sample by SHA256 and show the decompiler:
|
||||
|
||||
```
|
||||
iaito://sha256/<hash>?addr=sym.main&view=decompiler
|
||||
```
|
||||
|
||||
Open a sample by SHA256 and select a region in the hex view:
|
||||
|
||||
```
|
||||
iaito://sha256/<hash>?addr=0x401000&size=0x100&view=hex
|
||||
```
|
||||
|
||||
Disassemble raw x86-64 shellcode:
|
||||
|
||||
```
|
||||
iaito://bytes/554889e54883ec10?arch=x86&bits=64&view=disasm
|
||||
```
|
||||
|
||||
Disassemble little-endian ARM code:
|
||||
|
||||
```
|
||||
iaito://bytes/e92d4800e8bd8800?arch=arm&bits=32&endian=little
|
||||
```
|
||||
|
||||
Analyze a raw big-endian MIPS firmware blob mapped at a base address:
|
||||
|
||||
```
|
||||
iaito://bytes/<hex>?arch=mips&bits=32&endian=big&base=0x80000000
|
||||
```
|
||||
|
||||
Open bytes and jump to an offset in the hex view:
|
||||
|
||||
```
|
||||
iaito://bytes/<hex>?arch=x86&bits=64&addr=0x10&view=hex
|
||||
```
|
||||
|
||||
|
||||
SHA256 samples database
|
||||
------------------------
|
||||
|
||||
`sha256` links resolve a hash to a local file through Iaito's samples database,
|
||||
which maps the SHA256 of every binary it has seen to its path on disk. Files are
|
||||
registered automatically when opened and can be managed from the *Sha256* tab in
|
||||
the New File dialog. If the hash is unknown, Iaito shows a dialog explaining how
|
||||
to register it; downloading unknown samples is out of scope.
|
||||
|
||||
See [sha256.md](sha256.md) for the on-disk layout, hashing, how to populate the
|
||||
database, and the configurable online lookup URL.
|
||||
|
||||
|
||||
Registering the URI handler
|
||||
---------------------------
|
||||
|
||||
The `iaito://` scheme must be associated with the Iaito executable at the OS
|
||||
level. This happens automatically with the standard install targets, and can
|
||||
also be run on its own.
|
||||
|
||||
### Make targets
|
||||
|
||||
```
|
||||
make install-deeplink # register iaito:// for the current user
|
||||
make uninstall-deeplink # remove the association
|
||||
```
|
||||
|
||||
`make user-install` registers the handler as part of installation, and
|
||||
`make uninstall` / `make user-uninstall` removes it. A system-wide
|
||||
`make install` refreshes the desktop database so the `.desktop` MimeType
|
||||
association is picked up.
|
||||
|
||||
### Linux
|
||||
|
||||
Registration relies on the desktop entry `src/org.radare.iaito.desktop`, which
|
||||
declares:
|
||||
|
||||
```
|
||||
Exec=iaito %u
|
||||
MimeType=x-scheme-handler/iaito;
|
||||
```
|
||||
|
||||
`scripts/deeplink/register.sh` runs `xdg-mime default` and
|
||||
`update-desktop-database`; `scripts/deeplink/unregister.sh` removes the
|
||||
association from `~/.config/mimeapps.list`.
|
||||
|
||||
### macOS
|
||||
|
||||
The app bundle declares the scheme in `Info.plist` via `CFBundleURLTypes`
|
||||
(`CFBundleURLSchemes = iaito`). LaunchServices registers it automatically when
|
||||
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
|
||||
(including `URL Protocol` and `shell\open\command`) and
|
||||
`scripts/deeplink/unregister.bat` deletes them. Pass the path to `iaito.exe` as
|
||||
the first argument:
|
||||
|
||||
```
|
||||
scripts\deeplink\register.bat "C:\Program Files\iaito\iaito.exe"
|
||||
scripts\deeplink\unregister.bat
|
||||
```
|
||||
|
||||
|
||||
Using deep links from the UI
|
||||
----------------------------
|
||||
|
||||
Every code view and listing exposes a **Copy deep link** action that builds a
|
||||
link to the current address:
|
||||
|
||||
- Disassembly, decompiler and graph context menus (under *Copy* -> *Deep link*),
|
||||
embedding the matching `view=` value (`disasm`, `decompiler`, `graph`).
|
||||
- Hexdump context menu (*Copy* -> *Deep link*, `view=hex`).
|
||||
- The listing widgets (functions, symbols, imports, exports, strings, relocs,
|
||||
sections, ...) via the *Copy deep link* entry in their context menu.
|
||||
|
||||
When triggered, a dialog asks how the open file should be referenced: by loaded
|
||||
project name, by `sha256`, or by absolute `file` path. The resulting URI is
|
||||
placed on the clipboard, ready to paste into a report or a comment.
|
||||
|
||||
Links pasted into comments are rendered as clickable text in the disassembly and
|
||||
decompiler views: hovering shows a pointing-hand cursor and a left click follows
|
||||
the link. A link that points back into the file already open navigates in place
|
||||
(seek + view switch) instead of reopening the binary.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
- `addr` is resolved through radare2's `s` command, so it accepts the full range
|
||||
of radare2 address expressions (flags, symbols, math, etc.).
|
||||
- `size` is always resolved to a number before use, so it cannot inject
|
||||
additional radare2 commands.
|
||||
- Query parameters are preferred over `#fragments`; future parameters can be
|
||||
added without changing the URI format.
|
||||
- The four handlers (`file`, `project`, `sha256`, `bytes`) are intentionally the
|
||||
only top-level entry points.
|
||||
@@ -0,0 +1,94 @@
|
||||
Iaito SHA256 Samples Database
|
||||
=============================
|
||||
|
||||
Iaito keeps a local database that maps the SHA256 of every binary it has seen to
|
||||
its path on disk. It powers `iaito://sha256/<hash>` deep links (see
|
||||
[deeplinks.md](deeplinks.md)) and the *Sha256* tab in the New File dialog, and it
|
||||
lets you look a sample up online (VirusTotal by default).
|
||||
|
||||
The implementation lives in `src/common/SamplesDB.{h,cpp}`.
|
||||
|
||||
|
||||
On-disk layout
|
||||
--------------
|
||||
|
||||
The database lives under Iaito's application data directory
|
||||
(`QStandardPaths::AppDataLocation`, e.g. `~/.local/share/radareorg/iaito/samples/`
|
||||
on Linux).
|
||||
|
||||
To scale to large collections without a single huge index file, entries are
|
||||
sharded git/rainbow-table style across two hash-prefix directory levels. The
|
||||
leaf is a plain text bucket file holding one `"<sha256> <absolute-path>"` entry
|
||||
per line:
|
||||
|
||||
```
|
||||
samples/<h0:2>/<h2:4>/<h0:4>
|
||||
|
||||
samples/53/a9/53a9 -> 53a9c0557a948069...172161625 /samples/a.out
|
||||
53a9ffeede... /samples/b.bin
|
||||
```
|
||||
|
||||
- The two directory levels are the first two and next two hex characters of the
|
||||
hash, giving up to 65536 buckets so no single directory grows unbounded.
|
||||
- The leaf bucket file is named after the 4-character prefix and contains all
|
||||
entries sharing it, hash and absolute path separated by a single space.
|
||||
- Lookups read just one bucket file; the whole database is only walked when the
|
||||
*Sha256* tab lists every entry.
|
||||
|
||||
When the same hash is registered again with a different path, the latest path
|
||||
wins (one path per hash).
|
||||
|
||||
|
||||
Hashing
|
||||
-------
|
||||
|
||||
`SamplesDB::sha256File()` computes the digest with radare2's `r_muta` SHA256
|
||||
session API, reading the file in fixed-size blocks. Memory stays bounded
|
||||
regardless of file size, so even multi-gigabyte files hash safely without being
|
||||
loaded into memory at once.
|
||||
|
||||
|
||||
Populating the database
|
||||
-----------------------
|
||||
|
||||
- **Automatically** — every file you open in Iaito is registered (sha256 -> path)
|
||||
once it finishes loading, from `MainWindow::finalizeOpen()`. Hashing runs on a
|
||||
background thread so the UI stays responsive on large files. Live debug
|
||||
sessions and non-file targets (`malloc://`, `dbg://`, ...) are skipped, as are
|
||||
paths that no longer exist.
|
||||
- **Manually** — the *Sha256* tab in the New File dialog lets you:
|
||||
- **Scan folder…** — recursively register every file under a chosen directory
|
||||
(runs in the background).
|
||||
- **Filter** — the input field filters the list by file name or hash as you
|
||||
type.
|
||||
- **On disk** — a column shows whether each registered file still exists.
|
||||
- **Open** — open the selected sample (also on double-click).
|
||||
- **Lookup** — open the configured lookup URL for the selected hash.
|
||||
- **Delete** — remove the selected entry from the database (the file is left
|
||||
untouched).
|
||||
- **Right-click** — *Copy path*, *Copy hash*, *Open containing folder*,
|
||||
*Open in lookup URL*, *Remove from database*.
|
||||
|
||||
|
||||
Online lookup URL
|
||||
-----------------
|
||||
|
||||
The *Lookup* button and the *Open in lookup URL* context action open a base URL
|
||||
with the selected file's SHA256 appended. The base URL defaults to VirusTotal:
|
||||
|
||||
```
|
||||
https://www.virustotal.com/gui/file/<sha256>
|
||||
```
|
||||
|
||||
It is configurable in *Settings -> Samples* (stored as `sha256.lookupBaseUrl`),
|
||||
where a *Reset* button restores the VirusTotal default. This lets you point the
|
||||
lookup at any service that resolves a file by its SHA256.
|
||||
|
||||
|
||||
Deep link resolution
|
||||
---------------------
|
||||
|
||||
`iaito://sha256/<hash>` looks the hash up in the database and opens the mapped
|
||||
file if it still exists, applying any `addr`/`view`/`size` parameters. If the
|
||||
hash is unknown, Iaito shows a dialog explaining how to register it. Downloading
|
||||
unknown samples is intentionally out of scope.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
rem Register the iaito:// URI scheme handler for the current user.
|
||||
rem Usage: register.bat [path-to-iaito.exe]
|
||||
|
||||
setlocal
|
||||
set "IAITO=%~1"
|
||||
if "%IAITO%"=="" set "IAITO=%~dp0..\..\build\iaito.exe"
|
||||
|
||||
reg add "HKCU\Software\Classes\iaito" /ve /d "URL:iaito Protocol" /f
|
||||
reg add "HKCU\Software\Classes\iaito" /v "URL Protocol" /d "" /f
|
||||
reg add "HKCU\Software\Classes\iaito\DefaultIcon" /ve /d "\"%IAITO%\",0" /f
|
||||
reg add "HKCU\Software\Classes\iaito\shell\open\command" /ve /d "\"%IAITO%\" \"%%1\"" /f
|
||||
echo Registered iaito:// -^> %IAITO%
|
||||
endlocal
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Register the iaito:// URI scheme handler on Linux and macOS.
|
||||
# Usage: register.sh [path-to-iaito.app] (the argument is only used on macOS)
|
||||
|
||||
case "$(uname)" in
|
||||
Darwin)
|
||||
APP="$1"
|
||||
[ -d "$APP" ] || APP="/Applications/iaito.app"
|
||||
[ -d "$APP" ] || APP="$HOME/Applications/iaito.app"
|
||||
if [ ! -d "$APP" ]; then
|
||||
echo "Cannot find iaito.app, pass its path as first argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
LSREG="/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"
|
||||
"$LSREG" -f "$APP"
|
||||
echo "Registered iaito:// for $APP"
|
||||
;;
|
||||
*)
|
||||
DESKTOP="org.radare.iaito.desktop"
|
||||
if command -v xdg-mime >/dev/null 2>&1; then
|
||||
xdg-mime default "$DESKTOP" x-scheme-handler/iaito
|
||||
fi
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
|
||||
fi
|
||||
echo "Registered iaito:// -> $DESKTOP"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
rem Unregister the iaito:// URI scheme handler for the current user.
|
||||
reg delete "HKCU\Software\Classes\iaito" /f
|
||||
echo Unregistered iaito://
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# Unregister the iaito:// URI scheme handler on Linux and macOS.
|
||||
|
||||
case "$(uname)" in
|
||||
Darwin)
|
||||
echo "macOS drops the iaito:// handler when the iaito.app bundle is removed."
|
||||
;;
|
||||
*)
|
||||
MIMEAPPS="$HOME/.config/mimeapps.list"
|
||||
if [ -f "$MIMEAPPS" ]; then
|
||||
sed -i.bak '/x-scheme-handler\/iaito=/d' "$MIMEAPPS"
|
||||
rm -f "$MIMEAPPS.bak"
|
||||
fi
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
|
||||
fi
|
||||
echo "Unregistered iaito:// handler"
|
||||
;;
|
||||
esac
|
||||
+63
-12
@@ -3,7 +3,7 @@ TEMPLATE = app
|
||||
TARGET = iaito
|
||||
|
||||
IAITO_VERSION_MAJOR = 6
|
||||
IAITO_VERSION_MINOR = 1
|
||||
IAITO_VERSION_MINOR = 2
|
||||
IAITO_VERSION_PATCH = 6
|
||||
|
||||
CONFIG += 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 \
|
||||
@@ -183,10 +185,13 @@ SOURCES += \
|
||||
dialogs/NativeDebugDialog.cpp \
|
||||
dialogs/XrefsDialog.cpp \
|
||||
core/MainWindow.cpp \
|
||||
core/DockManager.cpp \
|
||||
core/QtDockBackend.cpp \
|
||||
common/Helpers.cpp \
|
||||
common/TextEditDialog.cpp \
|
||||
common/Highlighter.cpp \
|
||||
common/MdHighlighter.cpp \
|
||||
common/Markdown.cpp \
|
||||
common/DirectionalComboBox.cpp \
|
||||
common/TypeScriptHighlighter.cpp \
|
||||
common/AnalTask.cpp \
|
||||
@@ -201,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 \
|
||||
@@ -219,12 +225,15 @@ SOURCES += \
|
||||
widgets/DisassemblyWidget.cpp \
|
||||
widgets/HexdumpWidget.cpp \
|
||||
common/Configuration.cpp \
|
||||
common/theme/Theme.cpp \
|
||||
common/theme/IaitoStyle.cpp \
|
||||
common/Colors.cpp \
|
||||
dialogs/SaveProjectDialog.cpp \
|
||||
common/TempConfig.cpp \
|
||||
common/SvgIconEngine.cpp \
|
||||
common/SyntaxHighlighter.cpp \
|
||||
widgets/DecompilerWidget.cpp \
|
||||
widgets/DecompilerTextEdit.cpp \
|
||||
widgets/VisualNavbar.cpp \
|
||||
widgets/GraphView.cpp \
|
||||
dialogs/settings/SettingsDialog.cpp \
|
||||
@@ -238,8 +247,11 @@ SOURCES += \
|
||||
widgets/VTablesWidget.cpp \
|
||||
widgets/TypesWidget.cpp \
|
||||
widgets/HeadersWidget.cpp \
|
||||
widgets/InterfacesWidget.cpp \
|
||||
widgets/SearchWidget.cpp \
|
||||
IaitoApplication.cpp \
|
||||
common/DeepLink.cpp \
|
||||
common/SamplesDB.cpp \
|
||||
dialogs/R2PluginsDialog.cpp \
|
||||
widgets/IaitoDockWidget.cpp \
|
||||
widgets/IaitoTreeWidget.cpp \
|
||||
@@ -247,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 \
|
||||
@@ -270,6 +283,7 @@ SOURCES += \
|
||||
widgets/MemoryMapWidget.cpp \
|
||||
dialogs/settings/DebugOptionsWidget.cpp \
|
||||
dialogs/settings/WebserverOptionsWidget.cpp \
|
||||
dialogs/settings/SamplesOptionsWidget.cpp \
|
||||
dialogs/settings/PluginsOptionsWidget.cpp \
|
||||
widgets/BreakpointWidget.cpp \
|
||||
dialogs/BreakpointsDialog.cpp \
|
||||
@@ -298,12 +312,15 @@ SOURCES += \
|
||||
widgets/ColorThemeComboBox.cpp \
|
||||
widgets/ColorThemeListView.cpp \
|
||||
dialogs/settings/ColorThemeEditDialog.cpp \
|
||||
dialogs/settings/InterfaceThemeEditDialog.cpp \
|
||||
widgets/MemoryDockWidget.cpp \
|
||||
common/CrashHandler.cpp \
|
||||
common/BugReporting.cpp \
|
||||
common/HighDpiPixmap.cpp \
|
||||
widgets/GraphGridLayout.cpp \
|
||||
widgets/HexWidget.cpp \
|
||||
widgets/HexPatWidget.cpp \
|
||||
widgets/AddressScrollBar.cpp \
|
||||
widgets/R2AIWidget.cpp \
|
||||
widgets/R2McpWidget.cpp \
|
||||
common/SelectionHighlight.cpp \
|
||||
@@ -311,7 +328,9 @@ SOURCES += \
|
||||
common/R2GhidraCmdDecompiler.cpp \
|
||||
common/R2JadxDecompiler.cpp \
|
||||
common/ShortcutKeys.cpp \
|
||||
common/ShortcutManager.cpp \
|
||||
dialogs/ShortcutKeysDialog.cpp \
|
||||
common/R2SourceDecompiler.cpp \
|
||||
common/R2pdcCmdDecompiler.cpp \
|
||||
common/R2DecaiDecompiler.cpp \
|
||||
common/R2AiDecompiler.cpp \
|
||||
@@ -320,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 \
|
||||
@@ -336,18 +357,24 @@ SOURCES += \
|
||||
widgets/SyscallsWidget.cpp \
|
||||
widgets/AddressableDockWidget.cpp \
|
||||
dialogs/settings/AnalOptionsWidget.cpp \
|
||||
dialogs/settings/KeyboardOptionsWidget.cpp \
|
||||
dialogs/settings/ShortcutsOptionsWidget.cpp \
|
||||
common/DecompilerHighlighter.cpp \
|
||||
dialogs/PackageManagerDialog.cpp \
|
||||
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 \
|
||||
@@ -364,10 +391,12 @@ HEADERS += \
|
||||
common/TypeScriptHighlighter.h \
|
||||
common/Helpers.h \
|
||||
common/ShortcutKeys.h \
|
||||
common/ShortcutManager.h \
|
||||
dialogs/ShortcutKeysDialog.h \
|
||||
common/TextEditDialog.h \
|
||||
common/Highlighter.h \
|
||||
common/MdHighlighter.h \
|
||||
common/Markdown.h \
|
||||
common/DirectionalComboBox.h \
|
||||
common/AnalTask.h \
|
||||
dialogs/AboutDialog.h \
|
||||
@@ -380,6 +409,9 @@ HEADERS += \
|
||||
dialogs/NativeDebugDialog.h \
|
||||
dialogs/XrefsDialog.h \
|
||||
core/MainWindow.h \
|
||||
core/DockManager.h \
|
||||
core/DockBackend.h \
|
||||
core/QtDockBackend.h \
|
||||
dialogs/InitialOptionsDialog.h \
|
||||
dialogs/NewFileDialog.h \
|
||||
widgets/CommentsWidget.h \
|
||||
@@ -390,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 \
|
||||
@@ -403,17 +436,21 @@ HEADERS += \
|
||||
widgets/CodeInfoWidget.h \
|
||||
menus/DisassemblyContextMenu.h \
|
||||
menus/DecompilerContextMenu.h \
|
||||
menus/ContextMenuIcons.h \
|
||||
menus/ColorPickerMenu.h \
|
||||
menus/PinPickerMenu.h \
|
||||
widgets/DisassemblyWidget.h \
|
||||
widgets/HexdumpWidget.h \
|
||||
common/Configuration.h \
|
||||
common/theme/Theme.h \
|
||||
common/theme/IaitoStyle.h \
|
||||
common/Colors.h \
|
||||
dialogs/SaveProjectDialog.h \
|
||||
common/TempConfig.h \
|
||||
common/SvgIconEngine.h \
|
||||
common/SyntaxHighlighter.h \
|
||||
widgets/DecompilerWidget.h \
|
||||
widgets/DecompilerTextEdit.h \
|
||||
widgets/VisualNavbar.h \
|
||||
widgets/GraphView.h \
|
||||
dialogs/settings/SettingsDialog.h \
|
||||
@@ -425,9 +462,12 @@ HEADERS += \
|
||||
widgets/ClassesWidget.h \
|
||||
widgets/ResourcesWidget.h \
|
||||
IaitoApplication.h \
|
||||
common/DeepLink.h \
|
||||
common/SamplesDB.h \
|
||||
widgets/VTablesWidget.h \
|
||||
widgets/TypesWidget.h \
|
||||
widgets/HeadersWidget.h \
|
||||
widgets/InterfacesWidget.h \
|
||||
widgets/SearchWidget.h \
|
||||
dialogs/R2PluginsDialog.h \
|
||||
widgets/IaitoDockWidget.h \
|
||||
@@ -436,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 \
|
||||
@@ -463,6 +504,7 @@ HEADERS += \
|
||||
widgets/MemoryMapWidget.h \
|
||||
dialogs/settings/DebugOptionsWidget.h \
|
||||
dialogs/settings/WebserverOptionsWidget.h \
|
||||
dialogs/settings/SamplesOptionsWidget.h \
|
||||
dialogs/settings/PluginsOptionsWidget.h \
|
||||
widgets/BreakpointWidget.h \
|
||||
dialogs/BreakpointsDialog.h \
|
||||
@@ -494,13 +536,17 @@ HEADERS += \
|
||||
widgets/MemoryDockWidget.h \
|
||||
widgets/ColorThemeListView.h \
|
||||
dialogs/settings/ColorThemeEditDialog.h \
|
||||
dialogs/settings/KeyboardOptionsWidget.h \
|
||||
dialogs/settings/InterfaceThemeEditDialog.h \
|
||||
dialogs/settings/ShortcutsOptionsWidget.h \
|
||||
dialogs/LinkTypeDialog.h \
|
||||
common/BugReporting.h \
|
||||
common/HighDpiPixmap.h \
|
||||
widgets/GraphLayout.h \
|
||||
widgets/GraphGridLayout.h \
|
||||
widgets/HexWidget.h \
|
||||
widgets/HexPatWidget.h \
|
||||
common/SparseHexLayout.h \
|
||||
widgets/AddressScrollBar.h \
|
||||
widgets/R2AIWidget.h \
|
||||
widgets/R2McpWidget.h \
|
||||
common/SelectionHighlight.h \
|
||||
@@ -512,9 +558,12 @@ HEADERS += \
|
||||
common/R2AnotesDecompiler.h \
|
||||
common/R2HermesDecompiler.h \
|
||||
common/R2pdcCmdDecompiler.h \
|
||||
common/R2SourceDecompiler.h \
|
||||
common/R2retdecDecompiler.h \
|
||||
menus/AddressableItemContextMenu.h \
|
||||
common/AddressableItemModel.h \
|
||||
common/NameListModel.h \
|
||||
widgets/NameListView.h \
|
||||
widgets/ListDockWidget.h \
|
||||
widgets/AddressableItemList.h \
|
||||
dialogs/MultitypeFileSaveDialog.h \
|
||||
@@ -540,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 \
|
||||
@@ -568,12 +623,12 @@ FORMS += \
|
||||
widgets/FlagsWidget.ui \
|
||||
widgets/StringsWidget.ui \
|
||||
widgets/HexdumpWidget.ui \
|
||||
widgets/HexPatWidget.ui \
|
||||
dialogs/SaveProjectDialog.ui \
|
||||
dialogs/settings/SettingsDialog.ui \
|
||||
dialogs/settings/AppearanceOptionsWidget.ui \
|
||||
dialogs/settings/GraphOptionsWidget.ui \
|
||||
dialogs/settings/InitializationFileEditor.ui \
|
||||
dialogs/settings/KeyboardOptionsWidget.ui \
|
||||
widgets/QuickFilterView.ui \
|
||||
widgets/DecompilerWidget.ui \
|
||||
widgets/ClassesWidget.ui \
|
||||
@@ -593,6 +648,7 @@ FORMS += \
|
||||
dialogs/MapFileDialog.ui \
|
||||
dialogs/settings/DebugOptionsWidget.ui \
|
||||
dialogs/settings/WebserverOptionsWidget.ui \
|
||||
dialogs/settings/SamplesOptionsWidget.ui \
|
||||
widgets/BreakpointWidget.ui \
|
||||
dialogs/BreakpointsDialog.ui \
|
||||
dialogs/AttachProcDialog.ui \
|
||||
@@ -600,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 \
|
||||
@@ -617,11 +672,7 @@ FORMS += \
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc \
|
||||
themes/native/native.qrc \
|
||||
themes/qdarkstyle/dark.qrc \
|
||||
themes/midnight/midnight.qrc \
|
||||
themes/lightstyle/light.qrc \
|
||||
themes/classic/classic.qrc
|
||||
themes/engine/engine.qrc
|
||||
|
||||
|
||||
DISTFILES += Iaito.astylerc
|
||||
|
||||
+62
-26
@@ -6,11 +6,16 @@
|
||||
#include "R2GhidraCmdDecompiler.h"
|
||||
#include "R2HermesDecompiler.h"
|
||||
#include "R2JadxDecompiler.h"
|
||||
#include "R2SourceDecompiler.h"
|
||||
#include "R2pdcCmdDecompiler.h"
|
||||
#include "R2retdecDecompiler.h"
|
||||
#include "common/CrashHandler.h"
|
||||
#include "common/Decompiler.h"
|
||||
#include "common/DeepLink.h"
|
||||
#include "common/ResourcePaths.h"
|
||||
#include "common/SamplesDB.h"
|
||||
#include "common/ShortcutManager.h"
|
||||
#include "common/theme/IaitoStyle.h"
|
||||
#include "plugins/PluginManager.h"
|
||||
|
||||
#include <QApplication>
|
||||
@@ -34,6 +39,7 @@
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
#include <QTranslator>
|
||||
#include <QUrl>
|
||||
#ifdef Q_OS_WIN
|
||||
#include <QtNetwork/QtNetwork>
|
||||
#endif // Q_OS_WIN
|
||||
@@ -45,24 +51,6 @@
|
||||
#include <R2GhidraDecompiler.h>
|
||||
#endif
|
||||
|
||||
// Forces QComboBox to use the scrollable QListView popup (honors maxVisibleItems).
|
||||
class IaitoProxyStyle : public QProxyStyle
|
||||
{
|
||||
public:
|
||||
using QProxyStyle::QProxyStyle;
|
||||
int styleHint(
|
||||
StyleHint hint,
|
||||
const QStyleOption *option = nullptr,
|
||||
const QWidget *widget = nullptr,
|
||||
QStyleHintReturn *returnData = nullptr) const override
|
||||
{
|
||||
if (hint == SH_ComboBox_Popup) {
|
||||
return 0;
|
||||
}
|
||||
return QProxyStyle::styleHint(hint, option, widget, returnData);
|
||||
}
|
||||
};
|
||||
|
||||
static bool versionCheck()
|
||||
{
|
||||
// Check r2 version
|
||||
@@ -109,6 +97,7 @@ static void iaitoMessageHandler(QtMsgType type, const QMessageLogContext &ctx, c
|
||||
|
||||
IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
: QApplication(argc, argv)
|
||||
, m_FileAlreadyDropped(false)
|
||||
{
|
||||
// Install message handler before widgets load to filter Qt's libpng spam.
|
||||
g_previousMessageHandler = qInstallMessageHandler(iaitoMessageHandler);
|
||||
@@ -123,7 +112,7 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
#endif
|
||||
setLayoutDirection(Qt::LeftToRight);
|
||||
|
||||
setStyle(new IaitoProxyStyle(style()->objectName()));
|
||||
setStyle(new IaitoStyle());
|
||||
|
||||
// WARN!!! Put initialization code below this line. Code above this line is
|
||||
// mandatory to be run First
|
||||
@@ -183,8 +172,7 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
#endif
|
||||
#endif
|
||||
if (!parseCommandLineOptions()) {
|
||||
QCoreApplication::exit();
|
||||
// std::exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!versionCheck()) {
|
||||
@@ -220,6 +208,10 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
r2prefix.cd("Resources/radare2"); // Contents/Resources/radare2
|
||||
qputenv("R2_PREFIX", r2prefix.absolutePath().toLocal8Bit());
|
||||
|
||||
auto r2www = appdir; // Contents
|
||||
r2www.cd("Resources/radare2/share/radare2/last/www");
|
||||
qputenv("R2_WWWROOT", r2www.absolutePath().toLocal8Bit());
|
||||
|
||||
auto r2bin = appdir; // Contents
|
||||
r2bin.cd("Helpers"); // Contents/Helpers
|
||||
auto paths = QStringList(QString::fromLocal8Bit(qgetenv("PATH")));
|
||||
@@ -248,12 +240,16 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
Core()->setSettings();
|
||||
Config()->loadInitial();
|
||||
Core()->loadIaitoRC(0);
|
||||
ShortcutMgr()->migrateLegacyCommandKeys();
|
||||
|
||||
Config()->setOutputRedirectionEnabled(clOptions.outputRedirectionEnabled);
|
||||
|
||||
if (R2pdcCmdDecompiler::isAvailable()) {
|
||||
Core()->registerDecompiler(new R2pdcCmdDecompiler(Core()));
|
||||
}
|
||||
if (R2SourceDecompiler::isAvailable()) {
|
||||
Core()->registerDecompiler(new R2SourceDecompiler(Core()));
|
||||
}
|
||||
if (R2retdecDecompiler::isAvailable()) {
|
||||
Core()->registerDecompiler(new R2retdecDecompiler(Core()));
|
||||
}
|
||||
@@ -295,6 +291,8 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
RCore *kore = iaitoPluginCore();
|
||||
if (kore) {
|
||||
mainWindow->openCurrentCore(clOptions.fileOpenOptions, false);
|
||||
} else if (!clOptions.deepLinkUrl.isEmpty()) {
|
||||
DeepLink::handle(mainWindow, clOptions.deepLinkUrl);
|
||||
} else if (clOptions.args.empty()) {
|
||||
// check if this is the first execution of Iaito in this computer
|
||||
// Note: the execution after the settings been reset, will be
|
||||
@@ -339,8 +337,11 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
|
||||
|
||||
IaitoApplication::~IaitoApplication()
|
||||
{
|
||||
Plugins()->destroyPlugins();
|
||||
delete mainWindow;
|
||||
if (mainWindow) {
|
||||
SamplesDB::waitForPending();
|
||||
Plugins()->destroyPlugins();
|
||||
delete mainWindow;
|
||||
}
|
||||
}
|
||||
|
||||
void IaitoApplication::launchNewInstance(const QStringList &args)
|
||||
@@ -367,11 +368,16 @@ bool IaitoApplication::event(QEvent *e)
|
||||
}
|
||||
QFileOpenEvent *openEvent = static_cast<QFileOpenEvent *>(e);
|
||||
if (openEvent) {
|
||||
const QUrl url = openEvent->url();
|
||||
const bool isDeep = DeepLink::isDeepLink(url.toString());
|
||||
if (m_FileAlreadyDropped) {
|
||||
// We already dropped a file in macOS, let's spawn another
|
||||
// instance (Like the File -> Open)
|
||||
QString fileName = openEvent->file();
|
||||
launchNewInstance({fileName});
|
||||
launchNewInstance({isDeep ? url.toString() : openEvent->file()});
|
||||
} else if (isDeep) {
|
||||
m_FileAlreadyDropped = true;
|
||||
mainWindow->closeNewFileDialog();
|
||||
DeepLink::handle(mainWindow, url.toString());
|
||||
} else {
|
||||
QString fileName = openEvent->file();
|
||||
// eprintf ("FILE %s\n", fileName.toStdString().c_str());
|
||||
@@ -460,6 +466,10 @@ bool IaitoApplication::parseCommandLineOptions()
|
||||
cmd_parser.addVersionOption();
|
||||
cmd_parser.addPositionalArgument("filename", QObject::tr("Filename to open."));
|
||||
|
||||
QCommandLineOption environmentOption(
|
||||
"H", QObject::tr("List environment variables, or print the value of a selected variable."));
|
||||
cmd_parser.addOption(environmentOption);
|
||||
|
||||
QCommandLineOption analOption(
|
||||
{"A", "analysis"},
|
||||
QObject::tr(
|
||||
@@ -481,6 +491,10 @@ bool IaitoApplication::parseCommandLineOptions()
|
||||
QObject::tr("base address"));
|
||||
cmd_parser.addOption(baddrOption);
|
||||
|
||||
QCommandLineOption mapaddrOption(
|
||||
{"m", "map"}, QObject::tr("Map the file at a specific address"), QObject::tr("map address"));
|
||||
cmd_parser.addOption(mapaddrOption);
|
||||
|
||||
QCommandLineOption scriptOption("i", QObject::tr("Run script file"), QObject::tr("file"));
|
||||
cmd_parser.addOption(scriptOption);
|
||||
|
||||
@@ -522,11 +536,26 @@ 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();
|
||||
|
||||
// An iaito:// deep link is passed as the positional argument by the OS URI
|
||||
// handler. Pull it out so it is not treated as a regular filename.
|
||||
if (!opts.args.isEmpty() && DeepLink::isDeepLink(opts.args.first())) {
|
||||
opts.deepLinkUrl = opts.args.takeFirst();
|
||||
}
|
||||
|
||||
if (cmd_parser.isSet(analOption)) {
|
||||
bool analLevelSpecified = false;
|
||||
int analLevel = cmd_parser.value(analOption).toInt(&analLevelSpecified);
|
||||
@@ -576,6 +605,13 @@ bool IaitoApplication::parseCommandLineOptions()
|
||||
opts.fileOpenOptions.binLoadAddr = baddr;
|
||||
}
|
||||
}
|
||||
if (cmd_parser.isSet(mapaddrOption)) {
|
||||
bool ok;
|
||||
RVA mapaddr = cmd_parser.value(mapaddrOption).toULongLong(&ok, 0);
|
||||
if (ok) {
|
||||
opts.fileOpenOptions.mapAddr = mapaddr;
|
||||
}
|
||||
}
|
||||
switch (opts.analLevel) {
|
||||
case AutomaticAnalysisLevel::Ask:
|
||||
break;
|
||||
|
||||
@@ -18,6 +18,7 @@ struct IaitoCommandLineOptions
|
||||
bool enableIaitoPlugins = true;
|
||||
bool enableR2Plugins = true;
|
||||
bool showWelcomeDialog = false;
|
||||
QString deepLinkUrl;
|
||||
};
|
||||
|
||||
class IaitoApplication : public QApplication
|
||||
@@ -29,6 +30,7 @@ public:
|
||||
~IaitoApplication();
|
||||
|
||||
MainWindow *getMainWindow() { return mainWindow; }
|
||||
int getExitCode() const { return exitCode; }
|
||||
|
||||
void launchNewInstance(const QStringList &args = {});
|
||||
|
||||
@@ -50,7 +52,8 @@ private:
|
||||
|
||||
private:
|
||||
bool m_FileAlreadyDropped;
|
||||
MainWindow *mainWindow;
|
||||
MainWindow *mainWindow = nullptr;
|
||||
int exitCode = 1;
|
||||
IaitoCommandLineOptions clOptions;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,75 @@
|
||||
#include "core/MainWindow.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
static void connectToConsole();
|
||||
#endif
|
||||
|
||||
static bool printEnvironmentVariables(int argc, char *argv[])
|
||||
{
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!std::strcmp(argv[i], "--")) {
|
||||
return false;
|
||||
}
|
||||
if (std::strncmp(argv[i], "-H", 2)) {
|
||||
continue;
|
||||
}
|
||||
const char *selectedVariable = nullptr;
|
||||
if (argv[i][2]) {
|
||||
selectedVariable = argv[i] + 2;
|
||||
} else if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
selectedVariable = argv[i + 1];
|
||||
}
|
||||
#ifdef Q_OS_WIN
|
||||
connectToConsole();
|
||||
#endif
|
||||
QByteArray extraPluginDirs = IAITO_EXTRA_PLUGIN_DIRS;
|
||||
const QByteArray environmentPluginDirs = qgetenv("IAITO_EXTRA_PLUGIN_DIRS");
|
||||
if (!environmentPluginDirs.isEmpty()) {
|
||||
if (!extraPluginDirs.isEmpty()) {
|
||||
extraPluginDirs.append(QDir::listSeparator().toLatin1());
|
||||
}
|
||||
extraPluginDirs.append(environmentPluginDirs);
|
||||
}
|
||||
struct EnvironmentVariable
|
||||
{
|
||||
const char *name;
|
||||
QByteArray value;
|
||||
};
|
||||
const EnvironmentVariable variables[] = {
|
||||
{"IAITO_VERSION", IAITO_VERSION_FULL},
|
||||
{"IAITO_VERSION_MAJOR", QByteArray::number(IAITO_VERSION_MAJOR)},
|
||||
{"IAITO_VERSION_MINOR", QByteArray::number(IAITO_VERSION_MINOR)},
|
||||
{"IAITO_VERSION_PATCH", QByteArray::number(IAITO_VERSION_PATCH)},
|
||||
{"IAITO_EXTRA_PLUGIN_DIRS", extraPluginDirs},
|
||||
{"R2_DEBUG", qgetenv("R2_DEBUG")},
|
||||
{"R2_NOPLUGINS", qgetenv("R2_NOPLUGINS")},
|
||||
};
|
||||
for (const auto &variable : variables) {
|
||||
const char *shortName = std::strchr(variable.name, '_');
|
||||
shortName = shortName ? shortName + 1 : variable.name;
|
||||
if (selectedVariable && std::strcmp(selectedVariable, variable.name)
|
||||
&& std::strcmp(selectedVariable, shortName)) {
|
||||
continue;
|
||||
}
|
||||
if (selectedVariable) {
|
||||
std::fprintf(stdout, "%s\n", variable.value.constData());
|
||||
} else {
|
||||
std::fprintf(stdout, "%s=%s\n", variable.name, variable.value.constData());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attempt to connect to a parent console and configure outputs.
|
||||
*
|
||||
@@ -52,6 +118,10 @@ static void connectToConsole()
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (printEnvironmentVariables(argc, argv)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (argc >= 3 && QString::fromLocal8Bit(argv[1]) == "--start-crash-handler") {
|
||||
QApplication app(argc, argv);
|
||||
QString dumpLocation = QString::fromLocal8Bit(argv[2]);
|
||||
@@ -84,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
|
||||
@@ -24,13 +24,28 @@ static QJsonArray variantToRgba(const QVariant &v)
|
||||
} // namespace
|
||||
|
||||
const QStringList ColorThemeWorker::iaitoSpecificOptions
|
||||
= {"wordHighlight", "lineHighlight", "gui.main",
|
||||
"gui.imports", "highlightPC", "gui.navbar.err",
|
||||
"gui.navbar.seek", "gui.navbar.pc", "gui.navbar.sym",
|
||||
"gui.dataoffset", "gui.navbar.code", "gui.navbar.empty",
|
||||
"gui.navbar.str", "gui.disass_selected", "gui.breakpoint_background",
|
||||
"gui.overview.node", "gui.overview.fill", "gui.overview.border",
|
||||
"gui.border", "gui.background", "gui.alt_background",
|
||||
= {"wordHighlight",
|
||||
"lineHighlight",
|
||||
"gui.main",
|
||||
"gui.imports",
|
||||
"highlightPC",
|
||||
"gui.navbar.err",
|
||||
"gui.navbar.seek",
|
||||
"gui.navbar.pc",
|
||||
"gui.navbar.sym",
|
||||
"gui.dataoffset",
|
||||
"gui.navbar.code",
|
||||
"gui.navbar.data",
|
||||
"gui.navbar.empty",
|
||||
"gui.navbar.str",
|
||||
"gui.disass_selected",
|
||||
"gui.breakpoint_background",
|
||||
"gui.overview.node",
|
||||
"gui.overview.fill",
|
||||
"gui.overview.border",
|
||||
"gui.border",
|
||||
"gui.background",
|
||||
"gui.alt_background",
|
||||
"gui.disass_selected"};
|
||||
|
||||
const QStringList ColorThemeWorker::radare2UnusedOptions
|
||||
|
||||
+283
-195
@@ -4,10 +4,15 @@
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFontDatabase>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QLineEdit>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSet>
|
||||
#include <QStandardPaths>
|
||||
#include <QStyle>
|
||||
#include <QTextEdit>
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
@@ -23,6 +28,8 @@
|
||||
#include "common/ColorThemeWorker.h"
|
||||
#include "common/ResourcePaths.h"
|
||||
#include "common/SyntaxHighlighter.h"
|
||||
#include "common/theme/IaitoStyle.h"
|
||||
#include "common/theme/Theme.h"
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QVariant>
|
||||
@@ -39,9 +46,12 @@ static inline int variantTypeId(const QVariant &v)
|
||||
|
||||
constexpr int VisualNavbarThicknessDefault = 15;
|
||||
constexpr int VisualNavbarThicknessMin = 8;
|
||||
constexpr int VisualNavbarThicknessMax = 64;
|
||||
constexpr bool VisualNavbarUseThemeColorsDefault = false;
|
||||
constexpr int VisualNavbarThicknessMax = 128;
|
||||
constexpr int VisualNavbarResolutionMax = 32;
|
||||
constexpr const char *AddressRangeSelectionSyncKey = "syncAddressRangeSelection";
|
||||
constexpr const char *DisassemblyJumpLineAdaptiveSpacingKey = "disassemblyJumpLineAdaptiveSpacing";
|
||||
constexpr const char *DisassemblyJumpLineSpacingKey = "disassemblyJumpLineSpacing";
|
||||
constexpr int DisassemblyJumpLineSpacingDefault = 20;
|
||||
} // namespace
|
||||
|
||||
/* Map with names of themes associated with its color palette
|
||||
@@ -99,6 +109,8 @@ const QHash<QString, QHash<ColorFlags, QColor>> Configuration::iaitoOptionColors
|
||||
{{DarkFlag, QColor(0x42, 0xee, 0xf4)}, {LightFlag, QColor(0x42, 0xee, 0xf4)}}},
|
||||
{"gui.navbar.code",
|
||||
{{DarkFlag, QColor(0x82, 0xa8, 0x6f)}, {LightFlag, QColor(0x68, 0xc5, 0x45)}}},
|
||||
{"gui.navbar.data",
|
||||
{{DarkFlag, QColor(0xa0, 0x94, 0x68)}, {LightFlag, QColor(0xb0, 0x87, 0x2f)}}},
|
||||
{"gui.navbar.str",
|
||||
{{DarkFlag, QColor(0x6f, 0x86, 0xd8)}, {LightFlag, QColor(0x45, 0x68, 0xe5)}}},
|
||||
{"gui.navbar.sym",
|
||||
@@ -173,6 +185,7 @@ static const QHash<QString, QVariant> asmOptions
|
||||
{"graph.addr", false},
|
||||
{"asm.addr", true},
|
||||
{"asm.addr.relto", ""},
|
||||
{"asm.addr.base", "16"},
|
||||
{"asm.flags.offset", false}};
|
||||
|
||||
Configuration::Configuration()
|
||||
@@ -294,6 +307,16 @@ void Configuration::setRecentFolder(const QString &dir)
|
||||
s.setValue("dir.recentFolder", QDir::toNativeSeparators(dir));
|
||||
}
|
||||
|
||||
QString Configuration::getSha256LookupBaseUrl() const
|
||||
{
|
||||
return s.value("sha256.lookupBaseUrl", "https://www.virustotal.com/gui/file/").toString();
|
||||
}
|
||||
|
||||
void Configuration::setSha256LookupBaseUrl(const QString &url)
|
||||
{
|
||||
s.setValue("sha256.lookupBaseUrl", url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Configuration::setFilesTabLastClicked
|
||||
* Set the new file dialog last clicked tab
|
||||
@@ -409,198 +432,149 @@ static void applyFontEverywhere(const QFont &font)
|
||||
}
|
||||
}
|
||||
|
||||
static void setDarkPalette(
|
||||
QPalette &palette,
|
||||
const QColor &window,
|
||||
const QColor &base,
|
||||
const QColor &alternateBase,
|
||||
const QColor &highlight)
|
||||
Theme Configuration::nativeEngineTheme()
|
||||
{
|
||||
const QColor text(0xef, 0xf0, 0xf1);
|
||||
palette.setColor(QPalette::Window, window);
|
||||
palette.setColor(QPalette::WindowText, text);
|
||||
palette.setColor(QPalette::Base, base);
|
||||
palette.setColor(QPalette::AlternateBase, alternateBase);
|
||||
palette.setColor(QPalette::Text, text);
|
||||
palette.setColor(QPalette::Button, window);
|
||||
palette.setColor(QPalette::ButtonText, text);
|
||||
palette.setColor(QPalette::Highlight, highlight);
|
||||
palette.setColor(QPalette::HighlightedText, text);
|
||||
palette.setColor(QPalette::ToolTipBase, QColor(0x5a, 0x75, 0x66));
|
||||
palette.setColor(QPalette::ToolTipText, Qt::white);
|
||||
palette.setColor(QPalette::Disabled, QPalette::Text, QColor(0x45, 0x45, 0x45));
|
||||
palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(0x45, 0x45, 0x45));
|
||||
return Theme::fromPalette(nativePalette, nativeWindowIsDark());
|
||||
}
|
||||
|
||||
void Configuration::loadNativeStylesheet()
|
||||
void Configuration::loadEngineTheme(const Theme &t)
|
||||
{
|
||||
/* Load Qt Theme */
|
||||
QFile f(":native/native.qss");
|
||||
if (!f.exists()) {
|
||||
qWarning() << "Can't find Native theme stylesheet.";
|
||||
} else if (f.open(QFile::ReadOnly | QFile::Text)) {
|
||||
QTextStream ts(&f);
|
||||
QString stylesheet = ts.readAll();
|
||||
qApp->setStyleSheet(stylesheet);
|
||||
qApp->setStyleSheet(QString());
|
||||
m_interfaceChrome = t.toChrome();
|
||||
const QPalette p = t.toPalette(nativePalette);
|
||||
qApp->setPalette(p);
|
||||
applyFontEverywhere(t.fonts.custom ? t.fonts.base : defaultAppFont);
|
||||
for (QWidget *w : qApp->allWidgets()) {
|
||||
w->setPalette(p);
|
||||
}
|
||||
|
||||
applyFontEverywhere(defaultAppFont);
|
||||
qApp->setPalette(nativePalette);
|
||||
/* Some widgets does not change its palette when QApplication changes one
|
||||
* so this loop force all widgets do this, but all widgets take palette from
|
||||
* QApplication::palette() when they are created so line above is necessary
|
||||
* too.
|
||||
*/
|
||||
for (auto widget : qApp->allWidgets()) {
|
||||
widget->setPalette(nativePalette);
|
||||
if (IaitoStyle::instance()) {
|
||||
IaitoStyle::instance()->setTheme(t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Loads the Light theme of Iaito and modify special theme colors
|
||||
*/
|
||||
void Configuration::loadLightStylesheet()
|
||||
QString Configuration::userThemesDir()
|
||||
{
|
||||
/* Load Qt Theme */
|
||||
QFile f(":lightstyle/light.qss");
|
||||
if (!f.exists()) {
|
||||
qWarning() << "Can't find Light theme stylesheet.";
|
||||
} else if (f.open(QFile::ReadOnly | QFile::Text)) {
|
||||
QTextStream ts(&f);
|
||||
QString stylesheet = ts.readAll();
|
||||
|
||||
QPalette p = qApp->palette();
|
||||
p.setColor(QPalette::Text, Qt::black);
|
||||
qApp->setPalette(p);
|
||||
|
||||
applyFontEverywhere(defaultAppFont);
|
||||
qApp->setStyleSheet(stylesheet);
|
||||
}
|
||||
const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
||||
+ QDir::separator() + QStringLiteral("themes");
|
||||
QDir().mkpath(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
void Configuration::loadDarkStylesheet()
|
||||
bool Configuration::isCustomInterfaceTheme(const QString &name)
|
||||
{
|
||||
/* Load Qt Theme */
|
||||
QFile f(":qdarkstyle/style.qss");
|
||||
if (!f.exists()) {
|
||||
qWarning() << "Can't find Dark theme stylesheet.";
|
||||
} else if (f.open(QFile::ReadOnly | QFile::Text)) {
|
||||
QTextStream ts(&f);
|
||||
QString stylesheet = ts.readAll();
|
||||
#ifdef Q_OS_MACOS
|
||||
// see
|
||||
// https://github.com/ColinDuquesnoy/QDarkStyleSheet/issues/22#issuecomment-96179529
|
||||
stylesheet += "QDockWidget::title"
|
||||
"{"
|
||||
" background-color: #31363b;"
|
||||
" text-align: center;"
|
||||
" height: 12px;"
|
||||
"}";
|
||||
#endif
|
||||
QPalette p = qApp->palette();
|
||||
setDarkPalette(
|
||||
p,
|
||||
QColor(0x31, 0x36, 0x3b),
|
||||
QColor(0x23, 0x26, 0x29),
|
||||
QColor(0x31, 0x36, 0x3b),
|
||||
QColor(0x3d, 0xae, 0xe9));
|
||||
qApp->setPalette(p);
|
||||
applyFontEverywhere(defaultAppFont);
|
||||
qApp->setStyleSheet(stylesheet);
|
||||
}
|
||||
return QFileInfo::exists(QDir(userThemesDir()).filePath(name + QStringLiteral(".theme")));
|
||||
}
|
||||
|
||||
void Configuration::loadMidnightStylesheet()
|
||||
bool Configuration::isValidThemeName(const QString &name)
|
||||
{
|
||||
/* Load Qt Theme */
|
||||
QFile f(":midnight/style.css");
|
||||
if (!f.exists()) {
|
||||
qWarning() << "Can't find Midnight theme stylesheet.";
|
||||
} else if (f.open(QFile::ReadOnly | QFile::Text)) {
|
||||
QTextStream ts(&f);
|
||||
QString stylesheet = ts.readAll();
|
||||
|
||||
QPalette p = qApp->palette();
|
||||
setDarkPalette(
|
||||
p,
|
||||
QColor(0x1f, 0x20, 0x22),
|
||||
QColor(0x23, 0x26, 0x29),
|
||||
QColor(0x1f, 0x20, 0x22),
|
||||
QColor(0x36, 0x39, 0x3c));
|
||||
qApp->setPalette(p);
|
||||
|
||||
qApp->setFont(defaultAppFont);
|
||||
qApp->setStyleSheet(stylesheet);
|
||||
}
|
||||
return r_name_check(name.toUtf8().constData());
|
||||
}
|
||||
|
||||
void Configuration::loadClassicStylesheet()
|
||||
const QStringList &Configuration::interfaceThemeVariableKeys()
|
||||
{
|
||||
QFile f(":classic/classic.qss");
|
||||
if (!f.exists()) {
|
||||
qWarning() << "Can't find Classic theme stylesheet.";
|
||||
} else if (f.open(QFile::ReadOnly | QFile::Text)) {
|
||||
QTextStream ts(&f);
|
||||
QString stylesheet = ts.readAll();
|
||||
static const QStringList keys
|
||||
= {"palette/background",
|
||||
"palette/panel",
|
||||
"palette/surface",
|
||||
"palette/border",
|
||||
"palette/text",
|
||||
"palette/mutedText",
|
||||
"palette/accent",
|
||||
"palette/accentText"};
|
||||
return keys;
|
||||
}
|
||||
|
||||
QPalette p = qApp->palette();
|
||||
p.setColor(QPalette::Window, QColor(0xc0, 0xc0, 0xc0));
|
||||
p.setColor(QPalette::WindowText, Qt::black);
|
||||
p.setColor(QPalette::Base, Qt::white);
|
||||
p.setColor(QPalette::AlternateBase, QColor(0xe0, 0xe0, 0xe0));
|
||||
p.setColor(QPalette::Text, Qt::black);
|
||||
p.setColor(QPalette::Button, QColor(0xc0, 0xc0, 0xc0));
|
||||
p.setColor(QPalette::ButtonText, Qt::black);
|
||||
p.setColor(QPalette::Highlight, QColor(0x00, 0x00, 0x80));
|
||||
p.setColor(QPalette::HighlightedText, Qt::white);
|
||||
p.setColor(QPalette::ToolTipBase, QColor(0xff, 0xff, 0xe1));
|
||||
p.setColor(QPalette::ToolTipText, Qt::black);
|
||||
p.setColor(QPalette::Disabled, QPalette::Text, QColor(0x80, 0x80, 0x80));
|
||||
p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(0x80, 0x80, 0x80));
|
||||
qApp->setPalette(p);
|
||||
QHash<QString, QColor> Configuration::defaultInterfaceThemeVariables()
|
||||
{
|
||||
QWidget base;
|
||||
QLineEdit edit;
|
||||
QPushButton button;
|
||||
base.ensurePolished();
|
||||
edit.ensurePolished();
|
||||
button.ensurePolished();
|
||||
const QPalette &bp = base.palette();
|
||||
return {
|
||||
{"palette/background", bp.color(QPalette::Window)},
|
||||
{"palette/panel", edit.palette().color(QPalette::Base)},
|
||||
{"palette/surface", button.palette().color(QPalette::Button)},
|
||||
{"palette/border", bp.color(QPalette::Midlight)},
|
||||
{"palette/text", bp.color(QPalette::WindowText)},
|
||||
{"palette/mutedText", bp.color(QPalette::Disabled, QPalette::WindowText)},
|
||||
{"palette/accent", bp.color(QPalette::Highlight)},
|
||||
{"palette/accentText", bp.color(QPalette::HighlightedText)}};
|
||||
}
|
||||
|
||||
QFont classicFont(QStringLiteral("Windows"));
|
||||
classicFont.setPixelSize(16);
|
||||
classicFont.setStyleStrategy(QFont::PreferAntialias);
|
||||
classicFont.setHintingPreference(QFont::PreferFullHinting);
|
||||
qApp->setFont(classicFont);
|
||||
for (const char *cls : {"QWidget", "QMenu", "QMenuBar",
|
||||
"QPushButton", "QToolButton", "QHeaderView",
|
||||
"QAbstractItemView", "QListView", "QTreeView",
|
||||
"QTableView", "QComboBox", "QLineEdit",
|
||||
"QTextEdit", "QPlainTextEdit", "QLabel",
|
||||
"QGroupBox", "QTabBar", "QTabWidget",
|
||||
"QStatusBar", "QToolTip", "QCheckBox",
|
||||
"QRadioButton", "QSpinBox", "QDoubleSpinBox",
|
||||
"QAbstractSpinBox", "QDockWidget", "QToolBar",
|
||||
"QDialog", "QMainWindow"}) {
|
||||
qApp->setFont(classicFont, cls);
|
||||
QHash<QString, QColor> Configuration::loadInterfaceThemeVariables(const QString &name)
|
||||
{
|
||||
QHash<QString, QColor> vars = defaultInterfaceThemeVariables();
|
||||
QSettings
|
||||
theme(QDir(userThemesDir()).filePath(name + QStringLiteral(".theme")), QSettings::IniFormat);
|
||||
for (const QString &key : interfaceThemeVariableKeys()) {
|
||||
const QColor color(theme.value(key).toString());
|
||||
if (color.isValid()) {
|
||||
vars[key] = color;
|
||||
}
|
||||
|
||||
qApp->setStyleSheet(stylesheet);
|
||||
|
||||
for (auto widget : qApp->allWidgets()) {
|
||||
if (qobject_cast<QPlainTextEdit *>(widget) || qobject_cast<QTextEdit *>(widget)) {
|
||||
continue;
|
||||
}
|
||||
widget->setFont(classicFont);
|
||||
}
|
||||
emit fontsUpdated();
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
void Configuration::saveInterfaceThemeVariables(
|
||||
const QString &name, const QHash<QString, QColor> &vars, bool dark)
|
||||
{
|
||||
QSettings
|
||||
theme(QDir(userThemesDir()).filePath(name + QStringLiteral(".theme")), QSettings::IniFormat);
|
||||
for (const QString &key : interfaceThemeVariableKeys()) {
|
||||
const QColor c = vars.value(key);
|
||||
theme.setValue(key, c.alpha() == 255 ? c.name(QColor::HexRgb) : c.name(QColor::HexArgb));
|
||||
}
|
||||
theme.setValue(QStringLiteral("meta/dark"), dark);
|
||||
theme.setValue(QStringLiteral("meta/name"), name);
|
||||
}
|
||||
|
||||
void Configuration::applyInterfaceVariables(const QHash<QString, QColor> &vars, bool dark)
|
||||
{
|
||||
loadEngineTheme(Theme::fromChrome(vars, dark));
|
||||
}
|
||||
|
||||
bool Configuration::interfaceThemeIsDark(const QString &name)
|
||||
{
|
||||
if (isCustomInterfaceTheme(name)) {
|
||||
QSettings
|
||||
s(QDir(userThemesDir()).filePath(name + QStringLiteral(".theme")), QSettings::IniFormat);
|
||||
if (s.contains(QStringLiteral("meta/dark"))) {
|
||||
return s.value(QStringLiteral("meta/dark")).toBool();
|
||||
}
|
||||
}
|
||||
const auto &list = iaitoInterfaceThemesList();
|
||||
for (const IaitoInterfaceTheme &t : list) {
|
||||
if (t.name == name) {
|
||||
return t.flag == DarkFlag;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Configuration::loadUserStylesheet(const QString &name)
|
||||
{
|
||||
if (!isCustomInterfaceTheme(name)) {
|
||||
return false;
|
||||
}
|
||||
const QString path = QDir(userThemesDir()).filePath(name + QStringLiteral(".theme"));
|
||||
Theme t = Theme::fromChrome(loadInterfaceThemeVariables(name), interfaceThemeIsDark(name));
|
||||
t.applyIni(path);
|
||||
if (t.name.isEmpty()) {
|
||||
t.name = name;
|
||||
}
|
||||
loadEngineTheme(t);
|
||||
return true;
|
||||
}
|
||||
|
||||
const QFont Configuration::getBaseFont() const
|
||||
{
|
||||
int themeIdx = s.value("ColorPalette", 0).toInt();
|
||||
const auto &themes = iaitoInterfaceThemesList();
|
||||
if (themeIdx >= 0 && themeIdx < themes.size() && themes[themeIdx].name == "Classic") {
|
||||
QFont font(QStringLiteral("Windows"));
|
||||
font.setPixelSize(16);
|
||||
font.setStyleStrategy(QFont::PreferAntialias);
|
||||
return font;
|
||||
if (IaitoStyle::instance() && IaitoStyle::instance()->theme().fonts.custom) {
|
||||
return IaitoStyle::instance()->theme().fonts.base;
|
||||
}
|
||||
QFont font = s.value("font", QFont("IBM Plex Mono", 13)).value<QFont>();
|
||||
return font;
|
||||
return s.value("font", QFont("IBM Plex Mono", 13)).value<QFont>();
|
||||
}
|
||||
|
||||
const QFont Configuration::getFont() const
|
||||
@@ -615,15 +589,13 @@ const QFont Configuration::getFont() const
|
||||
return font;
|
||||
}
|
||||
|
||||
const QFont Configuration::getSmallFont() const
|
||||
const QFont Configuration::getSmallFont(bool zoomed) const
|
||||
{
|
||||
int themeIdx = s.value("ColorPalette", 0).toInt();
|
||||
const auto &themes = iaitoInterfaceThemesList();
|
||||
if (themeIdx >= 0 && themeIdx < themes.size() && themes[themeIdx].name == "Classic") {
|
||||
return getFont();
|
||||
if (IaitoStyle::instance() && IaitoStyle::instance()->theme().fonts.custom) {
|
||||
return zoomed ? getFont() : getBaseFont();
|
||||
}
|
||||
QFont font = getBaseFont();
|
||||
qreal eff = getZoomFactor() * 0.85;
|
||||
qreal eff = (zoomed ? getZoomFactor() : 1.0) * 0.85;
|
||||
if (font.pixelSize() > 0) {
|
||||
font.setPixelSize(qMax(int(font.pixelSize() * eff), 8));
|
||||
} else {
|
||||
@@ -697,19 +669,43 @@ void Configuration::setVisualNavbarThickness(int thickness)
|
||||
emit visualNavbarThicknessChanged(thickness);
|
||||
}
|
||||
|
||||
bool Configuration::getVisualNavbarUseThemeColors() const
|
||||
int Configuration::getVisualNavbarResolution() const
|
||||
{
|
||||
return s.value("visualNavbarUseThemeColors", VisualNavbarUseThemeColorsDefault).toBool();
|
||||
return qBound(1, s.value("visualNavbarResolution", 1).toInt(), VisualNavbarResolutionMax);
|
||||
}
|
||||
|
||||
void Configuration::setVisualNavbarUseThemeColors(bool enabled)
|
||||
void Configuration::setVisualNavbarResolution(int divisor)
|
||||
{
|
||||
if (getVisualNavbarUseThemeColors() == enabled) {
|
||||
divisor = qBound(1, divisor, VisualNavbarResolutionMax);
|
||||
if (getVisualNavbarResolution() == divisor) {
|
||||
return;
|
||||
}
|
||||
s.setValue("visualNavbarResolution", divisor);
|
||||
emit visualNavbarResolutionChanged(divisor);
|
||||
}
|
||||
|
||||
s.setValue("visualNavbarUseThemeColors", enabled);
|
||||
emit colorsUpdated();
|
||||
Configuration::MemoryScrollBarMode Configuration::getMemoryScrollBarMode() const
|
||||
{
|
||||
const int mode
|
||||
= s.value("memoryScrollBarMode", static_cast<int>(MemoryScrollBarMode::Bounded)).toInt();
|
||||
switch (mode) {
|
||||
case static_cast<int>(MemoryScrollBarMode::Spring):
|
||||
return MemoryScrollBarMode::Spring;
|
||||
case static_cast<int>(MemoryScrollBarMode::Hidden):
|
||||
return MemoryScrollBarMode::Hidden;
|
||||
case static_cast<int>(MemoryScrollBarMode::Bounded):
|
||||
default:
|
||||
return MemoryScrollBarMode::Bounded;
|
||||
}
|
||||
}
|
||||
|
||||
void Configuration::setMemoryScrollBarMode(MemoryScrollBarMode mode)
|
||||
{
|
||||
if (getMemoryScrollBarMode() == mode) {
|
||||
return;
|
||||
}
|
||||
s.setValue("memoryScrollBarMode", static_cast<int>(mode));
|
||||
emit memoryScrollBarModeChanged(mode);
|
||||
}
|
||||
|
||||
QString Configuration::getLastThemeOf(const IaitoInterfaceTheme &currInterfaceTheme) const
|
||||
@@ -727,18 +723,24 @@ void Configuration::setInterfaceTheme(int theme)
|
||||
IaitoInterfaceTheme interfaceTheme = iaitoInterfaceThemesList()[theme];
|
||||
|
||||
applyingTheme = true;
|
||||
static const QSet<QString> builtinEngineThemes
|
||||
= {"Dark",
|
||||
"Midnight",
|
||||
"Light",
|
||||
"Classic",
|
||||
"Pro",
|
||||
"Pro Dark",
|
||||
"Nord",
|
||||
"High Contrast",
|
||||
"Cobalt",
|
||||
"Capsule",
|
||||
"Phosphor"};
|
||||
if (interfaceTheme.name == "Native") {
|
||||
loadNativeStylesheet();
|
||||
} else if (interfaceTheme.name == "Dark") {
|
||||
loadDarkStylesheet();
|
||||
} else if (interfaceTheme.name == "Midnight") {
|
||||
loadMidnightStylesheet();
|
||||
} else if (interfaceTheme.name == "Light") {
|
||||
loadLightStylesheet();
|
||||
} else if (interfaceTheme.name == "Classic") {
|
||||
loadClassicStylesheet();
|
||||
} else {
|
||||
loadNativeStylesheet();
|
||||
loadEngineTheme(nativeEngineTheme());
|
||||
} else if (builtinEngineThemes.contains(interfaceTheme.name)) {
|
||||
loadEngineTheme(Theme::builtin(interfaceTheme.name));
|
||||
} else if (!loadUserStylesheet(interfaceTheme.name)) {
|
||||
loadEngineTheme(nativeEngineTheme());
|
||||
}
|
||||
|
||||
for (auto it = iaitoOptionColors.cbegin(); it != iaitoOptionColors.cend(); it++) {
|
||||
@@ -837,6 +839,9 @@ void Configuration::setLastThemeOf(
|
||||
|
||||
const QColor Configuration::getColor(const QString &name) const
|
||||
{
|
||||
if (name.contains(QLatin1Char('/'))) {
|
||||
return m_interfaceChrome.value(name);
|
||||
}
|
||||
if (s.contains("colors." + name)) {
|
||||
return s.value("colors." + name).value<QColor>();
|
||||
} else {
|
||||
@@ -916,6 +921,30 @@ void Configuration::resetToDefaultAsmOptions()
|
||||
for (auto it = asmOptions.cbegin(); it != asmOptions.cend(); it++) {
|
||||
setConfig(it.key(), it.value());
|
||||
}
|
||||
setDisassemblyJumpLineAdaptiveSpacingEnabled(true);
|
||||
setDisassemblyJumpLineSpacing(DisassemblyJumpLineSpacingDefault);
|
||||
}
|
||||
|
||||
bool Configuration::getDisassemblyJumpLineAdaptiveSpacingEnabled() const
|
||||
{
|
||||
return s.value(DisassemblyJumpLineAdaptiveSpacingKey, true).toBool();
|
||||
}
|
||||
|
||||
void Configuration::setDisassemblyJumpLineAdaptiveSpacingEnabled(bool enabled)
|
||||
{
|
||||
s.setValue(DisassemblyJumpLineAdaptiveSpacingKey, enabled);
|
||||
}
|
||||
|
||||
int Configuration::getDisassemblyJumpLineSpacing() const
|
||||
{
|
||||
const int spacing
|
||||
= s.value(DisassemblyJumpLineSpacingKey, DisassemblyJumpLineSpacingDefault).toInt();
|
||||
return spacing > 0 ? spacing : DisassemblyJumpLineSpacingDefault;
|
||||
}
|
||||
|
||||
void Configuration::setDisassemblyJumpLineSpacing(int spacing)
|
||||
{
|
||||
s.setValue(DisassemblyJumpLineSpacingKey, qMax(1, spacing));
|
||||
}
|
||||
|
||||
void Configuration::applySavedAsmOptions()
|
||||
@@ -925,17 +954,66 @@ void Configuration::applySavedAsmOptions()
|
||||
}
|
||||
}
|
||||
|
||||
QList<QPair<QString, QString>> Configuration::getAddrBaseOptions()
|
||||
{
|
||||
QStringList values = Core()->cmdList("e asm.addr.base=?");
|
||||
values.removeAll(QString());
|
||||
if (values.isEmpty()) {
|
||||
values = QStringList{"8", "10", "16", "36"};
|
||||
}
|
||||
QList<QPair<QString, QString>> options;
|
||||
for (const QString &v : values) {
|
||||
QString label = v;
|
||||
if (v == QLatin1String("8")) {
|
||||
label = tr("Octal (8)");
|
||||
} else if (v == QLatin1String("10")) {
|
||||
label = tr("Decimal (10)");
|
||||
} else if (v == QLatin1String("16")) {
|
||||
label = tr("Hexadecimal (16)");
|
||||
} else if (v == QLatin1String("36")) {
|
||||
label = tr("Base36 (36)");
|
||||
}
|
||||
options.append({v, label});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
QString Configuration::getAddrBase()
|
||||
{
|
||||
QString base = getConfigString("asm.addr.base");
|
||||
return base.isEmpty() ? QStringLiteral("16") : base;
|
||||
}
|
||||
|
||||
void Configuration::setAddrBase(const QString &base)
|
||||
{
|
||||
setConfig("asm.addr.base", base);
|
||||
}
|
||||
|
||||
const QList<IaitoInterfaceTheme> &Configuration::iaitoInterfaceThemesList()
|
||||
{
|
||||
// Cached once, but the Native entry's darkness flag is refreshed on
|
||||
// every access so that live OS theme changes are reflected immediately.
|
||||
static QList<IaitoInterfaceTheme> list
|
||||
static QList<IaitoInterfaceTheme> list;
|
||||
list
|
||||
= {{"Native", Configuration::nativeWindowIsDark() ? DarkFlag : LightFlag},
|
||||
{"Dark", DarkFlag},
|
||||
{"Midnight", DarkFlag},
|
||||
{"Light", LightFlag},
|
||||
{"Classic", LightFlag}};
|
||||
list[0].flag = Configuration::nativeWindowIsDark() ? DarkFlag : LightFlag;
|
||||
{"Classic", LightFlag},
|
||||
{"Pro", LightFlag},
|
||||
{"Pro Dark", DarkFlag},
|
||||
{"Nord", DarkFlag},
|
||||
{"High Contrast", DarkFlag},
|
||||
{"Cobalt", DarkFlag},
|
||||
{"Capsule", LightFlag},
|
||||
{"Phosphor", DarkFlag}};
|
||||
const auto userThemes
|
||||
= QDir(userThemesDir())
|
||||
.entryInfoList(QStringList{QStringLiteral("*.theme")}, QDir::Files, QDir::Name);
|
||||
for (const QFileInfo &fi : userThemes) {
|
||||
const bool dark = QSettings(fi.absoluteFilePath(), QSettings::IniFormat)
|
||||
.value("meta/dark", true)
|
||||
.toBool();
|
||||
list.append({fi.completeBaseName(), dark ? DarkFlag : LightFlag});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -1086,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>();
|
||||
|
||||
@@ -17,6 +17,7 @@ class Theme;
|
||||
|
||||
class QSyntaxHighlighter;
|
||||
class QTextDocument;
|
||||
class Theme;
|
||||
|
||||
enum ColorFlags {
|
||||
LightFlag = 1,
|
||||
@@ -35,11 +36,14 @@ class IAITO_EXPORT Configuration : public QObject
|
||||
public:
|
||||
enum class VisualNavbarLocation { Top = 0, Bottom, Left, Right, SuperTop, SuperBottom };
|
||||
Q_ENUM(VisualNavbarLocation)
|
||||
enum class MemoryScrollBarMode { Spring = 0, Bounded, Hidden };
|
||||
Q_ENUM(MemoryScrollBarMode)
|
||||
|
||||
private:
|
||||
QPalette nativePalette;
|
||||
QFont defaultAppFont;
|
||||
QSettings s;
|
||||
QHash<QString, QColor> m_interfaceChrome;
|
||||
static Configuration *mPtr;
|
||||
|
||||
#ifdef IAITO_ENABLE_KSYNTAXHIGHLIGHTING
|
||||
@@ -57,11 +61,8 @@ private:
|
||||
// Colors
|
||||
void loadBaseThemeNative();
|
||||
void loadBaseThemeDark();
|
||||
void loadNativeStylesheet();
|
||||
void loadLightStylesheet();
|
||||
void loadDarkStylesheet();
|
||||
void loadMidnightStylesheet();
|
||||
void loadClassicStylesheet();
|
||||
Theme nativeEngineTheme();
|
||||
bool loadUserStylesheet(const QString &name);
|
||||
|
||||
void onSystemColorSchemeChanged();
|
||||
|
||||
@@ -73,6 +74,17 @@ protected:
|
||||
|
||||
public:
|
||||
static const QList<IaitoInterfaceTheme> &iaitoInterfaceThemesList();
|
||||
static QString userThemesDir();
|
||||
static bool isCustomInterfaceTheme(const QString &name);
|
||||
static bool interfaceThemeIsDark(const QString &name);
|
||||
static bool isValidThemeName(const QString &name);
|
||||
static const QStringList &interfaceThemeVariableKeys();
|
||||
static QHash<QString, QColor> defaultInterfaceThemeVariables();
|
||||
static QHash<QString, QColor> loadInterfaceThemeVariables(const QString &name);
|
||||
static void saveInterfaceThemeVariables(
|
||||
const QString &name, const QHash<QString, QColor> &vars, bool dark);
|
||||
void applyInterfaceVariables(const QHash<QString, QColor> &vars, bool dark);
|
||||
void loadEngineTheme(const Theme &theme);
|
||||
static const QHash<QString, ColorFlags> relevantThemes;
|
||||
static const QHash<QString, QHash<ColorFlags, QColor>> iaitoOptionColors;
|
||||
|
||||
@@ -108,8 +120,9 @@ public:
|
||||
* @brief Gets a smaller variant of the configured font, suitable for
|
||||
* list/tree views. Uses 85% of the normal font size.
|
||||
* @return the configured font at reduced size, adjusted by zoom level
|
||||
* unless zoomed is false
|
||||
*/
|
||||
const QFont getSmallFont() const;
|
||||
const QFont getSmallFont(bool zoomed = true) const;
|
||||
void setFont(const QFont &font);
|
||||
qreal getZoomFactor() const;
|
||||
void setZoomFactor(qreal zoom);
|
||||
@@ -117,8 +130,10 @@ public:
|
||||
void setVisualNavbarLocation(VisualNavbarLocation location);
|
||||
int getVisualNavbarThickness() const;
|
||||
void setVisualNavbarThickness(int thickness);
|
||||
bool getVisualNavbarUseThemeColors() const;
|
||||
void setVisualNavbarUseThemeColors(bool enabled);
|
||||
int getVisualNavbarResolution() const;
|
||||
void setVisualNavbarResolution(int divisor);
|
||||
MemoryScrollBarMode getMemoryScrollBarMode() const;
|
||||
void setMemoryScrollBarMode(MemoryScrollBarMode mode);
|
||||
|
||||
// Colors
|
||||
bool windowColorIsDark();
|
||||
@@ -148,6 +163,9 @@ public:
|
||||
QString getRecentFolder();
|
||||
void setRecentFolder(const QString &dir);
|
||||
|
||||
QString getSha256LookupBaseUrl() const;
|
||||
void setSha256LookupBaseUrl(const QString &url);
|
||||
|
||||
void setNewFileLastClicked(int lastClicked);
|
||||
int getNewFileLastClicked();
|
||||
|
||||
@@ -156,6 +174,10 @@ public:
|
||||
|
||||
// Asm Options
|
||||
void resetToDefaultAsmOptions();
|
||||
bool getDisassemblyJumpLineAdaptiveSpacingEnabled() const;
|
||||
void setDisassemblyJumpLineAdaptiveSpacingEnabled(bool enabled);
|
||||
int getDisassemblyJumpLineSpacing() const;
|
||||
void setDisassemblyJumpLineSpacing(int spacing);
|
||||
|
||||
QString getColorTheme() const { return s.value("theme", "iaito").toString(); }
|
||||
void setColorTheme(const QString &theme);
|
||||
@@ -185,6 +207,20 @@ public:
|
||||
void setConfig(const QString &key, const QVariant &value);
|
||||
bool isFirstExecution();
|
||||
|
||||
/**
|
||||
* @brief Numeric base used to render addresses (asm.addr.base).
|
||||
* @return the current base value (e.g. "8", "10", "16", "36").
|
||||
*/
|
||||
QString getAddrBase();
|
||||
void setAddrBase(const QString &base);
|
||||
/**
|
||||
* @brief Ordered (value, label) pairs for the available address bases.
|
||||
*
|
||||
* Queried from radare2 via "e asm.addr.base=?", falling back to the
|
||||
* known set (8/10/16/36) when the option is unavailable.
|
||||
*/
|
||||
static QList<QPair<QString, QString>> getAddrBaseOptions();
|
||||
|
||||
/**
|
||||
* @return id of the last selected decompiler (see
|
||||
* IaitoCore::getDecompilerById)
|
||||
@@ -200,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);
|
||||
@@ -261,6 +299,8 @@ signals:
|
||||
void interfaceThemeChanged();
|
||||
void visualNavbarLocationChanged(VisualNavbarLocation location);
|
||||
void visualNavbarThicknessChanged(int thickness);
|
||||
void visualNavbarResolutionChanged(int divisor);
|
||||
void memoryScrollBarModeChanged(MemoryScrollBarMode mode);
|
||||
void addressRangeSelectionSyncEnabledChanged(bool enabled);
|
||||
#ifdef IAITO_ENABLE_KSYNTAXHIGHLIGHTING
|
||||
void kSyntaxHighlightingThemeChanged();
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "DataAnalysis.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace DataAnalysis {
|
||||
|
||||
Range sizedRange(RVA start, RVA size)
|
||||
{
|
||||
return {start, start + qMin(size, RVA_MAX - start)};
|
||||
}
|
||||
|
||||
Range intersect(Range a, Range b)
|
||||
{
|
||||
const RVA start = qMax(a.start, b.start);
|
||||
return {start, qMax(start, qMin(a.end, b.end))};
|
||||
}
|
||||
|
||||
Range bucket(Range range, int index, int count)
|
||||
{
|
||||
if (!range.valid() || count <= 0 || index < 0 || index >= count) {
|
||||
return {};
|
||||
}
|
||||
// Avoid both floating point address rounding and multiplication overflow.
|
||||
auto boundary = [range, count](RVA i) {
|
||||
return range.start + (range.size() / count) * i + (range.size() % count) * i / count;
|
||||
};
|
||||
return {boundary(index), boundary(index + 1)};
|
||||
}
|
||||
|
||||
Range centered(Range domain, RVA address, RVA size)
|
||||
{
|
||||
if (!domain.valid()) {
|
||||
return {};
|
||||
}
|
||||
size = qBound<RVA>(1, size, domain.size());
|
||||
address = qBound(domain.start, address, domain.end - 1);
|
||||
const RVA start = address - qMin(address - domain.start, size / 2);
|
||||
const RVA clamped = qMin(start, domain.end - size);
|
||||
return {clamped, clamped + size};
|
||||
}
|
||||
|
||||
QVector<Range> mergeRanges(QVector<Range> ranges)
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), [](Range a, Range b) { return a.start < b.start; });
|
||||
QVector<Range> result;
|
||||
for (auto range : ranges) {
|
||||
if (!range.valid()) {
|
||||
continue;
|
||||
}
|
||||
if (result.isEmpty() || result.last().end < range.start) {
|
||||
result.append(range);
|
||||
} else {
|
||||
result.last().end = qMax(result.last().end, range.end);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Sample sample(
|
||||
Range range,
|
||||
const QVector<Range> &readable,
|
||||
const std::function<QByteArray(RVA, int)> &read,
|
||||
int budget)
|
||||
{
|
||||
Sample result;
|
||||
result.range = range;
|
||||
if (!range.valid() || budget <= 0) {
|
||||
return result;
|
||||
}
|
||||
QVector<Range> spans;
|
||||
for (auto candidate : readable) {
|
||||
const Range span = intersect(range, candidate);
|
||||
if (span.valid()) {
|
||||
spans.append(span);
|
||||
result.covered += span.size();
|
||||
}
|
||||
}
|
||||
std::array<int, 256> histogram{};
|
||||
RVA remaining = result.covered;
|
||||
for (auto span : spans) {
|
||||
if (budget <= 0) {
|
||||
break;
|
||||
}
|
||||
// Allocate the budget proportionally among disjoint readable spans.
|
||||
const int share = qMax(1, int((long double) span.size() / remaining * budget));
|
||||
const int length = int(qMin<RVA>(span.size(), qMin(budget, share)));
|
||||
const RVA address = span.start + (span.size() - length) / 2;
|
||||
const QByteArray data = read(address, length).left(length);
|
||||
for (unsigned char byte : data) {
|
||||
++histogram[byte];
|
||||
}
|
||||
result.bytes += data.size();
|
||||
budget -= length;
|
||||
remaining -= span.size();
|
||||
}
|
||||
if (!result.bytes) {
|
||||
return result;
|
||||
}
|
||||
auto &v = result.values;
|
||||
for (int byte = 0; byte < 256; ++byte) {
|
||||
const int n = histogram[byte];
|
||||
if (!n) {
|
||||
continue;
|
||||
}
|
||||
const double p = double(n) / result.bytes;
|
||||
v[Entropy] -= p * std::log2(p) / 8.0;
|
||||
v[Printable] += (byte >= 32 && byte <= 126) ? p : 0;
|
||||
v[Zero] += byte == 0 ? p : 0;
|
||||
v[FF] += byte == 255 ? p : 0;
|
||||
v[Unique] += 1.0 / 256;
|
||||
v[Mean] += p * byte / 255;
|
||||
v[Alpha] += ((byte >= 'a' && byte <= 'z') || (byte >= 'A' && byte <= 'Z')) ? p : 0;
|
||||
v[Digit] += (byte >= '0' && byte <= '9') ? p : 0;
|
||||
v[Space] += (byte == ' ' || (byte >= 9 && byte <= 13)) ? p : 0;
|
||||
v[Control] += (byte < 32 || byte == 127) ? p : 0;
|
||||
v[High] += byte >= 128 ? p : 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace DataAnalysis
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef DATAANALYSIS_H
|
||||
#define DATAANALYSIS_H
|
||||
|
||||
#include "core/IaitoCommon.h"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <QByteArray>
|
||||
#include <QVector>
|
||||
|
||||
namespace DataAnalysis {
|
||||
|
||||
// Half-open ranges, including when the exclusive end is RVA_MAX.
|
||||
struct Range
|
||||
{
|
||||
RVA start = 0;
|
||||
RVA end = 0;
|
||||
bool valid() const { return start < end; }
|
||||
RVA size() const { return end - start; }
|
||||
bool contains(RVA address) const { return start <= address && address < end; }
|
||||
bool operator==(const Range &other) const { return start == other.start && end == other.end; }
|
||||
bool operator!=(const Range &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
enum Metric {
|
||||
Entropy,
|
||||
Printable,
|
||||
Zero,
|
||||
FF,
|
||||
Unique,
|
||||
Mean,
|
||||
Alpha,
|
||||
Digit,
|
||||
Space,
|
||||
Control,
|
||||
High,
|
||||
Count
|
||||
};
|
||||
|
||||
struct Sample
|
||||
{
|
||||
Range range;
|
||||
RVA covered = 0;
|
||||
int bytes = 0;
|
||||
std::array<double, Count> values{}; // Normalized to [0, 1]; entropy / 8.
|
||||
};
|
||||
|
||||
Range sizedRange(RVA start, RVA size);
|
||||
Range intersect(Range a, Range b);
|
||||
Range bucket(Range range, int index, int count);
|
||||
Range centered(Range domain, RVA address, RVA size);
|
||||
QVector<Range> mergeRanges(QVector<Range> ranges);
|
||||
|
||||
// Bounded, deterministic sampling. Reads only known readable spans; missing bytes
|
||||
// never contribute fabricated 0xff data. Large buckets use centered windows.
|
||||
Sample sample(
|
||||
Range range,
|
||||
const QVector<Range> &readable,
|
||||
const std::function<QByteArray(RVA, int)> &read,
|
||||
int budget = 4096);
|
||||
|
||||
} // namespace DataAnalysis
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,347 @@
|
||||
#include "DeepLink.h"
|
||||
|
||||
#include "MemoryDockWidget.h"
|
||||
#include "common/InitialOptions.h"
|
||||
#include "common/SamplesDB.h"
|
||||
#include "core/Iaito.h"
|
||||
#include "core/MainWindow.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QFileInfo>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
|
||||
namespace {
|
||||
|
||||
// Parsed query parameters shared by every handler.
|
||||
struct Params
|
||||
{
|
||||
QString addr;
|
||||
QString view;
|
||||
QString size;
|
||||
QString arch;
|
||||
QString cpu;
|
||||
QString os;
|
||||
QString bits;
|
||||
QString endian;
|
||||
QString base;
|
||||
};
|
||||
|
||||
// Strips the single leading slash QUrl keeps in the path of authority URIs.
|
||||
QString stripLeadingSlash(const QString &path)
|
||||
{
|
||||
return path.startsWith(QLatin1Char('/')) ? path.mid(1) : path;
|
||||
}
|
||||
|
||||
void applyAnalParams(InitialOptions &options, const Params &p)
|
||||
{
|
||||
if (!p.arch.isEmpty()) {
|
||||
options.arch = p.arch;
|
||||
}
|
||||
if (!p.cpu.isEmpty()) {
|
||||
options.cpu = p.cpu;
|
||||
}
|
||||
if (!p.os.isEmpty()) {
|
||||
options.os = p.os;
|
||||
}
|
||||
if (!p.bits.isEmpty()) {
|
||||
options.bits = p.bits.toInt();
|
||||
}
|
||||
if (p.endian.compare(QStringLiteral("big"), Qt::CaseInsensitive) == 0) {
|
||||
options.endian = InitialOptions::Endianness::Big;
|
||||
} else if (p.endian.compare(QStringLiteral("little"), Qt::CaseInsensitive) == 0) {
|
||||
options.endian = InitialOptions::Endianness::Little;
|
||||
}
|
||||
if (!p.base.isEmpty()) {
|
||||
options.binLoadAddr = Core()->math(p.base);
|
||||
options.mapAddr = options.binLoadAddr;
|
||||
}
|
||||
}
|
||||
|
||||
void showView(MainWindow *main, const QString &view)
|
||||
{
|
||||
const QString v = view.toLower();
|
||||
if (v == QStringLiteral("disasm")) {
|
||||
main->showMemoryWidget(MemoryWidgetType::Disassembly);
|
||||
} else if (v == QStringLiteral("graph")) {
|
||||
main->showMemoryWidget(MemoryWidgetType::Graph);
|
||||
} else if (v == QStringLiteral("hex")) {
|
||||
main->showMemoryWidget(MemoryWidgetType::Hexdump);
|
||||
} else if (v == QStringLiteral("decompiler")) {
|
||||
main->showMemoryWidget(MemoryWidgetType::Decompiler);
|
||||
} else {
|
||||
// strings, imports, exports, functions and any other dock panel
|
||||
main->focusPanelByName(v);
|
||||
}
|
||||
}
|
||||
|
||||
void applyLocation(MainWindow *main, const Params &p)
|
||||
{
|
||||
if (!p.size.isEmpty()) {
|
||||
// Resolve numerically so size= can never inject extra r2 commands.
|
||||
Core()->cmdRaw(QStringLiteral("b %1").arg(Core()->math(p.size)));
|
||||
}
|
||||
if (!p.addr.isEmpty()) {
|
||||
Core()->seek(p.addr);
|
||||
}
|
||||
if (!p.view.isEmpty()) {
|
||||
showView(main, p.view);
|
||||
} else if (!p.addr.isEmpty()) {
|
||||
Core()->seekAndShow(Core()->getOffset());
|
||||
}
|
||||
}
|
||||
|
||||
// Arms a one-shot uiReady handler that applies addr/view/size once the open
|
||||
// that follows finishes loading. Call this only right before an open is issued,
|
||||
// so a parse/lookup failure never leaves a connection that misfires on a later
|
||||
// unrelated open.
|
||||
void scheduleApply(MainWindow *main, const Params &p)
|
||||
{
|
||||
if (p.addr.isEmpty() && p.view.isEmpty() && p.size.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto conn = std::make_shared<QMetaObject::Connection>();
|
||||
*conn = QObject::connect(main, &MainWindow::uiReady, main, [main, p, conn]() {
|
||||
QObject::disconnect(*conn);
|
||||
applyLocation(main, p);
|
||||
});
|
||||
}
|
||||
|
||||
void openFile(MainWindow *main, const QString &path, const Params &p)
|
||||
{
|
||||
InitialOptions options;
|
||||
options.filename = path;
|
||||
applyAnalParams(options, p);
|
||||
scheduleApply(main, p);
|
||||
main->openNewFile(options, true);
|
||||
}
|
||||
|
||||
void openProject(MainWindow *main, const QString &name, const Params &p)
|
||||
{
|
||||
scheduleApply(main, p);
|
||||
main->openProject(name);
|
||||
}
|
||||
|
||||
void openBytes(MainWindow *main, const QString &hex, const Params &p)
|
||||
{
|
||||
QString clean = hex;
|
||||
clean.remove(QRegularExpression(QStringLiteral("[^0-9a-fA-F]")));
|
||||
if (clean.size() < 2) {
|
||||
QMessageBox::critical(
|
||||
main, QObject::tr("Deep link"), QObject::tr("Empty or invalid bytes payload."));
|
||||
return;
|
||||
}
|
||||
const int nbytes = clean.size() / 2;
|
||||
InitialOptions options;
|
||||
options.filename = QStringLiteral("malloc://%1").arg(nbytes);
|
||||
options.shellcode = clean.left(nbytes * 2);
|
||||
applyAnalParams(options, p);
|
||||
scheduleApply(main, p);
|
||||
main->openNewFile(options, true);
|
||||
}
|
||||
|
||||
// Returns true when host/path already refer to the file open in this session,
|
||||
// so a deep link to "another part of the same program" navigates in place
|
||||
// instead of reopening the binary.
|
||||
bool sameAsCurrent(const QString &host, const QString &path)
|
||||
{
|
||||
const QString current = Core()->getFilePath();
|
||||
if (current.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const QString currentAbs = QFileInfo(current).absoluteFilePath();
|
||||
if (host.isEmpty() || host == QStringLiteral("file")) {
|
||||
return QFileInfo(path).absoluteFilePath() == currentAbs;
|
||||
}
|
||||
if (host == QStringLiteral("project")) {
|
||||
return stripLeadingSlash(path) == Core()->getConfig("prj.name");
|
||||
}
|
||||
if (host == QStringLiteral("sha256")) {
|
||||
const QString p = SamplesDB::pathForHash(stripLeadingSlash(path));
|
||||
return !p.isEmpty() && QFileInfo(p).absoluteFilePath() == currentAbs;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Looks up the sample by sha256 in the local database and opens it. Downloading
|
||||
// unknown samples is intentionally out of scope (see DEEPLINK.md); samples are
|
||||
// registered automatically when opened and via the New File dialog's Sha256 tab.
|
||||
void openHash(MainWindow *main, const QString &hash, const Params &p)
|
||||
{
|
||||
const QString path = SamplesDB::pathForHash(hash);
|
||||
if (!path.isEmpty() && QFileInfo::exists(path)) {
|
||||
openFile(main, path, p);
|
||||
return;
|
||||
}
|
||||
QMessageBox::warning(
|
||||
main,
|
||||
QObject::tr("Deep link"),
|
||||
QObject::tr(
|
||||
"No local sample is registered for SHA256:\n%1\n\nOpen the file once or use "
|
||||
"the Sha256 tab in the New File dialog to register it (database: %2).")
|
||||
.arg(hash.toLower(), SamplesDB::dbRoot()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace DeepLink {
|
||||
|
||||
bool isDeepLink(const QString &arg)
|
||||
{
|
||||
return arg.startsWith(QStringLiteral("iaito://"), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool handle(MainWindow *main, const QString &uri)
|
||||
{
|
||||
if (main == nullptr || !isDeepLink(uri)) {
|
||||
return false;
|
||||
}
|
||||
const QUrl url(uri, QUrl::TolerantMode);
|
||||
if (url.scheme().compare(QStringLiteral("iaito"), Qt::CaseInsensitive) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Params p;
|
||||
const QUrlQuery q(url);
|
||||
p.addr = q.queryItemValue(QStringLiteral("addr"));
|
||||
p.view = q.queryItemValue(QStringLiteral("view"));
|
||||
p.size = q.queryItemValue(QStringLiteral("size"));
|
||||
p.arch = q.queryItemValue(QStringLiteral("arch"));
|
||||
p.cpu = q.queryItemValue(QStringLiteral("cpu"));
|
||||
p.os = q.queryItemValue(QStringLiteral("os"));
|
||||
p.bits = q.queryItemValue(QStringLiteral("bits"));
|
||||
p.endian = q.queryItemValue(QStringLiteral("endian"));
|
||||
p.base = q.queryItemValue(QStringLiteral("base"));
|
||||
|
||||
const QString host = url.host().toLower();
|
||||
const QString path = url.path();
|
||||
|
||||
// A link into the already-open file just navigates, never reopens it.
|
||||
if (host != QStringLiteral("bytes") && sameAsCurrent(host, path)) {
|
||||
applyLocation(main, p);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (host.isEmpty() || host == QStringLiteral("file")) {
|
||||
// iaito:///bin/ls and iaito://file/bin/ls both map to /bin/ls
|
||||
if (path.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
openFile(main, path, p);
|
||||
} else if (host == QStringLiteral("project")) {
|
||||
openProject(main, stripLeadingSlash(path), p);
|
||||
} else if (host == QStringLiteral("sha256")) {
|
||||
openHash(main, stripLeadingSlash(path), p);
|
||||
} else if (host == QStringLiteral("bytes")) {
|
||||
openBytes(main, stripLeadingSlash(path), p);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString buildForOffset(QWidget *parent, RVA offset, const QString &view)
|
||||
{
|
||||
const QString filePath = Core()->getFilePath();
|
||||
const QString project = Core()->getConfig("prj.name");
|
||||
|
||||
// Reference candidates in priority order: project, sha256, absolute path.
|
||||
enum Kind { Project, Sha256, FilePath };
|
||||
QStringList labels;
|
||||
QList<int> kinds;
|
||||
if (!project.isEmpty()) {
|
||||
labels << QObject::tr("Project: %1").arg(project);
|
||||
kinds << Project;
|
||||
}
|
||||
if (!filePath.isEmpty() && QFileInfo::exists(filePath)) {
|
||||
labels << QObject::tr("SHA256 hash of the file");
|
||||
kinds << Sha256;
|
||||
labels << QObject::tr("File path: %1").arg(filePath);
|
||||
kinds << FilePath;
|
||||
}
|
||||
if (labels.isEmpty()) {
|
||||
QMessageBox::warning(
|
||||
parent,
|
||||
QObject::tr("Copy deep link"),
|
||||
QObject::tr("No file or project is open to reference."));
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QString choice = QInputDialog::getItem(
|
||||
parent,
|
||||
QObject::tr("Copy deep link"),
|
||||
QObject::tr("Reference the current file by:"),
|
||||
labels,
|
||||
0,
|
||||
false,
|
||||
&ok);
|
||||
if (!ok || choice.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
const int kind = kinds.at(labels.indexOf(choice));
|
||||
|
||||
QUrl url;
|
||||
url.setScheme(QStringLiteral("iaito"));
|
||||
if (kind == Project) {
|
||||
url.setHost(QStringLiteral("project"));
|
||||
url.setPath(QStringLiteral("/") + project);
|
||||
} else if (kind == Sha256) {
|
||||
const QString hash = SamplesDB::registerFile(filePath);
|
||||
if (hash.isEmpty()) {
|
||||
QMessageBox::warning(
|
||||
parent,
|
||||
QObject::tr("Copy deep link"),
|
||||
QObject::tr("Could not compute the sha256 of:\n%1").arg(filePath));
|
||||
return QString();
|
||||
}
|
||||
url.setHost(QStringLiteral("sha256"));
|
||||
url.setPath(QStringLiteral("/") + hash);
|
||||
} else {
|
||||
url.setHost(QStringLiteral("file"));
|
||||
url.setPath(QFileInfo(filePath).absoluteFilePath());
|
||||
}
|
||||
|
||||
QUrlQuery q;
|
||||
if (offset != RVA_INVALID) {
|
||||
q.addQueryItem(QStringLiteral("addr"), RAddressString(offset));
|
||||
}
|
||||
if (!view.isEmpty()) {
|
||||
q.addQueryItem(QStringLiteral("view"), view);
|
||||
}
|
||||
url.setQuery(q);
|
||||
return url.toString(QUrl::FullyEncoded);
|
||||
}
|
||||
|
||||
void copyForOffset(QWidget *parent, RVA offset, const QString &view)
|
||||
{
|
||||
const QString link = buildForOffset(parent, offset, view);
|
||||
if (!link.isEmpty()) {
|
||||
QApplication::clipboard()->setText(link);
|
||||
}
|
||||
}
|
||||
|
||||
QString linkAt(const QString &line, int posInLine)
|
||||
{
|
||||
if (posInLine < 0 || posInLine > line.length()) {
|
||||
return QString();
|
||||
}
|
||||
// A deep link ends at whitespace or characters that cannot belong to a URI.
|
||||
static const QRegularExpression re(QStringLiteral("iaito://[^\\s\"'<>)\\]}]+"));
|
||||
auto it = re.globalMatch(line);
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch m = it.next();
|
||||
if (posInLine >= m.capturedStart() && posInLine < m.capturedEnd()) {
|
||||
return m.captured();
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace DeepLink
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef IAITO_DEEPLINK_H
|
||||
#define IAITO_DEEPLINK_H
|
||||
|
||||
#include "core/IaitoCommon.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
class MainWindow;
|
||||
class QWidget;
|
||||
|
||||
// iaito:// deep link handling. See DEEPLINK.md for the URI specification.
|
||||
namespace DeepLink {
|
||||
|
||||
// Returns true if arg looks like an iaito:// deep link.
|
||||
bool isDeepLink(const QString &arg);
|
||||
|
||||
// Parses uri and opens the referenced file/project/sha256/bytes, applying the
|
||||
// addr/view/size query parameters once the UI is ready. Returns false when the
|
||||
// uri is not a valid iaito:// deep link.
|
||||
bool handle(MainWindow *main, const QString &uri);
|
||||
|
||||
// Builds an iaito:// deep link pointing at offset (and optionally a view) in the
|
||||
// currently open file. Prompts the user to choose how the file is referenced
|
||||
// (loaded project, sha256 or absolute path). Returns an empty string when the
|
||||
// user cancels or no reference is available.
|
||||
QString buildForOffset(QWidget *parent, RVA offset, const QString &view = QString());
|
||||
|
||||
// buildForOffset + copy the result to the clipboard. No-op when cancelled.
|
||||
void copyForOffset(QWidget *parent, RVA offset, const QString &view = QString());
|
||||
|
||||
// If posInLine falls inside an iaito:// link contained in line, returns that
|
||||
// link, otherwise an empty string. Used to make links in comments clickable.
|
||||
QString linkAt(const QString &line, int posInLine);
|
||||
|
||||
} // namespace DeepLink
|
||||
|
||||
#endif // IAITO_DEEPLINK_H
|
||||
@@ -0,0 +1,124 @@
|
||||
#include "ExceptionInfo.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <QCoreApplication>
|
||||
#include <QJsonObject>
|
||||
#include <QVariant>
|
||||
|
||||
static QString exceptionTr(const char *text)
|
||||
{
|
||||
return QCoreApplication::translate("ExceptionInfo", text);
|
||||
}
|
||||
|
||||
static quint64 exceptionAddress(const QJsonValue &value)
|
||||
{
|
||||
bool ok = false;
|
||||
quint64 result = ExceptionRegion::Invalid;
|
||||
if (value.isString()) {
|
||||
result = value.toString().toULongLong(&ok, 0);
|
||||
} else if (
|
||||
value.isDouble() && value.toDouble() >= 0
|
||||
&& std::floor(value.toDouble()) == value.toDouble()) {
|
||||
result = value.toVariant().toULongLong(&ok);
|
||||
}
|
||||
return ok ? result : ExceptionRegion::Invalid;
|
||||
}
|
||||
|
||||
bool ExceptionRegion::protects(quint64 address, quint64 size) const
|
||||
{
|
||||
// Subtraction avoids overflowing a block's end near the address-space limit.
|
||||
return size && address < to && (address >= from || from - address < size);
|
||||
}
|
||||
|
||||
bool ExceptionRegion::touches(quint64 address, quint64 size) const
|
||||
{
|
||||
return protects(address, size)
|
||||
|| (handler != Invalid && handler >= address && handler - address < size);
|
||||
}
|
||||
|
||||
QString ExceptionRegion::label() const
|
||||
{
|
||||
if (kind == QStringLiteral("cleanup")) {
|
||||
return exceptionTr("Cleanup / finally");
|
||||
}
|
||||
const QString name = kind == QStringLiteral("filter") ? exceptionTr("Filter")
|
||||
: exceptionTr("Catch");
|
||||
return QStringLiteral("%1 (%2)").arg(
|
||||
name,
|
||||
catchAll ? exceptionTr("all exceptions")
|
||||
: type.isEmpty() ? exceptionTr("unknown type")
|
||||
: type);
|
||||
}
|
||||
|
||||
QString ExceptionRegion::description() const
|
||||
{
|
||||
QString text = exceptionTr("#%1 · %2\nProtected: [0x%3, 0x%4)\nHandler: 0x%5")
|
||||
.arg(index)
|
||||
.arg(label())
|
||||
.arg(from, 0, 16)
|
||||
.arg(to, 0, 16)
|
||||
.arg(handler, 0, 16);
|
||||
if (source != Invalid) {
|
||||
text += exceptionTr("\nMetadata source: 0x%1").arg(source, 0, 16);
|
||||
}
|
||||
if (filter && filter != Invalid) {
|
||||
text += exceptionTr("\nFilter value / address: 0x%1").arg(filter, 0, 16);
|
||||
}
|
||||
if (typeFilter) {
|
||||
text += exceptionTr("\nType filter: %1").arg(typeFilter);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
QColor ExceptionRegion::color() const
|
||||
{
|
||||
// Stable across views and independent of metadata order; shared ranges share a color.
|
||||
const quint64 hash = (from >> 2) ^ (to >> 1) ^ from;
|
||||
return QColor::fromHsv(int(hash % 12) * 30, 145, 210);
|
||||
}
|
||||
|
||||
QList<ExceptionRegion> ExceptionRegion::parse(const QJsonArray &array)
|
||||
{
|
||||
QList<ExceptionRegion> result;
|
||||
for (int i = 0; i < array.size(); ++i) {
|
||||
const auto object = array.at(i).toObject();
|
||||
ExceptionRegion region;
|
||||
region.from = exceptionAddress(object.value(QStringLiteral("from")));
|
||||
region.to = exceptionAddress(object.value(QStringLiteral("to")));
|
||||
region.handler = exceptionAddress(object.value(QStringLiteral("handler")));
|
||||
if (region.from == Invalid || region.to == Invalid || region.from >= region.to
|
||||
|| region.handler == Invalid) {
|
||||
continue;
|
||||
}
|
||||
region.index = exceptionAddress(object.value(QStringLiteral("index")));
|
||||
if (region.index == Invalid) {
|
||||
region.index = quint64(i);
|
||||
}
|
||||
region.source = exceptionAddress(object.value(QStringLiteral("source")));
|
||||
region.filter = exceptionAddress(object.value(QStringLiteral("filter")));
|
||||
region.kind = object.value(QStringLiteral("kind")).toString();
|
||||
region.type = object.value(QStringLiteral("type")).toString();
|
||||
region.typeFilter = object.value(QStringLiteral("typeFilter")).toVariant().toLongLong();
|
||||
region.catchAll = object.value(QStringLiteral("catchAll")).toBool();
|
||||
result.append(region);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QList<ExceptionRegion> ExceptionRegion::forBlocks(
|
||||
const QList<ExceptionRegion> ®ions, const QJsonArray &blocks)
|
||||
{
|
||||
QList<ExceptionRegion> result;
|
||||
for (const auto ®ion : regions) {
|
||||
for (const auto &value : blocks) {
|
||||
const auto block = value.toObject();
|
||||
const auto address = exceptionAddress(block.value(QStringLiteral("addr")));
|
||||
const auto size = exceptionAddress(block.value(QStringLiteral("size")));
|
||||
if (address != Invalid && size != Invalid && region.touches(address, size)) {
|
||||
result.append(region);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef EXCEPTIONINFO_H
|
||||
#define EXCEPTIONINFO_H
|
||||
|
||||
#include <limits>
|
||||
#include <QColor>
|
||||
#include <QJsonArray>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
// Addresses in iwj use half-open ranges: [from, to).
|
||||
struct ExceptionRegion
|
||||
{
|
||||
static constexpr quint64 Invalid = std::numeric_limits<quint64>::max();
|
||||
quint64 index = 0;
|
||||
quint64 source = Invalid;
|
||||
quint64 from = Invalid;
|
||||
quint64 to = Invalid;
|
||||
quint64 handler = Invalid;
|
||||
quint64 filter = 0;
|
||||
QString kind;
|
||||
QString type;
|
||||
qint64 typeFilter = 0;
|
||||
bool catchAll = false;
|
||||
|
||||
bool contains(quint64 address) const { return from <= address && address < to; }
|
||||
bool protects(quint64 address, quint64 size) const;
|
||||
bool touches(quint64 address, quint64 size) const;
|
||||
QString label() const;
|
||||
QString description() const;
|
||||
QColor color() const;
|
||||
static QList<ExceptionRegion> parse(const QJsonArray &array);
|
||||
static QList<ExceptionRegion> forBlocks(
|
||||
const QList<ExceptionRegion> ®ions, const QJsonArray &blocks);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cmath>
|
||||
#include <QAbstractButton>
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QCryptographicHash>
|
||||
@@ -151,6 +152,28 @@ void bindFont(QWidget *view, bool smallFont, bool monospace)
|
||||
QObject::connect(Config(), &Configuration::fontsUpdated, view, apply);
|
||||
}
|
||||
|
||||
// Monospace code font scaled to the size of the regular UI font
|
||||
QFont logFont()
|
||||
{
|
||||
QFont f = Config()->getBaseFont();
|
||||
const QFont ui = QApplication::font();
|
||||
if (ui.pixelSize() > 0) {
|
||||
f.setPixelSize(ui.pixelSize());
|
||||
} else if (ui.pointSizeF() > 0) {
|
||||
f.setPointSizeF(ui.pointSizeF());
|
||||
}
|
||||
f.setStyleHint(QFont::Monospace);
|
||||
return f;
|
||||
}
|
||||
|
||||
void bindLogFont(QWidget *view)
|
||||
{
|
||||
view->setFont(logFont());
|
||||
QObject::connect(Config(), &Configuration::fontsUpdated, view, [view]() {
|
||||
view->setFont(logFont());
|
||||
});
|
||||
}
|
||||
|
||||
SizePolicyMinMax forceWidth(QWidget *widget, int width)
|
||||
{
|
||||
SizePolicyMinMax r;
|
||||
|
||||
@@ -67,6 +67,8 @@ IAITO_EXPORT void setVerticalScrollMode(QAbstractItemView *tw);
|
||||
IAITO_EXPORT void setCheckedWithoutSignals(QAbstractButton *button, bool checked);
|
||||
|
||||
IAITO_EXPORT void bindFont(QWidget *view, bool smallFont = true, bool monospace = false);
|
||||
IAITO_EXPORT QFont logFont();
|
||||
IAITO_EXPORT void bindLogFont(QWidget *view);
|
||||
|
||||
struct IAITO_EXPORT SizePolicyMinMax
|
||||
{
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#include "IaitoSeekable.h"
|
||||
#include "core/MainWindow.h"
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
IaitoSeekable::IaitoSeekable(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
widgetOffset = Core()->getOffset();
|
||||
connect(Core(), &IaitoCore::seekChanged, this, &IaitoSeekable::onCoreSeekChanged);
|
||||
}
|
||||
|
||||
@@ -13,35 +11,98 @@ IaitoSeekable::~IaitoSeekable() {}
|
||||
|
||||
void IaitoSeekable::setSynchronization(bool sync)
|
||||
{
|
||||
if (synchronized == sync) {
|
||||
return;
|
||||
}
|
||||
const RVA address = Core()->getOffset();
|
||||
synchronized = sync;
|
||||
onCoreSeekChanged(Core()->getOffset());
|
||||
seekHistory = {address};
|
||||
historyIndex = 0;
|
||||
updateSeek(address);
|
||||
emit syncChanged();
|
||||
emit historyChanged();
|
||||
}
|
||||
|
||||
void IaitoSeekable::onCoreSeekChanged(RVA addr)
|
||||
{
|
||||
if (synchronized && widgetOffset != addr) {
|
||||
updateSeek(addr, true);
|
||||
if (synchronized) {
|
||||
updateSeek(addr);
|
||||
emit historyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void IaitoSeekable::updateSeek(RVA addr, bool localOnly)
|
||||
void IaitoSeekable::seek(RVA addr)
|
||||
{
|
||||
previousOffset = widgetOffset;
|
||||
widgetOffset = addr;
|
||||
if (synchronized && !localOnly) {
|
||||
Core()->seek(addr);
|
||||
if (addr == RVA_INVALID) {
|
||||
return;
|
||||
}
|
||||
if (synchronized) {
|
||||
Core()->seek(addr);
|
||||
return;
|
||||
}
|
||||
if (addr == widgetOffset) {
|
||||
return;
|
||||
}
|
||||
seekHistory.resize(historyIndex + 1);
|
||||
seekHistory.append(addr);
|
||||
++historyIndex;
|
||||
updateSeek(addr);
|
||||
emit historyChanged();
|
||||
}
|
||||
|
||||
void IaitoSeekable::updateSeek(RVA addr)
|
||||
{
|
||||
if (addr == RVA_INVALID || addr == widgetOffset) {
|
||||
return;
|
||||
}
|
||||
widgetOffset = addr;
|
||||
emit seekableSeekChanged(addr);
|
||||
}
|
||||
|
||||
bool IaitoSeekable::canSeekPrev() const
|
||||
{
|
||||
if (synchronized) {
|
||||
return Core()->core()->io->undo.undos > 0;
|
||||
}
|
||||
return historyIndex > 0;
|
||||
}
|
||||
|
||||
bool IaitoSeekable::canSeekNext() const
|
||||
{
|
||||
if (synchronized) {
|
||||
return Core()->core()->io->undo.redos > 0;
|
||||
}
|
||||
return historyIndex + 1 < seekHistory.size();
|
||||
}
|
||||
|
||||
QVector<RVA> IaitoSeekable::localHistory(bool forward) const
|
||||
{
|
||||
QVector<RVA> result;
|
||||
if (!synchronized) {
|
||||
for (int i = historyIndex; i >= 0 && i < seekHistory.size(); i += forward ? 1 : -1) {
|
||||
result.append(seekHistory.at(i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void IaitoSeekable::seekPrev()
|
||||
{
|
||||
if (synchronized) {
|
||||
Core()->seekPrev();
|
||||
} else {
|
||||
this->seek(previousOffset);
|
||||
} else if (canSeekPrev()) {
|
||||
updateSeek(seekHistory.at(--historyIndex));
|
||||
emit historyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void IaitoSeekable::seekNext()
|
||||
{
|
||||
if (synchronized) {
|
||||
Core()->seekNext();
|
||||
} else if (canSeekNext()) {
|
||||
updateSeek(seekHistory.at(++historyIndex));
|
||||
emit historyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Iaito.h"
|
||||
#include <QVector>
|
||||
|
||||
class MainWindow;
|
||||
|
||||
@@ -20,7 +21,7 @@ public:
|
||||
* In any case, IaitoSeekable::seekableSeekChanged is emitted.
|
||||
* @param addr the location to seek at.
|
||||
*/
|
||||
void seek(RVA addr) { updateSeek(addr, false); }
|
||||
void seek(RVA addr);
|
||||
|
||||
/**
|
||||
* @brief setSynchronization sets
|
||||
@@ -44,6 +45,10 @@ public:
|
||||
*/
|
||||
bool isSynchronized();
|
||||
|
||||
bool canSeekPrev() const;
|
||||
bool canSeekNext() const;
|
||||
QVector<RVA> localHistory(bool forward) const;
|
||||
|
||||
/**
|
||||
* @brief seekToReference will seek to the function or the object which is
|
||||
* referenced in a given offset
|
||||
@@ -56,6 +61,7 @@ public slots:
|
||||
* @brief seekPrev seeks to last location.
|
||||
*/
|
||||
void seekPrev();
|
||||
void seekNext();
|
||||
|
||||
/**
|
||||
* @brief toggleSyncWithCore toggles
|
||||
@@ -75,11 +81,8 @@ private:
|
||||
*/
|
||||
RVA widgetOffset = RVA_INVALID;
|
||||
|
||||
/**
|
||||
* @brief previousOffset last seek location.
|
||||
* @todo maybe use an actual history?
|
||||
*/
|
||||
RVA previousOffset = RVA_INVALID;
|
||||
QVector<RVA> seekHistory;
|
||||
int historyIndex = 0;
|
||||
|
||||
/**
|
||||
* @brief synchronized tells with the seekable's offset is
|
||||
@@ -89,12 +92,11 @@ private:
|
||||
|
||||
/**
|
||||
* @brief internal method for changing the seek
|
||||
* @param localOnly whether the seek should be updated globally if
|
||||
* synchronized
|
||||
*/
|
||||
void updateSeek(RVA addr, bool localOnly);
|
||||
void updateSeek(RVA addr);
|
||||
|
||||
signals:
|
||||
void seekableSeekChanged(RVA addr);
|
||||
void syncChanged();
|
||||
void historyChanged();
|
||||
};
|
||||
@@ -30,7 +30,7 @@ struct InitialOptions
|
||||
QString os;
|
||||
bool analVars = true;
|
||||
|
||||
Endianness endian;
|
||||
Endianness endian = Endianness::Auto;
|
||||
|
||||
bool writeEnabled = false;
|
||||
bool loadBinInfo = true;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "common/Markdown.h"
|
||||
|
||||
#include <QScrollBar>
|
||||
#include <QTextBrowser>
|
||||
#include <QTextDocument>
|
||||
#include <QTextOption>
|
||||
#include <QtGlobal>
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
#define IAITO_HAS_NATIVE_MARKDOWN_READER QT_CONFIG(textmarkdownreader)
|
||||
#else
|
||||
#define IAITO_HAS_NATIVE_MARKDOWN_READER 0
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
QString markdownStyleSheet()
|
||||
{
|
||||
return QStringLiteral(
|
||||
"h1 { margin-top: 0.45em; margin-bottom: 0.35em; font-size: 160%; }"
|
||||
"h2 { margin-top: 0.45em; margin-bottom: 0.30em; font-size: 140%; }"
|
||||
"h3 { margin-top: 0.40em; margin-bottom: 0.25em; font-size: 125%; }"
|
||||
"h4, h5, h6 { margin-top: 0.35em; margin-bottom: 0.20em; }"
|
||||
"p { margin-top: 0; margin-bottom: 0.65em; }"
|
||||
"ul, ol { margin-top: 0.20em; margin-bottom: 0.65em; }"
|
||||
"li { margin-top: 0.12em; margin-bottom: 0.12em; }"
|
||||
"blockquote { margin-left: 1em; padding-left: 0.6em; }"
|
||||
"pre { margin-top: 0.25em; margin-bottom: 0.75em; white-space: pre-wrap; }"
|
||||
"code { font-family: monospace; }"
|
||||
".task-checkbox { font-family: sans-serif; }");
|
||||
}
|
||||
|
||||
void setDocumentMarkdown(QTextDocument *document, const QString &markdown)
|
||||
{
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
document->setDocumentMargin(8);
|
||||
#ifndef QT_NO_TEXTHTMLPARSER
|
||||
document->setDefaultStyleSheet(markdownStyleSheet());
|
||||
#endif
|
||||
|
||||
#if IAITO_HAS_NATIVE_MARKDOWN_READER
|
||||
document->setMarkdown(markdown, QTextDocument::MarkdownDialectGitHub);
|
||||
#else
|
||||
document->setPlainText(markdown);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Markdown {
|
||||
|
||||
void configureBrowser(QTextBrowser *browser)
|
||||
{
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
browser->setReadOnly(true);
|
||||
browser->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
||||
browser->document()->setDocumentMargin(8);
|
||||
}
|
||||
|
||||
void render(QTextBrowser *browser, const QString &markdown)
|
||||
{
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
QScrollBar *bar = browser->verticalScrollBar();
|
||||
const bool wasAtBottom = !bar || bar->value() >= bar->maximum() - 4;
|
||||
setDocumentMarkdown(browser->document(), markdown);
|
||||
if (bar && wasAtBottom) {
|
||||
bar->setValue(bar->maximum());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Markdown
|
||||
|
||||
#undef IAITO_HAS_NATIVE_MARKDOWN_READER
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef MARKDOWN_H
|
||||
#define MARKDOWN_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
class QTextBrowser;
|
||||
|
||||
namespace Markdown {
|
||||
|
||||
void configureBrowser(QTextBrowser *browser);
|
||||
void render(QTextBrowser *browser, const QString &markdown);
|
||||
|
||||
} // namespace Markdown
|
||||
|
||||
#endif // MARKDOWN_H
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "R2SourceDecompiler.h"
|
||||
#include "Iaito.h"
|
||||
|
||||
R2SourceDecompiler::R2SourceDecompiler(QObject *parent)
|
||||
: R2JsonDecompiler(
|
||||
QStringLiteral("source"),
|
||||
QStringLiteral("source"),
|
||||
QStringLiteral("CLdj"),
|
||||
QStringLiteral("CLd"),
|
||||
AnnotationMode::OffsetsOnly,
|
||||
parent)
|
||||
{}
|
||||
|
||||
bool R2SourceDecompiler::isAvailable()
|
||||
{
|
||||
return Core()->cmd(QStringLiteral("CLd?")).contains(QStringLiteral("CLdj"));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef R2SOURCEDECOMPILER_H
|
||||
#define R2SOURCEDECOMPILER_H
|
||||
|
||||
#include "Decompiler.h"
|
||||
|
||||
class R2SourceDecompiler : public R2JsonDecompiler
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit R2SourceDecompiler(QObject *parent = nullptr);
|
||||
|
||||
static bool isAvailable();
|
||||
};
|
||||
|
||||
#endif // R2SOURCEDECOMPILER_H
|
||||
@@ -25,6 +25,13 @@
|
||||
#error "iaito requires radare2 6.0.0 or newer"
|
||||
#endif
|
||||
|
||||
// The complete generic RBin resource API is available starting with ABI 117.
|
||||
#if defined(R2_ABIVERSION) && R2_ABIVERSION >= 117
|
||||
#define IAITO_HAS_R_BIN_RESOURCES 1
|
||||
#else
|
||||
#define IAITO_HAS_R_BIN_RESOURCES 0
|
||||
#endif
|
||||
|
||||
#ifdef IAITO_RESTORE_QT_EMIT
|
||||
#pragma pop_macro("emit")
|
||||
#undef IAITO_RESTORE_QT_EMIT
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
#include "SamplesDB.h"
|
||||
|
||||
#include <r_muta.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMutex>
|
||||
#include <QRunnable>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QThreadPool>
|
||||
|
||||
namespace {
|
||||
|
||||
// Heap-allocated and never freed: a process-lifetime mutex so a background
|
||||
// worker that is still finishing during shutdown can never lock a destroyed
|
||||
// object (function-local statics are destroyed at exit).
|
||||
QMutex &dbMutex()
|
||||
{
|
||||
static QMutex *m = new QMutex();
|
||||
return *m;
|
||||
}
|
||||
|
||||
QThreadPool &pool()
|
||||
{
|
||||
static QThreadPool *p = [] {
|
||||
auto *tp = new QThreadPool();
|
||||
tp->setMaxThreadCount(2);
|
||||
return tp;
|
||||
}();
|
||||
return *p;
|
||||
}
|
||||
|
||||
std::atomic<bool> g_shutdown{false};
|
||||
|
||||
class FnRunnable : public QRunnable
|
||||
{
|
||||
public:
|
||||
explicit FnRunnable(std::function<void()> fn)
|
||||
: m_fn(std::move(fn))
|
||||
{
|
||||
setAutoDelete(true);
|
||||
}
|
||||
void run() override { m_fn(); }
|
||||
|
||||
private:
|
||||
std::function<void()> m_fn;
|
||||
};
|
||||
|
||||
bool isValidSha256(const QString &h)
|
||||
{
|
||||
if (h.size() != 64) {
|
||||
return false;
|
||||
}
|
||||
for (const QChar &c : h) {
|
||||
if (!((c >= QLatin1Char('0') && c <= QLatin1Char('9'))
|
||||
|| (c >= QLatin1Char('a') && c <= QLatin1Char('f')))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// samples/<h0:2>/<h2:4>/<h0:4> bucket file holding "<sha256> <path>" lines.
|
||||
QString bucketFile(const QString &h)
|
||||
{
|
||||
return QStringLiteral("%1/%2/%3/%4")
|
||||
.arg(SamplesDB::dbRoot(), h.mid(0, 2), h.mid(2, 2), h.mid(0, 4));
|
||||
}
|
||||
|
||||
QList<QPair<QString, QString>> readBucketFile(const QString &file)
|
||||
{
|
||||
QList<QPair<QString, QString>> out;
|
||||
QFile f(file);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return out;
|
||||
}
|
||||
const QList<QByteArray> lines = f.readAll().split('\n');
|
||||
for (const QByteArray &raw : lines) {
|
||||
QString line = QString::fromUtf8(raw);
|
||||
// Strip only the line terminator; paths may legitimately end in spaces.
|
||||
while (line.endsWith(QLatin1Char('\r'))) {
|
||||
line.chop(1);
|
||||
}
|
||||
const int sp = line.indexOf(QLatin1Char(' '));
|
||||
if (sp <= 0) {
|
||||
continue;
|
||||
}
|
||||
const QString path = line.mid(sp + 1);
|
||||
if (!path.isEmpty()) {
|
||||
out.append({line.left(sp), path});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool writeBucketFile(const QString &file, const QList<QPair<QString, QString>> &entries)
|
||||
{
|
||||
if (entries.isEmpty()) {
|
||||
return QFile::remove(file) || !QFile::exists(file);
|
||||
}
|
||||
QDir().mkpath(QFileInfo(file).absolutePath());
|
||||
// QSaveFile writes to a temp file and atomically renames over the target on
|
||||
// commit, so the live bucket is never truncated mid-write.
|
||||
QSaveFile f(file);
|
||||
if (!f.open(QIODevice::WriteOnly)) {
|
||||
return false;
|
||||
}
|
||||
QByteArray data;
|
||||
for (const auto &e : entries) {
|
||||
data += e.first.toUtf8();
|
||||
data += ' ';
|
||||
data += e.second.toUtf8();
|
||||
data += '\n';
|
||||
}
|
||||
if (f.write(data) != data.size()) {
|
||||
f.cancelWriting();
|
||||
return false;
|
||||
}
|
||||
return f.commit();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace SamplesDB {
|
||||
|
||||
QString dbRoot()
|
||||
{
|
||||
return QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))
|
||||
.filePath(QStringLiteral("samples"));
|
||||
}
|
||||
|
||||
QString sha256File(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return QString();
|
||||
}
|
||||
RMuta *muta = r_muta_new();
|
||||
if (!muta) {
|
||||
return QString();
|
||||
}
|
||||
RMutaSession *session = r_muta_use(muta, "sha256");
|
||||
if (!session) {
|
||||
r_muta_free(muta);
|
||||
return QString();
|
||||
}
|
||||
|
||||
const qint64 blockSize = 1024 * 1024;
|
||||
QByteArray buf(blockSize, Qt::Uninitialized);
|
||||
bool ok = true;
|
||||
bool readAny = false;
|
||||
qint64 n;
|
||||
while ((n = f.read(buf.data(), blockSize)) > 0) {
|
||||
readAny = true;
|
||||
// Abort promptly on shutdown so a huge in-flight hash can't stall exit.
|
||||
if (g_shutdown.load()) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (!r_muta_session_update(
|
||||
session, reinterpret_cast<const ut8 *>(buf.constData()), static_cast<int>(n))) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n < 0) {
|
||||
ok = false;
|
||||
}
|
||||
QString hex;
|
||||
if (ok) {
|
||||
if (readAny) {
|
||||
ok = r_muta_session_end(session, nullptr, 0);
|
||||
if (ok) {
|
||||
int digestSize = 0;
|
||||
ut8 *digest = r_muta_session_get_output(session, &digestSize);
|
||||
if (digest && digestSize > 0) {
|
||||
hex = QString::fromLatin1(
|
||||
QByteArray(reinterpret_cast<const char *>(digest), digestSize).toHex());
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
std::free(digest);
|
||||
}
|
||||
} else {
|
||||
hex = QString::fromLatin1(
|
||||
QCryptographicHash::hash(QByteArray(), QCryptographicHash::Sha256).toHex());
|
||||
}
|
||||
}
|
||||
r_muta_session_free(session);
|
||||
r_muta_free(muta);
|
||||
return ok ? hex : QString();
|
||||
}
|
||||
|
||||
QString registerFile(const QString &path)
|
||||
{
|
||||
const QFileInfo fi(path);
|
||||
if (!fi.exists() || !fi.isFile()) {
|
||||
return QString();
|
||||
}
|
||||
const QString abs = fi.absoluteFilePath();
|
||||
if (abs.contains(QLatin1Char('\n')) || abs.contains(QLatin1Char('\r'))) {
|
||||
return QString();
|
||||
}
|
||||
const QString h = sha256File(abs);
|
||||
if (!isValidSha256(h)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QMutexLocker lock(&dbMutex());
|
||||
const QString file = bucketFile(h);
|
||||
auto entries = readBucketFile(file);
|
||||
bool found = false;
|
||||
for (auto &e : entries) {
|
||||
if (e.first == h) {
|
||||
if (e.second == abs) {
|
||||
return h;
|
||||
}
|
||||
e.second = abs;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
entries.append({h, abs});
|
||||
}
|
||||
return writeBucketFile(file, entries) ? h : QString();
|
||||
}
|
||||
|
||||
void registerFileAsync(const QString &path)
|
||||
{
|
||||
if (g_shutdown.load()) {
|
||||
return;
|
||||
}
|
||||
const QString p = path;
|
||||
pool().start(new FnRunnable([p]() { registerFile(p); }));
|
||||
}
|
||||
|
||||
void scanFolderAsync(const QString &dir, std::function<void()> onComplete)
|
||||
{
|
||||
if (g_shutdown.load()) {
|
||||
return;
|
||||
}
|
||||
const QString d = dir;
|
||||
pool().start(new FnRunnable([d, onComplete]() {
|
||||
const QString rootPrefix = dbRoot() + QLatin1Char('/');
|
||||
QDirIterator it(d, QDir::Files, QDirIterator::Subdirectories);
|
||||
while (it.hasNext() && !g_shutdown.load()) {
|
||||
const QString f = it.next();
|
||||
// Never register the database's own bucket files.
|
||||
if (QFileInfo(f).absoluteFilePath().startsWith(rootPrefix)) {
|
||||
continue;
|
||||
}
|
||||
registerFile(f);
|
||||
}
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void waitForPending()
|
||||
{
|
||||
g_shutdown.store(true);
|
||||
pool().clear();
|
||||
pool().waitForDone();
|
||||
}
|
||||
|
||||
QString pathForHash(const QString &sha256)
|
||||
{
|
||||
const QString h = sha256.toLower();
|
||||
if (!isValidSha256(h)) {
|
||||
return QString();
|
||||
}
|
||||
QMutexLocker lock(&dbMutex());
|
||||
const auto entries = readBucketFile(bucketFile(h));
|
||||
for (const auto &e : entries) {
|
||||
if (e.first == h) {
|
||||
return e.second;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool remove(const QString &sha256)
|
||||
{
|
||||
const QString h = sha256.toLower();
|
||||
if (!isValidSha256(h)) {
|
||||
return false;
|
||||
}
|
||||
QMutexLocker lock(&dbMutex());
|
||||
const QString file = bucketFile(h);
|
||||
const auto entries = readBucketFile(file);
|
||||
QList<QPair<QString, QString>> kept;
|
||||
for (const auto &e : entries) {
|
||||
if (e.first != h) {
|
||||
kept.append(e);
|
||||
}
|
||||
}
|
||||
if (kept.size() == entries.size()) {
|
||||
return false;
|
||||
}
|
||||
return writeBucketFile(file, kept);
|
||||
}
|
||||
|
||||
QList<QPair<QString, QString>> all()
|
||||
{
|
||||
QList<QPair<QString, QString>> out;
|
||||
QMutexLocker lock(&dbMutex());
|
||||
QDirIterator it(dbRoot(), QDir::Files, QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
out += readBucketFile(it.next());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace SamplesDB
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef IAITO_SAMPLESDB_H
|
||||
#define IAITO_SAMPLESDB_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
|
||||
// On-disk database mapping sha256 <-> absolute file path. Entries are sharded
|
||||
// git/rainbow-table style across two hash-prefix directory levels, each leaf
|
||||
// being a plain text file with "<sha256> <path>" lines. See doc/deeplinks.md.
|
||||
namespace SamplesDB {
|
||||
|
||||
// Absolute path of the database root (lazily created).
|
||||
QString dbRoot();
|
||||
|
||||
// Computes the lowercase hex sha256 of a file using radare2's r_muta API in
|
||||
// fixed-size blocks (bounded memory for huge files). Returns an
|
||||
// empty string on failure.
|
||||
QString sha256File(const QString &path);
|
||||
|
||||
// Hashes path and stores the sha256 -> absolute path mapping (latest path wins).
|
||||
// Idempotent. Returns the computed sha256, or an empty string on failure.
|
||||
QString registerFile(const QString &path);
|
||||
|
||||
// registerFile on a background worker (hashing must not block the UI thread).
|
||||
void registerFileAsync(const QString &path);
|
||||
|
||||
// Recursively registers every file under dir on a background worker, skipping
|
||||
// the database's own storage. onComplete (if set) runs on the worker thread
|
||||
// once the scan finishes; marshal back to the UI thread yourself.
|
||||
void scanFolderAsync(const QString &dir, std::function<void()> onComplete);
|
||||
|
||||
// Stops accepting new work and blocks until pending workers finish. Call once
|
||||
// on application shutdown so no worker outlives the process.
|
||||
void waitForPending();
|
||||
|
||||
// Returns the path registered for sha256, or an empty string if unknown.
|
||||
QString pathForHash(const QString &sha256);
|
||||
|
||||
// Removes the entry for sha256. Returns true if something was removed.
|
||||
bool remove(const QString &sha256);
|
||||
|
||||
// Returns every {sha256, path} pair stored in the database.
|
||||
QList<QPair<QString, QString>> all();
|
||||
|
||||
} // namespace SamplesDB
|
||||
|
||||
#endif // IAITO_SAMPLESDB_H
|
||||
@@ -53,8 +53,7 @@ QList<QTextEdit::ExtraSelection> createHighlightSelections(
|
||||
highlightSelection.cursor = document->find(text, highlightSelection.cursor);
|
||||
|
||||
if (!highlightSelection.cursor.isNull()) {
|
||||
highlightSelection.format.setBackground(QColor(0xe8, 0xbb, 0x32, 0xc0));
|
||||
highlightSelection.format.setForeground(QColor(Qt::black));
|
||||
highlightSelection.format.setBackground(ConfigColor("wordHighlight"));
|
||||
selections.append(highlightSelection);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,109 @@
|
||||
#ifndef SHORTCUTMANAGER_H
|
||||
#define SHORTCUTMANAGER_H
|
||||
|
||||
#include "core/IaitoCommon.h"
|
||||
#include <QHash>
|
||||
#include <QKeySequence>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QSettings>
|
||||
#include <QVector>
|
||||
|
||||
#define ShortcutMgr() (ShortcutManager::instance())
|
||||
|
||||
class QShortcut;
|
||||
class QAction;
|
||||
class QWidget;
|
||||
class QKeyEvent;
|
||||
|
||||
enum class ShortcutScope {
|
||||
Global,
|
||||
Disassembly,
|
||||
Decompiler,
|
||||
Hexdump,
|
||||
Graph,
|
||||
GraphOverview,
|
||||
Console,
|
||||
AddressableList,
|
||||
Debug,
|
||||
Overview,
|
||||
};
|
||||
|
||||
struct ShortcutConflict
|
||||
{
|
||||
QString idA;
|
||||
QString idB;
|
||||
QKeySequence sequence;
|
||||
};
|
||||
|
||||
struct CommandShortcut
|
||||
{
|
||||
QKeySequence key;
|
||||
QString command;
|
||||
bool needsInput = false;
|
||||
};
|
||||
|
||||
class IAITO_EXPORT ShortcutManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static ShortcutManager *instance();
|
||||
|
||||
void load();
|
||||
|
||||
QStringList ids() const;
|
||||
static QList<ShortcutScope> scopes();
|
||||
QStringList idsForScope(ShortcutScope scope) const;
|
||||
static QString scopeName(ShortcutScope scope);
|
||||
QString displayName(const QString &id) const;
|
||||
ShortcutScope scope(const QString &id) const;
|
||||
bool contains(const QString &id) const;
|
||||
|
||||
QList<QKeySequence> sequences(const QString &id) const;
|
||||
QKeySequence sequence(const QString &id) const;
|
||||
QList<QKeySequence> defaults(const QString &id) const;
|
||||
bool isOverridden(const QString &id) const;
|
||||
|
||||
void setSequences(const QString &id, const QList<QKeySequence> &seqs);
|
||||
void setSequence(const QString &id, const QKeySequence &seq);
|
||||
void resetToDefault(const QString &id);
|
||||
void resetAll();
|
||||
|
||||
QShortcut *registerShortcut(
|
||||
const QString &id, QWidget *parent, Qt::ShortcutContext context = Qt::WindowShortcut);
|
||||
void bindAction(const QString &id, QAction *action);
|
||||
|
||||
bool matches(const QString &id, const QKeyEvent *event) const;
|
||||
|
||||
QList<ShortcutConflict> conflicts() const;
|
||||
|
||||
QVector<CommandShortcut> commandShortcuts() const;
|
||||
void setCommandShortcuts(const QVector<CommandShortcut> &list);
|
||||
int matchCommandShortcut(const QKeyEvent *event) const;
|
||||
void migrateLegacyCommandKeys();
|
||||
|
||||
signals:
|
||||
void shortcutChanged(const QString &id);
|
||||
void shortcutsReset();
|
||||
void commandShortcutsChanged();
|
||||
|
||||
private:
|
||||
explicit ShortcutManager(QObject *parent = nullptr);
|
||||
void applyTo(const QString &id);
|
||||
void persist(const QString &id);
|
||||
|
||||
struct Binding
|
||||
{
|
||||
QPointer<QShortcut> shortcut;
|
||||
QPointer<QAction> action;
|
||||
int seqIndex = 0;
|
||||
};
|
||||
|
||||
QSettings s;
|
||||
QHash<QString, QList<QKeySequence>> m_overrides;
|
||||
QHash<QString, QVector<Binding>> m_bindings;
|
||||
QVector<CommandShortcut> m_commandShortcuts;
|
||||
};
|
||||
|
||||
#endif // SHORTCUTMANAGER_H
|
||||
@@ -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
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,70 @@
|
||||
#ifndef IAITO_STYLE_H
|
||||
#define IAITO_STYLE_H
|
||||
|
||||
#include "Theme.h"
|
||||
|
||||
#include <QCommonStyle>
|
||||
#include <QList>
|
||||
#include <QPointer>
|
||||
|
||||
class QTimer;
|
||||
|
||||
class IaitoStyle : public QCommonStyle
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit IaitoStyle();
|
||||
|
||||
static IaitoStyle *instance();
|
||||
|
||||
void setTheme(const Theme &theme);
|
||||
const Theme &theme() const { return m_theme; }
|
||||
|
||||
int styleHint(
|
||||
StyleHint hint,
|
||||
const QStyleOption *option = nullptr,
|
||||
const QWidget *widget = nullptr,
|
||||
QStyleHintReturn *returnData = nullptr) const override;
|
||||
|
||||
void drawPrimitive(
|
||||
PrimitiveElement element,
|
||||
const QStyleOption *option,
|
||||
QPainter *painter,
|
||||
const QWidget *widget = nullptr) const override;
|
||||
|
||||
void drawControl(
|
||||
ControlElement element,
|
||||
const QStyleOption *option,
|
||||
QPainter *painter,
|
||||
const QWidget *widget = nullptr) const override;
|
||||
|
||||
void drawComplexControl(
|
||||
ComplexControl control,
|
||||
const QStyleOptionComplex *option,
|
||||
QPainter *painter,
|
||||
const QWidget *widget = nullptr) const override;
|
||||
|
||||
int pixelMetric(
|
||||
PixelMetric metric,
|
||||
const QStyleOption *option = nullptr,
|
||||
const QWidget *widget = nullptr) const override;
|
||||
|
||||
QSize sizeFromContents(
|
||||
ContentsType type,
|
||||
const QStyleOption *option,
|
||||
const QSize &contentsSize,
|
||||
const QWidget *widget = nullptr) const override;
|
||||
|
||||
void polish(QWidget *widget) override;
|
||||
void unpolish(QWidget *widget) override;
|
||||
|
||||
private:
|
||||
static IaitoStyle *s_instance;
|
||||
Theme m_theme;
|
||||
QTimer *m_busyTimer;
|
||||
mutable QList<QPointer<QWidget>> m_busyBars;
|
||||
int m_busyPhase = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,353 @@
|
||||
#include "Theme.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
|
||||
QColor ColorGroup::forState(QStyle::State state) const
|
||||
{
|
||||
if (!(state & QStyle::State_Enabled)) {
|
||||
return disabled;
|
||||
}
|
||||
if (state & QStyle::State_Sunken) {
|
||||
return pressed;
|
||||
}
|
||||
if (state & QStyle::State_On) {
|
||||
return checked;
|
||||
}
|
||||
if (state & QStyle::State_MouseOver) {
|
||||
return hovered;
|
||||
}
|
||||
if (state & QStyle::State_HasFocus) {
|
||||
return focused;
|
||||
}
|
||||
return normal;
|
||||
}
|
||||
|
||||
static ColorGroup deriveGroup(const QColor &base, bool dark)
|
||||
{
|
||||
ColorGroup g;
|
||||
g.normal = base;
|
||||
g.disabled = base;
|
||||
if (dark) {
|
||||
g.hovered = base.lighter(120);
|
||||
g.pressed = base.darker(115);
|
||||
g.checked = base.lighter(135);
|
||||
g.focused = base.lighter(110);
|
||||
} else {
|
||||
g.hovered = base.darker(108);
|
||||
g.pressed = base.darker(120);
|
||||
g.checked = base.darker(112);
|
||||
g.focused = base.darker(105);
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
Theme Theme::fromChrome(const QHash<QString, QColor> &v, bool dark)
|
||||
{
|
||||
Theme t;
|
||||
t.dark = dark;
|
||||
t.background = deriveGroup(v.value("palette/background"), dark);
|
||||
t.panel = deriveGroup(v.value("palette/panel"), dark);
|
||||
t.surface = deriveGroup(v.value("palette/surface"), dark);
|
||||
t.border = deriveGroup(v.value("palette/border"), dark);
|
||||
t.text = deriveGroup(v.value("palette/text"), dark);
|
||||
t.mutedText = deriveGroup(v.value("palette/mutedText"), dark);
|
||||
t.accent = deriveGroup(v.value("palette/accent"), dark);
|
||||
t.accentText = deriveGroup(v.value("palette/accentText"), dark);
|
||||
|
||||
const QColor muted = v.value("palette/mutedText");
|
||||
t.text.disabled = muted;
|
||||
t.accentText.disabled = muted;
|
||||
t.surface.disabled = t.surface.normal;
|
||||
t.border.disabled = muted;
|
||||
|
||||
const QColor face = t.surface.normal;
|
||||
t.bevelHighlight = face.lighter(160);
|
||||
t.bevelLight = face.lighter(115);
|
||||
t.bevelDark = face.darker(160);
|
||||
t.bevelShadow = face.darker(320);
|
||||
|
||||
t.tooltipBase = t.surface.normal;
|
||||
t.tooltipText = t.text.normal;
|
||||
return t;
|
||||
}
|
||||
|
||||
Theme Theme::loadBuiltin(const QString &name)
|
||||
{
|
||||
const QString path = QStringLiteral(":/themes/engine/") + name + QStringLiteral(".theme");
|
||||
if (QFile::exists(path)) {
|
||||
return load(path, true);
|
||||
}
|
||||
QHash<QString, QColor> v
|
||||
= {{"palette/background", QColor(0x31, 0x36, 0x3b)},
|
||||
{"palette/panel", QColor(0x23, 0x26, 0x29)},
|
||||
{"palette/surface", QColor(0x31, 0x36, 0x3b)},
|
||||
{"palette/border", QColor(0x4f, 0x56, 0x5c)},
|
||||
{"palette/text", QColor(0xef, 0xf0, 0xf1)},
|
||||
{"palette/mutedText", QColor(0x80, 0x80, 0x80)},
|
||||
{"palette/accent", QColor(0x3d, 0xae, 0xe9)},
|
||||
{"palette/accentText", QColor(0xff, 0xff, 0xff)}};
|
||||
Theme t = fromChrome(v, true);
|
||||
t.name = name;
|
||||
return t;
|
||||
}
|
||||
|
||||
Theme Theme::fromPalette(const QPalette &p, bool dark)
|
||||
{
|
||||
QHash<QString, QColor> v
|
||||
= {{"palette/background", p.color(QPalette::Active, QPalette::Window)},
|
||||
{"palette/panel", p.color(QPalette::Active, QPalette::Base)},
|
||||
{"palette/surface", p.color(QPalette::Active, QPalette::Button)},
|
||||
{"palette/border", p.color(QPalette::Active, QPalette::Mid)},
|
||||
{"palette/text", p.color(QPalette::Active, QPalette::WindowText)},
|
||||
{"palette/mutedText", p.color(QPalette::Disabled, QPalette::WindowText)},
|
||||
{"palette/accent", p.color(QPalette::Active, QPalette::Highlight)},
|
||||
{"palette/accentText", p.color(QPalette::Active, QPalette::HighlightedText)}};
|
||||
Theme t = fromChrome(v, dark);
|
||||
t.name = QStringLiteral("Native");
|
||||
return t;
|
||||
}
|
||||
|
||||
Theme Theme::iaitoDefault()
|
||||
{
|
||||
if (qApp) {
|
||||
const QPalette p = qApp->palette();
|
||||
const bool dark = p.color(QPalette::Active, QPalette::Base).lightnessF()
|
||||
< p.color(QPalette::Active, QPalette::WindowText).lightnessF();
|
||||
return fromPalette(p, dark);
|
||||
}
|
||||
return loadBuiltin(QStringLiteral("Dark"));
|
||||
}
|
||||
|
||||
Theme Theme::builtin(const QString &name)
|
||||
{
|
||||
return loadBuiltin(name);
|
||||
}
|
||||
|
||||
QHash<QString, QColor> Theme::toChrome() const
|
||||
{
|
||||
return {
|
||||
{"palette/background", background.normal},
|
||||
{"palette/panel", panel.normal},
|
||||
{"palette/surface", surface.normal},
|
||||
{"palette/border", border.normal},
|
||||
{"palette/text", text.normal},
|
||||
{"palette/mutedText", mutedText.normal},
|
||||
{"palette/accent", accent.normal},
|
||||
{"palette/accentText", accentText.normal}};
|
||||
}
|
||||
|
||||
QPalette Theme::toPalette(const QPalette &base) const
|
||||
{
|
||||
QPalette p = base;
|
||||
struct Map
|
||||
{
|
||||
QPalette::ColorRole role;
|
||||
const ColorGroup *group;
|
||||
};
|
||||
const Map table[]
|
||||
= {{QPalette::Window, &background},
|
||||
{QPalette::WindowText, &text},
|
||||
{QPalette::Base, &panel},
|
||||
{QPalette::AlternateBase, &panel},
|
||||
{QPalette::Text, &text},
|
||||
{QPalette::Button, &surface},
|
||||
{QPalette::ButtonText, &text},
|
||||
{QPalette::Highlight, &accent},
|
||||
{QPalette::HighlightedText, &accentText},
|
||||
{QPalette::PlaceholderText, &mutedText}};
|
||||
for (const Map &m : table) {
|
||||
p.setColor(QPalette::Active, m.role, m.group->normal);
|
||||
p.setColor(QPalette::Inactive, m.role, m.group->normal);
|
||||
p.setColor(QPalette::Disabled, m.role, m.group->disabled);
|
||||
}
|
||||
for (QPalette::ColorGroup g : {QPalette::Active, QPalette::Inactive, QPalette::Disabled}) {
|
||||
p.setColor(g, QPalette::ToolTipBase, tooltipBase);
|
||||
p.setColor(g, QPalette::ToolTipText, tooltipText);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const struct
|
||||
{
|
||||
const char *key;
|
||||
int ThemeMetrics::*field;
|
||||
} kMetricFields[] = {
|
||||
{"metrics/radius", &ThemeMetrics::borderRadius},
|
||||
{"metrics/lineWidth", &ThemeMetrics::borderWidth},
|
||||
{"metrics/focusWidth", &ThemeMetrics::focusWidth},
|
||||
{"metrics/spacing", &ThemeMetrics::spacing},
|
||||
{"metrics/iconSize", &ThemeMetrics::iconSize},
|
||||
{"metrics/sliderGroove", &ThemeMetrics::sliderGrooveHeight},
|
||||
{"controls/height", &ThemeMetrics::controlHeight},
|
||||
{"controls/checkbox", &ThemeMetrics::checkBoxSize},
|
||||
{"controls/checkboxRadius", &ThemeMetrics::checkBoxRadius},
|
||||
{"controls/menuPadding", &ThemeMetrics::menuItemPadding},
|
||||
{"scrollbar/thickness", &ThemeMetrics::scrollBarThickness},
|
||||
{"tabs/border", &ThemeMetrics::tabBorderWidth},
|
||||
{"tabs/indicator", &ThemeMetrics::tabIndicatorWidth},
|
||||
};
|
||||
|
||||
const struct
|
||||
{
|
||||
const char *key;
|
||||
QColor Theme::*field;
|
||||
} kOverrideColors[] = {
|
||||
{"bevel/highlight", &Theme::bevelHighlight},
|
||||
{"bevel/light", &Theme::bevelLight},
|
||||
{"bevel/dark", &Theme::bevelDark},
|
||||
{"bevel/shadow", &Theme::bevelShadow},
|
||||
{"tooltip/base", &Theme::tooltipBase},
|
||||
{"tooltip/text", &Theme::tooltipText},
|
||||
};
|
||||
|
||||
QString skinToString(Skin s)
|
||||
{
|
||||
return s == Skin::Bevel ? QStringLiteral("bevel") : QStringLiteral("modern");
|
||||
}
|
||||
|
||||
Skin skinFromString(const QString &s)
|
||||
{
|
||||
return s == QLatin1String("bevel") ? Skin::Bevel : Skin::Modern;
|
||||
}
|
||||
|
||||
QString chromeToString(ChromeStyle c)
|
||||
{
|
||||
switch (c) {
|
||||
case ChromeStyle::Accent:
|
||||
return QStringLiteral("accent");
|
||||
case ChromeStyle::AccentTabs:
|
||||
return QStringLiteral("accent-tabs");
|
||||
case ChromeStyle::Flat:
|
||||
return QStringLiteral("flat");
|
||||
}
|
||||
return QStringLiteral("flat");
|
||||
}
|
||||
|
||||
ChromeStyle chromeFromString(const QString &s)
|
||||
{
|
||||
if (s == QLatin1String("accent")) {
|
||||
return ChromeStyle::Accent;
|
||||
}
|
||||
if (s == QLatin1String("accent-tabs")) {
|
||||
return ChromeStyle::AccentTabs;
|
||||
}
|
||||
return ChromeStyle::Flat;
|
||||
}
|
||||
|
||||
QString hintingToString(int h)
|
||||
{
|
||||
switch (h) {
|
||||
case QFont::PreferNoHinting:
|
||||
return QStringLiteral("none");
|
||||
case QFont::PreferVerticalHinting:
|
||||
return QStringLiteral("vertical");
|
||||
case QFont::PreferFullHinting:
|
||||
return QStringLiteral("full");
|
||||
default:
|
||||
return QStringLiteral("default");
|
||||
}
|
||||
}
|
||||
|
||||
QFont::HintingPreference hintingFromString(const QString &s)
|
||||
{
|
||||
if (s == QLatin1String("none"))
|
||||
return QFont::PreferNoHinting;
|
||||
if (s == QLatin1String("vertical"))
|
||||
return QFont::PreferVerticalHinting;
|
||||
if (s == QLatin1String("full"))
|
||||
return QFont::PreferFullHinting;
|
||||
if (s == QLatin1String("default"))
|
||||
return QFont::PreferDefaultHinting;
|
||||
bool ok = false;
|
||||
const int v = s.toInt(&ok);
|
||||
return ok ? QFont::HintingPreference(v) : QFont::PreferDefaultHinting;
|
||||
}
|
||||
|
||||
QString colorString(const QColor &c)
|
||||
{
|
||||
return c.alpha() == 255 ? c.name(QColor::HexRgb) : c.name(QColor::HexArgb);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Theme::applyIni(const QString &path)
|
||||
{
|
||||
QSettings s(path, QSettings::IniFormat);
|
||||
for (const auto &e : kMetricFields) {
|
||||
metrics.*e.field = s.value(e.key, metrics.*e.field).toInt();
|
||||
}
|
||||
for (const auto &e : kOverrideColors) {
|
||||
const QColor c(s.value(e.key).toString());
|
||||
if (c.isValid()) {
|
||||
this->*e.field = c;
|
||||
}
|
||||
}
|
||||
skin = skinFromString(s.value("meta/skin", skinToString(skin)).toString());
|
||||
chromeStyle = chromeFromString(s.value("meta/chrome", chromeToString(chromeStyle)).toString());
|
||||
name = s.value("meta/name", name).toString();
|
||||
|
||||
const QString family = s.value("font/family").toString();
|
||||
if (!family.isEmpty()) {
|
||||
QFont f(family);
|
||||
const QString size = s.value("font/size").toString();
|
||||
if (size.endsWith(QLatin1String("px"))) {
|
||||
f.setPixelSize(size.left(size.size() - 2).toInt());
|
||||
} else if (size.endsWith(QLatin1String("pt"))) {
|
||||
f.setPointSizeF(size.left(size.size() - 2).toDouble());
|
||||
}
|
||||
f.setHintingPreference(hintingFromString(s.value("font/hinting").toString()));
|
||||
fonts.base = f;
|
||||
fonts.custom = true;
|
||||
}
|
||||
}
|
||||
|
||||
Theme Theme::load(const QString &path, bool dark)
|
||||
{
|
||||
QSettings s(path, QSettings::IniFormat);
|
||||
QHash<QString, QColor> vars = Theme().toChrome();
|
||||
const QStringList keys = vars.keys();
|
||||
for (const QString &key : keys) {
|
||||
const QColor c(s.value(key).toString());
|
||||
if (c.isValid()) {
|
||||
vars[key] = c;
|
||||
}
|
||||
}
|
||||
Theme t = fromChrome(vars, s.value("meta/dark", dark).toBool());
|
||||
t.applyIni(path);
|
||||
return t;
|
||||
}
|
||||
|
||||
void Theme::save(const QString &path) const
|
||||
{
|
||||
QSettings s(path, QSettings::IniFormat);
|
||||
const QHash<QString, QColor> chrome = toChrome();
|
||||
for (auto it = chrome.constBegin(); it != chrome.constEnd(); ++it) {
|
||||
s.setValue(it.key(), colorString(it.value()));
|
||||
}
|
||||
s.setValue("bevel/highlight", colorString(bevelHighlight));
|
||||
s.setValue("bevel/light", colorString(bevelLight));
|
||||
s.setValue("bevel/dark", colorString(bevelDark));
|
||||
s.setValue("bevel/shadow", colorString(bevelShadow));
|
||||
s.setValue("tooltip/base", colorString(tooltipBase));
|
||||
s.setValue("tooltip/text", colorString(tooltipText));
|
||||
for (const auto &e : kMetricFields) {
|
||||
s.setValue(e.key, metrics.*e.field);
|
||||
}
|
||||
s.setValue("meta/name", name);
|
||||
s.setValue("meta/dark", dark);
|
||||
s.setValue("meta/skin", skinToString(skin));
|
||||
s.setValue("meta/chrome", chromeToString(chromeStyle));
|
||||
if (fonts.custom) {
|
||||
s.setValue("font/family", fonts.base.family());
|
||||
s.setValue(
|
||||
"font/size",
|
||||
fonts.base.pixelSize() > 0
|
||||
? QString::number(fonts.base.pixelSize()) + QStringLiteral("px")
|
||||
: QString::number(fonts.base.pointSizeF()) + QStringLiteral("pt"));
|
||||
s.setValue("font/hinting", hintingToString(fonts.base.hintingPreference()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef IAITO_THEME_H
|
||||
#define IAITO_THEME_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QHash>
|
||||
#include <QPalette>
|
||||
#include <QString>
|
||||
#include <QStyle>
|
||||
|
||||
struct ColorGroup
|
||||
{
|
||||
QColor normal;
|
||||
QColor hovered;
|
||||
QColor pressed;
|
||||
QColor disabled;
|
||||
QColor checked;
|
||||
QColor focused;
|
||||
|
||||
QColor forState(QStyle::State state) const;
|
||||
};
|
||||
|
||||
struct ThemeMetrics
|
||||
{
|
||||
int borderRadius = 4;
|
||||
int checkBoxRadius = 4;
|
||||
int borderWidth = 1;
|
||||
int focusWidth = 2;
|
||||
int spacing = 6;
|
||||
int menuItemPadding = 6;
|
||||
int iconSize = 16;
|
||||
int controlHeight = 24;
|
||||
int checkBoxSize = 16;
|
||||
int scrollBarThickness = 12;
|
||||
int sliderGrooveHeight = 4;
|
||||
int tabBorderWidth = 0;
|
||||
int tabIndicatorWidth = 2;
|
||||
};
|
||||
|
||||
struct ThemeFonts
|
||||
{
|
||||
QFont base;
|
||||
bool custom = false;
|
||||
};
|
||||
|
||||
enum class ChromeStyle {
|
||||
Flat,
|
||||
Accent,
|
||||
AccentTabs,
|
||||
};
|
||||
|
||||
enum class Skin {
|
||||
Modern,
|
||||
Bevel,
|
||||
};
|
||||
|
||||
class Theme
|
||||
{
|
||||
public:
|
||||
ColorGroup background;
|
||||
ColorGroup panel;
|
||||
ColorGroup surface;
|
||||
ColorGroup border;
|
||||
ColorGroup text;
|
||||
ColorGroup mutedText;
|
||||
ColorGroup accent;
|
||||
ColorGroup accentText;
|
||||
|
||||
QColor bevelHighlight;
|
||||
QColor bevelLight;
|
||||
QColor bevelDark;
|
||||
QColor bevelShadow;
|
||||
|
||||
QColor tooltipBase;
|
||||
QColor tooltipText;
|
||||
|
||||
ThemeMetrics metrics;
|
||||
ThemeFonts fonts;
|
||||
ChromeStyle chromeStyle = ChromeStyle::Flat;
|
||||
Skin skin = Skin::Modern;
|
||||
bool dark = true;
|
||||
QString name;
|
||||
|
||||
static Theme fromChrome(const QHash<QString, QColor> &vars, bool dark);
|
||||
static Theme fromPalette(const QPalette &palette, bool dark);
|
||||
static Theme loadBuiltin(const QString &name);
|
||||
static Theme iaitoDefault();
|
||||
static Theme builtin(const QString &name);
|
||||
QHash<QString, QColor> toChrome() const;
|
||||
|
||||
QPalette toPalette(const QPalette &base) const;
|
||||
|
||||
static Theme load(const QString &path, bool dark);
|
||||
void save(const QString &path) const;
|
||||
void applyIni(const QString &path);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef DOCKBACKEND_H
|
||||
#define DOCKBACKEND_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <Qt>
|
||||
|
||||
class QDockWidget;
|
||||
class QTabBar;
|
||||
|
||||
// Abstract realization backend for the dock layout model. The DockManager owns
|
||||
// the structure (the node tree); the backend turns structural intent into a
|
||||
// concrete widget arrangement. QtDockBackend realizes it onto QMainWindow's
|
||||
// native docking; a future backend could use QSplitter/QStackedWidget instead
|
||||
// without touching the model, registry, drag, or MainWindow.
|
||||
class DockBackend
|
||||
{
|
||||
public:
|
||||
virtual ~DockBackend() = default;
|
||||
|
||||
virtual void addDock(QDockWidget *dock, Qt::DockWidgetArea area) = 0;
|
||||
virtual void splitDock(QDockWidget *first, QDockWidget *second, Qt::Orientation orientation) = 0;
|
||||
virtual void tabifyDock(QDockWidget *first, QDockWidget *second) = 0;
|
||||
virtual void removeDock(QDockWidget *dock) = 0;
|
||||
virtual void setDockFloating(QDockWidget *dock, bool floating) = 0;
|
||||
|
||||
virtual QList<QDockWidget *> tabifiedWith(QDockWidget *dock) const = 0;
|
||||
virtual Qt::DockWidgetArea areaOf(QDockWidget *dock) const = 0;
|
||||
virtual void resizeDocksTo(
|
||||
const QList<QDockWidget *> &docks, const QList<int> &sizes, Qt::Orientation orientation)
|
||||
= 0;
|
||||
|
||||
virtual QList<QTabBar *> dockTabBars() const = 0;
|
||||
virtual void setTabsOnTop(bool onTop) = 0;
|
||||
virtual void setGroupedDragging(bool enabled) = 0;
|
||||
|
||||
virtual QByteArray saveState() const = 0;
|
||||
virtual bool restoreState(const QByteArray &state) = 0;
|
||||
};
|
||||
|
||||
#endif // DOCKBACKEND_H
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,153 @@
|
||||
#ifndef DOCKMANAGER_H
|
||||
#define DOCKMANAGER_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QPoint>
|
||||
#include <QPointer>
|
||||
#include <QRect>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
class MainWindow;
|
||||
class DockBackend;
|
||||
class IaitoDockWidget;
|
||||
class QDockWidget;
|
||||
class QTabBar;
|
||||
class QToolButton;
|
||||
class QMouseEvent;
|
||||
class QEvent;
|
||||
class QWidget;
|
||||
|
||||
// Where a dragged panel would land relative to the panel under the cursor.
|
||||
enum class DockDropKind {
|
||||
None,
|
||||
Float,
|
||||
Tabify,
|
||||
SplitLeft,
|
||||
SplitRight,
|
||||
SplitTop,
|
||||
SplitBottom,
|
||||
};
|
||||
|
||||
struct DockDropPlan
|
||||
{
|
||||
IaitoDockWidget *target = nullptr;
|
||||
DockDropKind kind = DockDropKind::None;
|
||||
IaitoDockWidget *previewHost = nullptr;
|
||||
QRect region; // previewHost-local coords, for the preview overlay
|
||||
};
|
||||
|
||||
// Owns the live set of dock panels and the homegrown chrome/drag layer that
|
||||
// used to live inline in MainWindow. This is the seam through which all docking
|
||||
// behavior is funneled, so the structure can later be backed by an explicit
|
||||
// layout model without touching MainWindow.
|
||||
class DockManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DockManager(MainWindow *mainWindow);
|
||||
~DockManager() override;
|
||||
|
||||
const QList<IaitoDockWidget *> &docks() const { return dockWidgets; }
|
||||
const QList<IaitoDockWidget *> &pluginDockWidgets() const { return pluginDocks; }
|
||||
void addWidget(IaitoDockWidget *widget);
|
||||
void removeWidget(IaitoDockWidget *widget);
|
||||
void addPluginDock(IaitoDockWidget *widget) { pluginDocks.push_back(widget); }
|
||||
QString getUniqueObjectName(const QString &widgetType) const;
|
||||
QStringList getPanelNames() const;
|
||||
bool focusPanelByName(const QString &name);
|
||||
|
||||
QMap<QString, std::function<IaitoDockWidget *(MainWindow *)>> &constructorMap()
|
||||
{
|
||||
return widgetTypeToConstructorMap;
|
||||
}
|
||||
|
||||
void applyDockPanelChrome();
|
||||
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);
|
||||
|
||||
signals:
|
||||
void panelAdded(IaitoDockWidget *widget);
|
||||
void panelRemoved(IaitoDockWidget *widget);
|
||||
// Emitted whenever the docked arrangement may have changed (add/remove,
|
||||
// visibility, float/unfloat) so the owner can re-check layout invariants.
|
||||
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 updateDockTabCloseButtons(QTabBar *tabBar, int hoveredIndex = -1);
|
||||
QToolButton *dockTabCloseButton(QTabBar *tabBar);
|
||||
void closeDockTab(QTabBar *tabBar, int index);
|
||||
void showDockContextMenu(QWidget *parent, IaitoDockWidget *dock, const QPoint &globalPos);
|
||||
void detachDockTab(IaitoDockWidget *dock, const QPoint &globalPos);
|
||||
void placeDockTab(IaitoDockWidget *dock, DockDropKind kind);
|
||||
void refreshNativeDockLayout();
|
||||
IaitoDockWidget *dockSplitReference(IaitoDockWidget *dock) const;
|
||||
bool dockHasOpenTab(IaitoDockWidget *dock) const;
|
||||
bool rebuildMacDockLayoutForSplit(
|
||||
IaitoDockWidget *dock, IaitoDockWidget *targetDock, Qt::Orientation orientation);
|
||||
bool splitDockRelativeTo(
|
||||
IaitoDockWidget *dock,
|
||||
IaitoDockWidget *targetDock,
|
||||
Qt::Orientation orientation,
|
||||
IaitoDockWidget *restoreTabHost);
|
||||
IaitoDockWidget *dockForDockTab(QTabBar *tabBar, int index) const;
|
||||
IaitoDockWidget *dockForDockDragHandle(QWidget *handle) const;
|
||||
bool showDockHandleContextMenu(QWidget *handle, const QPoint &globalPos);
|
||||
bool maybeStartDockTabDrag(QTabBar *tabBar, QMouseEvent *event);
|
||||
bool maybeStartDockHandleDrag(QWidget *handle, QMouseEvent *event);
|
||||
void startDockWidgetDrag(IaitoDockWidget *dock, const QPoint &globalPos, const QPoint &offset);
|
||||
bool updateDockTabDrag(QMouseEvent *event);
|
||||
DockDropPlan computeDropPlan(const QPoint &globalPos) const;
|
||||
void updateDropOverlay(const QPoint &globalPos);
|
||||
void finishDockTabDrag(const QPoint &globalPos);
|
||||
void clearDockDrag();
|
||||
|
||||
MainWindow *mainWindow;
|
||||
DockBackend *backend;
|
||||
|
||||
QList<IaitoDockWidget *> dockWidgets;
|
||||
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;
|
||||
QPointer<QWidget> dropOverlay;
|
||||
QPoint dockDragStartGlobalPos;
|
||||
QPoint dockDragOffset;
|
||||
bool dockTabDragActive = false;
|
||||
bool dockRelayoutInProgress = false;
|
||||
bool updateScheduled = false;
|
||||
};
|
||||
|
||||
#endif // DOCKMANAGER_H
|
||||
+135
-7
@@ -1,8 +1,73 @@
|
||||
#include "Iaito.h"
|
||||
#include "IaitoApplication.h"
|
||||
#include "common/Radare2Compat.h"
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
|
||||
static MainWindow *uiaitoMainWindow()
|
||||
{
|
||||
auto *app = qobject_cast<IaitoApplication *>(qApp);
|
||||
return app ? app->getMainWindow() : nullptr;
|
||||
}
|
||||
|
||||
static QString uiaitoStringArgument(const char *input)
|
||||
{
|
||||
QString arg = QString::fromUtf8(input).trimmed();
|
||||
if (arg.size() >= 2) {
|
||||
const QChar first = arg.at(0);
|
||||
const QChar last = arg.at(arg.size() - 1);
|
||||
if ((first == QLatin1Char('"') && last == QLatin1Char('"'))
|
||||
|| (first == QLatin1Char('\'') && last == QLatin1Char('\''))) {
|
||||
arg = arg.mid(1, arg.size() - 2).trimmed();
|
||||
}
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
||||
static bool uiaitoParseRangeArgument(const char *input, RVA *start, RVA *end, QString *error)
|
||||
{
|
||||
const QString args = QString::fromUtf8(input).simplified();
|
||||
const QStringList parts = args.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty() || parts.size() > 2) {
|
||||
*error = QStringLiteral("Usage: uis [address] ([length])");
|
||||
return false;
|
||||
}
|
||||
|
||||
const RVA address = Core()->math(parts.at(0));
|
||||
if (address == RVA_INVALID) {
|
||||
*error = QStringLiteral("Invalid address");
|
||||
return false;
|
||||
}
|
||||
|
||||
const RVA length = parts.size() == 2 ? Core()->math(parts.at(1)) : 1;
|
||||
if (length == 0 || length == RVA_INVALID) {
|
||||
*error = QStringLiteral("Invalid length");
|
||||
return false;
|
||||
}
|
||||
if (length - 1 > RVA_MAX - address) {
|
||||
*error = QStringLiteral("Selection range overflows");
|
||||
return false;
|
||||
}
|
||||
|
||||
*start = address;
|
||||
*end = address + length - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void uiaitoPrintUsage(RCore *core)
|
||||
{
|
||||
static const char *usage = "Usage: ui[..] [..args] - uiaito interactions\n"
|
||||
"| ui [message] - show popup dialog with given message\n"
|
||||
"| uid ([path]) - select directory and print it\n"
|
||||
"| uif ([path]) - select file and print it\n"
|
||||
"| uig [offset] - goto offset in UI\n"
|
||||
"| uip ([name]) - list panels or focus panel by name\n"
|
||||
"| uir - refresh UI contents\n"
|
||||
"| uis [addr] ([len]) - select byte range in UI\n";
|
||||
|
||||
r_cons_printf(core->cons, "%s", usage);
|
||||
}
|
||||
|
||||
static bool r2plugin_ui_call(RCorePluginSession *cps, const char *input)
|
||||
{
|
||||
RCore *core = cps->core;
|
||||
@@ -42,14 +107,77 @@ static bool r2plugin_ui_call(RCorePluginSession *cps, const char *input)
|
||||
r_cons_printf(core->cons, "%s\n", filename.toUtf8().constData());
|
||||
}
|
||||
} break;
|
||||
case 'g': {
|
||||
MainWindow *mainWindow = uiaitoMainWindow();
|
||||
if (!mainWindow) {
|
||||
r_core_return_code(core, 1);
|
||||
r_cons_printf(core->cons, "No iaito main window available\n");
|
||||
break;
|
||||
}
|
||||
|
||||
const QString offset = uiaitoStringArgument(input + 3);
|
||||
if (offset.isEmpty()) {
|
||||
r_core_return_code(core, 1);
|
||||
r_cons_printf(core->cons, "Usage: uig [offset]\n");
|
||||
break;
|
||||
}
|
||||
|
||||
mainWindow->gotoOffset(offset);
|
||||
r_core_return_code(core, 0);
|
||||
} break;
|
||||
case 's': {
|
||||
MainWindow *mainWindow = uiaitoMainWindow();
|
||||
if (!mainWindow) {
|
||||
r_core_return_code(core, 1);
|
||||
r_cons_printf(core->cons, "No iaito main window available\n");
|
||||
break;
|
||||
}
|
||||
|
||||
RVA start = RVA_INVALID;
|
||||
RVA end = RVA_INVALID;
|
||||
QString error;
|
||||
if (!uiaitoParseRangeArgument(input + 3, &start, &end, &error)) {
|
||||
r_core_return_code(core, 1);
|
||||
r_cons_printf(core->cons, "%s\n", error.toUtf8().constData());
|
||||
break;
|
||||
}
|
||||
|
||||
mainWindow->gotoOffset(RAddressString(start));
|
||||
Core()->setAddressRangeSelection(start, end);
|
||||
r_core_return_code(core, 0);
|
||||
} break;
|
||||
case 'r': {
|
||||
Core()->triggerRefreshAll();
|
||||
r_core_return_code(core, 0);
|
||||
} break;
|
||||
case 'p': {
|
||||
MainWindow *mainWindow = uiaitoMainWindow();
|
||||
if (!mainWindow) {
|
||||
r_core_return_code(core, 1);
|
||||
r_cons_printf(core->cons, "No iaito main window available\n");
|
||||
break;
|
||||
}
|
||||
|
||||
const QString panelName = uiaitoStringArgument(input + 3);
|
||||
if (panelName.isEmpty()) {
|
||||
const QStringList panelNames = mainWindow->getPanelNames();
|
||||
for (const QString &name : panelNames) {
|
||||
r_cons_printf(core->cons, "%s\n", name.toUtf8().constData());
|
||||
}
|
||||
r_core_return_code(core, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!mainWindow->focusPanelByName(panelName)) {
|
||||
r_core_return_code(core, 1);
|
||||
r_cons_printf(core->cons, "Panel not found: %s\n", panelName.toUtf8().constData());
|
||||
break;
|
||||
}
|
||||
|
||||
r_core_return_code(core, 0);
|
||||
} break;
|
||||
default:
|
||||
r_cons_printf(core->cons, "Usage: ui[..] [..args] - uiaito interactions\n");
|
||||
r_cons_printf(
|
||||
core->cons,
|
||||
"| ui [message] - show popup dialog with given "
|
||||
"message\n");
|
||||
r_cons_printf(core->cons, "| uid ([path]) - select directory and print it\n");
|
||||
r_cons_printf(core->cons, "| uif ([path]) - select file and print it\n");
|
||||
uiaitoPrintUsage(core);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
|
||||
+557
-36
@@ -4,6 +4,7 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QSaveFile>
|
||||
#include <QSet> // for deduplicating entrypoints by address
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
@@ -20,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"
|
||||
|
||||
@@ -176,14 +178,14 @@ static InstructionProbeResult probeInstructionAddress(RCore *core, RVA address,
|
||||
{
|
||||
InstructionProbeResult result;
|
||||
ut8 buf[64];
|
||||
int readSize = r_io_read_at(core->io, address, buf, probeSize);
|
||||
if (readSize < 1) {
|
||||
probeSize = qBound(1, probeSize, (int) sizeof(buf));
|
||||
if (!r_io_read_at(core->io, address, buf, probeSize)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
RAnalOp op;
|
||||
r_anal_op_init(&op);
|
||||
int ret = r_anal_op(core->anal, &op, address, buf, readSize, R_ARCH_OP_MASK_BASIC);
|
||||
int ret = r_anal_op(core->anal, &op, address, buf, probeSize, R_ARCH_OP_MASK_BASIC);
|
||||
result.size = op.size > 0 ? op.size : 1;
|
||||
result.valid = ret > 0 && op.size > 0 && op.type != R_ANAL_OP_TYPE_ILL;
|
||||
r_anal_op_fini(&op);
|
||||
@@ -775,8 +777,20 @@ bool IaitoCore::loadFile(
|
||||
r_config_set_i(core->config, "io.va", va);
|
||||
r_config_set_b(core->config, "bin.cache", bincache);
|
||||
|
||||
// An empty "map offset" field results in 0, but the callers that don't go
|
||||
// through the dialog may pass RVA_INVALID. Normalize both to "not given".
|
||||
const bool haveMapAddr = (mapaddr != 0 && mapaddr != RVA_INVALID);
|
||||
const ut64 realMapAddr = haveMapAddr ? mapaddr : 0;
|
||||
// Address where the contents of the file are mapped (bin.laddr).
|
||||
// Headerless files are mapped there as a whole, binaries with sections
|
||||
// are mapped by RBin and ignore it. When there's no map offset use the
|
||||
// base address, so a headerless file loaded at a given base address is
|
||||
// also mapped there instead of exposing its contents somewhere else.
|
||||
// (this is what r2 warns about with "Don't use -B on unknown files")
|
||||
const ut64 loadAddr = haveMapAddr ? realMapAddr : (baddr == RVA_INVALID ? 0 : baddr);
|
||||
|
||||
Core()->loadIaitoRC(0);
|
||||
RIODesc *f = core_file_open_strip_file_uri(core, path.toUtf8().constData(), perms, mapaddr);
|
||||
RIODesc *f = core_file_open_strip_file_uri(core, path.toUtf8().constData(), perms, loadAddr);
|
||||
if (!f) {
|
||||
R_LOG_ERROR("r_core_file_open failed");
|
||||
return false;
|
||||
@@ -788,7 +802,17 @@ bool IaitoCore::loadFile(
|
||||
|
||||
bool haveEntry = true;
|
||||
if (loadbin && va) {
|
||||
if (!r_core_bin_load(core, path.toUtf8().constData(), baddr)) {
|
||||
// When a map offset is given, RBin must use it as base address too.
|
||||
// Otherwise all the addresses coming from the bin object (strings,
|
||||
// symbols, sections, imports, ...) are relative to the base address
|
||||
// stored in the binary, while its contents are mapped somewhere
|
||||
// else, so every widget ends up pointing outside of the io maps.
|
||||
// r2 does something similar with `oba 0 <mapaddr>` when using -m.
|
||||
ut64 binAddr = baddr;
|
||||
if (haveMapAddr && baddr == RVA_INVALID) {
|
||||
binAddr = realMapAddr;
|
||||
}
|
||||
if (!r_core_bin_load(core, path.toUtf8().constData(), binAddr)) {
|
||||
R_LOG_ERROR("Cannot find rbin information");
|
||||
}
|
||||
|
||||
@@ -805,13 +829,10 @@ bool IaitoCore::loadFile(
|
||||
#endif
|
||||
} else {
|
||||
// Loading shellcodes here
|
||||
ut64 addr = mapaddr != 0 ? mapaddr : baddr;
|
||||
char *at = strdup(QString::number(addr).toUtf8().constData());
|
||||
r_core_cmdf(core, "'-e bin.laddr=%s", at);
|
||||
r_core_cmdf(core, "'om 3 %s", at);
|
||||
r_core_cmdf(core, "'s %s", at);
|
||||
r_core_cmdf(core, "'-e bin.laddr=0x%" PFMT64x, loadAddr);
|
||||
r_core_cmdf(core, "'om %d 0x%" PFMT64x, f->fd, loadAddr);
|
||||
r_core_cmdf(core, "'s 0x%" PFMT64x, loadAddr);
|
||||
haveEntry = false;
|
||||
free(at);
|
||||
}
|
||||
r_core_bin_export_info(core, R_MODE_SET);
|
||||
|
||||
@@ -822,8 +843,16 @@ bool IaitoCore::loadFile(
|
||||
*/
|
||||
auto debug = r_config_get_b(core->config, "cfg.debug");
|
||||
|
||||
if (haveEntry && !debug && r_flag_get(core->flags, "entry0")) {
|
||||
r_core_cmd0(core, "'s entry0");
|
||||
if (!debug) {
|
||||
if (haveMapAddr) {
|
||||
// like `r2 -m`, seek where the file has been mapped
|
||||
r_core_seek(core, realMapAddr, true);
|
||||
} else if (haveEntry && r_flag_get(core->flags, "entry0")) {
|
||||
r_core_cmd0(core, "'s entry0");
|
||||
} else if (loadAddr) {
|
||||
// no entrypoint, at least seek where the contents are
|
||||
r_core_seek(core, loadAddr, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (perms & R_PERM_W) {
|
||||
@@ -863,10 +892,14 @@ bool IaitoCore::tryFile(QString path, bool rw)
|
||||
bool IaitoCore::mapFile(QString path, RVA mapaddr)
|
||||
{
|
||||
CORE_LOCK();
|
||||
RVA addr = mapaddr != RVA_INVALID ? mapaddr : 0;
|
||||
ut64 baddr = Core()->getFileInfo().object()["bin"].toObject()["baddr"].toVariant().toULongLong();
|
||||
const bool haveMapAddr = (mapaddr != 0 && mapaddr != RVA_INVALID);
|
||||
const RVA addr = haveMapAddr ? mapaddr : 0;
|
||||
if (core_file_open_strip_file_uri(core, path.toUtf8().constData(), R_PERM_RX, addr)) {
|
||||
r_core_bin_load(core, path.toUtf8().constData(), baddr);
|
||||
// Load the bin information at the address where the file is mapped,
|
||||
// otherwise its strings, symbols and sections are shown at addresses
|
||||
// that don't match the contents of the io maps. Without a map address
|
||||
// let RBin use the base address defined by the file itself.
|
||||
r_core_bin_load(core, path.toUtf8().constData(), haveMapAddr ? addr : UT64_MAX);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -1154,10 +1187,6 @@ void IaitoCore::updateSeek()
|
||||
|
||||
void IaitoCore::setAddressRangeSelection(RVA start, RVA end)
|
||||
{
|
||||
if (!Config()->getAddressRangeSelectionSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (start == RVA_INVALID || end == RVA_INVALID || end < start) {
|
||||
clearAddressRangeSelection();
|
||||
return;
|
||||
@@ -1292,12 +1321,47 @@ RVA IaitoCore::getOffset()
|
||||
return ADDRESS_OF(core_);
|
||||
}
|
||||
|
||||
QString IaitoCore::getCommandAnnotation(RVA address, RVA *size)
|
||||
{
|
||||
CORE_LOCK();
|
||||
if (size) {
|
||||
*size = 0;
|
||||
}
|
||||
RAnalMetaItem *item = r_meta_get_at(core->anal, address, R_META_TYPE_RUN, size);
|
||||
return item && item->str ? QString::fromUtf8(item->str) : QString();
|
||||
}
|
||||
|
||||
void IaitoCore::setCommandAnnotation(RVA address, RVA size, const QString &command)
|
||||
{
|
||||
CORE_LOCK();
|
||||
// Store the command verbatim without passing it through the r2 command parser.
|
||||
r_meta_set(core->anal, R_META_TYPE_RUN, address, size, command.toUtf8().constData());
|
||||
emit refreshCodeViews();
|
||||
}
|
||||
|
||||
ut64 IaitoCore::math(const QString &expr)
|
||||
{
|
||||
CORE_LOCK();
|
||||
return r_num_math(core ? core->num : NULL, expr.toUtf8().constData());
|
||||
}
|
||||
|
||||
bool IaitoCore::tryMath(const QString &expr, ut64 &value)
|
||||
{
|
||||
const QByteArray expression = expr.trimmed().toUtf8();
|
||||
if (expression.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CORE_LOCK();
|
||||
RNum *number = core ? core->num : nullptr;
|
||||
if (!number) {
|
||||
return false;
|
||||
}
|
||||
const char *error = nullptr;
|
||||
value = r_num_math_err(number, expression.constData(), &error);
|
||||
return !error && !number->dbz && !r_num_failed(number);
|
||||
}
|
||||
|
||||
ut64 IaitoCore::num(const QString &expr)
|
||||
{
|
||||
CORE_LOCK();
|
||||
@@ -1557,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;
|
||||
}
|
||||
|
||||
@@ -3262,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();
|
||||
@@ -3283,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);
|
||||
@@ -3397,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;
|
||||
@@ -3513,9 +3615,15 @@ QList<RelocDescription> IaitoCore::getAllRelocs()
|
||||
if (core && core->bin && core->bin->cur && core->bin->cur->BO) {
|
||||
RBinReloc *br;
|
||||
auto relocs = core->bin->cur->BO->relocs;
|
||||
//// RBIter iter;
|
||||
if (!relocs) {
|
||||
return ret;
|
||||
}
|
||||
#if R2_ABIVERSION >= 136
|
||||
R_VEC_FOREACH(relocs, br)
|
||||
#else
|
||||
RRBNode *iter;
|
||||
r_crbtree_foreach(relocs, iter, RBinReloc, br)
|
||||
#endif
|
||||
{
|
||||
RelocDescription reloc;
|
||||
|
||||
@@ -3607,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();
|
||||
@@ -3767,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;
|
||||
@@ -4068,24 +4295,198 @@ QList<ResourcesDescription> IaitoCore::getAllResources()
|
||||
CORE_LOCK();
|
||||
QList<ResourcesDescription> resources;
|
||||
|
||||
QJsonArray resourcesArray = cmdj("iRj").array();
|
||||
for (const QJsonValue value : resourcesArray) {
|
||||
QJsonObject resourceObject = value.toObject();
|
||||
|
||||
#if IAITO_HAS_R_BIN_RESOURCES
|
||||
RBinFile *binFile = core && core->bin ? r_bin_cur(core->bin) : nullptr;
|
||||
RVecRBinResource *binResources = binFile ? r_bin_file_get_resources(binFile) : nullptr;
|
||||
if (binResources) {
|
||||
RBinResource *resource;
|
||||
R_VEC_FOREACH(binResources, resource)
|
||||
{
|
||||
ResourcesDescription res;
|
||||
res.name = QString::fromUtf8(resource->name ? resource->name : "");
|
||||
res.type = QString::fromUtf8(resource->type ? resource->type : "");
|
||||
res.language = QString::fromUtf8(resource->language ? resource->language : "");
|
||||
res.timestamp = QString::fromUtf8(resource->timestamp ? resource->timestamp : "");
|
||||
res.origin = QString::fromUtf8(resource->origin ? resource->origin : "");
|
||||
res.vaddr = resource->vaddr;
|
||||
res.paddr = resource->paddr;
|
||||
res.size = resource->size;
|
||||
res.id = resource->id;
|
||||
res.index = resource->index;
|
||||
res.typeId = resource->type_id;
|
||||
res.languageId = resource->language_id;
|
||||
res.codepage = resource->codepage;
|
||||
res.named = resource->named;
|
||||
res.hasExtendedMetadata = true;
|
||||
resources.append(res);
|
||||
}
|
||||
}
|
||||
#else
|
||||
const QJsonArray resourcesArray = cmdj(QStringLiteral("iRj")).array();
|
||||
for (const QJsonValue &value : resourcesArray) {
|
||||
const QJsonObject resourceObject = value.toObject();
|
||||
ResourcesDescription res;
|
||||
|
||||
res.name = resourceObject[RJsonKey::name].toString();
|
||||
res.vaddr = resourceObject[RJsonKey::vaddr].toVariant().toULongLong();
|
||||
res.index = resourceObject[RJsonKey::index].toVariant().toULongLong();
|
||||
res.index = resourceObject[RJsonKey::index].toVariant().toUInt();
|
||||
res.type = resourceObject[RJsonKey::type].toString();
|
||||
res.size = resourceObject[RJsonKey::size].toVariant().toULongLong();
|
||||
res.lang = resourceObject[RJsonKey::lang].toString();
|
||||
|
||||
resources << res;
|
||||
res.language = resourceObject[RJsonKey::lang].toString();
|
||||
res.timestamp = resourceObject[QStringLiteral("timestamp")].toString();
|
||||
resources.append(res);
|
||||
}
|
||||
#endif
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
bool IaitoCore::dumpResource(
|
||||
const ResourcesDescription &resource, const QString &fileName, QString *errorMessage)
|
||||
{
|
||||
#if IAITO_HAS_R_BIN_RESOURCES
|
||||
CORE_LOCK();
|
||||
RBinFile *binFile = core && core->bin ? r_bin_cur(core->bin) : nullptr;
|
||||
RVecRBinResource *binResources = binFile ? r_bin_file_get_resources(binFile) : nullptr;
|
||||
if (!binResources) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = tr("No binary resources are available.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RBinResource *binResource = nullptr;
|
||||
RBinResource *candidate;
|
||||
R_VEC_FOREACH(binResources, candidate)
|
||||
{
|
||||
if (candidate->index == resource.index && candidate->paddr == resource.paddr
|
||||
&& candidate->size == resource.size) {
|
||||
binResource = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!binResource) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = tr("The selected resource is no longer available.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#if R2_ABIVERSION >= 125
|
||||
RBuffer *buffer = r_bin_file_get_resource_data(binFile, binResource, false);
|
||||
#else
|
||||
RBuffer *buffer = r_bin_file_get_resource_data(binFile, binResource);
|
||||
#endif
|
||||
if (!buffer) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = tr("Unable to read the selected resource.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QSaveFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = file.errorString();
|
||||
}
|
||||
r_unref(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr int ChunkSize = 64 * 1024;
|
||||
QByteArray chunk(ChunkSize, '\0');
|
||||
const ut64 bufferSize = r_buf_size(buffer);
|
||||
ut64 offset = 0;
|
||||
while (offset < bufferSize) {
|
||||
const int requested = static_cast<int>(qMin<ut64>(ChunkSize, bufferSize - offset));
|
||||
const st64 readSize
|
||||
= r_buf_read_at(buffer, offset, reinterpret_cast<ut8 *>(chunk.data()), requested);
|
||||
if (readSize <= 0) {
|
||||
file.cancelWriting();
|
||||
if (errorMessage) {
|
||||
*errorMessage = tr("Unable to read the selected resource.");
|
||||
}
|
||||
r_unref(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
qint64 written = 0;
|
||||
while (written < readSize) {
|
||||
const qint64 writeSize = file.write(chunk.constData() + written, readSize - written);
|
||||
if (writeSize <= 0) {
|
||||
file.cancelWriting();
|
||||
if (errorMessage) {
|
||||
*errorMessage = file.errorString();
|
||||
}
|
||||
r_unref(buffer);
|
||||
return false;
|
||||
}
|
||||
written += writeSize;
|
||||
}
|
||||
offset += static_cast<ut64>(readSize);
|
||||
}
|
||||
|
||||
const bool committed = file.commit();
|
||||
if (!committed && errorMessage) {
|
||||
*errorMessage = file.errorString();
|
||||
}
|
||||
r_unref(buffer);
|
||||
return committed;
|
||||
#else
|
||||
CORE_LOCK();
|
||||
if (resource.vaddr == RVA_INVALID) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = tr("The selected resource is not readable.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QSaveFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = file.errorString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr int ChunkSize = 64 * 1024;
|
||||
QByteArray chunk(ChunkSize, '\0');
|
||||
ut64 offset = 0;
|
||||
while (offset < resource.size) {
|
||||
const int requested = static_cast<int>(qMin<ut64>(ChunkSize, resource.size - offset));
|
||||
if (!r_io_read_at(
|
||||
core->io,
|
||||
resource.vaddr + offset,
|
||||
reinterpret_cast<ut8 *>(chunk.data()),
|
||||
requested)) {
|
||||
file.cancelWriting();
|
||||
if (errorMessage) {
|
||||
*errorMessage = tr("Unable to read the selected resource.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
qint64 written = 0;
|
||||
while (written < requested) {
|
||||
const qint64 writeSize = file.write(chunk.constData() + written, requested - written);
|
||||
if (writeSize <= 0) {
|
||||
file.cancelWriting();
|
||||
if (errorMessage) {
|
||||
*errorMessage = file.errorString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
written += writeSize;
|
||||
}
|
||||
offset += static_cast<ut64>(requested);
|
||||
}
|
||||
|
||||
const bool committed = file.commit();
|
||||
if (!committed && errorMessage) {
|
||||
*errorMessage = file.errorString();
|
||||
}
|
||||
return committed;
|
||||
#endif
|
||||
}
|
||||
|
||||
QList<VTableDescription> IaitoCore::getAllVTables()
|
||||
{
|
||||
CORE_LOCK();
|
||||
@@ -4519,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());
|
||||
}
|
||||
@@ -4617,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)) {
|
||||
@@ -4624,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"));
|
||||
@@ -4645,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())));
|
||||
@@ -4664,6 +5173,18 @@ void IaitoCore::deleteProject(const QString &name)
|
||||
cmdRaw0("'P-" + name);
|
||||
}
|
||||
|
||||
bool IaitoCore::isProjectDirty()
|
||||
{
|
||||
CORE_LOCK();
|
||||
return R_DIRTY_CHECK(core->config) || R_DIRTY_CHECK(core->anal) || R_DIRTY_CHECK(core->flags);
|
||||
}
|
||||
|
||||
void IaitoCore::markProjectClean()
|
||||
{
|
||||
CORE_LOCK();
|
||||
r_core_project_undirty(core);
|
||||
}
|
||||
|
||||
bool IaitoCore::isProjectNameValid(const QString &name)
|
||||
{
|
||||
// see is_valid_project_name() in libr/core/project.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#define MONOTHREAD 1
|
||||
|
||||
#include "common/BasicInstructionHighlighter.h"
|
||||
#include "common/ExceptionInfo.h"
|
||||
#include "core/IaitoCommon.h"
|
||||
#include "core/IaitoDescriptions.h"
|
||||
|
||||
@@ -357,6 +358,7 @@ public:
|
||||
|
||||
/* Math functions */
|
||||
ut64 math(const QString &expr);
|
||||
bool tryMath(const QString &expr, ut64 &value);
|
||||
ut64 num(const QString &expr);
|
||||
QString itoa(ut64 num, int rdx = 16);
|
||||
|
||||
@@ -463,6 +465,7 @@ public:
|
||||
void suspendDebug();
|
||||
|
||||
private:
|
||||
int exceptionJsonSupport = -1;
|
||||
void finishStopDebug();
|
||||
|
||||
public:
|
||||
@@ -552,6 +555,8 @@ public:
|
||||
void loadPDB(const QString &file);
|
||||
|
||||
QByteArray ioRead(RVA addr, int len);
|
||||
QString getCommandAnnotation(RVA address, RVA *size = nullptr);
|
||||
void setCommandAnnotation(RVA address, RVA size, const QString &command);
|
||||
|
||||
QList<RVA> getSeekHistory();
|
||||
|
||||
@@ -564,6 +569,8 @@ public:
|
||||
void openProject(const QString &name);
|
||||
void saveProject(const QString &name);
|
||||
void deleteProject(const QString &name);
|
||||
bool isProjectDirty();
|
||||
void markProjectClean();
|
||||
static bool isProjectNameValid(const QString &name);
|
||||
|
||||
/* Widgets */
|
||||
@@ -577,6 +584,7 @@ public:
|
||||
QList<RDebugPluginDescription> getRDebugPluginDescriptions();
|
||||
QList<RMutaPluginDescription> getRMutaPluginDescriptions();
|
||||
QList<FunctionDescription> getAllFunctions();
|
||||
QList<ExceptionRegion> getExceptionRegions(RVA function = RVA_INVALID);
|
||||
QList<ImportDescription> getAllImports();
|
||||
QList<ExportDescription> getAllExports();
|
||||
QList<SymbolDescription> getAllSymbols();
|
||||
@@ -587,12 +595,24 @@ 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();
|
||||
QList<BinClassDescription> getAllClassesFromBin();
|
||||
QList<BinClassDescription> getAllClassesFromFlags();
|
||||
QList<ResourcesDescription> getAllResources();
|
||||
bool dumpResource(
|
||||
const ResourcesDescription &resource,
|
||||
const QString &fileName,
|
||||
QString *errorMessage = nullptr);
|
||||
QList<VTableDescription> getAllVTables();
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -310,10 +320,19 @@ struct ResourcesDescription
|
||||
{
|
||||
QString name;
|
||||
RVA vaddr = 0;
|
||||
ut64 index = 0;
|
||||
QString type;
|
||||
RVA paddr = RVA_INVALID;
|
||||
ut64 size = 0;
|
||||
QString lang;
|
||||
ut64 id = UT64_MAX;
|
||||
ut32 index = 0;
|
||||
ut32 typeId = UT32_MAX;
|
||||
ut32 languageId = 0;
|
||||
ut32 codepage = 0;
|
||||
bool named = false;
|
||||
bool hasExtendedMetadata = false;
|
||||
QString type;
|
||||
QString language;
|
||||
QString timestamp;
|
||||
QString origin;
|
||||
};
|
||||
|
||||
struct VTableDescription
|
||||
@@ -416,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)
|
||||
|
||||
+776
-229
File diff suppressed because it is too large.
Load diff
+46
-8
@@ -15,6 +15,8 @@
|
||||
#include <QEvent>
|
||||
#include <QList>
|
||||
#include <QMainWindow>
|
||||
#include <QPointer>
|
||||
#include <QStringList>
|
||||
#include <QToolButton>
|
||||
|
||||
class IaitoCore;
|
||||
@@ -26,14 +28,18 @@ class AsciiHighlighter;
|
||||
class VisualNavbar;
|
||||
class FunctionsWidget;
|
||||
class ImportsWidget;
|
||||
class InterfacesWidget;
|
||||
class ExportsWidget;
|
||||
class SymbolsWidget;
|
||||
class RelocsWidget;
|
||||
class CommentsWidget;
|
||||
class StringsWidget;
|
||||
class FlagsWidget;
|
||||
class GlobalVariablesWidget;
|
||||
class Dashboard;
|
||||
class QLineEdit;
|
||||
class QRubberBand;
|
||||
class QTabBar;
|
||||
class SdbWidget;
|
||||
class QAction;
|
||||
class SectionsWidget;
|
||||
@@ -61,9 +67,11 @@ class FilesWidget;
|
||||
class FilesystemWidget;
|
||||
class BinariesWidget;
|
||||
class ZoomWidget;
|
||||
class DataAnalysisWidget;
|
||||
class XrefsWidget;
|
||||
class RefsWidget;
|
||||
class CodeInfoWidget;
|
||||
class DockManager;
|
||||
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
@@ -127,6 +135,8 @@ public:
|
||||
addPluginDockWidget(dockWidget);
|
||||
}
|
||||
void addPluginDockWidget(IaitoDockWidget *dockWidget);
|
||||
QStringList getPanelNames() const;
|
||||
bool focusPanelByName(const QString &name);
|
||||
enum class MenuType { File, Edit, Code, View, Tools, Windows, Debug, Help, Plugins };
|
||||
/**
|
||||
* @brief Getter for MainWindow's different menus
|
||||
@@ -143,11 +153,14 @@ 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(
|
||||
QWidget *parent, RVA address, AddressTypeHint addressType = AddressTypeHint::Unknown);
|
||||
void setCurrentMemoryWidget(MemoryDockWidget *memoryWidget);
|
||||
MemoryDockWidget *getLastMemoryWidget();
|
||||
void raiseDefaultMemoryWidget();
|
||||
|
||||
/* Context menu plugins */
|
||||
enum class ContextMenuType { Disassembly, Addressable };
|
||||
@@ -251,6 +264,7 @@ private slots:
|
||||
|
||||
void on_actionExport_as_code_triggered();
|
||||
void on_actionDump_triggered();
|
||||
void on_actionExportClassdump_triggered();
|
||||
void on_actionExportDWARF_triggered();
|
||||
void on_actionExportProjectArchive_triggered();
|
||||
|
||||
@@ -275,8 +289,23 @@ private slots:
|
||||
void repositionSuperBottomNavbar();
|
||||
|
||||
private:
|
||||
enum class PendingCloseAction { None, Save, Discard };
|
||||
|
||||
struct WriteRedoEntry
|
||||
{
|
||||
RVA address;
|
||||
QByteArray data;
|
||||
};
|
||||
|
||||
IaitoCore *core;
|
||||
|
||||
void runCommandShortcut(const QString &command, bool needsInput);
|
||||
void undoWriteCache();
|
||||
void redoWriteCache();
|
||||
QByteArray currentWriteCacheFingerprint(bool *hasUndo = nullptr);
|
||||
bool completePendingClose();
|
||||
void resumePendingClose();
|
||||
|
||||
bool tabsOnTop;
|
||||
ut64 hexdumpTopOffset;
|
||||
ut64 hexdumpBottomOffset;
|
||||
@@ -289,17 +318,22 @@ private:
|
||||
QToolButton *webserverButton = nullptr;
|
||||
bool webserverRunning = false;
|
||||
QByteArray emptyState;
|
||||
QList<WriteRedoEntry> writeRedoStack;
|
||||
QByteArray writeRedoCacheFingerprint;
|
||||
IOModesController ioModesController;
|
||||
PendingCloseAction pendingCloseAction = PendingCloseAction::None;
|
||||
bool pendingProjectSave = false;
|
||||
bool pendingCloseScheduled = false;
|
||||
|
||||
Configuration *configuration;
|
||||
|
||||
QList<IaitoDockWidget *> dockWidgets;
|
||||
QList<IaitoDockWidget *> pluginDocks;
|
||||
DockManager *m_dockManager = nullptr;
|
||||
OverviewWidget *overviewDock = nullptr;
|
||||
QAction *actionOverview = nullptr;
|
||||
EntrypointWidget *entrypointDock = nullptr;
|
||||
FunctionsWidget *functionsDock = nullptr;
|
||||
ImportsWidget *importsDock = nullptr;
|
||||
InterfacesWidget *interfacesDock = nullptr;
|
||||
ExportsWidget *exportsDock = nullptr;
|
||||
HeadersWidget *headersDock = nullptr;
|
||||
TypesWidget *typesDock = nullptr;
|
||||
@@ -309,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;
|
||||
@@ -335,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;
|
||||
@@ -371,6 +407,12 @@ private:
|
||||
* @param widget that needs to be docked
|
||||
*/
|
||||
void dockOnMainArea(QDockWidget *widget);
|
||||
/**
|
||||
* @brief Layout invariant guard: guarantees at least one panel is always
|
||||
* visible and docked, recovering a main view if the user closed/tore off
|
||||
* everything. Prevents a blank main window or empty centre.
|
||||
*/
|
||||
void ensureMainAreaNotEmpty();
|
||||
void enableDebugWidgetsMenu(bool enable);
|
||||
/**
|
||||
* @brief Fill menu with seek history entries.
|
||||
@@ -378,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();
|
||||
@@ -406,14 +449,9 @@ private:
|
||||
|
||||
MemoryWidgetType getMemoryWidgetTypeToRestore();
|
||||
|
||||
/**
|
||||
* @brief Map from a widget type (e.g. DisassemblyWidget::getWidgetType())
|
||||
* to the respective contructor of the widget
|
||||
*/
|
||||
QMap<QString, std::function<IaitoDockWidget *(MainWindow *)>> widgetTypeToConstructorMap;
|
||||
|
||||
MemoryDockWidget *lastSyncMemoryWidget = nullptr;
|
||||
MemoryDockWidget *lastMemoryWidget = nullptr;
|
||||
QPointer<AddressableDockWidget> navigationDock;
|
||||
int functionDockWidthToRestore = 0;
|
||||
|
||||
// True when the main window UI has been fully initialized and it's safe to
|
||||
|
||||
+43
-78
@@ -30,7 +30,7 @@
|
||||
</property>
|
||||
<widget class="QMenu" name="addExtraWidgets">
|
||||
<property name="title">
|
||||
<string>Add extra...</string>
|
||||
<string>New Panel</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menuBar">
|
||||
@@ -59,7 +59,7 @@
|
||||
</property>
|
||||
<widget class="QMenu" name="menuImport">
|
||||
<property name="title">
|
||||
<string>Import...</string>
|
||||
<string>Import</string>
|
||||
</property>
|
||||
<addaction name="actionMap"/>
|
||||
<addaction name="actionImportPDB"/>
|
||||
@@ -71,10 +71,11 @@
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuExport">
|
||||
<property name="title">
|
||||
<string>Export...</string>
|
||||
<string>Export</string>
|
||||
</property>
|
||||
<addaction name="actionExport_as_code"/>
|
||||
<addaction name="actionDump"/>
|
||||
<addaction name="actionExportClassdump"/>
|
||||
<addaction name="actionExportDWARF"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionExportProjectArchive"/>
|
||||
@@ -129,9 +130,15 @@
|
||||
<string>Analysis</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuData">
|
||||
<property name="title">
|
||||
<string>Data</string>
|
||||
</property>
|
||||
</widget>
|
||||
<addaction name="actionHighlight"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menuAnalysis"/>
|
||||
<addaction name="menuData"/>
|
||||
<addaction name="menuAddInfoWidgets"/>
|
||||
<addaction name="menuAddIoWidgets"/>
|
||||
<addaction name="separator"/>
|
||||
@@ -225,12 +232,12 @@
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAddInfoWidgets">
|
||||
<property name="title">
|
||||
<string>Info...</string>
|
||||
<string>Bin headers</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAddIoWidgets">
|
||||
<property name="title">
|
||||
<string>IO...</string>
|
||||
<string>I/O Panels</string>
|
||||
</property>
|
||||
<!--
|
||||
<addaction name="actionBinaries"/>
|
||||
@@ -241,12 +248,12 @@
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAddDebugWidgets">
|
||||
<property name="title">
|
||||
<string>Debug...</string>
|
||||
<string>Debug Panels</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAddAnother">
|
||||
<property name="title">
|
||||
<string>Add another</string>
|
||||
<string>New Panel</string>
|
||||
</property>
|
||||
<addaction name="actionExtraDecompiler"/>
|
||||
<addaction name="actionExtraDisassembly"/>
|
||||
@@ -259,7 +266,6 @@
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menuWindowLayout"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionUnlock"/>
|
||||
<addaction name="actionTabs_on_Top"/>
|
||||
<addaction name="actionGrouped_dock_dragging"/>
|
||||
<addaction name="separator"/>
|
||||
@@ -343,10 +349,7 @@
|
||||
</action>
|
||||
<action name="actionNew">
|
||||
<property name="text">
|
||||
<string>Open...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+N</string>
|
||||
<string>Open…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionClose">
|
||||
@@ -384,15 +387,12 @@
|
||||
</action>
|
||||
<action name="actionMap">
|
||||
<property name="text">
|
||||
<string>Map file at...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+M</string>
|
||||
<string>Map file at…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionManageAddressSpace">
|
||||
<property name="text">
|
||||
<string>Manage files...</string>
|
||||
<string>Manage files</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Manage files, maps, binaries, filesystems, and address space options</string>
|
||||
@@ -405,9 +405,6 @@
|
||||
<property name="text">
|
||||
<string>Save Project</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCut">
|
||||
<property name="text">
|
||||
@@ -479,31 +476,6 @@
|
||||
<string>Redo Seek</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionUnlock">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../resources.qrc">
|
||||
<normaloff>:/lock</normaloff>
|
||||
<normalon>:/unlock</normalon>:/lock</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Unlock Panels</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Toggle panel locks</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Meta+L</string>
|
||||
</property>
|
||||
<property name="iconVisibleInMenu">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionTheme">
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
@@ -773,7 +745,7 @@
|
||||
</action>
|
||||
<action name="actionRun_Script">
|
||||
<property name="text">
|
||||
<string>Radare Script...</string>
|
||||
<string>Radare Script…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Run a radare2 script file once</string>
|
||||
@@ -784,7 +756,7 @@
|
||||
</action>
|
||||
<action name="actionRunR2jsScript">
|
||||
<property name="text">
|
||||
<string>r2js script...</string>
|
||||
<string>r2js script…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Run an r2js script file once</string>
|
||||
@@ -795,7 +767,7 @@
|
||||
</action>
|
||||
<action name="actionScripts">
|
||||
<property name="text">
|
||||
<string>Script Editor...</string>
|
||||
<string>Script Editor</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Open the script editor</string>
|
||||
@@ -816,9 +788,6 @@
|
||||
<property name="text">
|
||||
<string>Quit</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Q</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExports">
|
||||
<property name="checkable">
|
||||
@@ -846,9 +815,6 @@
|
||||
<property name="toolTip">
|
||||
<string>Highlight matching words in disassembly</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Meta+F</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDisplay_Esil">
|
||||
<property name="checkable">
|
||||
@@ -879,7 +845,7 @@
|
||||
</action>
|
||||
<action name="actionSettings">
|
||||
<property name="text">
|
||||
<string>Settings...</string>
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionPackageManagerPrefs">
|
||||
@@ -889,7 +855,7 @@
|
||||
</action>
|
||||
<action name="actionSaveAs">
|
||||
<property name="text">
|
||||
<string>Save Project...</string>
|
||||
<string>Save Project…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionGraph">
|
||||
@@ -902,12 +868,12 @@
|
||||
</action>
|
||||
<action name="actionImportPDB">
|
||||
<property name="text">
|
||||
<string>PDB file...</string>
|
||||
<string>PDB file…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionImportSymbols">
|
||||
<property name="text">
|
||||
<string>Binary symbols...</string>
|
||||
<string>Binary symbols…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Import symbols from another binary object file</string>
|
||||
@@ -953,7 +919,7 @@
|
||||
</action>
|
||||
<action name="actionSwitchJumpTable">
|
||||
<property name="text">
|
||||
<string>SwitchJumpTable...</string>
|
||||
<string>SwitchJumpTable…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Create or edit a switch / jump table at the current dispatcher</string>
|
||||
@@ -964,20 +930,28 @@
|
||||
</action>
|
||||
<action name="actionExport_as_code">
|
||||
<property name="text">
|
||||
<string>Disassembly/code...</string>
|
||||
<string>Disassembly/code…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDump">
|
||||
<property name="text">
|
||||
<string>Binary range...</string>
|
||||
<string>Binary range…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Dump a range of bytes to a file</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExportClassdump">
|
||||
<property name="text">
|
||||
<string>Class dump…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Export class information using icc</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionImportDWARF">
|
||||
<property name="text">
|
||||
<string>DWARF information...</string>
|
||||
<string>DWARF information…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Import DWARF information from another binary object file</string>
|
||||
@@ -985,7 +959,7 @@
|
||||
</action>
|
||||
<action name="actionImportFLIRT">
|
||||
<property name="text">
|
||||
<string>FLIRT signatures...</string>
|
||||
<string>FLIRT signatures…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Load and scan FLIRT signature files</string>
|
||||
@@ -993,12 +967,12 @@
|
||||
</action>
|
||||
<action name="actionImportProjectArchive">
|
||||
<property name="text">
|
||||
<string>Project archive...</string>
|
||||
<string>Project archive…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExportDWARF">
|
||||
<property name="text">
|
||||
<string>DWARF information...</string>
|
||||
<string>DWARF information…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Export DWARF source line information</string>
|
||||
@@ -1006,7 +980,7 @@
|
||||
</action>
|
||||
<action name="actionExportProjectArchive">
|
||||
<property name="text">
|
||||
<string>Project archive...</string>
|
||||
<string>Project archive…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExtraHexdump">
|
||||
@@ -1049,9 +1023,6 @@
|
||||
<property name="text">
|
||||
<string>Zoom In</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl++</string>
|
||||
</property>
|
||||
<property name="shortcutContext">
|
||||
<enum>Qt::ApplicationShortcut</enum>
|
||||
</property>
|
||||
@@ -1060,9 +1031,6 @@
|
||||
<property name="text">
|
||||
<string>Zoom Out</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+-</string>
|
||||
</property>
|
||||
<property name="shortcutContext">
|
||||
<enum>Qt::ApplicationShortcut</enum>
|
||||
</property>
|
||||
@@ -1071,9 +1039,6 @@
|
||||
<property name="text">
|
||||
<string>Reset</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+0</string>
|
||||
</property>
|
||||
<property name="shortcutContext">
|
||||
<enum>Qt::ApplicationShortcut</enum>
|
||||
</property>
|
||||
@@ -1101,7 +1066,7 @@
|
||||
</action>
|
||||
<action name="actionPatchInstruction">
|
||||
<property name="text">
|
||||
<string>Patch Instruction...</string>
|
||||
<string>Patch Instruction…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Patch the instruction at the current address</string>
|
||||
@@ -1109,7 +1074,7 @@
|
||||
</action>
|
||||
<action name="actionPatchString">
|
||||
<property name="text">
|
||||
<string>Patch String...</string>
|
||||
<string>Patch String…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Patch a string at the current address</string>
|
||||
@@ -1117,7 +1082,7 @@
|
||||
</action>
|
||||
<action name="actionPatchBytes">
|
||||
<property name="text">
|
||||
<string>Patch Bytes...</string>
|
||||
<string>Patch Bytes…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Patch bytes at the current address</string>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "QtDockBackend.h"
|
||||
|
||||
#include <QDockWidget>
|
||||
#include <QMainWindow>
|
||||
#include <QTabBar>
|
||||
#include <QTabWidget>
|
||||
|
||||
QtDockBackend::QtDockBackend(QMainWindow *host)
|
||||
: host(host)
|
||||
{}
|
||||
|
||||
void QtDockBackend::addDock(QDockWidget *dock, Qt::DockWidgetArea area)
|
||||
{
|
||||
host->addDockWidget(area, dock);
|
||||
}
|
||||
|
||||
void QtDockBackend::splitDock(QDockWidget *first, QDockWidget *second, Qt::Orientation orientation)
|
||||
{
|
||||
host->splitDockWidget(first, second, orientation);
|
||||
}
|
||||
|
||||
void QtDockBackend::tabifyDock(QDockWidget *first, QDockWidget *second)
|
||||
{
|
||||
host->tabifyDockWidget(first, second);
|
||||
}
|
||||
|
||||
void QtDockBackend::removeDock(QDockWidget *dock)
|
||||
{
|
||||
host->removeDockWidget(dock);
|
||||
}
|
||||
|
||||
void QtDockBackend::setDockFloating(QDockWidget *dock, bool floating)
|
||||
{
|
||||
dock->setFloating(floating);
|
||||
}
|
||||
|
||||
QList<QDockWidget *> QtDockBackend::tabifiedWith(QDockWidget *dock) const
|
||||
{
|
||||
return host->tabifiedDockWidgets(dock);
|
||||
}
|
||||
|
||||
Qt::DockWidgetArea QtDockBackend::areaOf(QDockWidget *dock) const
|
||||
{
|
||||
return host->dockWidgetArea(dock);
|
||||
}
|
||||
|
||||
void QtDockBackend::resizeDocksTo(
|
||||
const QList<QDockWidget *> &docks, const QList<int> &sizes, Qt::Orientation orientation)
|
||||
{
|
||||
host->resizeDocks(docks, sizes, orientation);
|
||||
}
|
||||
|
||||
QList<QTabBar *> QtDockBackend::dockTabBars() const
|
||||
{
|
||||
return host->findChildren<QTabBar *>();
|
||||
}
|
||||
|
||||
void QtDockBackend::setTabsOnTop(bool onTop)
|
||||
{
|
||||
host->setTabPosition(Qt::AllDockWidgetAreas, onTop ? QTabWidget::North : QTabWidget::South);
|
||||
}
|
||||
|
||||
void QtDockBackend::setGroupedDragging(bool enabled)
|
||||
{
|
||||
QMainWindow::DockOptions options = host->dockOptions();
|
||||
options.setFlag(QMainWindow::GroupedDragging, enabled);
|
||||
host->setDockOptions(options);
|
||||
}
|
||||
|
||||
QByteArray QtDockBackend::saveState() const
|
||||
{
|
||||
return host->saveState();
|
||||
}
|
||||
|
||||
bool QtDockBackend::restoreState(const QByteArray &state)
|
||||
{
|
||||
return host->restoreState(state);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef QTDOCKBACKEND_H
|
||||
#define QTDOCKBACKEND_H
|
||||
|
||||
#include "DockBackend.h"
|
||||
|
||||
class QMainWindow;
|
||||
|
||||
// DockBackend realized on a QMainWindow's native QDockWidget docking. Every
|
||||
// method forwards to the equivalent QMainWindow/QDockWidget call, so behavior
|
||||
// stays identical to the inline implementation it replaces.
|
||||
class QtDockBackend : public DockBackend
|
||||
{
|
||||
public:
|
||||
explicit QtDockBackend(QMainWindow *host);
|
||||
|
||||
void addDock(QDockWidget *dock, Qt::DockWidgetArea area) override;
|
||||
void splitDock(QDockWidget *first, QDockWidget *second, Qt::Orientation orientation) override;
|
||||
void tabifyDock(QDockWidget *first, QDockWidget *second) override;
|
||||
void removeDock(QDockWidget *dock) override;
|
||||
void setDockFloating(QDockWidget *dock, bool floating) override;
|
||||
|
||||
QList<QDockWidget *> tabifiedWith(QDockWidget *dock) const override;
|
||||
Qt::DockWidgetArea areaOf(QDockWidget *dock) const override;
|
||||
void resizeDocksTo(
|
||||
const QList<QDockWidget *> &docks,
|
||||
const QList<int> &sizes,
|
||||
Qt::Orientation orientation) override;
|
||||
|
||||
QList<QTabBar *> dockTabBars() const override;
|
||||
void setTabsOnTop(bool onTop) override;
|
||||
void setGroupedDragging(bool enabled) override;
|
||||
|
||||
QByteArray saveState() const override;
|
||||
bool restoreState(const QByteArray &state) override;
|
||||
|
||||
private:
|
||||
QMainWindow *host;
|
||||
};
|
||||
|
||||
#endif // QTDOCKBACKEND_H
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
#include "AsyncTaskDialog.h"
|
||||
#include "common/AsyncTask.h"
|
||||
#include "common/Configuration.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "ui_AsyncTaskDialog.h"
|
||||
|
||||
@@ -20,8 +21,10 @@ AsyncTaskDialog::AsyncTaskDialog(AsyncTask::Ptr task, QWidget *parent)
|
||||
setWindowTitle(title);
|
||||
}
|
||||
|
||||
qhelpers::bindFont(ui->logTextEdit, false, true);
|
||||
ui->logTextEdit->setStyleSheet("QPlainTextEdit { background-color: #1a1a1a; color: #cccccc; }");
|
||||
qhelpers::bindLogFont(ui->logTextEdit);
|
||||
ui->logTextEdit->setStyleSheet(
|
||||
QStringLiteral("QPlainTextEdit { background-color: %1; color: %2; }")
|
||||
.arg(ConfigColor("gui.background").name(), ConfigColor("btext").name()));
|
||||
|
||||
// Replace the default Cancel button with an Interrupt button
|
||||
ui->buttonBox->clear();
|
||||
|
||||
+44
-39
@@ -3,13 +3,12 @@
|
||||
#include <QAction>
|
||||
#include <QColor>
|
||||
#include <QColorDialog>
|
||||
#include <QEvent>
|
||||
#include <QMenu>
|
||||
|
||||
#include "core/Iaito.h"
|
||||
#include <QAbstractButton>
|
||||
#include <QIntValidator>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpressionValidator>
|
||||
|
||||
// Forwarding constructor: default flag size when only offset & parent provided
|
||||
FlagDialog::FlagDialog(RVA offset, QWidget *parent)
|
||||
@@ -38,15 +37,14 @@ FlagDialog::FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent)
|
||||
connect(ui->colorEdit, &QLineEdit::textChanged, this, [this](const QString &color) {
|
||||
ui->colorPreview->setStyleSheet(QStringLiteral("background-color: %1;").arg(color));
|
||||
});
|
||||
RFlagItem *flag = r_flag_get_i(Core()->core()->flags, offset);
|
||||
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, offset);
|
||||
if (flag) {
|
||||
flagName = QString(flag->name);
|
||||
flagOffset = flag->addr;
|
||||
}
|
||||
|
||||
auto size_validator = new QIntValidator(ui->sizeEdit);
|
||||
size_validator->setBottom(1);
|
||||
ui->sizeEdit->setValidator(size_validator);
|
||||
ui->sizeEdit->setValidator(new QRegularExpressionValidator(
|
||||
QRegularExpression(QStringLiteral("[0-9]{1,20}")), ui->sizeEdit));
|
||||
// Pre-fill size field: use defaultSize for new flags, override with existing for edits
|
||||
ui->sizeEdit->setText(QString::number(defaultSize));
|
||||
if (flag) {
|
||||
@@ -61,15 +59,31 @@ FlagDialog::FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Enable color picker on color field
|
||||
ui->colorEdit->installEventFilter(this);
|
||||
connect(ui->colorButton, &QPushButton::clicked, this, [this]() {
|
||||
QColor color
|
||||
= QColorDialog::getColor(QColor(ui->colorEdit->text()), this, tr("Select flag color"));
|
||||
if (color.isValid()) {
|
||||
ui->colorEdit->setText(color.name());
|
||||
}
|
||||
});
|
||||
if (flag) {
|
||||
setWindowTitle(tr("Edit Flag"));
|
||||
ui->nameEdit->setText(flag->name);
|
||||
ui->labelAction->setText(tr("Edit flag at %1").arg(RAddressString(offset)));
|
||||
} else {
|
||||
ui->labelAction->setText(tr("Add flag at %1").arg(RAddressString(offset)));
|
||||
}
|
||||
|
||||
auto updateAcceptButton = [this]() {
|
||||
bool validSize = false;
|
||||
const ut64 size = ui->sizeEdit->text().toULongLong(&validSize);
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)
|
||||
->setEnabled(!ui->nameEdit->text().trimmed().isEmpty() && validSize && size > 0);
|
||||
};
|
||||
connect(ui->nameEdit, &QLineEdit::textChanged, this, updateAcceptButton);
|
||||
connect(ui->sizeEdit, &QLineEdit::textChanged, this, updateAcceptButton);
|
||||
updateAcceptButton();
|
||||
|
||||
// Connect slots for OK/Cancel
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &FlagDialog::buttonBoxAccepted);
|
||||
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &FlagDialog::buttonBoxRejected);
|
||||
@@ -88,34 +102,27 @@ FlagDialog::~FlagDialog() {}
|
||||
|
||||
void FlagDialog::buttonBoxAccepted()
|
||||
{
|
||||
RVA size = ui->sizeEdit->text().toULongLong();
|
||||
QString name = ui->nameEdit->text();
|
||||
bool validSize = false;
|
||||
RVA size = ui->sizeEdit->text().toULongLong(&validSize);
|
||||
QString name = ui->nameEdit->text().trimmed();
|
||||
if (name.isEmpty() || !validSize || size == 0) {
|
||||
return;
|
||||
}
|
||||
QString color = ui->colorEdit->text();
|
||||
QString comment = ui->commentEdit->text();
|
||||
if (flagOffset != RVA_INVALID) {
|
||||
// Existing flag: remove old flag by name
|
||||
Core()->delFlag(flagName);
|
||||
// If a name is provided, recreate/update the flag with new attributes
|
||||
if (!name.isEmpty()) {
|
||||
Core()->addFlag(offset, name, size, color, comment);
|
||||
}
|
||||
} else {
|
||||
// New flag: only add if name is provided
|
||||
if (!name.isEmpty()) {
|
||||
Core()->addFlag(offset, name, size, color, comment);
|
||||
}
|
||||
}
|
||||
close();
|
||||
setResult(QDialog::Accepted);
|
||||
Core()->addFlag(offset, name, size, color, comment);
|
||||
accept();
|
||||
}
|
||||
|
||||
void FlagDialog::buttonBoxRejected()
|
||||
{
|
||||
close();
|
||||
this->setResult(QDialog::Rejected);
|
||||
reject();
|
||||
}
|
||||
|
||||
std::pair<QAction *, QAction *> FlagDialog::addFlagMenuActions(
|
||||
std::tuple<QAction *, QAction *, QAction *> FlagDialog::addFlagMenuActions(
|
||||
QMenu *menu,
|
||||
QWidget *dialogParent,
|
||||
RVA addr,
|
||||
@@ -145,19 +152,17 @@ std::pair<QAction *, QAction *> FlagDialog::addFlagMenuActions(
|
||||
}
|
||||
});
|
||||
|
||||
return {addAction, editAction};
|
||||
}
|
||||
|
||||
bool FlagDialog::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
// Open color picker when clicking on color field
|
||||
if (watched == ui->colorEdit && event->type() == QEvent::MouseButtonPress) {
|
||||
QColor initial = QColor(ui->colorEdit->text());
|
||||
QColor c = QColorDialog::getColor(initial, this, tr("Select flag color"));
|
||||
if (c.isValid()) {
|
||||
ui->colorEdit->setText(c.name());
|
||||
QAction *deleteAction = menu->addAction(tr("Delete flag"));
|
||||
deleteAction->setEnabled(existing != nullptr);
|
||||
QObject::connect(deleteAction, &QAction::triggered, dialogParent, [addr, onChanged]() {
|
||||
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, addr);
|
||||
if (flag) {
|
||||
Core()->delFlag(QString::fromUtf8(flag->name));
|
||||
if (onChanged) {
|
||||
onChanged();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return QDialog::eventFilter(watched, event);
|
||||
});
|
||||
|
||||
return {addAction, editAction, deleteAction};
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "core/IaitoCommon.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <tuple>
|
||||
#include <QDialog>
|
||||
|
||||
class QAction;
|
||||
@@ -19,18 +19,14 @@ class FlagDialog : public QDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// offset: address of flag; defaultSize: initial size to pre-fill (number of bytes)
|
||||
// Constructor: offset is address; defaultSize optional size for new flags
|
||||
// defaultSize pre-fills the byte count for new flags.
|
||||
explicit FlagDialog(RVA offset, QWidget *parent = nullptr);
|
||||
explicit FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent = nullptr);
|
||||
~FlagDialog();
|
||||
|
||||
// Append an "Add flag..." / "Edit flag..." action pair to menu, wired to open
|
||||
// FlagDialog at addr. Whichever action is inapplicable (add when a flag already
|
||||
// exists, edit when none does) is created disabled. onChanged is invoked when
|
||||
// the dialog is accepted (typically a refresh callback). Returns the two
|
||||
// actions so callers can attach icons, etc.
|
||||
static std::pair<QAction *, QAction *> addFlagMenuActions(
|
||||
// Append add, edit and delete actions at addr, disabling inapplicable actions.
|
||||
// onChanged is called after accepting the dialog or deleting the flag.
|
||||
static std::tuple<QAction *, QAction *, QAction *> addFlagMenuActions(
|
||||
QMenu *menu,
|
||||
QWidget *dialogParent,
|
||||
RVA addr,
|
||||
@@ -48,9 +44,6 @@ private:
|
||||
RVA offset;
|
||||
QString flagName;
|
||||
ut64 flagOffset;
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
};
|
||||
|
||||
#endif // FLAGDIALOG_H
|
||||
@@ -107,6 +107,13 @@
|
||||
<item>
|
||||
<widget class="QLineEdit" name="colorEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="colorButton">
|
||||
<property name="text">
|
||||
<string>Choose color...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="colorPreview">
|
||||
<property name="minimumSize">
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
#include "common/AsyncTask.h"
|
||||
#include "ui_InitialOptionsDialog.h"
|
||||
|
||||
#include "common/Helpers.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "dialogs/AsyncTaskDialog.h"
|
||||
#include "dialogs/NewFileDialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCloseEvent>
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QElapsedTimer>
|
||||
@@ -27,6 +25,7 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "common/AnalTask.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "core/Iaito.h"
|
||||
|
||||
static QPlainTextEdit *s_analysisLogWidget = nullptr;
|
||||
@@ -189,6 +188,52 @@ void InitialOptionsDialog::updateCPUComboBox()
|
||||
ui->cpuComboBox->lineEdit()->setText(currentText);
|
||||
}
|
||||
|
||||
void InitialOptionsDialog::savePersistentOptions()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.beginGroup("InitialOptions");
|
||||
settings.setValue("analLevel", ui->analSlider->value());
|
||||
AnalysisCommands item;
|
||||
foreach (item, analysisCommands) {
|
||||
settings.setValue("anal/" + item.commandDesc.command, item.checkbox->isChecked());
|
||||
}
|
||||
settings.setValue("writeEnabled", ui->writeCheckBox->isChecked());
|
||||
settings.setValue("loadBinInfo", ui->binCheckBox->isChecked());
|
||||
settings.setValue("loadBinCache", ui->binCacheCheckBox->isChecked());
|
||||
settings.setValue("demangle", ui->demangleCheckBox->isChecked());
|
||||
settings.setValue("analVars", ui->varCheckBox->isChecked());
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
void InitialOptionsDialog::restorePersistentOptions()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.beginGroup("InitialOptions");
|
||||
if (!settings.contains("analLevel")) {
|
||||
settings.endGroup();
|
||||
return;
|
||||
}
|
||||
int level = settings.value("analLevel").toInt();
|
||||
if (level == 6) {
|
||||
AnalysisCommands item;
|
||||
foreach (item, analysisCommands) {
|
||||
item.checkbox->setChecked(
|
||||
settings.value("anal/" + item.commandDesc.command, item.checked).toBool());
|
||||
}
|
||||
}
|
||||
ui->analSlider->setValue(level);
|
||||
ui->writeCheckBox->setChecked(
|
||||
settings.value("writeEnabled", ui->writeCheckBox->isChecked()).toBool());
|
||||
ui->binCheckBox->setChecked(
|
||||
settings.value("loadBinInfo", ui->binCheckBox->isChecked()).toBool());
|
||||
ui->binCacheCheckBox->setChecked(
|
||||
settings.value("loadBinCache", ui->binCacheCheckBox->isChecked()).toBool());
|
||||
ui->demangleCheckBox->setChecked(
|
||||
settings.value("demangle", ui->demangleCheckBox->isChecked()).toBool());
|
||||
ui->varCheckBox->setChecked(settings.value("analVars", ui->varCheckBox->isChecked()).toBool());
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
QList<QString> InitialOptionsDialog::getAnalysisCommands(const InitialOptions &options)
|
||||
{
|
||||
QList<QString> commands;
|
||||
@@ -287,6 +332,10 @@ void InitialOptionsDialog::loadOptions(const InitialOptions &options)
|
||||
ui->binCacheCheckBox->setChecked(options.loadBinCache);
|
||||
ui->demangleCheckBox->setChecked(options.demangle);
|
||||
ui->varCheckBox->setChecked(options.analVars);
|
||||
|
||||
if (!debugMode && options.script.isEmpty()) {
|
||||
restorePersistentOptions();
|
||||
}
|
||||
}
|
||||
|
||||
void InitialOptionsDialog::setTooltipWithConfigHelp(QWidget *w, const char *config)
|
||||
@@ -451,8 +500,7 @@ void InitialOptionsDialog::setupAndStartAnalysis()
|
||||
|
||||
QPlainTextEdit logText;
|
||||
logText.setReadOnly(true);
|
||||
qhelpers::bindFont(&logText, false, true);
|
||||
logText.setStyleSheet("QPlainTextEdit { background-color: #1a1a1a; color: #cccccc; }");
|
||||
logText.setFont(qhelpers::logFont());
|
||||
layout.addWidget(&logText);
|
||||
|
||||
QPushButton interruptBtn(tr("Interrupt"));
|
||||
@@ -685,14 +733,10 @@ void InitialOptionsDialog::setupAndStartAnalysis()
|
||||
void InitialOptionsDialog::on_okButton_clicked()
|
||||
{
|
||||
ui->okButton->setEnabled(false);
|
||||
savePersistentOptions();
|
||||
setupAndStartAnalysis();
|
||||
}
|
||||
|
||||
void InitialOptionsDialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
event->accept();
|
||||
}
|
||||
|
||||
QString InitialOptionsDialog::analysisDescription(int level)
|
||||
{
|
||||
// TODO: replace this with meaningful descriptions
|
||||
|
||||
@@ -35,9 +35,6 @@ private slots:
|
||||
void updatePDBLayout();
|
||||
void updateScriptLayout();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::InitialOptionsDialog> ui;
|
||||
|
||||
@@ -52,6 +49,8 @@ private:
|
||||
QList<RAsmPluginDescription> asmPlugins;
|
||||
|
||||
void updateCPUComboBox();
|
||||
void savePersistentOptions();
|
||||
void restorePersistentOptions();
|
||||
struct AnalysisCommands
|
||||
{
|
||||
CommandDescription commandDesc;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "dialogs/NewFileDialog.h"
|
||||
#include "InitialOptionsDialog.h"
|
||||
#include "common/Configuration.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "common/HighDpiPixmap.h"
|
||||
#include "common/SamplesDB.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "dialogs/AboutDialog.h"
|
||||
#ifdef IAITO_ENABLE_DEBUGGER
|
||||
@@ -10,20 +12,52 @@
|
||||
#include "ui_NewFileDialog.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QScrollBar>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStandardItemModel>
|
||||
#include <QUrl>
|
||||
#include <QtGui>
|
||||
|
||||
const int NewFileDialog::MaxRecentFiles;
|
||||
|
||||
namespace {
|
||||
// Filters the samples list on the Name (0) and SHA256 (1) columns only, so the
|
||||
// "On disk" yes/no column never matches the filter text.
|
||||
class Sha256FilterProxy : public QSortFilterProxyModel
|
||||
{
|
||||
public:
|
||||
using QSortFilterProxyModel::QSortFilterProxyModel;
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int row, const QModelIndex &parent) const override
|
||||
{
|
||||
const QRegularExpression re = filterRegularExpression();
|
||||
if (re.pattern().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
const QString name = sourceModel()->index(row, 0, parent).data().toString();
|
||||
const QString hash = sourceModel()->index(row, 1, parent).data().toString();
|
||||
return name.contains(re) || hash.contains(re);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
static QColor getColorFor(int pos)
|
||||
{
|
||||
static const QList<QColor> colors = {
|
||||
@@ -162,10 +196,7 @@ NewFileDialog::NewFileDialog(MainWindow *main)
|
||||
ui->setupUi(this);
|
||||
setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));
|
||||
setAcceptDrops(true);
|
||||
// ui->recentsListWidget->setFont(Config()->getBaseFont());
|
||||
// ui->projectsListWidget->setFont(Config()->getBaseFont());
|
||||
ui->recentsListWidget->setFont(Config()->getSmallFont());
|
||||
ui->projectsListWidget->setFont(Config()->getSmallFont());
|
||||
applyListFonts();
|
||||
ui->recentsListWidget->addAction(ui->actionRemove_item);
|
||||
ui->recentsListWidget->addAction(ui->actionClear_all);
|
||||
ui->projectsListWidget->addAction(ui->actionRemove_project);
|
||||
@@ -472,6 +503,15 @@ void NewFileDialog::on_exportProjectButton_clicked()
|
||||
Core()->cmdRaw(QStringLiteral("Pze %1 %2").arg(project).arg(zipFile));
|
||||
}
|
||||
|
||||
void NewFileDialog::applyListFonts()
|
||||
{
|
||||
const QFont font = QApplication::font();
|
||||
const auto views = findChildren<QAbstractItemView *>();
|
||||
for (QAbstractItemView *view : views) {
|
||||
view->setFont(font);
|
||||
}
|
||||
}
|
||||
|
||||
void NewFileDialog::dragEnterEvent(QDragEnterEvent *event)
|
||||
{
|
||||
// Accept drag & drop events only if they provide a URL
|
||||
@@ -689,9 +729,192 @@ void NewFileDialog::setSpacerEnabled(QSpacerItem *s, bool enabled, int w, int h)
|
||||
}
|
||||
}
|
||||
|
||||
void NewFileDialog::initSha256Model()
|
||||
{
|
||||
if (sha256Model) {
|
||||
return;
|
||||
}
|
||||
sha256Model = new QStandardItemModel(this);
|
||||
sha256Model->setHorizontalHeaderLabels({tr("Name"), tr("SHA256"), tr("On disk")});
|
||||
sha256Proxy = new Sha256FilterProxy(this);
|
||||
sha256Proxy->setSourceModel(sha256Model);
|
||||
sha256Proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
|
||||
ui->sha256TableView->setModel(sha256Proxy);
|
||||
ui->sha256TableView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
ui->sha256TableView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
ui->sha256TableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
ui->sha256TableView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
ui->sha256TableView->verticalHeader()->hide();
|
||||
ui->sha256TableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
ui->sha256TableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
ui->sha256TableView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
|
||||
connect(
|
||||
ui->sha256FilterEdit,
|
||||
&QLineEdit::textChanged,
|
||||
sha256Proxy,
|
||||
&QSortFilterProxyModel::setFilterFixedString);
|
||||
|
||||
refreshSha256Model();
|
||||
}
|
||||
|
||||
void NewFileDialog::refreshSha256Model()
|
||||
{
|
||||
if (!sha256Model) {
|
||||
return;
|
||||
}
|
||||
sha256Model->removeRows(0, sha256Model->rowCount());
|
||||
const auto entries = SamplesDB::all();
|
||||
for (const auto &e : entries) {
|
||||
const QString &hash = e.first;
|
||||
const QString &path = e.second;
|
||||
auto *name = new QStandardItem(QFileInfo(path).fileName());
|
||||
name->setData(path, Qt::UserRole);
|
||||
name->setToolTip(path);
|
||||
auto *hashItem = new QStandardItem(hash);
|
||||
auto *existsItem = new QStandardItem(QFileInfo::exists(path) ? tr("yes") : tr("no"));
|
||||
sha256Model->appendRow({name, hashItem, existsItem});
|
||||
}
|
||||
}
|
||||
|
||||
QString NewFileDialog::selectedSha256() const
|
||||
{
|
||||
if (!sha256Proxy) {
|
||||
return QString();
|
||||
}
|
||||
const QModelIndex idx = ui->sha256TableView->currentIndex();
|
||||
if (!idx.isValid()) {
|
||||
return QString();
|
||||
}
|
||||
const QModelIndex src = sha256Proxy->mapToSource(idx);
|
||||
return sha256Model->item(src.row(), 1)->text();
|
||||
}
|
||||
|
||||
QString NewFileDialog::selectedSha256Path() const
|
||||
{
|
||||
if (!sha256Proxy) {
|
||||
return QString();
|
||||
}
|
||||
const QModelIndex idx = ui->sha256TableView->currentIndex();
|
||||
if (!idx.isValid()) {
|
||||
return QString();
|
||||
}
|
||||
const QModelIndex src = sha256Proxy->mapToSource(idx);
|
||||
return sha256Model->item(src.row(), 0)->data(Qt::UserRole).toString();
|
||||
}
|
||||
|
||||
void NewFileDialog::openSamplePath(const QString &path)
|
||||
{
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!QFileInfo::exists(path)) {
|
||||
QMessageBox::warning(this, tr("Open sample"), tr("File no longer exists:\n%1").arg(path));
|
||||
return;
|
||||
}
|
||||
InitialOptions options;
|
||||
options.filename = QDir::toNativeSeparators(path);
|
||||
main->openNewFile(options);
|
||||
close();
|
||||
}
|
||||
|
||||
void NewFileDialog::on_sha256OpenButton_clicked()
|
||||
{
|
||||
openSamplePath(selectedSha256Path());
|
||||
}
|
||||
|
||||
void NewFileDialog::on_sha256TableView_doubleClicked(const QModelIndex &index)
|
||||
{
|
||||
if (!index.isValid() || !sha256Proxy) {
|
||||
return;
|
||||
}
|
||||
const QModelIndex src = sha256Proxy->mapToSource(index);
|
||||
openSamplePath(sha256Model->item(src.row(), 0)->data(Qt::UserRole).toString());
|
||||
}
|
||||
|
||||
void NewFileDialog::on_sha256DeleteButton_clicked()
|
||||
{
|
||||
const QString hash = selectedSha256();
|
||||
if (hash.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
SamplesDB::remove(hash);
|
||||
refreshSha256Model();
|
||||
}
|
||||
|
||||
void NewFileDialog::on_sha256LookupButton_clicked()
|
||||
{
|
||||
const QString hash = selectedSha256();
|
||||
if (hash.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QDesktopServices::openUrl(QUrl(Config()->getSha256LookupBaseUrl() + hash, QUrl::TolerantMode));
|
||||
}
|
||||
|
||||
void NewFileDialog::on_sha256ScanButton_clicked()
|
||||
{
|
||||
const QString dir = QFileDialog::getExistingDirectory(
|
||||
this, tr("Select folder to scan"), Config()->getRecentFolder());
|
||||
if (dir.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
initSha256Model();
|
||||
ui->sha256ScanButton->setEnabled(false);
|
||||
ui->sha256ScanButton->setText(tr("Scanning..."));
|
||||
QPointer<NewFileDialog> self(this);
|
||||
SamplesDB::scanFolderAsync(dir, [self]() {
|
||||
QMetaObject::invokeMethod(qApp, [self]() {
|
||||
if (!self) {
|
||||
return;
|
||||
}
|
||||
self->refreshSha256Model();
|
||||
self->ui->sha256ScanButton->setText(QObject::tr("Scan folder..."));
|
||||
self->ui->sha256ScanButton->setEnabled(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void NewFileDialog::on_sha256TableView_customContextMenuRequested(const QPoint &pos)
|
||||
{
|
||||
if (!sha256Proxy) {
|
||||
return;
|
||||
}
|
||||
const QModelIndex idx = ui->sha256TableView->indexAt(pos);
|
||||
if (!idx.isValid()) {
|
||||
return;
|
||||
}
|
||||
const QModelIndex src = sha256Proxy->mapToSource(idx);
|
||||
const QString path = sha256Model->item(src.row(), 0)->data(Qt::UserRole).toString();
|
||||
const QString hash = sha256Model->item(src.row(), 1)->text();
|
||||
|
||||
QMenu menu(this);
|
||||
menu.addAction(tr("Open"), this, [this, path]() { openSamplePath(path); });
|
||||
menu.addAction(tr("Open in lookup URL"), this, [hash]() {
|
||||
QDesktopServices::openUrl(
|
||||
QUrl(Config()->getSha256LookupBaseUrl() + hash, QUrl::TolerantMode));
|
||||
});
|
||||
menu.addSeparator();
|
||||
menu.addAction(tr("Copy path"), this, [path]() { QApplication::clipboard()->setText(path); });
|
||||
menu.addAction(tr("Copy hash"), this, [hash]() { QApplication::clipboard()->setText(hash); });
|
||||
menu.addAction(tr("Open containing folder"), this, [path]() {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(path).absolutePath()));
|
||||
});
|
||||
menu.addSeparator();
|
||||
menu.addAction(tr("Remove from database"), this, [this, hash]() {
|
||||
SamplesDB::remove(hash);
|
||||
refreshSha256Model();
|
||||
});
|
||||
menu.exec(ui->sha256TableView->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void NewFileDialog::on_tabWidget_currentChanged(int index)
|
||||
{
|
||||
Config()->setNewFileLastClicked(index);
|
||||
if (index == ui->tabWidget->indexOf(ui->sha256Tab) && !sha256TabPopulated) {
|
||||
sha256TabPopulated = true;
|
||||
initSha256Model();
|
||||
}
|
||||
#ifdef IAITO_ENABLE_DEBUGGER
|
||||
if (index == ui->tabWidget->indexOf(ui->attachTab) && !attachTabPopulated) {
|
||||
attachTabPopulated = true;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <memory>
|
||||
#include <QDialog>
|
||||
#include <QListWidgetItem>
|
||||
#include <QModelIndex>
|
||||
#include <QPoint>
|
||||
#include <QSpacerItem>
|
||||
|
||||
#include "core/Iaito.h"
|
||||
@@ -13,6 +15,8 @@
|
||||
class ProcessModel;
|
||||
class ProcessProxyModel;
|
||||
#endif
|
||||
class QStandardItemModel;
|
||||
class QSortFilterProxyModel;
|
||||
|
||||
namespace Ui {
|
||||
class NewFileDialog;
|
||||
@@ -56,6 +60,13 @@ private slots:
|
||||
|
||||
void on_tabWidget_currentChanged(int index);
|
||||
|
||||
void on_sha256ScanButton_clicked();
|
||||
void on_sha256OpenButton_clicked();
|
||||
void on_sha256DeleteButton_clicked();
|
||||
void on_sha256LookupButton_clicked();
|
||||
void on_sha256TableView_doubleClicked(const QModelIndex &index);
|
||||
void on_sha256TableView_customContextMenuRequested(const QPoint &pos);
|
||||
|
||||
#ifdef IAITO_ENABLE_DEBUGGER
|
||||
void on_attachButton_clicked();
|
||||
void on_attachProcView_doubleClicked(const QModelIndex &index);
|
||||
@@ -63,8 +74,8 @@ private slots:
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void dragEnterEvent(QDragEnterEvent *event);
|
||||
void dropEvent(QDropEvent *event);
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::NewFileDialog> ui;
|
||||
@@ -77,6 +88,16 @@ private:
|
||||
bool attachTabPopulated = false;
|
||||
#endif
|
||||
|
||||
QStandardItemModel *sha256Model = nullptr;
|
||||
QSortFilterProxyModel *sha256Proxy = nullptr;
|
||||
bool sha256TabPopulated = false;
|
||||
|
||||
void initSha256Model();
|
||||
void refreshSha256Model();
|
||||
void openSamplePath(const QString &path);
|
||||
QString selectedSha256() const;
|
||||
QString selectedSha256Path() const;
|
||||
|
||||
/**
|
||||
* @return true if list is not empty
|
||||
*/
|
||||
@@ -102,6 +123,8 @@ private:
|
||||
void setDisableAndHideWidget(QWidget *w, bool disable_and_hide = true);
|
||||
void setSpacerEnabled(QSpacerItem *s, bool enabled, int w = 10, int h = 10);
|
||||
|
||||
void applyListFonts();
|
||||
|
||||
static const int MaxRecentFiles = 5;
|
||||
};
|
||||
|
||||
|
||||
@@ -515,6 +515,81 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="sha256Tab">
|
||||
<attribute name="title">
|
||||
<string>Sha256</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="sha256Layout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="sha256FilterEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Filter by name or hash...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableView" name="sha256TableView">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="sha256BottomLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="sha256ScanButton">
|
||||
<property name="text">
|
||||
<string>Scan folder...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="sha256Spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="sha256LookupButton">
|
||||
<property name="text">
|
||||
<string>Lookup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="sha256DeleteButton">
|
||||
<property name="text">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="sha256OpenButton">
|
||||
<property name="text">
|
||||
<string>Open</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// PackageManagerDialog.cpp
|
||||
#include "PackageManagerDialog.h"
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QJsonArray>
|
||||
@@ -9,6 +10,7 @@
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
@@ -19,6 +21,14 @@
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
static QString processError(QProcess &process, const QString &message)
|
||||
{
|
||||
const QString details = QString::fromLocal8Bit(
|
||||
process.readAllStandardOutput() + process.readAllStandardError())
|
||||
.trimmed();
|
||||
return details.isEmpty() ? message : message + "\n\n" + details;
|
||||
}
|
||||
|
||||
void CheckBoxDelegate::paint(
|
||||
QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
@@ -38,20 +48,30 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_process(new QProcess(this))
|
||||
{
|
||||
setWindowTitle(tr("Package Manager"));
|
||||
setWindowTitle(tr("Radare2 Package Manager (r2pm)"));
|
||||
resize(900, 600);
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
auto *filterLayout = new QHBoxLayout();
|
||||
m_filterLineEdit = new QLineEdit(this);
|
||||
m_filterLineEdit->setPlaceholderText(tr("Filter packages..."));
|
||||
m_filterLineEdit->setMinimumHeight(32);
|
||||
layout->addWidget(m_filterLineEdit);
|
||||
m_binaryPackagesCheckBox = new QCheckBox(tr("Binary packages"), this);
|
||||
m_binaryPackagesCheckBox->setToolTip(
|
||||
tr("Install ready-to-use packages, including scripts, without building from source."));
|
||||
m_showAllPlatformsCheckBox = new QCheckBox(tr("Show all platforms"), this);
|
||||
filterLayout->addWidget(m_filterLineEdit);
|
||||
filterLayout->addWidget(m_binaryPackagesCheckBox);
|
||||
filterLayout->addWidget(m_showAllPlatformsCheckBox);
|
||||
layout->addLayout(filterLayout);
|
||||
|
||||
m_tableWidget = new QTableWidget(this);
|
||||
m_tableWidget->setColumnCount(3);
|
||||
m_tableWidget->setHorizontalHeaderLabels({tr("Installed"), tr("Package"), tr("Description")});
|
||||
m_tableWidget->setColumnCount(4);
|
||||
m_tableWidget->setHorizontalHeaderLabels(
|
||||
{tr("Installed"), tr("Package"), tr("Platforms"), tr("Description")});
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_tableWidget->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
|
||||
m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_tableWidget->setSortingEnabled(true);
|
||||
@@ -61,11 +81,15 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
|
||||
|
||||
auto *buttonLayout = new QHBoxLayout();
|
||||
m_refreshButton = new QPushButton(tr("Refresh"), this);
|
||||
m_installButton = new QPushButton(tr("Install"), this);
|
||||
m_uninstallButton = new QPushButton(tr("Uninstall"), this);
|
||||
m_installButton = new QPushButton(tr("Install"), this);
|
||||
for (auto *button : {m_refreshButton, m_uninstallButton, m_installButton}) {
|
||||
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
}
|
||||
buttonLayout->addWidget(m_refreshButton);
|
||||
buttonLayout->addWidget(m_installButton);
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(m_uninstallButton);
|
||||
buttonLayout->addWidget(m_installButton);
|
||||
layout->addLayout(buttonLayout);
|
||||
|
||||
m_logTextEdit = new QTextEdit(this);
|
||||
@@ -74,6 +98,12 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
|
||||
layout->addWidget(m_logTextEdit);
|
||||
|
||||
connect(m_filterLineEdit, &QLineEdit::textChanged, this, &PackageManagerDialog::filterPackages);
|
||||
connect(m_binaryPackagesCheckBox, &QCheckBox::toggled, this, [this]() {
|
||||
filterPackages(m_filterLineEdit->text());
|
||||
});
|
||||
connect(m_showAllPlatformsCheckBox, &QCheckBox::toggled, this, [this]() {
|
||||
filterPackages(m_filterLineEdit->text());
|
||||
});
|
||||
connect(m_refreshButton, &QPushButton::clicked, this, &PackageManagerDialog::refreshPackages);
|
||||
connect(m_installButton, &QPushButton::clicked, this, &PackageManagerDialog::installPackage);
|
||||
connect(m_uninstallButton, &QPushButton::clicked, this, &PackageManagerDialog::uninstallPackage);
|
||||
@@ -104,23 +134,30 @@ void PackageManagerDialog::refreshPackages()
|
||||
QProcess updateProc;
|
||||
updateProc.start("r2pm", QStringList() << "-U");
|
||||
if (!updateProc.waitForFinished(30000)) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to run r2pm -U"));
|
||||
QMessageBox::warning(
|
||||
this, tr("Error"), processError(updateProc, tr("Failed to run r2pm -U.")));
|
||||
return;
|
||||
}
|
||||
// Then list packages
|
||||
QProcess listProc;
|
||||
listProc.start("r2pm", QStringList() << "-sj");
|
||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
||||
environment.remove("R2PM_PLATFORM");
|
||||
listProc.setProcessEnvironment(environment);
|
||||
listProc.start("r2pm", QStringList() << "-Asj");
|
||||
if (!listProc.waitForFinished(30000)) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to run r2pm -sj"));
|
||||
QMessageBox::warning(
|
||||
this, tr("Error"), processError(listProc, tr("Failed to run r2pm -Asj.")));
|
||||
return;
|
||||
}
|
||||
QByteArray out = listProc.readAllStandardOutput();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(out);
|
||||
if (!doc.isArray()) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -sj"));
|
||||
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -Asj"));
|
||||
return;
|
||||
}
|
||||
QJsonArray array = doc.array();
|
||||
const bool sortingEnabled = m_tableWidget->isSortingEnabled();
|
||||
m_tableWidget->setSortingEnabled(false);
|
||||
m_tableWidget->setRowCount(0);
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject())
|
||||
@@ -131,6 +168,13 @@ void PackageManagerDialog::refreshPackages()
|
||||
if (desc == "") {
|
||||
desc = obj["desc"].toString();
|
||||
}
|
||||
QStringList platforms;
|
||||
for (const QJsonValue &platformValue : obj["platforms"].toArray()) {
|
||||
const QString platform = platformValue.toString();
|
||||
if (!platform.isEmpty()) {
|
||||
platforms << platform;
|
||||
}
|
||||
}
|
||||
int row = m_tableWidget->rowCount();
|
||||
m_tableWidget->insertRow(row);
|
||||
QTableWidgetItem *itemInstalled = new QTableWidgetItem();
|
||||
@@ -138,12 +182,20 @@ void PackageManagerDialog::refreshPackages()
|
||||
itemInstalled->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
m_tableWidget->setItem(row, 0, itemInstalled);
|
||||
QTableWidgetItem *itemName = new QTableWidgetItem(name);
|
||||
itemName->setData(Qt::UserRole, obj["binary"].toBool());
|
||||
itemName->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
m_tableWidget->setItem(row, 1, itemName);
|
||||
QTableWidgetItem *itemPlatforms = new QTableWidgetItem(platforms.join(' '));
|
||||
itemPlatforms->setData(Qt::UserRole, obj["supported"].toBool(true));
|
||||
itemPlatforms->setToolTip(platforms.join(", "));
|
||||
itemPlatforms->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
itemPlatforms->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
m_tableWidget->setItem(row, 2, itemPlatforms);
|
||||
QTableWidgetItem *itemDesc = new QTableWidgetItem(desc);
|
||||
itemDesc->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
m_tableWidget->setItem(row, 2, itemDesc);
|
||||
m_tableWidget->setItem(row, 3, itemDesc);
|
||||
}
|
||||
m_tableWidget->setSortingEnabled(sortingEnabled);
|
||||
filterPackages(m_filterLineEdit->text());
|
||||
}
|
||||
|
||||
@@ -166,30 +218,43 @@ void PackageManagerDialog::filterPackages(const QString &text)
|
||||
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
|
||||
bool visible = text.isEmpty()
|
||||
|| m_tableWidget->item(row, 1)->text().contains(text, Qt::CaseInsensitive)
|
||||
|| m_tableWidget->item(row, 2)->text().contains(text, Qt::CaseInsensitive);
|
||||
|| m_tableWidget->item(row, 2)->text().contains(text, Qt::CaseInsensitive)
|
||||
|| m_tableWidget->item(row, 3)->text().contains(text, Qt::CaseInsensitive);
|
||||
if (m_binaryPackagesCheckBox->isChecked()) {
|
||||
visible = visible && m_tableWidget->item(row, 1)->data(Qt::UserRole).toBool();
|
||||
}
|
||||
if (!m_showAllPlatformsCheckBox->isChecked()) {
|
||||
visible = visible && m_tableWidget->item(row, 2)->data(Qt::UserRole).toBool();
|
||||
}
|
||||
m_tableWidget->setRowHidden(row, !visible);
|
||||
}
|
||||
if (m_tableWidget->currentRow() >= 0
|
||||
&& m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
|
||||
m_tableWidget->setCurrentItem(nullptr);
|
||||
m_tableWidget->clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void PackageManagerDialog::installPackage()
|
||||
{
|
||||
if (m_process->state() != QProcess::NotRunning)
|
||||
return;
|
||||
if (m_tableWidget->currentRow() < 0) {
|
||||
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
|
||||
QMessageBox::information(this, tr("Install"), tr("Please select a package to install."));
|
||||
return;
|
||||
}
|
||||
QString pkg = m_tableWidget->item(m_tableWidget->currentRow(), 1)->text();
|
||||
m_logTextEdit->clear();
|
||||
m_logTextEdit->setVisible(true);
|
||||
m_process->start("r2pm", QStringList() << "-ci" << pkg);
|
||||
const QString flags = m_binaryPackagesCheckBox->isChecked() ? "-bi" : "-ci";
|
||||
m_process->start("r2pm", QStringList() << flags << pkg);
|
||||
}
|
||||
|
||||
void PackageManagerDialog::uninstallPackage()
|
||||
{
|
||||
if (m_process->state() != QProcess::NotRunning)
|
||||
return;
|
||||
if (m_tableWidget->currentRow() < 0) {
|
||||
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
|
||||
QMessageBox::information(this, tr("Uninstall"), tr("Please select a package to uninstall."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
class QLineEdit;
|
||||
class QCheckBox;
|
||||
class QTableWidget;
|
||||
class QPushButton;
|
||||
class QTextEdit;
|
||||
@@ -42,6 +43,8 @@ private slots:
|
||||
|
||||
private:
|
||||
QLineEdit *m_filterLineEdit;
|
||||
QCheckBox *m_binaryPackagesCheckBox;
|
||||
QCheckBox *m_showAllPlatformsCheckBox;
|
||||
QTableWidget *m_tableWidget;
|
||||
QPushButton *m_refreshButton;
|
||||
QPushButton *m_installButton;
|
||||
@@ -52,4 +55,4 @@ private:
|
||||
void populateInstalledPackages();
|
||||
};
|
||||
|
||||
#endif // PACKAGEMANAGERDIALOG_H
|
||||
#endif // PACKAGEMANAGERDIALOG_H
|
||||
@@ -1,27 +1,102 @@
|
||||
#include "R2PluginsDialog.h"
|
||||
#include "ui_R2PluginsDialog.h"
|
||||
|
||||
#include "common/Configuration.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "core/Iaito.h"
|
||||
#include "plugins/PluginManager.h"
|
||||
|
||||
R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QPushButton>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
namespace {
|
||||
|
||||
QString r2QuotedPluginFileArg(const QString &path)
|
||||
{
|
||||
QString result = IaitoCore::sanitizeStringForCommand(path);
|
||||
result.replace(QLatin1Char('"'), QLatin1Char('_'));
|
||||
result.replace(QLatin1Char('\n'), QLatin1Char('_'));
|
||||
result.replace(QLatin1Char('\r'), QLatin1Char('_'));
|
||||
return QStringLiteral("\"%1\"").arg(result);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
R2PluginsDialog::R2PluginsDialog(QWidget *parent, bool showDialogButtons)
|
||||
: QDialog(parent)
|
||||
, ui(new Ui::R2PluginsDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
if (showDialogButtons) {
|
||||
ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok);
|
||||
QPushButton *loadPluginButton
|
||||
= ui->buttonBox->addButton(tr("Load plugin"), QDialogButtonBox::ActionRole);
|
||||
connect(loadPluginButton, &QPushButton::clicked, this, &R2PluginsDialog::loadPlugin);
|
||||
} else {
|
||||
ui->buttonBox->hide();
|
||||
}
|
||||
|
||||
refreshPluginDescriptions();
|
||||
}
|
||||
|
||||
R2PluginsDialog::~R2PluginsDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void R2PluginsDialog::loadPlugin()
|
||||
{
|
||||
const QString nativeExt = QStringLiteral(R_LIB_EXT);
|
||||
const QString filter
|
||||
= tr("radare2 plugins (*.%1 *.r2.js);;Native plugins (*.so *.dll *.dylib);;"
|
||||
"r2js plugins (*.r2.js);;All files (*)")
|
||||
.arg(nativeExt);
|
||||
const QString fileName = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
tr("Load radare2 plugin"),
|
||||
Config()->getRecentFolder(),
|
||||
filter,
|
||||
nullptr,
|
||||
QFILEDIALOG_FLAGS);
|
||||
if (fileName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Config()->setRecentFolder(QFileInfo(fileName).absolutePath());
|
||||
const bool loaded = Core()->cmdRaw0(QStringLiteral("L %1").arg(r2QuotedPluginFileArg(fileName)));
|
||||
if (loaded) {
|
||||
Core()->message(tr("Loaded plugin: %1").arg(fileName));
|
||||
refreshPluginDescriptions();
|
||||
} else {
|
||||
Core()->message(tr("Failed to load plugin: %1").arg(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
void R2PluginsDialog::refreshPluginDescriptions()
|
||||
{
|
||||
ui->RBinTreeWidget->clear();
|
||||
ui->RDemangleTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRBinPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
item->setText(1, plugin.description);
|
||||
item->setText(2, plugin.license);
|
||||
item->setText(3, plugin.type);
|
||||
ui->RBinTreeWidget->addTopLevelItem(item);
|
||||
if (plugin.type == QStringLiteral("dem")) {
|
||||
ui->RDemangleTreeWidget->addTopLevelItem(item);
|
||||
} else {
|
||||
item->setText(3, plugin.type);
|
||||
ui->RBinTreeWidget->addTopLevelItem(item);
|
||||
}
|
||||
}
|
||||
ui->RBinTreeWidget->sortByColumn(0, Qt::AscendingOrder);
|
||||
qhelpers::adjustColumns(ui->RBinTreeWidget, 0);
|
||||
ui->RDemangleTreeWidget->sortByColumn(0, Qt::AscendingOrder);
|
||||
qhelpers::adjustColumns(ui->RDemangleTreeWidget, 0);
|
||||
|
||||
ui->RIOTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRIOPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
@@ -33,6 +108,7 @@ R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
ui->RIOTreeWidget->sortByColumn(0, Qt::AscendingOrder);
|
||||
qhelpers::adjustColumns(ui->RIOTreeWidget, 0);
|
||||
|
||||
ui->RCoreTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRCorePluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
@@ -42,6 +118,7 @@ R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
ui->RCoreTreeWidget->sortByColumn(0, Qt::AscendingOrder);
|
||||
qhelpers::adjustColumns(ui->RCoreTreeWidget, 0);
|
||||
|
||||
ui->RAsmTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRAsmPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
@@ -58,6 +135,7 @@ R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
|
||||
// RAnal
|
||||
|
||||
ui->RAnalTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRAnalPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
@@ -74,6 +152,7 @@ R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
|
||||
// RMuta
|
||||
|
||||
ui->RMutaTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRMutaPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
@@ -88,6 +167,7 @@ R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
|
||||
// RLang
|
||||
|
||||
ui->RLangTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRLangPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
@@ -102,6 +182,7 @@ R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
|
||||
// RFS
|
||||
|
||||
ui->RFSTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRFSPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
@@ -115,6 +196,7 @@ R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
|
||||
// RDebug
|
||||
|
||||
ui->RDebugTreeWidget->clear();
|
||||
for (const auto &plugin : Core()->getRDebugPluginDescriptions()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, plugin.name);
|
||||
@@ -127,8 +209,3 @@ R2PluginsDialog::R2PluginsDialog(QWidget *parent)
|
||||
ui->RDebugTreeWidget->sortByColumn(0, Qt::AscendingOrder);
|
||||
qhelpers::adjustColumns(ui->RDebugTreeWidget, 0);
|
||||
}
|
||||
|
||||
R2PluginsDialog::~R2PluginsDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
@@ -15,10 +15,15 @@ class R2PluginsDialog : public QDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit R2PluginsDialog(QWidget *parent = nullptr);
|
||||
explicit R2PluginsDialog(QWidget *parent = nullptr, bool showDialogButtons = true);
|
||||
~R2PluginsDialog();
|
||||
|
||||
public slots:
|
||||
void loadPlugin();
|
||||
|
||||
private:
|
||||
void refreshPluginDescriptions();
|
||||
|
||||
Ui::R2PluginsDialog *ui;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>4</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="RBinTab">
|
||||
<attribute name="title">
|
||||
@@ -60,6 +60,42 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="RDemangleTab">
|
||||
<attribute name="title">
|
||||
<string>Demangle</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_11">
|
||||
<item>
|
||||
<widget class="QLabel" name="RDemangleLabel">
|
||||
<property name="text">
|
||||
<string>Demangle symbol names for supported languages and compilers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="RDemangleTreeWidget">
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Description</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>License</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="RIOTab">
|
||||
<attribute name="title">
|
||||
<string>RIO</string>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="toTreeWidget">
|
||||
<widget class="AddressableItemView" name="toTreeWidget">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
@@ -73,7 +73,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="fromTreeWidget">
|
||||
<widget class="AddressableItemView" name="fromTreeWidget">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
@@ -142,7 +142,7 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AddressableItemList<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
</customwidget>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include <QFontDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
#include <QStandardPaths>
|
||||
#include <QTranslator>
|
||||
@@ -18,11 +20,15 @@
|
||||
|
||||
#include "common/Configuration.h"
|
||||
#include "common/Helpers.h"
|
||||
#include "common/theme/IaitoStyle.h"
|
||||
#include "common/theme/Theme.h"
|
||||
|
||||
#include "common/ColorThemeWorker.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "dialogs/settings/ColorThemeEditDialog.h"
|
||||
#include "dialogs/settings/InterfaceThemeEditDialog.h"
|
||||
#include "widgets/ColorPicker.h"
|
||||
#include <QPushButton>
|
||||
|
||||
namespace {
|
||||
struct FontFamilyEntry
|
||||
@@ -42,6 +48,16 @@ static const FontFamilyEntry kFontFamilies[] = {
|
||||
static const char *kCustomLabel = "Custom...";
|
||||
static const char *kDefaultFamily = "IBM Plex Mono";
|
||||
static const int kDefaultPointSize = 13;
|
||||
|
||||
static bool interfaceThemeExists(const QString &name)
|
||||
{
|
||||
for (const auto &theme : Configuration::iaitoInterfaceThemesList()) {
|
||||
if (theme.name == name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
|
||||
@@ -49,6 +65,7 @@ AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
|
||||
, ui(new Ui::AppearanceOptionsWidget)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
for (const auto &entry : kFontFamilies) {
|
||||
ui->fontFamilyComboBox
|
||||
->addItem(QString::fromLatin1(entry.label), QString::fromLatin1(entry.family));
|
||||
@@ -66,6 +83,17 @@ AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
|
||||
->addItem(tr("Bottom"), static_cast<int>(Configuration::VisualNavbarLocation::Bottom));
|
||||
ui->visualNavbarLocationComboBox->addItem(
|
||||
tr("SuperBottom"), static_cast<int>(Configuration::VisualNavbarLocation::SuperBottom));
|
||||
{
|
||||
// Filling the list must not write the first entry into the settings
|
||||
QSignalBlocker blocker(ui->memoryScrollBarComboBox);
|
||||
ui->memoryScrollBarComboBox
|
||||
->addItem(tr("Hidden"), static_cast<int>(Configuration::MemoryScrollBarMode::Hidden));
|
||||
ui->memoryScrollBarComboBox->addItem(
|
||||
tr("Spring (unbounded)"), static_cast<int>(Configuration::MemoryScrollBarMode::Spring));
|
||||
ui->memoryScrollBarComboBox->addItem(
|
||||
tr("Bounded (navigation bar range)"),
|
||||
static_cast<int>(Configuration::MemoryScrollBarMode::Bounded));
|
||||
}
|
||||
updateFromConfig();
|
||||
|
||||
QStringList langs = Config()->getAvailableTranslations();
|
||||
@@ -88,6 +116,12 @@ AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
|
||||
ui->importButton->setIcon(getIconFromSvg(":/img/icons/download_black.svg", textColor));
|
||||
ui->exportButton->setIcon(getIconFromSvg(":/img/icons/upload_black.svg", textColor));
|
||||
ui->renameButton->setIcon(getIconFromSvg(":/img/icons/rename.svg", textColor));
|
||||
ui->ifaceEditButton->setIcon(getIconFromSvg(":/img/icons/pencil_thin.svg", textColor));
|
||||
ui->ifaceRenameButton->setIcon(getIconFromSvg(":/img/icons/rename.svg", textColor));
|
||||
ui->ifaceCopyButton->setIcon(getIconFromSvg(":/img/icons/copy.svg", textColor));
|
||||
ui->ifaceDeleteButton->setIcon(getIconFromSvg(":/img/icons/trash_bin.svg", textColor));
|
||||
ui->ifaceExportButton->setIcon(getIconFromSvg(":/img/icons/upload_black.svg", textColor));
|
||||
ui->ifaceImportButton->setIcon(getIconFromSvg(":/img/icons/download_black.svg", textColor));
|
||||
};
|
||||
setIcons();
|
||||
connect(Config(), &Configuration::interfaceThemeChanged, this, setIcons);
|
||||
@@ -111,6 +145,11 @@ AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
|
||||
connect(Config(), &Configuration::visualNavbarThicknessChanged, this, [this](int) {
|
||||
updateVisualNavbarFromConfig();
|
||||
});
|
||||
connect(
|
||||
Config(),
|
||||
&Configuration::memoryScrollBarModeChanged,
|
||||
this,
|
||||
[this](Configuration::MemoryScrollBarMode) { updateVisualNavbarFromConfig(); });
|
||||
connect(Config(), &Configuration::colorsUpdated, this, [this]() {
|
||||
updateVisualNavbarFromConfig();
|
||||
});
|
||||
@@ -204,6 +243,7 @@ void AppearanceOptionsWidget::updateThemeFromConfig(bool interfaceThemeChanged)
|
||||
Config()->setInterfaceTheme(currInterfaceThemeIndex);
|
||||
}
|
||||
ui->themeComboBox->setCurrentIndex(currInterfaceThemeIndex);
|
||||
updateInterfaceModificationButtons(ui->themeComboBox->currentText());
|
||||
ui->colorComboBox->updateFromConfig(interfaceThemeChanged);
|
||||
updateModificationButtons(ui->colorComboBox->currentText());
|
||||
}
|
||||
@@ -212,7 +252,7 @@ void AppearanceOptionsWidget::updateVisualNavbarFromConfig()
|
||||
{
|
||||
QSignalBlocker locationBlocker(ui->visualNavbarLocationComboBox);
|
||||
QSignalBlocker thicknessBlocker(ui->visualNavbarThicknessSpinBox);
|
||||
QSignalBlocker themeColorBlocker(ui->visualNavbarUseThemeColorsCheckBox);
|
||||
QSignalBlocker scrollBarBlocker(ui->memoryScrollBarComboBox);
|
||||
|
||||
auto location = static_cast<int>(Config()->getVisualNavbarLocation());
|
||||
int index = ui->visualNavbarLocationComboBox->findData(location);
|
||||
@@ -221,7 +261,11 @@ void AppearanceOptionsWidget::updateVisualNavbarFromConfig()
|
||||
}
|
||||
|
||||
ui->visualNavbarThicknessSpinBox->setValue(Config()->getVisualNavbarThickness());
|
||||
ui->visualNavbarUseThemeColorsCheckBox->setChecked(Config()->getVisualNavbarUseThemeColors());
|
||||
const int scrollBarMode = static_cast<int>(Config()->getMemoryScrollBarMode());
|
||||
const int scrollBarIndex = ui->memoryScrollBarComboBox->findData(scrollBarMode);
|
||||
if (scrollBarIndex >= 0) {
|
||||
ui->memoryScrollBarComboBox->setCurrentIndex(scrollBarIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::onFontZoomBoxValueChanged(int zoom)
|
||||
@@ -290,23 +334,218 @@ void AppearanceOptionsWidget::on_visualNavbarThicknessSpinBox_valueChanged(int v
|
||||
Config()->setVisualNavbarThickness(value);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_visualNavbarUseThemeColorsCheckBox_toggled(bool checked)
|
||||
void AppearanceOptionsWidget::on_memoryScrollBarComboBox_currentIndexChanged(int index)
|
||||
{
|
||||
Config()->setVisualNavbarUseThemeColors(checked);
|
||||
Config()->setMemoryScrollBarMode(
|
||||
static_cast<Configuration::MemoryScrollBarMode>(
|
||||
ui->memoryScrollBarComboBox->itemData(index).toInt()));
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_editButton_clicked()
|
||||
{
|
||||
QString theme = ui->colorComboBox->currentText();
|
||||
if (!ThemeWorker().isCustomTheme(theme)) {
|
||||
if (!confirmSystemThemeCopy(theme) || !copyColorTheme(theme)) {
|
||||
return;
|
||||
}
|
||||
theme = ui->colorComboBox->currentText();
|
||||
}
|
||||
|
||||
ColorThemeEditDialog dial;
|
||||
dial.setWindowTitle(tr("Theme Editor - <%1>").arg(ui->colorComboBox->currentText()));
|
||||
dial.setWindowTitle(tr("Theme Editor - <%1>").arg(theme));
|
||||
dial.exec();
|
||||
ui->colorComboBox->updateFromConfig(false);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::selectInterfaceThemeByName(const QString &name)
|
||||
{
|
||||
const auto &themes = Configuration::iaitoInterfaceThemesList();
|
||||
for (int i = 0; i < themes.size(); ++i) {
|
||||
if (themes[i].name == name) {
|
||||
Config()->setInterfaceTheme(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateThemeFromConfig(false);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceEditButton_clicked()
|
||||
{
|
||||
QString theme = ui->themeComboBox->currentText();
|
||||
if (!Configuration::isCustomInterfaceTheme(theme)) {
|
||||
if (!confirmSystemThemeCopy(theme) || !copyInterfaceTheme(theme)) {
|
||||
return;
|
||||
}
|
||||
theme = ui->themeComboBox->currentText();
|
||||
}
|
||||
|
||||
InterfaceThemeEditDialog dialog(theme, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
selectInterfaceThemeByName(dialog.savedName());
|
||||
}
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceCopyButton_clicked()
|
||||
{
|
||||
copyInterfaceTheme(ui->themeComboBox->currentText());
|
||||
}
|
||||
|
||||
bool AppearanceOptionsWidget::copyInterfaceTheme(const QString &theme)
|
||||
{
|
||||
QString newName;
|
||||
do {
|
||||
newName = QInputDialog::getText(
|
||||
this,
|
||||
tr("Enter theme name"),
|
||||
tr("Name:"),
|
||||
QLineEdit::Normal,
|
||||
theme + tr(" - copy"))
|
||||
.trimmed();
|
||||
if (newName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!Configuration::isValidThemeName(newName)) {
|
||||
QMessageBox::warning(this, tr("Theme Copy"), tr("Invalid theme name."));
|
||||
} else if (interfaceThemeExists(newName)) {
|
||||
QMessageBox::information(
|
||||
this, tr("Theme Copy"), tr("Theme named %1 already exists.").arg(newName));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
|
||||
const QString dst
|
||||
= QDir(Configuration::userThemesDir()).filePath(newName + QStringLiteral(".theme"));
|
||||
bool copied = false;
|
||||
if (Configuration::isCustomInterfaceTheme(theme)) {
|
||||
copied = QFile::copy(
|
||||
QDir(Configuration::userThemesDir()).filePath(theme + QStringLiteral(".theme")), dst);
|
||||
} else {
|
||||
Theme t = theme == QStringLiteral("Native") ? IaitoStyle::instance()->theme()
|
||||
: Theme::builtin(theme);
|
||||
t.name = newName;
|
||||
t.save(dst);
|
||||
copied = QFileInfo::exists(dst);
|
||||
}
|
||||
if (!copied) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Cannot copy theme."));
|
||||
return false;
|
||||
}
|
||||
selectInterfaceThemeByName(newName);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceRenameButton_clicked()
|
||||
{
|
||||
const QString current = ui->themeComboBox->currentText();
|
||||
if (!Configuration::isCustomInterfaceTheme(current)) {
|
||||
return;
|
||||
}
|
||||
const QString newName
|
||||
= QInputDialog::getText(
|
||||
this, tr("Enter new theme name"), tr("Name:"), QLineEdit::Normal, current)
|
||||
.trimmed();
|
||||
if (newName.isEmpty() || newName == current) {
|
||||
return;
|
||||
}
|
||||
if (!Configuration::isValidThemeName(newName)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid theme name."));
|
||||
return;
|
||||
}
|
||||
if (interfaceThemeExists(newName)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Theme named %1 already exists.").arg(newName));
|
||||
return;
|
||||
}
|
||||
QDir dir(Configuration::userThemesDir());
|
||||
QFile::rename(
|
||||
dir.filePath(current + QStringLiteral(".theme")),
|
||||
dir.filePath(newName + QStringLiteral(".theme")));
|
||||
selectInterfaceThemeByName(newName);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceDeleteButton_clicked()
|
||||
{
|
||||
const QString current = ui->themeComboBox->currentText();
|
||||
if (!Configuration::isCustomInterfaceTheme(current)) {
|
||||
return;
|
||||
}
|
||||
int ret = QMessageBox::question(
|
||||
this, tr("Delete"), tr("Are you sure you want to delete <b>%1</b>?").arg(current));
|
||||
if (ret != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
QFile::remove(QDir(Configuration::userThemesDir()).filePath(current + QStringLiteral(".theme")));
|
||||
Config()->setInterfaceTheme(0);
|
||||
updateThemeFromConfig(false);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceExportButton_clicked()
|
||||
{
|
||||
const QString current = ui->themeComboBox->currentText();
|
||||
QString file = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
"",
|
||||
QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QDir::separator() + current
|
||||
+ QStringLiteral(".theme"));
|
||||
if (file.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QHash<QString, QColor> vars = Configuration::isCustomInterfaceTheme(current)
|
||||
? Configuration::loadInterfaceThemeVariables(current)
|
||||
: Configuration::defaultInterfaceThemeVariables();
|
||||
QSettings out(file, QSettings::IniFormat);
|
||||
for (const QString &key : Configuration::interfaceThemeVariableKeys()) {
|
||||
const QColor c = vars.value(key);
|
||||
out.setValue(key, c.alpha() == 255 ? c.name(QColor::HexRgb) : c.name(QColor::HexArgb));
|
||||
}
|
||||
out.setValue(QStringLiteral("meta/dark"), Configuration::interfaceThemeIsDark(current));
|
||||
out.setValue(QStringLiteral("meta/name"), current);
|
||||
out.sync();
|
||||
if (out.status() != QSettings::NoError) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Cannot write file."));
|
||||
}
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_ifaceImportButton_clicked()
|
||||
{
|
||||
QString fileName = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
"",
|
||||
QStandardPaths::writableLocation(QStandardPaths::HomeLocation),
|
||||
tr("Interface theme (*.theme)"),
|
||||
nullptr,
|
||||
QFILEDIALOG_FLAGS);
|
||||
if (fileName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QString name = QFileInfo(fileName).completeBaseName();
|
||||
const QString dst
|
||||
= QDir(Configuration::userThemesDir()).filePath(name + QStringLiteral(".theme"));
|
||||
if (QFileInfo::exists(dst)) {
|
||||
QFile::remove(dst);
|
||||
}
|
||||
if (!QFile::copy(fileName, dst)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Cannot import theme."));
|
||||
return;
|
||||
}
|
||||
selectInterfaceThemeByName(name);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::updateInterfaceModificationButtons(const QString &theme)
|
||||
{
|
||||
bool editable = Configuration::isCustomInterfaceTheme(theme);
|
||||
ui->ifaceEditButton->setEnabled(!theme.isEmpty());
|
||||
ui->ifaceRenameButton->setEnabled(editable);
|
||||
ui->ifaceDeleteButton->setEnabled(editable);
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_copyButton_clicked()
|
||||
{
|
||||
QString currColorTheme = ui->colorComboBox->currentText();
|
||||
copyColorTheme(ui->colorComboBox->currentText());
|
||||
}
|
||||
|
||||
bool AppearanceOptionsWidget::copyColorTheme(const QString &theme)
|
||||
{
|
||||
QString newThemeName;
|
||||
do {
|
||||
newThemeName = QInputDialog::getText(
|
||||
@@ -314,10 +553,10 @@ void AppearanceOptionsWidget::on_copyButton_clicked()
|
||||
tr("Enter theme name"),
|
||||
tr("Name:"),
|
||||
QLineEdit::Normal,
|
||||
currColorTheme + tr(" - copy"))
|
||||
theme + tr(" - copy"))
|
||||
.trimmed();
|
||||
if (newThemeName.isEmpty()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (ThemeWorker().isThemeExist(newThemeName)) {
|
||||
QMessageBox::information(
|
||||
@@ -327,9 +566,14 @@ void AppearanceOptionsWidget::on_copyButton_clicked()
|
||||
}
|
||||
} while (true);
|
||||
|
||||
ThemeWorker().copy(currColorTheme, newThemeName);
|
||||
QString err = ThemeWorker().copy(theme, newThemeName);
|
||||
if (!err.isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"), err);
|
||||
return false;
|
||||
}
|
||||
Config()->setColorTheme(newThemeName);
|
||||
updateThemeFromConfig(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::on_deleteButton_clicked()
|
||||
@@ -436,11 +680,26 @@ void AppearanceOptionsWidget::onLanguageComboBoxCurrentIndexChanged(int index)
|
||||
void AppearanceOptionsWidget::updateModificationButtons(const QString &theme)
|
||||
{
|
||||
bool editable = ThemeWorker().isCustomTheme(theme);
|
||||
ui->editButton->setEnabled(editable);
|
||||
ui->editButton->setEnabled(!theme.isEmpty());
|
||||
ui->deleteButton->setEnabled(editable);
|
||||
ui->renameButton->setEnabled(editable);
|
||||
}
|
||||
|
||||
bool AppearanceOptionsWidget::confirmSystemThemeCopy(const QString &theme)
|
||||
{
|
||||
QMessageBox prompt(this);
|
||||
prompt.setIcon(QMessageBox::Question);
|
||||
prompt.setWindowTitle(tr("Edit Theme"));
|
||||
prompt.setText(
|
||||
tr("The system theme <b>%1</b> cannot be edited directly.").arg(theme.toHtmlEscaped()));
|
||||
prompt.setInformativeText(tr("Would you like to make a copy and start editing it?"));
|
||||
QPushButton *copyButton = prompt.addButton(tr("Make a Copy"), QMessageBox::AcceptRole);
|
||||
prompt.addButton(QMessageBox::Cancel);
|
||||
prompt.setDefaultButton(copyButton);
|
||||
prompt.exec();
|
||||
return prompt.clickedButton() == copyButton;
|
||||
}
|
||||
|
||||
void AppearanceOptionsWidget::updateFromConfig()
|
||||
{
|
||||
updateFontFromConfig();
|
||||
|
||||
@@ -38,7 +38,7 @@ private slots:
|
||||
void on_themeComboBox_currentIndexChanged(int index);
|
||||
void on_visualNavbarLocationComboBox_currentIndexChanged(int index);
|
||||
void on_visualNavbarThicknessSpinBox_valueChanged(int value);
|
||||
void on_visualNavbarUseThemeColorsCheckBox_toggled(bool checked);
|
||||
void on_memoryScrollBarComboBox_currentIndexChanged(int index);
|
||||
void on_copyButton_clicked();
|
||||
void on_deleteButton_clicked();
|
||||
|
||||
@@ -59,10 +59,23 @@ private slots:
|
||||
*/
|
||||
void on_renameButton_clicked();
|
||||
void on_editButton_clicked();
|
||||
|
||||
void on_ifaceEditButton_clicked();
|
||||
void on_ifaceCopyButton_clicked();
|
||||
void on_ifaceRenameButton_clicked();
|
||||
void on_ifaceDeleteButton_clicked();
|
||||
void on_ifaceExportButton_clicked();
|
||||
void on_ifaceImportButton_clicked();
|
||||
|
||||
void onLanguageComboBoxCurrentIndexChanged(int index);
|
||||
|
||||
private:
|
||||
bool confirmSystemThemeCopy(const QString &theme);
|
||||
bool copyColorTheme(const QString &theme);
|
||||
bool copyInterfaceTheme(const QString &theme);
|
||||
void updateModificationButtons(const QString &theme);
|
||||
void updateInterfaceModificationButtons(const QString &theme);
|
||||
void selectInterfaceThemeByName(const QString &name);
|
||||
void updateFromConfig();
|
||||
|
||||
/**
|
||||
|
||||
@@ -171,24 +171,115 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="themeComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="interfaceThemeLayout">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Default</string>
|
||||
</property>
|
||||
<widget class="QComboBox" name="themeComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Dark</string>
|
||||
</property>
|
||||
<widget class="QPushButton" name="ifaceEditButton">
|
||||
<property name="toolTip">
|
||||
<string>Edit Theme</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources.qrc">
|
||||
<normaloff>:/img/icons/pencil_thin.svg</normaloff>:/img/icons/pencil_thin.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</widget>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ifaceRenameButton">
|
||||
<property name="toolTip">
|
||||
<string>Rename</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources.qrc">
|
||||
<normaloff>:/img/icons/rename.svg</normaloff>:/img/icons/rename.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ifaceCopyButton">
|
||||
<property name="toolTip">
|
||||
<string>Copy</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources.qrc">
|
||||
<normaloff>:/img/icons/copy.svg</normaloff>:/img/icons/copy.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ifaceDeleteButton">
|
||||
<property name="toolTip">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources.qrc">
|
||||
<normaloff>:/img/icons/trash_bin.svg</normaloff>:/img/icons/trash_bin.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ifaceExportButton">
|
||||
<property name="toolTip">
|
||||
<string>Export</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources.qrc">
|
||||
<normaloff>:/img/icons/upload_black.svg</normaloff>:/img/icons/upload_black.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ifaceImportButton">
|
||||
<property name="toolTip">
|
||||
<string>Import</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources.qrc">
|
||||
<normaloff>:/img/icons/download_black.svg</normaloff>:/img/icons/download_black.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="interfaceThemeSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="visualNavbarLabel">
|
||||
@@ -234,11 +325,28 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="visualNavbarUseThemeColorsCheckBox">
|
||||
<property name="text">
|
||||
<string>Use colors from the current radare2 theme</string>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QHBoxLayout" name="memoryScrollBarLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="memoryScrollBarLabel">
|
||||
<property name="text">
|
||||
<string>Hexdump and disassembly scrollbar</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="memoryScrollBarComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Hidden: no scrollbar. Spring: the handle rests in the middle and scrolls faster the further and longer it is dragged. Bounded: a regular scrollbar over the navigation bar range.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
|
||||
@@ -17,6 +17,12 @@ AsmOptionsWidget::AsmOptionsWidget(SettingsDialog *dialog)
|
||||
, ui(new Ui::AsmOptionsWidget)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->jumpLineSpacingComboBox->blockSignals(true);
|
||||
for (int spacing : {2, 4, 6, 8, 10, 12, 16, 20, 24, 32}) {
|
||||
ui->jumpLineSpacingComboBox->addItem(tr("%1 px").arg(spacing), spacing);
|
||||
}
|
||||
ui->jumpLineSpacingComboBox->blockSignals(false);
|
||||
|
||||
// Initialize "Relative to" combobox for asm.addr.relto values
|
||||
ui->relToComboBox->setEditable(true);
|
||||
ui->relToComboBox->blockSignals(true);
|
||||
@@ -47,6 +53,18 @@ AsmOptionsWidget::AsmOptionsWidget(SettingsDialog *dialog)
|
||||
this,
|
||||
&AsmOptionsWidget::on_relToComboBox_currentIndexChanged);
|
||||
|
||||
// Initialize "Address base" combobox for asm.addr.base values
|
||||
ui->addrBaseComboBox->blockSignals(true);
|
||||
for (const auto &opt : Config()->getAddrBaseOptions()) {
|
||||
ui->addrBaseComboBox->addItem(opt.second, opt.first);
|
||||
}
|
||||
ui->addrBaseComboBox->blockSignals(false);
|
||||
connect(
|
||||
ui->addrBaseComboBox,
|
||||
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
|
||||
this,
|
||||
&AsmOptionsWidget::on_addrBaseComboBox_currentIndexChanged);
|
||||
|
||||
ui->syntaxComboBox->blockSignals(true);
|
||||
for (const auto &syntax : Core()->cmdList("e asm.syntax=?"))
|
||||
ui->syntaxComboBox->addItem(syntax, syntax);
|
||||
@@ -104,6 +122,13 @@ AsmOptionsWidget::AsmOptionsWidget(SettingsDialog *dialog)
|
||||
connect(ui->offsetCheckBox, &QCheckBox::toggled, this, &AsmOptionsWidget::offsetCheckBoxToggled);
|
||||
connect(
|
||||
ui->relOffsetCheckBox, &QCheckBox::toggled, this, &AsmOptionsWidget::relOffCheckBoxToggled);
|
||||
connect(ui->linesCheckBox, &QCheckBox::toggled, ui->jumpLineSpacingLabel, &QWidget::setEnabled);
|
||||
connect(ui->linesCheckBox, &QCheckBox::toggled, ui->jumpLineSpacingComboBox, &QWidget::setEnabled);
|
||||
connect(
|
||||
ui->linesCheckBox,
|
||||
&QCheckBox::toggled,
|
||||
ui->adaptiveJumpLineSpacingCheckBox,
|
||||
&QWidget::setEnabled);
|
||||
connect(Core(), &IaitoCore::asmOptionsChanged, this, &AsmOptionsWidget::updateAsmOptionsFromVars);
|
||||
updateAsmOptionsFromVars();
|
||||
}
|
||||
@@ -171,6 +196,19 @@ void AsmOptionsWidget::updateAsmOptionsFromVars()
|
||||
qhelpers::setCheckedWithoutSignals(
|
||||
confCheckbox->checkBox, Config()->getConfigBool(confCheckbox->config));
|
||||
}
|
||||
const bool adaptiveSpacing = Config()->getDisassemblyJumpLineAdaptiveSpacingEnabled();
|
||||
qhelpers::setCheckedWithoutSignals(ui->adaptiveJumpLineSpacingCheckBox, adaptiveSpacing);
|
||||
ui->jumpLineSpacingLabel->setText(
|
||||
adaptiveSpacing ? tr("Maximum spacing:") : tr("Fixed spacing:"));
|
||||
ui->jumpLineSpacingComboBox->blockSignals(true);
|
||||
const int spacingIndex = ui->jumpLineSpacingComboBox->findData(
|
||||
Config()->getDisassemblyJumpLineSpacing());
|
||||
ui->jumpLineSpacingComboBox->setCurrentIndex(spacingIndex < 0 ? 0 : spacingIndex);
|
||||
ui->jumpLineSpacingComboBox->blockSignals(false);
|
||||
const bool jumpLinesEnabled = Config()->getConfigBool("asm.lines");
|
||||
ui->adaptiveJumpLineSpacingCheckBox->setEnabled(jumpLinesEnabled);
|
||||
ui->jumpLineSpacingLabel->setEnabled(jumpLinesEnabled);
|
||||
ui->jumpLineSpacingComboBox->setEnabled(jumpLinesEnabled);
|
||||
// Update "Relative to" combobox based on asm.reloff
|
||||
bool reloffEnabled = Config()->getConfigBool("asm.reloff");
|
||||
ui->relToComboBox->blockSignals(true);
|
||||
@@ -183,6 +221,14 @@ void AsmOptionsWidget::updateAsmOptionsFromVars()
|
||||
ui->relToComboBox->setEditText(curRelTo);
|
||||
}
|
||||
ui->relToComboBox->blockSignals(false);
|
||||
|
||||
// Update "Address base" combobox based on asm.addr.base
|
||||
ui->addrBaseComboBox->blockSignals(true);
|
||||
int idxBase = ui->addrBaseComboBox->findData(Config()->getAddrBase());
|
||||
if (idxBase >= 0) {
|
||||
ui->addrBaseComboBox->setCurrentIndex(idxBase);
|
||||
}
|
||||
ui->addrBaseComboBox->blockSignals(false);
|
||||
}
|
||||
|
||||
void AsmOptionsWidget::resetToDefault()
|
||||
@@ -222,6 +268,22 @@ void AsmOptionsWidget::on_nbytesSpinBox_valueChanged(int value)
|
||||
triggerAsmOptionsChanged();
|
||||
}
|
||||
|
||||
void AsmOptionsWidget::on_jumpLineSpacingComboBox_currentIndexChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
Config()->setDisassemblyJumpLineSpacing(ui->jumpLineSpacingComboBox->itemData(index).toInt());
|
||||
triggerAsmOptionsChanged();
|
||||
}
|
||||
|
||||
void AsmOptionsWidget::on_adaptiveJumpLineSpacingCheckBox_toggled(bool checked)
|
||||
{
|
||||
Config()->setDisassemblyJumpLineAdaptiveSpacingEnabled(checked);
|
||||
ui->jumpLineSpacingLabel->setText(checked ? tr("Maximum spacing:") : tr("Fixed spacing:"));
|
||||
triggerAsmOptionsChanged();
|
||||
}
|
||||
|
||||
void AsmOptionsWidget::on_syntaxComboBox_currentIndexChanged(int index)
|
||||
{
|
||||
Config()->setConfig(
|
||||
@@ -347,3 +409,10 @@ void AsmOptionsWidget::on_relToComboBox_currentIndexChanged(const QString &text)
|
||||
Config()->setConfig("asm.addr.relto", text);
|
||||
triggerAsmOptionsChanged();
|
||||
}
|
||||
|
||||
// Handle changes to "Address base" combobox for asm.addr.base
|
||||
void AsmOptionsWidget::on_addrBaseComboBox_currentIndexChanged(int index)
|
||||
{
|
||||
Config()->setAddrBase(ui->addrBaseComboBox->itemData(index).toString());
|
||||
triggerAsmOptionsChanged();
|
||||
}
|
||||
@@ -45,6 +45,8 @@ private slots:
|
||||
void on_asmTabsSpinBox_valueChanged(int value);
|
||||
void on_asmTabsOffSpinBox_valueChanged(int value);
|
||||
void on_nbytesSpinBox_valueChanged(int value);
|
||||
void on_jumpLineSpacingComboBox_currentIndexChanged(int index);
|
||||
void on_adaptiveJumpLineSpacingCheckBox_toggled(bool checked);
|
||||
|
||||
void on_bytesCheckBox_toggled(bool checked);
|
||||
void on_varsubCheckBox_toggled(bool checked);
|
||||
@@ -59,6 +61,10 @@ private slots:
|
||||
* @brief Called when the user selects or edits a "Relative to" base
|
||||
*/
|
||||
void on_relToComboBox_currentIndexChanged(const QString &text);
|
||||
/**
|
||||
* @brief Called when the user selects an "Address base" (asm.addr.base)
|
||||
*/
|
||||
void on_addrBaseComboBox_currentIndexChanged(int index);
|
||||
void checkboxEnabler(QCheckBox *checkbox, QString config);
|
||||
};
|
||||
|
||||
|
||||
@@ -140,6 +140,27 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="addrBaseLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="addrBaseLabel">
|
||||
<property name="text">
|
||||
<string>Address base:</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>asm.addr.base</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="addrBaseComboBox">
|
||||
<property name="toolTip">
|
||||
<string>asm.addr.base</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="relOffsetLabel">
|
||||
<property name="text">
|
||||
@@ -413,6 +434,34 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="adaptiveJumpLineSpacingCheckBox">
|
||||
<property name="text">
|
||||
<string>Fit jump lines to gutter</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Shrink jump line spacing as needed to keep every lane visible</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="jumpLineSpacingLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="jumpLineSpacingLabel">
|
||||
<property name="text">
|
||||
<string>Maximum spacing:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="jumpLineSpacingComboBox">
|
||||
<property name="toolTip">
|
||||
<string>Maximum spacing when fitting to the gutter, or exact spacing otherwise</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="fcnlinesCheckBox">
|
||||
<property name="text">
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
#include "InterfaceThemeEditDialog.h"
|
||||
|
||||
#include "common/Configuration.h"
|
||||
#include "common/theme/IaitoStyle.h"
|
||||
#include "widgets/ColorPicker.h"
|
||||
#include "widgets/ColorThemeListView.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QSpinBox>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
struct MetricField
|
||||
{
|
||||
const char *label;
|
||||
int ThemeMetrics::*field;
|
||||
int min;
|
||||
int max;
|
||||
};
|
||||
|
||||
const MetricField kEditorMetrics[] = {
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Corner radius"),
|
||||
&ThemeMetrics::borderRadius,
|
||||
0,
|
||||
24},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Checkbox radius"),
|
||||
&ThemeMetrics::checkBoxRadius,
|
||||
0,
|
||||
16},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Border width"), &ThemeMetrics::borderWidth, 0, 4},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Focus width"), &ThemeMetrics::focusWidth, 0, 6},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Spacing"), &ThemeMetrics::spacing, 0, 24},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Menu padding"),
|
||||
&ThemeMetrics::menuItemPadding,
|
||||
0,
|
||||
24},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Control height"),
|
||||
&ThemeMetrics::controlHeight,
|
||||
16,
|
||||
48},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Checkbox size"),
|
||||
&ThemeMetrics::checkBoxSize,
|
||||
10,
|
||||
28},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Icon size"), &ThemeMetrics::iconSize, 8, 32},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Scrollbar thickness"),
|
||||
&ThemeMetrics::scrollBarThickness,
|
||||
6,
|
||||
24},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Slider groove"),
|
||||
&ThemeMetrics::sliderGrooveHeight,
|
||||
2,
|
||||
16},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Tab border"),
|
||||
&ThemeMetrics::tabBorderWidth,
|
||||
0,
|
||||
4},
|
||||
{QT_TRANSLATE_NOOP("InterfaceThemeEditDialog", "Tab indicator"),
|
||||
&ThemeMetrics::tabIndicatorWidth,
|
||||
0,
|
||||
8},
|
||||
};
|
||||
const int kEditorMetricCount = int(sizeof(kEditorMetrics) / sizeof(kEditorMetrics[0]));
|
||||
|
||||
Theme loadSourceTheme(const QString &name)
|
||||
{
|
||||
if (!name.isEmpty() && Configuration::isCustomInterfaceTheme(name)) {
|
||||
return Theme::load(
|
||||
QDir(Configuration::userThemesDir()).filePath(name + QStringLiteral(".theme")),
|
||||
Configuration::interfaceThemeIsDark(name));
|
||||
}
|
||||
if (!name.isEmpty() && name != QStringLiteral("Native")) {
|
||||
return Theme::builtin(name);
|
||||
}
|
||||
if (IaitoStyle::instance()) {
|
||||
return IaitoStyle::instance()->theme();
|
||||
}
|
||||
return Theme::builtin(QStringLiteral("Dark"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
InterfaceThemeEditDialog::InterfaceThemeEditDialog(const QString &themeName, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(
|
||||
themeName.isEmpty() ? tr("Interface Theme Editor")
|
||||
: tr("Interface Theme Editor - <%1>").arg(themeName));
|
||||
|
||||
m_base = loadSourceTheme(themeName);
|
||||
m_vars = m_base.toChrome();
|
||||
m_dark = m_base.dark;
|
||||
|
||||
listView = new ColorThemeListView(this);
|
||||
listView->setSource(ColorSettingsModel::InterfacePalette);
|
||||
listView->colorSettingsModel()->setInterfaceColors(m_vars);
|
||||
colorPicker = new ColorPicker(this);
|
||||
|
||||
auto *top = new QHBoxLayout;
|
||||
top->addWidget(listView, 1);
|
||||
top->addWidget(colorPicker);
|
||||
|
||||
previewTimer = new QTimer(this);
|
||||
previewTimer->setSingleShot(true);
|
||||
previewTimer->setInterval(50);
|
||||
connect(previewTimer, &QTimer::timeout, this, [this]() {
|
||||
Config()->loadEngineTheme(buildTheme());
|
||||
emit Config() -> colorsUpdated();
|
||||
});
|
||||
|
||||
skinCombo = new QComboBox(this);
|
||||
skinCombo->addItem(tr("Modern (flat)"));
|
||||
skinCombo->addItem(tr("Bevel (3D)"));
|
||||
skinCombo->setCurrentIndex(m_base.skin == Skin::Bevel ? 1 : 0);
|
||||
chromeCombo = new QComboBox(this);
|
||||
chromeCombo->addItem(tr("Flat"));
|
||||
chromeCombo->addItem(tr("Accent"));
|
||||
chromeCombo->addItem(tr("Accent tabs"));
|
||||
chromeCombo->setCurrentIndex(static_cast<int>(m_base.chromeStyle));
|
||||
connect(skinCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int) {
|
||||
previewTimer->start();
|
||||
});
|
||||
connect(chromeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int) {
|
||||
previewTimer->start();
|
||||
});
|
||||
auto *styleRow = new QHBoxLayout;
|
||||
styleRow->addWidget(new QLabel(tr("Skin:"), this));
|
||||
styleRow->addWidget(skinCombo);
|
||||
styleRow->addSpacing(12);
|
||||
styleRow->addWidget(new QLabel(tr("Chrome:"), this));
|
||||
styleRow->addWidget(chromeCombo);
|
||||
styleRow->addStretch(1);
|
||||
|
||||
auto *metricsBox = new QGroupBox(tr("Metrics"), this);
|
||||
auto *grid = new QGridLayout(metricsBox);
|
||||
for (int i = 0; i < kEditorMetricCount; ++i) {
|
||||
const MetricField &m = kEditorMetrics[i];
|
||||
auto *spin = new QSpinBox(this);
|
||||
spin->setRange(m.min, m.max);
|
||||
spin->setSuffix(tr(" px"));
|
||||
spin->setValue(m_base.metrics.*(m.field));
|
||||
connect(spin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int) {
|
||||
previewTimer->start();
|
||||
});
|
||||
metricSpins.append(spin);
|
||||
const int row = i / 2;
|
||||
const int col = (i % 2) * 2;
|
||||
grid->addWidget(new QLabel(tr(m.label), this), row, col);
|
||||
grid->addWidget(spin, row, col + 1);
|
||||
}
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->setColumnStretch(3, 1);
|
||||
|
||||
nameEdit = new QLineEdit(this);
|
||||
nameEdit->setPlaceholderText(tr("Theme name"));
|
||||
nameEdit->setText(themeName);
|
||||
modeCombo = new QComboBox(this);
|
||||
modeCombo->addItem(tr("Dark"));
|
||||
modeCombo->addItem(tr("Light"));
|
||||
modeCombo->setCurrentIndex(m_dark ? 0 : 1);
|
||||
connect(modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int idx) {
|
||||
m_dark = (idx == 0);
|
||||
previewTimer->start();
|
||||
});
|
||||
auto *nameRow = new QHBoxLayout;
|
||||
nameRow->addWidget(new QLabel(tr("Save as:"), this));
|
||||
nameRow->addWidget(nameEdit, 1);
|
||||
nameRow->addWidget(new QLabel(tr("Mode:"), this));
|
||||
nameRow->addWidget(modeCombo);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, this);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(top);
|
||||
layout->addLayout(styleRow);
|
||||
layout->addWidget(metricsBox);
|
||||
layout->addLayout(nameRow);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(listView, &ColorThemeListView::itemChanged, colorPicker, &ColorPicker::updateColor);
|
||||
connect(
|
||||
colorPicker,
|
||||
&ColorPicker::colorChanged,
|
||||
this,
|
||||
&InterfaceThemeEditDialog::colorOptionChanged);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
listView->setCurrentIndex(listView->model()->index(0, 0));
|
||||
}
|
||||
|
||||
Theme InterfaceThemeEditDialog::buildTheme() const
|
||||
{
|
||||
Theme t = Theme::fromChrome(m_vars, m_dark);
|
||||
t.skin = skinCombo->currentIndex() == 1 ? Skin::Bevel : Skin::Modern;
|
||||
t.chromeStyle = static_cast<ChromeStyle>(chromeCombo->currentIndex());
|
||||
for (int i = 0; i < metricSpins.size(); ++i) {
|
||||
t.metrics.*(kEditorMetrics[i].field) = metricSpins[i]->value();
|
||||
}
|
||||
t.fonts = m_base.fonts;
|
||||
t.bevelHighlight = m_base.bevelHighlight;
|
||||
t.bevelLight = m_base.bevelLight;
|
||||
t.bevelDark = m_base.bevelDark;
|
||||
t.bevelShadow = m_base.bevelShadow;
|
||||
t.tooltipBase = m_base.tooltipBase;
|
||||
t.tooltipText = m_base.tooltipText;
|
||||
t.name = nameEdit->text().trimmed();
|
||||
t.dark = m_dark;
|
||||
return t;
|
||||
}
|
||||
|
||||
void InterfaceThemeEditDialog::colorOptionChanged(const QColor &newColor)
|
||||
{
|
||||
const QModelIndex idx = listView->currentIndex();
|
||||
if (!idx.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ColorOption opt = idx.data(Qt::UserRole).value<ColorOption>();
|
||||
opt.color = newColor;
|
||||
opt.changed = true;
|
||||
listView->model()->setData(idx, QVariant::fromValue(opt));
|
||||
|
||||
m_vars[opt.optionName] = newColor;
|
||||
previewTimer->start();
|
||||
}
|
||||
|
||||
void InterfaceThemeEditDialog::accept()
|
||||
{
|
||||
const QString name = nameEdit->text().trimmed();
|
||||
if (name.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Interface Theme Editor"), tr("Please enter a theme name."));
|
||||
return;
|
||||
}
|
||||
if (!Configuration::isValidThemeName(name)) {
|
||||
QMessageBox::warning(this, tr("Interface Theme Editor"), tr("Invalid theme name."));
|
||||
return;
|
||||
}
|
||||
|
||||
Theme t = buildTheme();
|
||||
t.save(QDir(Configuration::userThemesDir()).filePath(name + QStringLiteral(".theme")));
|
||||
m_savedName = name;
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void InterfaceThemeEditDialog::reject()
|
||||
{
|
||||
Config()->setInterfaceTheme(Config()->getInterfaceTheme());
|
||||
QDialog::reject();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#ifndef INTERFACETHEMEEDITDIALOG_H
|
||||
#define INTERFACETHEMEEDITDIALOG_H
|
||||
|
||||
#include "common/theme/Theme.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QDialog>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class ColorThemeListView;
|
||||
class ColorPicker;
|
||||
class QLineEdit;
|
||||
class QComboBox;
|
||||
class QSpinBox;
|
||||
class QTimer;
|
||||
|
||||
class InterfaceThemeEditDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InterfaceThemeEditDialog(
|
||||
const QString &themeName = QString(), QWidget *parent = nullptr);
|
||||
|
||||
QString savedName() const { return m_savedName; }
|
||||
|
||||
public slots:
|
||||
void accept() override;
|
||||
void reject() override;
|
||||
|
||||
private slots:
|
||||
void colorOptionChanged(const QColor &newColor);
|
||||
|
||||
private:
|
||||
Theme buildTheme() const;
|
||||
|
||||
ColorThemeListView *listView;
|
||||
ColorPicker *colorPicker;
|
||||
QLineEdit *nameEdit;
|
||||
QComboBox *modeCombo;
|
||||
QComboBox *skinCombo;
|
||||
QComboBox *chromeCombo;
|
||||
QVector<QSpinBox *> metricSpins;
|
||||
QTimer *previewTimer;
|
||||
QHash<QString, QColor> m_vars;
|
||||
Theme m_base;
|
||||
QString m_savedName;
|
||||
bool m_dark;
|
||||
};
|
||||
|
||||
#endif // INTERFACETHEMEEDITDIALOG_H
|
||||
@@ -1,33 +0,0 @@
|
||||
#include "KeyboardOptionsWidget.h"
|
||||
#include "SettingsDialog.h"
|
||||
#include "common/Configuration.h"
|
||||
#include "ui_KeyboardOptionsWidget.h"
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QString>
|
||||
|
||||
KeyboardOptionsWidget::KeyboardOptionsWidget(SettingsDialog *dialog)
|
||||
: QDialog(dialog)
|
||||
, ui(new Ui::KeyboardOptionsWidget)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
ui->setupUi(this);
|
||||
// Load existing key.fX configurations and connect signals
|
||||
for (int i = 1; i <= 12; ++i) {
|
||||
const QString key = QStringLiteral("key.f%1").arg(i);
|
||||
QLineEdit *le = findChild<QLineEdit *>(QStringLiteral("f%1LineEdit").arg(i));
|
||||
if (!le) {
|
||||
continue;
|
||||
}
|
||||
// Initialize without triggering signal
|
||||
le->blockSignals(true);
|
||||
le->setText(Core()->getConfig(key));
|
||||
le->blockSignals(false);
|
||||
// Update config when edited
|
||||
connect(le, &QLineEdit::textChanged, this, [key](const QString &text) {
|
||||
Config()->setConfig(key, text);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
KeyboardOptionsWidget::~KeyboardOptionsWidget() {}
|
||||
@@ -1,23 +0,0 @@
|
||||
// KeyboardOptionsWidget allows configuration of radare2 function key eval variables (key.f1..key.f12)
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <QDialog>
|
||||
|
||||
class SettingsDialog;
|
||||
|
||||
namespace Ui {
|
||||
class KeyboardOptionsWidget;
|
||||
}
|
||||
|
||||
class KeyboardOptionsWidget : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit KeyboardOptionsWidget(SettingsDialog *parent);
|
||||
~KeyboardOptionsWidget();
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::KeyboardOptionsWidget> ui;
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>KeyboardOptionsWidget</class>
|
||||
<widget class="QWidget" name="KeyboardOptionsWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="descriptionLabel">
|
||||
<property name="text">
|
||||
<string>Map radare2 commands to function keys (F1-F12):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="f1Label">
|
||||
<property name="text"><string>F1</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="f1LineEdit"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="f7Label">
|
||||
<property name="text"><string>F7</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QLineEdit" name="f7LineEdit"/>
|
||||
</item>
|
||||
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="f2Label">
|
||||
<property name="text"><string>F2</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="f2LineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLabel" name="f8Label">
|
||||
<property name="text"><string>F8</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QLineEdit" name="f8LineEdit"/>
|
||||
</item>
|
||||
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="f3Label">
|
||||
<property name="text"><string>F3</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="f3LineEdit"/>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="f9Label">
|
||||
<property name="text"><string>F9</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QLineEdit" name="f9LineEdit"/>
|
||||
</item>
|
||||
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="f4Label">
|
||||
<property name="text"><string>F4</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="f4LineEdit"/>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QLabel" name="f10Label">
|
||||
<property name="text"><string>F10</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="QLineEdit" name="f10LineEdit"/>
|
||||
</item>
|
||||
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="f5Label">
|
||||
<property name="text"><string>F5</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="f5LineEdit"/>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QLabel" name="f11Label">
|
||||
<property name="text"><string>F11</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<widget class="QLineEdit" name="f11LineEdit"/>
|
||||
</item>
|
||||
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="f6Label">
|
||||
<property name="text"><string>F6</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="f6LineEdit"/>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="QLabel" name="f12Label">
|
||||
<property name="text"><string>F12</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="3">
|
||||
<widget class="QLineEdit" name="f12LineEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "SamplesOptionsWidget.h"
|
||||
#include "SettingsDialog.h"
|
||||
#include "ui_SamplesOptionsWidget.h"
|
||||
|
||||
#include "common/Configuration.h"
|
||||
#include "common/SamplesDB.h"
|
||||
|
||||
static const char *DEFAULT_LOOKUP_URL = "https://www.virustotal.com/gui/file/";
|
||||
|
||||
SamplesOptionsWidget::SamplesOptionsWidget(SettingsDialog *dialog)
|
||||
: QDialog(dialog)
|
||||
, ui(new Ui::SamplesOptionsWidget)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
ui->lookupUrlEdit->setText(Config()->getSha256LookupBaseUrl());
|
||||
ui->dbPathLabel->setText(SamplesDB::dbRoot());
|
||||
|
||||
connect(
|
||||
ui->lookupUrlEdit,
|
||||
&QLineEdit::editingFinished,
|
||||
this,
|
||||
&SamplesOptionsWidget::updateLookupUrl);
|
||||
connect(ui->resetUrlButton, &QPushButton::clicked, this, &SamplesOptionsWidget::resetLookupUrl);
|
||||
}
|
||||
|
||||
SamplesOptionsWidget::~SamplesOptionsWidget() {}
|
||||
|
||||
void SamplesOptionsWidget::updateLookupUrl()
|
||||
{
|
||||
Config()->setSha256LookupBaseUrl(ui->lookupUrlEdit->text());
|
||||
}
|
||||
|
||||
void SamplesOptionsWidget::resetLookupUrl()
|
||||
{
|
||||
ui->lookupUrlEdit->setText(QString::fromLatin1(DEFAULT_LOOKUP_URL));
|
||||
Config()->setSha256LookupBaseUrl(ui->lookupUrlEdit->text());
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class SettingsDialog;
|
||||
|
||||
namespace Ui {
|
||||
class SamplesOptionsWidget;
|
||||
}
|
||||
|
||||
class SamplesOptionsWidget : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SamplesOptionsWidget(SettingsDialog *dialog);
|
||||
~SamplesOptionsWidget();
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::SamplesOptionsWidget> ui;
|
||||
|
||||
private slots:
|
||||
void updateLookupUrl();
|
||||
void resetLookupUrl();
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SamplesOptionsWidget</class>
|
||||
<widget class="QWidget" name="SamplesOptionsWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>489</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Samples</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::ExpandingFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lookupUrlLabel">
|
||||
<property name="text">
|
||||
<string>SHA256 lookup URL:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="lookupUrlLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lookupUrlEdit">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>https://www.virustotal.com/gui/file/</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="resetUrlButton">
|
||||
<property name="text">
|
||||
<string>Reset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="dbPathTitleLabel">
|
||||
<property name="text">
|
||||
<string>Database:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="dbPathLabel">
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="hintLabel">
|
||||
<property name="text">
|
||||
<string>The lookup URL is opened with a file's SHA256 appended. Files you open are registered automatically; use the Sha256 tab in the New File dialog to scan folders or manage entries.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -7,7 +7,8 @@
|
||||
#include "AsmOptionsWidget.h"
|
||||
#include "DebugOptionsWidget.h"
|
||||
#include "GraphOptionsWidget.h"
|
||||
#include "KeyboardOptionsWidget.h"
|
||||
#include "SamplesOptionsWidget.h"
|
||||
#include "ShortcutsOptionsWidget.h"
|
||||
#include "WebserverOptionsWidget.h"
|
||||
|
||||
#include "../ScriptManagerWidget.h"
|
||||
@@ -19,6 +20,7 @@
|
||||
#include "common/Helpers.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
@@ -28,6 +30,7 @@ SettingsDialog::SettingsDialog(QWidget *parent)
|
||||
ui->setupUi(this);
|
||||
setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));
|
||||
ui->logoSvgWidget->load(Config()->getLogoFile());
|
||||
pluginsDialog = new R2PluginsDialog(this, false);
|
||||
|
||||
QList<SettingCategory> prefs{
|
||||
|
||||
@@ -49,12 +52,13 @@ SettingsDialog::SettingsDialog(QWidget *parent)
|
||||
{// we must deprecate all the iaito plugins, just use r2 ones
|
||||
tr("Plugins"),
|
||||
// new PluginsOptionsWidget(this),
|
||||
new R2PluginsDialog(this),
|
||||
pluginsDialog,
|
||||
QIcon(":/img/icons/plugins.svg")},
|
||||
{tr("Scripts"), new ScriptManagerWidget(this), QIcon(":/img/icons/initialization.svg")},
|
||||
{tr("Analysis"), new AnalOptionsWidget(this), QIcon(":/img/icons/cog_light.svg")},
|
||||
{tr("Keyboard"), new KeyboardOptionsWidget(this), QIcon(":/img/icons/download_black.svg")},
|
||||
{tr("Webserver"), new WebserverOptionsWidget(this), QIcon(":/img/icons/cloud.svg")}};
|
||||
{tr("Shortcuts"), new ShortcutsOptionsWidget(this), QIcon(":/img/icons/download_black.svg")},
|
||||
{tr("Webserver"), new WebserverOptionsWidget(this), QIcon(":/img/icons/cloud.svg")},
|
||||
{tr("Samples"), new SamplesOptionsWidget(this), QIcon(":/img/icons/cog_light.svg")}};
|
||||
|
||||
for (auto &c : prefs) {
|
||||
c.addItem(*ui->configCategories, *ui->configPanel);
|
||||
@@ -62,9 +66,12 @@ SettingsDialog::SettingsDialog(QWidget *parent)
|
||||
|
||||
connect(ui->configCategories, &QTreeWidget::currentItemChanged, this, &SettingsDialog::changePage);
|
||||
connect(ui->saveButtons, &QDialogButtonBox::accepted, this, &QWidget::close);
|
||||
loadPluginButton = ui->saveButtons->addButton(tr("Load plugin"), QDialogButtonBox::ActionRole);
|
||||
connect(loadPluginButton, &QPushButton::clicked, pluginsDialog, &R2PluginsDialog::loadPlugin);
|
||||
|
||||
QTreeWidgetItem *defitem = ui->configCategories->topLevelItem(0);
|
||||
ui->configCategories->setCurrentItem(defitem, 0);
|
||||
updateLoadPluginButton();
|
||||
|
||||
connect(Config(), &Configuration::interfaceThemeChanged, this, &SettingsDialog::chooseThemeIcons);
|
||||
chooseThemeIcons();
|
||||
@@ -103,6 +110,14 @@ void SettingsDialog::changePage(QTreeWidgetItem *current, QTreeWidgetItem *previ
|
||||
|
||||
if (index)
|
||||
ui->configPanel->setCurrentIndex(index - 1);
|
||||
updateLoadPluginButton();
|
||||
}
|
||||
|
||||
void SettingsDialog::updateLoadPluginButton()
|
||||
{
|
||||
if (loadPluginButton) {
|
||||
loadPluginButton->setVisible(ui->configPanel->currentWidget() == pluginsDialog);
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::chooseThemeIcons()
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QPushButton;
|
||||
class R2PluginsDialog;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
namespace Ui {
|
||||
@@ -31,7 +33,11 @@ public slots:
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::SettingsDialog> ui;
|
||||
R2PluginsDialog *pluginsDialog = nullptr;
|
||||
QPushButton *loadPluginButton = nullptr;
|
||||
|
||||
void chooseThemeIcons();
|
||||
void updateLoadPluginButton();
|
||||
};
|
||||
|
||||
#endif // SETTINGSDIALOG_H
|
||||
Loaded 100 of 377 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user