├── resources ├── html.def ├── yaml.def ├── properties.def ├── strings.def ├── numbers.def ├── icon.ico ├── icon.png ├── icon.psd ├── icon.icns ├── splash.png ├── splash.psd ├── fugue │ ├── color.png │ ├── gear.png │ ├── android.png │ ├── binocular.png │ ├── edit-replace.png │ ├── edit-signiture.png │ └── application-terminal.png ├── screenshot.png ├── checkerboard.png ├── icons8 │ ├── icons8-gear-48.png │ ├── icons8-folder-48.png │ ├── icons8-hammer-48.png │ ├── icons8-android-os-48.png │ └── icons8-software-installer-48.png ├── yml.def ├── smali.def ├── apkstudio.desktop ├── xml.def ├── dark.theme ├── light.theme ├── java.def ├── about.html └── all.qrc ├── .gitmodules ├── sources ├── hexedit.h ├── versionresolveworker.h ├── signingconfigdialog.h ├── adbinstallworker.h ├── desktopdatabaseupdateworker.h ├── splashwindow.h ├── apkrecompileworker.h ├── appearancesettingswidget.h ├── imageviewerwidget.h ├── apksignworker.h ├── devicelistworker.h ├── signingconfigwidget.h ├── settingsdialog.h ├── hexedit.cpp ├── binarysettingswidget.h ├── apkdecompileworker.h ├── themedsyntaxhighlighter.h ├── apkdecompiledialog.h ├── adbinstallworker.cpp ├── flickcharm.h ├── findreplacedialog.h ├── deviceselectiondialog.h ├── signingconfigdialog.cpp ├── processutils.h ├── tooldownloaddialog.h ├── desktopdatabaseupdateworker.cpp ├── tooldownloadworker.h ├── apkrecompileworker.cpp ├── apksignworker.cpp ├── sourcecodeedit.h ├── settingsdialog.cpp ├── imageviewerwidget.cpp ├── apkdecompileworker.cpp ├── appearancesettingswidget.cpp ├── signingconfigwidget.cpp ├── splashwindow.cpp ├── devicelistworker.cpp ├── apkdecompiledialog.cpp ├── main.cpp ├── findreplacedialog.cpp ├── mainwindow.h ├── themedsyntaxhighlighter.cpp ├── versionresolveworker.cpp ├── flickcharm.cpp ├── processutils.cpp ├── binarysettingswidget.cpp ├── deviceselectiondialog.cpp ├── tooldownloaddialog.cpp └── sourcecodeedit.cpp ├── .gitignore ├── README.md ├── LICENSE ├── CMakeLists.txt ├── docs └── index.html └── .github └── workflows └── build.yml /resources/html.def: -------------------------------------------------------------------------------- 1 | @include html 2 | -------------------------------------------------------------------------------- /resources/yaml.def: -------------------------------------------------------------------------------- 1 | @include yml 2 | -------------------------------------------------------------------------------- /resources/properties.def: -------------------------------------------------------------------------------- 1 | keywords \b[a-zA-Z0-9.]+= 2 | -------------------------------------------------------------------------------- /resources/strings.def: -------------------------------------------------------------------------------- 1 | strings "[^<"]*" 2 | strings '[^<']*' 3 | -------------------------------------------------------------------------------- /resources/numbers.def: -------------------------------------------------------------------------------- 1 | numbers [-+]?\b\d+\b 2 | numbers \b0[xX][0-9a-fA-F]+\b 3 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icon.psd -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icon.icns -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/splash.png -------------------------------------------------------------------------------- /resources/splash.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/splash.psd -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "QHexView"] 2 | path = QHexView 3 | url = https://github.com/Dax89/QHexView.git 4 | -------------------------------------------------------------------------------- /resources/fugue/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/fugue/color.png -------------------------------------------------------------------------------- /resources/fugue/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/fugue/gear.png -------------------------------------------------------------------------------- /resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/screenshot.png -------------------------------------------------------------------------------- /resources/checkerboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/checkerboard.png -------------------------------------------------------------------------------- /resources/fugue/android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/fugue/android.png -------------------------------------------------------------------------------- /resources/fugue/binocular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/fugue/binocular.png -------------------------------------------------------------------------------- /resources/fugue/edit-replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/fugue/edit-replace.png -------------------------------------------------------------------------------- /resources/fugue/edit-signiture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/fugue/edit-signiture.png -------------------------------------------------------------------------------- /resources/icons8/icons8-gear-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icons8/icons8-gear-48.png -------------------------------------------------------------------------------- /resources/icons8/icons8-folder-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icons8/icons8-folder-48.png -------------------------------------------------------------------------------- /resources/icons8/icons8-hammer-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icons8/icons8-hammer-48.png -------------------------------------------------------------------------------- /resources/fugue/application-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/fugue/application-terminal.png -------------------------------------------------------------------------------- /resources/icons8/icons8-android-os-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icons8/icons8-android-os-48.png -------------------------------------------------------------------------------- /resources/icons8/icons8-software-installer-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaibhavpandeyvpz/apkstudio/HEAD/resources/icons8/icons8-software-installer-48.png -------------------------------------------------------------------------------- /resources/yml.def: -------------------------------------------------------------------------------- 1 | @include numbers 2 | @include strings 3 | namespace !!.*$ 4 | strings [-:](\s+)?.*$ 5 | strings (\s+)?.*$ 6 | variables \b[^:].*: 7 | comments #[^\n]* 8 | -------------------------------------------------------------------------------- /resources/smali.def: -------------------------------------------------------------------------------- 1 | @include numbers 2 | variables \b[pv][0-9]+\b 3 | namespace (?<=L)([a-zA-Z0-9/]+)(?=;) 4 | keywords [.][a-z\-]+\b 5 | @include strings 6 | comments #[^\n]* 7 | -------------------------------------------------------------------------------- /resources/apkstudio.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Categories=Development; 3 | Comment=Open-source, cross-platform Qt based IDE for reverse-engineering Android application packages. 4 | Exec=apkstudio 5 | Name=ApkStudio 6 | Icon=apkstudio 7 | Terminal=false 8 | Type=Application 9 | -------------------------------------------------------------------------------- /resources/xml.def: -------------------------------------------------------------------------------- 1 | keywords <[\s]*[/]?[\s]*[\w]+(?=[\s/>]) 2 | keywords <[\s]*[/]?[\s]*[\w]+-(\w+/?)+(?=[\s/>]) 3 | keywords [<>] 4 | keywords /> 5 | keywords 8 | attributes \w+(?=\=) 9 | attributes \w+:(\w+/?)+(?=\=) 10 | @include numbers 11 | strings "[^\n"]+"(?=[?\s/>]) 12 | resources @([a-zA-Z])+(?=/) 13 | comments? 122 |
123 | APK Studio 127 |

APK Studio

128 |

129 | Open-source, cross platform Qt6 based IDE for reverse-engineering 130 | Android application packages 131 |

132 |
133 | 134 | Build Status 135 | 136 | 137 | Release 138 | 139 | 140 | Downloads 141 | 142 | 143 | License 144 | 145 |
146 |
147 | 152 | Download Latest Release 153 | 154 |
155 |
156 | 157 | 158 |
159 | 160 |
161 | 162 | APK Studio Screenshot 167 | 168 |
169 | 170 | 171 |

172 | Features 173 |

174 | 183 | 184 | 185 |

186 | Downloads 187 |

188 |

189 | Please head over to Releases page for downloading. 190 |

191 | 195 | 196 | 200 | 201 | 202 |
203 |
204 |

205 | Disclaimer: Same as apktool, APK Studio is neither intended for piracy nor other non-legal uses. It could be used for localizing, adding some features or support for custom platforms, analyzing applications & much more. 206 |

207 |

208 | Developed & maintained by 209 | Vaibhav Pandey -aka- VPZ 210 | along with 211 | contributor(s). 212 |

213 |
214 |
215 | 216 | 217 | 218 | 219 | 220 | 225 | 226 | 230 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: [created] 12 | workflow_dispatch: 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | build-windows: 19 | name: Build Windows 20 | runs-on: windows-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | with: 25 | submodules: recursive 26 | 27 | - name: Setup Qt 28 | uses: jurplel/install-qt-action@v4 29 | with: 30 | version: "6.10.1" 31 | cache: true 32 | 33 | - name: Configure CMake 34 | run: | 35 | cmake -B build -S . -DCMAKE_BUILD_TYPE=Release 36 | 37 | - name: Build 38 | run: | 39 | cmake --build build --config Release 40 | 41 | - name: Deploy Qt dependencies 42 | shell: pwsh 43 | run: | 44 | $env:PATH = "$env:Qt6_DIR\bin;$env:PATH" 45 | 46 | # Find the executable 47 | $ExePath = "" 48 | if (Test-Path "build\bin\Release\ApkStudio.exe") { 49 | $ExePath = "build\bin\Release\ApkStudio.exe" 50 | } elseif (Test-Path "build\Release\ApkStudio.exe") { 51 | $ExePath = "build\Release\ApkStudio.exe" 52 | } else { 53 | Write-Host "Error: Executable not found. Searching..." 54 | Get-ChildItem -Path build -Recurse -Filter "ApkStudio.exe" -ErrorAction SilentlyContinue | Select-Object FullName 55 | exit 1 56 | } 57 | 58 | Write-Host "Found executable at: $ExePath" 59 | windeployqt --release $ExePath 60 | 61 | - name: Create archive 62 | shell: pwsh 63 | run: | 64 | # Find the directory containing the executable 65 | $SourceDir = "" 66 | if (Test-Path "build\bin\Release") { 67 | $SourceDir = "build\bin\Release" 68 | } elseif (Test-Path "build\Release") { 69 | $SourceDir = "build\Release" 70 | } else { 71 | Write-Host "Error: Release directory not found" 72 | exit 1 73 | } 74 | 75 | Write-Host "Creating archive from: $SourceDir" 76 | Compress-Archive -Path "$SourceDir\*" -DestinationPath ApkStudio-Windows-x64.zip 77 | 78 | - name: Upload artifact 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: ApkStudio-Windows-x64 82 | path: ApkStudio-Windows-x64.zip 83 | retention-days: 30 84 | 85 | - name: Upload to release 86 | if: github.event_name == 'release' 87 | uses: softprops/action-gh-release@v1 88 | with: 89 | tag_name: ${{ github.event.release.tag_name }} 90 | files: ApkStudio-Windows-x64.zip 91 | env: 92 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 93 | 94 | build-linux: 95 | name: Build Linux 96 | runs-on: ubuntu-latest 97 | steps: 98 | - name: Checkout code 99 | uses: actions/checkout@v4 100 | with: 101 | submodules: recursive 102 | 103 | - name: Install dependencies 104 | run: | 105 | sudo apt-get update 106 | sudo apt-get install -y libgl1-mesa-dev libxkbcommon-x11-0 107 | 108 | - name: Setup Qt 109 | uses: jurplel/install-qt-action@v4 110 | with: 111 | version: "6.10.1" 112 | cache: true 113 | 114 | - name: Configure CMake 115 | run: | 116 | cmake -B build -S . -DCMAKE_BUILD_TYPE=Release 117 | 118 | - name: Build 119 | run: | 120 | cmake --build build --config Release -j$(nproc) 121 | 122 | - name: Install 123 | run: | 124 | cmake --install build --prefix install 125 | 126 | - name: Download linuxdeploy and plugins 127 | run: | 128 | wget -c -nv "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" 129 | wget -c -nv "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" 130 | wget -c -nv "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" 131 | chmod +x linuxdeploy*.AppImage 132 | 133 | - name: Find executable 134 | run: | 135 | echo "Searching for executable..." 136 | find . -type f -executable \( -name "ApkStudio" -o -name "apkstudio" \) 2>/dev/null | head -5 137 | ls -la install/bin/ 2>/dev/null || echo "install/bin/ does not exist" 138 | ls -la build/ 2>/dev/null | head -10 || echo "build/ does not exist" 139 | 140 | - name: Prepare AppDir for AppImage 141 | run: | 142 | mkdir -p AppDir/usr/bin 143 | mkdir -p AppDir/usr/lib 144 | # Find the executable (could be ApkStudio or apkstudio, in build or install) 145 | EXECUTABLE="" 146 | if [ -f install/bin/ApkStudio ]; then 147 | EXECUTABLE="install/bin/ApkStudio" 148 | elif [ -f install/bin/apkstudio ]; then 149 | EXECUTABLE="install/bin/apkstudio" 150 | elif [ -f build/ApkStudio ]; then 151 | EXECUTABLE="build/ApkStudio" 152 | elif [ -f build/apkstudio ]; then 153 | EXECUTABLE="build/apkstudio" 154 | else 155 | echo "Error: Executable not found. Listing directories..." 156 | find . -type f -executable -name "*pk*" 2>/dev/null || true 157 | exit 1 158 | fi 159 | echo "Found executable at: $EXECUTABLE" 160 | cp "$EXECUTABLE" AppDir/usr/bin/apkstudio 161 | mkdir -p AppDir/usr/share/applications 162 | mkdir -p AppDir/usr/share/icons/hicolor/512x512/apps 163 | # Copy and fix desktop file to use correct icon name 164 | cp ${{ github.workspace }}/resources/apkstudio.desktop AppDir/usr/share/applications/ 165 | sed -i 's/^Icon=.*/Icon=apkstudio/' AppDir/usr/share/applications/apkstudio.desktop 166 | # Copy icon to both locations: root for linuxdeploy and hicolor for system 167 | cp ${{ github.workspace }}/resources/icon.png AppDir/apkstudio.png 168 | cp ${{ github.workspace }}/resources/icon.png AppDir/usr/share/icons/hicolor/512x512/apps/apkstudio.png 169 | chmod +x AppDir/usr/bin/apkstudio 170 | 171 | - name: Create AppImage with linuxdeploy 172 | run: | 173 | export QTDIR=${{ env.QT_ROOT_DIR }} 174 | ./linuxdeploy-x86_64.AppImage --appdir AppDir --executable AppDir/usr/bin/apkstudio --desktop-file AppDir/usr/share/applications/apkstudio.desktop --icon-file AppDir/apkstudio.png --plugin qt --output appimage 175 | 176 | - name: Rename AppImage if tagged 177 | run: | 178 | if [ ! -z "${{ github.ref }}" ] && [[ "${{ github.ref }}" == refs/tags/* ]]; then 179 | TAG_NAME=${GITHUB_REF#refs/tags/} 180 | if [ -f ApkStudio-x86_64.AppImage ]; then 181 | mv ApkStudio-x86_64.AppImage ApkStudio-${TAG_NAME}-x86_64.AppImage 182 | fi 183 | fi 184 | 185 | - name: Upload artifact 186 | uses: actions/upload-artifact@v4 187 | with: 188 | name: ApkStudio-Linux-x64 189 | path: ApkStudio*.AppImage 190 | if-no-files-found: error 191 | retention-days: 30 192 | 193 | - name: Upload to release 194 | if: github.event_name == 'release' 195 | run: | 196 | # Find the AppImage file 197 | APPIMAGE_FILE=$(find . -maxdepth 1 -name "ApkStudio*.AppImage" -type f | head -1) 198 | if [ -z "$APPIMAGE_FILE" ]; then 199 | echo "Error: AppImage file not found" 200 | exit 1 201 | fi 202 | echo "Found AppImage: $APPIMAGE_FILE" 203 | echo "APPIMAGE_FILE=$APPIMAGE_FILE" >> $GITHUB_ENV 204 | 205 | - name: Upload AppImage to release 206 | if: github.event_name == 'release' 207 | uses: softprops/action-gh-release@v1 208 | with: 209 | tag_name: ${{ github.event.release.tag_name }} 210 | files: ${{ env.APPIMAGE_FILE }} 211 | env: 212 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 213 | 214 | build-macos: 215 | name: Build macOS 216 | runs-on: macos-latest 217 | steps: 218 | - name: Checkout code 219 | uses: actions/checkout@v4 220 | with: 221 | submodules: recursive 222 | 223 | - name: Setup Qt 224 | uses: jurplel/install-qt-action@v4 225 | with: 226 | version: "6.10.1" 227 | cache: true 228 | 229 | - name: Set Qt directory 230 | run: | 231 | QT_DIR="" 232 | if [ -n "${{ env.Qt6_DIR }}" ]; then 233 | QT_DIR="${{ env.Qt6_DIR }}" 234 | elif [ -n "${{ env.Qt_DIR }}" ]; then 235 | QT_DIR="${{ env.Qt_DIR }}" 236 | else 237 | QT_CONFIG=$(find "$HOME" "$RUNNER_WORKSPACE" "/Users/runner" -name "Qt6Config.cmake" -type f 2>/dev/null | head -1) 238 | if [ -n "$QT_CONFIG" ]; then 239 | QT_DIR=$(dirname "$QT_CONFIG" | xargs dirname | xargs dirname | xargs dirname) 240 | fi 241 | fi 242 | if [ -z "$QT_DIR" ] || [ ! -d "$QT_DIR" ]; then 243 | echo "Error: Could not determine Qt directory" 244 | exit 1 245 | fi 246 | echo "Using Qt directory: $QT_DIR" 247 | echo "QT_DIR=$QT_DIR" >> $GITHUB_ENV 248 | 249 | - name: Configure CMake 250 | run: | 251 | cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="${{ env.QT_DIR }}" 252 | 253 | - name: Build 254 | run: | 255 | cmake --build build --config Release -j$(sysctl -n hw.ncpu) 256 | 257 | - name: Deploy Qt frameworks 258 | run: | 259 | QT_DIR="${{ env.QT_DIR }}" 260 | export PATH="$QT_DIR/bin:$PATH" 261 | APP_BUNDLE="build/bin/ApkStudio.app" 262 | 263 | # Verify app bundle exists 264 | if [ ! -d "$APP_BUNDLE" ]; then 265 | echo "Error: App bundle not found at $APP_BUNDLE" 266 | find . -type d -name "*.app" 2>/dev/null || true 267 | exit 1 268 | fi 269 | 270 | # Find macdeployqt 271 | MACDEPLOYQT="$QT_DIR/bin/macdeployqt" 272 | if [ ! -f "$MACDEPLOYQT" ]; then 273 | MACDEPLOYQT=$(find "$QT_DIR" -name "macdeployqt" -type f 2>/dev/null | head -1) 274 | fi 275 | 276 | if [ -z "$MACDEPLOYQT" ] || [ ! -f "$MACDEPLOYQT" ]; then 277 | echo "Error: macdeployqt not found" 278 | exit 1 279 | fi 280 | 281 | echo "Deploying Qt frameworks to: $APP_BUNDLE" 282 | chmod +x "$MACDEPLOYQT" 283 | "$MACDEPLOYQT" "$APP_BUNDLE" -always-overwrite 284 | 285 | - name: Install create-dmg 286 | run: | 287 | brew install create-dmg 288 | 289 | - name: Create DMG 290 | run: | 291 | APP_NAME="APK Studio" 292 | APP_BUNDLE_NAME="ApkStudio.app" 293 | APP_PATH="build/bin/${APP_BUNDLE_NAME}" 294 | DMG_NAME="ApkStudio-macOS-x64.dmg" 295 | 296 | if [ ! -d "$APP_PATH" ]; then 297 | echo "Error: App bundle not found at $APP_PATH" 298 | exit 1 299 | fi 300 | 301 | # Remove quarantine attribute to prevent "damaged" error 302 | xattr -cr "$APP_PATH" 303 | 304 | # Code sign the app with ad-hoc signature (required for macOS Gatekeeper) 305 | echo "Code signing app bundle..." 306 | codesign --force --deep --sign - "$APP_PATH" || { 307 | echo "Warning: Code signing failed, but continuing..." 308 | } 309 | 310 | # Verify the signature 311 | codesign --verify --verbose "$APP_PATH" || { 312 | echo "Warning: Code signature verification failed, but continuing..." 313 | } 314 | 315 | # Create a temporary directory for DMG contents 316 | mkdir -p dmg_temp 317 | cp -R "$APP_PATH" "dmg_temp/${APP_BUNDLE_NAME}" 318 | 319 | # Remove quarantine from copied app as well 320 | xattr -cr "dmg_temp/${APP_BUNDLE_NAME}" 321 | 322 | # Code sign the copied app as well 323 | codesign --force --deep --sign - "dmg_temp/${APP_BUNDLE_NAME}" || { 324 | echo "Warning: Code signing copied app failed, but continuing..." 325 | } 326 | 327 | # Touch the app bundle to refresh Finder icon cache 328 | touch "dmg_temp/${APP_BUNDLE_NAME}" 329 | touch "dmg_temp/${APP_BUNDLE_NAME}/Contents" 330 | touch "dmg_temp/${APP_BUNDLE_NAME}/Contents/Info.plist" 331 | 332 | # Verify icon exists in app bundle 333 | ICON_PATH="$APP_PATH/Contents/Resources/icon.icns" 334 | if [ ! -f "$ICON_PATH" ]; then 335 | echo "Warning: Icon not found at $ICON_PATH" 336 | echo "Contents of Resources directory:" 337 | ls -la "$APP_PATH/Contents/Resources/" 2>/dev/null || echo "Resources directory not found" 338 | fi 339 | 340 | # Rename the bundle in DMG to show "APK Studio" instead of "ApkStudio" 341 | # The executable name stays the same, only the bundle name changes 342 | if [ -d "dmg_temp/${APP_BUNDLE_NAME}" ]; then 343 | mv "dmg_temp/${APP_BUNDLE_NAME}" "dmg_temp/APK Studio.app" 344 | DMG_APP_NAME="APK Studio.app" 345 | echo "Renamed bundle to 'APK Studio.app' for DMG display" 346 | else 347 | DMG_APP_NAME="${APP_BUNDLE_NAME}" 348 | fi 349 | 350 | # Re-sign the renamed bundle 351 | if [ -d "dmg_temp/${DMG_APP_NAME}" ]; then 352 | codesign --force --deep --sign - "dmg_temp/${DMG_APP_NAME}" || { 353 | echo "Warning: Re-signing renamed bundle failed, but continuing..." 354 | } 355 | fi 356 | 357 | # Create DMG with volume icon and ensure app icon is visible 358 | if [ -f "$ICON_PATH" ]; then 359 | create-dmg \ 360 | --volname "APK Studio" \ 361 | --volicon "$ICON_PATH" \ 362 | --window-pos 200 120 \ 363 | --window-size 600 400 \ 364 | --icon-size 100 \ 365 | --icon "${DMG_APP_NAME}" 150 190 \ 366 | --hide-extension "${DMG_APP_NAME}" \ 367 | --app-drop-link 450 190 \ 368 | "$DMG_NAME" \ 369 | dmg_temp/ 370 | else 371 | echo "Creating DMG without volume icon (icon file not found)" 372 | create-dmg \ 373 | --volname "APK Studio" \ 374 | --window-pos 200 120 \ 375 | --window-size 600 400 \ 376 | --icon-size 100 \ 377 | --icon "${DMG_APP_NAME}" 150 190 \ 378 | --hide-extension "${DMG_APP_NAME}" \ 379 | --app-drop-link 450 190 \ 380 | "$DMG_NAME" \ 381 | dmg_temp/ 382 | fi 383 | 384 | 385 | # Verify DMG was created 386 | if [ -f "$DMG_NAME" ]; then 387 | echo "DMG created successfully: $DMG_NAME" 388 | ls -lh "$DMG_NAME" 389 | else 390 | echo "Error: DMG was not created" 391 | exit 1 392 | fi 393 | 394 | - name: Upload artifact 395 | uses: actions/upload-artifact@v4 396 | with: 397 | name: ApkStudio-macOS-x64 398 | path: ApkStudio-macOS-x64.dmg 399 | if-no-files-found: error 400 | retention-days: 30 401 | 402 | - name: Upload to release 403 | if: github.event_name == 'release' 404 | uses: softprops/action-gh-release@v1 405 | with: 406 | tag_name: ${{ github.event.release.tag_name }} 407 | files: ApkStudio-macOS-x64.dmg 408 | env: 409 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 410 | -------------------------------------------------------------------------------- /sources/sourcecodeedit.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 10 | #include 11 | #endif 12 | #include "sourcecodeedit.h" 13 | #include "themedsyntaxhighlighter.h" 14 | 15 | #define TAB_STOP_WIDTH 4 16 | #define TABS_TO_SPACES true 17 | 18 | SourceCodeEdit::SourceCodeEdit(QWidget *parent) 19 | : QPlainTextEdit(parent) 20 | { 21 | m_Sidebar = new SourceCodeSidebarWidget(this); 22 | QSettings settings; 23 | QFont font; 24 | #ifdef Q_OS_WIN 25 | font.setFamily(settings.value("editor_font", "Courier New").toString()); 26 | #elif defined(Q_OS_MACOS) 27 | font.setFamily(settings.value("editor_font", "Monaco").toString()); 28 | #else 29 | font.setFamily(settings.value("editor_font", "Ubuntu Mono").toString()); 30 | #endif 31 | font.setFixedPitch(true); 32 | font.setPointSize(settings.value("editor_font_size", 10).toInt()); 33 | font.setStyleHint(QFont::Monospace); 34 | const bool whitespaces = settings.value("editor_whitespaces", false).toBool(); 35 | if (whitespaces) { 36 | QTextOption options; 37 | options.setFlags(QTextOption::ShowTabsAndSpaces); 38 | document()->setDefaultTextOption(options); 39 | } 40 | setCursorWidth(2); 41 | setFrameStyle(QFrame::NoFrame); 42 | setFont(font); 43 | setTabChangesFocus(false); 44 | setWordWrapMode(QTextOption::NoWrap); 45 | connect(this, &QPlainTextEdit::cursorPositionChanged, this, &SourceCodeEdit::handleCursorPositionChanged); 46 | connect(this, &QPlainTextEdit::blockCountChanged, this, &SourceCodeEdit::handleBlockCountChanged); 47 | connect(this, &QPlainTextEdit::textChanged, this, &SourceCodeEdit::handleTextChanged); 48 | connect(this, &QPlainTextEdit::updateRequest, this, &SourceCodeEdit::handleUpdateRequest); 49 | connect(new QShortcut(Qt::CTRL | Qt::Key_U, this), &QShortcut::activated, [=] { 50 | transformText(true); 51 | }); 52 | connect(new QShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_U, this), &QShortcut::activated, [=] { 53 | transformText(false); 54 | }); 55 | connect(new QShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Up, this), &QShortcut::activated, [=] { 56 | moveSelection(true); 57 | }); 58 | connect(new QShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Down, this), &QShortcut::activated, [=] { 59 | moveSelection(false); 60 | }); 61 | } 62 | 63 | QRectF SourceCodeEdit::blockBoundingGeometryProxy(const QTextBlock &block) 64 | { 65 | return blockBoundingGeometry(block); 66 | } 67 | 68 | QRectF SourceCodeEdit::blockBoundingRectProxy(const QTextBlock &block) 69 | { 70 | return blockBoundingRect(block); 71 | } 72 | 73 | QPointF SourceCodeEdit::contentOffsetProxy() 74 | { 75 | return contentOffset(); 76 | } 77 | 78 | QString SourceCodeEdit::filePath() 79 | { 80 | return m_FilePath; 81 | } 82 | 83 | QTextBlock SourceCodeEdit::firstVisibleBlockProxy() 84 | { 85 | return firstVisibleBlock(); 86 | } 87 | 88 | void SourceCodeEdit::gotoLine(const int no) 89 | { 90 | QTextCursor cursor(document()->findBlockByLineNumber(no - 1)); 91 | setTextCursor(cursor); 92 | } 93 | 94 | void SourceCodeEdit::handleBlockCountChanged(const int count) 95 | { 96 | Q_UNUSED(count) 97 | setViewportMargins(m_Sidebar->sizeHint().width(), 0, 0, 0); 98 | } 99 | 100 | void SourceCodeEdit::handleCursorPositionChanged() 101 | { 102 | QList selections; 103 | if (!isReadOnly()) { 104 | QTextEdit::ExtraSelection selection; 105 | static QColor highlight = palette().color(QPalette::Text); 106 | highlight.setAlpha(25); 107 | selection.format.setBackground(highlight); 108 | selection.format.setProperty(QTextCharFormat::FullWidthSelection, true); 109 | selection.cursor = textCursor(); 110 | selection.cursor.clearSelection(); 111 | selections.append(selection); 112 | } 113 | setExtraSelections(selections); 114 | } 115 | 116 | void SourceCodeEdit::handleUpdateRequest(const QRect &rect, const int column) 117 | { 118 | if (column) { 119 | m_Sidebar->scroll(0, column); 120 | } 121 | m_Sidebar->update(0, rect.y(), m_Sidebar->width(), rect.height()); 122 | if (rect.contains(viewport()->rect())) { 123 | handleBlockCountChanged(0); 124 | } 125 | } 126 | 127 | void SourceCodeEdit::handleTextChanged() 128 | { 129 | handleCursorPositionChanged(); 130 | handleBlockCountChanged(0); 131 | } 132 | 133 | int SourceCodeEdit::indentSize(const QString &text) 134 | { 135 | int count = 0; 136 | int i = 0; 137 | int length = text.length(); 138 | if (length == 0) { 139 | return 0; 140 | } 141 | QChar current = text.at(i); 142 | while ((i < length) && ((current == ' ') || (current == QChar('\t')))) { 143 | if (current == QChar('\t')) { 144 | count++; 145 | i++; 146 | } else if (current == ' ') { 147 | int j = 0; 148 | while ((i + j < length) && (text.at(i + j) == ' ')) { 149 | j++; 150 | } 151 | i += j; 152 | count += j / TAB_STOP_WIDTH; 153 | } 154 | if (i < length) { 155 | current = text.at(i); 156 | } 157 | } 158 | return count; 159 | } 160 | 161 | bool SourceCodeEdit::indentText(const bool forward) 162 | { 163 | QTextCursor cursor = textCursor(); 164 | QTextCursor clone = cursor; 165 | if (!cursor.hasSelection()) { 166 | return false; 167 | } 168 | int start = cursor.selectionStart(); 169 | cursor.setPosition(cursor.selectionStart()); 170 | clone.setPosition(clone.selectionEnd()); 171 | int stop = clone.blockNumber(); 172 | cursor.beginEditBlock(); 173 | do { 174 | int position = cursor.position(); 175 | QString text = cursor.block().text(); 176 | int count = indentSize(text); 177 | if (forward) { 178 | count++; 179 | } else if (count > 0) { 180 | count--; 181 | } 182 | cursor.select(QTextCursor::LineUnderCursor); 183 | if (forward) { 184 | cursor.insertText(TABS_TO_SPACES ? QString(TAB_STOP_WIDTH, ' ') + text : QChar('\t') + text); 185 | } else { 186 | cursor.insertText(indentText(text, count)); 187 | } 188 | cursor.setPosition(position); 189 | if (!cursor.movePosition(QTextCursor::NextBlock)) { 190 | break; 191 | } 192 | } 193 | while(cursor.blockNumber() <= stop); 194 | cursor.setPosition(start, QTextCursor::MoveAnchor); 195 | cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); 196 | while (cursor.block().blockNumber() < stop) { 197 | cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); 198 | } 199 | cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 200 | cursor.endEditBlock(); 201 | setTextCursor(cursor); 202 | return true; 203 | } 204 | 205 | QString SourceCodeEdit::indentText(QString text, int count) const 206 | { 207 | if (text.isEmpty()) { 208 | return text; 209 | } 210 | while ((text.at(0) == ' ') || (text.at(0) == '\t')) { 211 | text.remove(0, 1); 212 | if (text.isEmpty()) { 213 | break; 214 | } 215 | } 216 | while (count != 0) { 217 | text = text.insert(0, TABS_TO_SPACES ? QString(TAB_STOP_WIDTH, ' ') : QChar('\t')); 218 | count--; 219 | } 220 | return text; 221 | } 222 | 223 | void SourceCodeEdit::keyPressEvent(QKeyEvent *event) 224 | { 225 | QTextCursor cursor = textCursor(); 226 | switch (event->key()) { 227 | case Qt::Key_Backtab: 228 | case Qt::Key_Tab: { 229 | bool forward = !QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier); 230 | if (indentText(forward)) { 231 | event->accept(); 232 | return; 233 | } else if (forward) { 234 | QString text = TABS_TO_SPACES ? QString(TAB_STOP_WIDTH, ' ') : QChar('\t'); 235 | QTextCursor cursor = textCursor(); 236 | cursor.insertText(text); 237 | setTextCursor(cursor); 238 | event->accept(); 239 | return; 240 | } 241 | break; 242 | } 243 | case Qt::Key_Enter: 244 | case Qt::Key_Return: 245 | break; 246 | case Qt::Key_Down: 247 | case Qt::Key_Up: 248 | if (QApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) { 249 | if (event->key() == Qt::Key_Down) { 250 | verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepAdd); 251 | } else { 252 | verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepSub); 253 | } 254 | event->accept(); 255 | } 256 | break; 257 | case Qt::Key_Escape: 258 | if (cursor.hasSelection()) { 259 | cursor.clearSelection(); 260 | setTextCursor(cursor); 261 | event->accept(); 262 | } 263 | break; 264 | case Qt::Key_Home: 265 | case Qt::Key_End: 266 | if (!QApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) { 267 | moveCursor(event->key() != Qt::Key_Home); 268 | event->accept(); 269 | } 270 | return; 271 | case Qt::Key_PageDown: 272 | case Qt::Key_PageUp: 273 | if (QApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) { 274 | if (event->key() == Qt::Key_Down) { 275 | verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); 276 | } else { 277 | verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); 278 | } 279 | event->accept(); 280 | } 281 | break; 282 | default: 283 | break; 284 | } 285 | QPlainTextEdit::keyPressEvent(event); 286 | } 287 | 288 | void SourceCodeEdit::moveCursor(const bool end) 289 | { 290 | QTextCursor cursor = textCursor(); 291 | int length = cursor.block().text().length(); 292 | if (length != 0) { 293 | int original = cursor.position(); 294 | QTextCursor::MoveMode mode = QTextCursor::MoveAnchor; 295 | if (QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { 296 | mode = QTextCursor::KeepAnchor; 297 | } 298 | cursor.movePosition(QTextCursor::StartOfLine, mode); 299 | int start = cursor.position(); 300 | int i; 301 | if (end) { 302 | cursor.movePosition(QTextCursor::EndOfLine, mode); 303 | i = length; 304 | while (cursor.block().text()[i - 1].isSpace()) { 305 | i--; 306 | if (i == 1) { 307 | i = length; 308 | break; 309 | } 310 | } 311 | } else { 312 | i = 0; 313 | while (cursor.block().text()[i].isSpace()) { 314 | i++; 315 | if (i == length) { 316 | i = 0; 317 | break; 318 | } 319 | } 320 | } 321 | if ((original == start) || ((start + i) != original)) { 322 | cursor.setPosition(start + i, mode); 323 | } 324 | setTextCursor(cursor); 325 | } 326 | } 327 | 328 | void SourceCodeEdit::moveSelection(const bool up) 329 | { 330 | QTextCursor original = textCursor(); 331 | QTextCursor moved = original; 332 | moved.setVisualNavigation(false); 333 | moved.beginEditBlock(); 334 | bool selected = original.hasSelection(); 335 | if (selected) { 336 | moved.setPosition(original.selectionStart()); 337 | moved.movePosition(QTextCursor::StartOfBlock); 338 | moved.setPosition(original.selectionEnd(), QTextCursor::KeepAnchor); 339 | moved.movePosition(moved.atBlockStart() ? QTextCursor::Left : QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 340 | } else { 341 | moved.movePosition(QTextCursor::StartOfBlock); 342 | moved.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 343 | } 344 | QString text = moved.selectedText(); 345 | moved.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); 346 | moved.removeSelectedText(); 347 | if (up) { 348 | moved.movePosition(QTextCursor::PreviousBlock); 349 | moved.insertBlock(); 350 | moved.movePosition(QTextCursor::Left); 351 | } else { 352 | moved.movePosition(QTextCursor::EndOfBlock); 353 | if (moved.atBlockStart()) { 354 | moved.movePosition(QTextCursor::NextBlock); 355 | moved.insertBlock(); 356 | moved.movePosition(QTextCursor::Left); 357 | } else { 358 | moved.insertBlock(); 359 | } 360 | } 361 | int start = moved.position(); 362 | moved.clearSelection(); 363 | moved.insertText(text); 364 | int end = moved.position(); 365 | if (selected) { 366 | moved.setPosition(start); 367 | moved.setPosition(end, QTextCursor::KeepAnchor); 368 | } 369 | moved.endEditBlock(); 370 | setTextCursor(moved); 371 | } 372 | 373 | void SourceCodeEdit::open(const QString &path) 374 | { 375 | QFile file(path); 376 | if (file.open(QFile::ReadOnly | QFile::Text)) { 377 | auto content = QString::fromUtf8(file.readAll()); 378 | setPlainText(content); 379 | QFileInfo info(path); 380 | QString extension = info.suffix().toLower(); 381 | QSettings settings; 382 | const bool dark = settings.value("dark_theme", false).toBool(); 383 | new ThemedSyntaxHighlighter( 384 | ThemedSyntaxHighlighter::theme(dark ? "dark" : "light"), 385 | ThemedSyntaxHighlighter::definitions(extension), 386 | document()); 387 | } 388 | m_FilePath = path; 389 | } 390 | 391 | void SourceCodeEdit::paintEvent(QPaintEvent *event) 392 | { 393 | QPainter line(viewport()); 394 | const int offset = static_cast((fontMetrics().horizontalAdvance('8') * 80) 395 | + contentOffset().x() 396 | + document()->documentMargin()); 397 | QPen pen = line.pen(); 398 | static QColor eol = palette().color(QPalette::Text); 399 | eol.setAlpha(50); 400 | pen.setColor(eol); 401 | pen.setStyle(Qt::DotLine); 402 | line.setPen(pen); 403 | line.drawLine(offset, 0, offset, viewport()->height()); 404 | QPlainTextEdit::paintEvent(event); 405 | } 406 | 407 | void SourceCodeEdit::resizeEvent(QResizeEvent *event) 408 | { 409 | QPlainTextEdit::resizeEvent(event); 410 | QRect rect = contentsRect(); 411 | m_Sidebar->setGeometry(QRect(rect.left(), rect.top(), m_Sidebar->sizeHint().width(), rect.height())); 412 | } 413 | 414 | bool SourceCodeEdit::save() 415 | { 416 | QFile file(m_FilePath); 417 | if (file.open(QFile::WriteOnly | QFile::Text)) { 418 | QTextStream out(&file); 419 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 420 | out.setCodec("UTF-8"); 421 | #else 422 | out.setEncoding(QStringConverter::Utf8); 423 | #endif 424 | out.setGenerateByteOrderMark(false); 425 | out << toPlainText(); 426 | out.flush(); 427 | file.close(); 428 | return true; 429 | } 430 | return false; 431 | } 432 | 433 | void SourceCodeEdit::transformText(const bool upper) 434 | { 435 | QTextCursor cursor = textCursor(); 436 | if (!cursor.hasSelection()) { 437 | cursor.select(QTextCursor::WordUnderCursor); 438 | } 439 | QString before = cursor.selectedText(); 440 | QString after = upper ? before.toUpper() : before.toLower(); 441 | if (before != after) { 442 | cursor.beginEditBlock(); 443 | cursor.deleteChar(); 444 | cursor.insertText(after); 445 | cursor.endEditBlock(); 446 | setTextCursor(cursor); 447 | } 448 | } 449 | 450 | void SourceCodeEdit::wheelEvent(QWheelEvent *event) 451 | { 452 | if (event->modifiers() & Qt::ControlModifier) { 453 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 454 | const int delta = event->angleDelta().y(); 455 | #else 456 | const int delta = event->delta(); 457 | #endif 458 | if (delta > 0) { 459 | zoomIn(); 460 | } else if (delta < 0) { 461 | zoomOut(); 462 | } 463 | } else { 464 | QPlainTextEdit::wheelEvent(event); 465 | } 466 | } 467 | 468 | SourceCodeSidebarWidget::SourceCodeSidebarWidget(SourceCodeEdit *edit) 469 | : QWidget(edit), m_Edit(edit) 470 | { 471 | } 472 | 473 | void SourceCodeSidebarWidget::leaveEvent(QEvent *e) 474 | { 475 | Q_UNUSED(e) 476 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 477 | QMouseEvent copy(QEvent::MouseMove, QPointF(-1, -1), Qt::NoButton, Qt::NoButton, Qt::NoModifier); 478 | #else 479 | QMouseEvent copy(QEvent::MouseMove, QPoint(-1, -1), Qt::NoButton, nullptr, nullptr); 480 | #endif 481 | mouseEvent(©); 482 | } 483 | 484 | void SourceCodeSidebarWidget::paintEvent(QPaintEvent *e) 485 | { 486 | QPainter painter(this); 487 | QTextBlock block = m_Edit->firstVisibleBlockProxy(); 488 | int i = block.blockNumber(); 489 | int top = static_cast(m_Edit->blockBoundingGeometryProxy(block).translated(m_Edit->contentOffsetProxy()).top()); 490 | int bottom = top + static_cast(m_Edit->blockBoundingRectProxy(block).height()); 491 | QRect full = e->rect(); 492 | painter.fillRect(full, palette().color(QPalette::Base)); 493 | while (block.isValid() && (top <= full.bottom())) { 494 | if (block.isVisible() && (bottom >= full.top())) { 495 | QRect box(0, top, width(), m_Edit->fontMetrics().height()); 496 | QFont font = painter.font(); 497 | font.setFamily(m_Edit->font().family()); 498 | font.setPointSize(m_Edit->font().pointSize()); 499 | if (m_Edit->textCursor().blockNumber() == i) { 500 | painter.fillRect(box, palette().color(QPalette::Highlight)); 501 | painter.setPen(palette().color(QPalette::HighlightedText)); 502 | font.setWeight(QFont::Bold); 503 | } else { 504 | font.setWeight(QFont::Normal); 505 | painter.setPen(palette().color(QPalette::Text)); 506 | } 507 | painter.setFont(font); 508 | painter.drawText(box.left(), box.top(), box.width(), box.height(), Qt::AlignRight, QString::number(i + 1).append(' ')); 509 | painter.setPen(palette().color(QPalette::Highlight)); 510 | painter.drawLine(full.topRight(), full.bottomRight()); 511 | } 512 | block = block.next(); 513 | top = bottom; 514 | bottom = (top + static_cast(m_Edit->blockBoundingRectProxy(block).height())); 515 | ++i; 516 | } 517 | } 518 | 519 | void SourceCodeSidebarWidget::mouseEvent(QMouseEvent *e) 520 | { 521 | QTextCursor cursor = m_Edit->cursorForPosition(QPoint(0, e->pos().y())); 522 | if ((e->type() == QEvent::MouseButtonPress) && (e->button() == Qt::LeftButton)) { 523 | cursor.movePosition(QTextCursor::EndOfBlock); 524 | cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); 525 | cursor.setVisualNavigation(true); 526 | m_Edit->setTextCursor(cursor); 527 | } 528 | } 529 | 530 | void SourceCodeSidebarWidget::mouseMoveEvent(QMouseEvent *event) 531 | { 532 | mouseEvent(event); 533 | } 534 | void SourceCodeSidebarWidget::mousePressEvent(QMouseEvent *event) 535 | { 536 | mouseEvent(event); 537 | } 538 | void SourceCodeSidebarWidget::mouseReleaseEvent(QMouseEvent *event) 539 | { 540 | mouseEvent(event); 541 | } 542 | 543 | QSize SourceCodeSidebarWidget::sizeHint() const 544 | { 545 | int digits = 1; 546 | int blocks = qMax(1, m_Edit->blockCount()); 547 | while (blocks >= 10) { 548 | blocks /= 10; 549 | digits++; 550 | } 551 | digits++; 552 | digits++; 553 | return QSize((3 + (m_Edit->fontMetrics().horizontalAdvance('8') * digits)), 0); 554 | } 555 | 556 | void SourceCodeSidebarWidget::wheelEvent(QWheelEvent *e) 557 | { 558 | QApplication::sendEvent(m_Edit->viewport(), e); 559 | } 560 | --------------------------------------------------------------------------------