├── .gitattributes ├── .github └── workflows │ └── releaser.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── assets └── color_loupe.ini ├── buffered_dc.hpp ├── color_abgr.hpp ├── color_loupe.cpp ├── color_loupe.def ├── color_loupe.rc ├── color_loupe.sln ├── color_loupe.vcxproj ├── color_loupe.vcxproj.filters ├── dialogs.cpp ├── dialogs.hpp ├── dialogs_basics.hpp ├── drag_states.hpp ├── key_states.hpp ├── resource.h ├── resource.hpp └── settings.hpp /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | *.ini text working-tree-encoding=UTF-8 eol=crlf 6 | 7 | ############################################################################### 8 | # Set default behavior for command prompt diff. 9 | # 10 | # This is need for earlier builds of msysgit that does not have it on by 11 | # default for csharp files. 12 | # Note: This is only used by command line 13 | ############################################################################### 14 | #*.cs diff=csharp 15 | 16 | ############################################################################### 17 | # Set the merge driver for project and solution files 18 | # 19 | # Merging from the command prompt will add diff markers to the files if there 20 | # are conflicts (Merging from VS is not affected by the settings below, in VS 21 | # the diff markers are never inserted). Diff markers may cause the following 22 | # file extensions to fail to load in VS. An alternative would be to treat 23 | # these files as binary and thus will always conflict and require user 24 | # intervention with every merge. To do so, just uncomment the entries below 25 | ############################################################################### 26 | #*.sln merge=binary 27 | #*.csproj merge=binary 28 | #*.vbproj merge=binary 29 | #*.vcxproj merge=binary 30 | #*.vcproj merge=binary 31 | #*.dbproj merge=binary 32 | #*.fsproj merge=binary 33 | #*.lsproj merge=binary 34 | #*.wixproj merge=binary 35 | #*.modelproj merge=binary 36 | #*.sqlproj merge=binary 37 | #*.wwaproj merge=binary 38 | 39 | ############################################################################### 40 | # behavior for image files 41 | # 42 | # image files are treated as binary by default. 43 | ############################################################################### 44 | #*.jpg binary 45 | #*.png binary 46 | #*.gif binary 47 | 48 | ############################################################################### 49 | # diff behavior for common document formats 50 | # 51 | # Convert binary document formats to text before diffing them. This feature 52 | # is only available from the command line. Turn it on by uncommenting the 53 | # entries below. 54 | ############################################################################### 55 | #*.doc diff=astextplain 56 | #*.DOC diff=astextplain 57 | #*.docx diff=astextplain 58 | #*.DOCX diff=astextplain 59 | #*.dot diff=astextplain 60 | #*.DOT diff=astextplain 61 | #*.pdf diff=astextplain 62 | #*.PDF diff=astextplain 63 | #*.rtf diff=astextplain 64 | #*.RTF diff=astextplain 65 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: Make Release 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - '*' 7 | branches: 8 | - '*' 9 | 10 | env: 11 | SOLUTION_NAME: color_loupe 12 | # Path to the solution file relative to the root of the project. 13 | 14 | TIME_ZONE: Asia/Tokyo 15 | # Time zone for the timestamp when releasing. 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | build: 22 | name: Build 23 | runs-on: windows-latest 24 | strategy: 25 | matrix: 26 | configuration: [Release] 27 | platform: [x86] 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | with: 32 | submodules: recursive 33 | 34 | - name: Add MSBuild to PATH 35 | uses: microsoft/setup-msbuild@v2 36 | 37 | - name: Build 38 | working-directory: ${{env.GITHUB_WORKSPACE}} 39 | # Add additional options to the MSBuild command line here (like platform or verbosity level). 40 | # See https://docs.microsoft.com/visualstudio/msbuild/msbuild-command-line-reference 41 | run: msbuild /m /p:Configuration=${{ matrix.configuration }} /p:Platform=${{ matrix.platform }} ${{ env.SOLUTION_NAME }}.sln 42 | 43 | - name: Pack into a folder 44 | run: | 45 | mkdir pack 46 | copy ${{ matrix.configuration }}/*.auf pack 47 | copy assets/*.ini pack 48 | copy *.md pack 49 | copy LICENSE pack 50 | # pick up and add any files you may need. 51 | 52 | - name: Upload the build as artifacts 53 | uses: actions/upload-artifact@v4 54 | with: 55 | path: pack/* 56 | name: ${{ env.SOLUTION_NAME }}-${{ github.ref_name }}_${{ matrix.platform }}${{ matrix.configuration }} 57 | # name can be anything as long as you can recognize. 58 | 59 | release: 60 | name: Create Release 61 | if: github.event_name == 'push' && github.ref_type == 'tag' 62 | needs: build 63 | runs-on: ubuntu-latest 64 | permissions: 65 | contents: write 66 | 67 | steps: 68 | - uses: actions/download-artifact@v4 69 | with: 70 | path: artifacts 71 | merge-multiple: true # assuming a single artifact. 72 | 73 | - name: Compress 74 | run: | 75 | cd artifacts 76 | zip -r ../aviutl_${{ env.SOLUTION_NAME }}-${{ github.ref_name }}.zip * 77 | # rename .zip file for downloading if necessary. 78 | 79 | - name: Prepare release notes 80 | id: release-notes 81 | run: | 82 | echo "name=${{ github.ref_name }} ($(TZ='${{ env.TIME_ZONE }}' date +'%Y-%m-%d'))" >> $GITHUB_OUTPUT 83 | echo "### 更新内容" >> ReleaseNotes.txt 84 | phase=0 85 | IFS=$'\n' 86 | cat artifacts/README.md | while read line; do 87 | if [[ $phase == 0 ]]; then 88 | if [[ $line =~ ^##*[[:space:]]{1,}'改版履歴'[[:space:]]*$ ]]; then phase=1; fi 89 | elif [[ $phase == 1 ]]; then 90 | if [[ $line =~ ^-[[:space:]] ]]; then phase=2; fi 91 | elif [[ $line =~ ^(-|##*)[[:space:]] ]]; then break 92 | else 93 | echo ${line:2} >> ReleaseNotes.txt 94 | fi 95 | done 96 | 97 | - name: Release 98 | uses: softprops/action-gh-release@v2 99 | with: 100 | name: ${{ steps.release-notes.outputs.name }} 101 | files: '*.zip' 102 | body_path: ReleaseNotes.txt 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sdk"] 2 | path = sdk 3 | url = https://github.com/sigma-axis/aviutl_exedit_sdk.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 sigma-axis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 色ルーペ AviUtl プラグイン 2 | 3 | 色ピッカー付き拡大鏡プラグインです.他にも拡大画面のまま拡張編集のオブジェクトをつかんでドラッグ移動できたりもします. 4 | 5 | [ダウンロードはこちら.](https://github.com/sigma-axis/aviutl_color_loupe/releases) [紹介動画.](https://www.nicovideo.jp/watch/sm43213687) 6 | 7 | ![色や座標を表示してくれます.](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/d2c57a35-ab0d-438b-8181-4fa3093f2128) 8 | ![ダークモードっぽい色合いにもできます.](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/ee36c902-3544-4cc0-9a62-5766dd3592e5) 9 | 10 | ## 動作要件 11 | 12 | - AviUtl 1.10 (1.00 でも動作するが 1.10 推奨) 13 | 14 | http://spring-fragrance.mints.ne.jp/aviutl 15 | 16 | - 拡張編集 0.92 (「[拡張編集ドラッグ](#拡張編集ドラッグ)」の機能を利用する場合) 17 | 18 | - 0.93rc1 でも動作するはずだが未確認 / 非推奨. 19 | 20 | - Visual C++ 再頒布可能パッケージ(\[2015/2017/2019/2022\] の x86 対応版が必要) 21 | 22 | https://learn.microsoft.com/ja-jp/cpp/windows/latest-supported-vc-redist 23 | 24 | ## 導入方法 25 | 26 | 以下のフォルダのいずれかに `color_loupe.auf` と `color_loupe.ini` をコピーしてください. 27 | 28 | 1. `aviutl.exe` のあるフォルダ 29 | 1. (1) のフォルダにある `plugins` フォルダ 30 | 1. (2) のフォルダにある任意のフォルダ 31 | 32 | ## 初期設定での操作 33 | 34 | 以下は初期状態での操作方法です.操作はマウスボタンの割り当てやキーの組み合わせなど[カスタマイズ](#操作のカスタマイズ)できます. 35 | 36 | - **左クリックドラッグ** 37 | 38 | [ルーペ移動ドラッグ](#ルーペ移動ドラッグ). 39 | 40 | - Shift+ドラッグで上下左右の軸に沿って移動できます. 41 | 42 | - **ホイールスクロール** 43 | 44 | ズーム操作. 45 | 46 | - 拡大率を増減します. 47 | - 最大で 32 倍,最小で 0.1 倍です.[設定を変更](#ズーム操作の設定)すれば最大で 64 倍,最小で 0.05 倍まで範囲を広げられます. 48 | 49 | - **右クリックドラッグ** 50 | 51 | [色・座標の情報表示](#色座標の情報表示). 52 | 53 | - 現在マウスカーソルのある点のカラーコードと座標が表示されます. 54 | - 初期状態だとカラーコードは `#RRGGBB`の形式で座標は左上を原点とした値ですが,[設定](#ドラッグ操作の設定)で変更できます. 55 | - Shift+ドラッグで上下左右の軸に沿って移動できます. 56 | 57 | ![色・座標の情報表示](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/d2c57a35-ab0d-438b-8181-4fa3093f2128) 58 | 59 | - **Ctrl+左クリックドラッグ** 60 | 61 | [拡張編集ドラッグ](#拡張編集ドラッグ). 62 | 63 | - 拡張編集で配置されたオブジェクトやアンカーをドラッグ移動します.メイン画面をドラッグしているような操作を,ルーペで拡大した状態でできます. 64 | - ドラッグ開始時に Ctrl キーを押していれば,そのあと離してもドラッグ操作を続けられます. 65 | - メイン画面でのドラッグと同様に Alt での拡大縮小や Shift での上下左右の方向固定もできます. 66 | 67 | - **AviUtlメインウィンドウの画面上でホイールクリックドラッグ** 68 | 69 | ルーペ位置をメイン画面上の位置へ移動. 70 | 71 | - ホイールクリックだけでは反応しません.ホイールを押したままマウスを動かす必要があります. 72 | 73 | - **Ctrl+右クリック / Shift+F10 キー** 74 | 75 | [設定メニュー](#設定メニュー)の表示. 76 | 77 | - 各種コマンド実行や設定ができます. 78 | 79 | ![設定メニュー](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/ca96e1a6-c5aa-40ff-aa10-f5146c589869) 80 | 81 | - **左ダブルクリック** 82 | 83 | ズーム切り替え. 84 | 85 | - 拡大率がいったん等倍に変わります.もう一度ダブルクリックで元の倍率に戻ります. 86 | - 正確には裏に「もう1つの拡大率」があって,それと入れ替えます.予め設定しておいた小さい拡大率と大きい拡大率を瞬時に切り替えて操作できます. 87 | 88 | - **ホイールクリック** 89 | 90 | カーソル追従切り替え. 91 | 92 | - 「カーソル追従」を「ON」にすると,メイン画面でカーソルを動かすと(ホイールクリックがなくても)ルーペ位置が追従するようになります. 93 | - 「カーソル追従」が「OFF」だと,ホイールクリックを押している状態に限りメイン画面のカーソル位置を追従します. 94 | - 初期状態だと「OFF」です. 95 | 96 | - **右ダブルクリック** 97 | 98 | カラーコードをコピー. 99 | 100 | - カーソル位置のピクセルのカラーコードをクリップボードにコピー. 101 | - `rrggbb` の16進6桁の形式でコピーします.カラーコードの書式は[設定](#各種クリックコマンドの設定)で変更できます. 102 | - 蛇色様の[カラーコード追加プラグイン](https://github.com/hebiiro/AviUtl-Plugin-AddColorCode)との併用が便利になる目的で実装しました. 103 | 104 | - 拡大率変更など一部操作をすると通知メッセージが表示されます.通知項目や表示位置,表示時間は[設定](#通知メッセージの設定)で変更できます. 105 | 106 | ![拡大率変更通知.](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/80176dc8-7a0b-423c-80ff-5f01e68031dc) ![カラーコードのコピーの通知.](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/ff33c524-a485-4f19-bf37-8ec0e06b726d) ![カーソル追従切り替えの通知.](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/61d2a5e4-8fe0-4dbb-b1d3-2f10083f6cb6) ![グリッド表示の切り替え通知.](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/d48b4f6c-a7f0-4c6c-83e5-454322d5045e) 107 | 108 | ## ドラッグ操作について 109 | 110 | ドラッグ操作には4種類あります: 111 | - [ルーペ移動ドラッグ](#ルーペ移動ドラッグ) 112 | - [色・座標の情報表示](#色座標の情報表示) 113 | - [拡大率ドラッグ](#拡大率ドラッグ) 114 | - [拡張編集ドラッグ](#拡張編集ドラッグ) 115 | 116 | ### ルーペ移動ドラッグ 117 | 118 | 拡大表示する位置を移動できます. 119 | 120 | [設定](#ドラッグ操作の設定)によってはピクセル単位にスナップしたり,Shift キーと組み合わせて上下左右や斜め 45° の直線上に沿って動かしたりすることもできます. 121 | 122 | ### 色・座標の情報表示 123 | 124 | 現在マウスカーソルがあるピクセルのカラーコードや座標を表示します. 125 | 126 | ![色・座標の情報表示](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/d2c57a35-ab0d-438b-8181-4fa3093f2128) 127 | 128 | [設定](#ドラッグ操作の設定)を変更すればカラーコードの書式を変更したり,表示される座標の原点を画像の左上から中央に変更したりもできます. 129 | 130 | ### 拡大率ドラッグ 131 | 132 | マウスカーソルを上下に動かすことで拡大率を操作します. 133 | - 初期状態でマウスボタンの割り当てはありません.使用するには[設定](#ドラッグ操作の設定)でボタンを割り当ててください. 134 | 135 | [設定](#ドラッグ操作の設定)で倍率変化の速度や拡大縮小の中心を変更したり,ドラッグ方向を反転したりできます. 136 | 137 | ### 拡張編集ドラッグ 138 | 139 | メイン画面をドラッグして拡張編集のオブジェクトを移動するのと同等の操作が,拡大したルーペ画面上で行えます.Shift キーで上下左右に方向を固定したり,Alt キーで拡大率を変化させたりなどのキー操作も可能です. 140 | 141 | [設定](#ドラッグ操作の設定)によっては Shift キーや Alt キーの押下状態を,実際のキー入力とは違うものに上書きすることもできます. 142 | 143 | ## 操作のカスタマイズ 144 | 145 | ポップアップメニュー (デフォルトだと Ctrl+右クリック,Shift+F10 でも可能) の「色ルーペの設定...」を選択するとダイアログが表示され,操作のカスタマイズなど各種設定ができます. 146 | 147 | ![「色ルーペの設定」ダイアログ](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/ff963c78-4a0b-4688-b465-a43a3ec8b242) 148 | 149 | 設定項目の中には説明書きが添えられていることもあるので,そちらもご参照ください. 150 | 151 | 以下の動作を変更できます. 152 | 153 | ### ドラッグ操作の設定 154 | 155 | [ルーペ移動ドラッグ](#ルーペ移動ドラッグ),[色・座標の情報表示](#色座標の情報表示),[拡張編集ドラッグ](#拡張編集ドラッグ)のタブで各種ドラッグ操作の設定ができます. 156 | 157 | - ボタンの割り当てや修飾キーの組み合わせが変更できます. 158 | 159 | ![ボタンや修飾キーの設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/824d6b28-b281-42cf-80e6-f1f1c227926a) 160 | 161 | - 通常のクリック操作と判別するための条件を,移動距離とボタンを押す時間で指定することができます. 162 | 163 | ![クリック判定の基準](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/f5e146ee-4788-4b99-8ef1-668c2b701943) 164 | 165 | - 「ピクセル距離」に `-1` を指定するとボタンを押した瞬間からドラッグ判定になります.同じボタンに割り当てた通常のクリックのコマンドがない場合は使いやすくなります. 166 | 167 | - マウスホイールによるズーム操作を,選択タブのドラッグ操作中限定の挙動として設定変更できます.ここでの設定は通常のズーム操作の設定とは別枠で,独立に指定できます. 168 | 169 | ![ドラッグ中のホイールによるズーム操作の設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/054f24dd-f6a4-45e0-af7e-c883c41da6e8) 170 | 171 | - その他,各種ドラッグ操作に関する設定項目もあります. 172 | 173 | 主要なものだけピックアップ: 174 | 175 | - **ピクセル単位で移動** 176 | 177 | [ルーペ移動ドラッグ](#ルーペ移動ドラッグ)時に,ピクセル単位にスナップされます. 178 | 179 | - **Shift キーで方向固定** 180 | 181 | Shift キーを押している間,ドラッグの開始地点から上下左右や斜め 45° の直線上に沿ってドラッグ移動できます. 182 | 183 | - **書式設定** 184 | 185 | [色・座標の情報表示](#色座標の情報表示)でのカラーコードや座標の表示方式を設定できます. 186 | 187 | - **フォントの設定** 188 | 189 | ![フォントの設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/de423163-8e81-4f53-980e-79b041c975d3) 190 | 191 | [色・座標の情報表示](#ドラッグ操作の設定)に使われるフォント名やフォントサイズを指定できます. 192 | - **太字**,*斜体*,***太字斜体***も指定できます. 193 | - ここでの指定は[通知メッセージ](#通知メッセージの設定)とは別枠で,独立に指定できます. 194 | 195 | - **修飾キーの上書き** 196 | 197 | [拡張編集ドラッグ](#拡張編集ドラッグ)で,Shift キーによる方向固定や Alt キーによる拡大率操作でのキー認識を上書き・変更できます. 198 | 199 | ![修飾キーの上書き設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/3ac91be4-e9aa-4996-9753-1c931e60ade2) 200 | 201 | - そのまま 202 | 203 | キー入力の認識に操作・介入しません. 204 | 205 | - ON 固定 206 | 207 | 実際のキー入力に関わらず,Shift の方向固定や Alt の拡大率操作が常に有効になります. 208 | 209 | - OFF 固定 210 | 211 | 実際のキー入力に関わらず,Shift の方向固定や Alt の拡大率操作が常に無効になります. 212 | 213 | - 反転 214 | 215 | 実際のキー入力と ON/OFF が真逆になります. 216 | 217 | ### 各マウスボタンのクリック割り当て 218 | 219 | 各マウスボタンのシングルクリック,ダブルクリックに対してコマンドを割り当てることができます. 220 | 221 | ![クリックコマンドの割り当て](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/8a7f1f0f-1d75-4146-8356-cfa3ff576206) 222 | 223 | 1. 以下のコマンドを割り当てられます: 224 | 225 | |コマンド| 226 | |---| 227 | |(何もしない)| 228 | |ズーム切り替え| 229 | |カラーコードをコピー| 230 | |座標をコピー| 231 | |カーソル追従切り替え| 232 | |編集画面の中央へ移動| 233 | |ルーペの中央へ移動| 234 | |グリッド表示切り替え| 235 | |ズームダウン| 236 | |ズームアップ| 237 | |設定メニューを表示| 238 | |このウィンドウを表示| 239 | 240 | 1. 「他ボタンのドラッグをキャンセルする」のチェックを外すと,他ボタンでドラッグ操作中にも指定のクリックコマンドが実行できるようになります. 241 | 242 | 1. 「各コマンドの説明」の枠からコマンドの内容を確認できます. 243 | 244 | その他以下の点に留意してください. 245 | - シングルクリックのコマンド指定時,同じマウスボタンにドラッグ操作が割り当てられている場合,[ドラッグ操作の設定](#ドラッグ操作の設定)の「クリック判定範囲」でシングルクリックとドラッグを判別する条件を設定してください. 246 | - ダブルクリック時には,1回目のクリックでシングルクリックも発生します.両方にコマンドを割り当てる際には注意してください. 247 | - 各コマンドの細かい設定変更は[クリックコマンドの設定](#各種クリックコマンドの設定)タブできます. 248 | 249 | ### 各種クリックコマンドの設定 250 | 251 | クリックコマンドに対しての追加設定ができます.一部の設定は[ポップアップメニュー](#設定メニュー)でのコマンドにも影響します. 252 | 253 | ![クリックコマンドの動作設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/773baafa-b748-40fd-968b-0eb0fb5b406c) 254 | 255 | ### ズーム操作の設定 256 | 257 | マウスホイールによる拡大率の操作に関する設定ができます. 258 | 259 | - 「マウスホイールで拡大縮小」枠 260 | 261 | 有効/無効化,ホイールの方向を反転,拡大縮小の中心,ズームの段数(ホイール入力に対する拡大率変化のスピード)を設定できます. 262 | 263 | ![通常のホイールによるズーム操作の設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/4f293d02-2e7d-4efb-8192-c3d750788507) 264 | 265 | - これはドラッグ操作中のホイールによるズーム操作の設定とは別枠で,独立に指定できます. 266 | 267 | - 拡大率の範囲. 268 | 269 | 拡大率の最大,最小値を設定できます.初期状態だと最小値は `0.10` 倍,最大値は `32` 倍ですが,広げたり使いやすい範囲に制限したりできます. 270 | 271 | ![拡大率の範囲の設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/32184ebc-7df5-44df-b34a-7978a78da4d2) 272 | 273 | - こちらはドラッグ操作中のホイールによるズーム操作にも影響します. 274 | 275 | ### グリッドの設定 276 | 277 | グリッドの表示される最小の拡大率を指定できます. 278 | 279 | ![グリッドの表示条件の設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/e87c4ebe-f0d3-4f08-bd7e-fd8f82b06b32) 280 | 281 | - グリッドには2種類あり,「細いグリッド」「太いグリッド」でそれぞれ設定できます. 282 | 283 | | 細いグリッド | 太いグリッド| 284 | |:---:|:---:| 285 | | ![細いグリッド](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/9ad4fbaf-4fb0-4913-b6f4-227ec1e6d7b2) | ![太いグリッド](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/0a106fb3-2729-4cc9-969f-dd8aeaae176f) | 286 | | 線幅 1ピクセル | 線幅 2ピクセル | 287 | | 全て同じ罫線 | 5, 10 の倍数ごとに目立つ罫線 | 288 | 289 | ### 通知メッセージの設定 290 | 291 | 拡大率が変化したり,クリップボードにテキストをコピーしたときなどに表示される通知メッセージの設定ができます. 292 | 293 | ![通知メッセージの設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/84609f90-4d0d-4481-b603-9f987d1f2a4f) 294 | 295 | - 拡大率の表示形式は「分数」「小数」「%表示」の3つから選びます.等倍以上と等倍未満の2通りで個別に指定します. 296 | 297 | 表示例: 298 | 299 | | 分数 | 小数 | %表示 | 300 | |---:|---:|---:| 301 | | `x 2` | `x 2.00` | `200%` | 302 | | `x 3/2` | `x 1.50` | `150%` | 303 | | `x 1/2` | `x 0.50` | `50%` | 304 | 305 | - ここでのフォント指定は[色・座標の情報表示](#ドラッグ操作の設定)とは別枠で,独立に指定できます. 306 | 307 | ### 色の設定 308 | 309 | ![色の設定](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/79db70aa-2ca1-4fed-8fd2-c320b1968380) 310 | 311 | 色・座標の情報表示や通知メッセージに表示される枠の色や,色ルーペウィンドウの背景の色を設定できます. 312 | 313 | - 「プリセット...」を選ぶと「ライトモード」「ダークモード」の2種類から既定の色設定に切り替えられます. 314 | 315 | ## 設定メニュー 316 | 317 | Ctrl+右クリック(デフォルト設定の場合)や Shift+F10 で表示されます. 318 | 319 | ![設定メニュー](https://github.com/sigma-axis/aviutl_color_loupe/assets/132639613/ca96e1a6-c5aa-40ff-aa10-f5146c589869) 320 | 321 | 以下のコマンドや設定変更が利用できます. 322 | 323 | - **この点のカラーコードをコピー** 324 | 325 | ポップアップメニューを開く際にクリックした点のカラーコードをクリップボードにコピーします. 326 | - クリックコマンドの「カラーコードをコピー」と同機能です. 327 | - Shift+F10 で表示させた場合は利用できません. 328 | 329 | - **この点の座標をコピー** 330 | 331 | ポップアップメニューを開く際にクリックした点の座標をクリップボードにコピーします. 332 | - クリックコマンドの「座標をコピー」と同機能です. 333 | - Shift+F10 で表示させた場合は利用できません. 334 | 335 | - **この点のをルーペ中央へ移動** 336 | 337 | ポップアップメニューを開く際にクリックした点がルーペウィンドウの中央になるようにルーペ位置を移動します. 338 | - クリックコマンドの「ルーペの中央へ移動」と同機能です. 339 | - Shift+F10 で表示させた場合は利用できません. 340 | 341 | - **メインウィンドウでカーソル追従** 342 | 343 | カーソル追従モードを切り替えます.メイン画面をカーソル移動した際,ホイールクリックがなくてもルーペ位置が移動するようにしたり,移動しないようにできます. 344 | - クリックコマンドの「カーソル追従切り替え」と同機能です. 345 | 346 | - **グリッドの表示** 347 | 348 | グリッドの表示/非表示の状態を切り替えます.拡大率が一定のしきい値以上でないと表示されません. 349 | - クリックコマンドの「グリッド表示切り替え」と同機能です. 350 | 351 | - **ズーム切り替え** 352 | 353 | "裏にあるもう1つの拡大率" と現在の拡大率を入れ替えます.大きい拡大率と小さい拡大率を瞬時に切り替えて操作できます. 354 | - クリックコマンドの「ズーム切り替え」と同機能です. 355 | 356 | - **編集画面の中央へ移動** 357 | 358 | ルーペの位置を初期位置にリセットします. 359 | - クリックコマンドの「編集画面の中央へ移動」と同機能です. 360 | 361 | - **色・座標表示のモード** 362 | 363 | ドラッグ操作の[色・座標の情報表示](#色座標の情報表示)のモードを「ホールド」「トグル / 固定」「トグル / 追従」から指定します. 364 | - 「色ルーペの設定」ダイアログでも[設定変更](#ドラッグ操作の設定)できます.詳しい動作の説明はそちらに記述があります. 365 | 366 | - **マウスホイールでの拡縮反転** 367 | 368 | マウスホイールによる拡大縮小操作のホイール方向を反転します. 369 | - 通常のホイール操作に加えて,各種ドラッグ中のホイール操作での個別設定も反転します. 370 | 371 | - **色ルーペの設定...** 372 | 373 | 「色ルーペの設定」ダイアログを表示します. 374 | - クリックコマンドの「このウィンドウを表示」と同機能です. 375 | 376 | ## 設定ファイルについて 377 | 378 | カスタマイズした設定や現在のルーペの拡大率などの情報は `color_loupe.ini` に保存されています. 379 | 380 | 全ての設定項目は[設定メニュー](#設定メニュー)(デフォルトだと Ctrl+右クリック)から開ける[色ルーペの設定](#操作のカスタマイズ)で設定できますが,必要な場合は直接編集して変更することもできます.ただしその場合,AviUtl を終了した状態で編集を行い,エンコード方式を UTF8 にして保存してください.設定の変更は次回起動時に反映されます. 381 | 382 | ## TIPS 383 | 384 | - 各ドラッグ操作は ESC キーや他のマウスボタンでキャンセルできます. 385 | - [ルーペ移動ドラッグ](#ルーペ移動ドラッグ)をキャンセルした場合,ルーペ位置と拡大率がドラッグ前まで戻ります. 386 | - [色・座標の情報表示](#色座標の情報表示)をキャンセルした場合,表示モードの選択によらず非表示になります. 387 | - [拡大率ドラッグ](#拡大率ドラッグ)をキャンセルした場合,拡大率がドラッグ前まで戻ります. 388 | - [拡張編集ドラッグ](#拡張編集ドラッグ)をキャンセルした場合,「マウスカーソルを元の位置に戻してボタンを離す」という操作をシミュレートします. 389 | 390 | **注意**: 拡張編集ドラッグに関しては,ドラッグ中に Ctrl キーや Alt キーを押したり離したりした場合,キャンセルしても元の状態に戻るとは限りません.この場合,AviUtl のコマンドの「元に戻す」を利用してください.あくまでも単純なドラッグ操作に対する手短なキャンセル手段として利用してください. 391 | 392 | - [色・座標の情報表示](#色座標の情報表示)やクリップボードにコピーされる座標は厳密に言うなら,注目ピクセルの左上の位置を表す座標です. 393 | 394 | 例えばシーンのサイズが $3\times 3$ ピクセルだったとして中央のピクセルを表示させた場合,左上を原点とする基準だと `X: 1, Y: 1`, 中央を原点とした場合 `X: -0.5, Y: -0.5` と表示されます. 395 | - この場合注目ピクセルの中央の座標は左上原点で $(1.5, 1.5)$, 中央原点で $(0, 0)$ となっています. 396 | 397 | - ポップアップメニューはデフォルト設定での Ctrl+右クリックの他にも Shift+F10 でも表示できます. 398 | 399 | うっかり設定を変更してポップアップメニューを表示させる手段を消してしまった際の,設定をやり直す手段として用意しています.この Shift+F10 のキーは変更できません. 400 | 401 | - 各種クリック操作のコマンドに直接は修飾キーの条件は設定できません.デフォルト設定のポップアップメニュー表示の「Ctrl+右クリック」は,次の仕組みで実現しています: 402 | 1. [色・座標の情報表示](#色座標の情報表示)は「Ctrl を押していない右ボタンドラッグ」として割り当てられている. 403 | 1. Ctrl を押していない右クリックは,ドラッグ操作の[色・座標の情報表示](#色座標の情報表示)が優先するのでクリック操作と認識されない. 404 | 1. Ctrl を押しているときは[色・座標の情報表示](#色座標の情報表示)の条件に当てはまらないので,クリック操作と認識される. 405 | 1. 右クリックにはポップアップメニューの表示が割り当てられていて,それが実行される. 406 | 407 | - 各種ドラッグ操作のボタン割り当てによっては,複数のドラッグ操作に当てはまる場合があります(例: 「ルーペ移動ドラッグ」と「拡張編集ドラッグ」に左ボタンを,修飾キー条件を指定せず割り当てるなど).この場合,次の優先順位でドラッグ操作が決定されます: 408 | > ルーペ移動ドラッグ > 色・座標の情報表示 > 拡大率ドラッグ > 拡張編集ドラッグ 409 | 410 | この優先順位は hard-coded で変更不可能です. 411 | 412 | ## 改版履歴 413 | 414 | - **v2.23** (2024-07-19) 415 | 416 | - シーンのサイズを変更している状態で色ルーペウィンドウを再表示時させると表示がおかしくなっていたのを修正. 417 | 418 | - **v2.22** (2024-04-27) 419 | 420 | - フォント選択のコンボボックスに直接フォント名を入力できるように変更. 421 | 422 | 半角空白に続いて後ろに `bold`, `italic`, `bold italic` と付け足すことで **太字**, *斜体*, ***太字斜体*** が使えます. 423 | 424 | - **v2.21** (2024-04-21) 425 | 426 | - バージョン表記が間違っていたのを修正. 427 | 428 | - **v2.20** (2024-04-21) 429 | 430 | - 「拡大率ドラッグ」の機能を追加.ドラッグ操作で拡大率を操作できるように. 431 | 432 | - 通知メッセージで表示される拡大率の形式を,等倍以上と縮小した場合とで個別に指定できるように. 433 | 434 | - **v2.10** (2024-04-20) 435 | 436 | - 全ての設定項目に対して GUI を用意.テキストエディタで `.ini` ファイルを編集しなくてもダイアログ上でフルにカスタマイズできます. 437 | 438 | - 設定ファイル内の項目のうち `chrome_radius` を `chrome_corner` に名前変更 (2箇所).**この項目を変更していた場合は再設定が必要になります.** 439 | - 描画関数への指定が,丸角の円の半径 (radius) ではなく直径だったことに気付いていなかったのが原因.名前からの推測による齟齬を避けるため改名. 440 | - 更新後は `chrome_radius` の項目を削除しても問題ありません. 441 | 442 | - 各マウスボタンに「他ボタンのドラッグをキャンセルする」オプションを追加(初期値は ON). 以前までは必ずドラッグをキャンセルしていたのを,キャンセルせずにドラッグしたままクリックコマンドを実行できるように. 443 | 444 | - クリックコマンド「ルーペの中央へ移動」を追加.クリックしたピクセルをルーペ中央へ持ってくるようにルーペを移動. 445 | 446 | - 色・座標の情報表示や通知メッセージの丸角矩形の描画を改善. 447 | - 高 DPI の設定で「システム(拡張)」を選んでいたときには特に綺麗に表示されるように. 448 | - グラデーションでない単色指定の場合は軽量な描画方法を選ぶように変更. 449 | 450 | - 色・座標の情報表示や通知メッセージの位置やサイズを微調整できる設定項目を増やした. 451 | 452 | - 拡大率の最大,最小値を広げた.ただしデフォルトだと以前の範囲に制限されているので,必要なら設定を変更して範囲を拡大してください. 453 | 454 | - いくつかの設定が正常に保存・復元されていなかったのを修正. 455 | 456 | - 色・座標情報の表示や通知メッセージで日本語フォントが正しく表示されなかったのを修正. 457 | 458 | - ルーペ移動ドラッグ中に拡大率を変化させると,直後の移動が不連続になっていたのを修正. 459 | 460 | - **v2.00** (2024-04-17) 461 | 462 | **v1.20 の設定ファイルとは互換性がなくなりました.更新した際には以前の設定ファイルが正しく読み込まれない可能性があるため `.auf` ファイル,`.ini` ファイルの両方を上書き更新してください.以前の設定を参照したい場合は `.ini` ファイルのバックアップを取って,新しいバージョンで再設定をお願いします.** 463 | 464 | **拡張編集のオブジェクトをドラッグする機能のデフォルト設定を,v1.20 の「Alt+左ドラッグ」から「Ctrl+左ドラッグ」に変更しました.「Ctrl の方が都合が良さそう」との考えで変更しましたが,これは設定で v1.20 の挙動に変更できます.** 465 | 466 | - 設定変更の GUI 追加.設定メニューを刷新. 467 | 468 | - 各種ドラッグ操作のマウスボタンや修飾キーを設定変更できるように. 469 | 470 | - ドラッグ操作やクリック操作に,左,右,中,X1, X2 の5つのマウスボタンを利用できるように.クリック操作も全ボタンでシングルクリック,ダブルクリックの両方に各々コマンドを割り当てられるように. 471 | 472 | - 新しいクリックコマンドを追加: 473 | - 「座標をコピー」: クリックした点の座標を `123,456` の形式でコピーします. 474 | - 「ズームアップ」「ズームダウン」: 拡大率を指定した段階だけ上げ下げします. 475 | 476 | - カラーコードの表示に `RGB( 8, 64,255)` の形式も選べるように.クリップボードへのコピーでも形式の選択ができます. 477 | 478 | - 座標の表示に画面中央からの相対座標が選べるように.クリップボードへのコピーでも形式の選択ができます. 479 | 480 | - ホイールスクロールでの拡大縮小で,1回ホイール入力するごとに変化する段階数を変更できるように. 481 | 482 | - 拡大縮小の最大,最小倍率を変更できるように. 483 | 484 | - 一部クリックコマンドの名前を変更. 485 | 486 | - ファイルを開いていない状態でも通知メッセージが表示されるように. 487 | 488 | - 通知メッセージの配置を左や上にしていた場合の余白が消えていたのを修正. 489 | 490 | - **v1.20** (2024-04-01) 491 | 492 | - Alt+左クリックドラッグで,拡張編集のオブジェクトやアンカーのドラッグ操作をルーペ画面でできる機能を追加. 493 | 494 | - **v1.13** (2024-03-04) 495 | 496 | - 編集のレジューム機能が有効でかつ,プロジェクト内で参照中のファイル見つからない場合,起動時に例外が発生していたのを修正. 497 | 498 | - ルーペウィンドウの IME 入力を無効化.IME が有効な状態だとショートカットキーが使えなかった問題に対処. 499 | 500 | - **v1.12** (2024-02-28) 501 | 502 | - 初期化周りを少し改善. 503 | 504 | - 無駄な関数呼び出しを抑える処理が働いていなかったのを修正. 505 | 506 | - **v1.11** (2024-01-14) 507 | 508 | - グリッド表示に必要な最小の拡大率の上限を緩めた. 509 | 510 | 今までは最大拡大率にすると必ず太線グリッドになっていたが,細線グリッドのまま表示できるように. 511 | 512 | - リソースIDの隙間埋め. 513 | 514 | - **v1.10** (2024-01-08) 515 | 516 | - グリッド機能追加. 517 | 518 | - コード整理. 519 | 520 | - README の語句修正. 521 | 522 | - ライセンス文の年表記を更新. 523 | 524 | - **v1.01** (2023-12-27) 525 | 526 | - 編集データがない状態で画像データを要求していたのを修正. 527 | 528 | - 微修正でコードサイズ微減少. 529 | 530 | - **v1.00** (2023-12-27) 531 | 532 | 初版. 533 | 534 | ## ライセンス 535 | 536 | このプログラムの利用・改変・再頒布等に関しては MIT ライセンスに従うものとします. 537 | 538 | --- 539 | 540 | The MIT License (MIT) 541 | 542 | Copyright (C) 2024 sigma-axis 543 | 544 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 545 | 546 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 547 | 548 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 549 | 550 | https://mit-license.org/ 551 | 552 | # Credits 553 | 554 | ## aviutl_exedit_sdk 555 | 556 | https://github.com/ePi5131/aviutl_exedit_sdk 557 | 558 | --- 559 | 560 | 1条項BSD 561 | 562 | Copyright (c) 2022 563 | ePi All rights reserved. 564 | 565 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 566 | 567 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 568 | THIS SOFTWARE IS PROVIDED BY ePi “AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ePi BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 569 | 570 | # 連絡・バグ報告 571 | 572 | - GitHub: https://github.com/sigma-axis 573 | - Twitter: https://twitter.com/sigma_axis 574 | - nicovideo: https://www.nicovideo.jp/user/51492481 575 | - Misskey.io: https://misskey.io/@sigma_axis 576 | - Bluesky: https://bsky.app/profile/sigma-axis.bsky.social 577 | -------------------------------------------------------------------------------- /assets/color_loupe.ini: -------------------------------------------------------------------------------- 1 | ; 全ての項目が Ctrl+右クリック (デフォルト設定の場合) か, 2 | ; Shift+F10 キーを押して出てくるポップアップメニューから 3 | ;「色ルーペの設定...」を選んで出てくるダイアログで変更できます. 4 | [loupe_drag] 5 | keys.button=1 6 | keys.ctrl=0 7 | keys.shift=-1 8 | keys.alt=-1 9 | range.distance=-1 10 | range.timespan=0 11 | wheel.enabled=1 12 | wheel.reversed=0 13 | wheel.num_steps=1 14 | wheel.pivot=1 15 | lattice=0 16 | rail_mode=1 17 | 18 | [tip_drag] 19 | keys.button=2 20 | keys.ctrl=0 21 | keys.shift=-1 22 | keys.alt=-1 23 | range.distance=-1 24 | range.timespan=0 25 | wheel.enabled=1 26 | wheel.reversed=0 27 | wheel.num_steps=1 28 | wheel.pivot=1 29 | mode=0 30 | rail_mode=1 31 | color_fmt=0 32 | coord_fmt=0 33 | font_name=Consolas 34 | font_size=16 35 | box_inflate=4 36 | box_tip_gap=10 37 | chrome_thick=1 38 | chrome_corner=8 39 | chrome_margin_h=4 40 | chrome_margin_v=4 41 | chrome_pad_h=10 42 | chrome_pad_v=3 43 | 44 | [zoom_drag] 45 | keys.button=0 46 | keys.ctrl=-1 47 | keys.shift=-1 48 | keys.alt=-1 49 | range.distance=-1 50 | range.timespan=0 51 | expand_up=1 52 | num_steps=1 53 | len_per_step=20 54 | pivot=0 55 | 56 | [exedit_drag] 57 | keys.button=1 58 | keys.ctrl=1 59 | keys.shift=-1 60 | keys.alt=-1 61 | range.distance=-1 62 | range.timespan=0 63 | wheel.enabled=1 64 | wheel.reversed=0 65 | wheel.num_steps=1 66 | wheel.pivot=1 67 | fake_shift=0 68 | fake_alt=0 69 | 70 | [zoom] 71 | wheel.enabled=1 72 | wheel.reversed=0 73 | wheel.num_steps=1 74 | wheel.pivot=0 75 | level_min=-13 76 | level_max=20 77 | 78 | [color] 79 | chrome=0x767676 80 | back_top=0xffffff 81 | back_bottom=0xe4ecf7 82 | text=0x3b3d3f 83 | blank=0xf0f0f0 84 | 85 | [toast] 86 | notify_scale=1 87 | notify_follow_cursor=1 88 | notify_grid=1 89 | notify_clipboard=1 90 | placement=8 91 | scale_format=1 92 | duration=3000 93 | font_name=Yu Gothic UI 94 | font_size=18 95 | chrome_thick=1 96 | chrome_corner=8 97 | chrome_margin_h=4 98 | chrome_margin_v=4 99 | chrome_pad_h=6 100 | chrome_pad_v=4 101 | 102 | [grid] 103 | least_zoom_thin=8 104 | least_zoom_thick=12 105 | 106 | [commands] 107 | left.click=0 108 | left.dblclk=1 109 | left.cancels_drag=1 110 | right.click=202 111 | right.dblclk=2 112 | right.cancels_drag=1 113 | middle.click=4 114 | middle.dblclk=0 115 | middle.cancels_drag=1 116 | x1.click=0 117 | x1.dblclk=0 118 | x1.cancels_drag=1 119 | x2.click=0 120 | x2.dblclk=0 121 | x2.cancels_drag=1 122 | swap_zoom_level_pivot=1 123 | step_zoom_pivot=1 124 | step_zoom_num_steps=1 125 | copy_color_fmt=0 126 | copy_coord_fmt=0 127 | 128 | [state] 129 | zoom_level=8 130 | zoom_second=0 131 | follow_cursor=0 132 | show_grid=0 133 | ; 現在のルーペ状態の保存データ. 134 | ; zoom_level: 135 | ; 現在の拡大率レベル.初期値は 8. (4倍) 136 | ; zoom_second: 137 | ; 「ズーム切り替え」をした時の拡大率レベル.初期値は 0. (等倍) 138 | ; follow_cursor: 139 | ; 現在のカーソル追従モード.初期値は 0. (追従しない) 140 | ; show_grid: 141 | ; 現在のグリッド表示状態.初期値は 0. (非表示) 142 | -------------------------------------------------------------------------------- /buffered_dc.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | #define NOMINMAX 18 | #define WIN32_LEAN_AND_MEAN 19 | #include 20 | 21 | namespace sigma_lib::W32::GDI 22 | { 23 | // ダブルバファリング用の HDC ラッパー. 24 | class BufferedDC { 25 | HWND const owner; 26 | HDC const front_dc; 27 | RECT const rect; 28 | HDC back_dc; 29 | HGDIOBJ bmp; 30 | 31 | public: 32 | BufferedDC(HWND hwnd, int width, int height, bool use_back = true) 33 | : owner{ hwnd }, front_dc{ ::GetDC(hwnd) }, rect{ 0, 0, width, height } 34 | { 35 | if (use_back) { 36 | back_dc = ::CreateCompatibleDC(front_dc); 37 | bmp = ::SelectObject(back_dc, ::CreateCompatibleBitmap(front_dc, width, height)); 38 | } 39 | else { 40 | back_dc = nullptr; 41 | bmp = nullptr; 42 | } 43 | } 44 | BufferedDC(HWND hwnd, SIZE const& sz, bool use_back) : BufferedDC(hwnd, sz.cx, sz.cy, use_back) {} 45 | BufferedDC(HWND hwnd, bool use_back = true) : BufferedDC{ hwnd, client_size(hwnd), use_back } {} 46 | 47 | ~BufferedDC() 48 | { 49 | if (is_wrapped()) { 50 | ::BitBlt(front_dc, 0, 0, rect.right, rect.bottom, back_dc, 0, 0, SRCCOPY); 51 | ::DeleteObject(::SelectObject(back_dc, bmp)); 52 | ::DeleteDC(back_dc); 53 | } 54 | 55 | ::ReleaseDC(owner, front_dc); 56 | } 57 | BufferedDC(const BufferedDC&) = delete; 58 | BufferedDC(BufferedDC&&) = delete; 59 | 60 | HWND hwnd() const { return owner; } 61 | constexpr HDC hdc() const { return is_wrapped() ? back_dc : front_dc; } 62 | // width of the client. 63 | constexpr int wd() const { return rect.right; } 64 | // height of the client. 65 | constexpr int ht() const { return rect.bottom; } 66 | 67 | // returns the reference to the client rect. 68 | constexpr RECT const& rc() const { return rect; } 69 | constexpr SIZE const& sz() const { return std::bit_cast(&rect)[1]; } 70 | constexpr bool is_wrapped() const { return back_dc != nullptr; } 71 | 72 | static SIZE client_size(HWND hwnd) { 73 | RECT rc; ::GetClientRect(hwnd, &rc); 74 | return { rc.right, rc.bottom }; 75 | } 76 | }; 77 | 78 | // BitBlt 用の一時描画領域. 79 | class CompatDC { 80 | HDC const back_dc; 81 | HGDIOBJ const bmp; 82 | RECT const rect; 83 | 84 | public: 85 | CompatDC(HDC hdc, int width, int height) 86 | : back_dc{ ::CreateCompatibleDC(hdc) } 87 | , bmp{ ::SelectObject(back_dc, ::CreateCompatibleBitmap(hdc, width, height)) } 88 | , rect{ 0, 0, width, height } {} 89 | CompatDC(const CompatDC&) = delete; 90 | CompatDC(CompatDC&&) = delete; 91 | 92 | HDC hdc() const { return back_dc; } 93 | // width of the bitmap. 94 | constexpr int wd() const { return rect.right; } 95 | // height of the bitmap. 96 | constexpr int ht() const { return rect.bottom; } 97 | 98 | // returns the reference to the bitmap rect. 99 | constexpr RECT const& rc() const { return rect; } 100 | constexpr SIZE const& sz() const { return std::bit_cast(&rect)[1]; } 101 | 102 | ~CompatDC() { 103 | ::DeleteObject(::SelectObject(back_dc, bmp)); 104 | ::DeleteDC(back_dc); 105 | } 106 | }; 107 | } -------------------------------------------------------------------------------- /color_abgr.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #define NOMINMAX 20 | #define WIN32_LEAN_AND_MEAN 21 | #include 22 | 23 | namespace sigma_lib::W32::GDI 24 | { 25 | using byte = uint8_t; 26 | // Win32標準の COLORREF を拡張. 27 | union Color { 28 | COLORREF raw; 29 | struct { byte R, G, B, A; }; 30 | 31 | constexpr Color() : raw{ 0 } {} 32 | template TVal> 33 | constexpr Color(TVal c) : raw{ static_cast(c) } {} 34 | template TVal> 35 | constexpr Color(TVal r, TVal g, TVal b) : R{ static_cast(r) }, G{ static_cast(g) }, B{ static_cast(b) }, A{ 0 } {} 36 | template TVal> 37 | constexpr Color(TVal r, TVal g, TVal b, TVal a) : R{ static_cast(r) }, G{ static_cast(g) }, B{ static_cast(b) }, A{ static_cast(a) } {} 38 | 39 | // conversion between COLORREF, avoiding accidental copies. 40 | static Color& from(COLORREF& c) { return reinterpret_cast(c); } 41 | static const Color& from(const COLORREF& c) { return reinterpret_cast(c); } 42 | constexpr operator COLORREF& () { return raw; } 43 | constexpr operator const COLORREF& () const { return raw; } 44 | 45 | constexpr Color remove_alpha() const { return { raw & 0x00ffffff }; } 46 | // note: doesn't negate the alpha channel. 47 | constexpr Color negate() const { return { raw ^ 0x00ffffff }; } 48 | constexpr static Color interpolate(Color c1, Color c2, int wt1, int wt2) 49 | { 50 | auto i = [=](byte x1, byte x2) { return static_cast((wt1 * x1 + wt2 * x2) / (wt1 + wt2)); }; 51 | return { i(c1.R, c2.R), i(c1.G, c2.G), i(c1.B, c2.B), i(c1.A, c2.A) }; 52 | } 53 | constexpr Color interpolate_to(Color col2, int wt1, int wt2) const { 54 | return interpolate(*this, col2, wt1, wt2); 55 | } 56 | 57 | // compatibilities with #AARRGGBB format. 58 | private: 59 | static constexpr uint32_t swapRB(uint32_t c) { return (0xff00ff00 & c) | std::rotl(0x00ff00ff & c, 16); } 60 | public: 61 | constexpr uint32_t to_formattable() const { return swapRB(raw); } 62 | template TVal> 63 | static constexpr Color fromARGB(TVal c) { return { swapRB(static_cast(c)) }; } 64 | 65 | // brightness properties. 66 | 67 | // lightness of HLS coordinate; MAX+MIN. 68 | // values range from 0 to 510, inclusive. 69 | constexpr static short lightness(byte R, byte G, byte B) 70 | { 71 | const byte* x = &R, * y = &G, * z = &B; 72 | if (*x > *y) std::swap(x, y); 73 | if (*x > *z) std::swap(x, z); 74 | if (*y > *z) std::swap(y, z); 75 | return *x + *z; 76 | } 77 | constexpr short lightness() const { return lightness(R, G, B); } 78 | constexpr static short max_lightness = 510; 79 | 80 | // luma of Y'CrCb coordinate; weighted sum of R, G, B channels. 81 | // values range from 0 to 65535. 82 | static constexpr uint16_t luma(byte r, byte g, byte b) { 83 | return 77 * r + 151 * g + 29 * b; 84 | } 85 | constexpr uint16_t luma() const { return luma(R, G, B); } 86 | constexpr static uint16_t max_luma = 65535; 87 | }; 88 | static_assert(sizeof(Color) == 4); 89 | } 90 | -------------------------------------------------------------------------------- /color_loupe.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #define NOMINMAX 21 | #define WIN32_LEAN_AND_MEAN 22 | #include 23 | #pragma comment(lib, "imm32") 24 | 25 | using byte = uint8_t; 26 | #include 27 | using namespace AviUtl; 28 | 29 | #include "color_abgr.hpp" 30 | #include "buffered_dc.hpp" 31 | using namespace sigma_lib::W32::GDI; 32 | #include "key_states.hpp" 33 | using namespace sigma_lib::W32::UI; 34 | #include "drag_states.hpp" 35 | using namespace sigma_lib::W32::custom::mouse; 36 | 37 | #include "resource.hpp" 38 | #include "settings.hpp" 39 | #include "dialogs.hpp" 40 | 41 | namespace resources { using namespace sigma_lib::W32::resources; } 42 | 43 | //////////////////////////////// 44 | // ルーペ状態の定義 45 | //////////////////////////////// 46 | static inline constinit struct LoupeState { 47 | // position in the picture in pixels, relative to the top-left corner, 48 | // which is displayed at the center of the loupe window. 49 | struct Position { 50 | double x, y; 51 | bool follow_cursor = false; 52 | void clamp(int w, int h) { 53 | x = std::clamp(x, 0, w); 54 | y = std::clamp(y, 0, h); 55 | } 56 | } position{ 0,0 }; 57 | 58 | // zoom --- manages the scaling ratio of zooming. 59 | struct Zoom { 60 | int zoom_level, second_level; 61 | constexpr static int zoom_level_min = -17, zoom_level_max = 24; 62 | 63 | private: 64 | constexpr static int scale_denominator = 4; 65 | static constexpr int scale_base(int level) { 66 | return (4 + (level & 3)) << (level >> 2); 67 | } 68 | public: 69 | static constexpr double scale_ratio(int scale_level) { return scale_level >= 0 ? scale_level == 0 ? 1 : upscale(scale_level) : 1 / downscale(scale_level); } 70 | static constexpr double scale_ratio_inv(int scale_level) { return scale_level >= 0 ? scale_level == 0 ? 1 : 1 / upscale(scale_level) : downscale(scale_level); } 71 | static constexpr auto scale_ratios(int scale_level) { 72 | double s; 73 | return scale_level >= 0 ? scale_level == 0 ? std::make_pair(1.0, 1.0) : 74 | ((s = upscale(scale_level)), std::make_pair(s, 1 / s)) : 75 | ((s=downscale(scale_level)), std::make_pair(1 / s, s)); 76 | } 77 | static constexpr auto scale_ratio_Q(int zoom_level) 78 | { 79 | int n = scale_denominator, d = scale_denominator; 80 | if (zoom_level < 0) d = scale_base(-zoom_level); 81 | else n = scale_base(zoom_level); 82 | // as of either the numerator or the denominator is a power of 2, so is the GCD. 83 | auto gcd = n | d; gcd &= -gcd; // = std::gcd(n, d); 84 | return std::make_pair(n / gcd, d / gcd); 85 | } 86 | static constexpr double upscale(int zoom_level) { return (1.0 / scale_denominator) * scale_base(zoom_level); } 87 | static constexpr double downscale(int zoom_level) { return upscale(-zoom_level); } 88 | 89 | constexpr double scale_ratio() const { return scale_ratio(zoom_level); } 90 | constexpr double scale_ratio_inv() const { return scale_ratio_inv(zoom_level); } 91 | constexpr auto scale_ratios() const { return scale_ratios(zoom_level); } 92 | constexpr auto scale_ratio_Q() const { return scale_ratio_Q(zoom_level); } 93 | } zoom{ 8, 0 }; 94 | 95 | // tip --- tooltip-like info. 96 | struct Tip { 97 | int x = 0, y = 0; 98 | uint8_t visible_level = 0; // 0: not visible, 1: not visible until mouse enters, >= 2: visible. 99 | constexpr bool is_visible() const { return visible_level > 1; } 100 | bool prefer_above = false; 101 | } tip; 102 | 103 | // toast --- notification message. 104 | struct Toast { 105 | constexpr static int max_len_message = 32; 106 | wchar_t message[max_len_message]{ L"" }; 107 | bool visible = false; 108 | } toast{}; 109 | 110 | // grid 111 | struct { 112 | bool visible = false; 113 | } grid; 114 | 115 | //////////////////////////////// 116 | // coordinate transforms. 117 | //////////////////////////////// 118 | 119 | // returned coordinates are relative to the center of the window. 120 | constexpr auto pic2win(double x, double y) const { 121 | auto s = zoom.scale_ratio(); 122 | return std::make_pair(s * (x - position.x), s * (y - position.y)); 123 | } 124 | // input coordinates are relative to the center of the window. 125 | constexpr auto win2pic(double x, double y) const { 126 | auto s = zoom.scale_ratio_inv(); 127 | return std::make_pair(s * x + position.x, s * y + position.y); 128 | } 129 | 130 | // returns { pic2win, win2pic }-pair. 131 | constexpr auto transforms() const { 132 | auto [s1, s2] = zoom.scale_ratios(); 133 | 134 | return std::make_pair([s1, px = position.x, py = position.y](double x, double y) { 135 | return std::make_pair(s1 * (x - px), s1 * (y - py)); 136 | }, [s2, px = position.x, py = position.y](double x, double y) { 137 | return std::make_pair(s2 * x + px, s2 * y + py); 138 | }); 139 | } 140 | 141 | // returns the pair of "view box" and "view port". 142 | // view box: the area of the picture to be on-screen. 143 | // view port: the area of the window where the picture will be on. 144 | // those are rounded so view box covers the entire pixels to be on-screen, 145 | // and view port may cover beyond the window corners. 146 | std::pair viewbox_viewport(int picture_w, int picture_h, int client_w, int client_h) const 147 | { 148 | auto [p2w, w2p] = transforms(); 149 | auto [pl, pt] = w2p(-0.5 * client_w, -0.5 * client_h); 150 | auto [pr, pb] = w2p(+0.5 * client_w, +0.5 * client_h); 151 | pl = std::max(std::floor(pl), 0); 152 | pt = std::max(std::floor(pt), 0); 153 | pr = std::min(std::ceil(pr), picture_w); 154 | pb = std::min(std::ceil(pb), picture_h); 155 | 156 | auto [wl, wt] = p2w(pl, pt); 157 | auto [wr, wb] = p2w(pr, pb); 158 | wl = std::floor(0.5 + wl + 0.5 * client_w); 159 | wt = std::floor(0.5 + wt + 0.5 * client_h); 160 | wr = std::floor(0.5 + wr + 0.5 * client_w); 161 | wb = std::floor(0.5 + wb + 0.5 * client_h); 162 | 163 | constexpr auto i = [](double x) { return static_cast(x); }; 164 | return { 165 | { i(pl), i(pt), i(pr), i(pb) }, 166 | { i(wl), i(wt), i(wr), i(wb) }, 167 | }; 168 | } 169 | 170 | //////////////////////////////// 171 | // UI helping functions. 172 | //////////////////////////////// 173 | 174 | // change the scale keeping the specified position unchanged. 175 | void apply_zoom(int new_level, double win_ox, double win_oy, int pic_w, int pic_h) { 176 | auto s = zoom.scale_ratio_inv(); 177 | zoom.zoom_level = std::clamp(new_level, Zoom::zoom_level_min, Zoom::zoom_level_max); 178 | s -= zoom.scale_ratio_inv(); 179 | 180 | position.x += s * win_ox; position.y += s * win_oy; 181 | position.clamp(pic_w, pic_h); 182 | } 183 | 184 | // resets some states that is suitable for a new sized image. 185 | void on_resize(int picture_w, int picture_h) 186 | { 187 | auto w2 = picture_w / 2, h2 = picture_h / 2; 188 | position.x = static_cast(w2); position.y = static_cast(h2); 189 | tip = { .x = w2, .y = h2, .visible_level = 0 }; 190 | } 191 | } loupe_state; 192 | static_assert(LoupeState::Zoom::zoom_level_max == Settings::Zoom::level_max_max); 193 | static_assert(LoupeState::Zoom::zoom_level_min == Settings::Zoom::level_min_min); 194 | 195 | // export a function. 196 | std::tuple dialogs::ExtFunc::ScaleFromZoomLevel(int zoom_level) { 197 | return decltype(loupe_state.zoom)::scale_ratio_Q(zoom_level); 198 | } 199 | 200 | 201 | //////////////////////////////// 202 | // 設定ロードセーブ. 203 | //////////////////////////////// 204 | 205 | // replacing a file extension when it's known. 206 | template 207 | static void replace_tail(T(&dst)[len_max], size_t len, const T(&tail_old)[len_old], const T(&tail_new)[len_new]) 208 | { 209 | if (len < len_old || len - len_old + len_new > len_max) return; 210 | std::memcpy(dst + len - len_old, tail_new, len_new * sizeof(T)); 211 | } 212 | static inline void load_settings() 213 | { 214 | char path[MAX_PATH]; 215 | replace_tail(path, ::GetModuleFileNameA(this_dll, path, std::size(path)) + 1, "auf", "ini"); 216 | 217 | settings.load(path); 218 | 219 | // additionally load the following loupe states. 220 | loupe_state.zoom.zoom_level = std::clamp( 221 | static_cast(::GetPrivateProfileIntA("state", "zoom_level", loupe_state.zoom.zoom_level, path)), 222 | LoupeState::Zoom::zoom_level_min, LoupeState::Zoom::zoom_level_max); 223 | loupe_state.zoom.second_level = std::clamp( 224 | static_cast(::GetPrivateProfileIntA("state", "zoom_second", loupe_state.zoom.second_level, path)), 225 | LoupeState::Zoom::zoom_level_min, LoupeState::Zoom::zoom_level_max); 226 | loupe_state.position.follow_cursor = ::GetPrivateProfileIntA("state", "follow_cursor", 227 | loupe_state.position.follow_cursor ? 1 : 0, path) != 0; 228 | loupe_state.grid.visible = ::GetPrivateProfileIntA("state", "show_grid", 229 | loupe_state.position.follow_cursor ? 1 : 0, path) != 0; 230 | } 231 | static inline void save_settings() 232 | { 233 | char path[MAX_PATH]; 234 | replace_tail(path, ::GetModuleFileNameA(this_dll, path, std::size(path)) + 1, "auf", "ini"); 235 | 236 | settings.save(path); 237 | 238 | // additionally save the following loupe states. 239 | char buf[std::size("+4294967296")]; 240 | std::snprintf(buf, std::size(buf), "%d", loupe_state.zoom.zoom_level); 241 | ::WritePrivateProfileStringA("state", "zoom_level", buf, path); 242 | std::snprintf(buf, std::size(buf), "%d", loupe_state.zoom.second_level); 243 | ::WritePrivateProfileStringA("state", "zoom_second", buf, path); 244 | ::WritePrivateProfileStringA("state", "follow_cursor", 245 | loupe_state.position.follow_cursor ? "1" : "0", path); 246 | ::WritePrivateProfileStringA("state", "show_grid", 247 | loupe_state.grid.visible ? "1" : "0", path); 248 | } 249 | 250 | 251 | //////////////////////////////// 252 | // 画像バッファ. 253 | //////////////////////////////// 254 | static inline constinit class ImageBuffer { 255 | BITMAPINFO bi{ 256 | .bmiHeader = { 257 | .biSize = sizeof(bi.bmiHeader), 258 | .biPlanes = 1, 259 | .biBitCount = 24, 260 | .biCompression = BI_RGB, 261 | }, 262 | }; 263 | 264 | void* buf = nullptr; 265 | template 266 | constexpr auto& wd(this TSelf& self) { return self.bi.bmiHeader.biWidth; } 267 | template 268 | constexpr auto& ht(this TSelf& self) { return self.bi.bmiHeader.biHeight; } 269 | 270 | void reallocate(int w, int h) 271 | { 272 | int size_next = stride(w) * h, size_curr = stride() * ht(); 273 | wd() = w; ht() = h; 274 | 275 | if (size_next == size_curr) return; 276 | if (buf != nullptr) ::VirtualFree(buf, 0, MEM_RELEASE); 277 | buf = size_next > 0 ? ::VirtualAlloc(nullptr, size_next, MEM_COMMIT, PAGE_READWRITE) : nullptr; 278 | } 279 | 280 | public: 281 | constexpr const void* buffer() const { return buf; } 282 | constexpr const BITMAPINFO& info() const { return bi; } 283 | constexpr auto width() const { return wd(); } 284 | constexpr auto height() const { return ht(); } 285 | 286 | static constexpr int stride(int width) { 287 | // rounding upward into a multiple of 4. 288 | return (3 * width + 3) & (-4); 289 | } 290 | constexpr int stride() const { return stride(width()); } 291 | 292 | Color color_at(int x, int y) const 293 | { 294 | if (buf == nullptr || 295 | x < 0 || y < 0 || x >= wd() || y >= ht()) return CLR_INVALID; 296 | auto ptr = reinterpret_cast(buf) + 3 * x + stride() * (ht() - 1 - y); 297 | return { ptr[2], ptr[1], ptr[0] }; 298 | } 299 | 300 | // returns true if the image size has been changed. 301 | bool update(int w, int h, void* source) 302 | { 303 | // check for size changes to the image. 304 | bool size_changed = false; 305 | if (wd() != w || ht() != h) 306 | { 307 | size_changed = true; 308 | reallocate(w, h); 309 | } 310 | 311 | std::memcpy(buf, source, stride() * ht()); 312 | return size_changed; 313 | } 314 | 315 | void free() 316 | { 317 | if (buf != nullptr) { 318 | ::VirtualFree(buf, 0, MEM_RELEASE), buf = nullptr; 319 | wd() = ht() = 0; 320 | } 321 | } 322 | 323 | bool is_valid() const { return buf != nullptr; } 324 | } image; 325 | 326 | 327 | //////////////////////////////// 328 | // ハンドル管理. 329 | //////////////////////////////// 330 | template 331 | class Handle { 332 | std::tuple data; 333 | template 334 | constexpr auto& handle(this TSelf& self) { return std::get<0>(self.data); } 335 | public: 336 | constexpr Handle(TArgs... args) : data { nullptr, args... }{} 337 | H get() 338 | { 339 | if (handle() == nullptr) { 340 | handle() = [this](std::index_sequence) { 341 | return creator(std::get(data)...); 342 | }(std::make_index_sequence{}); 343 | } 344 | return handle(); 345 | } 346 | void free() { 347 | if (handle() != nullptr) { 348 | deleter(handle()); 349 | handle() = nullptr; 350 | } 351 | } 352 | 353 | operator H() { return get(); } 354 | constexpr bool is_valid() const { return handle() != nullptr; } 355 | 356 | Handle(const Handle&) = delete; 357 | constexpr Handle(Handle&& that) : data{ that.data } { that.handle() = nullptr; } 358 | ~Handle() { free(); } 359 | }; 360 | static HFONT create_font(const wchar_t* name, int size) { 361 | return ::CreateFontW( 362 | size, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, DEFAULT_CHARSET, 363 | OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, 364 | DEFAULT_PITCH | FF_DONTCARE, name); 365 | } 366 | static constinit Handle 369 | tip_font{ &settings.tip_drag.font_name, &settings.tip_drag.font_size }, 370 | toast_font{ &settings.toast.font_name, &settings.toast.font_size }; 371 | 372 | static constinit Handle 373 | cxt_menu{}; 374 | 375 | // export a function. 376 | HFONT dialogs::ExtFunc::CreateUprightFont(wchar_t const* name, int size) { return create_font(name, size); } 377 | 378 | 379 | //////////////////////////////// 380 | // 通知メッセージのタイマー管理. 381 | //////////////////////////////// 382 | static inline constinit class ToastManager { 383 | HWND hwnd = nullptr; 384 | bool active = false; 385 | constexpr static auto& toast = loupe_state.toast; 386 | 387 | auto timer_id() const { return reinterpret_cast(this); } 388 | static void CALLBACK timer_proc(HWND hwnd, auto, uintptr_t id, auto) 389 | { 390 | // turn the timer off. 391 | ::KillTimer(hwnd, id); 392 | 393 | auto* that = reinterpret_cast(id); 394 | if (that == nullptr || !that->active || that->hwnd != hwnd) return; // might be a wrong call. 395 | 396 | // update the variable associated to the timer. 397 | that->active = false; 398 | 399 | // remove the toast. 400 | that->erase_core(); 401 | } 402 | // hwnd must not be nullptr. 403 | void set_timer(int time_ms) { 404 | if (time_ms < USER_TIMER_MINIMUM) time_ms = USER_TIMER_MINIMUM; 405 | 406 | active = true; 407 | ::SetTimer(hwnd, timer_id(), time_ms, timer_proc); 408 | // WM_TIMER won't be posted to the window procedure. 409 | } 410 | void kill_timer() { 411 | if (active && hwnd != nullptr) { 412 | ::KillTimer(hwnd, timer_id()); 413 | active = false; 414 | } 415 | } 416 | 417 | void erase_core() { 418 | if (toast.visible) { 419 | toast.visible = false; 420 | if (hwnd != nullptr) 421 | ::PostMessageW(hwnd, WM_PAINT, {}, {}); // ::InvalidateRect() caused flickering. 422 | } 423 | } 424 | 425 | public: 426 | bool is_active() const { return active; } 427 | HWND host_window() const { return hwnd; } 428 | 429 | // set nullptr when exitting. 430 | void set_host(HWND hwnd) { 431 | erase(); 432 | this->hwnd = hwnd; 433 | } 434 | 435 | void set_message(int time_ms, uint32_t id, const auto&... args) 436 | { 437 | if (hwnd == nullptr) return; 438 | 439 | // load / format the message string from resource. 440 | if constexpr (sizeof...(args) > 0) 441 | std::swprintf(toast.message, std::size(toast.message), resources::string::get(id), args...); 442 | else resources::string::copy(id, toast.message); 443 | toast.visible = true; 444 | 445 | // turn a timer on. 446 | set_timer(time_ms); 447 | } 448 | 449 | // removes the toast message. 450 | void erase() 451 | { 452 | kill_timer(); // turn a timer off if exists. 453 | erase_core(); // remove the toast. 454 | } 455 | } toast_manager; 456 | 457 | 458 | //////////////////////////////// 459 | // 外部リソース管理. 460 | //////////////////////////////// 461 | static inline constinit class { 462 | bool activated = false; 463 | 464 | public: 465 | // global switch for allocating external resources. 466 | constexpr bool is_active() const { return activated; } 467 | void activate() { activated = true; } 468 | void deactivate() 469 | { 470 | activated = false; 471 | free(); 472 | } 473 | void free() 474 | { 475 | image.free(); 476 | tip_font.free(); 477 | toast_font.free(); 478 | cxt_menu.free(); 479 | toast_manager.erase(); 480 | } 481 | } ext_obj; 482 | 483 | 484 | //////////////////////////////// 485 | // 描画関数. 486 | //////////////////////////////// 487 | 488 | // 背景描画. 489 | static inline void draw_backplane(HDC hdc, const RECT& rc) 490 | { 491 | ::SetDCBrushColor(hdc, settings.color.blank); 492 | ::FillRect(hdc, &rc, static_cast(::GetStockObject(DC_BRUSH))); 493 | } 494 | 495 | // 画像描画. 496 | static inline void draw_picture(HDC hdc, const RECT& vb, const RECT& vp) 497 | { 498 | ::SetStretchBltMode(hdc, STRETCH_DELETESCANS); 499 | ::StretchDIBits(hdc, vp.left, vp.top, vp.right - vp.left, vp.bottom - vp.top, 500 | vb.left, image.height() - vb.bottom, vb.right - vb.left, vb.bottom - vb.top, 501 | image.buffer(), &image.info(), DIB_RGB_COLORS, SRCCOPY); 502 | } 503 | 504 | // グリッド描画 (thin) 505 | static inline void draw_grid_thin(HDC hdc, const RECT& vb, const RECT& vp) 506 | { 507 | int w = vb.right - vb.left, W = vp.right - vp.left, 508 | h = vb.bottom - vb.top, H = vp.bottom - vp.top; 509 | 510 | auto old_mode = ::SetROP2(hdc, R2_NOT); 511 | for (int x = vb.left; x < vb.right; x++) { 512 | auto X = (x - vb.left) * W / w + vp.left; 513 | ::MoveToEx(hdc, X, vp.top, nullptr); 514 | ::LineTo(hdc, X, vp.bottom); 515 | } 516 | for (int y = vb.top; y < vb.bottom; y++) { 517 | auto Y = (y - vb.top) * H / h + vp.top; 518 | ::MoveToEx(hdc, vp.left, Y, nullptr); 519 | ::LineTo(hdc, vp.right, Y); 520 | } 521 | ::SetROP2(hdc, old_mode); 522 | } 523 | 524 | // グリッド描画 (thick) 525 | static inline void draw_grid_thick(HDC hdc, const RECT& vb, const RECT& vp) 526 | { 527 | int w = vb.right - vb.left, W = vp.right - vp.left, 528 | h = vb.bottom - vb.top, H = vp.bottom - vp.top; 529 | 530 | auto dc_brush = static_cast(::GetStockObject(DC_BRUSH)); 531 | constexpr Color col0 = 0x222222, col1 = 0x444444, col2 = 0x666666, 532 | col3 = col2.negate(), col4 = col1.negate(), col5 = col0.negate(); 533 | 534 | RECT rch = vp, rcv = vp; 535 | auto hline = [&, hdc, dc_brush](int y) { rch.top = y; rch.bottom = y + 1; ::FillRect(hdc, &rch, dc_brush); }; 536 | auto vline = [&, hdc, dc_brush](int x) { rcv.left = x; rcv.right = x + 1; ::FillRect(hdc, &rcv, dc_brush); }; 537 | 538 | // least significant lines. 539 | ::SetDCBrushColor(hdc, col3); 540 | for (int x = vb.left; x < vb.right; x++) { 541 | if (x % 5 != 0) vline((x - vb.left) * W / w + vp.left); 542 | } 543 | for (int y = vb.top; y < vb.bottom; y++) { 544 | if (y % 5 != 0) hline((y - vb.top) * H / h + vp.top); 545 | } 546 | ::SetDCBrushColor(hdc, col2); 547 | for (int x = vb.left + 1; x <= vb.right; x++) { 548 | if (x % 5 != 0) vline((x - vb.left) * W / w + vp.left - 1); 549 | } 550 | for (int y = vb.top + 1; y <= vb.bottom; y++) { 551 | if (y % 5 != 0) hline((y - vb.top) * H / h + vp.top - 1); 552 | } 553 | 554 | // multiple of 5, but not 10. 555 | ::SetDCBrushColor(hdc, col4); 556 | for (int x = vb.left + 9 - ((vb.left + 4) % 10); x < vb.right; x += 10) 557 | vline((x - vb.left) * W / w + vp.left); 558 | for (int y = vb.top + 9 - ((vb.top + 4) % 10); y < vb.bottom; y += 10) 559 | hline((y - vb.top) * H / h + vp.top); 560 | ::SetDCBrushColor(hdc, col1); 561 | for (int x = vb.left + 10 - ((vb.left + 5) % 10); x <= vb.right; x += 10) 562 | vline((x - vb.left) * W / w + vp.left - 1); 563 | for (int y = vb.top + 10 - ((vb.top + 5) % 10); y <= vb.bottom; y += 10) 564 | hline((y - vb.top) * H / h + vp.top - 1); 565 | 566 | // multiple of 10. 567 | ::SetDCBrushColor(hdc, col5); 568 | for (int x = vb.left + 9 - ((vb.left + 9) % 10); x < vb.right; x += 10) 569 | vline((x - vb.left) * W / w + vp.left); 570 | for (int y = vb.top + 9 - ((vb.top + 9) % 10); y < vb.bottom; y += 10) 571 | hline((y - vb.top) * H / h + vp.top); 572 | ::SetDCBrushColor(hdc, col0); 573 | for (int x = vb.left + 10 - (vb.left % 10); x <= vb.right; x += 10) 574 | vline((x - vb.left) * W / w + vp.left - 1); 575 | for (int y = vb.top + 10 - (vb.top % 10); y <= vb.bottom; y += 10) 576 | hline((y - vb.top) * H / h + vp.top - 1); 577 | } 578 | 579 | // 背景グラデーション & 枠付きの丸角矩形を描画. 580 | static inline void draw_round_rect(HDC hdc, const RECT& rc, int corner, int thick, Color back_top, Color back_btm, Color chrome) 581 | { 582 | auto br = ::SelectObject(hdc, ::GetStockObject(DC_BRUSH)), 583 | pen = ::SelectObject(hdc, ::GetStockObject(NULL_PEN)); 584 | if (thick > 0) { 585 | ::SetDCBrushColor(hdc, chrome); 586 | ::RoundRect(hdc, rc.left - thick, rc.top - thick, rc.right + thick, rc.bottom + thick, corner, corner); 587 | corner = std::max(0, corner - 2 * thick); 588 | } 589 | 590 | if (back_top == back_btm) { 591 | // simply draw a solid rounded rect. 592 | ::SetDCBrushColor(hdc, back_top); 593 | ::RoundRect(hdc, rc.left, rc.top, rc.right, rc.bottom, corner, corner); 594 | } 595 | else { 596 | auto w = rc.right - rc.left, h = rc.bottom - rc.top; 597 | constexpr GRADIENT_RECT rcs{ 0, 1 }; 598 | TRIVERTEX vertices[]{ 599 | { 0, 0, static_cast(back_top.R << 8), static_cast(back_top.G << 8), static_cast(back_top.B << 8) }, 600 | { w, h, static_cast(back_btm.R << 8), static_cast(back_btm.G << 8), static_cast(back_btm.B << 8) } 601 | }; 602 | 603 | // draw the gradation to another buffer. 604 | auto grad = CompatDC(hdc, w, h); 605 | ::GdiGradientFill(grad.hdc(), vertices, 2, const_cast(&rcs), 1, GRADIENT_FILL_RECT_V); 606 | 607 | // use XOR two times and black brush to clip off the rounded corner. 608 | ::SetDCBrushColor(hdc, 0); // black 609 | ::BitBlt(hdc, rc.left, rc.top, w, h, grad.hdc(), 0, 0, SRCINVERT); 610 | ::RoundRect(hdc, rc.left, rc.top, rc.right, rc.bottom, corner, corner); 611 | ::BitBlt(hdc, rc.left, rc.top, w, h, grad.hdc(), 0, 0, SRCINVERT); 612 | } 613 | ::SelectObject(hdc, br); ::SelectObject(hdc, pen); 614 | } 615 | // 色・座標表示ボックス描画. 616 | static inline void draw_tip(HDC hdc, const SIZE& canvas, const RECT& box, 617 | Color pixel_color, const POINT& pix, const SIZE& screen, bool& prefer_above, 618 | HFONT font, const Settings::TipDrag& tip_drag, const Settings::ColorScheme& color_scheme) 619 | { 620 | RECT box_big = box; 621 | box_big.left -= tip_drag.box_inflate; box_big.right += tip_drag.box_inflate; 622 | box_big.top -= tip_drag.box_inflate; box_big.bottom += tip_drag.box_inflate; 623 | 624 | // draw the "pixel box". 625 | ::SetDCBrushColor(hdc, pixel_color); 626 | ::FillRect(hdc, &box_big, static_cast(::GetStockObject(DC_BRUSH))); 627 | ::FrameRect(hdc, &box_big, static_cast(::GetStockObject( 628 | pixel_color.luma() <= Color::max_luma / 2 ? WHITE_BRUSH : BLACK_BRUSH))); 629 | 630 | // prepare text. 631 | 632 | // prepare the string to place in. 633 | wchar_t tip_str[std::bit_ceil( 634 | std::max(std::size(L"#RRGGBB"), std::size(L"RGB(000,000,000)")) + 1 + 635 | std::max(std::size(L"X:1234, Y:1234"), std::size(L"X:-1234.5, Y:-1234.5")))]; 636 | int tip_strlen = 0; 637 | switch (tip_drag.color_fmt) { 638 | using enum Settings::ColorFormat; 639 | case dec3x3: 640 | tip_strlen += std::swprintf(tip_str, std::size(tip_str), 641 | L"RGB(%3u,%3u,%3u)\n", pixel_color.R, pixel_color.G, pixel_color.B); 642 | break; 643 | case hexdec6: 644 | default: 645 | tip_strlen += std::swprintf(tip_str, std::size(tip_str), 646 | L"#%06X\n", pixel_color.to_formattable()); 647 | break; 648 | } 649 | switch (tip_drag.coord_fmt) { 650 | using enum Settings::CoordFormat; 651 | case origin_center: 652 | { 653 | constexpr auto len_prec = [](int scr) -> std::pair { 654 | // no half-integer when (scr & 1) == 0. 655 | // at most 3 digits (plus sign) when scr < 2000. 4 digits is rarely necessary. 656 | return { 1 + (scr < 2000 ? 3 : 4) + (2 * (scr & 1)), scr & 1 }; 657 | }; 658 | auto [x_l, x_p] = len_prec(screen.cx); 659 | auto [y_l, y_p] = len_prec(screen.cy); 660 | tip_strlen += std::swprintf(&tip_str[tip_strlen], std::size(tip_str) - tip_strlen, 661 | L"X:%*.*f, Y:%*.*f", x_l, x_p, pix.x - screen.cx / 2.0, y_l, y_p, pix.y - screen.cy / 2.0); 662 | break; 663 | } 664 | case origin_top_left: 665 | default: 666 | tip_strlen += std::swprintf(&tip_str[tip_strlen], std::size(tip_str) - tip_strlen, 667 | L"X:%4d, Y:%4d", pix.x, pix.y); 668 | break; 669 | } 670 | 671 | // measure the text size. 672 | auto tmp_fon = ::SelectObject(hdc, font); 673 | RECT rc_txt{}, rc_frm; 674 | ::DrawTextW(hdc, tip_str, tip_strlen, &rc_txt, DT_CENTER | DT_TOP | DT_NOPREFIX | DT_NOCLIP | DT_CALCRECT); 675 | { 676 | const int w = rc_txt.right - rc_txt.left, h = rc_txt.bottom - rc_txt.top, 677 | x_min = tip_drag.chrome_margin_h + tip_drag.chrome_pad_h, 678 | x_max = canvas.cx - (w + tip_drag.chrome_pad_h + tip_drag.chrome_margin_h), 679 | y_min = tip_drag.chrome_margin_v + tip_drag.chrome_pad_v, 680 | y_max = canvas.cy - (h + tip_drag.chrome_pad_v + tip_drag.chrome_margin_v); 681 | 682 | int x = (box.left + box.right) / 2 - w / 2; 683 | 684 | // choose whether the tip should be placed above or below the box. 685 | const int yU = box.top - tip_drag.box_tip_gap - tip_drag.chrome_pad_v - h, 686 | yD = box.bottom + tip_drag.box_tip_gap + tip_drag.chrome_pad_v; 687 | if (auto up = y_min <= yU, dn = yD <= y_max; up || dn) 688 | prefer_above = prefer_above ? up : !dn; 689 | int y = prefer_above ? yU : yD; 690 | 691 | constexpr auto adjust = [](auto v, auto m, auto M) { return m > M ? (m + M) / 2 : std::clamp(v, m, M); }; 692 | x = adjust(x, x_min, x_max); 693 | y = adjust(y, y_min, y_max); 694 | 695 | rc_txt = { x, y, x + w, y + h }; 696 | rc_frm = { 697 | rc_txt.left - tip_drag.chrome_pad_h, rc_txt.top - tip_drag.chrome_pad_v, 698 | rc_txt.right + tip_drag.chrome_pad_h, rc_txt.bottom + tip_drag.chrome_pad_v 699 | }; 700 | } 701 | 702 | // draw the round rect and its frame. 703 | draw_round_rect(hdc, rc_frm, tip_drag.chrome_corner, tip_drag.chrome_thick, 704 | color_scheme.back_top, color_scheme.back_bottom, color_scheme.chrome); 705 | 706 | // then draw the text. 707 | ::SetTextColor(hdc, color_scheme.text); 708 | ::SetBkMode(hdc, TRANSPARENT); 709 | ::DrawTextW(hdc, tip_str, tip_strlen, &rc_txt, DT_CENTER | DT_TOP | DT_NOPREFIX | DT_NOCLIP); 710 | ::SelectObject(hdc, tmp_fon); 711 | } 712 | // 通知メッセージトースト描画. 713 | static inline void draw_toast(HDC hdc, const SIZE& canvas, const wchar_t* message, 714 | HFONT font, const Settings::Toast& toast, const Settings::ColorScheme& color_scheme) 715 | { 716 | _ASSERT(ext_obj.is_active()); 717 | 718 | // measure the text size. 719 | auto tmp_fon = ::SelectObject(hdc, font); 720 | RECT rc_txt{}, rc_frm{}; 721 | ::DrawTextW(hdc, message, -1, &rc_txt, DT_NOPREFIX | DT_NOCLIP | DT_CALCRECT); 722 | 723 | // align horizontally. 724 | auto l = rc_txt.right - rc_txt.left; 725 | switch (toast.placement) { 726 | using enum Settings::Toast::Placement; 727 | case top_left: 728 | case left: 729 | case bottom_left: 730 | rc_frm.left = toast.chrome_margin_h; 731 | goto from_left; 732 | case top_right: 733 | case right: 734 | case bottom_right: 735 | rc_frm.right = canvas.cx - toast.chrome_margin_h; 736 | rc_txt.right = rc_frm.right - toast.chrome_pad_h; 737 | rc_txt.left = rc_txt.right - l; 738 | rc_frm.left = rc_txt.left - toast.chrome_pad_h; 739 | break; 740 | default: 741 | rc_frm.left = canvas.cx / 2 - l / 2 - toast.chrome_pad_h; 742 | from_left: 743 | rc_txt.left = rc_frm.left + toast.chrome_pad_h; 744 | rc_txt.right = rc_txt.left + l; 745 | rc_frm.right = rc_txt.right + toast.chrome_pad_h; 746 | break; 747 | } 748 | 749 | // then vertically. 750 | l = rc_txt.bottom - rc_txt.top; 751 | switch (toast.placement) { 752 | using enum Settings::Toast::Placement; 753 | case top_left: 754 | case top: 755 | case top_right: 756 | rc_frm.top = toast.chrome_margin_v; 757 | goto from_top; 758 | case bottom_left: 759 | case bottom: 760 | case bottom_right: 761 | rc_frm.bottom = canvas.cy - toast.chrome_margin_v; 762 | rc_txt.bottom = rc_frm.bottom - toast.chrome_pad_v; 763 | rc_txt.top = rc_txt.bottom - l; 764 | rc_frm.top = rc_txt.top - toast.chrome_pad_v; 765 | break; 766 | default: 767 | rc_frm.top = canvas.cy / 2 - l / 2 - toast.chrome_pad_v; 768 | from_top: 769 | rc_txt.top = rc_frm.top + toast.chrome_pad_v; 770 | rc_txt.bottom = rc_txt.top + l; 771 | rc_frm.bottom = rc_txt.bottom + toast.chrome_pad_v; 772 | break; 773 | } 774 | 775 | // draw the round rect and its frame. 776 | draw_round_rect(hdc, rc_frm, toast.chrome_corner, toast.chrome_thick, 777 | color_scheme.back_top, color_scheme.back_bottom, color_scheme.chrome); 778 | 779 | // then draw the text. 780 | ::SetTextColor(hdc, color_scheme.text); 781 | ::SetBkMode(hdc, TRANSPARENT); 782 | ::DrawTextW(hdc, message, -1, &rc_txt, DT_NOPREFIX | DT_NOCLIP); 783 | ::SelectObject(hdc, tmp_fon); 784 | } 785 | 786 | // 未編集時などの無効状態で単色背景を描画 (+通知メッセージも). 787 | static inline void draw_blank(HWND hwnd) 788 | { 789 | auto toast_visible = ext_obj.is_active() && loupe_state.toast.visible; 790 | BufferedDC bf{ hwnd, toast_visible }; 791 | 792 | draw_backplane(bf.hdc(), bf.rc()); 793 | if (toast_visible) draw_toast(bf.hdc(), bf.sz(), 794 | loupe_state.toast.message, toast_font, settings.toast, settings.color); 795 | } 796 | 797 | // メインの描画関数. 798 | static inline void draw(HWND hwnd) 799 | { 800 | // image.is_valid() must be true here. 801 | _ASSERT(image.is_valid()); 802 | 803 | // firstly, collect information before drawing. 804 | auto [wd, ht] = BufferedDC::client_size(hwnd); 805 | 806 | // obtain the viewbox/viewport pair of the image. 807 | auto [vb, vp] = loupe_state.viewbox_viewport(image.width(), image.height(), wd, ht); 808 | 809 | // whether some part of window isn't covered by image. 810 | bool is_partial = vp.left > 0 || vp.right < wd || 811 | vp.top > 0 || vp.bottom < ht; 812 | 813 | uint8_t grid_thick = loupe_state.grid.visible ? 814 | settings.grid.grid_thick(loupe_state.zoom.zoom_level) : 0; 815 | 816 | // whether the tip is visible. 817 | constexpr auto& tip = loupe_state.tip; 818 | bool with_tip = tip.is_visible() && 819 | tip.x >= 0 && tip.x < image.width() && tip.y >= 0 && tip.y < image.height(); 820 | 821 | // now collected information to know whether double-buffering should help. 822 | // in most cases, whole window is covered by a single image and needs not wrapping. 823 | BufferedDC bf{ hwnd, wd, ht, is_partial || grid_thick > 0 || with_tip || loupe_state.toast.visible }; 824 | 825 | // now ready for drawing... 826 | // some part of the window is exposed. fill the background. 827 | if (is_partial) draw_backplane(bf.hdc(), bf.rc()); 828 | 829 | // draw the main image. 830 | draw_picture(bf.hdc(), vb, vp); 831 | 832 | // draw the grid. 833 | if (grid_thick == 1) draw_grid_thin(bf.hdc(), vb, vp); 834 | else if (grid_thick >= 2) draw_grid_thick(bf.hdc(), vb, vp); 835 | 836 | // draw the info tip. 837 | if (with_tip) { 838 | auto [x, y] = loupe_state.pic2win(tip.x, tip.y); 839 | x += bf.wd() / 2.0; y += bf.ht() / 2.0; 840 | auto s = loupe_state.zoom.scale_ratio(); 841 | draw_tip(bf.hdc(), bf.sz(), { 842 | static_cast(std::floor(x)), static_cast(std::floor(y)), 843 | static_cast(std::ceil(x + s)), static_cast(std::ceil(y + s)) 844 | }, image.color_at(tip.x, tip.y), { tip.x, tip.y }, { image.width(), image.height() }, 845 | tip.prefer_above, tip_font, settings.tip_drag, settings.color); 846 | } 847 | 848 | // draw the toast. 849 | if (loupe_state.toast.visible) draw_toast(bf.hdc(), bf.sz(), 850 | loupe_state.toast.message, toast_font, settings.toast, settings.color); 851 | } 852 | 853 | // export two functions. 854 | void dialogs::ExtFunc::DrawTip(HDC hdc, const SIZE& canvas, const RECT& box, 855 | Color pixel_color, const POINT& pix, const SIZE& screen, bool& prefer_above, 856 | HFONT font, const Settings::TipDrag& tip_drag, const Settings::ColorScheme& color_scheme){ 857 | draw_tip(hdc, canvas, box, pixel_color, pix, screen, prefer_above, font, tip_drag, color_scheme); 858 | } 859 | void dialogs::ExtFunc::DrawToast(HDC hdc, const SIZE& canvas, const wchar_t* message, 860 | HFONT font, const Settings::Toast& toast, const Settings::ColorScheme& color_scheme) { 861 | draw_toast(hdc, canvas, message, font, toast, color_scheme); 862 | } 863 | 864 | 865 | //////////////////////////////// 866 | // ドラッグ操作実装. 867 | //////////////////////////////// 868 | struct DragCxt { 869 | EditHandle* editp; 870 | WPARAM wparam; 871 | bool redraw_loupe; 872 | bool redraw_main; 873 | }; 874 | class DragState : public DragStateBase { 875 | friend class VoidDragState; 876 | protected: 877 | static std::pair win2pic(const POINT& p) { 878 | auto [w, h] = BufferedDC::client_size(hwnd); 879 | return loupe_state.win2pic(p.x - w / 2.0, p.y - h / 2.0); 880 | } 881 | static POINT win2pic_i(const POINT& p) { 882 | auto [x, y] = win2pic(p); 883 | return { .x = static_cast(std::floor(x)), .y = static_cast(std::floor(y)) }; 884 | } 885 | 886 | void Abort_core(context& cxt) override { Cancel_core(cxt); } 887 | 888 | virtual Settings::KeysActivate KeysActivate() { return {}; } 889 | virtual Settings::WheelZoom WheelZoom() { return settings.zoom.wheel; } 890 | 891 | public: 892 | static void InitiateDrag(HWND hwnd, const POINT& drag_start, context& cxt); 893 | static Settings::WheelZoom CurrentWheelZoom() { 894 | auto* drag = current_drag(); 895 | return drag != nullptr ? drag->WheelZoom() : settings.zoom.wheel; 896 | } 897 | }; 898 | 899 | static inline constinit class LoupeDrag : public DragState { 900 | int revert_zoom{}; 901 | double revert_x{}, revert_y{}, 902 | prev_x{}, prev_y{}, curr_x{}, curr_y{}; 903 | constexpr static auto& pos = loupe_state.position; 904 | 905 | protected: 906 | bool Ready_core(context& cxt) override 907 | { 908 | if (!image.is_valid()) return false; 909 | 910 | revert_x = curr_x = prev_x = pos.x; 911 | revert_y = curr_y = prev_y = pos.y; 912 | revert_zoom = loupe_state.zoom.zoom_level; 913 | 914 | using namespace resources::cursor; 915 | ::SetCursor(get(hand)); 916 | return true; 917 | } 918 | void Delta_core(const POINT& curr, context& cxt) override 919 | { 920 | double dx = curr.x - last_point.x, dy = curr.y - last_point.y; 921 | if (dx == 0 && dy == 0) return; 922 | 923 | auto s = loupe_state.zoom.scale_ratio_inv(); 924 | curr_x += (pos.x - prev_x) - s * dx; 925 | curr_y += (pos.y - prev_y) - s * dy; 926 | 927 | std::tie(dx, dy) = snap_rail(curr_x - revert_x, curr_y - revert_y, 928 | (cxt.wparam & MK_SHIFT) != 0 ? settings.loupe_drag.rail_mode : RailMode::none, 929 | settings.loupe_drag.lattice); 930 | double px = revert_x, py = revert_y; 931 | if (settings.loupe_drag.lattice) 932 | px = std::round(2 * px) / 2, py = std::round(2 * py) / 2; // half-integral. 933 | px = std::clamp(px + dx, 0, image.width()); 934 | py = std::clamp(py + dy, 0, image.height()); 935 | 936 | if (px == pos.x && py == pos.y) return; 937 | pos.x = prev_x = px; pos.y = prev_y = py; 938 | cxt.redraw_loupe = true; 939 | } 940 | void Cancel_core(context& cxt) override 941 | { 942 | pos.x = revert_x; pos.y = revert_y; 943 | loupe_state.zoom.zoom_level = revert_zoom; 944 | cxt.redraw_loupe = true; 945 | } 946 | 947 | DragInvalidRange InvalidRange() override { return settings.loupe_drag.range; } 948 | Settings::KeysActivate KeysActivate() override { return settings.loupe_drag.keys; } 949 | Settings::WheelZoom WheelZoom() override { return settings.loupe_drag.wheel; } 950 | } loupe_drag; 951 | 952 | static inline constinit class TipDrag : public DragState { 953 | constexpr static auto& tip = loupe_state.tip; 954 | POINT init{}; 955 | LoupeState::Tip revert{}; 956 | 957 | static bool in_image(int x, int y) { return 0 <= x && x < image.width() && 0 <= y && y < image.height(); } 958 | 959 | protected: 960 | bool Ready_core(context& cxt) override 961 | { 962 | if (!image.is_valid()) return false; 963 | 964 | revert = tip; 965 | 966 | bool hide_cursor = true; 967 | switch (settings.tip_drag.mode) { 968 | using enum Settings::TipDrag::Mode; 969 | case frail: 970 | tip.visible_level = 2; 971 | break; 972 | case stationary: 973 | case sticky: 974 | default: 975 | if (tip.is_visible()) { 976 | tip.visible_level = 0; 977 | hide_cursor = false; 978 | } 979 | else tip.visible_level = 2; 980 | break; 981 | } 982 | 983 | init = win2pic_i(drag_start); 984 | tip.x = init.x; tip.y = init.y; 985 | 986 | cxt.redraw_loupe = true; 987 | if (hide_cursor && in_image(tip.x, tip.y)) ::SetCursor(nullptr); 988 | 989 | return true; 990 | } 991 | void Unready_core(context& cxt, bool was_valid) override 992 | { 993 | if (was_valid) return; 994 | tip = revert; 995 | cxt.redraw_loupe = true; 996 | } 997 | void Delta_core(const POINT& curr, context& cxt) override 998 | { 999 | if (!tip.is_visible()) return; 1000 | 1001 | auto [dx, dy] = win2pic(curr); 1002 | std::tie(dx, dy) = snap_rail(dx - (0.5 + init.x), dy - (0.5 + init.y), 1003 | (cxt.wparam & MK_SHIFT) != 0 ? settings.tip_drag.rail_mode : RailMode::none, true); 1004 | auto x = init.x + static_cast(dx), y = init.y + static_cast(dy); 1005 | 1006 | if (x == tip.x && y == tip.y) return; 1007 | 1008 | if (auto hide = in_image(x, y); hide ^ in_image(tip.x, tip.y)) { 1009 | using namespace resources::cursor; 1010 | ::SetCursor(hide ? nullptr : get(arrow)); 1011 | } 1012 | 1013 | tip.x = x; tip.y = y; 1014 | cxt.redraw_loupe = true; 1015 | } 1016 | void End_core(context& cxt) override 1017 | { 1018 | if (!tip.is_visible()) return; 1019 | 1020 | switch (settings.tip_drag.mode) { 1021 | using enum Settings::TipDrag::Mode; 1022 | case frail: 1023 | tip.visible_level = 0; 1024 | cxt.redraw_loupe = true; 1025 | break; 1026 | } 1027 | return; 1028 | } 1029 | void Cancel_core(context& cxt) override 1030 | { 1031 | tip.visible_level = 0; 1032 | cxt.redraw_loupe = true; 1033 | } 1034 | 1035 | DragInvalidRange InvalidRange() override { return settings.tip_drag.range; } 1036 | Settings::KeysActivate KeysActivate() override { return settings.tip_drag.keys; } 1037 | Settings::WheelZoom WheelZoom() override { return settings.tip_drag.wheel; } 1038 | } tip_drag; 1039 | 1040 | static bool apply_zoom(int new_level, double win_ox, double win_oy); 1041 | static inline constinit class ZoomDrag : public DragState { 1042 | int revert_level{}; 1043 | double revert_x{}, revert_y{}, win_ox{}, win_oy{}; 1044 | 1045 | constexpr static auto& level = loupe_state.zoom.zoom_level; 1046 | constexpr static auto& level_min = settings.zoom.level_min; 1047 | constexpr static auto& level_max = settings.zoom.level_max; 1048 | constexpr static auto& pos_x = loupe_state.position.x; 1049 | constexpr static auto& pos_y = loupe_state.position.y; 1050 | 1051 | bool apply_zoom(int new_level) { 1052 | if (new_level == level) return false; 1053 | 1054 | if (new_level == revert_level) { 1055 | // firstly rewind the scale, so the toast will pop up. 1056 | ::apply_zoom(revert_level, 0, 0); 1057 | 1058 | // rewind the position too. 1059 | pos_x = revert_x; 1060 | pos_y = revert_y; 1061 | return true; 1062 | } 1063 | else { 1064 | // firstly rewind to the initial state, 1065 | // so the after effect of "clamping to the edge of the screen" doesn't affect. 1066 | level = revert_level; 1067 | pos_x = revert_x; 1068 | pos_y = revert_y; 1069 | return ::apply_zoom(new_level, win_ox, win_oy); 1070 | } 1071 | } 1072 | 1073 | protected: 1074 | bool Ready_core(context& cxt) override 1075 | { 1076 | if (!image.is_valid()) return false; 1077 | 1078 | revert_level = level; 1079 | revert_x = pos_x; 1080 | revert_y = pos_y; 1081 | if (settings.zoom_drag.pivot == Settings::WheelZoom::cursor) { 1082 | auto [cx, cy] = BufferedDC::client_size(hwnd); 1083 | win_ox = drag_start.x - cx / 2.0; 1084 | win_oy = drag_start.y - cy / 2.0; 1085 | } 1086 | else win_ox = win_oy = 0; 1087 | 1088 | using namespace resources::cursor; 1089 | ::SetCursor(get(sizens)); 1090 | return true; 1091 | } 1092 | void Delta_core(const POINT& curr, context& cxt) override 1093 | { 1094 | constexpr auto& cfg = settings.zoom_drag; 1095 | 1096 | auto diff = curr.y - drag_start.y; 1097 | if (cfg.expand_up) diff = -diff; 1098 | 1099 | constexpr auto floor_div = [](int n, int d) { 1100 | auto div = std::div(n, d); 1101 | if ((div.rem ^ d) < 0) div.quot--; 1102 | return div.quot; 1103 | }; 1104 | diff = floor_div(diff + cfg.len_per_step / 2, cfg.len_per_step); 1105 | 1106 | cxt.redraw_loupe |= apply_zoom(revert_level + cfg.num_steps * diff); 1107 | } 1108 | void Cancel_core(context& cxt) override 1109 | { 1110 | cxt.redraw_loupe |= apply_zoom(revert_level); 1111 | } 1112 | 1113 | DragInvalidRange InvalidRange() override { return settings.zoom_drag.range; } 1114 | Settings::KeysActivate KeysActivate() override { return settings.zoom_drag.keys; } 1115 | // intensionally disable. 1116 | Settings::WheelZoom WheelZoom() override { return Settings::WheelZoom{ .enabled = false }; } 1117 | } zoom_drag; 1118 | 1119 | static inline constinit class ExEditDrag : public DragState { 1120 | POINT revert{}, last{}; 1121 | 1122 | using FilterMessage = FilterPlugin::WindowMessage; 1123 | constexpr static auto name_exedit = "拡張編集"; 1124 | 1125 | static inline constinit FilterPlugin* exedit_fp = nullptr; 1126 | static void send_message(context& cxt, UINT message, const POINT& pos, WPARAM btn) 1127 | { 1128 | constexpr auto force_btn = [](WPARAM wparam, WPARAM btn) { 1129 | constexpr WPARAM all_btns = MK_LBUTTON | MK_MBUTTON | MK_RBUTTON | MK_XBUTTON1 | MK_XBUTTON2; 1130 | return (wparam & ~all_btns) | btn; 1131 | }; 1132 | constexpr auto code_pos = [](auto x, auto y) { 1133 | return static_cast((x & 0xffff) | (y << 16)); 1134 | }; 1135 | if (exedit_fp->func_WndProc != nullptr && exedit_fp->hwnd != nullptr) 1136 | cxt.redraw_main |= exedit_fp->func_WndProc(exedit_fp->hwnd, message, 1137 | force_btn(cxt.wparam, btn), code_pos(pos.x, pos.y), cxt.editp, exedit_fp) != FALSE; 1138 | } 1139 | static void fake_wparam(const Settings::ExEditDrag& op, WPARAM& wp) { 1140 | switch (op.fake_shift) { 1141 | case flag_map::off: wp &= ~MK_SHIFT; break; 1142 | case flag_map::on: wp |= MK_SHIFT; break; 1143 | case flag_map::inv: wp ^= MK_SHIFT; break; 1144 | case flag_map::id: default: break; 1145 | } 1146 | } 1147 | 1148 | public: 1149 | static void init(FilterPlugin* this_fp) { 1150 | if (exedit_fp != nullptr) return; 1151 | 1152 | SysInfo si; this_fp->exfunc->get_sys_info(nullptr, &si); 1153 | for (int i = 0; i < si.filter_n; i++) { 1154 | auto that_fp = this_fp->exfunc->get_filterp(i); 1155 | if (that_fp->name != nullptr && 1156 | 0 == std::strcmp(that_fp->name, name_exedit)) { 1157 | exedit_fp = that_fp; 1158 | return; 1159 | } 1160 | } 1161 | } 1162 | static bool is_valid() { return exedit_fp != nullptr; } 1163 | 1164 | protected: 1165 | bool Ready_core(context& cxt) override 1166 | { 1167 | if (!is_valid() || !image.is_valid()) return false; 1168 | 1169 | last = win2pic_i(last_point); 1170 | if (0 <= last.x && last.x < image.width() && 1171 | 0 <= last.y && last.y < image.height()) { 1172 | revert = last; 1173 | 1174 | using namespace resources::cursor; 1175 | ::SetCursor(get(sizeall)); 1176 | return true; 1177 | } 1178 | return false; 1179 | } 1180 | void Start_core(context& cxt) override 1181 | { 1182 | const auto& op = settings.exedit_drag; 1183 | ForceKeyState k{ VK_SHIFT, op.fake_shift, VK_MENU, op.fake_alt }; 1184 | fake_wparam(op, cxt.wparam); 1185 | send_message(cxt, FilterMessage::MainMouseDown, last, MK_LBUTTON); 1186 | } 1187 | void Delta_core(const POINT& curr, context& cxt) override 1188 | { 1189 | auto pt = win2pic_i(curr); 1190 | if (pt.x == last.x && pt.y == last.y) return; 1191 | last = pt; 1192 | 1193 | const auto& op = settings.exedit_drag; 1194 | ForceKeyState k{ VK_SHIFT, op.fake_shift, VK_MENU, op.fake_alt }; 1195 | fake_wparam(op, cxt.wparam); 1196 | send_message(cxt, FilterMessage::MainMouseMove, last, MK_LBUTTON); 1197 | } 1198 | void End_core(context& cxt) override 1199 | { 1200 | const auto& op = settings.exedit_drag; 1201 | ForceKeyState k{ VK_SHIFT, op.fake_shift, VK_MENU, op.fake_alt }; 1202 | fake_wparam(op, cxt.wparam); 1203 | send_message(cxt, FilterMessage::MainMouseUp, last, 0); 1204 | } 1205 | void Cancel_core(context& cxt) override 1206 | { 1207 | // may not revert back to previous state 1208 | // like when Alt is pressed or released during the drag, 1209 | // but sometimes it works well and can be helpful, so leave this feature as is. 1210 | cxt.wparam = 0; 1211 | ForceKeyState k{ VK_SHIFT, false, VK_MENU, false }; 1212 | send_message(cxt, FilterMessage::MainMouseMove, revert, MK_LBUTTON); 1213 | send_message(cxt, FilterMessage::MainMouseUp, revert, 0); 1214 | } 1215 | 1216 | DragInvalidRange InvalidRange() override { return settings.exedit_drag.range; } 1217 | Settings::KeysActivate KeysActivate() override { return settings.exedit_drag.keys; } 1218 | Settings::WheelZoom WheelZoom() override { return settings.exedit_drag.wheel; } 1219 | } exedit_drag; 1220 | 1221 | // placeholder drag. might be used as a fallback. 1222 | static inline constinit class VoidDragState : public VoidDrag { 1223 | protected: 1224 | // let resemble the "click-or-not" behavior to the origin of the fallback. 1225 | DragInvalidRange InvalidRange() override { 1226 | return surrogate != nullptr ? surrogate->InvalidRange() : VoidDrag::InvalidRange(); 1227 | } 1228 | 1229 | public: 1230 | DragState* surrogate = nullptr; 1231 | } void_drag; 1232 | 1233 | inline void DragState::InitiateDrag(HWND hwnd, const POINT& drag_start, context& cxt) 1234 | { 1235 | if (!ext_obj.is_active()) return; 1236 | 1237 | constexpr WPARAM btns = MK_LBUTTON | MK_RBUTTON | MK_MBUTTON | MK_XBUTTON1 | MK_XBUTTON2; 1238 | auto btn = [&] { 1239 | switch (cxt.wparam & btns) { 1240 | using enum MouseButton; 1241 | case MK_LBUTTON: return left; 1242 | case MK_RBUTTON: return right; 1243 | case MK_MBUTTON: return middle; 1244 | case MK_XBUTTON1: return x1; 1245 | case MK_XBUTTON2: return x2; 1246 | default: return none; // no button or multiple buttons. 1247 | } 1248 | }(); 1249 | if (btn == MouseButton::none) return; 1250 | 1251 | bool ctrl = (cxt.wparam & MK_CONTROL) != 0, 1252 | shift = (cxt.wparam & MK_SHIFT) != 0, 1253 | alt = ::GetKeyState(VK_MENU) < 0; 1254 | 1255 | constexpr DragState* drags[] = { &loupe_drag, &tip_drag, &zoom_drag, &exedit_drag }; 1256 | void_drag.surrogate = nullptr; 1257 | for (auto* drag : drags) { 1258 | // check the button/key-combination condition. 1259 | if (drag->KeysActivate().match(btn, ctrl, shift, alt)) { 1260 | // then type-specific condition. 1261 | if (drag->Start(hwnd, btn, drag_start, cxt)) return; 1262 | void_drag.surrogate = drag; 1263 | break; 1264 | } 1265 | } 1266 | 1267 | // initiate the "placeholder drag". 1268 | void_drag.Start(hwnd, btn, drag_start, cxt); 1269 | } 1270 | 1271 | 1272 | //////////////////////////////// 1273 | // その他コマンド. 1274 | //////////////////////////////// 1275 | // returns true if the window needs redrawing. 1276 | static inline bool centralize() 1277 | { 1278 | if (!image.is_valid()) return false; 1279 | 1280 | // centralize the target point. 1281 | loupe_state.position.x = image.width() / 2.0; 1282 | loupe_state.position.y = image.height() / 2.0; 1283 | return true; 1284 | } 1285 | static inline bool centralize_point(double win_ox, double win_oy) 1286 | { 1287 | if (!image.is_valid()) return false; 1288 | 1289 | // move the target point to the specified position. 1290 | auto [x, y] = loupe_state.win2pic(win_ox, win_oy); 1291 | loupe_state.position.x = std::floor(std::clamp(x, 0, image.width() - 1)) + 0.5; 1292 | loupe_state.position.y = std::floor(std::clamp(y, 0, image.height() - 1)) + 0.5; 1293 | return true; 1294 | } 1295 | static inline bool toggle_follow_cursor() 1296 | { 1297 | loupe_state.position.follow_cursor ^= true; 1298 | 1299 | // toast message. 1300 | if (!settings.toast.notify_follow_cursor) return false; 1301 | toast_manager.set_message(settings.toast.duration, 1302 | loupe_state.position.follow_cursor ? IDS_TOAST_FOLLOW_CURSOR_ON : IDS_TOAST_FOLLOW_CURSOR_OFF); 1303 | return true; 1304 | } 1305 | static inline bool toggle_grid() 1306 | { 1307 | loupe_state.grid.visible ^= true; 1308 | 1309 | // toast message. 1310 | if (!settings.toast.notify_grid) return true; 1311 | toast_manager.set_message(settings.toast.duration, 1312 | loupe_state.grid.visible ? IDS_TOAST_GRID_ON : IDS_TOAST_GRID_OFF); 1313 | return true; 1314 | } 1315 | static inline bool apply_zoom(int new_level, double win_ox, double win_oy) 1316 | { 1317 | // ignore if the zoom level is identical. 1318 | new_level = std::clamp(new_level, 1319 | std::max(settings.zoom.level_min, LoupeState::Zoom::zoom_level_min), 1320 | std::min(settings.zoom.level_max, LoupeState::Zoom::zoom_level_max)); 1321 | if (new_level == loupe_state.zoom.zoom_level) return false; 1322 | 1323 | // apply. 1324 | if (image.is_valid()) 1325 | loupe_state.apply_zoom(new_level, win_ox, win_oy, image.width(), image.height()); 1326 | else loupe_state.zoom.zoom_level = new_level; 1327 | 1328 | // toast message. 1329 | if (!settings.toast.notify_scale) return true; 1330 | wchar_t scale[std::max({ 1331 | std::size(L"x 123/456"), 1332 | std::size(L"x 123.45"), 1333 | std::size(L"123.45%"), 1334 | })]; 1335 | switch (new_level >= 0 ? settings.toast.scale_format : settings.toast.scale_format_low) { 1336 | using enum Settings::Toast::ScaleFormat; 1337 | case decimal: 1338 | std::swprintf(scale, std::size(scale), L"x %0.2f", loupe_state.zoom.scale_ratio()); 1339 | break; 1340 | case percent: 1341 | std::swprintf(scale, std::size(scale), L"%d%%", 1342 | static_cast(std::round(100 * loupe_state.zoom.scale_ratio()))); 1343 | break; 1344 | case fraction: 1345 | default: 1346 | if (auto [n, d] = loupe_state.zoom.scale_ratio_Q(); 1347 | d == 1) std::swprintf(scale, std::size(scale), L"x %d", n); 1348 | else std::swprintf(scale, std::size(scale), L"x %d/%d", n, d); 1349 | break; 1350 | } 1351 | toast_manager.set_message(settings.toast.duration, IDS_TOAST_SCALE_FORMAT, scale); 1352 | return true; 1353 | } 1354 | static inline bool swap_zoom_level(double win_ox, double win_oy, Settings::WheelZoom::Pivot pivot) 1355 | { 1356 | auto level = loupe_state.zoom.zoom_level; 1357 | std::swap(level, loupe_state.zoom.second_level); 1358 | 1359 | if (pivot == Settings::WheelZoom::center) win_ox = win_oy = 0; 1360 | return apply_zoom(level, win_ox, win_oy); 1361 | } 1362 | static bool copy_text(const wchar_t* text) 1363 | { 1364 | if (!::OpenClipboard(nullptr)) return false; 1365 | bool success = false; 1366 | 1367 | ::EmptyClipboard(); 1368 | auto len = std::wcslen(text); 1369 | if (auto h = ::GlobalAlloc(GHND, (len + 1) * sizeof(wchar_t))) { 1370 | if (auto ptr = reinterpret_cast(::GlobalLock(h))) { 1371 | std::memcpy(ptr, text, (len + 1) * sizeof(wchar_t)); 1372 | ::GlobalUnlock(h); 1373 | ::SetClipboardData(CF_UNICODETEXT, h); 1374 | 1375 | success = true; 1376 | } 1377 | else ::GlobalFree(h); 1378 | } 1379 | ::CloseClipboard(); 1380 | return success; 1381 | } 1382 | static inline bool copy_color_code(double win_ox, double win_oy) 1383 | { 1384 | if (!image.is_valid()) return false; 1385 | 1386 | auto [x, y] = loupe_state.win2pic(win_ox, win_oy); 1387 | auto color = image.color_at(static_cast(std::floor(x)), static_cast(std::floor(y))); 1388 | if (color.A != 0) return false; 1389 | 1390 | wchar_t buf[std::max(std::size(L"rrggbb"), std::size(L"RGB(255,255,255)"))]; 1391 | switch (settings.commands.copy_color_fmt) { 1392 | using enum Settings::ColorFormat; 1393 | case dec3x3: 1394 | std::swprintf(buf, std::size(buf), L"RGB(%u,%u,%u)", color.R, color.G, color.B); 1395 | break; 1396 | case hexdec6: 1397 | default: 1398 | std::swprintf(buf, std::size(buf), L"%06x", color.to_formattable()); 1399 | break; 1400 | } 1401 | if (!copy_text(buf)) return false; 1402 | 1403 | // toast message. 1404 | if (!settings.toast.notify_clipboard) return false; 1405 | toast_manager.set_message(settings.toast.duration, IDS_TOAST_CLIPBOARD, buf); 1406 | return true; 1407 | } 1408 | static inline bool copy_coordinate(double win_ox, double win_oy) 1409 | { 1410 | if (!image.is_valid()) return false; 1411 | 1412 | auto [x, y] = loupe_state.win2pic(win_ox, win_oy); 1413 | int img_x = static_cast(std::floor(x)), 1414 | img_y = static_cast(std::floor(y)), 1415 | w = image.width(), h = image.height(); 1416 | if (!(0 <= img_x && img_x < w && 0 <= img_y && img_y < h)) return false; 1417 | 1418 | wchar_t buf[std::max(std::size(L"1234,1234"), std::size(L"-1234.5,-1234.5"))]; 1419 | switch (settings.commands.copy_coord_fmt) { 1420 | using enum Settings::CoordFormat; 1421 | case origin_center: 1422 | { 1423 | x = img_x - w / 2.0; y = img_y - h / 2.0; 1424 | if (!(std::abs(x) < 10'000 && std::abs(y) < 10'000)) return false; 1425 | std::swprintf(buf, std::size(buf), L"%.*f,%.*f", w & 1, x, h & 1, y); 1426 | break; 1427 | } 1428 | case origin_top_left: 1429 | default: 1430 | if (!(img_x < 10'000 && img_y < 10'000)) return false; 1431 | std::swprintf(buf, std::size(buf), L"%d,%d", img_x, img_y); 1432 | break; 1433 | } 1434 | if (!copy_text(buf)) return false; 1435 | 1436 | // toast message. 1437 | if (!settings.toast.notify_clipboard) return false; 1438 | toast_manager.set_message(settings.toast.duration, IDS_TOAST_CLIPBOARD, buf); 1439 | return true; 1440 | } 1441 | // update the tip position when it's following the mouse cursor. returns true for all cases. 1442 | static inline bool tip_to_cursor(HWND hwnd) 1443 | { 1444 | if (!loupe_state.tip.is_visible() 1445 | || (settings.tip_drag.mode != Settings::TipDrag::sticky && 1446 | DragState::current_drag() != &tip_drag)) return true; 1447 | 1448 | POINT pt; ::GetCursorPos(&pt); ::ScreenToClient(hwnd, &pt); 1449 | auto [w, h] = BufferedDC::client_size(hwnd); 1450 | 1451 | auto [x, y] = loupe_state.win2pic(pt.x - w / 2.0, pt.y - h / 2.0); 1452 | loupe_state.tip.x = static_cast(std::floor(x)); 1453 | loupe_state.tip.y = static_cast(std::floor(y)); 1454 | return true; 1455 | } 1456 | static inline bool open_settings(HWND hwnd) 1457 | { 1458 | if (dialogs::open_settings(hwnd)) { 1459 | // hide the toast as its duration/visibility might have changed. 1460 | toast_manager.erase(); 1461 | 1462 | // hide the tip as settings for the tip might have changed. 1463 | loupe_state.tip.visible_level = 0; 1464 | 1465 | // re-apply the zoom level, as its valid range might have changed. 1466 | apply_zoom(loupe_state.zoom.zoom_level, 0, 0); 1467 | 1468 | // discard font handles for new font settings. 1469 | tip_font.free(); 1470 | toast_font.free(); 1471 | return true; 1472 | } 1473 | return false; 1474 | } 1475 | 1476 | 1477 | //////////////////////////////// 1478 | // メニュー操作. 1479 | //////////////////////////////// 1480 | struct Menu { 1481 | static bool popup_menu(HWND hwnd, bool by_mouse) 1482 | { 1483 | if (!ext_obj.is_active()) return false; 1484 | 1485 | HMENU menu = cxt_menu; 1486 | auto ena = [&](uint32_t id, bool enabled) { ::EnableMenuItem(menu, id, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_GRAYED)); }; 1487 | auto chk = [&](uint32_t id, bool check) { ::CheckMenuItem(menu, id, MF_BYCOMMAND | (check ? MF_CHECKED : MF_UNCHECKED)); }; 1488 | 1489 | // prepare the context menu. 1490 | ena(IDM_CXT_PT_COPY_COLOR, by_mouse && image.is_valid()); 1491 | ena(IDM_CXT_PT_COPY_COORD, by_mouse && image.is_valid()); 1492 | ena(IDM_CXT_PT_BRING_CENTER, by_mouse && image.is_valid()); 1493 | chk(IDM_CXT_FOLLOW_CURSOR, loupe_state.position.follow_cursor); 1494 | chk(IDM_CXT_SHOW_GRID, loupe_state.grid.visible); 1495 | ena(IDM_CXT_SWAP_ZOOM, image.is_valid()); 1496 | ena(IDM_CXT_CENTRALIZE, image.is_valid()); 1497 | chk(IDM_CXT_TIP_MODE_FRAIL, settings.tip_drag.mode == Settings::TipDrag::frail); 1498 | chk(IDM_CXT_TIP_MODE_STATIONARY, settings.tip_drag.mode == Settings::TipDrag::stationary); 1499 | chk(IDM_CXT_TIP_MODE_STICKY, settings.tip_drag.mode == Settings::TipDrag::sticky); 1500 | chk(IDM_CXT_REVERSE_WHEEL, settings.zoom.wheel.reversed); 1501 | 1502 | POINT pt; ::GetCursorPos(&pt); 1503 | auto id = ::TrackPopupMenuEx(::GetSubMenu(menu, 0), 1504 | TPM_RIGHTBUTTON | TPM_NONOTIFY | TPM_RETURNCMD, pt.x, pt.y, hwnd, nullptr); 1505 | 1506 | return id != 0 && on_menu_command(hwnd, id, by_mouse, pt); 1507 | } 1508 | // returns true if needs redraw. 1509 | static bool on_menu_command(HWND hwnd, uint32_t id, bool by_mouse, const POINT& pt_screen = {0, 0}) 1510 | { 1511 | if (!ext_obj.is_active()) return false; 1512 | 1513 | auto rel_win_center = [&] { 1514 | POINT pt = pt_screen; ::ScreenToClient(hwnd, &pt); 1515 | auto [w, h] = BufferedDC::client_size(hwnd); 1516 | return std::make_pair(pt.x - w / 2.0, pt.y - h / 2.0); 1517 | }; 1518 | 1519 | switch (id) { 1520 | case IDM_CXT_PT_COPY_COLOR: 1521 | if (by_mouse) { 1522 | auto [x, y] = rel_win_center(); 1523 | return copy_color_code(x, y); 1524 | } 1525 | break; 1526 | case IDM_CXT_PT_COPY_COORD: 1527 | if (by_mouse) { 1528 | auto [x, y] = rel_win_center(); 1529 | return copy_coordinate(x, y); 1530 | } 1531 | break; 1532 | case IDM_CXT_PT_BRING_CENTER: 1533 | if (by_mouse) { 1534 | auto [x, y] = rel_win_center(); 1535 | return centralize_point(x, y); 1536 | } 1537 | break; 1538 | 1539 | case IDM_CXT_FOLLOW_CURSOR: return toggle_follow_cursor(); 1540 | case IDM_CXT_SHOW_GRID: return toggle_grid(); 1541 | case IDM_CXT_SWAP_ZOOM: 1542 | { 1543 | double x = 0, y = 0; 1544 | auto pivot = Settings::WheelZoom::center; 1545 | if (by_mouse) { 1546 | std::tie(x, y) = rel_win_center(); 1547 | pivot = settings.commands.swap_zoom_level_pivot; 1548 | } 1549 | return swap_zoom_level(x, y, pivot) && tip_to_cursor(hwnd); 1550 | } 1551 | case IDM_CXT_CENTRALIZE: return centralize(); 1552 | 1553 | case IDM_CXT_REVERSE_WHEEL: 1554 | settings.loupe_drag.wheel.reversed ^= true; 1555 | settings.tip_drag.wheel.reversed ^= true; 1556 | settings.exedit_drag.wheel.reversed ^= true; 1557 | settings.zoom.wheel.reversed ^= true; 1558 | break; 1559 | 1560 | case IDM_CXT_TIP_MODE_FRAIL: settings.tip_drag.mode = Settings::TipDrag::frail; goto hide_tip_and_redraw; 1561 | case IDM_CXT_TIP_MODE_STATIONARY: settings.tip_drag.mode = Settings::TipDrag::stationary; goto hide_tip_and_redraw; 1562 | case IDM_CXT_TIP_MODE_STICKY: settings.tip_drag.mode = Settings::TipDrag::sticky; goto hide_tip_and_redraw; 1563 | hide_tip_and_redraw: 1564 | loupe_state.tip.visible_level = 0; 1565 | return true; 1566 | 1567 | case IDM_CXT_SETTINGS: 1568 | return open_settings(hwnd); 1569 | 1570 | default: break; 1571 | } 1572 | return false; 1573 | } 1574 | }; 1575 | 1576 | 1577 | //////////////////////////////// 1578 | // AviUtlに渡す関数の定義. 1579 | //////////////////////////////// 1580 | static inline void on_update(int w, int h, void* source) 1581 | { 1582 | if (source != nullptr && image.update(w, h, source)) 1583 | // notify the loupe of resizing. 1584 | loupe_state.on_resize(w, h); 1585 | } 1586 | static inline void on_command(bool& redraw_loupe, HWND hwnd, Settings::ClickActions::Command cmd, const POINT& pt) 1587 | { 1588 | if (!ext_obj.is_active()) return; 1589 | 1590 | auto rel_win_center = [&] { 1591 | auto [w, h] = BufferedDC::client_size(hwnd); 1592 | return std::make_pair(pt.x - w / 2.0, pt.y - h / 2.0); 1593 | }; 1594 | 1595 | switch (cmd) { 1596 | using ca = Settings::ClickActions; 1597 | case ca::swap_zoom_level: 1598 | { 1599 | auto [x, y] = rel_win_center(); 1600 | redraw_loupe |= swap_zoom_level(x, y, settings.commands.swap_zoom_level_pivot) && tip_to_cursor(hwnd); 1601 | break; 1602 | } 1603 | case ca::copy_color_code: 1604 | { 1605 | auto [x, y] = rel_win_center(); 1606 | redraw_loupe |= copy_color_code(x, y); 1607 | break; 1608 | } 1609 | case ca::copy_coord: 1610 | { 1611 | auto [x, y] = rel_win_center(); 1612 | redraw_loupe |= copy_coordinate(x, y); 1613 | break; 1614 | } 1615 | case ca::toggle_follow_cursor: redraw_loupe |= toggle_follow_cursor(); break; 1616 | case ca::centralize: redraw_loupe |= centralize(); break; 1617 | case ca::toggle_grid: redraw_loupe |= toggle_grid(); break; 1618 | case ca::zoom_step_down: 1619 | case ca::zoom_step_up: 1620 | { 1621 | int steps = settings.commands.step_zoom_num_steps; 1622 | if (cmd == ca::zoom_step_down) steps = -steps; 1623 | double x = 0, y = 0; 1624 | if (settings.commands.step_zoom_pivot != Settings::WheelZoom::center) 1625 | std::tie(x, y) = rel_win_center(); 1626 | redraw_loupe |= apply_zoom(loupe_state.zoom.zoom_level + steps, x, y) && tip_to_cursor(hwnd); 1627 | break; 1628 | } 1629 | case ca::bring_center: 1630 | { 1631 | auto [x, y] = rel_win_center(); 1632 | redraw_loupe |= centralize_point(x, y); 1633 | break; 1634 | } 1635 | 1636 | case ca::settings: 1637 | redraw_loupe |= open_settings(hwnd); 1638 | break; 1639 | case ca::context_menu: 1640 | // should redraw the window before the context menu popups. 1641 | if (redraw_loupe) ::InvalidateRect(hwnd, nullptr, FALSE); 1642 | redraw_loupe = Menu::popup_menu(hwnd, true); 1643 | break; 1644 | } 1645 | } 1646 | 1647 | static BOOL func_proc(FilterPlugin* fp, FilterProcInfo* fpip) 1648 | { 1649 | // updates to the target image. 1650 | if (ext_obj.is_active() && 1651 | fp->exfunc->is_editing(fpip->editp) && !fp->exfunc->is_saving(fpip->editp)) { 1652 | 1653 | on_update(fpip->w, fpip->h, fp->exfunc->get_disp_pixelp(fpip->editp, 0)); 1654 | draw(fp->hwnd); 1655 | } 1656 | return TRUE; 1657 | } 1658 | 1659 | static BOOL func_WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam, EditHandle* editp, FilterPlugin* fp) 1660 | { 1661 | using FilterMessage = FilterPlugin::WindowMessage; 1662 | constexpr auto cursor_pos = [](LPARAM l) { 1663 | return POINT{ .x = static_cast(0xffff & l), .y = static_cast(l >> 16) }; 1664 | }; 1665 | DragState::context cxt{ .editp = editp, .wparam = wparam, .redraw_loupe = false, .redraw_main = false }; 1666 | 1667 | static constinit bool track_mouse_event_sent = false; 1668 | switch (message) { 1669 | case FilterMessage::Init: 1670 | this_dll = fp->dll_hinst; 1671 | 1672 | // load settings. 1673 | load_settings(); 1674 | 1675 | // disable IME. 1676 | ::ImmReleaseContext(hwnd, ::ImmAssociateContext(hwnd, nullptr)); 1677 | 1678 | // find exedit. 1679 | exedit_drag.init(fp); 1680 | 1681 | // activate the toast manager. 1682 | toast_manager.set_host(hwnd); 1683 | break; 1684 | 1685 | case FilterMessage::Exit: 1686 | // deactivate the toast manager. 1687 | toast_manager.set_host(nullptr); 1688 | 1689 | // make sure new allocation would no longer occur. 1690 | ext_obj.deactivate(); 1691 | 1692 | // save settings. 1693 | save_settings(); 1694 | break; 1695 | 1696 | case FilterMessage::ChangeWindow: 1697 | // ルーペウィンドウ表示状態の切り替え. 1698 | if (fp->exfunc->is_filter_window_disp(fp)) { 1699 | ext_obj.activate(); 1700 | 1701 | if (fp->exfunc->is_editing(editp) && !fp->exfunc->is_saving(editp)) 1702 | on_update(editp->w1, editp->h1, fp->exfunc->get_disp_pixelp(editp, 0)); 1703 | cxt.redraw_loupe = true; 1704 | } 1705 | else ext_obj.deactivate(), DragState::Abort(cxt); 1706 | break; 1707 | case FilterMessage::FileClose: 1708 | ext_obj.free(); 1709 | DragState::Abort(cxt); 1710 | 1711 | // clear the window when a file is closed. 1712 | if (::IsWindowVisible(hwnd) != FALSE) 1713 | draw_blank(hwnd); // can't use `cxt.redraw_loupe = true` because exfunc->is_editing() is true at this moment. 1714 | break; 1715 | 1716 | case WM_PAINT: 1717 | cxt.redraw_loupe = true; 1718 | break; 1719 | 1720 | // UI handlers for mouse messages. 1721 | { 1722 | Settings::ClickActions::Button* cfg; 1723 | case WM_LBUTTONDOWN: cfg = &settings.commands.left; goto on_mouse_down; 1724 | case WM_RBUTTONDOWN: cfg = &settings.commands.right; goto on_mouse_down; 1725 | case WM_MBUTTONDOWN: cfg = &settings.commands.middle; goto on_mouse_down; 1726 | case WM_XBUTTONDOWN: 1727 | cfg = (wparam >> 16) == XBUTTON1 ? &settings.commands.x1 : &settings.commands.x2; 1728 | 1729 | on_mouse_down: 1730 | // cancel an existing drag operation if specified so. 1731 | if (cfg->cancels_drag ? !DragState::Cancel(cxt) : !DragState::is_dragging(cxt)) 1732 | // if nothing is canceled, try to initiate a new. 1733 | DragState::InitiateDrag(hwnd, cursor_pos(lparam), cxt); 1734 | break; 1735 | } 1736 | { 1737 | Settings::ClickActions::Button* cfg; 1738 | MouseButton btn; 1739 | case WM_LBUTTONUP: cfg = &settings.commands.left, btn = MouseButton::left; goto on_mouse_up; 1740 | case WM_RBUTTONUP: cfg = &settings.commands.right, btn = MouseButton::right; goto on_mouse_up; 1741 | case WM_MBUTTONUP: cfg = &settings.commands.middle, btn = MouseButton::middle; goto on_mouse_up; 1742 | case WM_XBUTTONUP: 1743 | (wparam >> 16) == XBUTTON1 ? 1744 | (cfg = &settings.commands.x1, btn = MouseButton::x1) : 1745 | (cfg = &settings.commands.x2, btn = MouseButton::x2); 1746 | 1747 | on_mouse_up: 1748 | // finish dragging if button corresponds. 1749 | if (DragState::End(btn, cxt).is_click) 1750 | // now that it's recognized as a single click, handle the assigned command. 1751 | on_command(cxt.redraw_loupe, hwnd, cfg->click, cursor_pos(lparam)); 1752 | break; 1753 | } 1754 | case WM_CAPTURECHANGED: 1755 | // abort dragging. 1756 | DragState::Abort(cxt); 1757 | break; 1758 | 1759 | case WM_MOUSEMOVE: 1760 | if (DragState::Delta(cursor_pos(lparam), cxt)) /* empty */; 1761 | else if (!image.is_valid()) break; 1762 | else if (settings.tip_drag.mode == Settings::TipDrag::sticky && loupe_state.tip.visible_level > 0) { 1763 | // update to the tip of "sticky" mode. 1764 | auto pt = cursor_pos(lparam); 1765 | auto [w, h] = BufferedDC::client_size(hwnd); 1766 | auto [x, y] = loupe_state.win2pic(pt.x - w / 2.0, pt.y - h / 2.0); 1767 | auto X = static_cast(std::floor(x)), Y = static_cast(std::floor(y)); 1768 | 1769 | if (!loupe_state.tip.is_visible() || 1770 | X != loupe_state.tip.x || Y != loupe_state.tip.y) { 1771 | 1772 | loupe_state.tip.visible_level = 2; 1773 | loupe_state.tip.x = X; 1774 | loupe_state.tip.y = Y; 1775 | 1776 | cxt.redraw_loupe = true; 1777 | } 1778 | } 1779 | if (!track_mouse_event_sent && 1780 | settings.tip_drag.mode == Settings::TipDrag::sticky && loupe_state.tip.is_visible()) { 1781 | track_mouse_event_sent = true; 1782 | // to make WM_MOUSELEAVE message be sent. 1783 | TRACKMOUSEEVENT tme{ .cbSize = sizeof(TRACKMOUSEEVENT), .dwFlags = TME_LEAVE, .hwndTrack = hwnd }; 1784 | ::TrackMouseEvent(&tme); 1785 | } 1786 | break; 1787 | case WM_MOUSELEAVE: 1788 | track_mouse_event_sent = false; 1789 | // hide the "sticky" tip. 1790 | if (settings.tip_drag.mode == Settings::TipDrag::sticky && loupe_state.tip.is_visible()) { 1791 | loupe_state.tip.visible_level = 1; 1792 | cxt.redraw_loupe = true; 1793 | } 1794 | break; 1795 | 1796 | case WM_MOUSEWHEEL: 1797 | // wheel to zoom in/out. 1798 | if (ext_obj.is_active()){ 1799 | auto zoom = DragState::CurrentWheelZoom(); 1800 | if (!zoom.enabled) break; 1801 | 1802 | // if dragging, it's no longer recognized as a click. 1803 | DragState::Validate(cxt); 1804 | 1805 | // find the number of steps. 1806 | int delta = zoom.num_steps; 1807 | 1808 | // reverse the wheel if necessary. 1809 | if ((static_cast(wparam >> 16) < 0) ^ zoom.reversed) delta = -delta; 1810 | 1811 | // take the pivot into account. 1812 | double ox = 0, oy = 0; 1813 | if (zoom.pivot != Settings::WheelZoom::center) { 1814 | auto pt = cursor_pos(lparam); 1815 | ::ScreenToClient(hwnd, &pt); 1816 | auto [w, h] = BufferedDC::client_size(hwnd); 1817 | ox = pt.x - w / 2.0; oy = pt.y - h / 2.0; 1818 | } 1819 | 1820 | // then apply the zoom. 1821 | cxt.redraw_loupe |= apply_zoom(loupe_state.zoom.zoom_level + delta, ox, oy) && tip_to_cursor(hwnd); 1822 | } 1823 | break; 1824 | 1825 | // option commands. 1826 | { 1827 | Settings::ClickActions::Button* btn; 1828 | case WM_LBUTTONDBLCLK: btn = &settings.commands.left; goto on_mouse_dblclk; 1829 | case WM_RBUTTONDBLCLK: btn = &settings.commands.right; goto on_mouse_dblclk; 1830 | case WM_MBUTTONDBLCLK: btn = &settings.commands.middle; goto on_mouse_dblclk; 1831 | case WM_XBUTTONDBLCLK: 1832 | btn = (wparam >> 16) == XBUTTON1 ? &settings.commands.x1 : &settings.commands.x2; 1833 | 1834 | on_mouse_dblclk: 1835 | on_command(cxt.redraw_loupe, hwnd, btn->dblclk, cursor_pos(lparam)); 1836 | break; 1837 | } 1838 | 1839 | case FilterMessage::MainMouseMove: 1840 | // mouse move on the main window while wheel is down moves the loupe position. 1841 | if (image.is_valid() && 1842 | (loupe_state.position.follow_cursor || (wparam & MK_MBUTTON) != 0)) { 1843 | auto pt = cursor_pos(lparam); 1844 | loupe_state.position.x = 0.5 + static_cast(pt.x); 1845 | loupe_state.position.y = 0.5 + static_cast(pt.y); 1846 | loupe_state.position.clamp(image.width(), image.height()); 1847 | 1848 | cxt.redraw_loupe = ::IsWindowVisible(hwnd) != FALSE; 1849 | } 1850 | break; 1851 | 1852 | case WM_COMMAND: 1853 | // menu commands. 1854 | if ((wparam >> 16) == 0 && lparam == 0) // criteria for the menu commands. 1855 | cxt.redraw_loupe = Menu::on_menu_command(hwnd, wparam & 0xffff, false); 1856 | break; 1857 | 1858 | case WM_KEYDOWN: 1859 | case WM_SYSKEYDOWN: 1860 | if (wparam == VK_ESCAPE) { 1861 | // if a drag operation is being held, cancel it with ESC key. 1862 | if (DragState::Cancel(cxt)) break; 1863 | } 1864 | else if (wparam == VK_F10 && (lparam & KF_ALTDOWN) == 0 && message == WM_SYSKEYDOWN 1865 | && ::GetKeyState(VK_SHIFT) < 0 && ::GetKeyState(VK_CONTROL) >= 0 1866 | && !DragState::is_dragging(cxt)) { 1867 | // Shift + F10: 1868 | // hard-coded shortcut key that shows up the context menu. 1869 | cxt.redraw_loupe = Menu::popup_menu(hwnd, false); 1870 | break; 1871 | } 1872 | [[fallthrough]]; 1873 | case WM_KEYUP: 1874 | case WM_SYSKEYUP: 1875 | // ショートカットキーメッセージをメインウィンドウに丸投げする. 1876 | if (fp->hwnd_parent != nullptr) 1877 | ::SendMessageW(fp->hwnd_parent, message, wparam, lparam); 1878 | break; 1879 | default: 1880 | break; 1881 | } 1882 | 1883 | if (cxt.redraw_loupe) { 1884 | // when redrawing is required, proccess here. 1885 | // returning TRUE from this function may cause flickering in the main window. 1886 | if (image.is_valid() && 1887 | fp->exfunc->is_editing(editp) && !fp->exfunc->is_saving(editp)) draw(hwnd); 1888 | else draw_blank(hwnd); 1889 | } 1890 | 1891 | return cxt.redraw_main ? TRUE : FALSE; 1892 | } 1893 | 1894 | 1895 | //////////////////////////////// 1896 | // Entry point. 1897 | //////////////////////////////// 1898 | BOOL WINAPI DllMain(HINSTANCE hinst, DWORD fdwReason, LPVOID lpvReserved) 1899 | { 1900 | switch (fdwReason) { 1901 | case DLL_PROCESS_ATTACH: 1902 | ::DisableThreadLibraryCalls(hinst); 1903 | break; 1904 | } 1905 | return TRUE; 1906 | } 1907 | 1908 | 1909 | //////////////////////////////// 1910 | // 看板. 1911 | //////////////////////////////// 1912 | #define PLUGIN_NAME "色ルーペ" 1913 | #define PLUGIN_VERSION "v2.23" 1914 | #define PLUGIN_AUTHOR "sigma-axis" 1915 | #define PLUGIN_INFO_FMT(name, ver, author) (name##" "##ver##" by "##author) 1916 | #define PLUGIN_INFO PLUGIN_INFO_FMT(PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_AUTHOR) 1917 | 1918 | extern "C" __declspec(dllexport) FilterPluginDLL* __stdcall GetFilterTable(void) 1919 | { 1920 | constexpr int initial_width = 256, initial_height = 256; 1921 | 1922 | // (フィルタとは名ばかりの)看板. 1923 | using Flag = FilterPlugin::Flag; 1924 | static constinit FilterPluginDLL filter{ 1925 | .flag = Flag::DispFilter | Flag::AlwaysActive | Flag::ExInformation | 1926 | Flag::MainMessage | Flag::NoInitData | 1927 | Flag::WindowThickFrame | Flag::WindowSize | 1928 | Flag::PriorityLowest, 1929 | .x = initial_width, .y = initial_height, 1930 | .name = PLUGIN_NAME, 1931 | 1932 | .func_proc = func_proc, 1933 | .func_WndProc = func_WndProc, 1934 | .information = PLUGIN_INFO, 1935 | }; 1936 | return &filter; 1937 | } 1938 | -------------------------------------------------------------------------------- /color_loupe.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | GetFilterTable @1 3 | -------------------------------------------------------------------------------- /color_loupe.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma-axis/aviutl_color_loupe/c51405a1224383fd13ae3215d80ef7de69ecb311/color_loupe.rc -------------------------------------------------------------------------------- /color_loupe.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34322.80 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "color_loupe", "color_loupe.vcxproj", "{576869FA-99DD-4D78-9DFF-4E47045C181F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {576869FA-99DD-4D78-9DFF-4E47045C181F}.Debug|x86.ActiveCfg = Debug|Win32 15 | {576869FA-99DD-4D78-9DFF-4E47045C181F}.Debug|x86.Build.0 = Debug|Win32 16 | {576869FA-99DD-4D78-9DFF-4E47045C181F}.Release|x86.ActiveCfg = Release|Win32 17 | {576869FA-99DD-4D78-9DFF-4E47045C181F}.Release|x86.Build.0 = Release|Win32 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {B0A1E144-E8F1-48F6-A014-8F4DEFE53587} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /color_loupe.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | 14 | 17.0 15 | Win32Proj 16 | {576869fa-99dd-4d78-9dff-4e47045c181f} 17 | colorloupe 18 | 10.0 19 | 20 | 21 | 22 | DynamicLibrary 23 | true 24 | v143 25 | MultiByte 26 | 27 | 28 | DynamicLibrary 29 | false 30 | v143 31 | true 32 | MultiByte 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | $(Configuration)\intermed\ 48 | .auf 49 | false 50 | 51 | 52 | $(Configuration)\intermed\ 53 | .auf 54 | false 55 | 56 | 57 | 58 | Level3 59 | true 60 | WIN32;_DEBUG;COLORLOUPE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 61 | true 62 | stdcpplatest 63 | true 64 | sdk/ 65 | /source-charset:utf-8 /execution-charset:shift_jis %(AdditionalOptions) 66 | 67 | 68 | Windows 69 | true 70 | false 71 | color_loupe.def 72 | 73 | 74 | 75 | 76 | true 77 | 78 | 79 | 80 | 81 | Level3 82 | true 83 | true 84 | true 85 | WIN32;NDEBUG;COLORLOUPE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 86 | true 87 | stdcpplatest 88 | true 89 | sdk/ 90 | true 91 | /source-charset:utf-8 /execution-charset:shift_jis %(AdditionalOptions) 92 | false 93 | 94 | 95 | Windows 96 | true 97 | true 98 | false 99 | false 100 | color_loupe.def 101 | UseLinkTimeCodeGeneration 102 | 103 | 104 | 105 | 106 | true 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /color_loupe.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | {6df0aff4-851b-4b8d-8f63-9e749510327e} 18 | 19 | 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | 29 | 30 | Source Files 31 | 32 | 33 | assets 34 | 35 | 36 | 37 | 38 | Header Files 39 | 40 | 41 | Header Files 42 | 43 | 44 | Header Files 45 | 46 | 47 | Header Files 48 | 49 | 50 | Header Files 51 | 52 | 53 | Header Files 54 | 55 | 56 | Header Files 57 | 58 | 59 | Header Files 60 | 61 | 62 | Header Files 63 | 64 | 65 | 66 | 67 | Resource Files 68 | 69 | 70 | -------------------------------------------------------------------------------- /dialogs.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #pragma once 14 | 15 | #define NOMINMAX 16 | #define WIN32_LEAN_AND_MEAN 17 | #include 18 | 19 | #include "settings.hpp" 20 | 21 | namespace dialogs 22 | { 23 | // opens a dialog for settings of this app. 24 | bool open_settings(HWND parent); 25 | 26 | // external functions that are helpful for the dialog functionality. 27 | namespace ExtFunc 28 | { 29 | using namespace sigma_lib::W32::GDI; 30 | 31 | std::tuple ScaleFromZoomLevel(int zoom_level); 32 | void DrawTip(HDC hdc, const SIZE& canvas, const RECT& box, 33 | Color pixel_color, const POINT& pix, const SIZE& screen, bool& prefer_above, 34 | HFONT font, const Settings::TipDrag& tip_drag, const Settings::ColorScheme& color_scheme); 35 | void DrawToast(HDC hdc, const SIZE& canvas, const wchar_t* message, 36 | HFONT font, const Settings::Toast& toast, const Settings::ColorScheme& color_scheme); 37 | HFONT CreateUprightFont(wchar_t const* name, int size); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /dialogs_basics.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #define NOMINMAX 20 | #define WIN32_LEAN_AND_MEAN 21 | #include 22 | 23 | #include "resource.hpp" 24 | 25 | namespace dialogs::basics 26 | { 27 | //////////////////////////////// 28 | // Base for dialog classes. 29 | //////////////////////////////// 30 | class dialog_base { 31 | struct create_context { 32 | dialog_base* that; 33 | RECT const* position; 34 | }; 35 | static intptr_t CALLBACK proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) 36 | { 37 | intptr_t ret; 38 | if (message == WM_INITDIALOG) { 39 | auto cxt = reinterpret_cast(lparam); 40 | cxt->that->hwnd = hwnd; 41 | ::SetWindowLongW(hwnd, GWL_USERDATA, reinterpret_cast(cxt->that)); 42 | ret = cxt->that->on_init(reinterpret_cast(wparam)) ? 1 : 0; 43 | if (cxt->position != nullptr) { 44 | auto& rc = *cxt->position; 45 | ::MoveWindow(hwnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE); 46 | } 47 | } 48 | else { 49 | auto that = reinterpret_cast(::GetWindowLongW(hwnd, GWL_USERDATA)); 50 | if (that == nullptr || that->no_callback || that->hwnd != hwnd) return FALSE; 51 | 52 | ret = that->handler(message, wparam, lparam); 53 | if (message == WM_NCDESTROY) that->hwnd = nullptr; 54 | } 55 | 56 | return ret; 57 | } 58 | bool no_callback = false; 59 | 60 | protected: 61 | virtual uintptr_t template_id() const = 0; 62 | virtual bool on_init(HWND def_control) = 0; 63 | virtual intptr_t handler(UINT message, WPARAM wparam, LPARAM lparam) = 0; 64 | 65 | const auto suppress_callback() { 66 | struct backyard { 67 | backyard(dialog_base& host) 68 | : ptr{ host.no_callback ? nullptr : &host.no_callback } 69 | { 70 | if (ptr != nullptr) *ptr = true; 71 | } 72 | ~backyard() { 73 | if (ptr != nullptr) *ptr = false; 74 | } 75 | private: 76 | bool* const ptr; 77 | }; 78 | return backyard{ *this }; 79 | } 80 | 81 | static bool clamp_on_sizing(WPARAM direction, LPARAM prect, 82 | const SIZE& sz_min, const SIZE& sz_max = { 0x7fff, 0x7fff }) 83 | { 84 | auto& rc = *reinterpret_cast(prect); 85 | bool ret = false; 86 | 87 | // clamp horizontally. 88 | if (int w = rc.right - rc.left, 89 | overshoot = std::min(w - sz_min.cx, 0) + std::max(w - sz_max.cx, 0); 90 | overshoot != 0) { 91 | switch (direction) { 92 | case WMSZ_LEFT: 93 | case WMSZ_TOPLEFT: 94 | case WMSZ_BOTTOMLEFT: 95 | rc.left += overshoot; 96 | break; 97 | default: 98 | rc.right -= overshoot; 99 | break; 100 | } 101 | ret = true; 102 | } 103 | 104 | // clamp vertically. 105 | if (int h = rc.bottom - rc.top, 106 | overshoot = std::min(h - sz_min.cy, 0) + std::max(h - sz_max.cy, 0); 107 | overshoot != 0) { 108 | switch (direction) { 109 | case WMSZ_TOP: 110 | case WMSZ_TOPLEFT: 111 | case WMSZ_TOPRIGHT: 112 | rc.top += overshoot; 113 | break; 114 | default: 115 | rc.bottom -= overshoot; 116 | break; 117 | } 118 | ret = true; 119 | } 120 | return ret; 121 | } 122 | 123 | public: 124 | HWND hwnd = nullptr; 125 | void create(HWND parent, const RECT& position = *static_cast(nullptr)) { 126 | if (hwnd != nullptr) return; 127 | 128 | create_context cxt{ .that = this, .position = &position }; 129 | ::CreateDialogParamW(this_dll, MAKEINTRESOURCEW(template_id()), 130 | parent, proc, reinterpret_cast(&cxt)); 131 | } 132 | intptr_t modal(HWND parent, const RECT& position = *static_cast(nullptr)) { 133 | if (hwnd != nullptr) return 0; 134 | 135 | // find the top-level window containing `parent`, 136 | // for the cases like SplitWindow or Nest is applied. 137 | parent = ::GetAncestor(parent, GA_ROOT); 138 | 139 | create_context cxt{ .that = this, .position = &position }; 140 | return ::DialogBoxParamW(this_dll, MAKEINTRESOURCEW(template_id()), 141 | parent, proc, reinterpret_cast(&cxt)); 142 | } 143 | 144 | dialog_base() = default; 145 | dialog_base(const dialog_base&) = delete; 146 | dialog_base(dialog_base&&) = delete; 147 | virtual ~dialog_base() { 148 | if (hwnd != nullptr) ::DestroyWindow(hwnd); 149 | } 150 | }; 151 | 152 | 153 | //////////////////////////////// 154 | // Scroll Viewer. 155 | //////////////////////////////// 156 | class vscroll_form : public dialog_base { 157 | int prev_scroll_pos = -1; 158 | void on_scroll(int pos, bool force = false) { 159 | auto diff = pos - prev_scroll_pos; 160 | if (!force && diff == 0) return; 161 | prev_scroll_pos = pos; 162 | 163 | // update positions of children. 164 | // the order is reversed when diff < 0 to prevent broken drawings. 165 | auto begin = children.begin(), end = children.end(); 166 | if (force || diff > 0) diff = +1; 167 | else diff = -1, std::swap(--end, --begin); 168 | for (auto child = begin; child != end; child += diff) 169 | ::MoveWindow(child->inst->hwnd, child->x, child->y - pos, child->w, child->h, TRUE); 170 | } 171 | 172 | void on_resize(int w, int h) 173 | { 174 | auto const sw = ::GetSystemMetrics(SM_CXVSCROLL); 175 | 176 | // calculate the layout. 177 | auto max_w = w - sw; 178 | for (auto& child : children) 179 | max_w = std::max(max_w, child.w); 180 | 181 | int tot_w = 0, tot_h = 0, max_h = 0; 182 | for (auto& child : children) { 183 | auto W = child.w, H = child.h; 184 | if (tot_w + W > max_w) { 185 | tot_w = 0; 186 | tot_h += max_h; 187 | max_h = 0; 188 | } 189 | child.x = tot_w; 190 | child.y = tot_h; 191 | 192 | tot_w += W; 193 | max_h = std::max(max_h, H); 194 | } 195 | 196 | // set the scroll range and redraw. 197 | SCROLLINFO si{ .cbSize = sizeof(si), .fMask = SIF_RANGE | SIF_PAGE }; 198 | si.nMin = 0; si.nMax = tot_h + max_h; 199 | si.nPage = h; 200 | on_scroll(::SetScrollInfo(hwnd, SB_VERT, &si, TRUE), true); 201 | } 202 | 203 | protected: 204 | uintptr_t template_id() const override { return IDD_VSCROLLFORM; } 205 | 206 | bool on_init(HWND) override 207 | { 208 | for (auto& child : children) { 209 | child.inst->create(hwnd); 210 | RECT rc; 211 | ::GetClientRect(child.inst->hwnd, &rc); 212 | child.w = rc.right; child.h = rc.bottom; 213 | } 214 | 215 | // no default control to focus. 216 | return false; 217 | } 218 | 219 | intptr_t handler(UINT message, WPARAM wparam, LPARAM lparam) override 220 | { 221 | // TODO: scroll to the focused element when a descendant control got a focus. 222 | switch (message) { 223 | case WM_SIZE: 224 | on_resize(static_cast(lparam), static_cast(lparam >> 16)); 225 | return true; 226 | case WM_VSCROLL: 227 | { 228 | SCROLLINFO si = get_scroll_info(SIF_POS | SIF_RANGE | SIF_PAGE); 229 | auto code = 0xffff & wparam; 230 | switch (code) { 231 | case SB_THUMBPOSITION: 232 | case SB_THUMBTRACK: 233 | si.nPos = static_cast(wparam >> 16); 234 | break; 235 | 236 | case SB_LINEUP: si.nPos -= scroll_delta; break; 237 | case SB_LINEDOWN: si.nPos += scroll_delta; break; 238 | case SB_PAGEUP: si.nPos -= si.nPage; break; 239 | case SB_PAGEDOWN: si.nPos += si.nPage; break; 240 | case SB_TOP: si.nPos = si.nMin; break; 241 | case SB_BOTTOM: si.nPos = si.nMax; break; 242 | default: return false; // includes SB_ENDSCROLL. 243 | } 244 | si.nPos = std::clamp(si.nPos, si.nMin, std::max(0, si.nMax - si.nPage)); 245 | if (code != SB_THUMBTRACK) { 246 | si.fMask = SIF_POS; 247 | si.nPos = ::SetScrollInfo(hwnd, SB_VERT, &si, TRUE); 248 | } 249 | on_scroll(si.nPos); 250 | return true; 251 | } 252 | case WM_MOUSEWHEEL: 253 | { 254 | auto wheel_delta = static_cast(wparam >> 16); 255 | set_scroll_pos(get_scroll_pos() - scroll_wheel * wheel_delta / WHEEL_DELTA); 256 | return true; 257 | } 258 | } 259 | return false; 260 | } 261 | public: 262 | int scroll_delta = 8, scroll_wheel = 32; 263 | 264 | private: 265 | struct child { 266 | std::unique_ptr inst; 267 | int x, y, w, h; 268 | child(dialog_base* ptr) : inst{ ptr } { x = y = w = h = 0; } 269 | }; 270 | std::vector children; 271 | 272 | public: 273 | vscroll_form(std::initializer_list children) 274 | { 275 | this->children.reserve(children.size()); 276 | for (auto* child : children) this->children.emplace_back(child); 277 | } 278 | 279 | bool get_scroll_info(SCROLLINFO& si) { return ::GetScrollInfo(hwnd, SB_VERT, &si) != FALSE; } 280 | SCROLLINFO get_scroll_info(UINT flags = SIF_ALL) { 281 | SCROLLINFO si{ .cbSize = sizeof(si), .fMask = flags }; 282 | get_scroll_info(si); 283 | return si; 284 | } 285 | int get_scroll_pos() { return get_scroll_info(SIF_POS).nPos; } 286 | int set_scroll_pos(int pos) { 287 | SCROLLINFO si{ .cbSize = sizeof(si), .fMask = SIF_POS, .nPos = pos }; 288 | int ret = ::SetScrollInfo(hwnd, SB_VERT, &si, TRUE); 289 | on_scroll(ret); 290 | return ret; 291 | } 292 | }; 293 | } 294 | -------------------------------------------------------------------------------- /drag_states.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #define NOMINMAX 20 | #define WIN32_LEAN_AND_MEAN 21 | #include 22 | 23 | //////////////////////////////// 24 | // ドラッグ操作の基底クラス. 25 | //////////////////////////////// 26 | namespace sigma_lib::W32::custom::mouse 27 | { 28 | enum class MouseButton : uint8_t { 29 | none = 0, 30 | left = 1, 31 | right = 2, 32 | middle = 3, 33 | x1 = 4, 34 | x2 = 5, 35 | }; 36 | enum class RailMode : uint8_t { 37 | none = 0, cross = 1, octagonal = 2, 38 | }; 39 | struct DragInvalidRange { 40 | // distance of locations where the mouse button was 41 | // pressed and released, measured in pixels. 42 | int16_t distance; 43 | // time taken from the mouse button was pressed 44 | // until it's released, measured in milli seconds. 45 | int16_t timespan; 46 | 47 | constexpr bool is_distance_valid(int dist) const { 48 | return cmp_d(dist, distance); 49 | } 50 | constexpr bool is_distance_valid(int dx, int dy) const { 51 | return cmp_d(dx, distance) || cmp_d(dy, distance); 52 | } 53 | constexpr bool distance_always_invalid() const { return distance == invalid_val; } 54 | 55 | constexpr bool is_timespan_valid(int time) const { 56 | return cmp_t(time, timespan); 57 | } 58 | constexpr bool timespan_always_invalid() const { return timespan == invalid_val; } 59 | 60 | constexpr bool is_valid(int dist, int time) const { 61 | return is_distance_valid(dist) || is_timespan_valid(time); 62 | } 63 | constexpr bool is_valid(int dx, int dy, int time) const { 64 | return is_distance_valid(dx, dy) || is_timespan_valid(time); 65 | } 66 | constexpr bool is_always_invalid() const { return distance_always_invalid() && timespan_always_invalid(); } 67 | 68 | consteval static DragInvalidRange AlwaysValid() { return { -1, 0 }; } 69 | consteval static DragInvalidRange AlwaysInvalid() { return { invalid_val, invalid_val }; } 70 | 71 | private: 72 | constexpr static int16_t invalid_val = 0x7fff; 73 | constexpr static bool cmp_d(int val, int16_t threshold) { 74 | return threshold != invalid_val && (val < 0 ? -val : val) > threshold; 75 | } 76 | constexpr static bool cmp_t(int val, int16_t threshold) { 77 | return threshold != invalid_val && val > threshold; 78 | } 79 | 80 | public: 81 | constexpr static int16_t distance_min = -1, distance_max = 64; 82 | constexpr static int16_t timespan_min = 0, timespan_max = invalid_val; 83 | }; 84 | 85 | template 86 | class DragStateBase { 87 | inline static constinit DragStateBase* current = nullptr; 88 | 89 | static inline constinit auto invalid_range = DragInvalidRange::AlwaysInvalid(); 90 | static inline bool was_validated = false; 91 | static bool is_distance_valid(const POINT& curr) { 92 | return invalid_range.is_distance_valid( 93 | curr.x - drag_start.x, curr.y - drag_start.y); 94 | } 95 | 96 | static inline uintptr_t timer_id = 0; 97 | static void CALLBACK timer_proc(HWND hwnd, auto, uintptr_t id, auto) 98 | { 99 | // turn the timer off. 100 | ::KillTimer(hwnd, id); 101 | 102 | if (hwnd != nullptr || id == 0 || id != timer_id) return; // might be a wrong call. 103 | timer_id = 0; // the timer is no longer alive. 104 | 105 | if (DragStateBase::hwnd == nullptr || current == nullptr) return; // wrong states. ignore. 106 | 107 | // prepare and send a "fake" message so the drag will turn into the "validated" state. 108 | constexpr auto make_lparam = [] { 109 | POINT pt; 110 | ::GetCursorPos(&pt); ::ScreenToClient(DragStateBase::hwnd, &pt); 111 | return static_cast((0xffff & pt.x) | (pt.y << 16)); 112 | }; 113 | constexpr auto make_wparam = [] { 114 | int8_t ks[256]; 115 | // one of the flaws in windows.h. 116 | std::ignore = ::GetKeyboardState(reinterpret_cast(ks)); 117 | WPARAM ret = 0; 118 | if (ks[VK_LBUTTON] < 0) ret |= MK_LBUTTON; 119 | if (ks[VK_RBUTTON] < 0) ret |= MK_RBUTTON; 120 | if (ks[VK_MBUTTON] < 0) ret |= MK_MBUTTON; 121 | if (ks[VK_XBUTTON1] < 0) ret |= MK_XBUTTON1; 122 | if (ks[VK_XBUTTON2] < 0) ret |= MK_XBUTTON2; 123 | if (ks[VK_CONTROL] < 0) ret |= MK_CONTROL; 124 | if (ks[VK_SHIFT] < 0) ret |= MK_SHIFT; 125 | return ret; 126 | }; 127 | // make sure to be recognized as "valid". 128 | invalid_range = DragInvalidRange::AlwaysValid(); 129 | ::PostMessageW(DragStateBase::hwnd, WM_MOUSEMOVE, make_wparam(), make_lparam()); 130 | } 131 | static void set_timer(int time_ms) { 132 | if (time_ms < USER_TIMER_MINIMUM) time_ms = USER_TIMER_MINIMUM; 133 | timer_id = ::SetTimer(nullptr, timer_id, time_ms, timer_proc); 134 | } 135 | static void kill_timer() { 136 | if (timer_id != 0) { 137 | ::KillTimer(nullptr, timer_id); 138 | timer_id = 0; 139 | } 140 | } 141 | 142 | public: 143 | using context = Context; 144 | 145 | protected: 146 | static inline constinit HWND hwnd{ nullptr }; 147 | static inline constinit POINT drag_start{}, last_point{}; // window coordinates. 148 | static inline constinit MouseButton button = MouseButton::none; 149 | 150 | // should return true if ready to initiate the drag. 151 | virtual bool Ready_core(context& cxt) = 0; 152 | virtual void Unready_core(context& cxt, bool was_valid) {} 153 | 154 | virtual void Start_core(context& cxt) {}; 155 | virtual void Delta_core(const POINT& curr, context& cxt) {} 156 | virtual void End_core(context& cxt) {} 157 | virtual void Cancel_core(context& cxt) {} 158 | virtual void Abort_core(context& cxt) {} 159 | 160 | // determines whether a short drag should be recognized as a single click. 161 | virtual DragInvalidRange InvalidRange() { return DragInvalidRange::AlwaysValid(); } 162 | 163 | // helper function for the "directional snapping" feature. 164 | static constexpr std::pair snap_rail(double x, double y, RailMode mode, bool lattice) { 165 | double lx = x < 0 ? -x : x, ly = y < 0 ? -y : y; //double lx = std::abs(x), ly = std::abs(y); 166 | // std::abs() isn't constexpr at this moment of writing this code... 167 | switch (mode) { 168 | case RailMode::cross: 169 | // '+'-shape. 170 | (lx < ly ? x : y) = 0; 171 | break; 172 | case RailMode::octagonal: 173 | { 174 | // combination of '+' and 'X'-shape. 175 | constexpr double thr = 0.41421356237309504880168872420969807857; // tan(pi/8) 176 | // std::tan() isn't constexpr at this moment of writing this code... 177 | if (lx < thr * ly) x = 0; 178 | else if (thr * lx > ly) y = 0; 179 | else { 180 | lx = (lx + ly) / 2; 181 | x = x < 0 ? -lx : lx; 182 | y = y < 0 ? -lx : lx; 183 | } 184 | break; 185 | } 186 | case RailMode::none: 187 | default: 188 | break; 189 | } 190 | if (lattice) { x = std::round(x); y = std::round(y); } 191 | return { x, y }; 192 | } 193 | 194 | public: 195 | // returns true if the dragging is made ready. button must not be MouseButton::none. 196 | bool Start(HWND hwnd, MouseButton button, const POINT& drag_start, context& cxt) 197 | { 198 | if (is_dragging(cxt)) return false; 199 | 200 | DragStateBase::hwnd = hwnd; 201 | DragStateBase::drag_start = last_point = drag_start; 202 | DragStateBase::button = button; 203 | 204 | if (Ready_core(cxt)) { 205 | invalid_range = InvalidRange(); 206 | was_validated = false; 207 | 208 | current = this; 209 | ::SetCapture(hwnd); 210 | 211 | // instantly start drag operation if the range insisits so. 212 | if (invalid_range.is_valid(0, 0)) { 213 | was_validated = true; 214 | current->Start_core(cxt); 215 | } 216 | else if (!invalid_range.timespan_always_invalid()) 217 | set_timer(invalid_range.timespan); 218 | return true; 219 | } 220 | else { 221 | DragStateBase::hwnd = nullptr; 222 | DragStateBase::button = MouseButton::none; 223 | return false; 224 | } 225 | } 226 | static void Validate(context& cxt) 227 | { 228 | if (was_validated || !is_dragging(cxt)) return; 229 | 230 | was_validated = true; 231 | kill_timer(); 232 | current->Start_core(cxt); 233 | } 234 | // returns true if the dragging operation has been properly processed. 235 | static bool Delta(const POINT& curr, context& cxt) 236 | { 237 | if (!is_dragging(cxt)) return false; 238 | 239 | // ignore the input until it exceeds a certain range. 240 | if (!was_validated && is_distance_valid(curr)) { 241 | was_validated = true; 242 | kill_timer(); 243 | current->Start_core(cxt); 244 | } 245 | if (was_validated) { 246 | current->Delta_core(curr, cxt); 247 | last_point = curr; 248 | } 249 | return true; 250 | } 251 | // returns true if the dragging operation has been properly canceled. 252 | static bool Cancel(context& cxt) 253 | { 254 | if (!is_dragging(cxt)) return false; 255 | 256 | kill_timer(); 257 | if (was_validated) current->Cancel_core(cxt); 258 | current->Unready_core(cxt, was_validated); 259 | 260 | hwnd = nullptr; 261 | button = MouseButton::none; 262 | current = nullptr; 263 | 264 | ::ReleaseCapture(); 265 | return true; 266 | } 267 | // returns whether the button-up should be recognized as a single click. 268 | // actually ends dragging only if the button used for dragging matches up_button. 269 | static struct { bool is_click; } End(MouseButton up_button, context& cxt) 270 | { 271 | if (!is_dragging(cxt)) return { false }; // dragging must have been canceled. 272 | if (up_button != button) return { true }; // different button, maybe a single click. 273 | 274 | kill_timer(); 275 | if (was_validated) current->End_core(cxt); 276 | current->Unready_core(cxt, was_validated); 277 | 278 | hwnd = nullptr; 279 | button = MouseButton::none; 280 | current = nullptr; 281 | 282 | ::ReleaseCapture(); 283 | return { !was_validated }; 284 | } 285 | // returns true if there sure was a dragging state that is now aborted. 286 | static bool Abort(context& cxt) 287 | { 288 | if (current == nullptr) return false; 289 | 290 | kill_timer(); 291 | if (was_validated) current->Abort_core(cxt); 292 | current->Unready_core(cxt, was_validated); 293 | 294 | auto tmp = hwnd; 295 | hwnd = nullptr; 296 | button = MouseButton::none; 297 | current = nullptr; 298 | 299 | if (tmp != nullptr && ::GetCapture() == tmp) 300 | ::ReleaseCapture(); 301 | return true; 302 | } 303 | // verifies the dragging status, might abort if the window is no longer captured. 304 | static bool is_dragging(context& cxt) { 305 | if (current != nullptr) { 306 | if (hwnd != nullptr && 307 | hwnd == ::GetCapture()) return true; 308 | 309 | Abort(cxt); 310 | } 311 | return false; 312 | } 313 | 314 | template DragState = DragStateBase> 315 | static DragState* current_drag() { return dynamic_cast(current); } 316 | 317 | // make sure derived classes finalize their fields. 318 | virtual ~DragStateBase() {} 319 | }; 320 | 321 | template 322 | requires(std::derived_from>) 323 | class VoidDrag : public DragBase { 324 | using context = DragBase::context; 325 | protected: 326 | bool Ready_core(context& cxt) override { return true; } 327 | void Unready_core(context& cxt, bool was_valid) override {} 328 | 329 | void Start_core(context& cxt) override {} 330 | void Delta_core(const POINT& curr, context& cxt) override {} 331 | void End_core(context& cxt) override {} 332 | void Cancel_core(context& cxt) override {} 333 | void Abort_core(context& cxt) override {} 334 | 335 | DragInvalidRange InvalidRange() override { return DragInvalidRange::AlwaysInvalid(); } 336 | }; 337 | } 338 | -------------------------------------------------------------------------------- /key_states.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | #include 17 | 18 | #define NOMINMAX 19 | #define WIN32_LEAN_AND_MEAN 20 | #include 21 | 22 | //////////////////////////////// 23 | // Windows API 利用の補助関数. 24 | //////////////////////////////// 25 | namespace sigma_lib::W32::UI 26 | { 27 | enum class flag_map : uint8_t { 28 | id = 0, 29 | off = 1, 30 | on = 2, 31 | inv = 3, 32 | }; 33 | 34 | // キー入力認識をちょろまかす補助クラス. 35 | template 36 | class ForceKeyState { 37 | template 38 | using arg_at = std::decay_t>>; 39 | 40 | template 41 | static consteval bool check_requirements() { 42 | constexpr auto check_arg = []() { 43 | return std::convertible_to, int>; 44 | }; 45 | constexpr auto check_result = []() { 46 | using result = arg_at; 47 | return std::same_as 48 | || std::same_as 49 | || std::convertible_to; 50 | }; 51 | 52 | if (!([](std::index_sequence) { 53 | if constexpr (!(check_arg.operator() < 2 * I > () && ...)) return false; 54 | if constexpr (!(check_result.operator() < 2 * I + 1 > () && ...)) return false; 55 | return true; 56 | }(std::make_index_sequence{}))) return false; 57 | 58 | return true; 59 | } 60 | 61 | constexpr static size_t num_keys = 256; 62 | uint8_t key[N], rew[N]; 63 | 64 | static constexpr uint8_t coerce_key(int val) { 65 | if (val < 0 || val >= num_keys) return 0; 66 | return static_cast(val); 67 | } 68 | struct modifier { 69 | uint8_t a, b; 70 | constexpr modifier(bool mod) : modifier{ mod ? flag_map::on : flag_map::off } {} 71 | constexpr modifier(flag_map mod) { 72 | using enum flag_map; 73 | switch (mod) { 74 | case off: a = 0x80, b = 0x80; break; 75 | case on: a = 0x80, b = 0x00; break; 76 | case inv: a = 0x00, b = 0x80; break; 77 | case id: default: a = b = 0x00; break; 78 | } 79 | } 80 | constexpr modifier(uint8_t mod) : a{ 0xff }, b{ ~mod } {} 81 | 82 | constexpr uint8_t operator()(uint8_t& state) const { 83 | auto ret = state; 84 | state = (state | a) ^ b; 85 | return ret; 86 | } 87 | constexpr operator bool() const { return a != 0 || b != 0; } 88 | }; 89 | 90 | template 91 | ForceKeyState(std::index_sequence, const auto&& data) noexcept 92 | : key{ coerce_key(std::get<2 * I>(data))... } 93 | { 94 | modifier m[]{ { std::get<2 * I + 1>(data) }... }; 95 | if (((key[I] > 0 && m) || ...)) { 96 | // overwrite the state. 97 | uint8_t state[num_keys]; std::ignore = ::GetKeyboardState(state); 98 | ((key[I] > 0 && m ? (rew[I] = m[I](state[key[I]])) : (key[I] = 0)), ...); 99 | ::SetKeyboardState(state); 100 | } 101 | } 102 | 103 | public: 104 | template 105 | requires(sizeof...(TArgs) == 2 * N && check_requirements()) 106 | ForceKeyState(TArgs const... args) noexcept 107 | : ForceKeyState{ std::make_index_sequence{}, std::make_tuple(args...) } {} 108 | 109 | ForceKeyState(const ForceKeyState&) = delete; 110 | ForceKeyState(ForceKeyState&&) = delete; 111 | 112 | ~ForceKeyState() noexcept { 113 | size_t i = 0; 114 | for (; i < N; i++) { if (key[i] > 0) goto rewind; } 115 | return; 116 | 117 | rewind: 118 | // rewind the state. 119 | uint8_t state[num_keys]; std::ignore = ::GetKeyboardState(state); 120 | do { 121 | if (key[i] > 0) state[key[i]] = rew[i]; 122 | } while (++i < N); 123 | ::SetKeyboardState(state); 124 | } 125 | }; 126 | template 127 | ForceKeyState(TArgs...)->ForceKeyState; 128 | template<> struct ForceKeyState<0> {}; 129 | } 130 | -------------------------------------------------------------------------------- /resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by color_loupe.rc 4 | // 5 | #define IDR_MENU_CXT 101 6 | #define IDS_TOAST_SCALE_FORMAT 102 7 | #define IDS_TOAST_FOLLOW_CURSOR_ON 103 8 | #define IDS_TOAST_FOLLOW_CURSOR_OFF 104 9 | #define IDS_TOAST_CLIPBOARD 105 10 | #define IDS_TOAST_GRID_ON 106 11 | #define IDS_TOAST_GRID_OFF 107 12 | #define IDS_CMD_NONE 108 13 | #define IDS_CMD_SWAP_ZOOM 109 14 | #define IDS_CMD_COPY_COLOR 110 15 | #define IDS_CMD_COPY_COORD 111 16 | #define IDS_CMD_FOLLOW_CURSOR 112 17 | #define IDS_CMD_CENTRALIZE 113 18 | #define IDS_CMD_TOGGLE_GRID 114 19 | #define IDS_CMD_ZOOM_STEP_DOWN 115 20 | #define IDS_CMD_ZOOM_STEP_UP 116 21 | #define IDS_CMD_BRING_CENTER 117 22 | #define IDS_CMD_CXT_MENU 118 23 | #define IDS_CMD_OPTIONS_DLG 119 24 | #define IDS_MOUSEBUTTON_NONE 120 25 | #define IDS_MOUSEBUTTON_LEFT 121 26 | #define IDS_MOUSEBUTTON_RIGHT 122 27 | #define IDS_MOUSEBUTTON_MIDDLE 123 28 | #define IDS_MOUSEBUTTON_X1 124 29 | #define IDS_MOUSEBUTTON_X2 125 30 | #define IDS_MODKEY_COND_NONE 126 31 | #define IDS_MODKEY_COND_ON 127 32 | #define IDS_MODKEY_COND_OFF 128 33 | #define IDS_MODKEY_FAKE_FLAT 129 34 | #define IDS_MODKEY_FAKE_ON 130 35 | #define IDS_MODKEY_FAKE_OFF 131 36 | #define IDS_MODKEY_FAKE_INV 132 37 | #define IDS_ZOOM_PIVOT_CENTER 133 38 | #define IDS_ZOOM_PIVOT_CURSOR 134 39 | #define IDS_ZOOM_PIVOT_START_PT 135 40 | #define IDS_RAILMODE_NONE 136 41 | #define IDS_RAILMODE_CROSS 137 42 | #define IDS_RAILMODE_OCTAGONAL 138 43 | #define IDS_TIPMODE_FRAIL 139 44 | #define IDS_TIPMODE_STATIONARY 140 45 | #define IDS_TIPMODE_STICKY 141 46 | #define IDS_TIP_COLOR_FMT_HEX 142 47 | #define IDS_TIP_COLOR_FMT_RGB 143 48 | #define IDS_CXT_COLOR_FMT_HEX 144 49 | #define IDS_CXT_COLOR_FMT_RGB 145 50 | #define IDS_COORD_FMT_TOPLEFT 146 51 | #define IDS_COORD_FMT_CENTER 147 52 | #define IDS_SCALE_FORMAT_FRAC 148 53 | #define IDS_SCALE_FORMAT_DEC 149 54 | #define IDS_SCALE_FORMAT_PCT 150 55 | #define IDS_DLG_TAB_ZOOM_WHEEL 151 56 | #define IDS_DLG_TAB_DRAG_LOUPE 152 57 | #define IDS_DLG_TAB_DRAG_TIP 153 58 | #define IDS_DLG_TAB_DRAG_ZOOM 154 59 | #define IDS_DLG_TAB_DRAG_EXEDIT 155 60 | #define IDS_DLG_TAB_CLK_LEFT 156 61 | #define IDS_DLG_TAB_CLK_RIGHT 157 62 | #define IDS_DLG_TAB_CLK_MIDDLE 158 63 | #define IDS_DLG_TAB_CLK_X1 159 64 | #define IDS_DLG_TAB_CLK_X2 160 65 | #define IDS_DLG_TAB_CMD_OPTIONS 161 66 | #define IDS_DLG_TAB_TOAST 162 67 | #define IDS_DLG_TAB_GRID 163 68 | #define IDS_DLG_TAB_COLOR 164 69 | #define IDS_DLG_TAB_SEPARATOR 165 70 | #define IDS_DESC_DRAG_KEYCOND 166 71 | #define IDS_DESC_DRAG_RANGE 167 72 | #define IDS_DESC_ZOOM_STEPS 168 73 | #define IDS_DESC_TIP_FRAIL 169 74 | #define IDS_DESC_TIP_STATIONARY 170 75 | #define IDS_DESC_TIP_STICKY 171 76 | #define IDS_DESC_DRAG_ZOOM 172 77 | #define IDS_DESC_EXEDIT_FAKE 173 78 | #define IDS_DESC_GRID_KINDS 174 79 | #define IDS_DESC_CLIPBOARD_FMT 175 80 | #define IDS_DESC_FONT_STYLE 176 81 | #define IDS_DESC_CMD_NONE 177 82 | #define IDS_DESC_CMD_SWAP_ZOOM 178 83 | #define IDS_DESC_CMD_COPY_COLOR 179 84 | #define IDS_DESC_CMD_COPY_COORD 180 85 | #define IDS_DESC_CMD_FOLLOW 181 86 | #define IDS_DESC_CMD_CENTRALIZE 182 87 | #define IDS_DESC_CMD_GRID 183 88 | #define IDS_DESC_CMD_ZOOM_UP 184 89 | #define IDS_DESC_CMD_ZOOM_DOWN 185 90 | #define IDS_DESC_CMD_BRING_CENTER 186 91 | #define IDS_DESC_CMD_CXT_MENU 187 92 | #define IDS_DESC_CMD_SETTINGS 188 93 | #define IDS_DLG_TOAST_SAMPLE 189 94 | #define IDS_COLOR_THEME_LIGHT 190 95 | #define IDS_COLOR_THEME_DARK 191 96 | #define IDD_VSCROLLFORM 800 97 | #define IDD_SETTINGS 801 98 | #define IDD_SETTINGS_FORM_CLICK_ACTION 802 99 | #define IDD_SETTINGS_FORM_DRAG_KEYS 803 100 | #define IDD_SETTINGS_FORM_DRAG_RANGE 804 101 | #define IDD_SETTINGS_FORM_WHEEL_ZOOM 805 102 | #define IDD_SETTINGS_FORM_DRAG_LOUPE 806 103 | #define IDD_SETTINGS_FORM_DRAG_TIP 807 104 | #define IDD_SETTINGS_FORM_DRAG_TIP_FMT 808 105 | #define IDD_SETTINGS_FORM_TIP_METRICS 809 106 | #define IDD_SETTINGS_FORM_DRAG_ZOOM 810 107 | #define IDD_SETTINGS_FORM_DRAG_EXEDIT 811 108 | #define IDD_SETTINGS_FORM_ZOOM 812 109 | #define IDD_SETTINGS_FORM_TOAST 813 110 | #define IDD_SETTINGS_FORM_TOAST_METRICS 814 111 | #define IDD_SETTINGS_FORM_GRID 815 112 | #define IDD_SETTINGS_FORM_CMD_SWAP 816 113 | #define IDD_SETTINGS_FORM_CMD_ZOOM_STEP 817 114 | #define IDD_SETTINGS_FORM_CMD_CLIPBOARD 818 115 | #define IDD_SETTINGS_FORM_CMD_DESC 819 116 | #define IDD_SETTINGS_FORM_FONT 820 117 | #define IDD_SETTINGS_FORM_COLOR 821 118 | #define IDC_LIST1 1001 119 | #define IDC_COMBO1 1002 120 | #define IDC_COMBO2 1003 121 | #define IDC_COMBO3 1004 122 | #define IDC_COMBO4 1005 123 | #define IDC_EDIT1 1006 124 | #define IDC_EDIT2 1007 125 | #define IDC_EDIT3 1008 126 | #define IDC_EDIT4 1009 127 | #define IDC_EDIT5 1010 128 | #define IDC_EDIT6 1011 129 | #define IDC_EDIT7 1012 130 | #define IDC_EDIT8 1013 131 | #define IDC_SPIN1 1014 132 | #define IDC_SPIN2 1015 133 | #define IDC_SPIN3 1016 134 | #define IDC_SPIN4 1017 135 | #define IDC_SPIN5 1018 136 | #define IDC_SPIN6 1019 137 | #define IDC_SPIN7 1020 138 | #define IDC_SPIN8 1021 139 | #define IDC_CHECK1 1022 140 | #define IDC_CHECK2 1023 141 | #define IDC_CHECK3 1024 142 | #define IDC_CHECK4 1025 143 | #define IDC_RADIO1 1026 144 | #define IDC_RADIO2 1027 145 | #define IDC_RADIO3 1028 146 | #define IDC_RADIO4 1029 147 | #define IDC_RADIO5 1030 148 | #define IDC_RADIO6 1031 149 | #define IDC_RADIO7 1032 150 | #define IDC_RADIO8 1033 151 | #define IDC_RADIO9 1034 152 | #define IDC_SLIDER1 1035 153 | #define IDC_SLIDER2 1036 154 | #define IDC_RECT1 1037 155 | #define IDC_BUTTON1 1038 156 | #define IDC_BUTTON2 1039 157 | #define IDC_BUTTON3 1040 158 | #define IDC_BUTTON4 1041 159 | #define IDC_BUTTON5 1042 160 | #define IDC_BUTTON6 1043 161 | #define IDM_CXT_FOLLOW_CURSOR 40001 162 | #define IDM_CXT_SHOW_GRID 40002 163 | #define IDM_CXT_SWAP_ZOOM 40003 164 | #define IDM_CXT_CENTRALIZE 40004 165 | #define IDM_CXT_PT_COPY_COLOR 40005 166 | #define IDM_CXT_PT_BRING_CENTER 40006 167 | #define IDM_CXT_PT_COPY_COORD 40007 168 | #define IDM_CXT_TIP_MODE_FRAIL 40008 169 | #define IDM_CXT_TIP_MODE_STATIONARY 40009 170 | #define IDM_CXT_TIP_MODE_STICKY 40010 171 | #define IDM_CXT_REVERSE_WHEEL 40011 172 | #define IDM_CXT_SETTINGS 40012 173 | 174 | // Next default values for new objects 175 | // 176 | #ifdef APSTUDIO_INVOKED 177 | #ifndef APSTUDIO_READONLY_SYMBOLS 178 | #define _APS_NEXT_RESOURCE_VALUE 822 179 | #define _APS_NEXT_COMMAND_VALUE 40013 180 | #define _APS_NEXT_CONTROL_VALUE 1044 181 | #define _APS_NEXT_SYMED_VALUE 101 182 | #endif 183 | #endif 184 | -------------------------------------------------------------------------------- /resource.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | #define NOMINMAX 18 | #define WIN32_LEAN_AND_MEAN 19 | #include 20 | 21 | #include "resource.h" 22 | inline HMODULE constinit this_dll = nullptr; 23 | 24 | namespace sigma_lib::W32::resources 25 | { 26 | namespace string 27 | { 28 | inline const wchar_t* get(uint32_t id) { 29 | const wchar_t* ret; 30 | ::LoadStringW(this_dll, id, reinterpret_cast(&ret), 0); 31 | return ret; 32 | } 33 | 34 | inline int copy(uint32_t id, wchar_t* buf, size_t len) { 35 | return ::LoadStringW(this_dll, id, buf, len); 36 | } 37 | template 38 | inline int copy(uint32_t id, wchar_t(&buf)[len]) { return copy(id, buf, len); } 39 | } 40 | namespace cursor 41 | { 42 | enum ID : uint32_t { 43 | arrow = reinterpret_cast(IDC_ARROW), 44 | ibeam = reinterpret_cast(IDC_IBEAM), 45 | wait = reinterpret_cast(IDC_WAIT), 46 | cross = reinterpret_cast(IDC_CROSS), 47 | uparrow = reinterpret_cast(IDC_UPARROW), 48 | size = reinterpret_cast(IDC_SIZE), 49 | icon = reinterpret_cast(IDC_ICON), 50 | sizenwse = reinterpret_cast(IDC_SIZENWSE), 51 | sizenesw = reinterpret_cast(IDC_SIZENESW), 52 | sizewe = reinterpret_cast(IDC_SIZEWE), 53 | sizens = reinterpret_cast(IDC_SIZENS), 54 | sizeall = reinterpret_cast(IDC_SIZEALL), 55 | no = reinterpret_cast(IDC_NO), 56 | hand = reinterpret_cast(IDC_HAND), 57 | appstarting = reinterpret_cast(IDC_APPSTARTING), 58 | help = reinterpret_cast(IDC_HELP), 59 | pin = reinterpret_cast(IDC_PIN), 60 | person = reinterpret_cast(IDC_PERSON), 61 | }; 62 | inline HCURSOR get(ID id) { 63 | return reinterpret_cast(::LoadImageW(nullptr, 64 | reinterpret_cast(id), IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED)); 65 | } 66 | inline HCURSOR load(uint32_t resource_id) { 67 | return reinterpret_cast(::LoadImageW(this_dll, 68 | MAKEINTRESOURCEW(resource_id), IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED)); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /settings.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 sigma-axis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | #define NOMINMAX 18 | #define WIN32_LEAN_AND_MEAN 19 | #include 20 | 21 | #include "key_states.hpp" 22 | 23 | #include "color_abgr.hpp" 24 | #include "drag_states.hpp" 25 | 26 | 27 | //////////////////////////////// 28 | // 設定項目. 29 | //////////////////////////////// 30 | inline constinit struct Settings { 31 | using Color = sigma_lib::W32::GDI::Color; 32 | using MouseButton = sigma_lib::W32::custom::mouse::MouseButton; 33 | using RailMode = sigma_lib::W32::custom::mouse::RailMode; 34 | using DragInvalidRange = sigma_lib::W32::custom::mouse::DragInvalidRange; 35 | 36 | enum class ColorFormat : uint8_t { 37 | hexdec6 = 0, dec3x3 = 1, 38 | }; 39 | enum class CoordFormat : uint8_t { 40 | origin_top_left = 0, origin_center = 1, 41 | }; 42 | struct KeysActivate { 43 | enum State : int8_t { 44 | off = 0, 45 | on = 1, 46 | dontcare = -1, 47 | }; 48 | MouseButton button; 49 | State ctrl, shift, alt; 50 | 51 | bool match(MouseButton button, bool ctrl, bool shift, bool alt) const { 52 | constexpr auto req = [](State r, bool k) { 53 | switch (r) { 54 | case off: return !k; 55 | case on: return k; 56 | case dontcare: 57 | default: 58 | return true; 59 | } 60 | }; 61 | return button != MouseButton::none 62 | && this->button == button 63 | && req(this->ctrl, ctrl) 64 | && req(this->shift, shift) 65 | && req(this->alt, alt); 66 | } 67 | }; 68 | 69 | struct WheelZoom { 70 | enum Pivot : uint8_t { center = 0, cursor = 1, }; 71 | 72 | bool enabled, reversed; 73 | uint8_t num_steps; 74 | Pivot pivot; 75 | 76 | constexpr static uint8_t num_steps_min = 1, num_steps_max = 16; 77 | }; 78 | 79 | struct LoupeDrag { 80 | KeysActivate keys{ MouseButton::left, KeysActivate::off, KeysActivate::dontcare, KeysActivate::dontcare }; 81 | DragInvalidRange range = DragInvalidRange::AlwaysValid(); 82 | WheelZoom wheel{ true, false, 1, WheelZoom::cursor }; 83 | 84 | bool lattice = false; 85 | RailMode rail_mode = RailMode::cross; 86 | } loupe_drag; 87 | 88 | struct TipDrag { 89 | KeysActivate keys{ MouseButton::right, KeysActivate::off, KeysActivate::dontcare, KeysActivate::dontcare }; 90 | DragInvalidRange range = DragInvalidRange::AlwaysValid(); 91 | WheelZoom wheel{ true, false, 1, WheelZoom::cursor }; 92 | 93 | enum Mode : uint8_t { 94 | frail = 0, stationary = 1, sticky = 2, 95 | }; 96 | Mode mode = frail; 97 | RailMode rail_mode = RailMode::cross; 98 | 99 | ColorFormat color_fmt = ColorFormat::hexdec6; 100 | CoordFormat coord_fmt = CoordFormat::origin_top_left; 101 | 102 | wchar_t font_name[LF_FACESIZE]{ L"Consolas" }; 103 | int8_t font_size = 16; 104 | 105 | int8_t box_inflate = 4; 106 | int8_t box_tip_gap = 10; 107 | int8_t chrome_thick = 1; 108 | int8_t chrome_corner = 8; // in diameter of the circle. 109 | int8_t chrome_margin_h = 4; 110 | int8_t chrome_margin_v = 4; 111 | int8_t chrome_pad_h = 10; 112 | int8_t chrome_pad_v = 3; 113 | 114 | constexpr static int8_t font_size_min = 4, font_size_max = 72; 115 | constexpr static int8_t box_inflate_min = 0, box_inflate_max = 16; 116 | constexpr static int8_t box_tip_gap_min = -64, box_tip_gap_max = 64; 117 | constexpr static int8_t chrome_thick_min = 0, chrome_thick_max = 16; 118 | constexpr static int8_t chrome_corner_min = 0, chrome_corner_max = 64; 119 | constexpr static int8_t chrome_margin_h_min = -16, chrome_margin_h_max = 100; 120 | constexpr static int8_t chrome_margin_v_min = -16, chrome_margin_v_max = 100; 121 | constexpr static int8_t chrome_pad_h_min = -16, chrome_pad_h_max = 32; 122 | constexpr static int8_t chrome_pad_v_min = -16, chrome_pad_v_max = 32; 123 | } tip_drag{}; 124 | 125 | struct ZoomDrag { 126 | using Pivot = WheelZoom::Pivot; 127 | 128 | KeysActivate keys{ MouseButton::none, KeysActivate::dontcare, KeysActivate::dontcare, KeysActivate::dontcare }; 129 | DragInvalidRange range = DragInvalidRange::AlwaysValid(); 130 | 131 | bool expand_up = true; 132 | uint8_t num_steps = 1; 133 | uint16_t len_per_step = 20; 134 | Pivot pivot = Pivot::center; 135 | 136 | constexpr static uint8_t 137 | num_steps_min = WheelZoom::num_steps_min, 138 | num_steps_max = WheelZoom::num_steps_max; 139 | constexpr static uint16_t len_per_step_min = 1, len_per_step_max = 1024; 140 | } zoom_drag{}; 141 | 142 | struct ExEditDrag { 143 | KeysActivate keys{ MouseButton::left, KeysActivate::on, KeysActivate::dontcare, KeysActivate::dontcare }; 144 | DragInvalidRange range = DragInvalidRange::AlwaysValid(); 145 | WheelZoom wheel{ true, false, 1, WheelZoom::cursor }; 146 | 147 | using flag_map = sigma_lib::W32::UI::flag_map; 148 | using enum flag_map; 149 | flag_map fake_shift = id, fake_alt = id; // no ctrl key; it's kind of special. 150 | } exedit_drag; 151 | 152 | struct Zoom { 153 | WheelZoom wheel{ 154 | .enabled = true, 155 | .reversed = false, 156 | .num_steps = 1, 157 | .pivot = WheelZoom::center, 158 | }; 159 | int8_t level_min = -13, 160 | level_max = 20; 161 | constexpr static int8_t level_min_min = -17, level_min_max = 24, 162 | level_max_min = level_min_min, level_max_max = level_min_max; 163 | } zoom; 164 | 165 | struct ColorScheme { 166 | Color 167 | // Light theme 168 | chrome = { 0x76,0x76,0x76 }, 169 | back_top = { 0xff,0xff,0xff }, 170 | back_bottom = { 0xe4,0xec,0xf7 }, 171 | text = { 0x3b,0x3d,0x3f }, 172 | blank = { 0xf0,0xf0,0xf0 }; 173 | 174 | constexpr static ColorScheme LightTheme() { return {}; } 175 | constexpr static ColorScheme DarkTheme() { 176 | return { 177 | // Dark theme 178 | .chrome = { 0x76,0x76,0x76 }, 179 | .back_top = { 0x2b,0x2b,0x2b }, 180 | .back_bottom= { 0x2b,0x2b,0x2b }, 181 | .text = { 0xff,0xff,0xff }, 182 | .blank = { 0x33,0x33,0x33 }, 183 | }; 184 | } 185 | } color; 186 | 187 | struct Toast { 188 | bool notify_scale = true; 189 | bool notify_follow_cursor = true; 190 | bool notify_grid = true; 191 | bool notify_clipboard = true; 192 | 193 | enum class Placement : uint8_t { 194 | top_left = 0, top = 1, top_right = 2, 195 | left = 3, center = 4, right = 5, 196 | bottom_left = 6, bottom = 7, bottom_right = 8, 197 | }; 198 | Placement placement = Placement::bottom_right; 199 | 200 | enum class ScaleFormat : uint8_t { 201 | fraction = 0, decimal = 1, percent = 2, 202 | }; 203 | ScaleFormat scale_format = ScaleFormat::decimal, 204 | scale_format_low = ScaleFormat::decimal; 205 | 206 | int duration = 3000; 207 | 208 | wchar_t font_name[LF_FACESIZE]{ L"Yu Gothic UI" }; 209 | int8_t font_size = 18; 210 | 211 | int8_t chrome_thick = 1; 212 | int8_t chrome_corner = 8; // in diameter of the circle. 213 | int8_t chrome_margin_h = 4; 214 | int8_t chrome_margin_v = 4; 215 | int8_t chrome_pad_h = 6; 216 | int8_t chrome_pad_v = 4; 217 | 218 | constexpr static int duration_min = 200, duration_max = 60'000; 219 | constexpr static int8_t font_size_min = 4, font_size_max = 72; 220 | constexpr static int8_t chrome_thick_min = 0, chrome_thick_max = 16; 221 | constexpr static int8_t chrome_corner_min = 0, chrome_corner_max = 32; 222 | constexpr static int8_t chrome_margin_h_min = -16, chrome_margin_h_max = 100; 223 | constexpr static int8_t chrome_margin_v_min = -16, chrome_margin_v_max = 100; 224 | constexpr static int8_t chrome_pad_h_min = -16, chrome_pad_h_max = 32; 225 | constexpr static int8_t chrome_pad_v_min = -16, chrome_pad_v_max = 32; 226 | } toast{}; 227 | 228 | struct Grid { 229 | int8_t least_zoom_thin = 8; 230 | int8_t least_zoom_thick = 12; 231 | constexpr static int8_t 232 | least_zoom_thin_min = 6, // x 3.00 233 | least_zoom_thin_max = Zoom::level_max_max + 1, 234 | least_zoom_thick_min = least_zoom_thin_min, 235 | least_zoom_thick_max = least_zoom_thin_max; 236 | 237 | // 0: no grid, 1: thin grid, 2: thick grid. 238 | uint8_t grid_thick(int zoom_level) const { 239 | if (zoom_level < least_zoom_thin) return 0; 240 | if (zoom_level < least_zoom_thick) return 1; 241 | return 2; 242 | } 243 | } grid; 244 | 245 | struct ClickActions { 246 | enum Command : uint8_t { 247 | none = 0, 248 | swap_zoom_level = 1, 249 | copy_color_code = 2, 250 | copy_coord = 3, 251 | toggle_follow_cursor = 4, 252 | centralize = 5, 253 | toggle_grid = 6, 254 | zoom_step_down = 7, 255 | zoom_step_up = 8, 256 | bring_center = 9, 257 | settings = 201, 258 | context_menu = 202, 259 | }; 260 | struct Button { 261 | Command click, dblclk; 262 | bool cancels_drag; 263 | } left{ none, swap_zoom_level, true }, 264 | right{ context_menu, copy_color_code, true }, 265 | middle{ toggle_follow_cursor, none, true }, 266 | x1{ none, none, true }, 267 | x2{ none,none, true }; 268 | 269 | WheelZoom::Pivot swap_zoom_level_pivot = WheelZoom::cursor; 270 | 271 | WheelZoom::Pivot step_zoom_pivot = WheelZoom::cursor; 272 | uint8_t step_zoom_num_steps = 1; 273 | constexpr static uint8_t 274 | step_zoom_num_steps_min = WheelZoom::num_steps_min, 275 | step_zoom_num_steps_max = WheelZoom::num_steps_max; 276 | 277 | ColorFormat copy_color_fmt = ColorFormat::hexdec6; 278 | CoordFormat copy_coord_fmt = CoordFormat::origin_top_left; 279 | } commands; 280 | 281 | // loading from .ini file. 282 | void load(const char* ini_file) 283 | { 284 | auto read_raw = [&](auto def, const char* section, const char* key) { 285 | return static_cast( 286 | ::GetPrivateProfileIntA(section, key, static_cast(def), ini_file)); 287 | }; 288 | #define load_gen(section, tgt, read, write) section.tgt = read(read_raw(write(section.tgt), #section, #tgt)) 289 | #define load_int(section, tgt) \ 290 | load_gen(section, tgt, [&](auto y) { return std::clamp(y, section.tgt##_min, section.tgt##_max); }, /*id*/) 291 | #define load_bool(section, tgt) \ 292 | load_gen(section, tgt, [](auto y) { return y != 0; }, [](auto x) { return x ? 1 : 0; }) 293 | #define load_enum(section, tgt) \ 294 | load_gen(section, tgt, [](auto y) { return static_cast(y); }, [](auto x) { return static_cast(x); }) 295 | #define load_color(section, tgt) \ 296 | load_gen(section, tgt, [](auto y) { return Color::fromARGB(y); }, [](auto x) { return x.to_formattable(); }) 297 | #define load_zoom(section, header) \ 298 | load_bool(section, header##enabled);\ 299 | load_bool(section, header##reversed);\ 300 | load_int(section, header##num_steps);\ 301 | load_enum(section, header##pivot) 302 | #define load_drag(section) \ 303 | load_enum(section, keys.button);\ 304 | load_enum(section, keys.ctrl);\ 305 | load_enum(section, keys.shift);\ 306 | load_enum(section, keys.alt);\ 307 | load_int(section, range.distance); \ 308 | load_int(section, range.timespan) 309 | 310 | load_drag(loupe_drag); 311 | load_zoom(loupe_drag, wheel.); 312 | load_bool(loupe_drag, lattice); 313 | load_enum(loupe_drag, rail_mode); 314 | 315 | load_drag(tip_drag); 316 | load_zoom(tip_drag, wheel.); 317 | load_enum(tip_drag, mode); 318 | load_enum(tip_drag, rail_mode); 319 | load_enum(tip_drag, color_fmt); 320 | load_enum(tip_drag, coord_fmt); 321 | load_int(tip_drag, font_size); 322 | load_int(tip_drag, box_inflate); 323 | load_int(tip_drag, box_tip_gap); 324 | load_int(tip_drag, chrome_thick); 325 | load_int(tip_drag, chrome_corner); 326 | load_int(tip_drag, chrome_margin_h); 327 | load_int(tip_drag, chrome_margin_v); 328 | load_int(tip_drag, chrome_pad_h); 329 | load_int(tip_drag, chrome_pad_v); 330 | 331 | load_drag(zoom_drag); 332 | load_bool(zoom_drag, expand_up); 333 | load_int(zoom_drag, num_steps); 334 | load_int(zoom_drag, len_per_step); 335 | load_enum(zoom_drag, pivot); 336 | 337 | load_drag(exedit_drag); 338 | load_zoom(exedit_drag, wheel.); 339 | load_enum(exedit_drag, fake_shift); 340 | load_enum(exedit_drag, fake_alt); 341 | 342 | load_zoom(zoom, wheel.); 343 | load_int(zoom, level_min); 344 | load_int(zoom, level_max); 345 | 346 | load_color(color, chrome); 347 | load_color(color, back_top); 348 | load_color(color, back_bottom); 349 | load_color(color, text); 350 | load_color(color, blank); 351 | 352 | load_bool(toast, notify_scale); 353 | load_bool(toast, notify_follow_cursor); 354 | load_bool(toast, notify_grid); 355 | load_bool(toast, notify_clipboard); 356 | load_enum(toast, placement); 357 | load_enum(toast, scale_format); 358 | toast.scale_format_low = toast.scale_format; // for versioning. 359 | load_enum(toast, scale_format_low); 360 | load_int(toast, duration); 361 | load_int(toast, font_size); 362 | load_int(toast, chrome_thick); 363 | load_int(toast, chrome_corner); 364 | load_int(toast, chrome_margin_h); 365 | load_int(toast, chrome_margin_v); 366 | load_int(toast, chrome_pad_h); 367 | load_int(toast, chrome_pad_v); 368 | 369 | { 370 | char buf_ansi[3 * std::extent_v]; 371 | if (::GetPrivateProfileStringA("tip_drag", "font_name", "", buf_ansi, std::size(buf_ansi), ini_file) > 0) 372 | ::MultiByteToWideChar(CP_UTF8, 0, buf_ansi, -1, tip_drag.font_name, std::size(tip_drag.font_name)); 373 | 374 | if (::GetPrivateProfileStringA("toast", "font_name", "", buf_ansi, std::size(buf_ansi), ini_file) > 0) 375 | ::MultiByteToWideChar(CP_UTF8, 0, buf_ansi, -1, toast.font_name, std::size(toast.font_name)); 376 | } 377 | 378 | load_int(grid, least_zoom_thin); 379 | load_int(grid, least_zoom_thick); 380 | 381 | load_enum(commands, left.click); 382 | load_enum(commands, left.dblclk); 383 | load_bool(commands, left.cancels_drag); 384 | load_enum(commands, right.click); 385 | load_enum(commands, right.dblclk); 386 | load_bool(commands, right.cancels_drag); 387 | load_enum(commands, middle.click); 388 | load_enum(commands, middle.dblclk); 389 | load_bool(commands, middle.cancels_drag); 390 | load_enum(commands, x1.click); 391 | load_enum(commands, x1.dblclk); 392 | load_bool(commands, x1.cancels_drag); 393 | load_enum(commands, x2.click); 394 | load_enum(commands, x2.dblclk); 395 | load_bool(commands, x2.cancels_drag); 396 | 397 | load_enum(commands, swap_zoom_level_pivot); 398 | load_enum(commands, step_zoom_pivot); 399 | load_int(commands, step_zoom_num_steps); 400 | load_enum(commands, copy_color_fmt); 401 | load_enum(commands, copy_coord_fmt); 402 | 403 | #undef load_drag 404 | #undef load_zoom 405 | #undef load_color 406 | #undef load_enum 407 | #undef load_bool 408 | #undef load_int 409 | #undef load_gen 410 | } 411 | 412 | // saving to .ini file. 413 | void save(const char* ini_file) 414 | { 415 | auto save_raw = [&](int32_t val, const char* section, const char* key, bool col = false) { 416 | char buf[std::size("+4294967296")]; 417 | std::snprintf(buf, std::size(buf), col ? "0x%06x" : "%d", val); 418 | ::WritePrivateProfileStringA(section, key, buf, ini_file); 419 | }; 420 | 421 | #define save_gen(section, tgt, write, col) save_raw(static_cast(write(section.tgt)), #section, #tgt, col) 422 | #define save_dec(section, tgt) save_gen(section, tgt, /* id */, false) 423 | #define save_color(section, tgt) save_gen(section, tgt, [](auto y) { return y.to_formattable(); }, true) 424 | #define save_bool(section, tgt) ::WritePrivateProfileStringA(#section, #tgt, section.tgt ? "1" : "0", ini_file) 425 | #define save_zoom(section, header) \ 426 | save_bool(section, header##enabled);\ 427 | save_bool(section, header##reversed);\ 428 | save_dec(section, header##num_steps);\ 429 | save_dec(section, header##pivot) 430 | #define save_drag(section) \ 431 | save_dec(section, keys.button);\ 432 | save_dec(section, keys.ctrl);\ 433 | save_dec(section, keys.shift);\ 434 | save_dec(section, keys.alt);\ 435 | save_dec(section, range.distance); \ 436 | save_dec(section, range.timespan) 437 | 438 | save_drag(loupe_drag); 439 | save_zoom(loupe_drag, wheel.); 440 | save_dec(loupe_drag, lattice); 441 | save_dec(loupe_drag, rail_mode); 442 | 443 | save_drag(tip_drag); 444 | save_zoom(tip_drag, wheel.); 445 | save_dec(tip_drag, mode); 446 | save_dec(tip_drag, rail_mode); 447 | save_dec(tip_drag, color_fmt); 448 | save_dec(tip_drag, coord_fmt); 449 | save_dec(tip_drag, font_size); 450 | save_dec(tip_drag, box_inflate); 451 | save_dec(tip_drag, box_tip_gap); 452 | save_dec(tip_drag, chrome_thick); 453 | save_dec(tip_drag, chrome_corner); 454 | save_dec(tip_drag, chrome_margin_h); 455 | save_dec(tip_drag, chrome_margin_v); 456 | save_dec(tip_drag, chrome_pad_h); 457 | save_dec(tip_drag, chrome_pad_v); 458 | 459 | save_drag(zoom_drag); 460 | save_bool(zoom_drag, expand_up); 461 | save_dec(zoom_drag, num_steps); 462 | save_dec(zoom_drag, len_per_step); 463 | save_dec(zoom_drag, pivot); 464 | 465 | save_drag(exedit_drag); 466 | save_zoom(exedit_drag, wheel.); 467 | save_dec(exedit_drag, fake_shift); 468 | save_dec(exedit_drag, fake_alt); 469 | 470 | save_zoom(zoom, wheel.); 471 | save_dec(zoom, level_min); 472 | save_dec(zoom, level_max); 473 | 474 | save_color(color, chrome); 475 | save_color(color, back_top); 476 | save_color(color, back_bottom); 477 | save_color(color, text); 478 | save_color(color, blank); 479 | 480 | save_bool(toast, notify_scale); 481 | save_bool(toast, notify_follow_cursor); 482 | save_bool(toast, notify_grid); 483 | save_bool(toast, notify_clipboard); 484 | save_dec(toast, placement); 485 | save_dec(toast, scale_format); 486 | save_dec(toast, scale_format_low); 487 | save_dec(toast, duration); 488 | save_dec(toast, font_size); 489 | save_dec(toast, chrome_thick); 490 | save_dec(toast, chrome_corner); 491 | save_dec(toast, chrome_margin_h); 492 | save_dec(toast, chrome_margin_v); 493 | save_dec(toast, chrome_pad_h); 494 | save_dec(toast, chrome_pad_v); 495 | 496 | { 497 | char buf_ansi[3 * std::extent_v]; 498 | ::WideCharToMultiByte(CP_UTF8, 0, tip_drag.font_name, -1, buf_ansi, std::size(buf_ansi), nullptr, nullptr); 499 | ::WritePrivateProfileStringA("tip_drag", "font_name", buf_ansi, ini_file); 500 | 501 | ::WideCharToMultiByte(CP_UTF8, 0, toast.font_name, -1, buf_ansi, std::size(buf_ansi), nullptr, nullptr); 502 | ::WritePrivateProfileStringA("toast", "font_name", buf_ansi, ini_file); 503 | } 504 | 505 | save_dec(grid, least_zoom_thin); 506 | save_dec(grid, least_zoom_thick); 507 | 508 | save_dec(commands, left.click); 509 | save_dec(commands, left.dblclk); 510 | save_bool(commands, left.cancels_drag); 511 | save_dec(commands, right.click); 512 | save_dec(commands, right.dblclk); 513 | save_bool(commands, right.cancels_drag); 514 | save_dec(commands, middle.click); 515 | save_dec(commands, middle.dblclk); 516 | save_bool(commands, middle.cancels_drag); 517 | save_dec(commands, x1.click); 518 | save_dec(commands, x1.dblclk); 519 | save_bool(commands, x1.cancels_drag); 520 | save_dec(commands, x2.click); 521 | save_dec(commands, x2.dblclk); 522 | save_bool(commands, x2.cancels_drag); 523 | 524 | save_dec(commands, swap_zoom_level_pivot); 525 | save_dec(commands, step_zoom_pivot); 526 | save_dec(commands, step_zoom_num_steps); 527 | save_dec(commands, copy_color_fmt); 528 | save_dec(commands, copy_coord_fmt); 529 | 530 | // lines commented out are setting items that threre're no means to change at runtime. (all cleared now.) 531 | 532 | #undef save_drag 533 | #undef save_zoom 534 | #undef save_bool 535 | #undef save_color 536 | #undef save_dec 537 | #undef save_gen 538 | } 539 | } settings; 540 | --------------------------------------------------------------------------------